From 963f4309fe29206f3ba92b493e922280feea30ed Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 30 Mar 2021 12:06:09 +0100 Subject: [PATCH 001/619] Make RateLimiter class check for ratelimit overrides (#9711) This should fix a class of bug where we forget to check if e.g. the appservice shouldn't be ratelimited. We also check the `ratelimit_override` table to check if the user has ratelimiting disabled. That table is really only meant to override the event sender ratelimiting, so we don't use any values from it (as they might not make sense for different rate limits), but we do infer that if ratelimiting is disabled for the user we should disabled all ratelimits. Fixes #9663 --- changelog.d/9711.bugfix | 1 + synapse/api/ratelimiting.py | 100 ++++++++------ synapse/federation/federation_server.py | 5 +- synapse/handlers/_base.py | 14 +- synapse/handlers/auth.py | 24 ++-- synapse/handlers/devicemessage.py | 5 +- synapse/handlers/federation.py | 2 +- synapse/handlers/identity.py | 12 +- synapse/handlers/register.py | 6 +- synapse/handlers/room_member.py | 23 ++-- synapse/replication/http/register.py | 2 +- synapse/rest/client/v1/login.py | 14 +- synapse/rest/client/v2_alpha/account.py | 10 +- synapse/rest/client/v2_alpha/register.py | 8 +- synapse/server.py | 1 + tests/api/test_ratelimiting.py | 168 +++++++++++++++-------- 16 files changed, 241 insertions(+), 154 deletions(-) create mode 100644 changelog.d/9711.bugfix diff --git a/changelog.d/9711.bugfix b/changelog.d/9711.bugfix new file mode 100644 index 0000000000..4ca3438d46 --- /dev/null +++ b/changelog.d/9711.bugfix @@ -0,0 +1 @@ +Fix recently added ratelimits to correctly honour the application service `rate_limited` flag. diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py index c3f07bc1a3..2244b8a340 100644 --- a/synapse/api/ratelimiting.py +++ b/synapse/api/ratelimiting.py @@ -17,6 +17,7 @@ from typing import Hashable, Optional, Tuple from synapse.api.errors import LimitExceededError +from synapse.storage.databases.main import DataStore from synapse.types import Requester from synapse.util import Clock @@ -31,10 +32,13 @@ class Ratelimiter: burst_count: How many actions that can be performed before being limited. """ - def __init__(self, clock: Clock, rate_hz: float, burst_count: int): + def __init__( + self, store: DataStore, clock: Clock, rate_hz: float, burst_count: int + ): self.clock = clock self.rate_hz = rate_hz self.burst_count = burst_count + self.store = store # A ordered dictionary keeping track of actions, when they were last # performed and how often. Each entry is a mapping from a key of arbitrary type @@ -46,45 +50,10 @@ def __init__(self, clock: Clock, rate_hz: float, burst_count: int): OrderedDict() ) # type: OrderedDict[Hashable, Tuple[float, int, float]] - def can_requester_do_action( - self, - requester: Requester, - rate_hz: Optional[float] = None, - burst_count: Optional[int] = None, - update: bool = True, - _time_now_s: Optional[int] = None, - ) -> Tuple[bool, float]: - """Can the requester perform the action? - - Args: - requester: The requester to key off when rate limiting. The user property - will be used. - rate_hz: The long term number of actions that can be performed in a second. - Overrides the value set during instantiation if set. - burst_count: How many actions that can be performed before being limited. - Overrides the value set during instantiation if set. - update: Whether to count this check as performing the action - _time_now_s: The current time. Optional, defaults to the current time according - to self.clock. Only used by tests. - - Returns: - A tuple containing: - * A bool indicating if they can perform the action now - * The reactor timestamp for when the action can be performed next. - -1 if rate_hz is less than or equal to zero - """ - # Disable rate limiting of users belonging to any AS that is configured - # not to be rate limited in its registration file (rate_limited: true|false). - if requester.app_service and not requester.app_service.is_rate_limited(): - return True, -1.0 - - return self.can_do_action( - requester.user.to_string(), rate_hz, burst_count, update, _time_now_s - ) - - def can_do_action( + async def can_do_action( self, - key: Hashable, + requester: Optional[Requester], + key: Optional[Hashable] = None, rate_hz: Optional[float] = None, burst_count: Optional[int] = None, update: bool = True, @@ -92,9 +61,16 @@ def can_do_action( ) -> Tuple[bool, float]: """Can the entity (e.g. user or IP address) perform the action? + Checks if the user has ratelimiting disabled in the database by looking + for null/zero values in the `ratelimit_override` table. (Non-zero + values aren't honoured, as they're specific to the event sending + ratelimiter, rather than all ratelimiters) + Args: - key: The key we should use when rate limiting. Can be a user ID - (when sending events), an IP address, etc. + requester: The requester that is doing the action, if any. Used to check + if the user has ratelimits disabled in the database. + key: An arbitrary key used to classify an action. Defaults to the + requester's user ID. rate_hz: The long term number of actions that can be performed in a second. Overrides the value set during instantiation if set. burst_count: How many actions that can be performed before being limited. @@ -109,6 +85,30 @@ def can_do_action( * The reactor timestamp for when the action can be performed next. -1 if rate_hz is less than or equal to zero """ + if key is None: + if not requester: + raise ValueError("Must supply at least one of `requester` or `key`") + + key = requester.user.to_string() + + if requester: + # Disable rate limiting of users belonging to any AS that is configured + # not to be rate limited in its registration file (rate_limited: true|false). + if requester.app_service and not requester.app_service.is_rate_limited(): + return True, -1.0 + + # Check if ratelimiting has been disabled for the user. + # + # Note that we don't use the returned rate/burst count, as the table + # is specifically for the event sending ratelimiter. Instead, we + # only use it to (somewhat cheekily) infer whether the user should + # be subject to any rate limiting or not. + override = await self.store.get_ratelimit_for_user( + requester.authenticated_entity + ) + if override and not override.messages_per_second: + return True, -1.0 + # Override default values if set time_now_s = _time_now_s if _time_now_s is not None else self.clock.time() rate_hz = rate_hz if rate_hz is not None else self.rate_hz @@ -175,9 +175,10 @@ def _prune_message_counts(self, time_now_s: int): else: del self.actions[key] - def ratelimit( + async def ratelimit( self, - key: Hashable, + requester: Optional[Requester], + key: Optional[Hashable] = None, rate_hz: Optional[float] = None, burst_count: Optional[int] = None, update: bool = True, @@ -185,8 +186,16 @@ def ratelimit( ): """Checks if an action can be performed. If not, raises a LimitExceededError + Checks if the user has ratelimiting disabled in the database by looking + for null/zero values in the `ratelimit_override` table. (Non-zero + values aren't honoured, as they're specific to the event sending + ratelimiter, rather than all ratelimiters) + Args: - key: An arbitrary key used to classify an action + requester: The requester that is doing the action, if any. Used to check for + if the user has ratelimits disabled. + key: An arbitrary key used to classify an action. Defaults to the + requester's user ID. rate_hz: The long term number of actions that can be performed in a second. Overrides the value set during instantiation if set. burst_count: How many actions that can be performed before being limited. @@ -201,7 +210,8 @@ def ratelimit( """ time_now_s = _time_now_s if _time_now_s is not None else self.clock.time() - allowed, time_allowed = self.can_do_action( + allowed, time_allowed = await self.can_do_action( + requester, key, rate_hz=rate_hz, burst_count=burst_count, diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d84e362070..71cb120ef7 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -870,6 +870,7 @@ def __init__(self, hs: "HomeServer"): # A rate limiter for incoming room key requests per origin. self._room_key_request_rate_limiter = Ratelimiter( + store=hs.get_datastore(), clock=self.clock, rate_hz=self.config.rc_key_requests.per_second, burst_count=self.config.rc_key_requests.burst_count, @@ -930,7 +931,9 @@ async def on_edu(self, edu_type: str, origin: str, content: dict): # the limit, drop them. if ( edu_type == EduTypes.RoomKeyRequest - and not self._room_key_request_rate_limiter.can_do_action(origin) + and not await self._room_key_request_rate_limiter.can_do_action( + None, origin + ) ): return diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index aade2c4a3a..fb899aa90d 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -49,7 +49,7 @@ def __init__(self, hs: "HomeServer"): # The rate_hz and burst_count are overridden on a per-user basis self.request_ratelimiter = Ratelimiter( - clock=self.clock, rate_hz=0, burst_count=0 + store=self.store, clock=self.clock, rate_hz=0, burst_count=0 ) self._rc_message = self.hs.config.rc_message @@ -57,6 +57,7 @@ def __init__(self, hs: "HomeServer"): # by the presence of rate limits in the config if self.hs.config.rc_admin_redaction: self.admin_redaction_ratelimiter = Ratelimiter( + store=self.store, clock=self.clock, rate_hz=self.hs.config.rc_admin_redaction.per_second, burst_count=self.hs.config.rc_admin_redaction.burst_count, @@ -91,11 +92,6 @@ async def ratelimit(self, requester, update=True, is_admin_redaction=False): if app_service is not None: return # do not ratelimit app service senders - # Disable rate limiting of users belonging to any AS that is configured - # not to be rate limited in its registration file (rate_limited: true|false). - if requester.app_service and not requester.app_service.is_rate_limited(): - return - messages_per_second = self._rc_message.per_second burst_count = self._rc_message.burst_count @@ -113,11 +109,11 @@ async def ratelimit(self, requester, update=True, is_admin_redaction=False): if is_admin_redaction and self.admin_redaction_ratelimiter: # If we have separate config for admin redactions, use a separate # ratelimiter as to not have user_ids clash - self.admin_redaction_ratelimiter.ratelimit(user_id, update=update) + await self.admin_redaction_ratelimiter.ratelimit(requester, update=update) else: # Override rate and burst count per-user - self.request_ratelimiter.ratelimit( - user_id, + await self.request_ratelimiter.ratelimit( + requester, rate_hz=messages_per_second, burst_count=burst_count, update=update, diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index d537ea8137..08e413bc98 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -238,6 +238,7 @@ def __init__(self, hs: "HomeServer"): # Ratelimiter for failed auth during UIA. Uses same ratelimit config # as per `rc_login.failed_attempts`. self._failed_uia_attempts_ratelimiter = Ratelimiter( + store=self.store, clock=self.clock, rate_hz=self.hs.config.rc_login_failed_attempts.per_second, burst_count=self.hs.config.rc_login_failed_attempts.burst_count, @@ -248,6 +249,7 @@ def __init__(self, hs: "HomeServer"): # Ratelimitier for failed /login attempts self._failed_login_attempts_ratelimiter = Ratelimiter( + store=self.store, clock=hs.get_clock(), rate_hz=self.hs.config.rc_login_failed_attempts.per_second, burst_count=self.hs.config.rc_login_failed_attempts.burst_count, @@ -352,7 +354,7 @@ async def validate_user_via_ui_auth( requester_user_id = requester.user.to_string() # Check if we should be ratelimited due to too many previous failed attempts - self._failed_uia_attempts_ratelimiter.ratelimit(requester_user_id, update=False) + await self._failed_uia_attempts_ratelimiter.ratelimit(requester, update=False) # build a list of supported flows supported_ui_auth_types = await self._get_available_ui_auth_types( @@ -373,7 +375,9 @@ def get_new_session_data() -> JsonDict: ) except LoginError: # Update the ratelimiter to say we failed (`can_do_action` doesn't raise). - self._failed_uia_attempts_ratelimiter.can_do_action(requester_user_id) + await self._failed_uia_attempts_ratelimiter.can_do_action( + requester, + ) raise # find the completed login type @@ -982,8 +986,8 @@ async def validate_login( # We also apply account rate limiting using the 3PID as a key, as # otherwise using 3PID bypasses the ratelimiting based on user ID. if ratelimit: - self._failed_login_attempts_ratelimiter.ratelimit( - (medium, address), update=False + await self._failed_login_attempts_ratelimiter.ratelimit( + None, (medium, address), update=False ) # Check for login providers that support 3pid login types @@ -1016,8 +1020,8 @@ async def validate_login( # this code path, which is fine as then the per-user ratelimit # will kick in below. if ratelimit: - self._failed_login_attempts_ratelimiter.can_do_action( - (medium, address) + await self._failed_login_attempts_ratelimiter.can_do_action( + None, (medium, address) ) raise LoginError(403, "", errcode=Codes.FORBIDDEN) @@ -1039,8 +1043,8 @@ async def validate_login( # Check if we've hit the failed ratelimit (but don't update it) if ratelimit: - self._failed_login_attempts_ratelimiter.ratelimit( - qualified_user_id.lower(), update=False + await self._failed_login_attempts_ratelimiter.ratelimit( + None, qualified_user_id.lower(), update=False ) try: @@ -1051,8 +1055,8 @@ async def validate_login( # exception and masking the LoginError. The actual ratelimiting # should have happened above. if ratelimit: - self._failed_login_attempts_ratelimiter.can_do_action( - qualified_user_id.lower() + await self._failed_login_attempts_ratelimiter.can_do_action( + None, qualified_user_id.lower() ) raise diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index eb547743be..5ee48be6ff 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -81,6 +81,7 @@ def __init__(self, hs: "HomeServer"): ) self._ratelimiter = Ratelimiter( + store=self.store, clock=hs.get_clock(), rate_hz=hs.config.rc_key_requests.per_second, burst_count=hs.config.rc_key_requests.burst_count, @@ -191,8 +192,8 @@ async def send_device_message( if ( message_type == EduTypes.RoomKeyRequest and user_id != sender_user_id - and self._ratelimiter.can_do_action( - (sender_user_id, requester.device_id) + and await self._ratelimiter.can_do_action( + requester, (sender_user_id, requester.device_id) ) ): continue diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 598a66f74c..3ebee38ebe 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1711,7 +1711,7 @@ async def on_invite_request( member_handler = self.hs.get_room_member_handler() # We don't rate limit based on room ID, as that should be done by # sending server. - member_handler.ratelimit_invite(None, event.state_key) + await member_handler.ratelimit_invite(None, None, event.state_key) # keep a record of the room version, if we don't yet know it. # (this may get overwritten if we later get a different room version in a diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 5f346f6d6d..d89fa5fb30 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -61,17 +61,19 @@ def __init__(self, hs): # Ratelimiters for `/requestToken` endpoints. self._3pid_validation_ratelimiter_ip = Ratelimiter( + store=self.store, clock=hs.get_clock(), rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second, burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count, ) self._3pid_validation_ratelimiter_address = Ratelimiter( + store=self.store, clock=hs.get_clock(), rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second, burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count, ) - def ratelimit_request_token_requests( + async def ratelimit_request_token_requests( self, request: SynapseRequest, medium: str, @@ -85,8 +87,12 @@ def ratelimit_request_token_requests( address: The actual threepid ID, e.g. the phone number or email address """ - self._3pid_validation_ratelimiter_ip.ratelimit((medium, request.getClientIP())) - self._3pid_validation_ratelimiter_address.ratelimit((medium, address)) + await self._3pid_validation_ratelimiter_ip.ratelimit( + None, (medium, request.getClientIP()) + ) + await self._3pid_validation_ratelimiter_address.ratelimit( + None, (medium, address) + ) async def threepid_from_creds( self, id_server: str, creds: Dict[str, str] diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 0fc2bf15d5..9701b76d0f 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -204,7 +204,7 @@ async def register_user( Raises: SynapseError if there was a problem registering. """ - self.check_registration_ratelimit(address) + await self.check_registration_ratelimit(address) result = await self.spam_checker.check_registration_for_spam( threepid, @@ -583,7 +583,7 @@ def check_user_id_not_appservice_exclusive( errcode=Codes.EXCLUSIVE, ) - def check_registration_ratelimit(self, address: Optional[str]) -> None: + async def check_registration_ratelimit(self, address: Optional[str]) -> None: """A simple helper method to check whether the registration rate limit has been hit for a given IP address @@ -597,7 +597,7 @@ def check_registration_ratelimit(self, address: Optional[str]) -> None: if not address: return - self.ratelimiter.ratelimit(address) + await self.ratelimiter.ratelimit(None, address) async def register_with_store( self, diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 4d20ed8357..1cf12f3255 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -75,22 +75,26 @@ def __init__(self, hs: "HomeServer"): self.allow_per_room_profiles = self.config.allow_per_room_profiles self._join_rate_limiter_local = Ratelimiter( + store=self.store, clock=self.clock, rate_hz=hs.config.ratelimiting.rc_joins_local.per_second, burst_count=hs.config.ratelimiting.rc_joins_local.burst_count, ) self._join_rate_limiter_remote = Ratelimiter( + store=self.store, clock=self.clock, rate_hz=hs.config.ratelimiting.rc_joins_remote.per_second, burst_count=hs.config.ratelimiting.rc_joins_remote.burst_count, ) self._invites_per_room_limiter = Ratelimiter( + store=self.store, clock=self.clock, rate_hz=hs.config.ratelimiting.rc_invites_per_room.per_second, burst_count=hs.config.ratelimiting.rc_invites_per_room.burst_count, ) self._invites_per_user_limiter = Ratelimiter( + store=self.store, clock=self.clock, rate_hz=hs.config.ratelimiting.rc_invites_per_user.per_second, burst_count=hs.config.ratelimiting.rc_invites_per_user.burst_count, @@ -159,15 +163,20 @@ async def _user_left_room(self, target: UserID, room_id: str) -> None: async def forget(self, user: UserID, room_id: str) -> None: raise NotImplementedError() - def ratelimit_invite(self, room_id: Optional[str], invitee_user_id: str): + async def ratelimit_invite( + self, + requester: Optional[Requester], + room_id: Optional[str], + invitee_user_id: str, + ): """Ratelimit invites by room and by target user. If room ID is missing then we just rate limit by target user. """ if room_id: - self._invites_per_room_limiter.ratelimit(room_id) + await self._invites_per_room_limiter.ratelimit(requester, room_id) - self._invites_per_user_limiter.ratelimit(invitee_user_id) + await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id) async def _local_membership_update( self, @@ -237,7 +246,7 @@ async def _local_membership_update( ( allowed, time_allowed, - ) = self._join_rate_limiter_local.can_requester_do_action(requester) + ) = await self._join_rate_limiter_local.can_do_action(requester) if not allowed: raise LimitExceededError( @@ -421,9 +430,7 @@ async def update_membership_locked( if effective_membership_state == Membership.INVITE: target_id = target.to_string() if ratelimit: - # Don't ratelimit application services. - if not requester.app_service or requester.app_service.is_rate_limited(): - self.ratelimit_invite(room_id, target_id) + await self.ratelimit_invite(requester, room_id, target_id) # block any attempts to invite the server notices mxid if target_id == self._server_notices_mxid: @@ -534,7 +541,7 @@ async def update_membership_locked( ( allowed, time_allowed, - ) = self._join_rate_limiter_remote.can_requester_do_action( + ) = await self._join_rate_limiter_remote.can_do_action( requester, ) diff --git a/synapse/replication/http/register.py b/synapse/replication/http/register.py index d005f38767..73d7477854 100644 --- a/synapse/replication/http/register.py +++ b/synapse/replication/http/register.py @@ -77,7 +77,7 @@ async def _serialize_payload( async def _handle_request(self, request, user_id): content = parse_json_object_from_request(request) - self.registration_handler.check_registration_ratelimit(content["address"]) + await self.registration_handler.check_registration_ratelimit(content["address"]) await self.registration_handler.register_with_store( user_id=user_id, diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index e4c352f572..3151e72d4f 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -74,11 +74,13 @@ def __init__(self, hs: "HomeServer"): self._well_known_builder = WellKnownBuilder(hs) self._address_ratelimiter = Ratelimiter( + store=hs.get_datastore(), clock=hs.get_clock(), rate_hz=self.hs.config.rc_login_address.per_second, burst_count=self.hs.config.rc_login_address.burst_count, ) self._account_ratelimiter = Ratelimiter( + store=hs.get_datastore(), clock=hs.get_clock(), rate_hz=self.hs.config.rc_login_account.per_second, burst_count=self.hs.config.rc_login_account.burst_count, @@ -141,20 +143,22 @@ async def on_POST(self, request: SynapseRequest): appservice = self.auth.get_appservice_by_req(request) if appservice.is_rate_limited(): - self._address_ratelimiter.ratelimit(request.getClientIP()) + await self._address_ratelimiter.ratelimit( + None, request.getClientIP() + ) result = await self._do_appservice_login(login_submission, appservice) elif self.jwt_enabled and ( login_submission["type"] == LoginRestServlet.JWT_TYPE or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED ): - self._address_ratelimiter.ratelimit(request.getClientIP()) + await self._address_ratelimiter.ratelimit(None, request.getClientIP()) result = await self._do_jwt_login(login_submission) elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE: - self._address_ratelimiter.ratelimit(request.getClientIP()) + await self._address_ratelimiter.ratelimit(None, request.getClientIP()) result = await self._do_token_login(login_submission) else: - self._address_ratelimiter.ratelimit(request.getClientIP()) + await self._address_ratelimiter.ratelimit(None, request.getClientIP()) result = await self._do_other_login(login_submission) except KeyError: raise SynapseError(400, "Missing JSON keys.") @@ -258,7 +262,7 @@ async def _complete_login( # too often. This happens here rather than before as we don't # necessarily know the user before now. if ratelimit: - self._account_ratelimiter.ratelimit(user_id.lower()) + await self._account_ratelimiter.ratelimit(None, user_id.lower()) if create_non_existent_users: canonical_uid = await self.auth_handler.check_user_exists(user_id) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index c2ba790bab..411fb57c47 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -103,7 +103,9 @@ async def on_POST(self, request): # Raise if the provided next_link value isn't valid assert_valid_next_link(self.hs, next_link) - self.identity_handler.ratelimit_request_token_requests(request, "email", email) + await self.identity_handler.ratelimit_request_token_requests( + request, "email", email + ) # The email will be sent to the stored address. # This avoids a potential account hijack by requesting a password reset to @@ -387,7 +389,9 @@ async def on_POST(self, request): Codes.THREEPID_DENIED, ) - self.identity_handler.ratelimit_request_token_requests(request, "email", email) + await self.identity_handler.ratelimit_request_token_requests( + request, "email", email + ) if next_link: # Raise if the provided next_link value isn't valid @@ -468,7 +472,7 @@ async def on_POST(self, request): Codes.THREEPID_DENIED, ) - self.identity_handler.ratelimit_request_token_requests( + await self.identity_handler.ratelimit_request_token_requests( request, "msisdn", msisdn ) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 8f68d8dfc8..c212da0cb2 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -126,7 +126,9 @@ async def on_POST(self, request): Codes.THREEPID_DENIED, ) - self.identity_handler.ratelimit_request_token_requests(request, "email", email) + await self.identity_handler.ratelimit_request_token_requests( + request, "email", email + ) existing_user_id = await self.hs.get_datastore().get_user_id_by_threepid( "email", email @@ -208,7 +210,7 @@ async def on_POST(self, request): Codes.THREEPID_DENIED, ) - self.identity_handler.ratelimit_request_token_requests( + await self.identity_handler.ratelimit_request_token_requests( request, "msisdn", msisdn ) @@ -406,7 +408,7 @@ async def on_POST(self, request): client_addr = request.getClientIP() - self.ratelimiter.ratelimit(client_addr, update=False) + await self.ratelimiter.ratelimit(None, client_addr, update=False) kind = b"user" if b"kind" in request.args: diff --git a/synapse/server.py b/synapse/server.py index e85b9391fa..e42f7b1a18 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -329,6 +329,7 @@ def get_distributor(self) -> Distributor: @cache_in_self def get_registration_ratelimiter(self) -> Ratelimiter: return Ratelimiter( + store=self.get_datastore(), clock=self.get_clock(), rate_hz=self.config.rc_registration.per_second, burst_count=self.config.rc_registration.burst_count, diff --git a/tests/api/test_ratelimiting.py b/tests/api/test_ratelimiting.py index 483418192c..fa96ba07a5 100644 --- a/tests/api/test_ratelimiting.py +++ b/tests/api/test_ratelimiting.py @@ -5,38 +5,25 @@ from tests import unittest -class TestRatelimiter(unittest.TestCase): +class TestRatelimiter(unittest.HomeserverTestCase): def test_allowed_via_can_do_action(self): - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) - allowed, time_allowed = limiter.can_do_action(key="test_id", _time_now_s=0) - self.assertTrue(allowed) - self.assertEquals(10.0, time_allowed) - - allowed, time_allowed = limiter.can_do_action(key="test_id", _time_now_s=5) - self.assertFalse(allowed) - self.assertEquals(10.0, time_allowed) - - allowed, time_allowed = limiter.can_do_action(key="test_id", _time_now_s=10) - self.assertTrue(allowed) - self.assertEquals(20.0, time_allowed) - - def test_allowed_user_via_can_requester_do_action(self): - user_requester = create_requester("@user:example.com") - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) - allowed, time_allowed = limiter.can_requester_do_action( - user_requester, _time_now_s=0 + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=1 + ) + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", _time_now_s=0) ) self.assertTrue(allowed) self.assertEquals(10.0, time_allowed) - allowed, time_allowed = limiter.can_requester_do_action( - user_requester, _time_now_s=5 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", _time_now_s=5) ) self.assertFalse(allowed) self.assertEquals(10.0, time_allowed) - allowed, time_allowed = limiter.can_requester_do_action( - user_requester, _time_now_s=10 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", _time_now_s=10) ) self.assertTrue(allowed) self.assertEquals(20.0, time_allowed) @@ -51,21 +38,23 @@ def test_allowed_appservice_ratelimited_via_can_requester_do_action(self): ) as_requester = create_requester("@user:example.com", app_service=appservice) - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) - allowed, time_allowed = limiter.can_requester_do_action( - as_requester, _time_now_s=0 + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=1 + ) + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(as_requester, _time_now_s=0) ) self.assertTrue(allowed) self.assertEquals(10.0, time_allowed) - allowed, time_allowed = limiter.can_requester_do_action( - as_requester, _time_now_s=5 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(as_requester, _time_now_s=5) ) self.assertFalse(allowed) self.assertEquals(10.0, time_allowed) - allowed, time_allowed = limiter.can_requester_do_action( - as_requester, _time_now_s=10 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(as_requester, _time_now_s=10) ) self.assertTrue(allowed) self.assertEquals(20.0, time_allowed) @@ -80,73 +69,89 @@ def test_allowed_appservice_via_can_requester_do_action(self): ) as_requester = create_requester("@user:example.com", app_service=appservice) - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) - allowed, time_allowed = limiter.can_requester_do_action( - as_requester, _time_now_s=0 + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=1 + ) + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(as_requester, _time_now_s=0) ) self.assertTrue(allowed) self.assertEquals(-1, time_allowed) - allowed, time_allowed = limiter.can_requester_do_action( - as_requester, _time_now_s=5 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(as_requester, _time_now_s=5) ) self.assertTrue(allowed) self.assertEquals(-1, time_allowed) - allowed, time_allowed = limiter.can_requester_do_action( - as_requester, _time_now_s=10 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(as_requester, _time_now_s=10) ) self.assertTrue(allowed) self.assertEquals(-1, time_allowed) def test_allowed_via_ratelimit(self): - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=1 + ) # Shouldn't raise - limiter.ratelimit(key="test_id", _time_now_s=0) + self.get_success_or_raise(limiter.ratelimit(None, key="test_id", _time_now_s=0)) # Should raise with self.assertRaises(LimitExceededError) as context: - limiter.ratelimit(key="test_id", _time_now_s=5) + self.get_success_or_raise( + limiter.ratelimit(None, key="test_id", _time_now_s=5) + ) self.assertEqual(context.exception.retry_after_ms, 5000) # Shouldn't raise - limiter.ratelimit(key="test_id", _time_now_s=10) + self.get_success_or_raise( + limiter.ratelimit(None, key="test_id", _time_now_s=10) + ) def test_allowed_via_can_do_action_and_overriding_parameters(self): """Test that we can override options of can_do_action that would otherwise fail an action """ # Create a Ratelimiter with a very low allowed rate_hz and burst_count - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=1 + ) # First attempt should be allowed - allowed, time_allowed = limiter.can_do_action( - ("test_id",), - _time_now_s=0, + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action( + None, + ("test_id",), + _time_now_s=0, + ) ) self.assertTrue(allowed) self.assertEqual(10.0, time_allowed) # Second attempt, 1s later, will fail - allowed, time_allowed = limiter.can_do_action( - ("test_id",), - _time_now_s=1, + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action( + None, + ("test_id",), + _time_now_s=1, + ) ) self.assertFalse(allowed) self.assertEqual(10.0, time_allowed) # But, if we allow 10 actions/sec for this request, we should be allowed # to continue. - allowed, time_allowed = limiter.can_do_action( - ("test_id",), _time_now_s=1, rate_hz=10.0 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, ("test_id",), _time_now_s=1, rate_hz=10.0) ) self.assertTrue(allowed) self.assertEqual(1.1, time_allowed) # Similarly if we allow a burst of 10 actions - allowed, time_allowed = limiter.can_do_action( - ("test_id",), _time_now_s=1, burst_count=10 + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, ("test_id",), _time_now_s=1, burst_count=10) ) self.assertTrue(allowed) self.assertEqual(1.0, time_allowed) @@ -156,29 +161,72 @@ def test_allowed_via_ratelimit_and_overriding_parameters(self): fail an action """ # Create a Ratelimiter with a very low allowed rate_hz and burst_count - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=1 + ) # First attempt should be allowed - limiter.ratelimit(key=("test_id",), _time_now_s=0) + self.get_success_or_raise( + limiter.ratelimit(None, key=("test_id",), _time_now_s=0) + ) # Second attempt, 1s later, will fail with self.assertRaises(LimitExceededError) as context: - limiter.ratelimit(key=("test_id",), _time_now_s=1) + self.get_success_or_raise( + limiter.ratelimit(None, key=("test_id",), _time_now_s=1) + ) self.assertEqual(context.exception.retry_after_ms, 9000) # But, if we allow 10 actions/sec for this request, we should be allowed # to continue. - limiter.ratelimit(key=("test_id",), _time_now_s=1, rate_hz=10.0) + self.get_success_or_raise( + limiter.ratelimit(None, key=("test_id",), _time_now_s=1, rate_hz=10.0) + ) # Similarly if we allow a burst of 10 actions - limiter.ratelimit(key=("test_id",), _time_now_s=1, burst_count=10) + self.get_success_or_raise( + limiter.ratelimit(None, key=("test_id",), _time_now_s=1, burst_count=10) + ) def test_pruning(self): - limiter = Ratelimiter(clock=None, rate_hz=0.1, burst_count=1) - limiter.can_do_action(key="test_id_1", _time_now_s=0) + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=1 + ) + self.get_success_or_raise( + limiter.can_do_action(None, key="test_id_1", _time_now_s=0) + ) self.assertIn("test_id_1", limiter.actions) - limiter.can_do_action(key="test_id_2", _time_now_s=10) + self.get_success_or_raise( + limiter.can_do_action(None, key="test_id_2", _time_now_s=10) + ) self.assertNotIn("test_id_1", limiter.actions) + + def test_db_user_override(self): + """Test that users that have ratelimiting disabled in the DB aren't + ratelimited. + """ + store = self.hs.get_datastore() + + user_id = "@user:test" + requester = create_requester(user_id) + + self.get_success( + store.db_pool.simple_insert( + table="ratelimit_override", + values={ + "user_id": user_id, + "messages_per_second": None, + "burst_count": None, + }, + desc="test_db_user_override", + ) + ) + + limiter = Ratelimiter(store=store, clock=None, rate_hz=0.1, burst_count=1) + + # Shouldn't raise + for _ in range(20): + self.get_success_or_raise(limiter.ratelimit(requester, _time_now_s=0)) From f02663c4ddfa259c96aebde848a83156540c9fb3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 30 Mar 2021 12:12:44 +0100 Subject: [PATCH 002/619] Replace `room_invite_state_types` with `room_prejoin_state` (#9700) `room_invite_state_types` was inconvenient as a configuration setting, because anyone that ever set it would not receive any new types that were added to the defaults. Here, we deprecate the old setting, and replace it with a couple of new settings under `room_prejoin_state`. --- changelog.d/9700.feature | 1 + docker/conf/homeserver.yaml | 8 -- docs/code_style.md | 3 + docs/sample_config.yaml | 33 +++-- synapse/config/api.py | 135 +++++++++++++++--- synapse/handlers/message.py | 2 +- .../storage/databases/main/events_worker.py | 4 +- tests/utils.py | 1 - 8 files changed, 144 insertions(+), 43 deletions(-) create mode 100644 changelog.d/9700.feature diff --git a/changelog.d/9700.feature b/changelog.d/9700.feature new file mode 100644 index 0000000000..037de8367f --- /dev/null +++ b/changelog.d/9700.feature @@ -0,0 +1 @@ +Replace the `room_invite_state_types` configuration setting with `room_prejoin_state`. diff --git a/docker/conf/homeserver.yaml b/docker/conf/homeserver.yaml index 0dea62a87d..a792899540 100644 --- a/docker/conf/homeserver.yaml +++ b/docker/conf/homeserver.yaml @@ -173,18 +173,10 @@ report_stats: False ## API Configuration ## -room_invite_state_types: - - "m.room.join_rules" - - "m.room.canonical_alias" - - "m.room.avatar" - - "m.room.name" - {% if SYNAPSE_APPSERVICES %} app_service_config_files: {% for appservice in SYNAPSE_APPSERVICES %} - "{{ appservice }}" {% endfor %} -{% else %} -app_service_config_files: [] {% endif %} macaroon_secret_key: "{{ SYNAPSE_MACAROON_SECRET_KEY }}" diff --git a/docs/code_style.md b/docs/code_style.md index 190f8ab2de..28fb7277c4 100644 --- a/docs/code_style.md +++ b/docs/code_style.md @@ -128,6 +128,9 @@ Some guidelines follow: will be if no sub-options are enabled). - Lines should be wrapped at 80 characters. - Use two-space indents. +- `true` and `false` are spelt thus (as opposed to `True`, etc.) +- Use single quotes (`'`) rather than double-quotes (`"`) or backticks + (`` ` ``) to refer to configuration options. Example: diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 17cda71adc..c73ea6b161 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1451,14 +1451,31 @@ metrics_flags: ## API Configuration ## -# A list of event types that will be included in the room_invite_state -# -#room_invite_state_types: -# - "m.room.join_rules" -# - "m.room.canonical_alias" -# - "m.room.avatar" -# - "m.room.encryption" -# - "m.room.name" +# Controls for the state that is shared with users who receive an invite +# to a room +# +room_prejoin_state: + # By default, the following state event types are shared with users who + # receive invites to the room: + # + # - m.room.join_rules + # - m.room.canonical_alias + # - m.room.avatar + # - m.room.encryption + # - m.room.name + # + # Uncomment the following to disable these defaults (so that only the event + # types listed in 'additional_event_types' are shared). Defaults to 'false'. + # + #disable_default_event_types: true + + # Additional state event types to share with users when they are invited + # to a room. + # + # By default, this list is empty (so only the default event types are shared). + # + #additional_event_types: + # - org.example.custom.event.type # A list of application service config files to use diff --git a/synapse/config/api.py b/synapse/config/api.py index 74cd53a8ed..91387a7f0e 100644 --- a/synapse/config/api.py +++ b/synapse/config/api.py @@ -1,4 +1,4 @@ -# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2015-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,38 +12,127 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging +from typing import Iterable + from synapse.api.constants import EventTypes +from synapse.config._base import Config, ConfigError +from synapse.config._util import validate_config +from synapse.types import JsonDict -from ._base import Config +logger = logging.getLogger(__name__) class ApiConfig(Config): section = "api" - def read_config(self, config, **kwargs): - self.room_invite_state_types = config.get( - "room_invite_state_types", - [ - EventTypes.JoinRules, - EventTypes.CanonicalAlias, - EventTypes.RoomAvatar, - EventTypes.RoomEncryption, - EventTypes.Name, - ], + def read_config(self, config: JsonDict, **kwargs): + validate_config(_MAIN_SCHEMA, config, ()) + self.room_prejoin_state = list(self._get_prejoin_state_types(config)) + + def generate_config_section(cls, **kwargs) -> str: + formatted_default_state_types = "\n".join( + " # - %s" % (t,) for t in _DEFAULT_PREJOIN_STATE_TYPES ) - def generate_config_section(cls, **kwargs): return """\ ## API Configuration ## - # A list of event types that will be included in the room_invite_state + # Controls for the state that is shared with users who receive an invite + # to a room # - #room_invite_state_types: - # - "{JoinRules}" - # - "{CanonicalAlias}" - # - "{RoomAvatar}" - # - "{RoomEncryption}" - # - "{Name}" - """.format( - **vars(EventTypes) - ) + room_prejoin_state: + # By default, the following state event types are shared with users who + # receive invites to the room: + # +%(formatted_default_state_types)s + # + # Uncomment the following to disable these defaults (so that only the event + # types listed in 'additional_event_types' are shared). Defaults to 'false'. + # + #disable_default_event_types: true + + # Additional state event types to share with users when they are invited + # to a room. + # + # By default, this list is empty (so only the default event types are shared). + # + #additional_event_types: + # - org.example.custom.event.type + """ % { + "formatted_default_state_types": formatted_default_state_types + } + + def _get_prejoin_state_types(self, config: JsonDict) -> Iterable[str]: + """Get the event types to include in the prejoin state + + Parses the config and returns an iterable of the event types to be included. + """ + room_prejoin_state_config = config.get("room_prejoin_state") or {} + + # backwards-compatibility support for room_invite_state_types + if "room_invite_state_types" in config: + # if both "room_invite_state_types" and "room_prejoin_state" are set, then + # we don't really know what to do. + if room_prejoin_state_config: + raise ConfigError( + "Can't specify both 'room_invite_state_types' and 'room_prejoin_state' " + "in config" + ) + + logger.warning(_ROOM_INVITE_STATE_TYPES_WARNING) + + yield from config["room_invite_state_types"] + return + + if not room_prejoin_state_config.get("disable_default_event_types"): + yield from _DEFAULT_PREJOIN_STATE_TYPES + + yield from room_prejoin_state_config.get("additional_event_types", []) + + +_ROOM_INVITE_STATE_TYPES_WARNING = """\ +WARNING: The 'room_invite_state_types' configuration setting is now deprecated, +and replaced with 'room_prejoin_state'. New features may not work correctly +unless 'room_invite_state_types' is removed. See the sample configuration file for +details of 'room_prejoin_state'. +-------------------------------------------------------------------------------- +""" + +_DEFAULT_PREJOIN_STATE_TYPES = [ + EventTypes.JoinRules, + EventTypes.CanonicalAlias, + EventTypes.RoomAvatar, + EventTypes.RoomEncryption, + EventTypes.Name, +] + + +# room_prejoin_state can either be None (as it is in the default config), or +# an object containing other config settings +_ROOM_PREJOIN_STATE_CONFIG_SCHEMA = { + "oneOf": [ + { + "type": "object", + "properties": { + "disable_default_event_types": {"type": "boolean"}, + "additional_event_types": { + "type": "array", + "items": {"type": "string"}, + }, + }, + }, + {"type": "null"}, + ] +} + +# the legacy room_invite_state_types setting +_ROOM_INVITE_STATE_TYPES_SCHEMA = {"type": "array", "items": {"type": "string"}} + +_MAIN_SCHEMA = { + "type": "object", + "properties": { + "room_prejoin_state": _ROOM_PREJOIN_STATE_CONFIG_SCHEMA, + "room_invite_state_types": _ROOM_INVITE_STATE_TYPES_SCHEMA, + }, +} diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 1b7c065b34..6069968f7f 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -385,7 +385,7 @@ def __init__(self, hs: "HomeServer"): self._events_shard_config = self.config.worker.events_shard_config self._instance_name = hs.get_instance_name() - self.room_invite_state_types = self.hs.config.room_invite_state_types + self.room_invite_state_types = self.hs.config.api.room_prejoin_state self.membership_types_to_include_profile_data_in = ( {Membership.JOIN, Membership.INVITE} diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index 952d4969b2..c00780969f 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -16,7 +16,7 @@ import logging import threading from collections import namedtuple -from typing import Dict, Iterable, List, Optional, Tuple, overload +from typing import Container, Dict, Iterable, List, Optional, Tuple, overload from constantly import NamedConstant, Names from typing_extensions import Literal @@ -544,7 +544,7 @@ def _get_events_from_cache(self, events, allow_rejected, update_metrics=True): async def get_stripped_room_state_from_event_context( self, context: EventContext, - state_types_to_include: List[EventTypes], + state_types_to_include: Container[str], membership_user_id: Optional[str] = None, ) -> List[JsonDict]: """ diff --git a/tests/utils.py b/tests/utils.py index be80b13760..a141ee6496 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -122,7 +122,6 @@ def default_config(name, parse=False): "enable_registration_captcha": False, "macaroon_secret_key": "not even a little secret", "trusted_third_party_id_servers": [], - "room_invite_state_types": [], "password_providers": [], "worker_replication_url": "", "worker_app": None, From 4dabcf026e3a8f480268451332b6bf3a7e671480 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 30 Mar 2021 14:03:17 +0100 Subject: [PATCH 003/619] Include m.room.create in invite_room_state for Spaces (#9710) --- changelog.d/9710.feature | 1 + synapse/config/api.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/9710.feature diff --git a/changelog.d/9710.feature b/changelog.d/9710.feature new file mode 100644 index 0000000000..fce308cc41 --- /dev/null +++ b/changelog.d/9710.feature @@ -0,0 +1 @@ +Experimental Spaces support: include `m.room.create` in the room state sent with room-invites. diff --git a/synapse/config/api.py b/synapse/config/api.py index 91387a7f0e..55c038c0c4 100644 --- a/synapse/config/api.py +++ b/synapse/config/api.py @@ -88,6 +88,10 @@ def _get_prejoin_state_types(self, config: JsonDict) -> Iterable[str]: if not room_prejoin_state_config.get("disable_default_event_types"): yield from _DEFAULT_PREJOIN_STATE_TYPES + if self.spaces_enabled: + # MSC1772 suggests adding m.room.create to the prejoin state + yield EventTypes.Create + yield from room_prejoin_state_config.get("additional_event_types", []) From ac99774dacc31907ace76c6eda142b835a0dda25 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 31 Mar 2021 11:58:12 +0100 Subject: [PATCH 004/619] Rewrite complement.sh (#9685) This PR rewrites the original complement.sh script with a number of improvements: * We can now use a local checkout of Complement (configurable with `COMPLEMENT_DIR`), though the default behaviour still downloads the master branch. * You can now specify a regex of test names to run, or just run all tests. * We now use the Synapse test blacklist tag (so all tests will pass). --- changelog.d/9685.misc | 1 + scripts-dev/complement.sh | 49 ++++++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 changelog.d/9685.misc diff --git a/changelog.d/9685.misc b/changelog.d/9685.misc new file mode 100644 index 0000000000..0506d8af0c --- /dev/null +++ b/changelog.d/9685.misc @@ -0,0 +1 @@ +Update `scripts-dev/complement.sh` to use a local checkout of Complement, allow running a subset of tests and have it use Synapse's Complement test blacklist. \ No newline at end of file diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index 3cde53f5c0..31cc20a826 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -1,22 +1,49 @@ -#! /bin/bash -eu +#!/usr/bin/env bash # This script is designed for developers who want to test their code # against Complement. # # It makes a Synapse image which represents the current checkout, -# then downloads Complement and runs it with that image. +# builds a synapse-complement image on top, then runs tests with it. +# +# By default the script will fetch the latest Complement master branch and +# run tests with that. This can be overridden to use a custom Complement +# checkout by setting the COMPLEMENT_DIR environment variable to the +# filepath of a local Complement checkout. +# +# A regular expression of test method names can be supplied as the first +# argument to the script. Complement will then only run those tests. If +# no regex is supplied, all tests are run. For example; +# +# ./complement.sh "TestOutboundFederation(Profile|Send)" +# + +# Exit if a line returns a non-zero exit code +set -e +# Change to the repository root cd "$(dirname $0)/.." +# Check for a user-specified Complement checkout +if [[ -z "$COMPLEMENT_DIR" ]]; then + echo "COMPLEMENT_DIR not set. Fetching the latest Complement checkout..." + wget -Nq https://github.com/matrix-org/complement/archive/master.tar.gz + tar -xzf master.tar.gz + COMPLEMENT_DIR=complement-master + echo "Checkout available at 'complement-master'" +fi + # Build the base Synapse image from the local checkout -docker build -t matrixdotorg/synapse:latest -f docker/Dockerfile . +docker build -t matrixdotorg/synapse -f docker/Dockerfile . +# Build the Synapse monolith image from Complement, based on the above image we just built +docker build -t complement-synapse -f "$COMPLEMENT_DIR/dockerfiles/Synapse.Dockerfile" "$COMPLEMENT_DIR/dockerfiles" -# Download Complement -wget -N https://github.com/matrix-org/complement/archive/master.tar.gz -tar -xzf master.tar.gz -cd complement-master +cd "$COMPLEMENT_DIR" -# Build the Synapse image from Complement, based on the above image we just built -docker build -t complement-synapse -f dockerfiles/Synapse.Dockerfile ./dockerfiles +EXTRA_COMPLEMENT_ARGS="" +if [[ -n "$1" ]]; then + # A test name regex has been set, supply it to Complement + EXTRA_COMPLEMENT_ARGS+="-run $1 " +fi -# Run the tests on the resulting image! -COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -count=1 ./tests +# Run the tests! +COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -tags synapse_blacklist -count=1 $EXTRA_COMPLEMENT_ARGS ./tests From 670564446cebb7e287dfc084f88d94e8e68dcc02 Mon Sep 17 00:00:00 2001 From: Cristina Date: Wed, 31 Mar 2021 06:04:27 -0500 Subject: [PATCH 005/619] Deprecate imp (#9718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #9642. Signed-off-by: Cristina Muñoz --- changelog.d/9718.removal | 1 + synapse/storage/prepare_database.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9718.removal diff --git a/changelog.d/9718.removal b/changelog.d/9718.removal new file mode 100644 index 0000000000..6de7814217 --- /dev/null +++ b/changelog.d/9718.removal @@ -0,0 +1 @@ +Replace deprecated `imp` module with successor `importlib`. Contributed by Cristina Muñoz. diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 6c3c2da520..c7f0b8ccb5 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -13,7 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import imp +import importlib.util import logging import os import re @@ -454,8 +454,13 @@ def _upgrade_existing_database( ) module_name = "synapse.storage.v%d_%s" % (v, root_name) - with open(absolute_path) as python_file: - module = imp.load_source(module_name, absolute_path, python_file) # type: ignore + + spec = importlib.util.spec_from_file_location( + module_name, absolute_path + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) # type: ignore + logger.info("Running script %s", relative_path) module.run_create(cur, database_engine) # type: ignore if not is_empty: From 5ff8eb97c646f9f8de74915e4b2926789695d4af Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Wed, 31 Mar 2021 12:27:20 +0000 Subject: [PATCH 006/619] Make sample config allowed_local_3pids regex stricter. (#9719) The regex should be terminated so that subdomain matches of another domain are not accepted. Just ensuring that someone doesn't shoot themselves in the foot by copying our example. Signed-off-by: Denis Kasak --- changelog.d/9719.doc | 1 + docs/sample_config.yaml | 4 ++-- synapse/config/registration.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/9719.doc diff --git a/changelog.d/9719.doc b/changelog.d/9719.doc new file mode 100644 index 0000000000..f018606dd6 --- /dev/null +++ b/changelog.d/9719.doc @@ -0,0 +1 @@ +Make the allowed_local_3pids regex example in the sample config stricter. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index c73ea6b161..b0bf987740 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1246,9 +1246,9 @@ account_validity: # #allowed_local_3pids: # - medium: email -# pattern: '.*@matrix\.org' +# pattern: '^[^@]+@matrix\.org$' # - medium: email -# pattern: '.*@vector\.im' +# pattern: '^[^@]+@vector\.im$' # - medium: msisdn # pattern: '\+44' diff --git a/synapse/config/registration.py b/synapse/config/registration.py index ead007ba5a..f27d1e14ac 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -298,9 +298,9 @@ def generate_config_section(self, generate_secrets=False, **kwargs): # #allowed_local_3pids: # - medium: email - # pattern: '.*@matrix\\.org' + # pattern: '^[^@]+@matrix\\.org$' # - medium: email - # pattern: '.*@vector\\.im' + # pattern: '^[^@]+@vector\\.im$' # - medium: msisdn # pattern: '\\+44' From 35c5ef2d24734889a20a0cf334bb971a9329806f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 31 Mar 2021 16:39:08 -0400 Subject: [PATCH 007/619] Add an experimental room version to support restricted join rules. (#9717) Per MSC3083. --- changelog.d/9717.feature | 1 + synapse/api/constants.py | 2 + synapse/api/room_versions.py | 24 +++- synapse/config/experimental.py | 7 +- synapse/event_auth.py | 28 +++- tests/test_event_auth.py | 246 ++++++++++++++++++++++++++++++++- 6 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 changelog.d/9717.feature diff --git a/changelog.d/9717.feature b/changelog.d/9717.feature new file mode 100644 index 0000000000..c2c74f13d5 --- /dev/null +++ b/changelog.d/9717.feature @@ -0,0 +1 @@ +Add experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 8f37d2cf3b..6856dab06c 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -59,6 +59,8 @@ class JoinRules: KNOCK = "knock" INVITE = "invite" PRIVATE = "private" + # As defined for MSC3083. + MSC3083_RESTRICTED = "restricted" class LoginType: diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index de2cc15d33..87038d436d 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -57,7 +57,7 @@ class RoomVersion: state_res = attr.ib(type=int) # one of the StateResolutionVersions enforce_key_validity = attr.ib(type=bool) - # bool: before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules + # Before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules special_case_aliases_auth = attr.ib(type=bool) # Strictly enforce canonicaljson, do not allow: # * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1] @@ -69,6 +69,8 @@ class RoomVersion: limit_notifications_power_levels = attr.ib(type=bool) # MSC2174/MSC2176: Apply updated redaction rules algorithm. msc2176_redaction_rules = attr.ib(type=bool) + # MSC3083: Support the 'restricted' join_rule. + msc3083_join_rules = attr.ib(type=bool) class RoomVersions: @@ -82,6 +84,7 @@ class RoomVersions: strict_canonicaljson=False, limit_notifications_power_levels=False, msc2176_redaction_rules=False, + msc3083_join_rules=False, ) V2 = RoomVersion( "2", @@ -93,6 +96,7 @@ class RoomVersions: strict_canonicaljson=False, limit_notifications_power_levels=False, msc2176_redaction_rules=False, + msc3083_join_rules=False, ) V3 = RoomVersion( "3", @@ -104,6 +108,7 @@ class RoomVersions: strict_canonicaljson=False, limit_notifications_power_levels=False, msc2176_redaction_rules=False, + msc3083_join_rules=False, ) V4 = RoomVersion( "4", @@ -115,6 +120,7 @@ class RoomVersions: strict_canonicaljson=False, limit_notifications_power_levels=False, msc2176_redaction_rules=False, + msc3083_join_rules=False, ) V5 = RoomVersion( "5", @@ -126,6 +132,7 @@ class RoomVersions: strict_canonicaljson=False, limit_notifications_power_levels=False, msc2176_redaction_rules=False, + msc3083_join_rules=False, ) V6 = RoomVersion( "6", @@ -137,6 +144,7 @@ class RoomVersions: strict_canonicaljson=True, limit_notifications_power_levels=True, msc2176_redaction_rules=False, + msc3083_join_rules=False, ) MSC2176 = RoomVersion( "org.matrix.msc2176", @@ -148,6 +156,19 @@ class RoomVersions: strict_canonicaljson=True, limit_notifications_power_levels=True, msc2176_redaction_rules=True, + msc3083_join_rules=False, + ) + MSC3083 = RoomVersion( + "org.matrix.msc3083", + RoomDisposition.UNSTABLE, + EventFormatVersions.V3, + StateResolutionVersions.V2, + enforce_key_validity=True, + special_case_aliases_auth=False, + strict_canonicaljson=True, + limit_notifications_power_levels=True, + msc2176_redaction_rules=False, + msc3083_join_rules=True, ) @@ -162,4 +183,5 @@ class RoomVersions: RoomVersions.V6, RoomVersions.MSC2176, ) + # Note that we do not include MSC3083 here unless it is enabled in the config. } # type: Dict[str, RoomVersion] diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 86f4d9af9d..eb96ecda74 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.config._base import Config from synapse.types import JsonDict @@ -27,7 +28,11 @@ def read_config(self, config: JsonDict, **kwargs): # MSC2858 (multiple SSO identity providers) self.msc2858_enabled = experimental.get("msc2858_enabled", False) # type: bool - # Spaces (MSC1772, MSC2946, etc) + + # Spaces (MSC1772, MSC2946, MSC3083, etc) self.spaces_enabled = experimental.get("spaces_enabled", False) # type: bool + if self.spaces_enabled: + KNOWN_ROOM_VERSIONS[RoomVersions.MSC3083.identifier] = RoomVersions.MSC3083 + # MSC3026 (busy presence state) self.msc3026_enabled = experimental.get("msc3026_enabled", False) # type: bool diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 91ad5b3d3c..9863953f5c 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -162,7 +162,7 @@ def check( logger.debug("Auth events: %s", [a.event_id for a in auth_events.values()]) if event.type == EventTypes.Member: - _is_membership_change_allowed(event, auth_events) + _is_membership_change_allowed(room_version_obj, event, auth_events) logger.debug("Allowing! %s", event) return @@ -220,8 +220,19 @@ def _can_federate(event: EventBase, auth_events: StateMap[EventBase]) -> bool: def _is_membership_change_allowed( - event: EventBase, auth_events: StateMap[EventBase] + room_version: RoomVersion, event: EventBase, auth_events: StateMap[EventBase] ) -> None: + """ + Confirms that the event which changes membership is an allowed change. + + Args: + room_version: The version of the room. + event: The event to check. + auth_events: The current auth events of the room. + + Raises: + AuthError if the event is not allowed. + """ membership = event.content["membership"] # Check if this is the room creator joining: @@ -315,14 +326,19 @@ def _is_membership_change_allowed( if user_level < invite_level: raise AuthError(403, "You don't have permission to invite users") elif Membership.JOIN == membership: - # Joins are valid iff caller == target and they were: - # invited: They are accepting the invitation - # joined: It's a NOOP + # Joins are valid iff caller == target and: + # * They are not banned. + # * They are accepting a previously sent invitation. + # * They are already joined (it's a NOOP). + # * The room is public or restricted. if event.user_id != target_user_id: raise AuthError(403, "Cannot force another user to join.") elif target_banned: raise AuthError(403, "You are banned from this room") - elif join_rule == JoinRules.PUBLIC: + elif join_rule == JoinRules.PUBLIC or ( + room_version.msc3083_join_rules + and join_rule == JoinRules.MSC3083_RESTRICTED + ): pass elif join_rule == JoinRules.INVITE: if not caller_in_room and not caller_invited: diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index 3f2691ee6b..b5f18344dc 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -207,6 +207,226 @@ def test_msc2209(self): do_sig_check=False, ) + def test_join_rules_public(self): + """ + Test joining a public room. + """ + creator = "@creator:example.com" + pleb = "@joiner:example.com" + + auth_events = { + ("m.room.create", ""): _create_event(creator), + ("m.room.member", creator): _join_event(creator), + ("m.room.join_rules", ""): _join_rules_event(creator, "public"), + } + + # Check join. + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user cannot be force-joined to a room. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V6, + _member_event(pleb, "join", sender=creator), + auth_events, + do_sig_check=False, + ) + + # Banned should be rejected. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "ban") + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user who left can re-join. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "leave") + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user can send a join if they're in the room. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "join") + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user can accept an invite. + auth_events[("m.room.member", pleb)] = _member_event( + pleb, "invite", sender=creator + ) + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + def test_join_rules_invite(self): + """ + Test joining an invite only room. + """ + creator = "@creator:example.com" + pleb = "@joiner:example.com" + + auth_events = { + ("m.room.create", ""): _create_event(creator), + ("m.room.member", creator): _join_event(creator), + ("m.room.join_rules", ""): _join_rules_event(creator, "invite"), + } + + # A join without an invite is rejected. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user cannot be force-joined to a room. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V6, + _member_event(pleb, "join", sender=creator), + auth_events, + do_sig_check=False, + ) + + # Banned should be rejected. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "ban") + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user who left cannot re-join. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "leave") + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user can send a join if they're in the room. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "join") + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user can accept an invite. + auth_events[("m.room.member", pleb)] = _member_event( + pleb, "invite", sender=creator + ) + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + def test_join_rules_msc3083_restricted(self): + """ + Test joining a restricted room from MSC3083. + + This is pretty much the same test as public. + """ + creator = "@creator:example.com" + pleb = "@joiner:example.com" + + auth_events = { + ("m.room.create", ""): _create_event(creator), + ("m.room.member", creator): _join_event(creator), + ("m.room.join_rules", ""): _join_rules_event(creator, "restricted"), + } + + # Older room versions don't understand this join rule + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.V6, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # Check join. + event_auth.check( + RoomVersions.MSC3083, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user cannot be force-joined to a room. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.MSC3083, + _member_event(pleb, "join", sender=creator), + auth_events, + do_sig_check=False, + ) + + # Banned should be rejected. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "ban") + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.MSC3083, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user who left can re-join. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "leave") + event_auth.check( + RoomVersions.MSC3083, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user can send a join if they're in the room. + auth_events[("m.room.member", pleb)] = _member_event(pleb, "join") + event_auth.check( + RoomVersions.MSC3083, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # A user can accept an invite. + auth_events[("m.room.member", pleb)] = _member_event( + pleb, "invite", sender=creator + ) + event_auth.check( + RoomVersions.MSC3083, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + # helpers for making events @@ -225,19 +445,24 @@ def _create_event(user_id): ) -def _join_event(user_id): +def _member_event(user_id, membership, sender=None): return make_event_from_dict( { "room_id": TEST_ROOM_ID, "event_id": _get_event_id(), "type": "m.room.member", - "sender": user_id, + "sender": sender or user_id, "state_key": user_id, - "content": {"membership": "join"}, + "content": {"membership": membership}, + "prev_events": [], } ) +def _join_event(user_id): + return _member_event(user_id, "join") + + def _power_levels_event(sender, content): return make_event_from_dict( { @@ -277,6 +502,21 @@ def _random_state_event(sender): ) +def _join_rules_event(sender, join_rule): + return make_event_from_dict( + { + "room_id": TEST_ROOM_ID, + "event_id": _get_event_id(), + "type": "m.room.join_rules", + "sender": sender, + "state_key": "", + "content": { + "join_rule": join_rule, + }, + } + ) + + event_count = 0 From bb0fe02a5278d0033825bee31a7a49af838d4a9a Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 1 Apr 2021 12:28:53 +0200 Subject: [PATCH 008/619] Add `order_by` to list user admin API (#9691) --- changelog.d/9691.feature | 1 + docs/admin_api/user_admin_api.rst | 85 +++++++++++---- synapse/rest/admin/users.py | 21 +++- synapse/storage/databases/main/__init__.py | 26 ++++- synapse/storage/databases/main/stats.py | 25 ++++- tests/rest/admin/test_user.py | 121 ++++++++++++++++++++- 6 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 changelog.d/9691.feature diff --git a/changelog.d/9691.feature b/changelog.d/9691.feature new file mode 100644 index 0000000000..3c711db4f5 --- /dev/null +++ b/changelog.d/9691.feature @@ -0,0 +1 @@ +Add `order_by` to the admin API `GET /_synapse/admin/v2/users`. Contributed by @dklimpel. \ No newline at end of file diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index 8d4ec5a6f9..a8a5a2628c 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -111,35 +111,16 @@ List Accounts ============= This API returns all local user accounts. +By default, the response is ordered by ascending user ID. -The api is:: +The API is:: GET /_synapse/admin/v2/users?from=0&limit=10&guests=false To use it, you will need to authenticate by providing an ``access_token`` for a server admin: see `README.rst `_. -The parameter ``from`` is optional but used for pagination, denoting the -offset in the returned results. This should be treated as an opaque value and -not explicitly set to anything other than the return value of ``next_token`` -from a previous call. - -The parameter ``limit`` is optional but is used for pagination, denoting the -maximum number of items to return in this call. Defaults to ``100``. - -The parameter ``user_id`` is optional and filters to only return users with user IDs -that contain this value. This parameter is ignored when using the ``name`` parameter. - -The parameter ``name`` is optional and filters to only return users with user ID localparts -**or** displaynames that contain this value. - -The parameter ``guests`` is optional and if ``false`` will **exclude** guest users. -Defaults to ``true`` to include guest users. - -The parameter ``deactivated`` is optional and if ``true`` will **include** deactivated users. -Defaults to ``false`` to exclude deactivated users. - -A JSON body is returned with the following shape: +A response body like the following is returned: .. code:: json @@ -175,6 +156,66 @@ with ``from`` set to the value of ``next_token``. This will return a new page. If the endpoint does not return a ``next_token`` then there are no more users to paginate through. +**Parameters** + +The following parameters should be set in the URL: + +- ``user_id`` - Is optional and filters to only return users with user IDs + that contain this value. This parameter is ignored when using the ``name`` parameter. +- ``name`` - Is optional and filters to only return users with user ID localparts + **or** displaynames that contain this value. +- ``guests`` - string representing a bool - Is optional and if ``false`` will **exclude** guest users. + Defaults to ``true`` to include guest users. +- ``deactivated`` - string representing a bool - Is optional and if ``true`` will **include** deactivated users. + Defaults to ``false`` to exclude deactivated users. +- ``limit`` - string representing a positive integer - Is optional but is used for pagination, + denoting the maximum number of items to return in this call. Defaults to ``100``. +- ``from`` - string representing a positive integer - Is optional but used for pagination, + denoting the offset in the returned results. This should be treated as an opaque value and + not explicitly set to anything other than the return value of ``next_token`` from a previous call. + Defaults to ``0``. +- ``order_by`` - The method by which to sort the returned list of users. + If the ordered field has duplicates, the second order is always by ascending ``name``, + which guarantees a stable ordering. Valid values are: + + - ``name`` - Users are ordered alphabetically by ``name``. This is the default. + - ``is_guest`` - Users are ordered by ``is_guest`` status. + - ``admin`` - Users are ordered by ``admin`` status. + - ``user_type`` - Users are ordered alphabetically by ``user_type``. + - ``deactivated`` - Users are ordered by ``deactivated`` status. + - ``shadow_banned`` - Users are ordered by ``shadow_banned`` status. + - ``displayname`` - Users are ordered alphabetically by ``displayname``. + - ``avatar_url`` - Users are ordered alphabetically by avatar URL. + +- ``dir`` - Direction of media order. Either ``f`` for forwards or ``b`` for backwards. + Setting this value to ``b`` will reverse the above sort order. Defaults to ``f``. + +Caution. The database only has indexes on the columns ``name`` and ``created_ts``. +This means that if a different sort order is used (``is_guest``, ``admin``, +``user_type``, ``deactivated``, ``shadow_banned``, ``avatar_url`` or ``displayname``), +this can cause a large load on the database, especially for large environments. + +**Response** + +The following fields are returned in the JSON response body: + +- ``users`` - An array of objects, each containing information about an user. + User objects contain the following fields: + + - ``name`` - string - Fully-qualified user ID (ex. `@user:server.com`). + - ``is_guest`` - bool - Status if that user is a guest account. + - ``admin`` - bool - Status if that user is a server administrator. + - ``user_type`` - string - Type of the user. Normal users are type ``None``. + This allows user type specific behaviour. There are also types ``support`` and ``bot``. + - ``deactivated`` - bool - Status if that user has been marked as deactivated. + - ``shadow_banned`` - bool - Status if that user has been marked as shadow banned. + - ``displayname`` - string - The user's display name if they have set one. + - ``avatar_url`` - string - The user's avatar URL if they have set one. + +- ``next_token``: string representing a positive integer - Indication for pagination. See above. +- ``total`` - integer - Total number of media. + + Query current sessions for a user ================================= diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 309bd2771b..fa7804583a 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -36,6 +36,7 @@ ) from synapse.rest.client.v2_alpha._base import client_patterns from synapse.storage.databases.main.media_repository import MediaSortOrder +from synapse.storage.databases.main.stats import UserSortOrder from synapse.types import JsonDict, UserID if TYPE_CHECKING: @@ -117,8 +118,26 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: guests = parse_boolean(request, "guests", default=True) deactivated = parse_boolean(request, "deactivated", default=False) + order_by = parse_string( + request, + "order_by", + default=UserSortOrder.NAME.value, + allowed_values=( + UserSortOrder.NAME.value, + UserSortOrder.DISPLAYNAME.value, + UserSortOrder.GUEST.value, + UserSortOrder.ADMIN.value, + UserSortOrder.DEACTIVATED.value, + UserSortOrder.USER_TYPE.value, + UserSortOrder.AVATAR_URL.value, + UserSortOrder.SHADOW_BANNED.value, + ), + ) + + direction = parse_string(request, "dir", default="f", allowed_values=("f", "b")) + users, total = await self.store.get_users_paginate( - start, limit, user_id, name, guests, deactivated + start, limit, user_id, name, guests, deactivated, order_by, direction ) ret = {"users": users, "total": total} if (start + limit) < total: diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index 1d44c3aa2c..b3d16ca7ac 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -21,6 +21,7 @@ from synapse.api.constants import PresenceState from synapse.config.homeserver import HomeServerConfig from synapse.storage.database import DatabasePool +from synapse.storage.databases.main.stats import UserSortOrder from synapse.storage.engines import PostgresEngine from synapse.storage.util.id_generators import ( IdGenerator, @@ -292,6 +293,8 @@ async def get_users_paginate( name: Optional[str] = None, guests: bool = True, deactivated: bool = False, + order_by: UserSortOrder = UserSortOrder.USER_ID.value, + direction: str = "f", ) -> Tuple[List[JsonDict], int]: """Function to retrieve a paginated list of users from users list. This will return a json list of users and the @@ -304,6 +307,8 @@ async def get_users_paginate( name: search for local part of user_id or display name guests: whether to in include guest users deactivated: whether to include deactivated users + order_by: the sort order of the returned list + direction: sort ascending or descending Returns: A tuple of a list of mappings from user to information and a count of total users. """ @@ -312,6 +317,14 @@ def get_users_paginate_txn(txn): filters = [] args = [self.hs.config.server_name] + # Set ordering + order_by_column = UserSortOrder(order_by).value + + if direction == "b": + order = "DESC" + else: + order = "ASC" + # `name` is in database already in lower case if name: filters.append("(name LIKE ? OR LOWER(displayname) LIKE ?)") @@ -339,10 +352,15 @@ def get_users_paginate_txn(txn): txn.execute(sql, args) count = txn.fetchone()[0] - sql = ( - "SELECT name, user_type, is_guest, admin, deactivated, shadow_banned, displayname, avatar_url " - + sql_base - + " ORDER BY u.name LIMIT ? OFFSET ?" + sql = """ + SELECT name, user_type, is_guest, admin, deactivated, shadow_banned, displayname, avatar_url + {sql_base} + ORDER BY {order_by_column} {order}, u.name ASC + LIMIT ? OFFSET ? + """.format( + sql_base=sql_base, + order_by_column=order_by_column, + order=order, ) args += [limit, start] txn.execute(sql, args) diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index 1c99393c65..bce8946c21 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -66,18 +66,37 @@ class UserSortOrder(Enum): """ Enum to define the sorting method used when returning users - with get_users_media_usage_paginate + with get_users_paginate in __init__.py + and get_users_media_usage_paginate in stats.py - MEDIA_LENGTH = ordered by size of uploaded media. Smallest to largest. - MEDIA_COUNT = ordered by number of uploaded media. Smallest to largest. + When moves this to __init__.py gets `builtins.ImportError` with + `most likely due to a circular import` + + MEDIA_LENGTH = ordered by size of uploaded media. + MEDIA_COUNT = ordered by number of uploaded media. USER_ID = ordered alphabetically by `user_id`. + NAME = ordered alphabetically by `user_id`. This is for compatibility reasons, + as the user_id is returned in the name field in the response in list users admin API. DISPLAYNAME = ordered alphabetically by `displayname` + GUEST = ordered by `is_guest` + ADMIN = ordered by `admin` + DEACTIVATED = ordered by `deactivated` + USER_TYPE = ordered alphabetically by `user_type` + AVATAR_URL = ordered alphabetically by `avatar_url` + SHADOW_BANNED = ordered by `shadow_banned` """ MEDIA_LENGTH = "media_length" MEDIA_COUNT = "media_count" USER_ID = "user_id" + NAME = "name" DISPLAYNAME = "displayname" + GUEST = "is_guest" + ADMIN = "admin" + DEACTIVATED = "deactivated" + USER_TYPE = "user_type" + AVATAR_URL = "avatar_url" + SHADOW_BANNED = "shadow_banned" class StatsStore(StateDeltasStore): diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index cf61f284cb..0c9ec133c2 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -28,7 +28,7 @@ from synapse.api.room_versions import RoomVersions from synapse.rest.client.v1 import login, logout, profile, room from synapse.rest.client.v2_alpha import devices, sync -from synapse.types import JsonDict +from synapse.types import JsonDict, UserID from tests import unittest from tests.server import FakeSite, make_request @@ -467,6 +467,8 @@ class UsersListTestCase(unittest.HomeserverTestCase): url = "/_synapse/admin/v2/users" def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + self.admin_user = self.register_user("admin", "pass", admin=True) self.admin_user_tok = self.login("admin", "pass") @@ -634,6 +636,26 @@ def test_invalid_parameter(self): self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) + # unkown order_by + channel = self.make_request( + "GET", + self.url + "?order_by=bar", + access_token=self.admin_user_tok, + ) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) + + # invalid search order + channel = self.make_request( + "GET", + self.url + "?dir=bar", + access_token=self.admin_user_tok, + ) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) + def test_limit(self): """ Testing list of users with limit @@ -759,6 +781,103 @@ def test_next_token(self): self.assertEqual(len(channel.json_body["users"]), 1) self.assertNotIn("next_token", channel.json_body) + def test_order_by(self): + """ + Testing order list with parameter `order_by` + """ + + user1 = self.register_user("user1", "pass1", admin=False, displayname="Name Z") + user2 = self.register_user("user2", "pass2", admin=False, displayname="Name Y") + + # Modify user + self.get_success(self.store.set_user_deactivated_status(user1, True)) + self.get_success(self.store.set_shadow_banned(UserID.from_string(user1), True)) + + # Set avatar URL to all users, that no user has a NULL value to avoid + # different sort order between SQlite and PostreSQL + self.get_success(self.store.set_profile_avatar_url("user1", "mxc://url3")) + self.get_success(self.store.set_profile_avatar_url("user2", "mxc://url2")) + self.get_success(self.store.set_profile_avatar_url("admin", "mxc://url1")) + + # order by default (name) + self._order_test([self.admin_user, user1, user2], None) + self._order_test([self.admin_user, user1, user2], None, "f") + self._order_test([user2, user1, self.admin_user], None, "b") + + # order by name + self._order_test([self.admin_user, user1, user2], "name") + self._order_test([self.admin_user, user1, user2], "name", "f") + self._order_test([user2, user1, self.admin_user], "name", "b") + + # order by displayname + self._order_test([user2, user1, self.admin_user], "displayname") + self._order_test([user2, user1, self.admin_user], "displayname", "f") + self._order_test([self.admin_user, user1, user2], "displayname", "b") + + # order by is_guest + # like sort by ascending name, as no guest user here + self._order_test([self.admin_user, user1, user2], "is_guest") + self._order_test([self.admin_user, user1, user2], "is_guest", "f") + self._order_test([self.admin_user, user1, user2], "is_guest", "b") + + # order by admin + self._order_test([user1, user2, self.admin_user], "admin") + self._order_test([user1, user2, self.admin_user], "admin", "f") + self._order_test([self.admin_user, user1, user2], "admin", "b") + + # order by deactivated + self._order_test([self.admin_user, user2, user1], "deactivated") + self._order_test([self.admin_user, user2, user1], "deactivated", "f") + self._order_test([user1, self.admin_user, user2], "deactivated", "b") + + # order by user_type + # like sort by ascending name, as no special user type here + self._order_test([self.admin_user, user1, user2], "user_type") + self._order_test([self.admin_user, user1, user2], "user_type", "f") + self._order_test([self.admin_user, user1, user2], "is_guest", "b") + + # order by shadow_banned + self._order_test([self.admin_user, user2, user1], "shadow_banned") + self._order_test([self.admin_user, user2, user1], "shadow_banned", "f") + self._order_test([user1, self.admin_user, user2], "shadow_banned", "b") + + # order by avatar_url + self._order_test([self.admin_user, user2, user1], "avatar_url") + self._order_test([self.admin_user, user2, user1], "avatar_url", "f") + self._order_test([user1, user2, self.admin_user], "avatar_url", "b") + + def _order_test( + self, + expected_user_list: List[str], + order_by: Optional[str], + dir: Optional[str] = None, + ): + """Request the list of users in a certain order. Assert that order is what + we expect + Args: + expected_user_list: The list of user_id in the order we expect to get + back from the server + order_by: The type of ordering to give the server + dir: The direction of ordering to give the server + """ + + url = self.url + "?deactivated=true&" + if order_by is not None: + url += "order_by=%s&" % (order_by,) + if dir is not None and dir in ("b", "f"): + url += "dir=%s" % (dir,) + channel = self.make_request( + "GET", + url.encode("ascii"), + access_token=self.admin_user_tok, + ) + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual(channel.json_body["total"], len(expected_user_list)) + + returned_order = [row["name"] for row in channel.json_body["users"]] + self.assertEqual(expected_user_list, returned_order) + self._check_fields(channel.json_body["users"]) + def _check_fields(self, content: JsonDict): """Checks that the expected user attributes are present in content Args: From 33548f37aa7858c4d9ce01bf3ec931cc3f08833a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 1 Apr 2021 17:08:21 +0100 Subject: [PATCH 009/619] Improve tracing for to device messages (#9686) --- changelog.d/9686.misc | 1 + .../sender/per_destination_queue.py | 8 ++++ synapse/handlers/devicemessage.py | 35 ++++++++------- synapse/handlers/sync.py | 18 +++++++- synapse/handlers/typing.py | 6 ++- synapse/logging/opentracing.py | 8 ++++ synapse/notifier.py | 45 ++++++++++++++++++- 7 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 changelog.d/9686.misc diff --git a/changelog.d/9686.misc b/changelog.d/9686.misc new file mode 100644 index 0000000000..bb2335acf9 --- /dev/null +++ b/changelog.d/9686.misc @@ -0,0 +1 @@ +Improve Jaeger tracing for `to_device` messages. diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index 89df9a619b..e9c8a9f20a 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -29,6 +29,7 @@ from synapse.events import EventBase from synapse.federation.units import Edu from synapse.handlers.presence import format_user_presence_state +from synapse.logging.opentracing import SynapseTags, set_tag from synapse.metrics import sent_transactions_counter from synapse.metrics.background_process_metrics import run_as_background_process from synapse.types import ReadReceipt @@ -557,6 +558,13 @@ async def _get_to_device_message_edus(self, limit: int) -> Tuple[List[Edu], int] contents, stream_id = await self._store.get_new_device_msgs_for_remote( self._destination, last_device_stream_id, to_device_stream_id, limit ) + for content in contents: + message_id = content.get("message_id") + if not message_id: + continue + + set_tag(SynapseTags.TO_DEVICE_MESSAGE_ID, message_id) + edus = [ Edu( origin=self._server_name, diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 5ee48be6ff..c971eeb4d2 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -21,10 +21,10 @@ from synapse.api.ratelimiting import Ratelimiter from synapse.logging.context import run_in_background from synapse.logging.opentracing import ( + SynapseTags, get_active_span_text_map, log_kv, set_tag, - start_active_span, ) from synapse.replication.http.devices import ReplicationUserDevicesResyncRestServlet from synapse.types import JsonDict, Requester, UserID, get_domain_from_id @@ -183,7 +183,10 @@ async def send_device_message( ) -> None: sender_user_id = requester.user.to_string() - set_tag("number_of_messages", len(messages)) + message_id = random_string(16) + set_tag(SynapseTags.TO_DEVICE_MESSAGE_ID, message_id) + + log_kv({"number_of_to_device_messages": len(messages)}) set_tag("sender", sender_user_id) local_messages = {} remote_messages = {} # type: Dict[str, Dict[str, Dict[str, JsonDict]]] @@ -205,32 +208,35 @@ async def send_device_message( "content": message_content, "type": message_type, "sender": sender_user_id, + "message_id": message_id, } for device_id, message_content in by_device.items() } if messages_by_device: local_messages[user_id] = messages_by_device + log_kv( + { + "user_id": user_id, + "device_id": list(messages_by_device), + } + ) else: destination = get_domain_from_id(user_id) remote_messages.setdefault(destination, {})[user_id] = by_device - message_id = random_string(16) - context = get_active_span_text_map() remote_edu_contents = {} for destination, messages in remote_messages.items(): - with start_active_span("to_device_for_user"): - set_tag("destination", destination) - remote_edu_contents[destination] = { - "messages": messages, - "sender": sender_user_id, - "type": message_type, - "message_id": message_id, - "org.matrix.opentracing_context": json_encoder.encode(context), - } + log_kv({"destination": destination}) + remote_edu_contents[destination] = { + "messages": messages, + "sender": sender_user_id, + "type": message_type, + "message_id": message_id, + "org.matrix.opentracing_context": json_encoder.encode(context), + } - log_kv({"local_messages": local_messages}) stream_id = await self.store.add_messages_to_device_inbox( local_messages, remote_edu_contents ) @@ -239,7 +245,6 @@ async def send_device_message( "to_device_key", stream_id, users=local_messages.keys() ) - log_kv({"remote_messages": remote_messages}) if self.federation_sender: for destination in remote_messages.keys(): # Enqueue a new federation transaction to send the new diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index ee607e6e65..7b356ba7e5 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -24,6 +24,7 @@ from synapse.api.filtering import FilterCollection from synapse.events import EventBase from synapse.logging.context import current_context +from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, start_active_span from synapse.push.clientformat import format_push_rules_for_user from synapse.storage.roommember import MemberSummary from synapse.storage.state import StateFilter @@ -340,7 +341,14 @@ async def current_sync_for_user( full_state: bool = False, ) -> SyncResult: """Get the sync for client needed to match what the server has now.""" - return await self.generate_sync_result(sync_config, since_token, full_state) + with start_active_span("current_sync_for_user"): + log_kv({"since_token": since_token}) + sync_result = await self.generate_sync_result( + sync_config, since_token, full_state + ) + + set_tag(SynapseTags.SYNC_RESULT, bool(sync_result)) + return sync_result async def push_rules_for_user(self, user: UserID) -> JsonDict: user_id = user.to_string() @@ -964,6 +972,7 @@ async def generate_sync_result( # to query up to a given point. # Always use the `now_token` in `SyncResultBuilder` now_token = self.event_sources.get_current_token() + log_kv({"now_token": now_token}) logger.debug( "Calculating sync response for %r between %s and %s", @@ -1225,6 +1234,13 @@ async def _generate_sync_entry_for_to_device( user_id, device_id, since_stream_id, now_token.to_device_key ) + for message in messages: + # We pop here as we shouldn't be sending the message ID down + # `/sync` + message_id = message.pop("message_id", None) + if message_id: + set_tag(SynapseTags.TO_DEVICE_MESSAGE_ID, message_id) + logger.debug( "Returning %d to-device messages between %d and %d (current token: %d)", len(messages), diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 096d199f4c..bb35af099d 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -19,7 +19,10 @@ from synapse.api.errors import AuthError, ShadowBanError, SynapseError from synapse.appservice import ApplicationService -from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.metrics.background_process_metrics import ( + run_as_background_process, + wrap_as_background_process, +) from synapse.replication.tcp.streams import TypingStream from synapse.types import JsonDict, Requester, UserID, get_domain_from_id from synapse.util.caches.stream_change_cache import StreamChangeCache @@ -86,6 +89,7 @@ def _reset(self) -> None: self._member_last_federation_poke = {} self.wheel_timer = WheelTimer(bucket_size=5000) + @wrap_as_background_process("typing._handle_timeouts") def _handle_timeouts(self) -> None: logger.debug("Checking for typing timeouts") diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index aa146e8bb8..b8081f197e 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -259,6 +259,14 @@ def report_span(self, span): logger = logging.getLogger(__name__) +class SynapseTags: + # The message ID of any to_device message processed + TO_DEVICE_MESSAGE_ID = "to_device.message_id" + + # Whether the sync response has new data to be returned to the client. + SYNC_RESULT = "sync.new_data" + + # Block everything by default # A regex which matches the server_names to expose traces for. # None means 'block everything'. diff --git a/synapse/notifier.py b/synapse/notifier.py index 1374aae490..d35c1f3f02 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -39,6 +39,7 @@ from synapse.events import EventBase from synapse.handlers.presence import format_user_presence_state from synapse.logging.context import PreserveLoggingContext +from synapse.logging.opentracing import log_kv, start_active_span from synapse.logging.utils import log_function from synapse.metrics import LaterGauge from synapse.streams.config import PaginationConfig @@ -136,6 +137,15 @@ def notify( self.last_notified_ms = time_now_ms noify_deferred = self.notify_deferred + log_kv( + { + "notify": self.user_id, + "stream": stream_key, + "stream_id": stream_id, + "listeners": self.count_listeners(), + } + ) + users_woken_by_stream_counter.labels(stream_key).inc() with PreserveLoggingContext(): @@ -404,6 +414,13 @@ def on_new_event( with Measure(self.clock, "on_new_event"): user_streams = set() + log_kv( + { + "waking_up_explicit_users": len(users), + "waking_up_explicit_rooms": len(rooms), + } + ) + for user in users: user_stream = self.user_to_user_stream.get(str(user)) if user_stream is not None: @@ -476,12 +493,34 @@ async def wait_for_events( (end_time - now) / 1000.0, self.hs.get_reactor(), ) - with PreserveLoggingContext(): - await listener.deferred + + with start_active_span("wait_for_events.deferred"): + log_kv( + { + "wait_for_events": "sleep", + "token": prev_token, + } + ) + + with PreserveLoggingContext(): + await listener.deferred + + log_kv( + { + "wait_for_events": "woken", + "token": user_stream.current_token, + } + ) current_token = user_stream.current_token result = await callback(prev_token, current_token) + log_kv( + { + "wait_for_events": "result", + "result": bool(result), + } + ) if result: break @@ -489,8 +528,10 @@ async def wait_for_events( # has happened between the old prev_token and the current_token prev_token = current_token except defer.TimeoutError: + log_kv({"wait_for_events": "timeout"}) break except defer.CancelledError: + log_kv({"wait_for_events": "cancelled"}) break if result is None: From 4609e58970a95768170ca8c8ab52abc1998b866c Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 2 Apr 2021 12:22:21 +0200 Subject: [PATCH 010/619] Fix version for bugbear (#9734) --- changelog.d/9734.misc | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9734.misc diff --git a/changelog.d/9734.misc b/changelog.d/9734.misc new file mode 100644 index 0000000000..20ed9a06a9 --- /dev/null +++ b/changelog.d/9734.misc @@ -0,0 +1 @@ +Pin flake8-bugbear's version. \ No newline at end of file diff --git a/setup.py b/setup.py index 1939a7b86b..29e9971dc1 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def exec_file(path_segments): "isort==5.7.0", "black==20.8b1", "flake8-comprehensions", - "flake8-bugbear", + "flake8-bugbear==21.3.2", "flake8", ] From e2b8a90897e137fd118768a3bf35b70642916eb7 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 5 Apr 2021 15:10:18 +0200 Subject: [PATCH 011/619] Update mypy configuration: `no_implicit_optional = True` (#9742) --- changelog.d/9742.misc | 1 + mypy.ini | 1 + synapse/handlers/account_validity.py | 7 +++++-- synapse/handlers/e2e_keys.py | 2 +- synapse/http/client.py | 2 +- synapse/notifier.py | 2 +- synapse/replication/tcp/redis.py | 2 +- synapse/storage/databases/main/group_server.py | 4 ++-- synapse/util/caches/deferred_cache.py | 4 +++- tests/rest/client/v2_alpha/test_auth.py | 7 +++++-- 10 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 changelog.d/9742.misc diff --git a/changelog.d/9742.misc b/changelog.d/9742.misc new file mode 100644 index 0000000000..681ab04df8 --- /dev/null +++ b/changelog.d/9742.misc @@ -0,0 +1 @@ +Start linting mypy with `no_implicit_optional`. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 3ae5d45787..32e6197409 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,6 +8,7 @@ show_traceback = True mypy_path = stubs warn_unreachable = True local_partial_types = True +no_implicit_optional = True # To find all folders that pass mypy you run: # diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index d781bb251d..bee1447c2e 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -18,7 +18,7 @@ import logging from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional from synapse.api.errors import StoreError, SynapseError from synapse.logging.context import make_deferred_yieldable @@ -241,7 +241,10 @@ async def renew_account(self, renewal_token: str) -> bool: return True async def renew_account_for_user( - self, user_id: str, expiration_ts: int = None, email_sent: bool = False + self, + user_id: str, + expiration_ts: Optional[int] = None, + email_sent: bool = False, ) -> int: """Renews the account attached to a given user by pushing back the expiration date by the current validity period in the server's diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 2ad9b6d930..739653a3fa 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -1008,7 +1008,7 @@ async def _process_other_signatures( return signature_list, failures async def _get_e2e_cross_signing_verify_key( - self, user_id: str, key_type: str, from_user_id: str = None + self, user_id: str, key_type: str, from_user_id: Optional[str] = None ) -> Tuple[JsonDict, str, VerifyKey]: """Fetch locally or remotely query for a cross-signing public key. diff --git a/synapse/http/client.py b/synapse/http/client.py index a0caba84e4..e691ba6d88 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -590,7 +590,7 @@ async def put_json( uri: str, json_body: Any, args: Optional[QueryParams] = None, - headers: RawHeaders = None, + headers: Optional[RawHeaders] = None, ) -> Any: """Puts some json to the given URI. diff --git a/synapse/notifier.py b/synapse/notifier.py index d35c1f3f02..c178db57e3 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -548,7 +548,7 @@ async def get_events_for( pagination_config: PaginationConfig, timeout: int, is_guest: bool = False, - explicit_room_id: str = None, + explicit_room_id: Optional[str] = None, ) -> EventStreamResult: """For the given user and rooms, return any new events for them. If there are no new events wait for up to `timeout` milliseconds for any diff --git a/synapse/replication/tcp/redis.py b/synapse/replication/tcp/redis.py index 2f4d407f94..98bdeb0ec6 100644 --- a/synapse/replication/tcp/redis.py +++ b/synapse/replication/tcp/redis.py @@ -60,7 +60,7 @@ class ConstantProperty(Generic[T, V]): constant = attr.ib() # type: V - def __get__(self, obj: Optional[T], objtype: Type[T] = None) -> V: + def __get__(self, obj: Optional[T], objtype: Optional[Type[T]] = None) -> V: return self.constant def __set__(self, obj: Optional[T], value: V): diff --git a/synapse/storage/databases/main/group_server.py b/synapse/storage/databases/main/group_server.py index ac07e0197b..8f462dfc31 100644 --- a/synapse/storage/databases/main/group_server.py +++ b/synapse/storage/databases/main/group_server.py @@ -1027,8 +1027,8 @@ async def add_user_to_group( user_id: str, is_admin: bool = False, is_public: bool = True, - local_attestation: dict = None, - remote_attestation: dict = None, + local_attestation: Optional[dict] = None, + remote_attestation: Optional[dict] = None, ) -> None: """Add a user to the group server. diff --git a/synapse/util/caches/deferred_cache.py b/synapse/util/caches/deferred_cache.py index 1adc92eb90..dd392cf694 100644 --- a/synapse/util/caches/deferred_cache.py +++ b/synapse/util/caches/deferred_cache.py @@ -283,7 +283,9 @@ def eb(_fail): # we return a new Deferred which will be called before any subsequent observers. return observable.observe() - def prefill(self, key: KT, value: VT, callback: Callable[[], None] = None): + def prefill( + self, key: KT, value: VT, callback: Optional[Callable[[], None]] = None + ): callbacks = [callback] if callback else [] self.cache.set(key, value, callbacks=callbacks) diff --git a/tests/rest/client/v2_alpha/test_auth.py b/tests/rest/client/v2_alpha/test_auth.py index 9734a2159a..ed433d9333 100644 --- a/tests/rest/client/v2_alpha/test_auth.py +++ b/tests/rest/client/v2_alpha/test_auth.py @@ -13,7 +13,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union +from typing import Optional, Union from twisted.internet.defer import succeed @@ -74,7 +74,10 @@ def register(self, expected_response: int, body: JsonDict) -> FakeChannel: return channel def recaptcha( - self, session: str, expected_post_response: int, post_session: str = None + self, + session: str, + expected_post_response: int, + post_session: Optional[str] = None, ) -> None: """Get and respond to a fallback recaptcha. Returns the second request.""" if post_session is None: From e7b769aea14495fec61386f5d42098843eb20525 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 6 Apr 2021 07:21:02 -0400 Subject: [PATCH 012/619] Convert storage test cases to HomeserverTestCase. (#9736) --- changelog.d/9736.misc | 1 + tests/storage/test_devices.py | 80 +++++-------- tests/storage/test_directory.py | 44 ++----- tests/storage/test_end_to_end_keys.py | 59 ++++----- tests/storage/test_event_push_actions.py | 133 ++++++++------------- tests/storage/test_profile.py | 35 ++---- tests/storage/test_redaction.py | 12 +- tests/storage/test_registration.py | 108 +++++++---------- tests/storage/test_room.py | 61 +++------- tests/storage/test_state.py | 145 +++++++---------------- tests/storage/test_user_directory.py | 86 +++++--------- 11 files changed, 265 insertions(+), 499 deletions(-) create mode 100644 changelog.d/9736.misc diff --git a/changelog.d/9736.misc b/changelog.d/9736.misc new file mode 100644 index 0000000000..1e445e4344 --- /dev/null +++ b/changelog.d/9736.misc @@ -0,0 +1 @@ +Convert various testcases to `HomeserverTestCase`. diff --git a/tests/storage/test_devices.py b/tests/storage/test_devices.py index dabc1c5f09..ef4cf8d0f1 100644 --- a/tests/storage/test_devices.py +++ b/tests/storage/test_devices.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2016 OpenMarket Ltd +# Copyright 2016-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,32 +13,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer - import synapse.api.errors -import tests.unittest -import tests.utils - - -class DeviceStoreTestCase(tests.unittest.TestCase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.store = None # type: synapse.storage.DataStore +from tests.unittest import HomeserverTestCase - @defer.inlineCallbacks - def setUp(self): - hs = yield tests.utils.setup_test_homeserver(self.addCleanup) +class DeviceStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - @defer.inlineCallbacks def test_store_new_device(self): - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id", "device_id", "display_name") ) - res = yield defer.ensureDeferred(self.store.get_device("user_id", "device_id")) + res = self.get_success(self.store.get_device("user_id", "device_id")) self.assertDictContainsSubset( { "user_id": "user_id", @@ -48,19 +37,18 @@ def test_store_new_device(self): res, ) - @defer.inlineCallbacks def test_get_devices_by_user(self): - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id", "device1", "display_name 1") ) - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id", "device2", "display_name 2") ) - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id2", "device3", "display_name 3") ) - res = yield defer.ensureDeferred(self.store.get_devices_by_user("user_id")) + res = self.get_success(self.store.get_devices_by_user("user_id")) self.assertEqual(2, len(res.keys())) self.assertDictContainsSubset( { @@ -79,43 +67,41 @@ def test_get_devices_by_user(self): res["device2"], ) - @defer.inlineCallbacks def test_count_devices_by_users(self): - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id", "device1", "display_name 1") ) - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id", "device2", "display_name 2") ) - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id2", "device3", "display_name 3") ) - res = yield defer.ensureDeferred(self.store.count_devices_by_users()) + res = self.get_success(self.store.count_devices_by_users()) self.assertEqual(0, res) - res = yield defer.ensureDeferred(self.store.count_devices_by_users(["unknown"])) + res = self.get_success(self.store.count_devices_by_users(["unknown"])) self.assertEqual(0, res) - res = yield defer.ensureDeferred(self.store.count_devices_by_users(["user_id"])) + res = self.get_success(self.store.count_devices_by_users(["user_id"])) self.assertEqual(2, res) - res = yield defer.ensureDeferred( + res = self.get_success( self.store.count_devices_by_users(["user_id", "user_id2"]) ) self.assertEqual(3, res) - @defer.inlineCallbacks def test_get_device_updates_by_remote(self): device_ids = ["device_id1", "device_id2"] # Add two device updates with a single stream_id - yield defer.ensureDeferred( + self.get_success( self.store.add_device_change_to_streams("user_id", device_ids, ["somehost"]) ) # Get all device updates ever meant for this remote - now_stream_id, device_updates = yield defer.ensureDeferred( + now_stream_id, device_updates = self.get_success( self.store.get_device_updates_by_remote("somehost", -1, limit=100) ) @@ -131,37 +117,35 @@ def _check_devices_in_updates(self, expected_device_ids, device_updates): } self.assertEqual(received_device_ids, set(expected_device_ids)) - @defer.inlineCallbacks def test_update_device(self): - yield defer.ensureDeferred( + self.get_success( self.store.store_device("user_id", "device_id", "display_name 1") ) - res = yield defer.ensureDeferred(self.store.get_device("user_id", "device_id")) + res = self.get_success(self.store.get_device("user_id", "device_id")) self.assertEqual("display_name 1", res["display_name"]) # do a no-op first - yield defer.ensureDeferred(self.store.update_device("user_id", "device_id")) - res = yield defer.ensureDeferred(self.store.get_device("user_id", "device_id")) + self.get_success(self.store.update_device("user_id", "device_id")) + res = self.get_success(self.store.get_device("user_id", "device_id")) self.assertEqual("display_name 1", res["display_name"]) # do the update - yield defer.ensureDeferred( + self.get_success( self.store.update_device( "user_id", "device_id", new_display_name="display_name 2" ) ) # check it worked - res = yield defer.ensureDeferred(self.store.get_device("user_id", "device_id")) + res = self.get_success(self.store.get_device("user_id", "device_id")) self.assertEqual("display_name 2", res["display_name"]) - @defer.inlineCallbacks def test_update_unknown_device(self): - with self.assertRaises(synapse.api.errors.StoreError) as cm: - yield defer.ensureDeferred( - self.store.update_device( - "user_id", "unknown_device_id", new_display_name="display_name 2" - ) - ) - self.assertEqual(404, cm.exception.code) + exc = self.get_failure( + self.store.update_device( + "user_id", "unknown_device_id", new_display_name="display_name 2" + ), + synapse.api.errors.StoreError, + ) + self.assertEqual(404, exc.value.code) diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py index da93ca3980..0db233fd68 100644 --- a/tests/storage/test_directory.py +++ b/tests/storage/test_directory.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,28 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from twisted.internet import defer - from synapse.types import RoomAlias, RoomID -from tests import unittest -from tests.utils import setup_test_homeserver +from tests.unittest import HomeserverTestCase -class DirectoryStoreTestCase(unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield setup_test_homeserver(self.addCleanup) - +class DirectoryStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() self.room = RoomID.from_string("!abcde:test") self.alias = RoomAlias.from_string("#my-room:test") - @defer.inlineCallbacks def test_room_to_alias(self): - yield defer.ensureDeferred( + self.get_success( self.store.create_room_alias_association( room_alias=self.alias, room_id=self.room.to_string(), servers=["test"] ) @@ -42,16 +34,11 @@ def test_room_to_alias(self): self.assertEquals( ["#my-room:test"], - ( - yield defer.ensureDeferred( - self.store.get_aliases_for_room(self.room.to_string()) - ) - ), + (self.get_success(self.store.get_aliases_for_room(self.room.to_string()))), ) - @defer.inlineCallbacks def test_alias_to_room(self): - yield defer.ensureDeferred( + self.get_success( self.store.create_room_alias_association( room_alias=self.alias, room_id=self.room.to_string(), servers=["test"] ) @@ -59,28 +46,19 @@ def test_alias_to_room(self): self.assertObjectHasAttributes( {"room_id": self.room.to_string(), "servers": ["test"]}, - ( - yield defer.ensureDeferred( - self.store.get_association_from_room_alias(self.alias) - ) - ), + (self.get_success(self.store.get_association_from_room_alias(self.alias))), ) - @defer.inlineCallbacks def test_delete_alias(self): - yield defer.ensureDeferred( + self.get_success( self.store.create_room_alias_association( room_alias=self.alias, room_id=self.room.to_string(), servers=["test"] ) ) - room_id = yield defer.ensureDeferred(self.store.delete_room_alias(self.alias)) + room_id = self.get_success(self.store.delete_room_alias(self.alias)) self.assertEqual(self.room.to_string(), room_id) self.assertIsNone( - ( - yield defer.ensureDeferred( - self.store.get_association_from_room_alias(self.alias) - ) - ) + (self.get_success(self.store.get_association_from_room_alias(self.alias))) ) diff --git a/tests/storage/test_end_to_end_keys.py b/tests/storage/test_end_to_end_keys.py index 3fc4bb13b6..1e54b940fd 100644 --- a/tests/storage/test_end_to_end_keys.py +++ b/tests/storage/test_end_to_end_keys.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2016 OpenMarket Ltd +# Copyright 2016-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,30 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer +from tests.unittest import HomeserverTestCase -import tests.unittest -import tests.utils - -class EndToEndKeyStoreTestCase(tests.unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield tests.utils.setup_test_homeserver(self.addCleanup) +class EndToEndKeyStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - @defer.inlineCallbacks def test_key_without_device_name(self): now = 1470174257070 json = {"key": "value"} - yield defer.ensureDeferred(self.store.store_device("user", "device", None)) + self.get_success(self.store.store_device("user", "device", None)) - yield defer.ensureDeferred( - self.store.set_e2e_device_keys("user", "device", now, json) - ) + self.get_success(self.store.set_e2e_device_keys("user", "device", now, json)) - res = yield defer.ensureDeferred( + res = self.get_success( self.store.get_e2e_device_keys_for_cs_api((("user", "device"),)) ) self.assertIn("user", res) @@ -44,38 +36,32 @@ def test_key_without_device_name(self): dev = res["user"]["device"] self.assertDictContainsSubset(json, dev) - @defer.inlineCallbacks def test_reupload_key(self): now = 1470174257070 json = {"key": "value"} - yield defer.ensureDeferred(self.store.store_device("user", "device", None)) + self.get_success(self.store.store_device("user", "device", None)) - changed = yield defer.ensureDeferred( + changed = self.get_success( self.store.set_e2e_device_keys("user", "device", now, json) ) self.assertTrue(changed) # If we try to upload the same key then we should be told nothing # changed - changed = yield defer.ensureDeferred( + changed = self.get_success( self.store.set_e2e_device_keys("user", "device", now, json) ) self.assertFalse(changed) - @defer.inlineCallbacks def test_get_key_with_device_name(self): now = 1470174257070 json = {"key": "value"} - yield defer.ensureDeferred( - self.store.set_e2e_device_keys("user", "device", now, json) - ) - yield defer.ensureDeferred( - self.store.store_device("user", "device", "display_name") - ) + self.get_success(self.store.set_e2e_device_keys("user", "device", now, json)) + self.get_success(self.store.store_device("user", "device", "display_name")) - res = yield defer.ensureDeferred( + res = self.get_success( self.store.get_e2e_device_keys_for_cs_api((("user", "device"),)) ) self.assertIn("user", res) @@ -85,29 +71,28 @@ def test_get_key_with_device_name(self): {"key": "value", "unsigned": {"device_display_name": "display_name"}}, dev ) - @defer.inlineCallbacks def test_multiple_devices(self): now = 1470174257070 - yield defer.ensureDeferred(self.store.store_device("user1", "device1", None)) - yield defer.ensureDeferred(self.store.store_device("user1", "device2", None)) - yield defer.ensureDeferred(self.store.store_device("user2", "device1", None)) - yield defer.ensureDeferred(self.store.store_device("user2", "device2", None)) + self.get_success(self.store.store_device("user1", "device1", None)) + self.get_success(self.store.store_device("user1", "device2", None)) + self.get_success(self.store.store_device("user2", "device1", None)) + self.get_success(self.store.store_device("user2", "device2", None)) - yield defer.ensureDeferred( + self.get_success( self.store.set_e2e_device_keys("user1", "device1", now, {"key": "json11"}) ) - yield defer.ensureDeferred( + self.get_success( self.store.set_e2e_device_keys("user1", "device2", now, {"key": "json12"}) ) - yield defer.ensureDeferred( + self.get_success( self.store.set_e2e_device_keys("user2", "device1", now, {"key": "json21"}) ) - yield defer.ensureDeferred( + self.get_success( self.store.set_e2e_device_keys("user2", "device2", now, {"key": "json22"}) ) - res = yield defer.ensureDeferred( + res = self.get_success( self.store.get_e2e_device_keys_for_cs_api( (("user1", "device1"), ("user2", "device2")) ) diff --git a/tests/storage/test_event_push_actions.py b/tests/storage/test_event_push_actions.py index 485f1ee033..239f7c9faf 100644 --- a/tests/storage/test_event_push_actions.py +++ b/tests/storage/test_event_push_actions.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2016 OpenMarket Ltd +# Copyright 2016-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,10 +15,7 @@ from mock import Mock -from twisted.internet import defer - -import tests.unittest -import tests.utils +from tests.unittest import HomeserverTestCase USER_ID = "@user:example.com" @@ -30,37 +27,31 @@ ] -class EventPushActionsStoreTestCase(tests.unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield tests.utils.setup_test_homeserver(self.addCleanup) +class EventPushActionsStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() self.persist_events_store = hs.get_datastores().persist_events - @defer.inlineCallbacks def test_get_unread_push_actions_for_user_in_range_for_http(self): - yield defer.ensureDeferred( + self.get_success( self.store.get_unread_push_actions_for_user_in_range_for_http( USER_ID, 0, 1000, 20 ) ) - @defer.inlineCallbacks def test_get_unread_push_actions_for_user_in_range_for_email(self): - yield defer.ensureDeferred( + self.get_success( self.store.get_unread_push_actions_for_user_in_range_for_email( USER_ID, 0, 1000, 20 ) ) - @defer.inlineCallbacks def test_count_aggregation(self): room_id = "!foo:example.com" user_id = "@user1235:example.com" - @defer.inlineCallbacks def _assert_counts(noitf_count, highlight_count): - counts = yield defer.ensureDeferred( + counts = self.get_success( self.store.db_pool.runInteraction( "", self.store._get_unread_counts_by_pos_txn, room_id, user_id, 0 ) @@ -74,7 +65,6 @@ def _assert_counts(noitf_count, highlight_count): }, ) - @defer.inlineCallbacks def _inject_actions(stream, action): event = Mock() event.room_id = room_id @@ -82,14 +72,14 @@ def _inject_actions(stream, action): event.internal_metadata.stream_ordering = stream event.depth = stream - yield defer.ensureDeferred( + self.get_success( self.store.add_push_actions_to_staging( event.event_id, {user_id: action}, False, ) ) - yield defer.ensureDeferred( + self.get_success( self.store.db_pool.runInteraction( "", self.persist_events_store._set_push_actions_for_event_and_users_txn, @@ -99,14 +89,14 @@ def _inject_actions(stream, action): ) def _rotate(stream): - return defer.ensureDeferred( + self.get_success( self.store.db_pool.runInteraction( "", self.store._rotate_notifs_before_txn, stream ) ) def _mark_read(stream, depth): - return defer.ensureDeferred( + self.get_success( self.store.db_pool.runInteraction( "", self.store._remove_old_push_actions_before_txn, @@ -116,49 +106,48 @@ def _mark_read(stream, depth): ) ) - yield _assert_counts(0, 0) - yield _inject_actions(1, PlAIN_NOTIF) - yield _assert_counts(1, 0) - yield _rotate(2) - yield _assert_counts(1, 0) + _assert_counts(0, 0) + _inject_actions(1, PlAIN_NOTIF) + _assert_counts(1, 0) + _rotate(2) + _assert_counts(1, 0) - yield _inject_actions(3, PlAIN_NOTIF) - yield _assert_counts(2, 0) - yield _rotate(4) - yield _assert_counts(2, 0) + _inject_actions(3, PlAIN_NOTIF) + _assert_counts(2, 0) + _rotate(4) + _assert_counts(2, 0) - yield _inject_actions(5, PlAIN_NOTIF) - yield _mark_read(3, 3) - yield _assert_counts(1, 0) + _inject_actions(5, PlAIN_NOTIF) + _mark_read(3, 3) + _assert_counts(1, 0) - yield _mark_read(5, 5) - yield _assert_counts(0, 0) + _mark_read(5, 5) + _assert_counts(0, 0) - yield _inject_actions(6, PlAIN_NOTIF) - yield _rotate(7) + _inject_actions(6, PlAIN_NOTIF) + _rotate(7) - yield defer.ensureDeferred( + self.get_success( self.store.db_pool.simple_delete( table="event_push_actions", keyvalues={"1": 1}, desc="" ) ) - yield _assert_counts(1, 0) + _assert_counts(1, 0) - yield _mark_read(7, 7) - yield _assert_counts(0, 0) + _mark_read(7, 7) + _assert_counts(0, 0) - yield _inject_actions(8, HIGHLIGHT) - yield _assert_counts(1, 1) - yield _rotate(9) - yield _assert_counts(1, 1) - yield _rotate(10) - yield _assert_counts(1, 1) + _inject_actions(8, HIGHLIGHT) + _assert_counts(1, 1) + _rotate(9) + _assert_counts(1, 1) + _rotate(10) + _assert_counts(1, 1) - @defer.inlineCallbacks def test_find_first_stream_ordering_after_ts(self): def add_event(so, ts): - return defer.ensureDeferred( + self.get_success( self.store.db_pool.simple_insert( "events", { @@ -177,24 +166,16 @@ def add_event(so, ts): ) # start with the base case where there are no events in the table - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(11) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(11)) self.assertEqual(r, 0) # now with one event - yield add_event(2, 10) - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(9) - ) + add_event(2, 10) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(9)) self.assertEqual(r, 2) - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(10) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(10)) self.assertEqual(r, 2) - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(11) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(11)) self.assertEqual(r, 3) # add a bunch of dummy events to the events table @@ -205,39 +186,27 @@ def add_event(so, ts): (10, 130), (20, 140), ): - yield add_event(stream_ordering, ts) + add_event(stream_ordering, ts) - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(110) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(110)) self.assertEqual(r, 3, "First event after 110ms should be 3, was %i" % r) # 4 and 5 are both after 120: we want 4 rather than 5 - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(120) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(120)) self.assertEqual(r, 4, "First event after 120ms should be 4, was %i" % r) - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(129) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(129)) self.assertEqual(r, 10, "First event after 129ms should be 10, was %i" % r) # check we can get the last event - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(140) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(140)) self.assertEqual(r, 20, "First event after 14ms should be 20, was %i" % r) # off the end - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(160) - ) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(160)) self.assertEqual(r, 21) # check we can find an event at ordering zero - yield add_event(0, 5) - r = yield defer.ensureDeferred( - self.store.find_first_stream_ordering_after_ts(1) - ) + add_event(0, 5) + r = self.get_success(self.store.find_first_stream_ordering_after_ts(1)) self.assertEqual(r, 0) diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py index ea63bd56b4..d18ceb41a9 100644 --- a/tests/storage/test_profile.py +++ b/tests/storage/test_profile.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,59 +13,50 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from twisted.internet import defer - from synapse.types import UserID from tests import unittest -from tests.utils import setup_test_homeserver - -class ProfileStoreTestCase(unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield setup_test_homeserver(self.addCleanup) +class ProfileStoreTestCase(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() self.u_frank = UserID.from_string("@frank:test") - @defer.inlineCallbacks def test_displayname(self): - yield defer.ensureDeferred(self.store.create_profile(self.u_frank.localpart)) + self.get_success(self.store.create_profile(self.u_frank.localpart)) - yield defer.ensureDeferred( + self.get_success( self.store.set_profile_displayname(self.u_frank.localpart, "Frank") ) self.assertEquals( "Frank", ( - yield defer.ensureDeferred( + self.get_success( self.store.get_profile_displayname(self.u_frank.localpart) ) ), ) # test set to None - yield defer.ensureDeferred( + self.get_success( self.store.set_profile_displayname(self.u_frank.localpart, None) ) self.assertIsNone( ( - yield defer.ensureDeferred( + self.get_success( self.store.get_profile_displayname(self.u_frank.localpart) ) ) ) - @defer.inlineCallbacks def test_avatar_url(self): - yield defer.ensureDeferred(self.store.create_profile(self.u_frank.localpart)) + self.get_success(self.store.create_profile(self.u_frank.localpart)) - yield defer.ensureDeferred( + self.get_success( self.store.set_profile_avatar_url( self.u_frank.localpart, "http://my.site/here" ) @@ -74,20 +65,20 @@ def test_avatar_url(self): self.assertEquals( "http://my.site/here", ( - yield defer.ensureDeferred( + self.get_success( self.store.get_profile_avatar_url(self.u_frank.localpart) ) ), ) # test set to None - yield defer.ensureDeferred( + self.get_success( self.store.set_profile_avatar_url(self.u_frank.localpart, None) ) self.assertIsNone( ( - yield defer.ensureDeferred( + self.get_success( self.store.get_profile_avatar_url(self.u_frank.localpart) ) ) diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index b2a0e60856..2622207639 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,8 +15,6 @@ from canonicaljson import json -from twisted.internet import defer - from synapse.api.constants import EventTypes, Membership from synapse.api.room_versions import RoomVersions from synapse.types import RoomID, UserID @@ -230,10 +227,9 @@ def __init__(self, base_builder, event_id): self._base_builder = base_builder self._event_id = event_id - @defer.inlineCallbacks - def build(self, prev_event_ids, auth_event_ids): - built_event = yield defer.ensureDeferred( - self._base_builder.build(prev_event_ids, auth_event_ids) + async def build(self, prev_event_ids, auth_event_ids): + built_event = await self._base_builder.build( + prev_event_ids, auth_event_ids ) built_event._event_id = self._event_id diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index 4eb41c46e8..c82cf15bc2 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,21 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from twisted.internet import defer - from synapse.api.constants import UserTypes from synapse.api.errors import ThreepidValidationError -from tests import unittest -from tests.utils import setup_test_homeserver - +from tests.unittest import HomeserverTestCase -class RegistrationStoreTestCase(unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield setup_test_homeserver(self.addCleanup) +class RegistrationStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() self.user_id = "@my-user:test" @@ -35,9 +28,8 @@ def setUp(self): self.pwhash = "{xx1}123456789" self.device_id = "akgjhdjklgshg" - @defer.inlineCallbacks def test_register(self): - yield defer.ensureDeferred(self.store.register_user(self.user_id, self.pwhash)) + self.get_success(self.store.register_user(self.user_id, self.pwhash)) self.assertEquals( { @@ -49,93 +41,81 @@ def test_register(self): "consent_version": None, "consent_server_notice_sent": None, "appservice_id": None, - "creation_ts": 1000, + "creation_ts": 0, "user_type": None, "deactivated": 0, "shadow_banned": 0, }, - (yield defer.ensureDeferred(self.store.get_user_by_id(self.user_id))), + (self.get_success(self.store.get_user_by_id(self.user_id))), ) - @defer.inlineCallbacks def test_add_tokens(self): - yield defer.ensureDeferred(self.store.register_user(self.user_id, self.pwhash)) - yield defer.ensureDeferred( + self.get_success(self.store.register_user(self.user_id, self.pwhash)) + self.get_success( self.store.add_access_token_to_user( self.user_id, self.tokens[1], self.device_id, valid_until_ms=None ) ) - result = yield defer.ensureDeferred( - self.store.get_user_by_access_token(self.tokens[1]) - ) + result = self.get_success(self.store.get_user_by_access_token(self.tokens[1])) self.assertEqual(result.user_id, self.user_id) self.assertEqual(result.device_id, self.device_id) self.assertIsNotNone(result.token_id) - @defer.inlineCallbacks def test_user_delete_access_tokens(self): # add some tokens - yield defer.ensureDeferred(self.store.register_user(self.user_id, self.pwhash)) - yield defer.ensureDeferred( + self.get_success(self.store.register_user(self.user_id, self.pwhash)) + self.get_success( self.store.add_access_token_to_user( self.user_id, self.tokens[0], device_id=None, valid_until_ms=None ) ) - yield defer.ensureDeferred( + self.get_success( self.store.add_access_token_to_user( self.user_id, self.tokens[1], self.device_id, valid_until_ms=None ) ) # now delete some - yield defer.ensureDeferred( + self.get_success( self.store.user_delete_access_tokens(self.user_id, device_id=self.device_id) ) # check they were deleted - user = yield defer.ensureDeferred( - self.store.get_user_by_access_token(self.tokens[1]) - ) + user = self.get_success(self.store.get_user_by_access_token(self.tokens[1])) self.assertIsNone(user, "access token was not deleted by device_id") # check the one not associated with the device was not deleted - user = yield defer.ensureDeferred( - self.store.get_user_by_access_token(self.tokens[0]) - ) + user = self.get_success(self.store.get_user_by_access_token(self.tokens[0])) self.assertEqual(self.user_id, user.user_id) # now delete the rest - yield defer.ensureDeferred(self.store.user_delete_access_tokens(self.user_id)) + self.get_success(self.store.user_delete_access_tokens(self.user_id)) - user = yield defer.ensureDeferred( - self.store.get_user_by_access_token(self.tokens[0]) - ) + user = self.get_success(self.store.get_user_by_access_token(self.tokens[0])) self.assertIsNone(user, "access token was not deleted without device_id") - @defer.inlineCallbacks def test_is_support_user(self): TEST_USER = "@test:test" SUPPORT_USER = "@support:test" - res = yield defer.ensureDeferred(self.store.is_support_user(None)) + res = self.get_success(self.store.is_support_user(None)) self.assertFalse(res) - yield defer.ensureDeferred( + self.get_success( self.store.register_user(user_id=TEST_USER, password_hash=None) ) - res = yield defer.ensureDeferred(self.store.is_support_user(TEST_USER)) + res = self.get_success(self.store.is_support_user(TEST_USER)) self.assertFalse(res) - yield defer.ensureDeferred( + self.get_success( self.store.register_user( user_id=SUPPORT_USER, password_hash=None, user_type=UserTypes.SUPPORT ) ) - res = yield defer.ensureDeferred(self.store.is_support_user(SUPPORT_USER)) + res = self.get_success(self.store.is_support_user(SUPPORT_USER)) self.assertTrue(res) - @defer.inlineCallbacks def test_3pid_inhibit_invalid_validation_session_error(self): """Tests that enabling the configuration option to inhibit 3PID errors on /requestToken also inhibits validation errors caused by an unknown session ID. @@ -143,30 +123,28 @@ def test_3pid_inhibit_invalid_validation_session_error(self): # Check that, with the config setting set to false (the default value), a # validation error is caused by the unknown session ID. - try: - yield defer.ensureDeferred( - self.store.validate_threepid_session( - "fake_sid", - "fake_client_secret", - "fake_token", - 0, - ) - ) - except ThreepidValidationError as e: - self.assertEquals(e.msg, "Unknown session_id", e) + e = self.get_failure( + self.store.validate_threepid_session( + "fake_sid", + "fake_client_secret", + "fake_token", + 0, + ), + ThreepidValidationError, + ) + self.assertEquals(e.value.msg, "Unknown session_id", e) # Set the config setting to true. self.store._ignore_unknown_session_error = True # Check that now the validation error is caused by the token not matching. - try: - yield defer.ensureDeferred( - self.store.validate_threepid_session( - "fake_sid", - "fake_client_secret", - "fake_token", - 0, - ) - ) - except ThreepidValidationError as e: - self.assertEquals(e.msg, "Validation token not found or has expired", e) + e = self.get_failure( + self.store.validate_threepid_session( + "fake_sid", + "fake_client_secret", + "fake_token", + 0, + ), + ThreepidValidationError, + ) + self.assertEquals(e.value.msg, "Validation token not found or has expired", e) diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index bc8400f240..0089d33c93 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2014-2016 OpenMarket Ltd +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,22 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from twisted.internet import defer - from synapse.api.constants import EventTypes from synapse.api.room_versions import RoomVersions from synapse.types import RoomAlias, RoomID, UserID -from tests import unittest -from tests.utils import setup_test_homeserver - +from tests.unittest import HomeserverTestCase -class RoomStoreTestCase(unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield setup_test_homeserver(self.addCleanup) +class RoomStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): # We can't test RoomStore on its own without the DirectoryStore, for # management of the 'room_aliases' table self.store = hs.get_datastore() @@ -37,7 +30,7 @@ def setUp(self): self.alias = RoomAlias.from_string("#a-room-name:test") self.u_creator = UserID.from_string("@creator:test") - yield defer.ensureDeferred( + self.get_success( self.store.store_room( self.room.to_string(), room_creator_user_id=self.u_creator.to_string(), @@ -46,7 +39,6 @@ def setUp(self): ) ) - @defer.inlineCallbacks def test_get_room(self): self.assertDictContainsSubset( { @@ -54,16 +46,12 @@ def test_get_room(self): "creator": self.u_creator.to_string(), "is_public": True, }, - (yield defer.ensureDeferred(self.store.get_room(self.room.to_string()))), + (self.get_success(self.store.get_room(self.room.to_string()))), ) - @defer.inlineCallbacks def test_get_room_unknown_room(self): - self.assertIsNone( - (yield defer.ensureDeferred(self.store.get_room("!uknown:test"))) - ) + self.assertIsNone((self.get_success(self.store.get_room("!uknown:test")))) - @defer.inlineCallbacks def test_get_room_with_stats(self): self.assertDictContainsSubset( { @@ -71,29 +59,17 @@ def test_get_room_with_stats(self): "creator": self.u_creator.to_string(), "public": True, }, - ( - yield defer.ensureDeferred( - self.store.get_room_with_stats(self.room.to_string()) - ) - ), + (self.get_success(self.store.get_room_with_stats(self.room.to_string()))), ) - @defer.inlineCallbacks def test_get_room_with_stats_unknown_room(self): self.assertIsNone( - ( - yield defer.ensureDeferred( - self.store.get_room_with_stats("!uknown:test") - ) - ), + (self.get_success(self.store.get_room_with_stats("!uknown:test"))), ) -class RoomEventsStoreTestCase(unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = setup_test_homeserver(self.addCleanup) - +class RoomEventsStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): # Room events need the full datastore, for persist_event() and # get_room_state() self.store = hs.get_datastore() @@ -102,7 +78,7 @@ def setUp(self): self.room = RoomID.from_string("!abcde:test") - yield defer.ensureDeferred( + self.get_success( self.store.store_room( self.room.to_string(), room_creator_user_id="@creator:text", @@ -111,23 +87,21 @@ def setUp(self): ) ) - @defer.inlineCallbacks def inject_room_event(self, **kwargs): - yield defer.ensureDeferred( + self.get_success( self.storage.persistence.persist_event( self.event_factory.create_event(room_id=self.room.to_string(), **kwargs) ) ) - @defer.inlineCallbacks def STALE_test_room_name(self): name = "A-Room-Name" - yield self.inject_room_event( + self.inject_room_event( etype=EventTypes.Name, name=name, content={"name": name}, depth=1 ) - state = yield defer.ensureDeferred( + state = self.get_success( self.store.get_current_state(room_id=self.room.to_string()) ) @@ -137,15 +111,14 @@ def STALE_test_room_name(self): state[0], ) - @defer.inlineCallbacks def STALE_test_room_topic(self): topic = "A place for things" - yield self.inject_room_event( + self.inject_room_event( etype=EventTypes.Topic, topic=topic, content={"topic": topic}, depth=1 ) - state = yield defer.ensureDeferred( + state = self.get_success( self.store.get_current_state(room_id=self.room.to_string()) ) diff --git a/tests/storage/test_state.py b/tests/storage/test_state.py index 2471f1267d..f06b452fa9 100644 --- a/tests/storage/test_state.py +++ b/tests/storage/test_state.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2018 New Vector Ltd +# Copyright 2018-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,24 +15,18 @@ import logging -from twisted.internet import defer - from synapse.api.constants import EventTypes, Membership from synapse.api.room_versions import RoomVersions from synapse.storage.state import StateFilter from synapse.types import RoomID, UserID -import tests.unittest -import tests.utils +from tests.unittest import HomeserverTestCase logger = logging.getLogger(__name__) -class StateStoreTestCase(tests.unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - hs = yield tests.utils.setup_test_homeserver(self.addCleanup) - +class StateStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() self.storage = hs.get_storage() self.state_datastore = self.storage.state.stores.state @@ -44,7 +38,7 @@ def setUp(self): self.room = RoomID.from_string("!abc123:test") - yield defer.ensureDeferred( + self.get_success( self.store.store_room( self.room.to_string(), room_creator_user_id="@creator:text", @@ -53,7 +47,6 @@ def setUp(self): ) ) - @defer.inlineCallbacks def inject_state_event(self, room, sender, typ, state_key, content): builder = self.event_builder_factory.for_room_version( RoomVersions.V1, @@ -66,13 +59,11 @@ def inject_state_event(self, room, sender, typ, state_key, content): }, ) - event, context = yield defer.ensureDeferred( + event, context = self.get_success( self.event_creation_handler.create_new_client_event(builder) ) - yield defer.ensureDeferred( - self.storage.persistence.persist_event(event, context) - ) + self.get_success(self.storage.persistence.persist_event(event, context)) return event @@ -82,16 +73,13 @@ def assertStateMapEqual(self, s1, s2): self.assertEqual(s1[t].event_id, s2[t].event_id) self.assertEqual(len(s1), len(s2)) - @defer.inlineCallbacks def test_get_state_groups_ids(self): - e1 = yield self.inject_state_event( - self.room, self.u_alice, EventTypes.Create, "", {} - ) - e2 = yield self.inject_state_event( + e1 = self.inject_state_event(self.room, self.u_alice, EventTypes.Create, "", {}) + e2 = self.inject_state_event( self.room, self.u_alice, EventTypes.Name, "", {"name": "test room"} ) - state_group_map = yield defer.ensureDeferred( + state_group_map = self.get_success( self.storage.state.get_state_groups_ids(self.room, [e2.event_id]) ) self.assertEqual(len(state_group_map), 1) @@ -101,16 +89,13 @@ def test_get_state_groups_ids(self): {(EventTypes.Create, ""): e1.event_id, (EventTypes.Name, ""): e2.event_id}, ) - @defer.inlineCallbacks def test_get_state_groups(self): - e1 = yield self.inject_state_event( - self.room, self.u_alice, EventTypes.Create, "", {} - ) - e2 = yield self.inject_state_event( + e1 = self.inject_state_event(self.room, self.u_alice, EventTypes.Create, "", {}) + e2 = self.inject_state_event( self.room, self.u_alice, EventTypes.Name, "", {"name": "test room"} ) - state_group_map = yield defer.ensureDeferred( + state_group_map = self.get_success( self.storage.state.get_state_groups(self.room, [e2.event_id]) ) self.assertEqual(len(state_group_map), 1) @@ -118,32 +103,29 @@ def test_get_state_groups(self): self.assertEqual({ev.event_id for ev in state_list}, {e1.event_id, e2.event_id}) - @defer.inlineCallbacks def test_get_state_for_event(self): # this defaults to a linear DAG as each new injection defaults to whatever # forward extremities are currently in the DB for this room. - e1 = yield self.inject_state_event( - self.room, self.u_alice, EventTypes.Create, "", {} - ) - e2 = yield self.inject_state_event( + e1 = self.inject_state_event(self.room, self.u_alice, EventTypes.Create, "", {}) + e2 = self.inject_state_event( self.room, self.u_alice, EventTypes.Name, "", {"name": "test room"} ) - e3 = yield self.inject_state_event( + e3 = self.inject_state_event( self.room, self.u_alice, EventTypes.Member, self.u_alice.to_string(), {"membership": Membership.JOIN}, ) - e4 = yield self.inject_state_event( + e4 = self.inject_state_event( self.room, self.u_bob, EventTypes.Member, self.u_bob.to_string(), {"membership": Membership.JOIN}, ) - e5 = yield self.inject_state_event( + e5 = self.inject_state_event( self.room, self.u_bob, EventTypes.Member, @@ -152,9 +134,7 @@ def test_get_state_for_event(self): ) # check we get the full state as of the final event - state = yield defer.ensureDeferred( - self.storage.state.get_state_for_event(e5.event_id) - ) + state = self.get_success(self.storage.state.get_state_for_event(e5.event_id)) self.assertIsNotNone(e4) @@ -170,7 +150,7 @@ def test_get_state_for_event(self): ) # check we can filter to the m.room.name event (with a '' state key) - state = yield defer.ensureDeferred( + state = self.get_success( self.storage.state.get_state_for_event( e5.event_id, StateFilter.from_types([(EventTypes.Name, "")]) ) @@ -179,7 +159,7 @@ def test_get_state_for_event(self): self.assertStateMapEqual({(e2.type, e2.state_key): e2}, state) # check we can filter to the m.room.name event (with a wildcard None state key) - state = yield defer.ensureDeferred( + state = self.get_success( self.storage.state.get_state_for_event( e5.event_id, StateFilter.from_types([(EventTypes.Name, None)]) ) @@ -188,7 +168,7 @@ def test_get_state_for_event(self): self.assertStateMapEqual({(e2.type, e2.state_key): e2}, state) # check we can grab the m.room.member events (with a wildcard None state key) - state = yield defer.ensureDeferred( + state = self.get_success( self.storage.state.get_state_for_event( e5.event_id, StateFilter.from_types([(EventTypes.Member, None)]) ) @@ -200,7 +180,7 @@ def test_get_state_for_event(self): # check we can grab a specific room member without filtering out the # other event types - state = yield defer.ensureDeferred( + state = self.get_success( self.storage.state.get_state_for_event( e5.event_id, state_filter=StateFilter( @@ -220,7 +200,7 @@ def test_get_state_for_event(self): ) # check that we can grab everything except members - state = yield defer.ensureDeferred( + state = self.get_success( self.storage.state.get_state_for_event( e5.event_id, state_filter=StateFilter( @@ -238,17 +218,14 @@ def test_get_state_for_event(self): ####################################################### room_id = self.room.to_string() - group_ids = yield defer.ensureDeferred( + group_ids = self.get_success( self.storage.state.get_state_groups_ids(room_id, [e5.event_id]) ) group = list(group_ids.keys())[0] # test _get_state_for_group_using_cache correctly filters out members # with types=[] - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_cache, group, state_filter=StateFilter( @@ -265,10 +242,7 @@ def test_get_state_for_event(self): state_dict, ) - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( @@ -281,10 +255,7 @@ def test_get_state_for_event(self): # test _get_state_for_group_using_cache correctly filters in members # with wildcard types - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_cache, group, state_filter=StateFilter( @@ -301,10 +272,7 @@ def test_get_state_for_event(self): state_dict, ) - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( @@ -324,10 +292,7 @@ def test_get_state_for_event(self): # test _get_state_for_group_using_cache correctly filters in members # with specific types - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_cache, group, state_filter=StateFilter( @@ -344,10 +309,7 @@ def test_get_state_for_event(self): state_dict, ) - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( @@ -360,10 +322,7 @@ def test_get_state_for_event(self): # test _get_state_for_group_using_cache correctly filters in members # with specific types - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( @@ -413,10 +372,7 @@ def test_get_state_for_event(self): # test _get_state_for_group_using_cache correctly filters out members # with types=[] room_id = self.room.to_string() - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_cache, group, state_filter=StateFilter( @@ -428,10 +384,7 @@ def test_get_state_for_event(self): self.assertDictEqual({(e1.type, e1.state_key): e1.event_id}, state_dict) room_id = self.room.to_string() - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( @@ -444,10 +397,7 @@ def test_get_state_for_event(self): # test _get_state_for_group_using_cache correctly filters in members # wildcard types - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_cache, group, state_filter=StateFilter( @@ -458,10 +408,7 @@ def test_get_state_for_event(self): self.assertEqual(is_all, False) self.assertDictEqual({(e1.type, e1.state_key): e1.event_id}, state_dict) - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( @@ -480,10 +427,7 @@ def test_get_state_for_event(self): # test _get_state_for_group_using_cache correctly filters in members # with specific types - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_cache, group, state_filter=StateFilter( @@ -494,10 +438,7 @@ def test_get_state_for_event(self): self.assertEqual(is_all, False) self.assertDictEqual({(e1.type, e1.state_key): e1.event_id}, state_dict) - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( @@ -510,10 +451,7 @@ def test_get_state_for_event(self): # test _get_state_for_group_using_cache correctly filters in members # with specific types - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_cache, group, state_filter=StateFilter( @@ -524,10 +462,7 @@ def test_get_state_for_event(self): self.assertEqual(is_all, False) self.assertDictEqual({}, state_dict) - ( - state_dict, - is_all, - ) = yield self.state_datastore._get_state_for_group_using_cache( + (state_dict, is_all,) = self.state_datastore._get_state_for_group_using_cache( self.state_datastore._state_group_members_cache, group, state_filter=StateFilter( diff --git a/tests/storage/test_user_directory.py b/tests/storage/test_user_directory.py index a6f63f4aaf..019c5b7b14 100644 --- a/tests/storage/test_user_directory.py +++ b/tests/storage/test_user_directory.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2018 New Vector Ltd +# Copyright 2018-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,10 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.internet import defer - -from tests import unittest -from tests.utils import setup_test_homeserver +from tests.unittest import HomeserverTestCase, override_config ALICE = "@alice:a" BOB = "@bob:b" @@ -25,73 +22,52 @@ BELA = "@somenickname:a" -class UserDirectoryStoreTestCase(unittest.TestCase): - @defer.inlineCallbacks - def setUp(self): - self.hs = yield setup_test_homeserver(self.addCleanup) - self.store = self.hs.get_datastore() +class UserDirectoryStoreTestCase(HomeserverTestCase): + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() # alice and bob are both in !room_id. bobby is not but shares # a homeserver with alice. - yield defer.ensureDeferred( - self.store.update_profile_in_user_dir(ALICE, "alice", None) - ) - yield defer.ensureDeferred( - self.store.update_profile_in_user_dir(BOB, "bob", None) - ) - yield defer.ensureDeferred( - self.store.update_profile_in_user_dir(BOBBY, "bobby", None) - ) - yield defer.ensureDeferred( - self.store.update_profile_in_user_dir(BELA, "Bela", None) - ) - yield defer.ensureDeferred( - self.store.add_users_in_public_rooms("!room:id", (ALICE, BOB)) - ) + self.get_success(self.store.update_profile_in_user_dir(ALICE, "alice", None)) + self.get_success(self.store.update_profile_in_user_dir(BOB, "bob", None)) + self.get_success(self.store.update_profile_in_user_dir(BOBBY, "bobby", None)) + self.get_success(self.store.update_profile_in_user_dir(BELA, "Bela", None)) + self.get_success(self.store.add_users_in_public_rooms("!room:id", (ALICE, BOB))) - @defer.inlineCallbacks def test_search_user_dir(self): # normally when alice searches the directory she should just find # bob because bobby doesn't share a room with her. - r = yield defer.ensureDeferred(self.store.search_user_dir(ALICE, "bob", 10)) + r = self.get_success(self.store.search_user_dir(ALICE, "bob", 10)) self.assertFalse(r["limited"]) self.assertEqual(1, len(r["results"])) self.assertDictEqual( r["results"][0], {"user_id": BOB, "display_name": "bob", "avatar_url": None} ) - @defer.inlineCallbacks + @override_config({"user_directory": {"search_all_users": True}}) def test_search_user_dir_all_users(self): - self.hs.config.user_directory_search_all_users = True - try: - r = yield defer.ensureDeferred(self.store.search_user_dir(ALICE, "bob", 10)) - self.assertFalse(r["limited"]) - self.assertEqual(2, len(r["results"])) - self.assertDictEqual( - r["results"][0], - {"user_id": BOB, "display_name": "bob", "avatar_url": None}, - ) - self.assertDictEqual( - r["results"][1], - {"user_id": BOBBY, "display_name": "bobby", "avatar_url": None}, - ) - finally: - self.hs.config.user_directory_search_all_users = False + r = self.get_success(self.store.search_user_dir(ALICE, "bob", 10)) + self.assertFalse(r["limited"]) + self.assertEqual(2, len(r["results"])) + self.assertDictEqual( + r["results"][0], + {"user_id": BOB, "display_name": "bob", "avatar_url": None}, + ) + self.assertDictEqual( + r["results"][1], + {"user_id": BOBBY, "display_name": "bobby", "avatar_url": None}, + ) - @defer.inlineCallbacks + @override_config({"user_directory": {"search_all_users": True}}) def test_search_user_dir_stop_words(self): """Tests that a user can look up another user by searching for the start if its display name even if that name happens to be a common English word that would usually be ignored in full text searches. """ - self.hs.config.user_directory_search_all_users = True - try: - r = yield defer.ensureDeferred(self.store.search_user_dir(ALICE, "be", 10)) - self.assertFalse(r["limited"]) - self.assertEqual(1, len(r["results"])) - self.assertDictEqual( - r["results"][0], - {"user_id": BELA, "display_name": "Bela", "avatar_url": None}, - ) - finally: - self.hs.config.user_directory_search_all_users = False + r = self.get_success(self.store.search_user_dir(ALICE, "be", 10)) + self.assertFalse(r["limited"]) + self.assertEqual(1, len(r["results"])) + self.assertDictEqual( + r["results"][0], + {"user_id": BELA, "display_name": "Bela", "avatar_url": None}, + ) From d959d28730ec6a0765ab72b10bcc96b1507233ac Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 6 Apr 2021 07:21:57 -0400 Subject: [PATCH 013/619] Add type hints to the federation handler and server. (#9743) --- changelog.d/9743.misc | 1 + synapse/federation/federation_server.py | 26 ++-- synapse/federation/transport/server.py | 4 +- synapse/handlers/federation.py | 161 ++++++++++++------------ 4 files changed, 97 insertions(+), 95 deletions(-) create mode 100644 changelog.d/9743.misc diff --git a/changelog.d/9743.misc b/changelog.d/9743.misc new file mode 100644 index 0000000000..c2f75c1df9 --- /dev/null +++ b/changelog.d/9743.misc @@ -0,0 +1 @@ +Add missing type hints to federation handler and server. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 71cb120ef7..b9f8d966a6 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -739,22 +739,20 @@ async def _handle_received_pdu(self, origin: str, pdu: EventBase) -> None: await self.handler.on_receive_pdu(origin, pdu, sent_to_us_directly=True) - def __str__(self): + def __str__(self) -> str: return "" % self.server_name async def exchange_third_party_invite( self, sender_user_id: str, target_user_id: str, room_id: str, signed: Dict - ): - ret = await self.handler.exchange_third_party_invite( + ) -> None: + await self.handler.exchange_third_party_invite( sender_user_id, target_user_id, room_id, signed ) - return ret - async def on_exchange_third_party_invite_request(self, event_dict: Dict): - ret = await self.handler.on_exchange_third_party_invite_request(event_dict) - return ret + async def on_exchange_third_party_invite_request(self, event_dict: Dict) -> None: + await self.handler.on_exchange_third_party_invite_request(event_dict) - async def check_server_matches_acl(self, server_name: str, room_id: str): + async def check_server_matches_acl(self, server_name: str, room_id: str) -> None: """Check if the given server is allowed by the server ACLs in the room Args: @@ -878,7 +876,7 @@ def __init__(self, hs: "HomeServer"): def register_edu_handler( self, edu_type: str, handler: Callable[[str, JsonDict], Awaitable[None]] - ): + ) -> None: """Sets the handler callable that will be used to handle an incoming federation EDU of the given type. @@ -897,7 +895,7 @@ def register_edu_handler( def register_query_handler( self, query_type: str, handler: Callable[[dict], Awaitable[JsonDict]] - ): + ) -> None: """Sets the handler callable that will be used to handle an incoming federation query of the given type. @@ -915,15 +913,17 @@ def register_query_handler( self.query_handlers[query_type] = handler - def register_instance_for_edu(self, edu_type: str, instance_name: str): + def register_instance_for_edu(self, edu_type: str, instance_name: str) -> None: """Register that the EDU handler is on a different instance than master.""" self._edu_type_to_instance[edu_type] = [instance_name] - def register_instances_for_edu(self, edu_type: str, instance_names: List[str]): + def register_instances_for_edu( + self, edu_type: str, instance_names: List[str] + ) -> None: """Register that the EDU handler is on multiple instances.""" self._edu_type_to_instance[edu_type] = instance_names - async def on_edu(self, edu_type: str, origin: str, content: dict): + async def on_edu(self, edu_type: str, origin: str, content: dict) -> None: if not self.config.use_presence and edu_type == EduTypes.Presence: return diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 84e39c5a46..5ef0556ef7 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -620,8 +620,8 @@ class FederationThirdPartyInviteExchangeServlet(BaseFederationServlet): PATH = "/exchange_third_party_invite/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id): - content = await self.handler.on_exchange_third_party_invite_request(content) - return 200, content + await self.handler.on_exchange_third_party_invite_request(content) + return 200, {} class FederationClientKeysQueryServlet(BaseFederationServlet): diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 3ebee38ebe..5ea8a7b603 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -21,7 +21,17 @@ import logging from collections.abc import Container from http import HTTPStatus -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, Union +from typing import ( + TYPE_CHECKING, + Dict, + Iterable, + List, + Optional, + Sequence, + Set, + Tuple, + Union, +) import attr from signedjson.key import decode_verify_key_bytes @@ -171,15 +181,17 @@ def __init__(self, hs: "HomeServer"): self._ephemeral_messages_enabled = hs.config.enable_ephemeral_messages - async def on_receive_pdu(self, origin, pdu, sent_to_us_directly=False) -> None: + async def on_receive_pdu( + self, origin: str, pdu: EventBase, sent_to_us_directly: bool = False + ) -> None: """Process a PDU received via a federation /send/ transaction, or via backfill of missing prev_events Args: - origin (str): server which initiated the /send/ transaction. Will + origin: server which initiated the /send/ transaction. Will be used to fetch missing events or state. - pdu (FrozenEvent): received PDU - sent_to_us_directly (bool): True if this event was pushed to us; False if + pdu: received PDU + sent_to_us_directly: True if this event was pushed to us; False if we pulled it as the result of a missing prev_event. """ @@ -411,13 +423,15 @@ async def on_receive_pdu(self, origin, pdu, sent_to_us_directly=False) -> None: await self._process_received_pdu(origin, pdu, state=state) - async def _get_missing_events_for_pdu(self, origin, pdu, prevs, min_depth): + async def _get_missing_events_for_pdu( + self, origin: str, pdu: EventBase, prevs: Set[str], min_depth: int + ) -> None: """ Args: - origin (str): Origin of the pdu. Will be called to get the missing events + origin: Origin of the pdu. Will be called to get the missing events pdu: received pdu - prevs (set(str)): List of event ids which we are missing - min_depth (int): Minimum depth of events to return. + prevs: List of event ids which we are missing + min_depth: Minimum depth of events to return. """ room_id = pdu.room_id @@ -778,7 +792,7 @@ async def _process_received_pdu( origin: str, event: EventBase, state: Optional[Iterable[EventBase]], - ): + ) -> None: """Called when we have a new pdu. We need to do auth checks and put it through the StateHandler. @@ -887,7 +901,9 @@ async def _resync_device(self, sender: str) -> None: logger.exception("Failed to resync device for %s", sender) @log_function - async def backfill(self, dest, room_id, limit, extremities): + async def backfill( + self, dest: str, room_id: str, limit: int, extremities: List[str] + ) -> List[EventBase]: """Trigger a backfill request to `dest` for the given `room_id` This will attempt to get more events from the remote. If the other side @@ -1142,16 +1158,15 @@ async def maybe_backfill( curr_state = await self.state_handler.get_current_state(room_id) - def get_domains_from_state(state): + def get_domains_from_state(state: StateMap[EventBase]) -> List[Tuple[str, int]]: """Get joined domains from state Args: - state (dict[tuple, FrozenEvent]): State map from type/state - key to event. + state: State map from type/state key to event. Returns: - list[tuple[str, int]]: Returns a list of servers with the - lowest depth of their joins. Sorted by lowest depth first. + Returns a list of servers with the lowest depth of their joins. + Sorted by lowest depth first. """ joined_users = [ (state_key, int(event.depth)) @@ -1179,7 +1194,7 @@ def get_domains_from_state(state): domain for domain, depth in curr_domains if domain != self.server_name ] - async def try_backfill(domains): + async def try_backfill(domains: List[str]) -> bool: # TODO: Should we try multiple of these at a time? for dom in domains: try: @@ -1258,21 +1273,25 @@ async def try_backfill(domains): } for e_id, _ in sorted_extremeties_tuple: - likely_domains = get_domains_from_state(states[e_id]) + likely_extremeties_domains = get_domains_from_state(states[e_id]) success = await try_backfill( - [dom for dom, _ in likely_domains if dom not in tried_domains] + [ + dom + for dom, _ in likely_extremeties_domains + if dom not in tried_domains + ] ) if success: return True - tried_domains.update(dom for dom, _ in likely_domains) + tried_domains.update(dom for dom, _ in likely_extremeties_domains) return False async def _get_events_and_persist( self, destination: str, room_id: str, events: Iterable[str] - ): + ) -> None: """Fetch the given events from a server, and persist them as outliers. This function *does not* recursively get missing auth events of the @@ -1348,7 +1367,7 @@ async def get_event(event_id: str): event_infos, ) - def _sanity_check_event(self, ev): + def _sanity_check_event(self, ev: EventBase) -> None: """ Do some early sanity checks of a received event @@ -1357,9 +1376,7 @@ def _sanity_check_event(self, ev): or cascade of event fetches. Args: - ev (synapse.events.EventBase): event to be checked - - Returns: None + ev: event to be checked Raises: SynapseError if the event does not pass muster @@ -1380,7 +1397,7 @@ def _sanity_check_event(self, ev): ) raise SynapseError(HTTPStatus.BAD_REQUEST, "Too many auth_events") - async def send_invite(self, target_host, event): + async def send_invite(self, target_host: str, event: EventBase) -> EventBase: """Sends the invite to the remote server for signing. Invites must be signed by the invitee's server before distribution. @@ -1528,12 +1545,13 @@ async def do_invite_join( run_in_background(self._handle_queued_pdus, room_queue) - async def _handle_queued_pdus(self, room_queue): + async def _handle_queued_pdus( + self, room_queue: List[Tuple[EventBase, str]] + ) -> None: """Process PDUs which got queued up while we were busy send_joining. Args: - room_queue (list[FrozenEvent, str]): list of PDUs to be processed - and the servers that sent them + room_queue: list of PDUs to be processed and the servers that sent them """ for p, origin in room_queue: try: @@ -1612,7 +1630,7 @@ async def on_make_join_request( return event - async def on_send_join_request(self, origin, pdu): + async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: """We have received a join event for a room. Fully process it and respond with the current state and auth chains. """ @@ -1668,7 +1686,7 @@ async def on_send_join_request(self, origin, pdu): async def on_invite_request( self, origin: str, event: EventBase, room_version: RoomVersion - ): + ) -> EventBase: """We've got an invite event. Process and persist it. Sign it. Respond with the now signed event. @@ -1841,7 +1859,7 @@ async def on_make_leave_request( return event - async def on_send_leave_request(self, origin, pdu): + async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: """ We have received a leave event for a room. Fully process it.""" event = pdu @@ -1969,12 +1987,17 @@ async def get_persisted_pdu( else: return None - async def get_min_depth_for_context(self, context): + async def get_min_depth_for_context(self, context: str) -> int: return await self.store.get_min_depth(context) async def _handle_new_event( - self, origin, event, state=None, auth_events=None, backfilled=False - ): + self, + origin: str, + event: EventBase, + state: Optional[Iterable[EventBase]] = None, + auth_events: Optional[MutableStateMap[EventBase]] = None, + backfilled: bool = False, + ) -> EventContext: context = await self._prep_event( origin, event, state=state, auth_events=auth_events, backfilled=backfilled ) @@ -2280,40 +2303,14 @@ async def _check_for_soft_fail( logger.warning("Soft-failing %r because %s", event, e) event.internal_metadata.soft_failed = True - async def on_query_auth( - self, origin, event_id, room_id, remote_auth_chain, rejects, missing - ): - in_room = await self.auth.check_host_in_room(room_id, origin) - if not in_room: - raise AuthError(403, "Host not in room.") - - event = await self.store.get_event(event_id, check_room_id=room_id) - - # Just go through and process each event in `remote_auth_chain`. We - # don't want to fall into the trap of `missing` being wrong. - for e in remote_auth_chain: - try: - await self._handle_new_event(origin, e) - except AuthError: - pass - - # Now get the current auth_chain for the event. - local_auth_chain = await self.store.get_auth_chain( - room_id, list(event.auth_event_ids()), include_given=True - ) - - # TODO: Check if we would now reject event_id. If so we need to tell - # everyone. - - ret = await self.construct_auth_difference(local_auth_chain, remote_auth_chain) - - logger.debug("on_query_auth returning: %s", ret) - - return ret - async def on_get_missing_events( - self, origin, room_id, earliest_events, latest_events, limit - ): + self, + origin: str, + room_id: str, + earliest_events: List[str], + latest_events: List[str], + limit: int, + ) -> List[EventBase]: in_room = await self.auth.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") @@ -2617,8 +2614,8 @@ async def construct_auth_difference( assumes that we have already processed all events in remote_auth Params: - local_auth (list) - remote_auth (list) + local_auth + remote_auth Returns: dict @@ -2742,8 +2739,8 @@ def get_next(it, opt=None): @log_function async def exchange_third_party_invite( - self, sender_user_id, target_user_id, room_id, signed - ): + self, sender_user_id: str, target_user_id: str, room_id: str, signed: JsonDict + ) -> None: third_party_invite = {"signed": signed} event_dict = { @@ -2835,8 +2832,12 @@ async def on_exchange_third_party_invite_request( await member_handler.send_membership_event(None, event, context) async def add_display_name_to_third_party_invite( - self, room_version, event_dict, event, context - ): + self, + room_version: str, + event_dict: JsonDict, + event: EventBase, + context: EventContext, + ) -> Tuple[EventBase, EventContext]: key = ( EventTypes.ThirdPartyInvite, event.content["third_party_invite"]["signed"]["token"], @@ -2872,13 +2873,13 @@ async def add_display_name_to_third_party_invite( EventValidator().validate_new(event, self.config) return (event, context) - async def _check_signature(self, event, context): + async def _check_signature(self, event: EventBase, context: EventContext) -> None: """ Checks that the signature in the event is consistent with its invite. Args: - event (Event): The m.room.member event to check - context (EventContext): + event: The m.room.member event to check + context: Raises: AuthError: if signature didn't match any keys, or key has been @@ -2964,13 +2965,13 @@ async def _check_signature(self, event, context): raise last_exception - async def _check_key_revocation(self, public_key, url): + async def _check_key_revocation(self, public_key: str, url: str) -> None: """ Checks whether public_key has been revoked. Args: - public_key (str): base-64 encoded public key. - url (str): Key revocation URL. + public_key: base-64 encoded public key. + url: Key revocation URL. Raises: AuthError: if they key has been revoked. From 0ef321ff3b81078fe5059a9012f8ff45ecbe7987 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 6 Apr 2021 13:36:05 +0100 Subject: [PATCH 014/619] Remove outdated constraint on remote_media_cache_thumbnails (#9725) The `remote_media_cache_thumbnails_media_origin_media_id_thumbna_key` constraint is superceded by `remote_media_repository_thumbn_media_origin_id_width_height_met` (which adds `thumbnail_method` to the unique key). PR #7124 made an attempt to remove the old constraint, but got the name wrong, so it didn't work. Here we update the bg update and rerun it. Fixes #8649. --- changelog.d/9725.bugfix | 1 + .../databases/main/media_repository.py | 21 +++++++++++++++--- .../11drop_thumbnail_constraint.sql.postgres | 22 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9725.bugfix create mode 100644 synapse/storage/databases/main/schema/delta/59/11drop_thumbnail_constraint.sql.postgres diff --git a/changelog.d/9725.bugfix b/changelog.d/9725.bugfix new file mode 100644 index 0000000000..71283685c8 --- /dev/null +++ b/changelog.d/9725.bugfix @@ -0,0 +1 @@ +Fix longstanding bug which caused `duplicate key value violates unique constraint "remote_media_cache_thumbnails_media_origin_media_id_thumbna_key"` errors. diff --git a/synapse/storage/databases/main/media_repository.py b/synapse/storage/databases/main/media_repository.py index 4f3d192562..b7820ac7ff 100644 --- a/synapse/storage/databases/main/media_repository.py +++ b/synapse/storage/databases/main/media_repository.py @@ -22,6 +22,9 @@ BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD = ( "media_repository_drop_index_wo_method" ) +BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD_2 = ( + "media_repository_drop_index_wo_method_2" +) class MediaSortOrder(Enum): @@ -85,23 +88,35 @@ def __init__(self, database: DatabasePool, db_conn, hs): unique=True, ) + # the original impl of _drop_media_index_without_method was broken (see + # https://github.com/matrix-org/synapse/issues/8649), so we replace the original + # impl with a no-op and run the fixed migration as + # media_repository_drop_index_wo_method_2. + self.db_pool.updates.register_noop_background_update( + BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD + ) self.db_pool.updates.register_background_update_handler( - BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD, + BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD_2, self._drop_media_index_without_method, ) async def _drop_media_index_without_method(self, progress, batch_size): + """background update handler which removes the old constraints. + + Note that this is only run on postgres. + """ + def f(txn): txn.execute( "ALTER TABLE local_media_repository_thumbnails DROP CONSTRAINT IF EXISTS local_media_repository_thumbn_media_id_thumbnail_width_thum_key" ) txn.execute( - "ALTER TABLE remote_media_cache_thumbnails DROP CONSTRAINT IF EXISTS remote_media_repository_thumbn_media_id_thumbnail_width_thum_key" + "ALTER TABLE remote_media_cache_thumbnails DROP CONSTRAINT IF EXISTS remote_media_cache_thumbnails_media_origin_media_id_thumbna_key" ) await self.db_pool.runInteraction("drop_media_indices_without_method", f) await self.db_pool.updates._end_background_update( - BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD + BG_UPDATE_REMOVE_MEDIA_REPO_INDEX_WITHOUT_METHOD_2 ) return 1 diff --git a/synapse/storage/databases/main/schema/delta/59/11drop_thumbnail_constraint.sql.postgres b/synapse/storage/databases/main/schema/delta/59/11drop_thumbnail_constraint.sql.postgres new file mode 100644 index 0000000000..54c1bca3b1 --- /dev/null +++ b/synapse/storage/databases/main/schema/delta/59/11drop_thumbnail_constraint.sql.postgres @@ -0,0 +1,22 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- drop old constraints on remote_media_cache_thumbnails +-- +-- This was originally part of 57.07, but it was done wrong, per +-- https://github.com/matrix-org/synapse/issues/8649, so we do it again. +INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES + (5911, 'media_repository_drop_index_wo_method_2', '{}', 'remote_media_repository_thumbnails_method_idx'); + From 024f121b744661f96d771393ddc469dbc322a1c5 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 6 Apr 2021 13:48:22 +0100 Subject: [PATCH 015/619] Fix reported bugbear: too broad exception assertion (#9753) --- changelog.d/9753.misc | 1 + tests/config/test_load.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9753.misc diff --git a/changelog.d/9753.misc b/changelog.d/9753.misc new file mode 100644 index 0000000000..31184fe0bd --- /dev/null +++ b/changelog.d/9753.misc @@ -0,0 +1 @@ +Check that a `ConfigError` is raised, rather than simply `Exception`, when appropriate in homeserver config file generation tests. \ No newline at end of file diff --git a/tests/config/test_load.py b/tests/config/test_load.py index 734a9983e8..c109425671 100644 --- a/tests/config/test_load.py +++ b/tests/config/test_load.py @@ -20,6 +20,7 @@ import yaml +from synapse.config import ConfigError from synapse.config.homeserver import HomeServerConfig from tests import unittest @@ -35,9 +36,9 @@ def tearDown(self): def test_load_fails_if_server_name_missing(self): self.generate_config_and_remove_lines_containing("server_name") - with self.assertRaises(Exception): + with self.assertRaises(ConfigError): HomeServerConfig.load_config("", ["-c", self.file]) - with self.assertRaises(Exception): + with self.assertRaises(ConfigError): HomeServerConfig.load_or_generate_config("", ["-c", self.file]) def test_generates_and_loads_macaroon_secret_key(self): From 44bb881096d7ad4d730e2dc31e0094c0324e0970 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 6 Apr 2021 08:58:18 -0400 Subject: [PATCH 016/619] Add type hints to expiring cache. (#9730) --- changelog.d/9730.misc | 1 + synapse/federation/federation_client.py | 2 +- synapse/handlers/device.py | 4 +- synapse/handlers/e2e_keys.py | 12 --- synapse/handlers/sync.py | 10 ++- synapse/rest/media/v1/preview_url_resource.py | 2 +- synapse/state/__init__.py | 5 +- synapse/util/caches/expiringcache.py | 83 ++++++++++++------- 8 files changed, 65 insertions(+), 54 deletions(-) create mode 100644 changelog.d/9730.misc diff --git a/changelog.d/9730.misc b/changelog.d/9730.misc new file mode 100644 index 0000000000..8063059b0b --- /dev/null +++ b/changelog.d/9730.misc @@ -0,0 +1 @@ +Add type hints to expiring cache. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index afdb5bf2fa..55533d7501 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -102,7 +102,7 @@ def __init__(self, hs: "HomeServer"): max_len=1000, expiry_ms=120 * 1000, reset_expiry_on_get=False, - ) + ) # type: ExpiringCache[str, EventBase] def _clear_tried_cache(self): """Clear pdu_destination_tried cache""" diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 54293d0b9c..7e76db3e2a 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -631,7 +631,7 @@ def __init__(self, hs: "HomeServer", device_handler: DeviceHandler): max_len=10000, expiry_ms=30 * 60 * 1000, iterable=True, - ) + ) # type: ExpiringCache[str, Set[str]] # Attempt to resync out of sync device lists every 30s. self._resync_retry_in_progress = False @@ -760,7 +760,7 @@ async def _need_to_do_resync( """Given a list of updates for a user figure out if we need to do a full resync, or whether we have enough data that we can just apply the delta. """ - seen_updates = self._seen_updates.get(user_id, set()) + seen_updates = self._seen_updates.get(user_id, set()) # type: Set[str] extremity = await self.store.get_device_list_last_stream_id_for_remote(user_id) diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 739653a3fa..92b18378fc 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -38,7 +38,6 @@ ) from synapse.util import json_decoder, unwrapFirstError from synapse.util.async_helpers import Linearizer -from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.retryutils import NotRetryingDestination if TYPE_CHECKING: @@ -1292,17 +1291,6 @@ def __init__(self, hs: "HomeServer", e2e_keys_handler: E2eKeysHandler): # user_id -> list of updates waiting to be handled. self._pending_updates = {} # type: Dict[str, List[Tuple[JsonDict, JsonDict]]] - # Recently seen stream ids. We don't bother keeping these in the DB, - # but they're useful to have them about to reduce the number of spurious - # resyncs. - self._seen_updates = ExpiringCache( - cache_name="signing_key_update_edu", - clock=self.clock, - max_len=10000, - expiry_ms=30 * 60 * 1000, - iterable=True, - ) - async def incoming_signing_key_update( self, origin: str, edu_content: JsonDict ) -> None: diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 7b356ba7e5..ff11266c67 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -252,13 +252,13 @@ def __init__(self, hs: "HomeServer"): self.storage = hs.get_storage() self.state_store = self.storage.state - # ExpiringCache((User, Device)) -> LruCache(state_key => event_id) + # ExpiringCache((User, Device)) -> LruCache(user_id => event_id) self.lazy_loaded_members_cache = ExpiringCache( "lazy_loaded_members_cache", self.clock, max_len=0, expiry_ms=LAZY_LOADED_MEMBERS_CACHE_MAX_AGE, - ) + ) # type: ExpiringCache[Tuple[str, Optional[str]], LruCache[str, str]] async def wait_for_sync_for_user( self, @@ -733,8 +733,10 @@ async def compute_summary( def get_lazy_loaded_members_cache( self, cache_key: Tuple[str, Optional[str]] - ) -> LruCache: - cache = self.lazy_loaded_members_cache.get(cache_key) + ) -> LruCache[str, str]: + cache = self.lazy_loaded_members_cache.get( + cache_key + ) # type: Optional[LruCache[str, str]] if cache is None: logger.debug("creating LruCache for %r", cache_key) cache = LruCache(LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE) diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index c4ed9dfdb4..814145a04a 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -175,7 +175,7 @@ def __init__( clock=self.clock, # don't spider URLs more often than once an hour expiry_ms=ONE_HOUR, - ) + ) # type: ExpiringCache[str, ObservableDeferred] if self._worker_run_media_background_jobs: self._cleaner_loop = self.clock.looping_call( diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index c3d6e80c49..c0f79ffdc8 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -22,6 +22,7 @@ Callable, DefaultDict, Dict, + FrozenSet, Iterable, List, Optional, @@ -515,7 +516,7 @@ def __init__(self, hs): expiry_ms=EVICTION_TIMEOUT_SECONDS * 1000, iterable=True, reset_expiry_on_get=True, - ) + ) # type: ExpiringCache[FrozenSet[int], _StateCacheEntry] # # stuff for tracking time spent on state-res by room @@ -536,7 +537,7 @@ async def resolve_state_groups( state_groups_ids: Dict[int, StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_res_store: "StateResolutionStore", - ): + ) -> _StateCacheEntry: """Resolves conflicts between a set of state groups Always generates a new state group (unless we hit the cache), so should diff --git a/synapse/util/caches/expiringcache.py b/synapse/util/caches/expiringcache.py index e15f7ee698..4dc3477e89 100644 --- a/synapse/util/caches/expiringcache.py +++ b/synapse/util/caches/expiringcache.py @@ -15,40 +15,50 @@ import logging from collections import OrderedDict +from typing import Any, Generic, Optional, TypeVar, Union, overload + +import attr +from typing_extensions import Literal from synapse.config import cache as cache_config from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.util import Clock from synapse.util.caches import register_cache logger = logging.getLogger(__name__) -SENTINEL = object() +SENTINEL = object() # type: Any + +T = TypeVar("T") +KT = TypeVar("KT") +VT = TypeVar("VT") -class ExpiringCache: + +class ExpiringCache(Generic[KT, VT]): def __init__( self, - cache_name, - clock, - max_len=0, - expiry_ms=0, - reset_expiry_on_get=False, - iterable=False, + cache_name: str, + clock: Clock, + max_len: int = 0, + expiry_ms: int = 0, + reset_expiry_on_get: bool = False, + iterable: bool = False, ): """ Args: - cache_name (str): Name of this cache, used for logging. - clock (Clock) - max_len (int): Max size of dict. If the dict grows larger than this + cache_name: Name of this cache, used for logging. + clock + max_len: Max size of dict. If the dict grows larger than this then the oldest items get automatically evicted. Default is 0, which indicates there is no max limit. - expiry_ms (int): How long before an item is evicted from the cache + expiry_ms: How long before an item is evicted from the cache in milliseconds. Default is 0, indicating items never get evicted based on time. - reset_expiry_on_get (bool): If true, will reset the expiry time for + reset_expiry_on_get: If true, will reset the expiry time for an item on access. Defaults to False. - iterable (bool): If true, the size is calculated by summing the + iterable: If true, the size is calculated by summing the sizes of all entries, rather than the number of entries. """ self._cache_name = cache_name @@ -62,7 +72,7 @@ def __init__( self._expiry_ms = expiry_ms self._reset_expiry_on_get = reset_expiry_on_get - self._cache = OrderedDict() + self._cache = OrderedDict() # type: OrderedDict[KT, _CacheEntry] self.iterable = iterable @@ -79,12 +89,12 @@ def f(): self._clock.looping_call(f, self._expiry_ms / 2) - def __setitem__(self, key, value): + def __setitem__(self, key: KT, value: VT) -> None: now = self._clock.time_msec() self._cache[key] = _CacheEntry(now, value) self.evict() - def evict(self): + def evict(self) -> None: # Evict if there are now too many items while self._max_size and len(self) > self._max_size: _key, value = self._cache.popitem(last=False) @@ -93,7 +103,7 @@ def evict(self): else: self.metrics.inc_evictions() - def __getitem__(self, key): + def __getitem__(self, key: KT) -> VT: try: entry = self._cache[key] self.metrics.inc_hits() @@ -106,7 +116,7 @@ def __getitem__(self, key): return entry.value - def pop(self, key, default=SENTINEL): + def pop(self, key: KT, default: T = SENTINEL) -> Union[VT, T]: """Removes and returns the value with the given key from the cache. If the key isn't in the cache then `default` will be returned if @@ -115,29 +125,40 @@ def pop(self, key, default=SENTINEL): Identical functionality to `dict.pop(..)`. """ - value = self._cache.pop(key, default) + value = self._cache.pop(key, SENTINEL) + # The key was not found. if value is SENTINEL: - raise KeyError(key) + if default is SENTINEL: + raise KeyError(key) + return default - return value + return value.value - def __contains__(self, key): + def __contains__(self, key: KT) -> bool: return key in self._cache - def get(self, key, default=None): + @overload + def get(self, key: KT, default: Literal[None] = None) -> Optional[VT]: + ... + + @overload + def get(self, key: KT, default: T) -> Union[VT, T]: + ... + + def get(self, key: KT, default: Optional[T] = None) -> Union[VT, Optional[T]]: try: return self[key] except KeyError: return default - def setdefault(self, key, value): + def setdefault(self, key: KT, value: VT) -> VT: try: return self[key] except KeyError: self[key] = value return value - def _prune_cache(self): + def _prune_cache(self) -> None: if not self._expiry_ms: # zero expiry time means don't expire. This should never get called # since we have this check in start too. @@ -166,7 +187,7 @@ def _prune_cache(self): len(self), ) - def __len__(self): + def __len__(self) -> int: if self.iterable: return sum(len(entry.value) for entry in self._cache.values()) else: @@ -190,9 +211,7 @@ def set_cache_factor(self, factor: float) -> bool: return False +@attr.s(slots=True) class _CacheEntry: - __slots__ = ["time", "value"] - - def __init__(self, time, value): - self.time = time - self.value = value + time = attr.ib(type=int) + value = attr.ib() From 04819239bae2b39ee42bfdb6f9b83c6d9fe34169 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 6 Apr 2021 14:38:30 +0100 Subject: [PATCH 017/619] Add a Synapse Module for configuring presence update routing (#9491) At the moment, if you'd like to share presence between local or remote users, those users must be sharing a room together. This isn't always the most convenient or useful situation though. This PR adds a module to Synapse that will allow deployments to set up extra logic on where presence updates should be routed. The module must implement two methods, `get_users_for_states` and `get_interested_users`. These methods are given presence updates or user IDs and must return information that Synapse will use to grant passing presence updates around. A method is additionally added to `ModuleApi` which allows triggering a set of users to receive the current, online presence information for all users they are considered interested in. This is the equivalent of that user receiving presence information during an initial sync. The goal of this module is to be fairly generic and useful for a variety of applications, with hard requirements being: * Sending state for a specific set or all known users to a defined set of local and remote users. * The ability to trigger an initial sync for specific users, so they receive all current state. --- README.rst | 7 +- changelog.d/9491.feature | 1 + docs/presence_router_module.md | 235 ++++++++++++++++ docs/sample_config.yaml | 23 +- synapse/app/generic_worker.py | 3 +- synapse/config/server.py | 39 ++- synapse/events/presence_router.py | 104 +++++++ synapse/federation/sender/__init__.py | 19 +- synapse/handlers/presence.py | 278 ++++++++++++++++--- synapse/module_api/__init__.py | 50 ++++ synapse/server.py | 5 + tests/events/test_presence_router.py | 386 ++++++++++++++++++++++++++ tests/handlers/test_sync.py | 21 +- tests/module_api/test_api.py | 175 +++++++++++- 14 files changed, 1282 insertions(+), 64 deletions(-) create mode 100644 changelog.d/9491.feature create mode 100644 docs/presence_router_module.md create mode 100644 synapse/events/presence_router.py create mode 100644 tests/events/test_presence_router.py diff --git a/README.rst b/README.rst index 655a2bf3be..1a5503572e 100644 --- a/README.rst +++ b/README.rst @@ -393,7 +393,12 @@ massive excess of outgoing federation requests (see `discussion indicate that your server is also issuing far more outgoing federation requests than can be accounted for by your users' activity, this is a likely cause. The misbehavior can be worked around by setting -``use_presence: false`` in the Synapse config file. +the following in the Synapse config file: + +.. code-block:: yaml + + presence: + enabled: false People can't accept room invitations from me -------------------------------------------- diff --git a/changelog.d/9491.feature b/changelog.d/9491.feature new file mode 100644 index 0000000000..8b56a95a44 --- /dev/null +++ b/changelog.d/9491.feature @@ -0,0 +1 @@ +Add a Synapse module for routing presence updates between users. diff --git a/docs/presence_router_module.md b/docs/presence_router_module.md new file mode 100644 index 0000000000..d6566d978d --- /dev/null +++ b/docs/presence_router_module.md @@ -0,0 +1,235 @@ +# Presence Router Module + +Synapse supports configuring a module that can specify additional users +(local or remote) to should receive certain presence updates from local +users. + +Note that routing presence via Application Service transactions is not +currently supported. + +The presence routing module is implemented as a Python class, which will +be imported by the running Synapse. + +## Python Presence Router Class + +The Python class is instantiated with two objects: + +* A configuration object of some type (see below). +* An instance of `synapse.module_api.ModuleApi`. + +It then implements methods related to presence routing. + +Note that one method of `ModuleApi` that may be useful is: + +```python +async def ModuleApi.send_local_online_presence_to(users: Iterable[str]) -> None +``` + +which can be given a list of local or remote MXIDs to broadcast known, online user +presence to (for those users that the receiving user is considered interested in). +It does not include state for users who are currently offline, and it can only be +called on workers that support sending federation. + +### Module structure + +Below is a list of possible methods that can be implemented, and whether they are +required. + +#### `parse_config` + +```python +def parse_config(config_dict: dict) -> Any +``` + +**Required.** A static method that is passed a dictionary of config options, and + should return a validated config object. This method is described further in + [Configuration](#configuration). + +#### `get_users_for_states` + +```python +async def get_users_for_states( + self, + state_updates: Iterable[UserPresenceState], +) -> Dict[str, Set[UserPresenceState]]: +``` + +**Required.** An asynchronous method that is passed an iterable of user presence +state. This method can determine whether a given presence update should be sent to certain +users. It does this by returning a dictionary with keys representing local or remote +Matrix User IDs, and values being a python set +of `synapse.handlers.presence.UserPresenceState` instances. + +Synapse will then attempt to send the specified presence updates to each user when +possible. + +#### `get_interested_users` + +```python +async def get_interested_users(self, user_id: str) -> Union[Set[str], str] +``` + +**Required.** An asynchronous method that is passed a single Matrix User ID. This +method is expected to return the users that the passed in user may be interested in the +presence of. Returned users may be local or remote. The presence routed as a result of +what this method returns is sent in addition to the updates already sent between users +that share a room together. Presence updates are deduplicated. + +This method should return a python set of Matrix User IDs, or the object +`synapse.events.presence_router.PresenceRouter.ALL_USERS` to indicate that the passed +user should receive presence information for *all* known users. + +For clarity, if the user `@alice:example.org` is passed to this method, and the Set +`{"@bob:example.com", "@charlie:somewhere.org"}` is returned, this signifies that Alice +should receive presence updates sent by Bob and Charlie, regardless of whether these +users share a room. + +### Example + +Below is an example implementation of a presence router class. + +```python +from typing import Dict, Iterable, Set, Union +from synapse.events.presence_router import PresenceRouter +from synapse.handlers.presence import UserPresenceState +from synapse.module_api import ModuleApi + +class PresenceRouterConfig: + def __init__(self): + # Config options with their defaults + # A list of users to always send all user presence updates to + self.always_send_to_users = [] # type: List[str] + + # A list of users to ignore presence updates for. Does not affect + # shared-room presence relationships + self.blacklisted_users = [] # type: List[str] + +class ExamplePresenceRouter: + """An example implementation of synapse.presence_router.PresenceRouter. + Supports routing all presence to a configured set of users, or a subset + of presence from certain users to members of certain rooms. + + Args: + config: A configuration object. + module_api: An instance of Synapse's ModuleApi. + """ + def __init__(self, config: PresenceRouterConfig, module_api: ModuleApi): + self._config = config + self._module_api = module_api + + @staticmethod + def parse_config(config_dict: dict) -> PresenceRouterConfig: + """Parse a configuration dictionary from the homeserver config, do + some validation and return a typed PresenceRouterConfig. + + Args: + config_dict: The configuration dictionary. + + Returns: + A validated config object. + """ + # Initialise a typed config object + config = PresenceRouterConfig() + always_send_to_users = config_dict.get("always_send_to_users") + blacklisted_users = config_dict.get("blacklisted_users") + + # Do some validation of config options... otherwise raise a + # synapse.config.ConfigError. + config.always_send_to_users = always_send_to_users + config.blacklisted_users = blacklisted_users + + return config + + async def get_users_for_states( + self, + state_updates: Iterable[UserPresenceState], + ) -> Dict[str, Set[UserPresenceState]]: + """Given an iterable of user presence updates, determine where each one + needs to go. Returned results will not affect presence updates that are + sent between users who share a room. + + Args: + state_updates: An iterable of user presence state updates. + + Returns: + A dictionary of user_id -> set of UserPresenceState that the user should + receive. + """ + destination_users = {} # type: Dict[str, Set[UserPresenceState] + + # Ignore any updates for blacklisted users + desired_updates = set() + for update in state_updates: + if update.state_key not in self._config.blacklisted_users: + desired_updates.add(update) + + # Send all presence updates to specific users + for user_id in self._config.always_send_to_users: + destination_users[user_id] = desired_updates + + return destination_users + + async def get_interested_users( + self, + user_id: str, + ) -> Union[Set[str], PresenceRouter.ALL_USERS]: + """ + Retrieve a list of users that `user_id` is interested in receiving the + presence of. This will be in addition to those they share a room with. + Optionally, the object PresenceRouter.ALL_USERS can be returned to indicate + that this user should receive all incoming local and remote presence updates. + + Note that this method will only be called for local users. + + Args: + user_id: A user requesting presence updates. + + Returns: + A set of user IDs to return additional presence updates for, or + PresenceRouter.ALL_USERS to return presence updates for all other users. + """ + if user_id in self._config.always_send_to_users: + return PresenceRouter.ALL_USERS + + return set() +``` + +#### A note on `get_users_for_states` and `get_interested_users` + +Both of these methods are effectively two different sides of the same coin. The logic +regarding which users should receive updates for other users should be the same +between them. + +`get_users_for_states` is called when presence updates come in from either federation +or local users, and is used to either direct local presence to remote users, or to +wake up the sync streams of local users to collect remote presence. + +In contrast, `get_interested_users` is used to determine the users that presence should +be fetched for when a local user is syncing. This presence is then retrieved, before +being fed through `get_users_for_states` once again, with only the syncing user's +routing information pulled from the resulting dictionary. + +Their routing logic should thus line up, else you may run into unintended behaviour. + +## Configuration + +Once you've crafted your module and installed it into the same Python environment as +Synapse, amend your homeserver config file with the following. + +```yaml +presence: + routing_module: + module: my_module.ExamplePresenceRouter + config: + # Any configuration options for your module. The below is an example. + # of setting options for ExamplePresenceRouter. + always_send_to_users: ["@presence_gobbler:example.org"] + blacklisted_users: + - "@alice:example.com" + - "@bob:example.com" + ... +``` + +The contents of `config` will be passed as a Python dictionary to the static +`parse_config` method of your class. The object returned by this method will +then be passed to the `__init__` method of your module as `config`. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index b0bf987740..9182dcd987 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -82,9 +82,28 @@ pid_file: DATADIR/homeserver.pid # #soft_file_limit: 0 -# Set to false to disable presence tracking on this homeserver. +# Presence tracking allows users to see the state (e.g online/offline) +# of other local and remote users. # -#use_presence: false +presence: + # Uncomment to disable presence tracking on this homeserver. This option + # replaces the previous top-level 'use_presence' option. + # + #enabled: false + + # Presence routers are third-party modules that can specify additional logic + # to where presence updates from users are routed. + # + presence_router: + # The custom module's class. Uncomment to use a custom presence router module. + # + #module: "my_custom_router.PresenceRouter" + + # Configuration options of the custom module. Refer to your module's + # documentation for available options. + # + #config: + # example_option: 'something' # Whether to require authentication to retrieve profile data (avatars, # display names) of other users through the client API. Defaults to diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 3df2aa5c2b..d1c2079233 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -281,6 +281,7 @@ def __init__(self, hs): self.hs = hs self.is_mine_id = hs.is_mine_id + self.presence_router = hs.get_presence_router() self._presence_enabled = hs.config.use_presence # The number of ongoing syncs on this process, by user id. @@ -395,7 +396,7 @@ def _user_syncing(): return _user_syncing() async def notify_from_replication(self, states, stream_id): - parties = await get_interested_parties(self.store, states) + parties = await get_interested_parties(self.store, self.presence_router, states) room_ids_to_states, users_to_states = parties self.notifier.on_new_event( diff --git a/synapse/config/server.py b/synapse/config/server.py index 5f8910b6e1..8decc9d10d 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -27,6 +27,7 @@ from netaddr import AddrFormatError, IPNetwork, IPSet from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.util.module_loader import load_module from synapse.util.stringutils import parse_and_validate_server_name from ._base import Config, ConfigError @@ -238,7 +239,20 @@ def read_config(self, config, **kwargs): self.public_baseurl = config.get("public_baseurl") # Whether to enable user presence. - self.use_presence = config.get("use_presence", True) + presence_config = config.get("presence") or {} + self.use_presence = presence_config.get("enabled") + if self.use_presence is None: + self.use_presence = config.get("use_presence", True) + + # Custom presence router module + self.presence_router_module_class = None + self.presence_router_config = None + presence_router_config = presence_config.get("presence_router") + if presence_router_config: + ( + self.presence_router_module_class, + self.presence_router_config, + ) = load_module(presence_router_config, ("presence", "presence_router")) # Whether to update the user directory or not. This should be set to # false only if we are updating the user directory in a worker @@ -834,9 +848,28 @@ def generate_config_section( # #soft_file_limit: 0 - # Set to false to disable presence tracking on this homeserver. + # Presence tracking allows users to see the state (e.g online/offline) + # of other local and remote users. # - #use_presence: false + presence: + # Uncomment to disable presence tracking on this homeserver. This option + # replaces the previous top-level 'use_presence' option. + # + #enabled: false + + # Presence routers are third-party modules that can specify additional logic + # to where presence updates from users are routed. + # + presence_router: + # The custom module's class. Uncomment to use a custom presence router module. + # + #module: "my_custom_router.PresenceRouter" + + # Configuration options of the custom module. Refer to your module's + # documentation for available options. + # + #config: + # example_option: 'something' # Whether to require authentication to retrieve profile data (avatars, # display names) of other users through the client API. Defaults to diff --git a/synapse/events/presence_router.py b/synapse/events/presence_router.py new file mode 100644 index 0000000000..24cd389d80 --- /dev/null +++ b/synapse/events/presence_router.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TYPE_CHECKING, Dict, Iterable, Set, Union + +from synapse.api.presence import UserPresenceState + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +class PresenceRouter: + """ + A module that the homeserver will call upon to help route user presence updates to + additional destinations. If a custom presence router is configured, calls will be + passed to that instead. + """ + + ALL_USERS = "ALL" + + def __init__(self, hs: "HomeServer"): + self.custom_presence_router = None + + # Check whether a custom presence router module has been configured + if hs.config.presence_router_module_class: + # Initialise the module + self.custom_presence_router = hs.config.presence_router_module_class( + config=hs.config.presence_router_config, module_api=hs.get_module_api() + ) + + # Ensure the module has implemented the required methods + required_methods = ["get_users_for_states", "get_interested_users"] + for method_name in required_methods: + if not hasattr(self.custom_presence_router, method_name): + raise Exception( + "PresenceRouter module '%s' must implement all required methods: %s" + % ( + hs.config.presence_router_module_class.__name__, + ", ".join(required_methods), + ) + ) + + async def get_users_for_states( + self, + state_updates: Iterable[UserPresenceState], + ) -> Dict[str, Set[UserPresenceState]]: + """ + Given an iterable of user presence updates, determine where each one + needs to go. + + Args: + state_updates: An iterable of user presence state updates. + + Returns: + A dictionary of user_id -> set of UserPresenceState, indicating which + presence updates each user should receive. + """ + if self.custom_presence_router is not None: + # Ask the custom module + return await self.custom_presence_router.get_users_for_states( + state_updates=state_updates + ) + + # Don't include any extra destinations for presence updates + return {} + + async def get_interested_users(self, user_id: str) -> Union[Set[str], ALL_USERS]: + """ + Retrieve a list of users that `user_id` is interested in receiving the + presence of. This will be in addition to those they share a room with. + Optionally, the object PresenceRouter.ALL_USERS can be returned to indicate + that this user should receive all incoming local and remote presence updates. + + Note that this method will only be called for local users, but can return users + that are local or remote. + + Args: + user_id: A user requesting presence updates. + + Returns: + A set of user IDs to return presence updates for, or ALL_USERS to return all + known updates. + """ + if self.custom_presence_router is not None: + # Ask the custom module for interested users + return await self.custom_presence_router.get_interested_users( + user_id=user_id + ) + + # A custom presence router is not defined. + # Don't report any additional interested users + return set() diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 8babb1ebbe..98bfce22ff 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -44,6 +44,7 @@ from synapse.util.metrics import Measure, measure_func if TYPE_CHECKING: + from synapse.events.presence_router import PresenceRouter from synapse.server import HomeServer logger = logging.getLogger(__name__) @@ -162,6 +163,7 @@ def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.is_mine_id = hs.is_mine_id + self._presence_router = None # type: Optional[PresenceRouter] self._transaction_manager = TransactionManager(hs) self._instance_name = hs.get_instance_name() @@ -584,7 +586,22 @@ async def _process_presence_inner(self, states: List[UserPresenceState]) -> None """Given a list of states populate self.pending_presence_by_dest and poke to send a new transaction to each destination """ - hosts_and_states = await get_interested_remotes(self.store, states, self.state) + # We pull the presence router here instead of __init__ + # to prevent a dependency cycle: + # + # AuthHandler -> Notifier -> FederationSender + # -> PresenceRouter -> ModuleApi -> AuthHandler + if self._presence_router is None: + self._presence_router = self.hs.get_presence_router() + + assert self._presence_router is not None + + hosts_and_states = await get_interested_remotes( + self.store, + self._presence_router, + states, + self.state, + ) for destinations, states in hosts_and_states: for destination in destinations: diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index da92feacc9..c817f2952d 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -25,7 +25,17 @@ import abc import logging from contextlib import contextmanager -from typing import TYPE_CHECKING, Dict, Iterable, List, Set, Tuple +from typing import ( + TYPE_CHECKING, + Dict, + FrozenSet, + Iterable, + List, + Optional, + Set, + Tuple, + Union, +) from prometheus_client import Counter from typing_extensions import ContextManager @@ -34,6 +44,7 @@ from synapse.api.constants import EventTypes, Membership, PresenceState from synapse.api.errors import SynapseError from synapse.api.presence import UserPresenceState +from synapse.events.presence_router import PresenceRouter from synapse.logging.context import run_in_background from synapse.logging.utils import log_function from synapse.metrics import LaterGauge @@ -42,7 +53,7 @@ from synapse.storage.databases.main import DataStore from synapse.types import Collection, JsonDict, UserID, get_domain_from_id from synapse.util.async_helpers import Linearizer -from synapse.util.caches.descriptors import cached +from synapse.util.caches.descriptors import _CacheContext, cached from synapse.util.metrics import Measure from synapse.util.wheel_timer import WheelTimer @@ -209,6 +220,7 @@ def __init__(self, hs: "HomeServer"): self.notifier = hs.get_notifier() self.federation = hs.get_federation_sender() self.state = hs.get_state_handler() + self.presence_router = hs.get_presence_router() self._presence_enabled = hs.config.use_presence federation_registry = hs.get_federation_registry() @@ -653,7 +665,7 @@ async def _persist_and_notify(self, states): """ stream_id, max_token = await self.store.update_presence(states) - parties = await get_interested_parties(self.store, states) + parties = await get_interested_parties(self.store, self.presence_router, states) room_ids_to_states, users_to_states = parties self.notifier.on_new_event( @@ -1041,7 +1053,12 @@ def __init__(self, hs: "HomeServer"): # # Presence -> Notifier -> PresenceEventSource -> Presence # + # Same with get_module_api, get_presence_router + # + # AuthHandler -> Notifier -> PresenceEventSource -> ModuleApi -> AuthHandler self.get_presence_handler = hs.get_presence_handler + self.get_module_api = hs.get_module_api + self.get_presence_router = hs.get_presence_router self.clock = hs.get_clock() self.store = hs.get_datastore() self.state = hs.get_state_handler() @@ -1055,7 +1072,7 @@ async def get_new_events( include_offline=True, explicit_room_id=None, **kwargs - ): + ) -> Tuple[List[UserPresenceState], int]: # The process for getting presence events are: # 1. Get the rooms the user is in. # 2. Get the list of user in the rooms. @@ -1068,7 +1085,17 @@ async def get_new_events( # We don't try and limit the presence updates by the current token, as # sending down the rare duplicate is not a concern. + user_id = user.to_string() + stream_change_cache = self.store.presence_stream_cache + with Measure(self.clock, "presence.get_new_events"): + if user_id in self.get_module_api()._send_full_presence_to_local_users: + # This user has been specified by a module to receive all current, online + # user presence. Removing from_key and setting include_offline to false + # will do effectively this. + from_key = None + include_offline = False + if from_key is not None: from_key = int(from_key) @@ -1091,59 +1118,209 @@ async def get_new_events( # doesn't return. C.f. #5503. return [], max_token - presence = self.get_presence_handler() - stream_change_cache = self.store.presence_stream_cache - + # Figure out which other users this user should receive updates for users_interested_in = await self._get_interested_in(user, explicit_room_id) - user_ids_changed = set() # type: Collection[str] - changed = None - if from_key: - changed = stream_change_cache.get_all_entities_changed(from_key) + # We have a set of users that we're interested in the presence of. We want to + # cross-reference that with the users that have actually changed their presence. - if changed is not None and len(changed) < 500: - assert isinstance(user_ids_changed, set) + # Check whether this user should see all user updates - # For small deltas, its quicker to get all changes and then - # work out if we share a room or they're in our presence list - get_updates_counter.labels("stream").inc() - for other_user_id in changed: - if other_user_id in users_interested_in: - user_ids_changed.add(other_user_id) - else: - # Too many possible updates. Find all users we can see and check - # if any of them have changed. - get_updates_counter.labels("full").inc() + if users_interested_in == PresenceRouter.ALL_USERS: + # Provide presence state for all users + presence_updates = await self._filter_all_presence_updates_for_user( + user_id, include_offline, from_key + ) - if from_key: - user_ids_changed = stream_change_cache.get_entities_changed( - users_interested_in, from_key + # Remove the user from the list of users to receive all presence + if user_id in self.get_module_api()._send_full_presence_to_local_users: + self.get_module_api()._send_full_presence_to_local_users.remove( + user_id ) + + return presence_updates, max_token + + # Make mypy happy. users_interested_in should now be a set + assert not isinstance(users_interested_in, str) + + # The set of users that we're interested in and that have had a presence update. + # We'll actually pull the presence updates for these users at the end. + interested_and_updated_users = ( + set() + ) # type: Union[Set[str], FrozenSet[str]] + + if from_key: + # First get all users that have had a presence update + updated_users = stream_change_cache.get_all_entities_changed(from_key) + + # Cross-reference users we're interested in with those that have had updates. + # Use a slightly-optimised method for processing smaller sets of updates. + if updated_users is not None and len(updated_users) < 500: + # For small deltas, it's quicker to get all changes and then + # cross-reference with the users we're interested in + get_updates_counter.labels("stream").inc() + for other_user_id in updated_users: + if other_user_id in users_interested_in: + # mypy thinks this variable could be a FrozenSet as it's possibly set + # to one in the `get_entities_changed` call below, and `add()` is not + # method on a FrozenSet. That doesn't affect us here though, as + # `interested_and_updated_users` is clearly a set() above. + interested_and_updated_users.add(other_user_id) # type: ignore else: - user_ids_changed = users_interested_in + # Too many possible updates. Find all users we can see and check + # if any of them have changed. + get_updates_counter.labels("full").inc() - updates = await presence.current_state_for_users(user_ids_changed) + interested_and_updated_users = ( + stream_change_cache.get_entities_changed( + users_interested_in, from_key + ) + ) + else: + # No from_key has been specified. Return the presence for all users + # this user is interested in + interested_and_updated_users = users_interested_in + + # Retrieve the current presence state for each user + users_to_state = await self.get_presence_handler().current_state_for_users( + interested_and_updated_users + ) + presence_updates = list(users_to_state.values()) - if include_offline: - return (list(updates.values()), max_token) + # Remove the user from the list of users to receive all presence + if user_id in self.get_module_api()._send_full_presence_to_local_users: + self.get_module_api()._send_full_presence_to_local_users.remove(user_id) + + if not include_offline: + # Filter out offline presence states + presence_updates = self._filter_offline_presence_state(presence_updates) + + return presence_updates, max_token + + async def _filter_all_presence_updates_for_user( + self, + user_id: str, + include_offline: bool, + from_key: Optional[int] = None, + ) -> List[UserPresenceState]: + """ + Computes the presence updates a user should receive. + + First pulls presence updates from the database. Then consults PresenceRouter + for whether any updates should be excluded by user ID. + + Args: + user_id: The User ID of the user to compute presence updates for. + include_offline: Whether to include offline presence states from the results. + from_key: The minimum stream ID of updates to pull from the database + before filtering. + + Returns: + A list of presence states for the given user to receive. + """ + if from_key: + # Only return updates since the last sync + updated_users = self.store.presence_stream_cache.get_all_entities_changed( + from_key + ) + if not updated_users: + updated_users = [] + + # Get the actual presence update for each change + users_to_state = await self.get_presence_handler().current_state_for_users( + updated_users + ) + presence_updates = list(users_to_state.values()) + + if not include_offline: + # Filter out offline states + presence_updates = self._filter_offline_presence_state(presence_updates) else: - return ( - [s for s in updates.values() if s.state != PresenceState.OFFLINE], - max_token, + users_to_state = await self.store.get_presence_for_all_users( + include_offline=include_offline ) + presence_updates = list(users_to_state.values()) + + # TODO: This feels wildly inefficient, and it's unfortunate we need to ask the + # module for information on a number of users when we then only take the info + # for a single user + + # Filter through the presence router + users_to_state_set = await self.get_presence_router().get_users_for_states( + presence_updates + ) + + # We only want the mapping for the syncing user + presence_updates = list(users_to_state_set[user_id]) + + # Return presence information for all users + return presence_updates + + def _filter_offline_presence_state( + self, presence_updates: Iterable[UserPresenceState] + ) -> List[UserPresenceState]: + """Given an iterable containing user presence updates, return a list with any offline + presence states removed. + + Args: + presence_updates: Presence states to filter + + Returns: + A new list with any offline presence states removed. + """ + return [ + update + for update in presence_updates + if update.state != PresenceState.OFFLINE + ] + def get_current_key(self): return self.store.get_current_presence_token() @cached(num_args=2, cache_context=True) - async def _get_interested_in(self, user, explicit_room_id, cache_context): + async def _get_interested_in( + self, + user: UserID, + explicit_room_id: Optional[str] = None, + cache_context: Optional[_CacheContext] = None, + ) -> Union[Set[str], str]: """Returns the set of users that the given user should see presence - updates for + updates for. + + Args: + user: The user to retrieve presence updates for. + explicit_room_id: The users that are in the room will be returned. + + Returns: + A set of user IDs to return presence updates for, or "ALL" to return all + known updates. """ user_id = user.to_string() users_interested_in = set() users_interested_in.add(user_id) # So that we receive our own presence + # cache_context isn't likely to ever be None due to the @cached decorator, + # but we can't have a non-optional argument after the optional argument + # explicit_room_id either. Assert cache_context is not None so we can use it + # without mypy complaining. + assert cache_context + + # Check with the presence router whether we should poll additional users for + # their presence information + additional_users = await self.get_presence_router().get_interested_users( + user.to_string() + ) + if additional_users == PresenceRouter.ALL_USERS: + # If the module requested that this user see the presence updates of *all* + # users, then simply return that instead of calculating what rooms this + # user shares + return PresenceRouter.ALL_USERS + + # Add the additional users from the router + users_interested_in.update(additional_users) + + # Find the users who share a room with this user users_who_share_room = await self.store.get_users_who_share_room_with_user( user_id, on_invalidate=cache_context.invalidate ) @@ -1314,14 +1491,15 @@ def handle_update(prev_state, new_state, is_mine, wheel_timer, now): async def get_interested_parties( - store: DataStore, states: List[UserPresenceState] + store: DataStore, presence_router: PresenceRouter, states: List[UserPresenceState] ) -> Tuple[Dict[str, List[UserPresenceState]], Dict[str, List[UserPresenceState]]]: """Given a list of states return which entities (rooms, users) are interested in the given states. Args: - store - states + store: The homeserver's data store. + presence_router: A module for augmenting the destinations for presence updates. + states: A list of incoming user presence updates. Returns: A 2-tuple of `(room_ids_to_states, users_to_states)`, @@ -1337,11 +1515,22 @@ async def get_interested_parties( # Always notify self users_to_states.setdefault(state.user_id, []).append(state) + # Ask a presence routing module for any additional parties if one + # is loaded. + router_users_to_states = await presence_router.get_users_for_states(states) + + # Update the dictionaries with additional destinations and state to send + for user_id, user_states in router_users_to_states.items(): + users_to_states.setdefault(user_id, []).extend(user_states) + return room_ids_to_states, users_to_states async def get_interested_remotes( - store: DataStore, states: List[UserPresenceState], state_handler: StateHandler + store: DataStore, + presence_router: PresenceRouter, + states: List[UserPresenceState], + state_handler: StateHandler, ) -> List[Tuple[Collection[str], List[UserPresenceState]]]: """Given a list of presence states figure out which remote servers should be sent which. @@ -1349,9 +1538,10 @@ async def get_interested_remotes( All the presence states should be for local users only. Args: - store - states - state_handler + store: The homeserver's data store. + presence_router: A module for augmenting the destinations for presence updates. + states: A list of incoming user presence updates. + state_handler: Returns: A list of 2-tuples of destinations and states, where for @@ -1363,7 +1553,9 @@ async def get_interested_remotes( # First we look up the rooms each user is in (as well as any explicit # subscriptions), then for each distinct room we look up the remote # hosts in those rooms. - room_ids_to_states, users_to_states = await get_interested_parties(store, states) + room_ids_to_states, users_to_states = await get_interested_parties( + store, presence_router, states + ) for room_id, states in room_ids_to_states.items(): hosts = await state_handler.get_current_hosts_in_room(room_id) diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 781e02fbbb..3ecd46c038 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -50,11 +50,20 @@ def __init__(self, hs, auth_handler): self._auth = hs.get_auth() self._auth_handler = auth_handler self._server_name = hs.hostname + self._presence_stream = hs.get_event_sources().sources["presence"] # We expose these as properties below in order to attach a helpful docstring. self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient self._public_room_list_manager = PublicRoomListManager(hs) + # The next time these users sync, they will receive the current presence + # state of all local users. Users are added by send_local_online_presence_to, + # and removed after a successful sync. + # + # We make this a private variable to deter modules from accessing it directly, + # though other classes in Synapse will still do so. + self._send_full_presence_to_local_users = set() + @property def http_client(self): """Allows making outbound HTTP requests to remote resources. @@ -385,6 +394,47 @@ async def create_and_send_event_into_room(self, event_dict: JsonDict) -> EventBa return event + async def send_local_online_presence_to(self, users: Iterable[str]) -> None: + """ + Forces the equivalent of a presence initial_sync for a set of local or remote + users. The users will receive presence for all currently online users that they + are considered interested in. + + Updates to remote users will be sent immediately, whereas local users will receive + them on their next sync attempt. + + Note that this method can only be run on the main or federation_sender worker + processes. + """ + if not self._hs.should_send_federation(): + raise Exception( + "send_local_online_presence_to can only be run " + "on processes that send federation", + ) + + for user in users: + if self._hs.is_mine_id(user): + # Modify SyncHandler._generate_sync_entry_for_presence to call + # presence_source.get_new_events with an empty `from_key` if + # that user's ID were in a list modified by ModuleApi somewhere. + # That user would then get all presence state on next incremental sync. + + # Force a presence initial_sync for this user next time + self._send_full_presence_to_local_users.add(user) + else: + # Retrieve presence state for currently online users that this user + # is considered interested in + presence_events, _ = await self._presence_stream.get_new_events( + UserID.from_string(user), from_key=None, include_offline=False + ) + + # Send to remote destinations + await make_deferred_yieldable( + # We pull the federation sender here as we can only do so on workers + # that support sending presence + self._hs.get_federation_sender().send_presence(presence_events) + ) + class PublicRoomListManager: """Contains methods for adding to, removing from and querying whether a room diff --git a/synapse/server.py b/synapse/server.py index e42f7b1a18..cfb55c230d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -51,6 +51,7 @@ from synapse.crypto.context_factory import RegularPolicyForHTTPS from synapse.crypto.keyring import Keyring from synapse.events.builder import EventBuilderFactory +from synapse.events.presence_router import PresenceRouter from synapse.events.spamcheck import SpamChecker from synapse.events.third_party_rules import ThirdPartyEventRules from synapse.events.utils import EventClientSerializer @@ -425,6 +426,10 @@ def get_typing_writer_handler(self) -> TypingWriterHandler: else: raise Exception("Workers cannot write typing") + @cache_in_self + def get_presence_router(self) -> PresenceRouter: + return PresenceRouter(self) + @cache_in_self def get_typing_handler(self) -> FollowerTypingHandler: if self.config.worker.writers.typing == self.get_instance_name(): diff --git a/tests/events/test_presence_router.py b/tests/events/test_presence_router.py new file mode 100644 index 0000000000..c6e547f11c --- /dev/null +++ b/tests/events/test_presence_router.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict, Iterable, List, Optional, Set, Tuple, Union + +from mock import Mock + +import attr + +from synapse.api.constants import EduTypes +from synapse.events.presence_router import PresenceRouter +from synapse.federation.units import Transaction +from synapse.handlers.presence import UserPresenceState +from synapse.module_api import ModuleApi +from synapse.rest import admin +from synapse.rest.client.v1 import login, presence, room +from synapse.types import JsonDict, StreamToken, create_requester + +from tests.handlers.test_sync import generate_sync_config +from tests.unittest import FederatingHomeserverTestCase, TestCase, override_config + + +@attr.s +class PresenceRouterTestConfig: + users_who_should_receive_all_presence = attr.ib(type=List[str], default=[]) + + +class PresenceRouterTestModule: + def __init__(self, config: PresenceRouterTestConfig, module_api: ModuleApi): + self._config = config + self._module_api = module_api + + async def get_users_for_states( + self, state_updates: Iterable[UserPresenceState] + ) -> Dict[str, Set[UserPresenceState]]: + users_to_state = { + user_id: set(state_updates) + for user_id in self._config.users_who_should_receive_all_presence + } + return users_to_state + + async def get_interested_users( + self, user_id: str + ) -> Union[Set[str], PresenceRouter.ALL_USERS]: + if user_id in self._config.users_who_should_receive_all_presence: + return PresenceRouter.ALL_USERS + + return set() + + @staticmethod + def parse_config(config_dict: dict) -> PresenceRouterTestConfig: + """Parse a configuration dictionary from the homeserver config, do + some validation and return a typed PresenceRouterConfig. + + Args: + config_dict: The configuration dictionary. + + Returns: + A validated config object. + """ + # Initialise a typed config object + config = PresenceRouterTestConfig() + + config.users_who_should_receive_all_presence = config_dict.get( + "users_who_should_receive_all_presence" + ) + + return config + + +class PresenceRouterTestCase(FederatingHomeserverTestCase): + servlets = [ + admin.register_servlets, + login.register_servlets, + room.register_servlets, + presence.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + return self.setup_test_homeserver( + federation_transport_client=Mock(spec=["send_transaction"]), + ) + + def prepare(self, reactor, clock, homeserver): + self.sync_handler = self.hs.get_sync_handler() + self.module_api = homeserver.get_module_api() + + @override_config( + { + "presence": { + "presence_router": { + "module": __name__ + ".PresenceRouterTestModule", + "config": { + "users_who_should_receive_all_presence": [ + "@presence_gobbler:test", + ] + }, + } + }, + "send_federation": True, + } + ) + def test_receiving_all_presence(self): + """Test that a user that does not share a room with another other can receive + presence for them, due to presence routing. + """ + # Create a user who should receive all presence of others + self.presence_receiving_user_id = self.register_user( + "presence_gobbler", "monkey" + ) + self.presence_receiving_user_tok = self.login("presence_gobbler", "monkey") + + # And two users who should not have any special routing + self.other_user_one_id = self.register_user("other_user_one", "monkey") + self.other_user_one_tok = self.login("other_user_one", "monkey") + self.other_user_two_id = self.register_user("other_user_two", "monkey") + self.other_user_two_tok = self.login("other_user_two", "monkey") + + # Put the other two users in a room with each other + room_id = self.helper.create_room_as( + self.other_user_one_id, tok=self.other_user_one_tok + ) + + self.helper.invite( + room_id, + self.other_user_one_id, + self.other_user_two_id, + tok=self.other_user_one_tok, + ) + self.helper.join(room_id, self.other_user_two_id, tok=self.other_user_two_tok) + # User one sends some presence + send_presence_update( + self, + self.other_user_one_id, + self.other_user_one_tok, + "online", + "boop", + ) + + # Check that the presence receiving user gets user one's presence when syncing + presence_updates, sync_token = sync_presence( + self, self.presence_receiving_user_id + ) + self.assertEqual(len(presence_updates), 1) + + presence_update = presence_updates[0] # type: UserPresenceState + self.assertEqual(presence_update.user_id, self.other_user_one_id) + self.assertEqual(presence_update.state, "online") + self.assertEqual(presence_update.status_msg, "boop") + + # Have all three users send presence + send_presence_update( + self, + self.other_user_one_id, + self.other_user_one_tok, + "online", + "user_one", + ) + send_presence_update( + self, + self.other_user_two_id, + self.other_user_two_tok, + "online", + "user_two", + ) + send_presence_update( + self, + self.presence_receiving_user_id, + self.presence_receiving_user_tok, + "online", + "presence_gobbler", + ) + + # Check that the presence receiving user gets everyone's presence + presence_updates, _ = sync_presence( + self, self.presence_receiving_user_id, sync_token + ) + self.assertEqual(len(presence_updates), 3) + + # But that User One only get itself and User Two's presence + presence_updates, _ = sync_presence(self, self.other_user_one_id) + self.assertEqual(len(presence_updates), 2) + + found = False + for update in presence_updates: + if update.user_id == self.other_user_two_id: + self.assertEqual(update.state, "online") + self.assertEqual(update.status_msg, "user_two") + found = True + + self.assertTrue(found) + + @override_config( + { + "presence": { + "presence_router": { + "module": __name__ + ".PresenceRouterTestModule", + "config": { + "users_who_should_receive_all_presence": [ + "@presence_gobbler1:test", + "@presence_gobbler2:test", + "@far_away_person:island", + ] + }, + } + }, + "send_federation": True, + } + ) + def test_send_local_online_presence_to_with_module(self): + """Tests that send_local_presence_to_users sends local online presence to a set + of specified local and remote users, with a custom PresenceRouter module enabled. + """ + # Create a user who will send presence updates + self.other_user_id = self.register_user("other_user", "monkey") + self.other_user_tok = self.login("other_user", "monkey") + + # And another two users that will also send out presence updates, as well as receive + # theirs and everyone else's + self.presence_receiving_user_one_id = self.register_user( + "presence_gobbler1", "monkey" + ) + self.presence_receiving_user_one_tok = self.login("presence_gobbler1", "monkey") + self.presence_receiving_user_two_id = self.register_user( + "presence_gobbler2", "monkey" + ) + self.presence_receiving_user_two_tok = self.login("presence_gobbler2", "monkey") + + # Have all three users send some presence updates + send_presence_update( + self, + self.other_user_id, + self.other_user_tok, + "online", + "I'm online!", + ) + send_presence_update( + self, + self.presence_receiving_user_one_id, + self.presence_receiving_user_one_tok, + "online", + "I'm also online!", + ) + send_presence_update( + self, + self.presence_receiving_user_two_id, + self.presence_receiving_user_two_tok, + "unavailable", + "I'm in a meeting!", + ) + + # Mark each presence-receiving user for receiving all user presence + self.get_success( + self.module_api.send_local_online_presence_to( + [ + self.presence_receiving_user_one_id, + self.presence_receiving_user_two_id, + ] + ) + ) + + # Perform a sync for each user + + # The other user should only receive their own presence + presence_updates, _ = sync_presence(self, self.other_user_id) + self.assertEqual(len(presence_updates), 1) + + presence_update = presence_updates[0] # type: UserPresenceState + self.assertEqual(presence_update.user_id, self.other_user_id) + self.assertEqual(presence_update.state, "online") + self.assertEqual(presence_update.status_msg, "I'm online!") + + # Whereas both presence receiving users should receive everyone's presence updates + presence_updates, _ = sync_presence(self, self.presence_receiving_user_one_id) + self.assertEqual(len(presence_updates), 3) + presence_updates, _ = sync_presence(self, self.presence_receiving_user_two_id) + self.assertEqual(len(presence_updates), 3) + + # Test that sending to a remote user works + remote_user_id = "@far_away_person:island" + + # Note that due to the remote user being in our module's + # users_who_should_receive_all_presence config, they would have + # received user presence updates already. + # + # Thus we reset the mock, and try sending all online local user + # presence again + self.hs.get_federation_transport_client().send_transaction.reset_mock() + + # Broadcast local user online presence + self.get_success( + self.module_api.send_local_online_presence_to([remote_user_id]) + ) + + # Check that the expected presence updates were sent + expected_users = [ + self.other_user_id, + self.presence_receiving_user_one_id, + self.presence_receiving_user_two_id, + ] + + calls = ( + self.hs.get_federation_transport_client().send_transaction.call_args_list + ) + for call in calls: + federation_transaction = call.args[0] # type: Transaction + + # Get the sent EDUs in this transaction + edus = federation_transaction.get_dict()["edus"] + + for edu in edus: + # Make sure we're only checking presence-type EDUs + if edu["edu_type"] != EduTypes.Presence: + continue + + # EDUs can contain multiple presence updates + for presence_update in edu["content"]["push"]: + # Check for presence updates that contain the user IDs we're after + expected_users.remove(presence_update["user_id"]) + + # Ensure that no offline states are being sent out + self.assertNotEqual(presence_update["presence"], "offline") + + self.assertEqual(len(expected_users), 0) + + +def send_presence_update( + testcase: TestCase, + user_id: str, + access_token: str, + presence_state: str, + status_message: Optional[str] = None, +) -> JsonDict: + # Build the presence body + body = {"presence": presence_state} + if status_message: + body["status_msg"] = status_message + + # Update the user's presence state + channel = testcase.make_request( + "PUT", "/presence/%s/status" % (user_id,), body, access_token=access_token + ) + testcase.assertEqual(channel.code, 200) + + return channel.json_body + + +def sync_presence( + testcase: TestCase, + user_id: str, + since_token: Optional[StreamToken] = None, +) -> Tuple[List[UserPresenceState], StreamToken]: + """Perform a sync request for the given user and return the user presence updates + they've received, as well as the next_batch token. + + This method assumes testcase.sync_handler points to the homeserver's sync handler. + + Args: + testcase: The testcase that is currently being run. + user_id: The ID of the user to generate a sync response for. + since_token: An optional token to indicate from at what point to sync from. + + Returns: + A tuple containing a list of presence updates, and the sync response's + next_batch token. + """ + requester = create_requester(user_id) + sync_config = generate_sync_config(requester.user.to_string()) + sync_result = testcase.get_success( + testcase.sync_handler.wait_for_sync_for_user( + requester, sync_config, since_token + ) + ) + + return sync_result.presence, sync_result.next_batch diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index e62586142e..8e950f25c5 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -37,7 +37,7 @@ def prepare(self, reactor, clock, hs): def test_wait_for_sync_for_user_auth_blocking(self): user_id1 = "@user1:test" user_id2 = "@user2:test" - sync_config = self._generate_sync_config(user_id1) + sync_config = generate_sync_config(user_id1) requester = create_requester(user_id1) self.reactor.advance(100) # So we get not 0 time @@ -60,7 +60,7 @@ def test_wait_for_sync_for_user_auth_blocking(self): self.auth_blocking._hs_disabled = False - sync_config = self._generate_sync_config(user_id2) + sync_config = generate_sync_config(user_id2) requester = create_requester(user_id2) e = self.get_failure( @@ -69,11 +69,12 @@ def test_wait_for_sync_for_user_auth_blocking(self): ) self.assertEquals(e.value.errcode, Codes.RESOURCE_LIMIT_EXCEEDED) - def _generate_sync_config(self, user_id): - return SyncConfig( - user=UserID(user_id.split(":")[0][1:], user_id.split(":")[1]), - filter_collection=DEFAULT_FILTER_COLLECTION, - is_guest=False, - request_key="request_key", - device_id="device_id", - ) + +def generate_sync_config(user_id: str) -> SyncConfig: + return SyncConfig( + user=UserID(user_id.split(":")[0][1:], user_id.split(":")[1]), + filter_collection=DEFAULT_FILTER_COLLECTION, + is_guest=False, + request_key="request_key", + device_id="device_id", + ) diff --git a/tests/module_api/test_api.py b/tests/module_api/test_api.py index edacd1b566..1d1fceeecf 100644 --- a/tests/module_api/test_api.py +++ b/tests/module_api/test_api.py @@ -14,25 +14,37 @@ # limitations under the License. from mock import Mock +from synapse.api.constants import EduTypes from synapse.events import EventBase +from synapse.federation.units import Transaction +from synapse.handlers.presence import UserPresenceState from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client.v1 import login, presence, room from synapse.types import create_requester -from tests.unittest import HomeserverTestCase +from tests.events.test_presence_router import send_presence_update, sync_presence +from tests.test_utils.event_injection import inject_member_event +from tests.unittest import FederatingHomeserverTestCase, override_config -class ModuleApiTestCase(HomeserverTestCase): +class ModuleApiTestCase(FederatingHomeserverTestCase): servlets = [ admin.register_servlets, login.register_servlets, room.register_servlets, + presence.register_servlets, ] def prepare(self, reactor, clock, homeserver): self.store = homeserver.get_datastore() self.module_api = homeserver.get_module_api() self.event_creation_handler = homeserver.get_event_creation_handler() + self.sync_handler = homeserver.get_sync_handler() + + def make_homeserver(self, reactor, clock): + return self.setup_test_homeserver( + federation_transport_client=Mock(spec=["send_transaction"]), + ) def test_can_register_user(self): """Tests that an external module can register a user""" @@ -205,3 +217,160 @@ def test_public_rooms(self): ) ) self.assertFalse(is_in_public_rooms) + + # The ability to send federation is required by send_local_online_presence_to. + @override_config({"send_federation": True}) + def test_send_local_online_presence_to(self): + """Tests that send_local_presence_to_users sends local online presence to local users.""" + # Create a user who will send presence updates + self.presence_receiver_id = self.register_user("presence_receiver", "monkey") + self.presence_receiver_tok = self.login("presence_receiver", "monkey") + + # And another user that will send presence updates out + self.presence_sender_id = self.register_user("presence_sender", "monkey") + self.presence_sender_tok = self.login("presence_sender", "monkey") + + # Put them in a room together so they will receive each other's presence updates + room_id = self.helper.create_room_as( + self.presence_receiver_id, + tok=self.presence_receiver_tok, + ) + self.helper.join(room_id, self.presence_sender_id, tok=self.presence_sender_tok) + + # Presence sender comes online + send_presence_update( + self, + self.presence_sender_id, + self.presence_sender_tok, + "online", + "I'm online!", + ) + + # Presence receiver should have received it + presence_updates, sync_token = sync_presence(self, self.presence_receiver_id) + self.assertEqual(len(presence_updates), 1) + + presence_update = presence_updates[0] # type: UserPresenceState + self.assertEqual(presence_update.user_id, self.presence_sender_id) + self.assertEqual(presence_update.state, "online") + + # Syncing again should result in no presence updates + presence_updates, sync_token = sync_presence( + self, self.presence_receiver_id, sync_token + ) + self.assertEqual(len(presence_updates), 0) + + # Trigger sending local online presence + self.get_success( + self.module_api.send_local_online_presence_to( + [ + self.presence_receiver_id, + ] + ) + ) + + # Presence receiver should have received online presence again + presence_updates, sync_token = sync_presence( + self, self.presence_receiver_id, sync_token + ) + self.assertEqual(len(presence_updates), 1) + + presence_update = presence_updates[0] # type: UserPresenceState + self.assertEqual(presence_update.user_id, self.presence_sender_id) + self.assertEqual(presence_update.state, "online") + + # Presence sender goes offline + send_presence_update( + self, + self.presence_sender_id, + self.presence_sender_tok, + "offline", + "I slink back into the darkness.", + ) + + # Trigger sending local online presence + self.get_success( + self.module_api.send_local_online_presence_to( + [ + self.presence_receiver_id, + ] + ) + ) + + # Presence receiver should *not* have received offline state + presence_updates, sync_token = sync_presence( + self, self.presence_receiver_id, sync_token + ) + self.assertEqual(len(presence_updates), 0) + + @override_config({"send_federation": True}) + def test_send_local_online_presence_to_federation(self): + """Tests that send_local_presence_to_users sends local online presence to remote users.""" + # Create a user who will send presence updates + self.presence_sender_id = self.register_user("presence_sender", "monkey") + self.presence_sender_tok = self.login("presence_sender", "monkey") + + # And a room they're a part of + room_id = self.helper.create_room_as( + self.presence_sender_id, + tok=self.presence_sender_tok, + ) + + # Mark them as online + send_presence_update( + self, + self.presence_sender_id, + self.presence_sender_tok, + "online", + "I'm online!", + ) + + # Make up a remote user to send presence to + remote_user_id = "@far_away_person:island" + + # Create a join membership event for the remote user into the room. + # This allows presence information to flow from one user to the other. + self.get_success( + inject_member_event( + self.hs, + room_id, + sender=remote_user_id, + target=remote_user_id, + membership="join", + ) + ) + + # The remote user would have received the existing room members' presence + # when they joined the room. + # + # Thus we reset the mock, and try sending online local user + # presence again + self.hs.get_federation_transport_client().send_transaction.reset_mock() + + # Broadcast local user online presence + self.get_success( + self.module_api.send_local_online_presence_to([remote_user_id]) + ) + + # Check that a presence update was sent as part of a federation transaction + found_update = False + calls = ( + self.hs.get_federation_transport_client().send_transaction.call_args_list + ) + for call in calls: + federation_transaction = call.args[0] # type: Transaction + + # Get the sent EDUs in this transaction + edus = federation_transaction.get_dict()["edus"] + + for edu in edus: + # Make sure we're only checking presence-type EDUs + if edu["edu_type"] != EduTypes.Presence: + continue + + # EDUs can contain multiple presence updates + for presence_update in edu["content"]["push"]: + if presence_update["user_id"] == self.presence_sender_id: + found_update = True + + self.assertTrue(found_update) From 0d87c6bd121d043d5474cc20e352794ff8cf5625 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 6 Apr 2021 16:32:04 +0100 Subject: [PATCH 018/619] Don't report anything from GaugeBucketCollector metrics until data is present (#8926) This PR modifies `GaugeBucketCollector` to only report data once it has been updated, rather than initially reporting a value of 0. Fixes zero values being reported for some metrics on startup until a background job to update the metric's value runs later. --- changelog.d/8926.bugfix | 1 + synapse/metrics/__init__.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 changelog.d/8926.bugfix diff --git a/changelog.d/8926.bugfix b/changelog.d/8926.bugfix new file mode 100644 index 0000000000..aad7bd83ce --- /dev/null +++ b/changelog.d/8926.bugfix @@ -0,0 +1 @@ +Prevent `synapse_forward_extremities` and `synapse_excess_extremity_events` Prometheus metrics from initially reporting zero-values after startup. diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index 3b499efc07..13a5bc4558 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -214,7 +214,12 @@ class GaugeBucketCollector: Prometheus, and optimise for that case. """ - __slots__ = ("_name", "_documentation", "_bucket_bounds", "_metric") + __slots__ = ( + "_name", + "_documentation", + "_bucket_bounds", + "_metric", + ) def __init__( self, @@ -242,11 +247,16 @@ def __init__( if self._bucket_bounds[-1] != float("inf"): self._bucket_bounds.append(float("inf")) - self._metric = self._values_to_metric([]) + # We initially set this to None. We won't report metrics until + # this has been initialised after a successful data update + self._metric = None # type: Optional[GaugeHistogramMetricFamily] + registry.register(self) def collect(self): - yield self._metric + # Don't report metrics unless we've already collected some data + if self._metric is not None: + yield self._metric def update_data(self, values: Iterable[float]): """Update the data to be reported by the metric From 48d44ab1425ffac721b7d407823c2315cda1929a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 8 Apr 2021 08:01:14 -0400 Subject: [PATCH 019/619] Record more information into structured logs. (#9654) Records additional request information into the structured logs, e.g. the requester, IP address, etc. --- changelog.d/9654.feature | 1 + synapse/http/site.py | 112 ++++++++++++++---- synapse/logging/context.py | 70 +++++++++-- synapse/metrics/background_process_metrics.py | 18 ++- synapse/replication/tcp/protocol.py | 5 +- tests/crypto/test_keyring.py | 23 ++-- tests/logging/test_terse_json.py | 70 ++++++++++- tests/unittest.py | 2 +- tests/util/caches/test_descriptors.py | 7 +- tests/util/test_logcontext.py | 35 ++---- 10 files changed, 255 insertions(+), 88 deletions(-) create mode 100644 changelog.d/9654.feature diff --git a/changelog.d/9654.feature b/changelog.d/9654.feature new file mode 100644 index 0000000000..a54c96cf19 --- /dev/null +++ b/changelog.d/9654.feature @@ -0,0 +1 @@ +Include request information in structured logging output. diff --git a/synapse/http/site.py b/synapse/http/site.py index 47754aff43..c0c873ce32 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -14,7 +14,7 @@ import contextlib import logging import time -from typing import Optional, Type, Union +from typing import Optional, Tuple, Type, Union import attr from zope.interface import implementer @@ -26,7 +26,11 @@ from synapse.config.server import ListenerConfig from synapse.http import get_request_user_agent, redact_uri from synapse.http.request_metrics import RequestMetrics, requests_counter -from synapse.logging.context import LoggingContext, PreserveLoggingContext +from synapse.logging.context import ( + ContextRequest, + LoggingContext, + PreserveLoggingContext, +) from synapse.types import Requester logger = logging.getLogger(__name__) @@ -63,7 +67,7 @@ def __init__(self, channel, *args, **kw): # The requester, if authenticated. For federation requests this is the # server name, for client requests this is the Requester object. - self.requester = None # type: Optional[Union[Requester, str]] + self._requester = None # type: Optional[Union[Requester, str]] # we can't yet create the logcontext, as we don't know the method. self.logcontext = None # type: Optional[LoggingContext] @@ -93,6 +97,31 @@ def __repr__(self): self.site.site_tag, ) + @property + def requester(self) -> Optional[Union[Requester, str]]: + return self._requester + + @requester.setter + def requester(self, value: Union[Requester, str]) -> None: + # Store the requester, and update some properties based on it. + + # This should only be called once. + assert self._requester is None + + self._requester = value + + # A logging context should exist by now (and have a ContextRequest). + assert self.logcontext is not None + assert self.logcontext.request is not None + + ( + requester, + authenticated_entity, + ) = self.get_authenticated_entity() + self.logcontext.request.requester = requester + # If there's no authenticated entity, it was the requester. + self.logcontext.request.authenticated_entity = authenticated_entity or requester + def get_request_id(self): return "%s-%i" % (self.get_method(), self.request_seq) @@ -126,13 +155,60 @@ def get_method(self) -> str: return self.method.decode("ascii") return method + def get_authenticated_entity(self) -> Tuple[Optional[str], Optional[str]]: + """ + Get the "authenticated" entity of the request, which might be the user + performing the action, or a user being puppeted by a server admin. + + Returns: + A tuple: + The first item is a string representing the user making the request. + + The second item is a string or None representing the user who + authenticated when making this request. See + Requester.authenticated_entity. + """ + # Convert the requester into a string that we can log + if isinstance(self._requester, str): + return self._requester, None + elif isinstance(self._requester, Requester): + requester = self._requester.user.to_string() + authenticated_entity = self._requester.authenticated_entity + + # If this is a request where the target user doesn't match the user who + # authenticated (e.g. and admin is puppetting a user) then we return both. + if self._requester.user.to_string() != authenticated_entity: + return requester, authenticated_entity + + return requester, None + elif self._requester is not None: + # This shouldn't happen, but we log it so we don't lose information + # and can see that we're doing something wrong. + return repr(self._requester), None # type: ignore[unreachable] + + return None, None + def render(self, resrc): # this is called once a Resource has been found to serve the request; in our # case the Resource in question will normally be a JsonResource. # create a LogContext for this request request_id = self.get_request_id() - self.logcontext = LoggingContext(request_id, request=request_id) + self.logcontext = LoggingContext( + request_id, + request=ContextRequest( + request_id=request_id, + ip_address=self.getClientIP(), + site_tag=self.site.site_tag, + # The requester is going to be unknown at this point. + requester=None, + authenticated_entity=None, + method=self.get_method(), + url=self.get_redacted_uri(), + protocol=self.clientproto.decode("ascii", errors="replace"), + user_agent=get_request_user_agent(self), + ), + ) # override the Server header which is set by twisted self.setHeader("Server", self.site.server_version_string) @@ -277,25 +353,6 @@ def _finished_processing(self): # to the client (nb may be negative) response_send_time = self.finish_time - self._processing_finished_time - # Convert the requester into a string that we can log - authenticated_entity = None - if isinstance(self.requester, str): - authenticated_entity = self.requester - elif isinstance(self.requester, Requester): - authenticated_entity = self.requester.authenticated_entity - - # If this is a request where the target user doesn't match the user who - # authenticated (e.g. and admin is puppetting a user) then we log both. - if self.requester.user.to_string() != authenticated_entity: - authenticated_entity = "{},{}".format( - authenticated_entity, - self.requester.user.to_string(), - ) - elif self.requester is not None: - # This shouldn't happen, but we log it so we don't lose information - # and can see that we're doing something wrong. - authenticated_entity = repr(self.requester) # type: ignore[unreachable] - user_agent = get_request_user_agent(self, "-") code = str(self.code) @@ -305,6 +362,13 @@ def _finished_processing(self): code += "!" log_level = logging.INFO if self._should_log_request() else logging.DEBUG + + # If this is a request where the target user doesn't match the user who + # authenticated (e.g. and admin is puppetting a user) then we log both. + requester, authenticated_entity = self.get_authenticated_entity() + if authenticated_entity: + requester = "{}.{}".format(authenticated_entity, requester) + self.site.access_logger.log( log_level, "%s - %s - {%s}" @@ -312,7 +376,7 @@ def _finished_processing(self): ' %sB %s "%s %s %s" "%s" [%d dbevts]', self.getClientIP(), self.site.site_tag, - authenticated_entity, + requester, processing_time, response_send_time, usage.ru_utime, diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 03cf3c2b8e..e78343f554 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -22,7 +22,6 @@ See doc/log_contexts.rst for details on how this works. """ - import inspect import logging import threading @@ -30,6 +29,7 @@ import warnings from typing import TYPE_CHECKING, Optional, Tuple, TypeVar, Union +import attr from typing_extensions import Literal from twisted.internet import defer, threads @@ -181,6 +181,29 @@ def __sub__(self, other: "ContextResourceUsage") -> "ContextResourceUsage": return res +@attr.s(slots=True) +class ContextRequest: + """ + A bundle of attributes from the SynapseRequest object. + + This exists to: + + * Avoid a cycle between LoggingContext and SynapseRequest. + * Be a single variable that can be passed from parent LoggingContexts to + their children. + """ + + request_id = attr.ib(type=str) + ip_address = attr.ib(type=str) + site_tag = attr.ib(type=str) + requester = attr.ib(type=Optional[str]) + authenticated_entity = attr.ib(type=Optional[str]) + method = attr.ib(type=str) + url = attr.ib(type=str) + protocol = attr.ib(type=str) + user_agent = attr.ib(type=str) + + LoggingContextOrSentinel = Union["LoggingContext", "_Sentinel"] @@ -256,7 +279,7 @@ def __init__( self, name: Optional[str] = None, parent_context: "Optional[LoggingContext]" = None, - request: Optional[str] = None, + request: Optional[ContextRequest] = None, ) -> None: self.previous_context = current_context() self.name = name @@ -281,7 +304,11 @@ def __init__( self.parent_context = parent_context if self.parent_context is not None: - self.parent_context.copy_to(self) + # we track the current request_id + self.request = self.parent_context.request + + # we also track the current scope: + self.scope = self.parent_context.scope if request is not None: # the request param overrides the request from the parent context @@ -289,7 +316,7 @@ def __init__( def __str__(self) -> str: if self.request: - return str(self.request) + return self.request.request_id return "%s@%x" % (self.name, id(self)) @classmethod @@ -556,8 +583,23 @@ def filter(self, record: logging.LogRecord) -> Literal[True]: # we end up in a death spiral of infinite loops, so let's check, for # robustness' sake. if context is not None: - # Logging is interested in the request. - record.request = context.request # type: ignore + # Logging is interested in the request ID. Note that for backwards + # compatibility this is stored as the "request" on the record. + record.request = str(context) # type: ignore + + # Add some data from the HTTP request. + request = context.request + if request is None: + return True + + record.ip_address = request.ip_address # type: ignore + record.site_tag = request.site_tag # type: ignore + record.requester = request.requester # type: ignore + record.authenticated_entity = request.authenticated_entity # type: ignore + record.method = request.method # type: ignore + record.url = request.url # type: ignore + record.protocol = request.protocol # type: ignore + record.user_agent = request.user_agent # type: ignore return True @@ -630,8 +672,8 @@ def set_current_context(context: LoggingContextOrSentinel) -> LoggingContextOrSe def nested_logging_context(suffix: str) -> LoggingContext: """Creates a new logging context as a child of another. - The nested logging context will have a 'request' made up of the parent context's - request, plus the given suffix. + The nested logging context will have a 'name' made up of the parent context's + name, plus the given suffix. CPU/db usage stats will be added to the parent context's on exit. @@ -641,7 +683,7 @@ def nested_logging_context(suffix: str) -> LoggingContext: # ... do stuff Args: - suffix: suffix to add to the parent context's 'request'. + suffix: suffix to add to the parent context's 'name'. Returns: LoggingContext: new logging context. @@ -653,11 +695,17 @@ def nested_logging_context(suffix: str) -> LoggingContext: ) parent_context = None prefix = "" + request = None else: assert isinstance(curr_context, LoggingContext) parent_context = curr_context - prefix = str(parent_context.request) - return LoggingContext(parent_context=parent_context, request=prefix + "-" + suffix) + prefix = str(parent_context.name) + request = parent_context.request + return LoggingContext( + prefix + "-" + suffix, + parent_context=parent_context, + request=request, + ) def preserve_fn(f): diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index b56986d8e7..e8a9096c03 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -16,7 +16,7 @@ import logging import threading from functools import wraps -from typing import TYPE_CHECKING, Dict, Optional, Set +from typing import TYPE_CHECKING, Dict, Optional, Set, Union from prometheus_client.core import REGISTRY, Counter, Gauge @@ -199,11 +199,11 @@ async def run(): _background_process_start_count.labels(desc).inc() _background_process_in_flight_count.labels(desc).inc() - with BackgroundProcessLoggingContext(desc, "%s-%i" % (desc, count)) as context: + with BackgroundProcessLoggingContext(desc, count) as context: try: ctx = noop_context_manager() if bg_start_span: - ctx = start_active_span(desc, tags={"request_id": context.request}) + ctx = start_active_span(desc, tags={"request_id": str(context)}) with ctx: return await maybe_awaitable(func(*args, **kwargs)) except Exception: @@ -242,13 +242,19 @@ class BackgroundProcessLoggingContext(LoggingContext): processes. """ - __slots__ = ["_proc"] + __slots__ = ["_id", "_proc"] - def __init__(self, name: str, request: Optional[str] = None): - super().__init__(name, request=request) + def __init__(self, name: str, id: Optional[Union[int, str]] = None): + super().__init__(name) + self._id = id self._proc = _BackgroundProcess(name, self) + def __str__(self) -> str: + if self._id is not None: + return "%s-%s" % (self.name, self._id) + return "%s@%x" % (self.name, id(self)) + def start(self, rusage: "Optional[resource._RUsage]"): """Log context has started running (again).""" diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index e829add257..d10d574246 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -184,8 +184,9 @@ def __init__(self, clock: Clock, handler: "ReplicationCommandHandler"): # a logcontext which we use for processing incoming commands. We declare it as a # background process so that the CPU stats get reported to prometheus. - ctx_name = "replication-conn-%s" % self.conn_id - self._logging_context = BackgroundProcessLoggingContext(ctx_name, ctx_name) + self._logging_context = BackgroundProcessLoggingContext( + "replication-conn", self.conn_id + ) def connectionMade(self): logger.info("[%s] Connection established", self.id()) diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index 30fcc4c1bf..946482b7e7 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -16,6 +16,7 @@ from mock import Mock +import attr import canonicaljson import signedjson.key import signedjson.sign @@ -68,6 +69,11 @@ def sign_response(self, res): signedjson.sign.sign_json(res, self.server_name, self.key) +@attr.s(slots=True) +class FakeRequest: + id = attr.ib() + + @logcontext_clean class KeyringTestCase(unittest.HomeserverTestCase): def check_context(self, val, expected): @@ -89,7 +95,7 @@ def test_verify_json_objects_for_server_awaits_previous_requests(self): first_lookup_deferred = Deferred() async def first_lookup_fetch(keys_to_fetch): - self.assertEquals(current_context().request, "context_11") + self.assertEquals(current_context().request.id, "context_11") self.assertEqual(keys_to_fetch, {"server10": {get_key_id(key1): 0}}) await make_deferred_yieldable(first_lookup_deferred) @@ -102,9 +108,7 @@ async def first_lookup_fetch(keys_to_fetch): mock_fetcher.get_keys.side_effect = first_lookup_fetch async def first_lookup(): - with LoggingContext("context_11") as context_11: - context_11.request = "context_11" - + with LoggingContext("context_11", request=FakeRequest("context_11")): res_deferreds = kr.verify_json_objects_for_server( [("server10", json1, 0, "test10"), ("server11", {}, 0, "test11")] ) @@ -130,7 +134,7 @@ async def first_lookup(): # should block rather than start a second call async def second_lookup_fetch(keys_to_fetch): - self.assertEquals(current_context().request, "context_12") + self.assertEquals(current_context().request.id, "context_12") return { "server10": { get_key_id(key1): FetchKeyResult(get_verify_key(key1), 100) @@ -142,9 +146,7 @@ async def second_lookup_fetch(keys_to_fetch): second_lookup_state = [0] async def second_lookup(): - with LoggingContext("context_12") as context_12: - context_12.request = "context_12" - + with LoggingContext("context_12", request=FakeRequest("context_12")): res_deferreds_2 = kr.verify_json_objects_for_server( [("server10", json1, 0, "test")] ) @@ -589,10 +591,7 @@ def get_key_id(key): @defer.inlineCallbacks def run_in_context(f, *args, **kwargs): - with LoggingContext("testctx") as ctx: - # we set the "request" prop to make it easier to follow what's going on in the - # logs. - ctx.request = "testctx" + with LoggingContext("testctx"): rv = yield f(*args, **kwargs) return rv diff --git a/tests/logging/test_terse_json.py b/tests/logging/test_terse_json.py index 48a74e2eee..bfe0d11c93 100644 --- a/tests/logging/test_terse_json.py +++ b/tests/logging/test_terse_json.py @@ -12,15 +12,20 @@ # 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 json import logging -from io import StringIO +from io import BytesIO, StringIO + +from mock import Mock, patch + +from twisted.web.server import Request +from synapse.http.site import SynapseRequest from synapse.logging._terse_json import JsonFormatter, TerseJsonFormatter from synapse.logging.context import LoggingContext, LoggingContextFilter from tests.logging import LoggerCleanupMixin +from tests.server import FakeChannel from tests.unittest import TestCase @@ -120,7 +125,7 @@ def test_with_context(self): handler.addFilter(LoggingContextFilter()) logger = self.get_logger(handler) - with LoggingContext(request="test"): + with LoggingContext("name"): logger.info("Hello there, %s!", "wally") log = self.get_log_line() @@ -134,4 +139,61 @@ def test_with_context(self): ] self.assertCountEqual(log.keys(), expected_log_keys) self.assertEqual(log["log"], "Hello there, wally!") - self.assertEqual(log["request"], "test") + self.assertTrue(log["request"].startswith("name@")) + + def test_with_request_context(self): + """ + Information from the logging context request should be added to the JSON response. + """ + handler = logging.StreamHandler(self.output) + handler.setFormatter(JsonFormatter()) + handler.addFilter(LoggingContextFilter()) + logger = self.get_logger(handler) + + # A full request isn't needed here. + site = Mock(spec=["site_tag", "server_version_string", "getResourceFor"]) + site.site_tag = "test-site" + site.server_version_string = "Server v1" + request = SynapseRequest(FakeChannel(site, None)) + # Call requestReceived to finish instantiating the object. + request.content = BytesIO() + # Partially skip some of the internal processing of SynapseRequest. + request._started_processing = Mock() + request.request_metrics = Mock(spec=["name"]) + with patch.object(Request, "render"): + request.requestReceived(b"POST", b"/_matrix/client/versions", b"1.1") + + # Also set the requester to ensure the processing works. + request.requester = "@foo:test" + + with LoggingContext(parent_context=request.logcontext): + logger.info("Hello there, %s!", "wally") + + log = self.get_log_line() + + # The terse logger includes additional request information, if possible. + expected_log_keys = [ + "log", + "level", + "namespace", + "request", + "ip_address", + "site_tag", + "requester", + "authenticated_entity", + "method", + "url", + "protocol", + "user_agent", + ] + self.assertCountEqual(log.keys(), expected_log_keys) + self.assertEqual(log["log"], "Hello there, wally!") + self.assertTrue(log["request"].startswith("POST-")) + self.assertEqual(log["ip_address"], "127.0.0.1") + self.assertEqual(log["site_tag"], "test-site") + self.assertEqual(log["requester"], "@foo:test") + self.assertEqual(log["authenticated_entity"], "@foo:test") + self.assertEqual(log["method"], "POST") + self.assertEqual(log["url"], "/_matrix/client/versions") + self.assertEqual(log["protocol"], "1.1") + self.assertEqual(log["user_agent"], "") diff --git a/tests/unittest.py b/tests/unittest.py index 58a4daa1ec..57b6a395c7 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -471,7 +471,7 @@ def setup_test_homeserver(self, *args, **kwargs): kwargs["config"] = config_obj async def run_bg_updates(): - with LoggingContext("run_bg_updates", request="run_bg_updates-1"): + with LoggingContext("run_bg_updates"): while not await stor.db_pool.updates.has_completed_background_updates(): await stor.db_pool.updates.do_next_background_update(1) diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index afb11b9caf..e434e21aee 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -661,14 +661,13 @@ def fn(self, arg1, arg2): @descriptors.cachedList("fn", "args1") async def list_fn(self, args1, arg2): - assert current_context().request == "c1" + assert current_context().name == "c1" # we want this to behave like an asynchronous function await run_on_reactor() - assert current_context().request == "c1" + assert current_context().name == "c1" return self.mock(args1, arg2) - with LoggingContext() as c1: - c1.request = "c1" + with LoggingContext("c1") as c1: obj = Cls() obj.mock.return_value = {10: "fish", 20: "chips"} d1 = obj.list_fn([10, 20], 2) diff --git a/tests/util/test_logcontext.py b/tests/util/test_logcontext.py index 58ee918f65..5d9c4665aa 100644 --- a/tests/util/test_logcontext.py +++ b/tests/util/test_logcontext.py @@ -17,11 +17,10 @@ class LoggingContextTestCase(unittest.TestCase): def _check_test_key(self, value): - self.assertEquals(current_context().request, value) + self.assertEquals(current_context().name, value) def test_with_context(self): - with LoggingContext() as context_one: - context_one.request = "test" + with LoggingContext("test"): self._check_test_key("test") @defer.inlineCallbacks @@ -30,15 +29,13 @@ def test_sleep(self): @defer.inlineCallbacks def competing_callback(): - with LoggingContext() as competing_context: - competing_context.request = "competing" + with LoggingContext("competing"): yield clock.sleep(0) self._check_test_key("competing") reactor.callLater(0, competing_callback) - with LoggingContext() as context_one: - context_one.request = "one" + with LoggingContext("one"): yield clock.sleep(0) self._check_test_key("one") @@ -47,9 +44,7 @@ def _test_run_in_background(self, function): callback_completed = [False] - with LoggingContext() as context_one: - context_one.request = "one" - + with LoggingContext("one"): # fire off function, but don't wait on it. d2 = run_in_background(function) @@ -133,9 +128,7 @@ def blocking_function(): sentinel_context = current_context() - with LoggingContext() as context_one: - context_one.request = "one" - + with LoggingContext("one"): d1 = make_deferred_yieldable(blocking_function()) # make sure that the context was reset by make_deferred_yieldable self.assertIs(current_context(), sentinel_context) @@ -149,9 +142,7 @@ def blocking_function(): def test_make_deferred_yieldable_with_chained_deferreds(self): sentinel_context = current_context() - with LoggingContext() as context_one: - context_one.request = "one" - + with LoggingContext("one"): d1 = make_deferred_yieldable(_chained_deferred_function()) # make sure that the context was reset by make_deferred_yieldable self.assertIs(current_context(), sentinel_context) @@ -166,9 +157,7 @@ def test_make_deferred_yieldable_on_non_deferred(self): """Check that make_deferred_yieldable does the right thing when its argument isn't actually a deferred""" - with LoggingContext() as context_one: - context_one.request = "one" - + with LoggingContext("one"): d1 = make_deferred_yieldable("bum") self._check_test_key("one") @@ -177,9 +166,9 @@ def test_make_deferred_yieldable_on_non_deferred(self): self._check_test_key("one") def test_nested_logging_context(self): - with LoggingContext(request="foo"): + with LoggingContext("foo"): nested_context = nested_logging_context(suffix="bar") - self.assertEqual(nested_context.request, "foo-bar") + self.assertEqual(nested_context.name, "foo-bar") @defer.inlineCallbacks def test_make_deferred_yieldable_with_await(self): @@ -193,9 +182,7 @@ async def blocking_function(): sentinel_context = current_context() - with LoggingContext() as context_one: - context_one.request = "one" - + with LoggingContext("one"): d1 = make_deferred_yieldable(blocking_function()) # make sure that the context was reset by make_deferred_yieldable self.assertIs(current_context(), sentinel_context) From 452991527a3e76715f0571f81f036ea20de0be36 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 8 Apr 2021 08:28:32 -0400 Subject: [PATCH 020/619] MSC3083: Check for space membership during a local join of restricted rooms. (#9735) When joining a room with join rules set to 'restricted', check if the user is a member of the spaces defined in the 'allow' key of the join rules. This only applies to an experimental room version, as defined in MSC3083. --- changelog.d/9735.feature | 1 + scripts-dev/complement.sh | 2 +- synapse/handlers/room_member.py | 75 ++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9735.feature diff --git a/changelog.d/9735.feature b/changelog.d/9735.feature new file mode 100644 index 0000000000..c2c74f13d5 --- /dev/null +++ b/changelog.d/9735.feature @@ -0,0 +1 @@ +Add experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index 31cc20a826..b77187472f 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -46,4 +46,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -tags synapse_blacklist -count=1 $EXTRA_COMPLEMENT_ARGS ./tests +COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -tags synapse_blacklist,msc3083 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 1cf12f3255..894ef859f4 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple from synapse import types -from synapse.api.constants import AccountDataTypes, EventTypes, Membership +from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules, Membership from synapse.api.errors import ( AuthError, Codes, @@ -29,6 +29,7 @@ SynapseError, ) from synapse.api.ratelimiting import Ratelimiter +from synapse.api.room_versions import RoomVersion from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID @@ -178,6 +179,62 @@ async def ratelimit_invite( await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id) + async def _can_join_without_invite( + self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str + ) -> bool: + """ + Check whether a user can join a room without an invite. + + When joining a room with restricted joined rules (as defined in MSC3083), + the membership of spaces must be checked during join. + + Args: + state_ids: The state of the room as it currently is. + room_version: The room version of the room being joined. + user_id: The user joining the room. + + Returns: + True if the user can join the room, false otherwise. + """ + # This only applies to room versions which support the new join rule. + if not room_version.msc3083_join_rules: + return True + + # If there's no join rule, then it defaults to public (so this doesn't apply). + join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) + if not join_rules_event_id: + return True + + # If the join rule is not restricted, this doesn't apply. + join_rules_event = await self.store.get_event(join_rules_event_id) + if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: + return True + + # If allowed is of the wrong form, then only allow invited users. + allowed_spaces = join_rules_event.content.get("allow", []) + if not isinstance(allowed_spaces, list): + return False + + # Get the list of joined rooms and see if there's an overlap. + joined_rooms = await self.store.get_rooms_for_user(user_id) + + # Pull out the other room IDs, invalid data gets filtered. + for space in allowed_spaces: + if not isinstance(space, dict): + continue + + space_id = space.get("space") + if not isinstance(space_id, str): + continue + + # The user was joined to one of the spaces specified, they can join + # this room! + if space_id in joined_rooms: + return True + + # The user was not in any of the required spaces. + return False + async def _local_membership_update( self, requester: Requester, @@ -235,9 +292,25 @@ async def _local_membership_update( if event.membership == Membership.JOIN: newly_joined = True + user_is_invited = False if prev_member_event_id: prev_member_event = await self.store.get_event(prev_member_event_id) newly_joined = prev_member_event.membership != Membership.JOIN + user_is_invited = prev_member_event.membership == Membership.INVITE + + # If the member is not already in the room and is not accepting an invite, + # check if they should be allowed access via membership in a space. + if ( + newly_joined + and not user_is_invited + and not await self._can_join_without_invite( + prev_state_ids, event.room_version, user_id + ) + ): + raise AuthError( + 403, + "You do not belong to any of the required spaces to join this room.", + ) # Only rate-limit if the user actually joined the room, otherwise we'll end # up blocking profile updates. From cb657eb2f85b29fc8e6ea7b692677ec63d76472b Mon Sep 17 00:00:00 2001 From: Johannes Wienke Date: Thu, 8 Apr 2021 14:49:14 +0200 Subject: [PATCH 021/619] Put opencontainers labels to the final image (#9765) They don't make any sense on the intermediate builder image. The final images needs them to be of use for anyone. Signed-off-by: Johannes Wienke --- changelog.d/9765.docker | 1 + docker/Dockerfile | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/9765.docker diff --git a/changelog.d/9765.docker b/changelog.d/9765.docker new file mode 100644 index 0000000000..f170a36714 --- /dev/null +++ b/changelog.d/9765.docker @@ -0,0 +1 @@ +Move opencontainers labels to the final Docker image such that users can inspect them. diff --git a/docker/Dockerfile b/docker/Dockerfile index 5b7bf02776..4f5cd06d72 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -18,11 +18,6 @@ ARG PYTHON_VERSION=3.8 ### FROM docker.io/python:${PYTHON_VERSION}-slim as builder -LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse' -LABEL org.opencontainers.image.documentation='https://github.com/matrix-org/synapse/blob/master/docker/README.md' -LABEL org.opencontainers.image.source='https://github.com/matrix-org/synapse.git' -LABEL org.opencontainers.image.licenses='Apache-2.0' - # install the OS build deps RUN apt-get update && apt-get install -y \ build-essential \ @@ -66,6 +61,11 @@ RUN pip install --prefix="/install" --no-deps --no-warn-script-location /synapse FROM docker.io/python:${PYTHON_VERSION}-slim +LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse' +LABEL org.opencontainers.image.documentation='https://github.com/matrix-org/synapse/blob/master/docker/README.md' +LABEL org.opencontainers.image.source='https://github.com/matrix-org/synapse.git' +LABEL org.opencontainers.image.licenses='Apache-2.0' + RUN apt-get update && apt-get install -y \ curl \ gosu \ From 5edd91caecbdb7b8fefaf016cb452eefe3f56772 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 16:21:32 +0100 Subject: [PATCH 022/619] Fix incompatibility with tox 2.5 Apparently on tox 2.5, `usedevelop` overrides `skip_install`, so we end up trying to install the full dependencies even for the `-old` environment. --- changelog.d/9769.misc | 1 + tox.ini | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 changelog.d/9769.misc diff --git a/changelog.d/9769.misc b/changelog.d/9769.misc new file mode 100644 index 0000000000..042a50615f --- /dev/null +++ b/changelog.d/9769.misc @@ -0,0 +1 @@ +Fix incompatibility with `tox` 2.5. diff --git a/tox.ini b/tox.ini index 9ff70fe312..8224edaef9 100644 --- a/tox.ini +++ b/tox.ini @@ -104,7 +104,8 @@ usedevelop=true # A test suite for the oldest supported versions of Python libraries, to catch # any uses of APIs not available in them. [testenv:py35-old] -skip_install=True +skip_install = true +usedevelop = false deps = # Old automat version for Twisted Automat == 0.3.0 @@ -136,7 +137,8 @@ commands = python -m synmark {posargs:} [testenv:packaging] -skip_install=True +skip_install = true +usedevelop = false deps = check-manifest commands = @@ -154,7 +156,8 @@ extras = lint commands = isort -c --df --sp setup.cfg {[base]lint_targets} [testenv:check-newsfragment] -skip_install = True +skip_install = true +usedevelop = false deps = towncrier>=18.6.0rc1 commands = python -m towncrier.check --compare-with=origin/develop @@ -163,7 +166,8 @@ commands = commands = {toxinidir}/scripts-dev/generate_sample_config --check [testenv:combine] -skip_install = True +skip_install = true +usedevelop = false deps = coverage pip>=10 ; python_version >= '3.6' @@ -173,14 +177,16 @@ commands= coverage report [testenv:cov-erase] -skip_install = True +skip_install = true +usedevelop = false deps = coverage commands= coverage erase [testenv:cov-html] -skip_install = True +skip_install = true +usedevelop = false deps = coverage commands= From 906065c75b629e0d2962359e3f5f531b1ba4fbc1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 16:41:35 +0100 Subject: [PATCH 023/619] unpin olddeps build from py36 --- .buildkite/scripts/test_old_deps.sh | 6 +++--- tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.buildkite/scripts/test_old_deps.sh b/.buildkite/scripts/test_old_deps.sh index 9fe5b696b0..3753f41a40 100755 --- a/.buildkite/scripts/test_old_deps.sh +++ b/.buildkite/scripts/test_old_deps.sh @@ -1,16 +1,16 @@ #!/usr/bin/env bash # this script is run by buildkite in a plain `xenial` container; it installs the -# minimal requirements for tox and hands over to the py35-old tox environment. +# minimal requirements for tox and hands over to the py3-old tox environment. set -ex apt-get update -apt-get install -y python3.5 python3.5-dev python3-pip libxml2-dev libxslt-dev xmlsec1 zlib1g-dev tox +apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt-dev xmlsec1 zlib1g-dev tox export LANG="C.UTF-8" # Prevent virtualenv from auto-updating pip to an incompatible version export VIRTUALENV_NO_DOWNLOAD=1 -exec tox -e py35-old,combine +exec tox -e py3-old,combine diff --git a/tox.ini b/tox.ini index 8224edaef9..b2bc6f23ef 100644 --- a/tox.ini +++ b/tox.ini @@ -74,7 +74,7 @@ commands = # we use "env" rather than putting a value in `setenv` so that it is not # inherited by other tox environments. # - # keep this in sync with the copy in `testenv:py35-old`. + # keep this in sync with the copy in `testenv:py3-old`. # /usr/bin/env COVERAGE_PROCESS_START={toxinidir}/.coveragerc "{envbindir}/trial" {env:TRIAL_FLAGS:} {posargs:tests} {env:TOXSUFFIX:} @@ -103,7 +103,7 @@ usedevelop=true # A test suite for the oldest supported versions of Python libraries, to catch # any uses of APIs not available in them. -[testenv:py35-old] +[testenv:py3-old] skip_install = true usedevelop = false deps = From abade34633da2002014a1dd7e4da3f558e610582 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 12:00:47 +0100 Subject: [PATCH 024/619] Require py36 and Postgres 9.6 --- changelog.d/9766.feature | 1 + setup.py | 2 +- synapse/storage/engines/postgres.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9766.feature diff --git a/changelog.d/9766.feature b/changelog.d/9766.feature new file mode 100644 index 0000000000..030bdb4561 --- /dev/null +++ b/changelog.d/9766.feature @@ -0,0 +1 @@ +Synapse now requires Python 3.6 or later and Postgres 9.6 or later. diff --git a/setup.py b/setup.py index 29e9971dc1..4e9e333c60 100755 --- a/setup.py +++ b/setup.py @@ -123,7 +123,7 @@ def exec_file(path_segments): zip_safe=False, long_description=long_description, long_description_content_type="text/x-rst", - python_requires="~=3.5", + python_requires="~=3.6", classifiers=[ "Development Status :: 5 - Production/Stable", "Topic :: Communications :: Chat", diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index 80a3558aec..d95f88b3e9 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -47,8 +47,8 @@ def check_database(self, db_conn, allow_outdated_version: bool = False): self._version = db_conn.server_version # Are we on a supported PostgreSQL version? - if not allow_outdated_version and self._version < 90500: - raise RuntimeError("Synapse requires PostgreSQL 9.5+ or above.") + if not allow_outdated_version and self._version < 90600: + raise RuntimeError("Synapse requires PostgreSQL 9.6 or above.") with db_conn.cursor() as txn: txn.execute("SHOW SERVER_ENCODING") From 3ada9b42640137d908fa2db64e78f3f79d11dd3a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 13:45:19 +0100 Subject: [PATCH 025/619] Drop support for sqlite<3.22 as well --- changelog.d/9766.feature | 2 +- synapse/storage/database.py | 62 ++++------------------------- synapse/storage/engines/_base.py | 8 ---- synapse/storage/engines/postgres.py | 7 ---- synapse/storage/engines/sqlite.py | 15 +++---- tests/storage/test_database.py | 12 +----- 6 files changed, 14 insertions(+), 92 deletions(-) diff --git a/changelog.d/9766.feature b/changelog.d/9766.feature index 030bdb4561..ecf49cfee1 100644 --- a/changelog.d/9766.feature +++ b/changelog.d/9766.feature @@ -1 +1 @@ -Synapse now requires Python 3.6 or later and Postgres 9.6 or later. +Synapse now requires Python 3.6 or later. It also requires Postgres 9.6 or later or SQLite 3.22 or later. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 94590e7b45..a2f016a7ef 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -2060,68 +2060,20 @@ def make_in_list_sql_clause( def make_tuple_comparison_clause( - database_engine: BaseDatabaseEngine, keys: List[Tuple[str, KV]] + _database_engine: BaseDatabaseEngine, keys: List[Tuple[str, KV]] ) -> Tuple[str, List[KV]]: """Returns a tuple comparison SQL clause - Depending what the SQL engine supports, builds a SQL clause that looks like either - "(a, b) > (?, ?)", or "(a > ?) OR (a == ? AND b > ?)". + Builds a SQL clause that looks like "(a, b) > (?, ?)" Args: - database_engine + _database_engine keys: A set of (column, value) pairs to be compared. Returns: A tuple of SQL query and the args """ - if database_engine.supports_tuple_comparison: - return ( - "(%s) > (%s)" % (",".join(k[0] for k in keys), ",".join("?" for _ in keys)), - [k[1] for k in keys], - ) - - # we want to build a clause - # (a > ?) OR - # (a == ? AND b > ?) OR - # (a == ? AND b == ? AND c > ?) - # ... - # (a == ? AND b == ? AND ... AND z > ?) - # - # or, equivalently: - # - # (a > ? OR (a == ? AND - # (b > ? OR (b == ? AND - # ... - # (y > ? OR (y == ? AND - # z > ? - # )) - # ... - # )) - # )) - # - # which itself is equivalent to (and apparently easier for the query optimiser): - # - # (a >= ? AND (a > ? OR - # (b >= ? AND (b > ? OR - # ... - # (y >= ? AND (y > ? OR - # z > ? - # )) - # ... - # )) - # )) - # - # - - clause = "" - args = [] # type: List[KV] - for k, v in keys[:-1]: - clause = clause + "(%s >= ? AND (%s > ? OR " % (k, k) - args.extend([v, v]) - - (k, v) = keys[-1] - clause += "%s > ?" % (k,) - args.append(v) - - clause += "))" * (len(keys) - 1) - return clause, args + return ( + "(%s) > (%s)" % (",".join(k[0] for k in keys), ",".join("?" for _ in keys)), + [k[1] for k in keys], + ) diff --git a/synapse/storage/engines/_base.py b/synapse/storage/engines/_base.py index cca839c70f..21db1645d3 100644 --- a/synapse/storage/engines/_base.py +++ b/synapse/storage/engines/_base.py @@ -42,14 +42,6 @@ def can_native_upsert(self) -> bool: """ ... - @property - @abc.abstractmethod - def supports_tuple_comparison(self) -> bool: - """ - Do we support comparing tuples, i.e. `(a, b) > (c, d)`? - """ - ... - @property @abc.abstractmethod def supports_using_any_list(self) -> bool: diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index d95f88b3e9..dba8cc51d3 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -129,13 +129,6 @@ def can_native_upsert(self): """ return True - @property - def supports_tuple_comparison(self): - """ - Do we support comparing tuples, i.e. `(a, b) > (c, d)`? - """ - return True - @property def supports_using_any_list(self): """Do we support using `a = ANY(?)` and passing a list""" diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index b87e7798da..f4f16456f2 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -56,14 +56,6 @@ def can_native_upsert(self): """ return self.module.sqlite_version_info >= (3, 24, 0) - @property - def supports_tuple_comparison(self): - """ - Do we support comparing tuples, i.e. `(a, b) > (c, d)`? This requires - SQLite 3.15+. - """ - return self.module.sqlite_version_info >= (3, 15, 0) - @property def supports_using_any_list(self): """Do we support using `a = ANY(?)` and passing a list""" @@ -72,8 +64,11 @@ def supports_using_any_list(self): def check_database(self, db_conn, allow_outdated_version: bool = False): if not allow_outdated_version: version = self.module.sqlite_version_info - if version < (3, 11, 0): - raise RuntimeError("Synapse requires sqlite 3.11 or above.") + # Synapse is untested against older SQLite versions, and we don't want + # to let users upgrade to a version of Synapse with broken support for their + # sqlite version, because it risks leaving them with a half-upgraded db. + if version < (3, 22, 0): + raise RuntimeError("Synapse requires sqlite 3.22 or above.") def check_new_database(self, txn): """Gets called when setting up a brand new database. This allows us to diff --git a/tests/storage/test_database.py b/tests/storage/test_database.py index 5a77c84962..1bba58fc03 100644 --- a/tests/storage/test_database.py +++ b/tests/storage/test_database.py @@ -36,17 +36,7 @@ def _stub_db_engine(**kwargs) -> BaseDatabaseEngine: class TupleComparisonClauseTestCase(unittest.TestCase): def test_native_tuple_comparison(self): - db_engine = _stub_db_engine(supports_tuple_comparison=True) + db_engine = _stub_db_engine() clause, args = make_tuple_comparison_clause(db_engine, [("a", 1), ("b", 2)]) self.assertEqual(clause, "(a,b) > (?,?)") self.assertEqual(args, [1, 2]) - - def test_emulated_tuple_comparison(self): - db_engine = _stub_db_engine(supports_tuple_comparison=False) - clause, args = make_tuple_comparison_clause( - db_engine, [("a", 1), ("b", 2), ("c", 3)] - ) - self.assertEqual( - clause, "(a >= ? AND (a > ? OR (b >= ? AND (b > ? OR c > ?))))" - ) - self.assertEqual(args, [1, 1, 2, 2, 3]) From 9278eb701e7f5358f80e416f3330e6bc380d100f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 12:03:12 +0100 Subject: [PATCH 026/619] drop support for stretch and xenial --- scripts-dev/build_debian_packages | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index d0685c8b35..bddc441df2 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -18,11 +18,9 @@ import threading from concurrent.futures import ThreadPoolExecutor DISTS = ( - "debian:stretch", "debian:buster", "debian:bullseye", "debian:sid", - "ubuntu:xenial", "ubuntu:bionic", "ubuntu:focal", "ubuntu:groovy", From 04ff88139a70bcc852480229672626de7e620b9f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 12:09:48 +0100 Subject: [PATCH 027/619] Update tox.ini to remove py35 --- tox.ini | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tox.ini b/tox.ini index b2bc6f23ef..998b04b224 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,8 @@ [tox] -envlist = packaging, py35, py36, py37, py38, py39, check_codestyle, check_isort +envlist = packaging, py36, py37, py38, py39, check_codestyle, check_isort + +# we require tox>=2.3.2 for the fix to https://github.com/tox-dev/tox/issues/208 +minversion = 2.3.2 [base] deps = @@ -48,6 +51,7 @@ deps = extras = # install the optional dependendencies for tox environments without # '-noextras' in their name + # (this requires tox 3) !noextras: all test @@ -74,8 +78,6 @@ commands = # we use "env" rather than putting a value in `setenv` so that it is not # inherited by other tox environments. # - # keep this in sync with the copy in `testenv:py3-old`. - # /usr/bin/env COVERAGE_PROCESS_START={toxinidir}/.coveragerc "{envbindir}/trial" {env:TRIAL_FLAGS:} {posargs:tests} {env:TOXSUFFIX:} # As of twisted 16.4, trial tries to import the tests as a package (previously @@ -121,11 +123,7 @@ commands = # Install Synapse itself. This won't update any libraries. pip install -e ".[test]" - # we have to duplicate the command from `testenv` rather than refer to it - # as `{[testenv]commands}`, because we run on ubuntu xenial, which has - # tox 2.3.1, and https://github.com/tox-dev/tox/issues/208. - # - /usr/bin/env COVERAGE_PROCESS_START={toxinidir}/.coveragerc "{envbindir}/trial" {env:TRIAL_FLAGS:} {posargs:tests} {env:TOXSUFFIX:} + {[testenv]commands} [testenv:benchmark] deps = From 77e56deffcf685ca5d0a264059f0c326e53e99af Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 15:26:49 +0100 Subject: [PATCH 028/619] update test_old_deps script --- .buildkite/scripts/test_old_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/scripts/test_old_deps.sh b/.buildkite/scripts/test_old_deps.sh index 3753f41a40..9270d55f04 100755 --- a/.buildkite/scripts/test_old_deps.sh +++ b/.buildkite/scripts/test_old_deps.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# this script is run by buildkite in a plain `xenial` container; it installs the +# this script is run by buildkite in a plain `bionic` container; it installs the # minimal requirements for tox and hands over to the py3-old tox environment. set -ex From 3a569fb2000e972efe2e145d57ffd9441ee41665 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 8 Apr 2021 17:30:01 +0100 Subject: [PATCH 029/619] Fix sharded federation sender sometimes using 100% CPU. We pull all destinations requiring catchup from the DB in batches. However, if all those destinations get filtered out (due to the federation sender being sharded), then the `last_processed` destination doesn't get updated, and we keep requesting the same set repeatedly. --- changelog.d/9770.bugfix | 1 + synapse/federation/sender/__init__.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9770.bugfix diff --git a/changelog.d/9770.bugfix b/changelog.d/9770.bugfix new file mode 100644 index 0000000000..baf93138de --- /dev/null +++ b/changelog.d/9770.bugfix @@ -0,0 +1 @@ +Fix bug where sharded federation senders could get stuck repeatedly querying the DB in a loop, using lots of CPU. diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 98bfce22ff..d821dcbf6a 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -734,16 +734,18 @@ async def _wake_destinations_needing_catchup(self) -> None: self._catchup_after_startup_timer = None break + last_processed = destinations_to_wake[-1] + destinations_to_wake = [ d for d in destinations_to_wake if self._federation_shard_config.should_handle(self._instance_name, d) ] - for last_processed in destinations_to_wake: + for destination in destinations_to_wake: logger.info( "Destination %s has outstanding catch-up, waking up.", last_processed, ) - self.wake_destination(last_processed) + self.wake_destination(destination) await self.clock.sleep(CATCH_UP_STARTUP_INTERVAL_SEC) From 24c58ebfc992eec4b37c5aead8d9eef8e7afdd16 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 8 Apr 2021 18:29:57 +0100 Subject: [PATCH 030/619] remove unused param on `make_tuple_comparison_clause` --- synapse/storage/database.py | 5 +---- synapse/storage/databases/main/client_ips.py | 1 - synapse/storage/databases/main/devices.py | 2 +- synapse/storage/databases/main/events_bg_updates.py | 1 - tests/storage/test_database.py | 3 +-- 5 files changed, 3 insertions(+), 9 deletions(-) diff --git a/synapse/storage/database.py b/synapse/storage/database.py index a2f016a7ef..b302cd5786 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -2059,15 +2059,12 @@ def make_in_list_sql_clause( KV = TypeVar("KV") -def make_tuple_comparison_clause( - _database_engine: BaseDatabaseEngine, keys: List[Tuple[str, KV]] -) -> Tuple[str, List[KV]]: +def make_tuple_comparison_clause(keys: List[Tuple[str, KV]]) -> Tuple[str, List[KV]]: """Returns a tuple comparison SQL clause Builds a SQL clause that looks like "(a, b) > (?, ?)" Args: - _database_engine keys: A set of (column, value) pairs to be compared. Returns: diff --git a/synapse/storage/databases/main/client_ips.py b/synapse/storage/databases/main/client_ips.py index 6d18e692b0..ea3c15fd0e 100644 --- a/synapse/storage/databases/main/client_ips.py +++ b/synapse/storage/databases/main/client_ips.py @@ -298,7 +298,6 @@ def _devices_last_seen_update_txn(txn): # times, which is fine. where_clause, where_args = make_tuple_comparison_clause( - self.database_engine, [("user_id", last_user_id), ("device_id", last_device_id)], ) diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index d327e9aa0b..9bf8ba888f 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -985,7 +985,7 @@ async def _remove_duplicate_outbound_pokes(self, progress, batch_size): def _txn(txn): clause, args = make_tuple_comparison_clause( - self.db_pool.engine, [(x, last_row[x]) for x in KEY_COLS] + [(x, last_row[x]) for x in KEY_COLS] ) sql = """ SELECT stream_id, destination, user_id, device_id, MAX(ts) AS ts diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index 78367ea58d..79e7df6ca9 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -838,7 +838,6 @@ def _calculate_chain_cover_txn( # We want to do a `(topological_ordering, stream_ordering) > (?,?)` # comparison, but that is not supported on older SQLite versions tuple_clause, tuple_args = make_tuple_comparison_clause( - self.database_engine, [ ("events.room_id", last_room_id), ("topological_ordering", last_depth), diff --git a/tests/storage/test_database.py b/tests/storage/test_database.py index 1bba58fc03..a906d30e73 100644 --- a/tests/storage/test_database.py +++ b/tests/storage/test_database.py @@ -36,7 +36,6 @@ def _stub_db_engine(**kwargs) -> BaseDatabaseEngine: class TupleComparisonClauseTestCase(unittest.TestCase): def test_native_tuple_comparison(self): - db_engine = _stub_db_engine() - clause, args = make_tuple_comparison_clause(db_engine, [("a", 1), ("b", 2)]) + clause, args = make_tuple_comparison_clause([("a", 1), ("b", 2)]) self.assertEqual(clause, "(a,b) > (?,?)") self.assertEqual(args, [1, 2]) From 2ca4e349e9d0c606d802ae15c06089080fa4f27e Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 8 Apr 2021 23:38:54 +0200 Subject: [PATCH 031/619] Bugbear: Add Mutable Parameter fixes (#9682) Part of #9366 Adds in fixes for B006 and B008, both relating to mutable parameter lint errors. Signed-off-by: Jonathan de Jong --- changelog.d/9682.misc | 1 + contrib/cmdclient/console.py | 5 +++- contrib/cmdclient/http.py | 24 +++++++++++++---- setup.cfg | 4 +-- synapse/appservice/scheduler.py | 6 ++--- synapse/config/ratelimiting.py | 6 +++-- synapse/events/__init__.py | 14 +++++++--- synapse/federation/units.py | 5 ++-- synapse/handlers/appservice.py | 4 +-- synapse/handlers/federation.py | 2 +- synapse/handlers/message.py | 11 +++++--- synapse/handlers/register.py | 4 ++- synapse/handlers/sync.py | 8 +++--- synapse/http/client.py | 4 +-- synapse/http/proxyagent.py | 6 +++-- synapse/logging/opentracing.py | 3 ++- synapse/module_api/__init__.py | 12 +++++---- synapse/notifier.py | 19 ++++++++------ synapse/storage/database.py | 20 +++++++++----- synapse/storage/databases/main/events.py | 7 +++-- .../storage/databases/main/group_server.py | 4 ++- synapse/storage/databases/main/state.py | 6 +++-- synapse/storage/databases/state/bg_updates.py | 5 +++- synapse/storage/databases/state/store.py | 5 ++-- synapse/storage/state.py | 26 ++++++++++++------- synapse/storage/util/id_generators.py | 11 ++++++-- synapse/util/caches/lrucache.py | 14 +++++----- .../test_matrix_federation_agent.py | 15 ++++++++--- tests/replication/_base.py | 4 +-- .../replication/slave/storage/test_events.py | 16 +++++++----- tests/rest/client/v1/test_rooms.py | 5 +++- tests/rest/client/v1/utils.py | 14 +++++++--- tests/rest/client/v2_alpha/test_relations.py | 5 ++-- tests/storage/test_id_generators.py | 14 +++++----- tests/storage/test_redaction.py | 10 +++++-- tests/test_state.py | 5 ++-- tests/test_visibility.py | 7 +++-- tests/util/test_ratelimitutils.py | 6 +++-- 38 files changed, 224 insertions(+), 113 deletions(-) create mode 100644 changelog.d/9682.misc diff --git a/changelog.d/9682.misc b/changelog.d/9682.misc new file mode 100644 index 0000000000..428a466fac --- /dev/null +++ b/changelog.d/9682.misc @@ -0,0 +1 @@ +Introduce flake8-bugbear to the test suite and fix some of its lint violations. diff --git a/contrib/cmdclient/console.py b/contrib/cmdclient/console.py index 67e032244e..856dd437db 100755 --- a/contrib/cmdclient/console.py +++ b/contrib/cmdclient/console.py @@ -24,6 +24,7 @@ import time import urllib from http import TwistedHttpClient +from typing import Optional import nacl.encoding import nacl.signing @@ -718,7 +719,7 @@ def _run_and_pprint( method, path, data=None, - query_params={"access_token": None}, + query_params: Optional[dict] = None, alt_text=None, ): """Runs an HTTP request and pretty prints the output. @@ -729,6 +730,8 @@ def _run_and_pprint( data: Raw JSON data if any query_params: dict of query parameters to add to the url """ + query_params = query_params or {"access_token": None} + url = self._url() + path if "access_token" in query_params: query_params["access_token"] = self._tok() diff --git a/contrib/cmdclient/http.py b/contrib/cmdclient/http.py index 851e80c25b..1cf913756e 100644 --- a/contrib/cmdclient/http.py +++ b/contrib/cmdclient/http.py @@ -16,6 +16,7 @@ import json import urllib from pprint import pformat +from typing import Optional from twisted.internet import defer, reactor from twisted.web.client import Agent, readBody @@ -85,8 +86,9 @@ def get_json(self, url, args=None): body = yield readBody(response) defer.returnValue(json.loads(body)) - def _create_put_request(self, url, json_data, headers_dict={}): + def _create_put_request(self, url, json_data, headers_dict: Optional[dict] = None): """Wrapper of _create_request to issue a PUT request""" + headers_dict = headers_dict or {} if "Content-Type" not in headers_dict: raise defer.error(RuntimeError("Must include Content-Type header for PUTs")) @@ -95,14 +97,22 @@ def _create_put_request(self, url, json_data, headers_dict={}): "PUT", url, producer=_JsonProducer(json_data), headers_dict=headers_dict ) - def _create_get_request(self, url, headers_dict={}): + def _create_get_request(self, url, headers_dict: Optional[dict] = None): """Wrapper of _create_request to issue a GET request""" - return self._create_request("GET", url, headers_dict=headers_dict) + return self._create_request("GET", url, headers_dict=headers_dict or {}) @defer.inlineCallbacks def do_request( - self, method, url, data=None, qparams=None, jsonreq=True, headers={} + self, + method, + url, + data=None, + qparams=None, + jsonreq=True, + headers: Optional[dict] = None, ): + headers = headers or {} + if qparams: url = "%s?%s" % (url, urllib.urlencode(qparams, True)) @@ -123,8 +133,12 @@ def do_request( defer.returnValue(json.loads(body)) @defer.inlineCallbacks - def _create_request(self, method, url, producer=None, headers_dict={}): + def _create_request( + self, method, url, producer=None, headers_dict: Optional[dict] = None + ): """Creates and sends a request to the given url""" + headers_dict = headers_dict or {} + headers_dict["User-Agent"] = ["Synapse Cmd Client"] retries_left = 5 diff --git a/setup.cfg b/setup.cfg index 7329eed213..5fdb51ac73 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,8 +18,8 @@ ignore = # E203: whitespace before ':' (which is contrary to pep8?) # E731: do not assign a lambda expression, use a def # E501: Line too long (black enforces this for us) -# B00*: Subsection of the bugbear suite (TODO: add in remaining fixes) -ignore=W503,W504,E203,E731,E501,B006,B007,B008 +# B007: Subsection of the bugbear suite (TODO: add in remaining fixes) +ignore=W503,W504,E203,E731,E501,B007 [isort] line_length = 88 diff --git a/synapse/appservice/scheduler.py b/synapse/appservice/scheduler.py index 366c476f80..5203ffe90f 100644 --- a/synapse/appservice/scheduler.py +++ b/synapse/appservice/scheduler.py @@ -49,7 +49,7 @@ components. """ import logging -from typing import List +from typing import List, Optional from synapse.appservice import ApplicationService, ApplicationServiceState from synapse.events import EventBase @@ -191,11 +191,11 @@ async def send( self, service: ApplicationService, events: List[EventBase], - ephemeral: List[JsonDict] = [], + ephemeral: Optional[List[JsonDict]] = None, ): try: txn = await self.store.create_appservice_txn( - service=service, events=events, ephemeral=ephemeral + service=service, events=events, ephemeral=ephemeral or [] ) service_is_up = await self._is_service_up(service) if service_is_up: diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index 3f3997f4e5..7a8d5851c4 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict +from typing import Dict, Optional from ._base import Config @@ -21,8 +21,10 @@ class RateLimitConfig: def __init__( self, config: Dict[str, float], - defaults={"per_second": 0.17, "burst_count": 3.0}, + defaults: Optional[Dict[str, float]] = None, ): + defaults = defaults or {"per_second": 0.17, "burst_count": 3.0} + self.per_second = config.get("per_second", defaults["per_second"]) self.burst_count = int(config.get("burst_count", defaults["burst_count"])) diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 8f6b955d17..f9032e3697 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -330,9 +330,11 @@ def __init__( self, event_dict: JsonDict, room_version: RoomVersion, - internal_metadata_dict: JsonDict = {}, + internal_metadata_dict: Optional[JsonDict] = None, rejected_reason: Optional[str] = None, ): + internal_metadata_dict = internal_metadata_dict or {} + event_dict = dict(event_dict) # Signatures is a dict of dicts, and this is faster than doing a @@ -386,9 +388,11 @@ def __init__( self, event_dict: JsonDict, room_version: RoomVersion, - internal_metadata_dict: JsonDict = {}, + internal_metadata_dict: Optional[JsonDict] = None, rejected_reason: Optional[str] = None, ): + internal_metadata_dict = internal_metadata_dict or {} + event_dict = dict(event_dict) # Signatures is a dict of dicts, and this is faster than doing a @@ -507,9 +511,11 @@ def _event_type_from_format_version(format_version: int) -> Type[EventBase]: def make_event_from_dict( event_dict: JsonDict, room_version: RoomVersion = RoomVersions.V1, - internal_metadata_dict: JsonDict = {}, + internal_metadata_dict: Optional[JsonDict] = None, rejected_reason: Optional[str] = None, ) -> EventBase: """Construct an EventBase from the given event dict""" event_type = _event_type_from_format_version(room_version.event_format) - return event_type(event_dict, room_version, internal_metadata_dict, rejected_reason) + return event_type( + event_dict, room_version, internal_metadata_dict or {}, rejected_reason + ) diff --git a/synapse/federation/units.py b/synapse/federation/units.py index b662c42621..0f8bf000ac 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -18,6 +18,7 @@ """ import logging +from typing import Optional import attr @@ -98,7 +99,7 @@ class Transaction(JsonEncodedObject): "pdus", ] - def __init__(self, transaction_id=None, pdus=[], **kwargs): + def __init__(self, transaction_id=None, pdus: Optional[list] = None, **kwargs): """If we include a list of pdus then we decode then as PDU's automatically. """ @@ -107,7 +108,7 @@ def __init__(self, transaction_id=None, pdus=[], **kwargs): if "edus" in kwargs and not kwargs["edus"]: del kwargs["edus"] - super().__init__(transaction_id=transaction_id, pdus=pdus, **kwargs) + super().__init__(transaction_id=transaction_id, pdus=pdus or [], **kwargs) @staticmethod def create_new(pdus, **kwargs): diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py index 996f9e5deb..9fb7ee335d 100644 --- a/synapse/handlers/appservice.py +++ b/synapse/handlers/appservice.py @@ -182,7 +182,7 @@ def notify_interested_services_ephemeral( self, stream_key: str, new_token: Optional[int], - users: Collection[Union[str, UserID]] = [], + users: Optional[Collection[Union[str, UserID]]] = None, ): """This is called by the notifier in the background when a ephemeral event handled by the homeserver. @@ -215,7 +215,7 @@ def notify_interested_services_ephemeral( # We only start a new background process if necessary rather than # optimistically (to cut down on overhead). self._notify_interested_services_ephemeral( - services, stream_key, new_token, users + services, stream_key, new_token, users or [] ) @wrap_as_background_process("notify_interested_services_ephemeral") diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 5ea8a7b603..67888898ff 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1790,7 +1790,7 @@ async def _make_and_verify_event( room_id: str, user_id: str, membership: str, - content: JsonDict = {}, + content: JsonDict, params: Optional[Dict[str, Union[str, Iterable[str]]]] = None, ) -> Tuple[str, EventBase, RoomVersion]: ( diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 6069968f7f..125dae6d25 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -137,7 +137,7 @@ async def get_state_events( self, user_id: str, room_id: str, - state_filter: StateFilter = StateFilter.all(), + state_filter: Optional[StateFilter] = None, at_token: Optional[StreamToken] = None, is_guest: bool = False, ) -> List[dict]: @@ -164,6 +164,8 @@ async def get_state_events( AuthError (403) if the user doesn't have permission to view members of this room. """ + state_filter = state_filter or StateFilter.all() + if at_token: # FIXME this claims to get the state at a stream position, but # get_recent_events_for_room operates by topo ordering. This therefore @@ -874,7 +876,7 @@ async def handle_new_client_event( event: EventBase, context: EventContext, ratelimit: bool = True, - extra_users: List[UserID] = [], + extra_users: Optional[List[UserID]] = None, ignore_shadow_ban: bool = False, ) -> EventBase: """Processes a new event. @@ -902,6 +904,7 @@ async def handle_new_client_event( Raises: ShadowBanError if the requester has been shadow-banned. """ + extra_users = extra_users or [] # we don't apply shadow-banning to membership events here. Invites are blocked # higher up the stack, and we allow shadow-banned users to send join and leave @@ -1071,7 +1074,7 @@ async def persist_and_notify_client_event( event: EventBase, context: EventContext, ratelimit: bool = True, - extra_users: List[UserID] = [], + extra_users: Optional[List[UserID]] = None, ) -> EventBase: """Called when we have fully built the event, have already calculated the push actions for the event, and checked auth. @@ -1083,6 +1086,8 @@ async def persist_and_notify_client_event( it was de-duplicated (e.g. because we had already persisted an event with the same transaction ID.) """ + extra_users = extra_users or [] + assert self.storage.persistence is not None assert self._events_shard_config.should_handle( self._instance_name, event.room_id diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 9701b76d0f..3b6660c873 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -169,7 +169,7 @@ async def register_user( user_type: Optional[str] = None, default_display_name: Optional[str] = None, address: Optional[str] = None, - bind_emails: Iterable[str] = [], + bind_emails: Optional[Iterable[str]] = None, by_admin: bool = False, user_agent_ips: Optional[List[Tuple[str, str]]] = None, auth_provider_id: Optional[str] = None, @@ -204,6 +204,8 @@ async def register_user( Raises: SynapseError if there was a problem registering. """ + bind_emails = bind_emails or [] + await self.check_registration_ratelimit(address) result = await self.spam_checker.check_registration_for_spam( diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index ff11266c67..f8d88ef77b 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -548,7 +548,7 @@ async def _load_filtered_recents( ) async def get_state_after_event( - self, event: EventBase, state_filter: StateFilter = StateFilter.all() + self, event: EventBase, state_filter: Optional[StateFilter] = None ) -> StateMap[str]: """ Get the room state after the given event @@ -558,7 +558,7 @@ async def get_state_after_event( state_filter: The state filter used to fetch state from the database. """ state_ids = await self.state_store.get_state_ids_for_event( - event.event_id, state_filter=state_filter + event.event_id, state_filter=state_filter or StateFilter.all() ) if event.is_state(): state_ids = dict(state_ids) @@ -569,7 +569,7 @@ async def get_state_at( self, room_id: str, stream_position: StreamToken, - state_filter: StateFilter = StateFilter.all(), + state_filter: Optional[StateFilter] = None, ) -> StateMap[str]: """Get the room state at a particular stream position @@ -589,7 +589,7 @@ async def get_state_at( if last_events: last_event = last_events[-1] state = await self.get_state_after_event( - last_event, state_filter=state_filter + last_event, state_filter=state_filter or StateFilter.all() ) else: diff --git a/synapse/http/client.py b/synapse/http/client.py index e691ba6d88..f7a07f0466 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -297,7 +297,7 @@ class SimpleHttpClient: def __init__( self, hs: "HomeServer", - treq_args: Dict[str, Any] = {}, + treq_args: Optional[Dict[str, Any]] = None, ip_whitelist: Optional[IPSet] = None, ip_blacklist: Optional[IPSet] = None, use_proxy: bool = False, @@ -317,7 +317,7 @@ def __init__( self._ip_whitelist = ip_whitelist self._ip_blacklist = ip_blacklist - self._extra_treq_args = treq_args + self._extra_treq_args = treq_args or {} self.user_agent = hs.version_string self.clock = hs.get_clock() diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py index 16ec850064..ea5ad14cb0 100644 --- a/synapse/http/proxyagent.py +++ b/synapse/http/proxyagent.py @@ -27,7 +27,7 @@ from twisted.web.client import URI, BrowserLikePolicyForHTTPS, _AgentBase from twisted.web.error import SchemeNotSupported from twisted.web.http_headers import Headers -from twisted.web.iweb import IAgent +from twisted.web.iweb import IAgent, IPolicyForHTTPS from synapse.http.connectproxyclient import HTTPConnectProxyEndpoint @@ -88,12 +88,14 @@ def __init__( self, reactor, proxy_reactor=None, - contextFactory=BrowserLikePolicyForHTTPS(), + contextFactory: Optional[IPolicyForHTTPS] = None, connectTimeout=None, bindAddress=None, pool=None, use_proxy=False, ): + contextFactory = contextFactory or BrowserLikePolicyForHTTPS() + _AgentBase.__init__(self, reactor, pool) if proxy_reactor is None: diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index b8081f197e..bfe9136fd8 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -486,7 +486,7 @@ def start_active_span_from_request( def start_active_span_from_edu( edu_content, operation_name, - references=[], + references: Optional[list] = None, tags=None, start_time=None, ignore_active_span=False, @@ -501,6 +501,7 @@ def start_active_span_from_edu( For the other args see opentracing.tracer """ + references = references or [] if opentracing is None: return noop_context_manager() diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 3ecd46c038..ca1bd4cdc9 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import TYPE_CHECKING, Any, Generator, Iterable, Optional, Tuple +from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, Tuple from twisted.internet import defer @@ -127,7 +127,7 @@ def check_user_exists(self, user_id): return defer.ensureDeferred(self._auth_handler.check_user_exists(user_id)) @defer.inlineCallbacks - def register(self, localpart, displayname=None, emails=[]): + def register(self, localpart, displayname=None, emails: Optional[List[str]] = None): """Registers a new user with given localpart and optional displayname, emails. Also returns an access token for the new user. @@ -147,11 +147,13 @@ def register(self, localpart, displayname=None, emails=[]): logger.warning( "Using deprecated ModuleApi.register which creates a dummy user device." ) - user_id = yield self.register_user(localpart, displayname, emails) + user_id = yield self.register_user(localpart, displayname, emails or []) _, access_token = yield self.register_device(user_id) return user_id, access_token - def register_user(self, localpart, displayname=None, emails=[]): + def register_user( + self, localpart, displayname=None, emails: Optional[List[str]] = None + ): """Registers a new user with given localpart and optional displayname, emails. Args: @@ -170,7 +172,7 @@ def register_user(self, localpart, displayname=None, emails=[]): self._hs.get_registration_handler().register_user( localpart=localpart, default_display_name=displayname, - bind_emails=emails, + bind_emails=emails or [], ) ) diff --git a/synapse/notifier.py b/synapse/notifier.py index c178db57e3..7ce34380af 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -276,7 +276,7 @@ def on_new_room_event( event: EventBase, event_pos: PersistedEventPosition, max_room_stream_token: RoomStreamToken, - extra_users: Collection[UserID] = [], + extra_users: Optional[Collection[UserID]] = None, ): """Unwraps event and calls `on_new_room_event_args`.""" self.on_new_room_event_args( @@ -286,7 +286,7 @@ def on_new_room_event( state_key=event.get("state_key"), membership=event.content.get("membership"), max_room_stream_token=max_room_stream_token, - extra_users=extra_users, + extra_users=extra_users or [], ) def on_new_room_event_args( @@ -297,7 +297,7 @@ def on_new_room_event_args( membership: Optional[str], event_pos: PersistedEventPosition, max_room_stream_token: RoomStreamToken, - extra_users: Collection[UserID] = [], + extra_users: Optional[Collection[UserID]] = None, ): """Used by handlers to inform the notifier something has happened in the room, room event wise. @@ -313,7 +313,7 @@ def on_new_room_event_args( self.pending_new_room_events.append( _PendingRoomEventEntry( event_pos=event_pos, - extra_users=extra_users, + extra_users=extra_users or [], room_id=room_id, type=event_type, state_key=state_key, @@ -382,14 +382,14 @@ def _notify_app_services_ephemeral( self, stream_key: str, new_token: Union[int, RoomStreamToken], - users: Collection[Union[str, UserID]] = [], + users: Optional[Collection[Union[str, UserID]]] = None, ): try: stream_token = None if isinstance(new_token, int): stream_token = new_token self.appservice_handler.notify_interested_services_ephemeral( - stream_key, stream_token, users + stream_key, stream_token, users or [] ) except Exception: logger.exception("Error notifying application services of event") @@ -404,13 +404,16 @@ def on_new_event( self, stream_key: str, new_token: Union[int, RoomStreamToken], - users: Collection[Union[str, UserID]] = [], - rooms: Collection[str] = [], + users: Optional[Collection[Union[str, UserID]]] = None, + rooms: Optional[Collection[str]] = None, ): """Used to inform listeners that something has happened event wise. Will wake up all listeners for the given users and rooms. """ + users = users or [] + rooms = rooms or [] + with Measure(self.clock, "on_new_event"): user_streams = set() diff --git a/synapse/storage/database.py b/synapse/storage/database.py index b302cd5786..fa15b0ce5b 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -900,7 +900,7 @@ async def simple_upsert( table: str, keyvalues: Dict[str, Any], values: Dict[str, Any], - insertion_values: Dict[str, Any] = {}, + insertion_values: Optional[Dict[str, Any]] = None, desc: str = "simple_upsert", lock: bool = True, ) -> Optional[bool]: @@ -927,6 +927,8 @@ async def simple_upsert( Native upserts always return None. Emulated upserts return True if a new entry was created, False if an existing one was updated. """ + insertion_values = insertion_values or {} + attempts = 0 while True: try: @@ -964,7 +966,7 @@ def simple_upsert_txn( table: str, keyvalues: Dict[str, Any], values: Dict[str, Any], - insertion_values: Dict[str, Any] = {}, + insertion_values: Optional[Dict[str, Any]] = None, lock: bool = True, ) -> Optional[bool]: """ @@ -982,6 +984,8 @@ def simple_upsert_txn( Native upserts always return None. Emulated upserts return True if a new entry was created, False if an existing one was updated. """ + insertion_values = insertion_values or {} + if self.engine.can_native_upsert and table not in self._unsafe_to_upsert_tables: self.simple_upsert_txn_native_upsert( txn, table, keyvalues, values, insertion_values=insertion_values @@ -1003,7 +1007,7 @@ def simple_upsert_txn_emulated( table: str, keyvalues: Dict[str, Any], values: Dict[str, Any], - insertion_values: Dict[str, Any] = {}, + insertion_values: Optional[Dict[str, Any]] = None, lock: bool = True, ) -> bool: """ @@ -1017,6 +1021,8 @@ def simple_upsert_txn_emulated( Returns True if a new entry was created, False if an existing one was updated. """ + insertion_values = insertion_values or {} + # We need to lock the table :(, unless we're *really* careful if lock: self.engine.lock_table(txn, table) @@ -1077,7 +1083,7 @@ def simple_upsert_txn_native_upsert( table: str, keyvalues: Dict[str, Any], values: Dict[str, Any], - insertion_values: Dict[str, Any] = {}, + insertion_values: Optional[Dict[str, Any]] = None, ) -> None: """ Use the native UPSERT functionality in recent PostgreSQL versions. @@ -1090,7 +1096,7 @@ def simple_upsert_txn_native_upsert( """ allvalues = {} # type: Dict[str, Any] allvalues.update(keyvalues) - allvalues.update(insertion_values) + allvalues.update(insertion_values or {}) if not values: latter = "NOTHING" @@ -1513,7 +1519,7 @@ async def simple_select_many_batch( column: str, iterable: Iterable[Any], retcols: Iterable[str], - keyvalues: Dict[str, Any] = {}, + keyvalues: Optional[Dict[str, Any]] = None, desc: str = "simple_select_many_batch", batch_size: int = 100, ) -> List[Any]: @@ -1531,6 +1537,8 @@ async def simple_select_many_batch( desc: description of the transaction, for logging and metrics batch_size: the number of rows for each select query """ + keyvalues = keyvalues or {} + results = [] # type: List[Dict[str, Any]] if not iterable: diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index 98dac19a95..ad17123915 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -320,8 +320,8 @@ def _persist_events_txn( txn: LoggingTransaction, events_and_contexts: List[Tuple[EventBase, EventContext]], backfilled: bool, - state_delta_for_room: Dict[str, DeltaState] = {}, - new_forward_extremeties: Dict[str, List[str]] = {}, + state_delta_for_room: Optional[Dict[str, DeltaState]] = None, + new_forward_extremeties: Optional[Dict[str, List[str]]] = None, ): """Insert some number of room events into the necessary database tables. @@ -342,6 +342,9 @@ def _persist_events_txn( extremities. """ + state_delta_for_room = state_delta_for_room or {} + new_forward_extremeties = new_forward_extremeties or {} + all_events_and_contexts = events_and_contexts min_stream_order = events_and_contexts[0][0].internal_metadata.stream_ordering diff --git a/synapse/storage/databases/main/group_server.py b/synapse/storage/databases/main/group_server.py index 8f462dfc31..bd7826f4e9 100644 --- a/synapse/storage/databases/main/group_server.py +++ b/synapse/storage/databases/main/group_server.py @@ -1171,7 +1171,7 @@ async def register_user_group_membership( user_id: str, membership: str, is_admin: bool = False, - content: JsonDict = {}, + content: Optional[JsonDict] = None, local_attestation: Optional[dict] = None, remote_attestation: Optional[dict] = None, is_publicised: bool = False, @@ -1192,6 +1192,8 @@ async def register_user_group_membership( is_publicised: Whether this should be publicised. """ + content = content or {} + def _register_user_group_membership_txn(txn, next_id): # TODO: Upsert? self.db_pool.simple_delete_txn( diff --git a/synapse/storage/databases/main/state.py b/synapse/storage/databases/main/state.py index a7f371732f..93431efe00 100644 --- a/synapse/storage/databases/main/state.py +++ b/synapse/storage/databases/main/state.py @@ -190,7 +190,7 @@ def _get_current_state_ids_txn(txn): # FIXME: how should this be cached? async def get_filtered_current_state_ids( - self, room_id: str, state_filter: StateFilter = StateFilter.all() + self, room_id: str, state_filter: Optional[StateFilter] = None ) -> StateMap[str]: """Get the current state event of a given type for a room based on the current_state_events table. This may not be as up-to-date as the result @@ -205,7 +205,9 @@ async def get_filtered_current_state_ids( Map from type/state_key to event ID. """ - where_clause, where_args = state_filter.make_sql_filter_clause() + where_clause, where_args = ( + state_filter or StateFilter.all() + ).make_sql_filter_clause() if not where_clause: # We delegate to the cached version diff --git a/synapse/storage/databases/state/bg_updates.py b/synapse/storage/databases/state/bg_updates.py index 1fd333b707..75c09b3687 100644 --- a/synapse/storage/databases/state/bg_updates.py +++ b/synapse/storage/databases/state/bg_updates.py @@ -14,6 +14,7 @@ # limitations under the License. import logging +from typing import Optional from synapse.storage._base import SQLBaseStore from synapse.storage.database import DatabasePool @@ -73,8 +74,10 @@ def _count_state_group_hops_txn(self, txn, state_group): return count def _get_state_groups_from_groups_txn( - self, txn, groups, state_filter=StateFilter.all() + self, txn, groups, state_filter: Optional[StateFilter] = None ): + state_filter = state_filter or StateFilter.all() + results = {group: {} for group in groups} where_clause, where_args = state_filter.make_sql_filter_clause() diff --git a/synapse/storage/databases/state/store.py b/synapse/storage/databases/state/store.py index 97ec65f757..dfcf89d91c 100644 --- a/synapse/storage/databases/state/store.py +++ b/synapse/storage/databases/state/store.py @@ -15,7 +15,7 @@ import logging from collections import namedtuple -from typing import Dict, Iterable, List, Set, Tuple +from typing import Dict, Iterable, List, Optional, Set, Tuple from synapse.api.constants import EventTypes from synapse.storage._base import SQLBaseStore @@ -210,7 +210,7 @@ def _get_state_for_group_using_cache(self, cache, group, state_filter): return state_filter.filter_state(state_dict_ids), not missing_types async def _get_state_for_groups( - self, groups: Iterable[int], state_filter: StateFilter = StateFilter.all() + self, groups: Iterable[int], state_filter: Optional[StateFilter] = None ) -> Dict[int, MutableStateMap[str]]: """Gets the state at each of a list of state groups, optionally filtering by type/state_key @@ -223,6 +223,7 @@ async def _get_state_for_groups( Returns: Dict of state group to state map. """ + state_filter = state_filter or StateFilter.all() member_filter, non_member_filter = state_filter.get_member_split() diff --git a/synapse/storage/state.py b/synapse/storage/state.py index 2e277a21c4..c1c147c62a 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -449,7 +449,7 @@ def _get_state_groups_from_groups( return self.stores.state._get_state_groups_from_groups(groups, state_filter) async def get_state_for_events( - self, event_ids: Iterable[str], state_filter: StateFilter = StateFilter.all() + self, event_ids: Iterable[str], state_filter: Optional[StateFilter] = None ) -> Dict[str, StateMap[EventBase]]: """Given a list of event_ids and type tuples, return a list of state dicts for each event. @@ -465,7 +465,7 @@ async def get_state_for_events( groups = set(event_to_groups.values()) group_to_state = await self.stores.state._get_state_for_groups( - groups, state_filter + groups, state_filter or StateFilter.all() ) state_event_map = await self.stores.main.get_events( @@ -485,7 +485,7 @@ async def get_state_for_events( return {event: event_to_state[event] for event in event_ids} async def get_state_ids_for_events( - self, event_ids: Iterable[str], state_filter: StateFilter = StateFilter.all() + self, event_ids: Iterable[str], state_filter: Optional[StateFilter] = None ) -> Dict[str, StateMap[str]]: """ Get the state dicts corresponding to a list of events, containing the event_ids @@ -502,7 +502,7 @@ async def get_state_ids_for_events( groups = set(event_to_groups.values()) group_to_state = await self.stores.state._get_state_for_groups( - groups, state_filter + groups, state_filter or StateFilter.all() ) event_to_state = { @@ -513,7 +513,7 @@ async def get_state_ids_for_events( return {event: event_to_state[event] for event in event_ids} async def get_state_for_event( - self, event_id: str, state_filter: StateFilter = StateFilter.all() + self, event_id: str, state_filter: Optional[StateFilter] = None ) -> StateMap[EventBase]: """ Get the state dict corresponding to a particular event @@ -525,11 +525,13 @@ async def get_state_for_event( Returns: A dict from (type, state_key) -> state_event """ - state_map = await self.get_state_for_events([event_id], state_filter) + state_map = await self.get_state_for_events( + [event_id], state_filter or StateFilter.all() + ) return state_map[event_id] async def get_state_ids_for_event( - self, event_id: str, state_filter: StateFilter = StateFilter.all() + self, event_id: str, state_filter: Optional[StateFilter] = None ) -> StateMap[str]: """ Get the state dict corresponding to a particular event @@ -541,11 +543,13 @@ async def get_state_ids_for_event( Returns: A dict from (type, state_key) -> state_event """ - state_map = await self.get_state_ids_for_events([event_id], state_filter) + state_map = await self.get_state_ids_for_events( + [event_id], state_filter or StateFilter.all() + ) return state_map[event_id] def _get_state_for_groups( - self, groups: Iterable[int], state_filter: StateFilter = StateFilter.all() + self, groups: Iterable[int], state_filter: Optional[StateFilter] = None ) -> Awaitable[Dict[int, MutableStateMap[str]]]: """Gets the state at each of a list of state groups, optionally filtering by type/state_key @@ -558,7 +562,9 @@ def _get_state_for_groups( Returns: Dict of state group to state map. """ - return self.stores.state._get_state_for_groups(groups, state_filter) + return self.stores.state._get_state_for_groups( + groups, state_filter or StateFilter.all() + ) async def store_state_group( self, diff --git a/synapse/storage/util/id_generators.py b/synapse/storage/util/id_generators.py index d4643c4fdf..32d6cc16b9 100644 --- a/synapse/storage/util/id_generators.py +++ b/synapse/storage/util/id_generators.py @@ -17,7 +17,7 @@ import threading from collections import OrderedDict from contextlib import contextmanager -from typing import Dict, List, Optional, Set, Tuple, Union +from typing import Dict, Iterable, List, Optional, Set, Tuple, Union import attr @@ -91,7 +91,14 @@ class StreamIdGenerator: # ... persist event ... """ - def __init__(self, db_conn, table, column, extra_tables=[], step=1): + def __init__( + self, + db_conn, + table, + column, + extra_tables: Iterable[Tuple[str, str]] = (), + step=1, + ): assert step != 0 self._lock = threading.Lock() self._step = step diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index 60bb6ff642..20c8e2d9f5 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -57,12 +57,14 @@ def enumerate_leaves(node, depth): class _Node: __slots__ = ["prev_node", "next_node", "key", "value", "callbacks"] - def __init__(self, prev_node, next_node, key, value, callbacks=set()): + def __init__( + self, prev_node, next_node, key, value, callbacks: Optional[set] = None + ): self.prev_node = prev_node self.next_node = next_node self.key = key self.value = value - self.callbacks = callbacks + self.callbacks = callbacks or set() class LruCache(Generic[KT, VT]): @@ -176,10 +178,10 @@ def cache_len(): self.len = synchronized(cache_len) - def add_node(key, value, callbacks=set()): + def add_node(key, value, callbacks: Optional[set] = None): prev_node = list_root next_node = prev_node.next_node - node = _Node(prev_node, next_node, key, value, callbacks) + node = _Node(prev_node, next_node, key, value, callbacks or set()) prev_node.next_node = node next_node.prev_node = node cache[key] = node @@ -237,7 +239,7 @@ def cache_get( def cache_get( key: KT, default: Optional[T] = None, - callbacks: Iterable[Callable[[], None]] = [], + callbacks: Iterable[Callable[[], None]] = (), update_metrics: bool = True, ): node = cache.get(key, None) @@ -253,7 +255,7 @@ def cache_get( return default @synchronized - def cache_set(key: KT, value: VT, callbacks: Iterable[Callable[[], None]] = []): + def cache_set(key: KT, value: VT, callbacks: Iterable[Callable[[], None]] = ()): node = cache.get(key, None) if node is not None: # We sometimes store large objects, e.g. dicts, which cause diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index 4c56253da5..73e12ea6c3 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import Optional from mock import Mock @@ -180,7 +181,11 @@ def _make_get_request(self, uri): _check_logcontext(context) def _handle_well_known_connection( - self, client_factory, expected_sni, content, response_headers={} + self, + client_factory, + expected_sni, + content, + response_headers: Optional[dict] = None, ): """Handle an outgoing HTTPs connection: wire it up to a server, check that the request is for a .well-known, and send the response. @@ -202,10 +207,12 @@ def _handle_well_known_connection( self.assertEqual( request.requestHeaders.getRawHeaders(b"user-agent"), [b"test-agent"] ) - self._send_well_known_response(request, content, headers=response_headers) + self._send_well_known_response(request, content, headers=response_headers or {}) return well_known_server - def _send_well_known_response(self, request, content, headers={}): + def _send_well_known_response( + self, request, content, headers: Optional[dict] = None + ): """Check that an incoming request looks like a valid .well-known request, and send back the response. """ @@ -213,7 +220,7 @@ def _send_well_known_response(self, request, content, headers={}): self.assertEqual(request.path, b"/.well-known/matrix/server") self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"testserv"]) # send back a response - for k, v in headers.items(): + for k, v in (headers or {}).items(): request.setHeader(k, v) request.write(content) request.finish() diff --git a/tests/replication/_base.py b/tests/replication/_base.py index 1d4a592862..aff19d9fb3 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -266,7 +266,7 @@ def create_test_resource(self): return resource def make_worker_hs( - self, worker_app: str, extra_config: dict = {}, **kwargs + self, worker_app: str, extra_config: Optional[dict] = None, **kwargs ) -> HomeServer: """Make a new worker HS instance, correctly connecting replcation stream to the master HS. @@ -283,7 +283,7 @@ def make_worker_hs( config = self._get_worker_hs_config() config["worker_app"] = worker_app - config.update(extra_config) + config.update(extra_config or {}) worker_hs = self.setup_test_homeserver( homeserver_to_use=GenericWorkerServer, diff --git a/tests/replication/slave/storage/test_events.py b/tests/replication/slave/storage/test_events.py index 0ceb0f935c..333374b183 100644 --- a/tests/replication/slave/storage/test_events.py +++ b/tests/replication/slave/storage/test_events.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import Iterable, Optional from canonicaljson import encode_canonical_json @@ -332,15 +333,18 @@ def build_event( room_id=ROOM_ID, type="m.room.message", key=None, - internal={}, + internal: Optional[dict] = None, depth=None, - prev_events=[], - auth_events=[], - prev_state=[], + prev_events: Optional[list] = None, + auth_events: Optional[list] = None, + prev_state: Optional[list] = None, redacts=None, - push_actions=[], + push_actions: Iterable = frozenset(), **content ): + prev_events = prev_events or [] + auth_events = auth_events or [] + prev_state = prev_state or [] if depth is None: depth = self.event_id @@ -369,7 +373,7 @@ def build_event( if redacts is not None: event_dict["redacts"] = redacts - event = make_event_from_dict(event_dict, internal_metadata_dict=internal) + event = make_event_from_dict(event_dict, internal_metadata_dict=internal or {}) self.event_id += 1 state_handler = self.hs.get_state_handler() diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index ed65f645fc..715414a310 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -19,6 +19,7 @@ """Tests REST events for /rooms paths.""" import json +from typing import Iterable from urllib import parse as urlparse from mock import Mock @@ -207,7 +208,9 @@ def test_topic_perms(self): ) self.assertEquals(403, channel.code, msg=channel.result["body"]) - def _test_get_membership(self, room=None, members=[], expect_code=None): + def _test_get_membership( + self, room=None, members: Iterable = frozenset(), expect_code=None + ): for member in members: path = "/rooms/%s/state/m.room.member/%s" % (room, member) channel = self.make_request("GET", path) diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index 946740aa5d..8a4dddae2b 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -132,7 +132,7 @@ def change_membership( src: str, targ: str, membership: str, - extra_data: dict = {}, + extra_data: Optional[dict] = None, tok: Optional[str] = None, expect_code: int = 200, ) -> None: @@ -156,7 +156,7 @@ def change_membership( path = path + "?access_token=%s" % tok data = {"membership": membership} - data.update(extra_data) + data.update(extra_data or {}) channel = make_request( self.hs.get_reactor(), @@ -187,7 +187,13 @@ def send(self, room_id, body=None, txn_id=None, tok=None, expect_code=200): ) def send_event( - self, room_id, type, content={}, txn_id=None, tok=None, expect_code=200 + self, + room_id, + type, + content: Optional[dict] = None, + txn_id=None, + tok=None, + expect_code=200, ): if txn_id is None: txn_id = "m%s" % (str(time.time())) @@ -201,7 +207,7 @@ def send_event( self.site, "PUT", path, - json.dumps(content).encode("utf8"), + json.dumps(content or {}).encode("utf8"), ) assert ( diff --git a/tests/rest/client/v2_alpha/test_relations.py b/tests/rest/client/v2_alpha/test_relations.py index e7bb5583fc..21ee436b91 100644 --- a/tests/rest/client/v2_alpha/test_relations.py +++ b/tests/rest/client/v2_alpha/test_relations.py @@ -16,6 +16,7 @@ import itertools import json import urllib +from typing import Optional from synapse.api.constants import EventTypes, RelationTypes from synapse.rest import admin @@ -681,7 +682,7 @@ def _send_relation( relation_type, event_type, key=None, - content={}, + content: Optional[dict] = None, access_token=None, parent_id=None, ): @@ -713,7 +714,7 @@ def _send_relation( "POST", "/_matrix/client/unstable/rooms/%s/send_relation/%s/%s/%s%s" % (self.room, original_id, relation_type, event_type, query), - json.dumps(content).encode("utf-8"), + json.dumps(content or {}).encode("utf-8"), access_token=access_token, ) return channel diff --git a/tests/storage/test_id_generators.py b/tests/storage/test_id_generators.py index aad6bc907e..6c389fe9ac 100644 --- a/tests/storage/test_id_generators.py +++ b/tests/storage/test_id_generators.py @@ -12,6 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, Optional + from synapse.storage.database import DatabasePool from synapse.storage.engines import IncorrectDatabaseSetup from synapse.storage.util.id_generators import MultiWriterIdGenerator @@ -43,7 +45,7 @@ def _setup_db(self, txn): ) def _create_id_generator( - self, instance_name="master", writers=["master"] + self, instance_name="master", writers: Optional[List[str]] = None ) -> MultiWriterIdGenerator: def _create(conn): return MultiWriterIdGenerator( @@ -53,7 +55,7 @@ def _create(conn): instance_name=instance_name, tables=[("foobar", "instance_name", "stream_id")], sequence_name="foobar_seq", - writers=writers, + writers=writers or ["master"], ) return self.get_success_or_raise(self.db_pool.runWithConnection(_create)) @@ -476,7 +478,7 @@ def _setup_db(self, txn): ) def _create_id_generator( - self, instance_name="master", writers=["master"] + self, instance_name="master", writers: Optional[List[str]] = None ) -> MultiWriterIdGenerator: def _create(conn): return MultiWriterIdGenerator( @@ -486,7 +488,7 @@ def _create(conn): instance_name=instance_name, tables=[("foobar", "instance_name", "stream_id")], sequence_name="foobar_seq", - writers=writers, + writers=writers or ["master"], positive=False, ) @@ -612,7 +614,7 @@ def _setup_db(self, txn): ) def _create_id_generator( - self, instance_name="master", writers=["master"] + self, instance_name="master", writers: Optional[List[str]] = None ) -> MultiWriterIdGenerator: def _create(conn): return MultiWriterIdGenerator( @@ -625,7 +627,7 @@ def _create(conn): ("foobar2", "instance_name", "stream_id"), ], sequence_name="foobar_seq", - writers=writers, + writers=writers or ["master"], ) return self.get_success_or_raise(self.db_pool.runWithConnection(_create)) diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index 2622207639..2d2f58903c 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional from canonicaljson import json @@ -47,10 +48,15 @@ def prepare(self, reactor, clock, hs): self.depth = 1 def inject_room_member( - self, room, user, membership, replaces_state=None, extra_content={} + self, + room, + user, + membership, + replaces_state=None, + extra_content: Optional[dict] = None, ): content = {"membership": membership} - content.update(extra_content) + content.update(extra_content or {}) builder = self.event_builder_factory.for_room_version( RoomVersions.V1, { diff --git a/tests/test_state.py b/tests/test_state.py index 6227a3ba95..1d2019699d 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import List, Optional from mock import Mock @@ -37,7 +38,7 @@ def create_event( state_key=None, depth=2, event_id=None, - prev_events=[], + prev_events: Optional[List[str]] = None, **kwargs ): global _next_event_id @@ -58,7 +59,7 @@ def create_event( "sender": "@user_id:example.com", "room_id": "!room_id:example.com", "depth": depth, - "prev_events": prev_events, + "prev_events": prev_events or [], } if state_key is not None: diff --git a/tests/test_visibility.py b/tests/test_visibility.py index 510b630114..1b4dd47a82 100644 --- a/tests/test_visibility.py +++ b/tests/test_visibility.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import Optional from mock import Mock @@ -147,9 +148,11 @@ def inject_visibility(self, user_id, visibility): return event @defer.inlineCallbacks - def inject_room_member(self, user_id, membership="join", extra_content={}): + def inject_room_member( + self, user_id, membership="join", extra_content: Optional[dict] = None + ): content = {"membership": membership} - content.update(extra_content) + content.update(extra_content or {}) builder = self.event_builder_factory.for_room_version( RoomVersions.V1, { diff --git a/tests/util/test_ratelimitutils.py b/tests/util/test_ratelimitutils.py index 4d1aee91d5..3fed55090a 100644 --- a/tests/util/test_ratelimitutils.py +++ b/tests/util/test_ratelimitutils.py @@ -12,6 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional + from synapse.config.homeserver import HomeServerConfig from synapse.util.ratelimitutils import FederationRateLimiter @@ -89,9 +91,9 @@ def _await_resolution(reactor, d): return (reactor.seconds() - start_time) * 1000 -def build_rc_config(settings={}): +def build_rc_config(settings: Optional[dict] = None): config_dict = default_config("test") - config_dict.update(settings) + config_dict.update(settings or {}) config = HomeServerConfig() config.parse_config_dict(config_dict, "", "") return config.rc_federation From 48a1f4db313026c79fbc7d9d950175693368d98b Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Fri, 9 Apr 2021 10:44:40 +0200 Subject: [PATCH 032/619] Remove old admin API `GET /_synapse/admin/v1/users/` (#9401) Related: #8334 Deprecated in: #9429 - Synapse 1.28.0 (2021-02-25) `GET /_synapse/admin/v1/users/` has no - unit tests - documentation API in v2 is available (#5925 - 12/2019, v1.7.0). API is misleading. It expects `user_id` and returns a list of all users. Signed-off-by: Dirk Klimpel dirk@klimpel.org --- UPGRADE.rst | 13 +++++++++++++ changelog.d/9401.removal | 1 + synapse/rest/admin/__init__.py | 2 -- synapse/rest/admin/users.py | 23 ----------------------- tests/storage/test_client_ips.py | 4 ++-- 5 files changed, 16 insertions(+), 27 deletions(-) create mode 100644 changelog.d/9401.removal diff --git a/UPGRADE.rst b/UPGRADE.rst index ba488e1041..665821d4ef 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -85,6 +85,19 @@ for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb +Upgrading to v1.32.0 +==================== + +Removal of old List Accounts Admin API +-------------------------------------- + +The deprecated v1 "list accounts" admin API (``GET /_synapse/admin/v1/users/``) has been removed in this version. + +The `v2 list accounts API `_ +has been available since Synapse 1.7.0 (2019-12-13), and is accessible under ``GET /_synapse/admin/v2/users``. + +The deprecation of the old endpoint was announced with Synapse 1.28.0 (released on 2021-02-25). + Upgrading to v1.29.0 ==================== diff --git a/changelog.d/9401.removal b/changelog.d/9401.removal new file mode 100644 index 0000000000..9c813e0215 --- /dev/null +++ b/changelog.d/9401.removal @@ -0,0 +1 @@ +Remove old admin API `GET /_synapse/admin/v1/users/`. \ No newline at end of file diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 8457db1e22..5daa795df1 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -62,7 +62,6 @@ UserMembershipRestServlet, UserRegisterServlet, UserRestServletV2, - UsersRestServlet, UsersRestServletV2, UserTokenRestServlet, WhoisRestServlet, @@ -248,7 +247,6 @@ def register_servlets_for_client_rest_resource(hs, http_server): PurgeHistoryStatusRestServlet(hs).register(http_server) DeactivateAccountRestServlet(hs).register(http_server) PurgeHistoryRestServlet(hs).register(http_server) - UsersRestServlet(hs).register(http_server) ResetPasswordRestServlet(hs).register(http_server) SearchUsersRestServlet(hs).register(http_server) ShutdownRoomRestServlet(hs).register(http_server) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index fa7804583a..595898c259 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -45,29 +45,6 @@ logger = logging.getLogger(__name__) -class UsersRestServlet(RestServlet): - PATTERNS = admin_patterns("/users/(?P[^/]*)$") - - def __init__(self, hs: "HomeServer"): - self.hs = hs - self.store = hs.get_datastore() - self.auth = hs.get_auth() - self.admin_handler = hs.get_admin_handler() - - async def on_GET( - self, request: SynapseRequest, user_id: str - ) -> Tuple[int, List[JsonDict]]: - target_user = UserID.from_string(user_id) - await assert_requester_is_admin(self.auth, request) - - if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only users a local user") - - ret = await self.store.get_users() - - return 200, ret - - class UsersRestServletV2(RestServlet): PATTERNS = admin_patterns("/users$", "v2") diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index 34e6526097..a8a6ddc466 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -390,7 +390,7 @@ def test_old_user_ips_pruned(self): class ClientIpAuthTestCase(unittest.HomeserverTestCase): servlets = [ - synapse.rest.admin.register_servlets_for_client_rest_resource, + synapse.rest.admin.register_servlets, login.register_servlets, ] @@ -434,7 +434,7 @@ def _runtest(self, headers, expected_ip, make_request_args): self.reactor, self.site, "GET", - "/_synapse/admin/v1/users/" + self.user_id, + "/_synapse/admin/v2/users/" + self.user_id, access_token=access_token, custom_headers=headers1.items(), **make_request_args, From 0277b8f3e6dc3d960a7192dfc8035e2cbf2559dc Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Fri, 9 Apr 2021 10:54:30 +0100 Subject: [PATCH 033/619] Proof of concept for GitHub Actions (#9661) Signed-off-by: Dan Callahan --- .github/workflows/tests.yml | 322 ++++++++++++++++++++++++++++++++++++ changelog.d/9661.misc | 1 + 2 files changed, 323 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 changelog.d/9661.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000..12c82ac620 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,322 @@ +name: Tests + +on: + push: + branches: ["develop", "release-*"] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + matrix: + toxenv: + - "check-sampleconfig" + - "check_codestyle" + - "check_isort" + - "mypy" + - "packaging" + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install tox + - run: tox -e ${{ matrix.toxenv }} + + lint-crlf: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Check line endings + run: scripts-dev/check_line_terminators.sh + + lint-newsfile: + if: ${{ github.base_ref == 'develop' || contains(github.base_ref, 'release-') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install tox + - name: Patch Buildkite-specific test script + run: | + sed -i -e 's/\$BUILDKITE_PULL_REQUEST/${{ github.event.number }}/' \ + scripts-dev/check-newsfragment + - run: scripts-dev/check-newsfragment + + lint-sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: "3.x" + - run: pip install wheel + - run: python setup.py sdist bdist_wheel + - uses: actions/upload-artifact@v2 + with: + name: Python Distributions + path: dist/* + + # Dummy step to gate other tests on without repeating the whole list + linting-done: + if: ${{ always() }} # Run this even if prior jobs were skipped + needs: [lint, lint-crlf, lint-newsfile, lint-sdist] + runs-on: ubuntu-latest + steps: + - run: "true" + + trial: + if: ${{ !failure() }} # Allow previous steps to be skipped, but not fail + needs: linting-done + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.6", "3.7", "3.8", "3.9"] + database: ["sqlite"] + include: + # Newest Python without optional deps + - python-version: "3.9" + toxenv: "py-noextras,combine" + + # Oldest Python with PostgreSQL + - python-version: "3.6" + database: "postgres" + postgres-version: "9.6" + + # Newest Python with PostgreSQL + - python-version: "3.9" + database: "postgres" + postgres-version: "13" + + steps: + - uses: actions/checkout@v2 + - run: sudo apt-get -qq install xmlsec1 + - name: Set up PostgreSQL ${{ matrix.postgres-version }} + if: ${{ matrix.postgres-version }} + run: | + docker run -d -p 5432:5432 \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_INITDB_ARGS="--lc-collate C --lc-ctype C --encoding UTF8" \ + postgres:${{ matrix.postgres-version }} + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - run: pip install tox + - name: Await PostgreSQL + if: ${{ matrix.postgres-version }} + timeout-minutes: 2 + run: until pg_isready -h localhost; do sleep 1; done + - run: tox -e py,combine + env: + TRIAL_FLAGS: "--jobs=2" + SYNAPSE_POSTGRES: ${{ matrix.database == 'postgres' || '' }} + SYNAPSE_POSTGRES_HOST: localhost + SYNAPSE_POSTGRES_USER: postgres + SYNAPSE_POSTGRES_PASSWORD: postgres + - name: Dump logs + # Note: Dumps to workflow logs instead of using actions/upload-artifact + # This keeps logs colocated with failing jobs + # It also ignores find's exit code; this is a best effort affair + run: >- + find _trial_temp -name '*.log' + -exec echo "::group::{}" \; + -exec cat {} \; + -exec echo "::endgroup::" \; + || true + + trial-olddeps: + if: ${{ !failure() }} # Allow previous steps to be skipped, but not fail + needs: linting-done + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Test with old deps + uses: docker://ubuntu:bionic # For old python and sqlite + with: + workdir: /github/workspace + entrypoint: .buildkite/scripts/test_old_deps.sh + env: + TRIAL_FLAGS: "--jobs=2" + - name: Dump logs + # Note: Dumps to workflow logs instead of using actions/upload-artifact + # This keeps logs colocated with failing jobs + # It also ignores find's exit code; this is a best effort affair + run: >- + find _trial_temp -name '*.log' + -exec echo "::group::{}" \; + -exec cat {} \; + -exec echo "::endgroup::" \; + || true + + trial-pypy: + # Very slow; only run if the branch name includes 'pypy' + if: ${{ contains(github.ref, 'pypy') && !failure() }} + needs: linting-done + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["pypy-3.6"] + + steps: + - uses: actions/checkout@v2 + - run: sudo apt-get -qq install xmlsec1 libxml2-dev libxslt-dev + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - run: pip install tox + - run: tox -e py,combine + env: + TRIAL_FLAGS: "--jobs=2" + - name: Dump logs + # Note: Dumps to workflow logs instead of using actions/upload-artifact + # This keeps logs colocated with failing jobs + # It also ignores find's exit code; this is a best effort affair + run: >- + find _trial_temp -name '*.log' + -exec echo "::group::{}" \; + -exec cat {} \; + -exec echo "::endgroup::" \; + || true + + sytest: + if: ${{ !failure() }} + needs: linting-done + runs-on: ubuntu-latest + container: + image: matrixdotorg/sytest-synapse:${{ matrix.sytest-tag }} + volumes: + - ${{ github.workspace }}:/src + env: + BUILDKITE_BRANCH: ${{ github.head_ref }} + POSTGRES: ${{ matrix.postgres && 1}} + MULTI_POSTGRES: ${{ (matrix.postgres == 'multi-postgres') && 1}} + WORKERS: ${{ matrix.workers && 1 }} + REDIS: ${{ matrix.redis && 1 }} + BLACKLIST: ${{ matrix.workers && 'synapse-blacklist-with-workers' }} + + strategy: + fail-fast: false + matrix: + include: + - sytest-tag: bionic + + - sytest-tag: bionic + postgres: postgres + + - sytest-tag: testing + postgres: postgres + + - sytest-tag: bionic + postgres: multi-postgres + workers: workers + + - sytest-tag: buster + postgres: multi-postgres + workers: workers + + - sytest-tag: buster + postgres: postgres + workers: workers + redis: redis + + steps: + - uses: actions/checkout@v2 + - name: Prepare test blacklist + run: cat sytest-blacklist .buildkite/worker-blacklist > synapse-blacklist-with-workers + - name: Run SyTest + run: /bootstrap.sh synapse + working-directory: /src + - name: Dump results.tap + if: ${{ always() }} + run: cat /logs/results.tap + - name: Upload SyTest logs + uses: actions/upload-artifact@v2 + if: ${{ always() }} + with: + name: Sytest Logs - ${{ job.status }} - (${{ join(matrix.*, ', ') }}) + path: | + /logs/results.tap + /logs/**/*.log* + + portdb: + if: ${{ !failure() }} # Allow previous steps to be skipped, but not fail + needs: linting-done + runs-on: ubuntu-latest + strategy: + matrix: + include: + - python-version: "3.6" + postgres-version: "9.6" + + - python-version: "3.9" + postgres-version: "13" + + services: + postgres: + image: postgres:${{ matrix.postgres-version }} + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: "postgres" + POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8" + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + - run: sudo apt-get -qq install xmlsec1 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Patch Buildkite-specific test scripts + run: | + sed -i -e 's/host="postgres"/host="localhost"/' .buildkite/scripts/create_postgres_db.py + sed -i -e 's/host: postgres/host: localhost/' .buildkite/postgres-config.yaml + sed -i -e 's|/src/||' .buildkite/{sqlite,postgres}-config.yaml + sed -i -e 's/\$TOP/\$GITHUB_WORKSPACE/' .coveragerc + - run: .buildkite/scripts/test_synapse_port_db.sh + + complement: + if: ${{ !failure() }} + needs: linting-done + runs-on: ubuntu-latest + container: + # https://github.com/matrix-org/complement/blob/master/dockerfiles/ComplementCIBuildkite.Dockerfile + image: matrixdotorg/complement:latest + env: + CI: true + ports: + - 8448:8448 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + steps: + - name: Run actions/checkout@v2 for synapse + uses: actions/checkout@v2 + with: + path: synapse + + - name: Run actions/checkout@v2 for complement + uses: actions/checkout@v2 + with: + repository: "matrix-org/complement" + path: complement + + # Build initial Synapse image + - run: docker build -t matrixdotorg/synapse:latest -f docker/Dockerfile . + working-directory: synapse + + # Build a ready-to-run Synapse image based on the initial image above. + # This new image includes a config file, keys for signing and TLS, and + # other settings to make it suitable for testing under Complement. + - run: docker build -t complement-synapse -f Synapse.Dockerfile . + working-directory: complement/dockerfiles + + # Run Complement + - run: go test -v -tags synapse_blacklist ./tests + env: + COMPLEMENT_BASE_IMAGE: complement-synapse:latest + working-directory: complement diff --git a/changelog.d/9661.misc b/changelog.d/9661.misc new file mode 100644 index 0000000000..b5beb4626c --- /dev/null +++ b/changelog.d/9661.misc @@ -0,0 +1 @@ +Experiment with GitHub Actions for CI. From abc814dcbf559282220c35a45b3959bb23a2ed50 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 9 Apr 2021 08:11:51 -0400 Subject: [PATCH 034/619] Enable complement tests for MSC2946. (#9771) By providing the additional build tag for `msc2946`. --- changelog.d/9771.misc | 1 + scripts-dev/complement.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9771.misc diff --git a/changelog.d/9771.misc b/changelog.d/9771.misc new file mode 100644 index 0000000000..42d651d4cc --- /dev/null +++ b/changelog.d/9771.misc @@ -0,0 +1 @@ +Enable Complement tests for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary API. diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index b77187472f..1612ab522c 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -46,4 +46,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -tags synapse_blacklist,msc3083 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests +COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -tags synapse_blacklist,msc2946,msc3083 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests From f9464501846755e09e882c97e2d8c1490c9bf74b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 9 Apr 2021 18:12:15 +0100 Subject: [PATCH 035/619] Fix duplicate logging of exceptions in transaction processing (#9780) There's no point logging this twice. --- changelog.d/9780.bugfix | 1 + synapse/federation/transport/server.py | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) create mode 100644 changelog.d/9780.bugfix diff --git a/changelog.d/9780.bugfix b/changelog.d/9780.bugfix new file mode 100644 index 0000000000..70985a050f --- /dev/null +++ b/changelog.d/9780.bugfix @@ -0,0 +1 @@ +Fix duplicate logging of exceptions thrown during federation transaction processing. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 5ef0556ef7..a9c1391d27 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -425,13 +425,9 @@ async def on_PUT(self, origin, content, query, transaction_id): logger.exception(e) return 400, {"error": "Invalid transaction"} - try: - code, response = await self.handler.on_incoming_transaction( - origin, transaction_data - ) - except Exception: - logger.exception("on_incoming_transaction failed") - raise + code, response = await self.handler.on_incoming_transaction( + origin, transaction_data + ) return code, response From 0b3112123da5fae4964db784e3bab0c4d83d9d62 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 9 Apr 2021 13:44:38 -0400 Subject: [PATCH 036/619] Use mock from the stdlib. (#9772) --- changelog.d/9772.misc | 1 + setup.cfg | 3 +-- setup.py | 2 +- synmark/suites/logging.py | 3 +-- tests/api/test_auth.py | 2 +- tests/app/test_openid_listener.py | 2 +- tests/appservice/test_appservice.py | 3 +-- tests/appservice/test_scheduler.py | 2 +- tests/crypto/test_keyring.py | 3 +-- tests/events/test_presence_router.py | 6 +++--- tests/federation/test_complexity.py | 2 +- tests/federation/test_federation_catch_up.py | 3 +-- tests/federation/test_federation_sender.py | 3 +-- tests/handlers/test_admin.py | 3 +-- tests/handlers/test_appservice.py | 2 +- tests/handlers/test_auth.py | 2 +- tests/handlers/test_cas.py | 2 +- tests/handlers/test_directory.py | 2 +- tests/handlers/test_e2e_keys.py | 2 +- tests/handlers/test_e2e_room_keys.py | 3 +-- tests/handlers/test_oidc.py | 3 +-- tests/handlers/test_password_providers.py | 3 +-- tests/handlers/test_presence.py | 2 +- tests/handlers/test_profile.py | 2 +- tests/handlers/test_register.py | 2 +- tests/handlers/test_saml.py | 3 +-- tests/handlers/test_typing.py | 3 +-- tests/handlers/test_user_directory.py | 2 +- tests/http/federation/test_matrix_federation_agent.py | 3 +-- tests/http/federation/test_srv_resolver.py | 2 +- tests/http/test_client.py | 3 +-- tests/http/test_fedclient.py | 2 +- tests/http/test_servlet.py | 3 +-- tests/http/test_simple_client.py | 2 +- tests/logging/test_terse_json.py | 3 +-- tests/module_api/test_api.py | 5 +++-- tests/push/test_http.py | 2 +- tests/replication/slave/storage/_base.py | 2 +- tests/replication/tcp/streams/test_receipts.py | 2 +- tests/replication/tcp/streams/test_typing.py | 2 +- tests/replication/test_federation_ack.py | 2 +- tests/replication/test_federation_sender_shard.py | 3 +-- tests/replication/test_pusher_shard.py | 3 +-- tests/replication/test_sharded_event_persister.py | 3 +-- tests/rest/admin/test_admin.py | 3 +-- tests/rest/admin/test_room.py | 3 +-- tests/rest/admin/test_user.py | 3 +-- tests/rest/client/test_retention.py | 2 +- tests/rest/client/test_shadow_banned.py | 2 +- tests/rest/client/test_third_party_rules.py | 3 +-- tests/rest/client/test_transactions.py | 2 +- tests/rest/client/v1/test_events.py | 2 +- tests/rest/client/v1/test_login.py | 3 +-- tests/rest/client/v1/test_presence.py | 2 +- tests/rest/client/v1/test_rooms.py | 3 +-- tests/rest/client/v1/test_typing.py | 2 +- tests/rest/client/v1/utils.py | 3 +-- tests/rest/key/v2/test_remote_key_resource.py | 3 +-- tests/rest/media/v1/test_media_storage.py | 3 +-- tests/rest/media/v1/test_url_preview.py | 3 +-- tests/scripts/test_new_matrix_user.py | 2 +- tests/server_notices/test_resource_limits_server_notices.py | 2 +- tests/storage/test_appservice.py | 3 +-- tests/storage/test_background_update.py | 2 +- tests/storage/test_base.py | 3 +-- tests/storage/test_cleanup_extrems.py | 4 +--- tests/storage/test_client_ips.py | 2 +- tests/storage/test_event_push_actions.py | 2 +- tests/storage/test_monthly_active_users.py | 2 +- tests/test_distributor.py | 2 +- tests/test_federation.py | 2 +- tests/test_phone_home.py | 3 +-- tests/test_state.py | 3 +-- tests/test_terms_auth.py | 3 +-- tests/test_utils/__init__.py | 3 +-- tests/test_visibility.py | 3 +-- tests/unittest.py | 3 +-- tests/util/caches/test_descriptors.py | 3 +-- tests/util/caches/test_ttlcache.py | 2 +- tests/util/test_file_consumer.py | 3 +-- tests/util/test_lrucache.py | 2 +- tests/utils.py | 3 +-- 82 files changed, 86 insertions(+), 126 deletions(-) create mode 100644 changelog.d/9772.misc diff --git a/changelog.d/9772.misc b/changelog.d/9772.misc new file mode 100644 index 0000000000..ec7d94cc25 --- /dev/null +++ b/changelog.d/9772.misc @@ -0,0 +1 @@ +Use mock from the standard library instead of a separate package. diff --git a/setup.cfg b/setup.cfg index 5fdb51ac73..33601b71d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,11 +23,10 @@ ignore=W503,W504,E203,E731,E501,B007 [isort] line_length = 88 -sections=FUTURE,STDLIB,COMPAT,THIRDPARTY,TWISTED,FIRSTPARTY,TESTS,LOCALFOLDER +sections=FUTURE,STDLIB,THIRDPARTY,TWISTED,FIRSTPARTY,TESTS,LOCALFOLDER default_section=THIRDPARTY known_first_party = synapse known_tests=tests -known_compat = mock known_twisted=twisted,OpenSSL multi_line_output=3 include_trailing_comma=true diff --git a/setup.py b/setup.py index 4e9e333c60..2eb70d0bb3 100755 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def exec_file(path_segments): # Tests assume that all optional dependencies are installed. # # parameterized_class decorator was introduced in parameterized 0.7.0 -CONDITIONAL_REQUIREMENTS["test"] = ["mock>=2.0", "parameterized>=0.7.0"] +CONDITIONAL_REQUIREMENTS["test"] = ["parameterized>=0.7.0"] setup( name="matrix-synapse", diff --git a/synmark/suites/logging.py b/synmark/suites/logging.py index c306891b27..b3abc6b254 100644 --- a/synmark/suites/logging.py +++ b/synmark/suites/logging.py @@ -16,8 +16,7 @@ import logging import warnings from io import StringIO - -from mock import Mock +from unittest.mock import Mock from pyperf import perf_counter diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 34f72ae795..28d77f0ca2 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock import pymacaroons diff --git a/tests/app/test_openid_listener.py b/tests/app/test_openid_listener.py index 467033e201..33a37fe35e 100644 --- a/tests/app/test_openid_listener.py +++ b/tests/app/test_openid_listener.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock, patch +from unittest.mock import Mock, patch from parameterized import parameterized diff --git a/tests/appservice/test_appservice.py b/tests/appservice/test_appservice.py index 0bffeb1150..03a7440eec 100644 --- a/tests/appservice/test_appservice.py +++ b/tests/appservice/test_appservice.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import re - -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/appservice/test_scheduler.py b/tests/appservice/test_scheduler.py index 97f8cad0dd..3c27d797fb 100644 --- a/tests/appservice/test_scheduler.py +++ b/tests/appservice/test_scheduler.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index 946482b7e7..a56063315b 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import time - -from mock import Mock +from unittest.mock import Mock import attr import canonicaljson diff --git a/tests/events/test_presence_router.py b/tests/events/test_presence_router.py index c6e547f11c..c996ecc221 100644 --- a/tests/events/test_presence_router.py +++ b/tests/events/test_presence_router.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Iterable, List, Optional, Set, Tuple, Union - -from mock import Mock +from unittest.mock import Mock import attr @@ -314,7 +313,8 @@ def test_send_local_online_presence_to_with_module(self): self.hs.get_federation_transport_client().send_transaction.call_args_list ) for call in calls: - federation_transaction = call.args[0] # type: Transaction + call_args = call[0] + federation_transaction = call_args[0] # type: Transaction # Get the sent EDUs in this transaction edus = federation_transaction.get_dict()["edus"] diff --git a/tests/federation/test_complexity.py b/tests/federation/test_complexity.py index 8186b8ca01..701fa8379f 100644 --- a/tests/federation/test_complexity.py +++ b/tests/federation/test_complexity.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.api.errors import Codes, SynapseError from synapse.rest import admin diff --git a/tests/federation/test_federation_catch_up.py b/tests/federation/test_federation_catch_up.py index 95eac6a5a3..802c5ad299 100644 --- a/tests/federation/test_federation_catch_up.py +++ b/tests/federation/test_federation_catch_up.py @@ -1,6 +1,5 @@ from typing import List, Tuple - -from mock import Mock +from unittest.mock import Mock from synapse.api.constants import EventTypes from synapse.events import EventBase diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index ecc3faa572..deb12433cf 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from typing import Optional - -from mock import Mock +from unittest.mock import Mock from signedjson import key, sign from signedjson.types import BaseKey, SigningKey diff --git a/tests/handlers/test_admin.py b/tests/handlers/test_admin.py index a01fdd0839..32669ae9ce 100644 --- a/tests/handlers/test_admin.py +++ b/tests/handlers/test_admin.py @@ -14,8 +14,7 @@ # limitations under the License. from collections import Counter - -from mock import Mock +from unittest.mock import Mock import synapse.api.errors import synapse.handlers.admin diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py index d5d3fdd99a..6e325b24ce 100644 --- a/tests/handlers/test_appservice.py +++ b/tests/handlers/test_appservice.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/handlers/test_auth.py b/tests/handlers/test_auth.py index c9f889b511..321c5ba045 100644 --- a/tests/handlers/test_auth.py +++ b/tests/handlers/test_auth.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock import pymacaroons diff --git a/tests/handlers/test_cas.py b/tests/handlers/test_cas.py index 7975af243c..0444b26798 100644 --- a/tests/handlers/test_cas.py +++ b/tests/handlers/test_cas.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.handlers.cas_handler import CasResponse diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 863d8737b2..6ae9d4f865 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -14,7 +14,7 @@ # limitations under the License. -from mock import Mock +from unittest.mock import Mock import synapse import synapse.api.errors diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 5e86c5e56b..6915ac0205 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -14,7 +14,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import mock +from unittest import mock from signedjson import key as key, sign as sign diff --git a/tests/handlers/test_e2e_room_keys.py b/tests/handlers/test_e2e_room_keys.py index d7498aa51a..07893302ec 100644 --- a/tests/handlers/test_e2e_room_keys.py +++ b/tests/handlers/test_e2e_room_keys.py @@ -16,8 +16,7 @@ # limitations under the License. import copy - -import mock +from unittest import mock from synapse.api.errors import SynapseError diff --git a/tests/handlers/test_oidc.py b/tests/handlers/test_oidc.py index c7796fb837..8702ee70e0 100644 --- a/tests/handlers/test_oidc.py +++ b/tests/handlers/test_oidc.py @@ -14,10 +14,9 @@ # limitations under the License. import json import os +from unittest.mock import ANY, Mock, patch from urllib.parse import parse_qs, urlparse -from mock import ANY, Mock, patch - import pymacaroons from synapse.handlers.sso import MappingException diff --git a/tests/handlers/test_password_providers.py b/tests/handlers/test_password_providers.py index a98a65ae67..e28e4159eb 100644 --- a/tests/handlers/test_password_providers.py +++ b/tests/handlers/test_password_providers.py @@ -16,8 +16,7 @@ """Tests for the password_auth_provider interface""" from typing import Any, Type, Union - -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 77330f59a9..9f16cc65fc 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -14,7 +14,7 @@ # limitations under the License. -from mock import Mock, call +from unittest.mock import Mock, call from signedjson.key import generate_signing_key diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 75c6a4e21c..d8b1bcac8b 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock import synapse.types from synapse.api.errors import AuthError, SynapseError diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index 94b6903594..69279a5ce9 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.api.auth import Auth from synapse.api.constants import UserTypes diff --git a/tests/handlers/test_saml.py b/tests/handlers/test_saml.py index 30efd43b40..8cfc184fef 100644 --- a/tests/handlers/test_saml.py +++ b/tests/handlers/test_saml.py @@ -13,8 +13,7 @@ # limitations under the License. from typing import Optional - -from mock import Mock +from unittest.mock import Mock import attr diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 24e7138196..9fa231a37a 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -16,8 +16,7 @@ import json from typing import Dict - -from mock import ANY, Mock, call +from unittest.mock import ANY, Mock, call from twisted.internet import defer from twisted.web.resource import Resource diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index 98b2f5b383..c68cb830af 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index 73e12ea6c3..ae9d4504a8 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -14,8 +14,7 @@ # limitations under the License. import logging from typing import Optional - -from mock import Mock +from unittest.mock import Mock import treq from netaddr import IPSet diff --git a/tests/http/federation/test_srv_resolver.py b/tests/http/federation/test_srv_resolver.py index fee2985d35..466ce722d9 100644 --- a/tests/http/federation/test_srv_resolver.py +++ b/tests/http/federation/test_srv_resolver.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer from twisted.internet.defer import Deferred diff --git a/tests/http/test_client.py b/tests/http/test_client.py index 0ce181a51e..7e2f2a01cc 100644 --- a/tests/http/test_client.py +++ b/tests/http/test_client.py @@ -13,8 +13,7 @@ # limitations under the License. from io import BytesIO - -from mock import Mock +from unittest.mock import Mock from netaddr import IPSet diff --git a/tests/http/test_fedclient.py b/tests/http/test_fedclient.py index 9c52c8fdca..21c1297171 100644 --- a/tests/http/test_fedclient.py +++ b/tests/http/test_fedclient.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from netaddr import IPSet from parameterized import parameterized diff --git a/tests/http/test_servlet.py b/tests/http/test_servlet.py index 45089158ce..f979c96f7c 100644 --- a/tests/http/test_servlet.py +++ b/tests/http/test_servlet.py @@ -14,8 +14,7 @@ # limitations under the License. import json from io import BytesIO - -from mock import Mock +from unittest.mock import Mock from synapse.api.errors import SynapseError from synapse.http.servlet import ( diff --git a/tests/http/test_simple_client.py b/tests/http/test_simple_client.py index a1cf0862d4..cc4cae320d 100644 --- a/tests/http/test_simple_client.py +++ b/tests/http/test_simple_client.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from netaddr import IPSet diff --git a/tests/logging/test_terse_json.py b/tests/logging/test_terse_json.py index bfe0d11c93..215fd8b0f9 100644 --- a/tests/logging/test_terse_json.py +++ b/tests/logging/test_terse_json.py @@ -15,8 +15,7 @@ import json import logging from io import BytesIO, StringIO - -from mock import Mock, patch +from unittest.mock import Mock, patch from twisted.web.server import Request diff --git a/tests/module_api/test_api.py b/tests/module_api/test_api.py index 1d1fceeecf..349f93560e 100644 --- a/tests/module_api/test_api.py +++ b/tests/module_api/test_api.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.api.constants import EduTypes from synapse.events import EventBase @@ -358,7 +358,8 @@ def test_send_local_online_presence_to_federation(self): self.hs.get_federation_transport_client().send_transaction.call_args_list ) for call in calls: - federation_transaction = call.args[0] # type: Transaction + call_args = call[0] + federation_transaction = call_args[0] # type: Transaction # Get the sent EDUs in this transaction edus = federation_transaction.get_dict()["edus"] diff --git a/tests/push/test_http.py b/tests/push/test_http.py index 60f0820cff..4074ade87a 100644 --- a/tests/push/test_http.py +++ b/tests/push/test_http.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet.defer import Deferred diff --git a/tests/replication/slave/storage/_base.py b/tests/replication/slave/storage/_base.py index 56497b8476..83e89383f6 100644 --- a/tests/replication/slave/storage/_base.py +++ b/tests/replication/slave/storage/_base.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from tests.replication._base import BaseStreamTestCase diff --git a/tests/replication/tcp/streams/test_receipts.py b/tests/replication/tcp/streams/test_receipts.py index 56b062ecc1..7d848e41ff 100644 --- a/tests/replication/tcp/streams/test_receipts.py +++ b/tests/replication/tcp/streams/test_receipts.py @@ -15,7 +15,7 @@ # type: ignore -from mock import Mock +from unittest.mock import Mock from synapse.replication.tcp.streams._base import ReceiptsStream diff --git a/tests/replication/tcp/streams/test_typing.py b/tests/replication/tcp/streams/test_typing.py index ca49d4dd3a..4a0b342264 100644 --- a/tests/replication/tcp/streams/test_typing.py +++ b/tests/replication/tcp/streams/test_typing.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.handlers.typing import RoomMember from synapse.replication.tcp.streams import TypingStream diff --git a/tests/replication/test_federation_ack.py b/tests/replication/test_federation_ack.py index 0d9e3bb11d..44ad5eec57 100644 --- a/tests/replication/test_federation_ack.py +++ b/tests/replication/test_federation_ack.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +from unittest import mock from synapse.app.generic_worker import GenericWorkerServer from synapse.replication.tcp.commands import FederationAckCommand diff --git a/tests/replication/test_federation_sender_shard.py b/tests/replication/test_federation_sender_shard.py index 2f2d117858..8ca595c3ee 100644 --- a/tests/replication/test_federation_sender_shard.py +++ b/tests/replication/test_federation_sender_shard.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging - -from mock import Mock +from unittest.mock import Mock from synapse.api.constants import EventTypes, Membership from synapse.events.builder import EventBuilderFactory diff --git a/tests/replication/test_pusher_shard.py b/tests/replication/test_pusher_shard.py index ab2988a6ba..1f12bde1aa 100644 --- a/tests/replication/test_pusher_shard.py +++ b/tests/replication/test_pusher_shard.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging - -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/replication/test_sharded_event_persister.py b/tests/replication/test_sharded_event_persister.py index c9b773fbd2..6c2e1674cb 100644 --- a/tests/replication/test_sharded_event_persister.py +++ b/tests/replication/test_sharded_event_persister.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging - -from mock import patch +from unittest.mock import patch from synapse.api.room_versions import RoomVersion from synapse.rest import admin diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 057e27372e..4abcbe3f55 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -17,8 +17,7 @@ import os import urllib.parse from binascii import unhexlify - -from mock import Mock +from unittest.mock import Mock from twisted.internet.defer import Deferred diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index b55160b70a..85f77c0a65 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -16,8 +16,7 @@ import json import urllib.parse from typing import List, Optional - -from mock import Mock +from unittest.mock import Mock import synapse.rest.admin from synapse.api.constants import EventTypes, Membership diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 0c9ec133c2..e47fd3ded8 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -19,8 +19,7 @@ import urllib.parse from binascii import unhexlify from typing import List, Optional - -from mock import Mock +from unittest.mock import Mock import synapse.rest.admin from synapse.api.constants import UserTypes diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py index aee99bb6a0..f892a71228 100644 --- a/tests/rest/client/test_retention.py +++ b/tests/rest/client/test_retention.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.api.constants import EventTypes from synapse.rest import admin diff --git a/tests/rest/client/test_shadow_banned.py b/tests/rest/client/test_shadow_banned.py index d2cce44032..288ee12888 100644 --- a/tests/rest/client/test_shadow_banned.py +++ b/tests/rest/client/test_shadow_banned.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock, patch +from unittest.mock import Mock, patch import synapse.rest.admin from synapse.api.constants import EventTypes diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index bf39014277..a7ebe0c3e9 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -14,8 +14,7 @@ # limitations under the License. import threading from typing import Dict - -from mock import Mock +from unittest.mock import Mock from synapse.events import EventBase from synapse.module_api import ModuleApi diff --git a/tests/rest/client/test_transactions.py b/tests/rest/client/test_transactions.py index 171632e195..3b5747cb12 100644 --- a/tests/rest/client/test_transactions.py +++ b/tests/rest/client/test_transactions.py @@ -1,4 +1,4 @@ -from mock import Mock, call +from unittest.mock import Mock, call from twisted.internet import defer, reactor diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py index 2ae896db1e..87a18d2cb9 100644 --- a/tests/rest/client/v1/test_events.py +++ b/tests/rest/client/v1/test_events.py @@ -15,7 +15,7 @@ """ Tests REST events for /events paths.""" -from mock import Mock +from unittest.mock import Mock import synapse.rest.admin from synapse.rest.client.v1 import events, login, room diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index 988821b16f..c7b79ab8a7 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -16,10 +16,9 @@ import time import urllib.parse from typing import Any, Dict, List, Optional, Union +from unittest.mock import Mock from urllib.parse import urlencode -from mock import Mock - import pymacaroons from twisted.web.resource import Resource diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 94a5154834..c136827f79 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 715414a310..4df20c90fd 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -20,10 +20,9 @@ import json from typing import Iterable +from unittest.mock import Mock from urllib import parse as urlparse -from mock import Mock - import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.handlers.pagination import PurgeStatus diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 329dbd06de..0b8f565121 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -16,7 +16,7 @@ """Tests REST events for /rooms paths.""" -from mock import Mock +from unittest.mock import Mock from synapse.rest.client.v1 import room from synapse.types import UserID diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index 8a4dddae2b..a6a292b20c 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -21,8 +21,7 @@ import time import urllib.parse from typing import Any, Dict, Mapping, MutableMapping, Optional - -from mock import patch +from unittest.mock import patch import attr diff --git a/tests/rest/key/v2/test_remote_key_resource.py b/tests/rest/key/v2/test_remote_key_resource.py index 9d0d0ef414..eb8687ce68 100644 --- a/tests/rest/key/v2/test_remote_key_resource.py +++ b/tests/rest/key/v2/test_remote_key_resource.py @@ -14,8 +14,7 @@ # limitations under the License. import urllib.parse from io import BytesIO, StringIO - -from mock import Mock +from unittest.mock import Mock import signedjson.key from canonicaljson import encode_canonical_json diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index 9f77125fd4..375f0b7977 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -18,10 +18,9 @@ from binascii import unhexlify from io import BytesIO from typing import Optional +from unittest.mock import Mock from urllib import parse -from mock import Mock - import attr from parameterized import parameterized_class from PIL import Image as Image diff --git a/tests/rest/media/v1/test_url_preview.py b/tests/rest/media/v1/test_url_preview.py index 6968502433..9067463e54 100644 --- a/tests/rest/media/v1/test_url_preview.py +++ b/tests/rest/media/v1/test_url_preview.py @@ -15,8 +15,7 @@ import json import os import re - -from mock import patch +from unittest.mock import patch from twisted.internet._resolver import HostResolution from twisted.internet.address import IPv4Address, IPv6Address diff --git a/tests/scripts/test_new_matrix_user.py b/tests/scripts/test_new_matrix_user.py index 6f56893f5e..885b95a51f 100644 --- a/tests/scripts/test_new_matrix_user.py +++ b/tests/scripts/test_new_matrix_user.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse._scripts.register_new_matrix_user import request_registration diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index d40d65b06a..450b4ec710 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/storage/test_appservice.py b/tests/storage/test_appservice.py index 1ce29af5fd..e755a4db62 100644 --- a/tests/storage/test_appservice.py +++ b/tests/storage/test_appservice.py @@ -15,8 +15,7 @@ import json import os import tempfile - -from mock import Mock +from unittest.mock import Mock import yaml diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index 1b4fae0bb5..069db0edc4 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -1,4 +1,4 @@ -from mock import Mock +from unittest.mock import Mock from synapse.storage.background_updates import BackgroundUpdater diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index eac7e4dcd2..54e9e7f6fe 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -15,8 +15,7 @@ from collections import OrderedDict - -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/storage/test_cleanup_extrems.py b/tests/storage/test_cleanup_extrems.py index 7791138688..b02fb32ced 100644 --- a/tests/storage/test_cleanup_extrems.py +++ b/tests/storage/test_cleanup_extrems.py @@ -14,9 +14,7 @@ # limitations under the License. import os.path -from unittest.mock import patch - -from mock import Mock +from unittest.mock import Mock, patch import synapse.rest.admin from synapse.api.constants import EventTypes diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index a8a6ddc466..f7f75320ba 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock import synapse.rest.admin from synapse.http.site import XForwardedForRequest diff --git a/tests/storage/test_event_push_actions.py b/tests/storage/test_event_push_actions.py index 239f7c9faf..0289942f88 100644 --- a/tests/storage/test_event_push_actions.py +++ b/tests/storage/test_event_push_actions.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from tests.unittest import HomeserverTestCase diff --git a/tests/storage/test_monthly_active_users.py b/tests/storage/test_monthly_active_users.py index 5858c7fcc4..47556791f4 100644 --- a/tests/storage/test_monthly_active_users.py +++ b/tests/storage/test_monthly_active_users.py @@ -12,7 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/test_distributor.py b/tests/test_distributor.py index b57f36e6ac..6a6cf709f6 100644 --- a/tests/test_distributor.py +++ b/tests/test_distributor.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock, patch +from unittest.mock import Mock, patch from synapse.util.distributor import Distributor diff --git a/tests/test_federation.py b/tests/test_federation.py index fc9aab32d0..8928597d17 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from twisted.internet.defer import succeed diff --git a/tests/test_phone_home.py b/tests/test_phone_home.py index e7aed092c2..0f800a075b 100644 --- a/tests/test_phone_home.py +++ b/tests/test_phone_home.py @@ -14,8 +14,7 @@ # limitations under the License. import resource - -import mock +from unittest import mock from synapse.app.phone_stats_home import phone_stats_home diff --git a/tests/test_state.py b/tests/test_state.py index 1d2019699d..83383d8872 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from typing import List, Optional - -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer diff --git a/tests/test_terms_auth.py b/tests/test_terms_auth.py index a743cdc3a9..0df480db9f 100644 --- a/tests/test_terms_auth.py +++ b/tests/test_terms_auth.py @@ -13,8 +13,7 @@ # limitations under the License. import json - -from mock import Mock +from unittest.mock import Mock from twisted.test.proto_helpers import MemoryReactorClock diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py index 43898d8142..b557ffd692 100644 --- a/tests/test_utils/__init__.py +++ b/tests/test_utils/__init__.py @@ -21,8 +21,7 @@ import warnings from asyncio import Future from typing import Any, Awaitable, Callable, TypeVar - -from mock import Mock +from unittest.mock import Mock import attr diff --git a/tests/test_visibility.py b/tests/test_visibility.py index 1b4dd47a82..e502ac197e 100644 --- a/tests/test_visibility.py +++ b/tests/test_visibility.py @@ -14,8 +14,7 @@ # limitations under the License. import logging from typing import Optional - -from mock import Mock +from unittest.mock import Mock from twisted.internet import defer from twisted.internet.defer import succeed diff --git a/tests/unittest.py b/tests/unittest.py index 57b6a395c7..92764434bd 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -21,8 +21,7 @@ import logging import time from typing import Callable, Dict, Iterable, Optional, Tuple, Type, TypeVar, Union - -from mock import Mock, patch +from unittest.mock import Mock, patch from canonicaljson import json diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index e434e21aee..2d1f9360e0 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -15,8 +15,7 @@ # limitations under the License. import logging from typing import Set - -import mock +from unittest import mock from twisted.internet import defer, reactor diff --git a/tests/util/caches/test_ttlcache.py b/tests/util/caches/test_ttlcache.py index 816795c136..23018081e5 100644 --- a/tests/util/caches/test_ttlcache.py +++ b/tests/util/caches/test_ttlcache.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.util.caches.ttlcache import TTLCache diff --git a/tests/util/test_file_consumer.py b/tests/util/test_file_consumer.py index 2012263184..d1372f6bc2 100644 --- a/tests/util/test_file_consumer.py +++ b/tests/util/test_file_consumer.py @@ -16,8 +16,7 @@ import threading from io import StringIO - -from mock import NonCallableMock +from unittest.mock import NonCallableMock from twisted.internet import defer, reactor diff --git a/tests/util/test_lrucache.py b/tests/util/test_lrucache.py index a739a6aaaf..ce4f1cc30a 100644 --- a/tests/util/test_lrucache.py +++ b/tests/util/test_lrucache.py @@ -14,7 +14,7 @@ # limitations under the License. -from mock import Mock +from unittest.mock import Mock from synapse.util.caches.lrucache import LruCache from synapse.util.caches.treecache import TreeCache diff --git a/tests/utils.py b/tests/utils.py index a141ee6496..2e34fad11c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -21,10 +21,9 @@ import uuid import warnings from typing import Type +from unittest.mock import Mock, patch from urllib import parse as urlparse -from mock import Mock, patch - from twisted.internet import defer from synapse.api.constants import EventTypes From e300ef64b16280ce2fdb7a8a9baef68b08aa9c88 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 12 Apr 2021 15:13:55 +0100 Subject: [PATCH 037/619] Require AppserviceRegistrationType (#9548) This change ensures that the appservice registration behaviour follows the spec. We decided to do this for Dendrite, so it made sense to also make a PR for synapse to correct the behaviour. --- changelog.d/9548.removal | 1 + synapse/api/constants.py | 5 ++++ synapse/rest/client/v2_alpha/register.py | 23 ++++++++++----- tests/rest/client/v2_alpha/test_register.py | 31 ++++++++++++++++++--- tests/test_mau.py | 23 ++++++++------- 5 files changed, 60 insertions(+), 23 deletions(-) create mode 100644 changelog.d/9548.removal diff --git a/changelog.d/9548.removal b/changelog.d/9548.removal new file mode 100644 index 0000000000..1fb88236c6 --- /dev/null +++ b/changelog.d/9548.removal @@ -0,0 +1 @@ +Make `/_matrix/client/r0/register` expect a type of `m.login.application_service` when an Application Service registers a user, to align with [the relevant spec](https://spec.matrix.org/unstable/application-service-api/#server-admin-style-permissions). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 6856dab06c..a8ae41de48 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -73,6 +73,11 @@ class LoginType: DUMMY = "m.login.dummy" +# This is used in the `type` parameter for /register when called by +# an appservice to register a new user. +APP_SERVICE_REGISTRATION_TYPE = "m.login.application_service" + + class EventTypes: Member = "m.room.member" Create = "m.room.create" diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index c212da0cb2..4a064849c1 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -13,7 +13,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 hmac import logging import random @@ -22,7 +21,7 @@ import synapse import synapse.api.auth import synapse.types -from synapse.api.constants import LoginType +from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType from synapse.api.errors import ( Codes, InteractiveAuthIncompleteError, @@ -430,15 +429,20 @@ async def on_POST(self, request): raise SynapseError(400, "Invalid username") desired_username = body["username"] - appservice = None - if self.auth.has_access_token(request): - appservice = self.auth.get_appservice_by_req(request) - # fork off as soon as possible for ASes which have completely # different registration flows to normal users # == Application Service Registration == - if appservice: + if body.get("type") == APP_SERVICE_REGISTRATION_TYPE: + if not self.auth.has_access_token(request): + raise SynapseError( + 400, + "Appservice token must be provided when using a type of m.login.application_service", + ) + + # Verify the AS + self.auth.get_appservice_by_req(request) + # Set the desired user according to the AS API (which uses the # 'user' key not 'username'). Since this is a new addition, we'll # fallback to 'username' if they gave one. @@ -459,6 +463,11 @@ async def on_POST(self, request): ) return 200, result + elif self.auth.has_access_token(request): + raise SynapseError( + 400, + "An access token should not be provided on requests to /register (except if type is m.login.application_service)", + ) # == Normal User Registration == (everyone else) if not self._registration_enabled: diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index 27db4f551e..cd60ea7081 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -14,7 +14,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 datetime import json import os @@ -22,7 +21,7 @@ import pkg_resources import synapse.rest.admin -from synapse.api.constants import LoginType +from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType from synapse.api.errors import Codes from synapse.appservice import ApplicationService from synapse.rest.client.v1 import login, logout @@ -59,7 +58,9 @@ def test_POST_appservice_registration_valid(self): ) self.hs.get_datastore().services_cache.append(appservice) - request_data = json.dumps({"username": "as_user_kermit"}) + request_data = json.dumps( + {"username": "as_user_kermit", "type": APP_SERVICE_REGISTRATION_TYPE} + ) channel = self.make_request( b"POST", self.url + b"?access_token=i_am_an_app_service", request_data @@ -69,9 +70,31 @@ def test_POST_appservice_registration_valid(self): det_data = {"user_id": user_id, "home_server": self.hs.hostname} self.assertDictContainsSubset(det_data, channel.json_body) + def test_POST_appservice_registration_no_type(self): + as_token = "i_am_an_app_service" + + appservice = ApplicationService( + as_token, + self.hs.config.server_name, + id="1234", + namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]}, + sender="@as:test", + ) + + self.hs.get_datastore().services_cache.append(appservice) + request_data = json.dumps({"username": "as_user_kermit"}) + + channel = self.make_request( + b"POST", self.url + b"?access_token=i_am_an_app_service", request_data + ) + + self.assertEquals(channel.result["code"], b"400", channel.result) + def test_POST_appservice_registration_invalid(self): self.appservice = None # no application service exists - request_data = json.dumps({"username": "kermit"}) + request_data = json.dumps( + {"username": "kermit", "type": APP_SERVICE_REGISTRATION_TYPE} + ) channel = self.make_request( b"POST", self.url + b"?access_token=i_am_an_app_service", request_data ) diff --git a/tests/test_mau.py b/tests/test_mau.py index 75d28a42df..7d92a16a8d 100644 --- a/tests/test_mau.py +++ b/tests/test_mau.py @@ -15,9 +15,7 @@ """Tests REST events for /rooms paths.""" -import json - -from synapse.api.constants import LoginType +from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType from synapse.api.errors import Codes, HttpResponseException, SynapseError from synapse.appservice import ApplicationService from synapse.rest.client.v2_alpha import register, sync @@ -113,7 +111,7 @@ def test_as_ignores_mau(self): ) ) - self.create_user("as_kermit4", token=as_token) + self.create_user("as_kermit4", token=as_token, appservice=True) def test_allowed_after_a_month_mau(self): # Create and sync so that the MAU counts get updated @@ -232,14 +230,15 @@ def test_tracked_but_not_limited(self): self.reactor.advance(100) self.assertEqual(2, self.successResultOf(count)) - def create_user(self, localpart, token=None): - request_data = json.dumps( - { - "username": localpart, - "password": "monkey", - "auth": {"type": LoginType.DUMMY}, - } - ) + def create_user(self, localpart, token=None, appservice=False): + request_data = { + "username": localpart, + "password": "monkey", + "auth": {"type": LoginType.DUMMY}, + } + + if appservice: + request_data["type"] = APP_SERVICE_REGISTRATION_TYPE channel = self.make_request( "POST", From 3efde8b69a660be22c65e0a548b4a7d7f815cb5b Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Mon, 12 Apr 2021 15:27:05 +0100 Subject: [PATCH 038/619] Add option to skip unit tests when building debs (#9793) Signed-off-by: Dan Callahan --- changelog.d/9793.misc | 1 + debian/build_virtualenv | 23 ++++++++++++++++------- debian/changelog | 6 ++++++ scripts-dev/build_debian_packages | 17 +++++++++++------ 4 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 changelog.d/9793.misc diff --git a/changelog.d/9793.misc b/changelog.d/9793.misc new file mode 100644 index 0000000000..6334689d26 --- /dev/null +++ b/changelog.d/9793.misc @@ -0,0 +1 @@ +Add option to skip unit tests when building Debian packages. diff --git a/debian/build_virtualenv b/debian/build_virtualenv index cad7d16883..21caad90cc 100755 --- a/debian/build_virtualenv +++ b/debian/build_virtualenv @@ -50,15 +50,24 @@ PACKAGE_BUILD_DIR="debian/matrix-synapse-py3" VIRTUALENV_DIR="${PACKAGE_BUILD_DIR}${DH_VIRTUALENV_INSTALL_ROOT}/matrix-synapse" TARGET_PYTHON="${VIRTUALENV_DIR}/bin/python" -# we copy the tests to a temporary directory so that we can put them on the -# PYTHONPATH without putting the uninstalled synapse on the pythonpath. -tmpdir=`mktemp -d` -trap "rm -r $tmpdir" EXIT +case "$DEB_BUILD_OPTIONS" in + *nocheck*) + # Skip running tests if "nocheck" present in $DEB_BUILD_OPTIONS + ;; + + *) + # Copy tests to a temporary directory so that we can put them on the + # PYTHONPATH without putting the uninstalled synapse on the pythonpath. + tmpdir=`mktemp -d` + trap "rm -r $tmpdir" EXIT + + cp -r tests "$tmpdir" -cp -r tests "$tmpdir" + PYTHONPATH="$tmpdir" \ + "${TARGET_PYTHON}" -m twisted.trial --reporter=text -j2 tests -PYTHONPATH="$tmpdir" \ - "${TARGET_PYTHON}" -m twisted.trial --reporter=text -j2 tests + ;; +esac # build the config file "${TARGET_PYTHON}" "${VIRTUALENV_DIR}/bin/generate_config" \ diff --git a/debian/changelog b/debian/changelog index 09602ff54b..5d526316fc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.31.0+nmu1) UNRELEASED; urgency=medium + + * Skip tests when DEB_BUILD_OPTIONS contains "nocheck". + + -- Dan Callahan Mon, 12 Apr 2021 13:07:36 +0000 + matrix-synapse-py3 (1.31.0) stable; urgency=medium * New synapse release 1.31.0. diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index bddc441df2..3bb6e2c7ea 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -41,7 +41,7 @@ class Builder(object): self._lock = threading.Lock() self._failed = False - def run_build(self, dist): + def run_build(self, dist, skip_tests=False): """Build deb for a single distribution""" if self._failed: @@ -49,13 +49,13 @@ class Builder(object): raise Exception("failed") try: - self._inner_build(dist) + self._inner_build(dist, skip_tests) except Exception as e: print("build of %s failed: %s" % (dist, e), file=sys.stderr) self._failed = True raise - def _inner_build(self, dist): + def _inner_build(self, dist, skip_tests=False): projdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) os.chdir(projdir) @@ -99,6 +99,7 @@ class Builder(object): "--volume=" + debsdir + ":/debs", "-e", "TARGET_USERID=%i" % (os.getuid(), ), "-e", "TARGET_GROUPID=%i" % (os.getgid(), ), + "-e", "DEB_BUILD_OPTIONS=%s" % ("nocheck" if skip_tests else ""), "dh-venv-builder:" + tag, ], stdout=stdout, stderr=subprocess.STDOUT) @@ -122,7 +123,7 @@ class Builder(object): self.active_containers.remove(c) -def run_builds(dists, jobs=1): +def run_builds(dists, jobs=1, skip_tests=False): builder = Builder(redirect_stdout=(jobs > 1)) def sig(signum, _frame): @@ -131,7 +132,7 @@ def run_builds(dists, jobs=1): signal.signal(signal.SIGINT, sig) with ThreadPoolExecutor(max_workers=jobs) as e: - res = e.map(builder.run_build, dists) + res = e.map(lambda dist: builder.run_build(dist, skip_tests), dists) # make sure we consume the iterable so that exceptions are raised. for r in res: @@ -146,9 +147,13 @@ if __name__ == '__main__': '-j', '--jobs', type=int, default=1, help='specify the number of builds to run in parallel', ) + parser.add_argument( + '--no-check', action='store_true', + help='skip running tests after building', + ) parser.add_argument( 'dist', nargs='*', default=DISTS, help='a list of distributions to build for. Default: %(default)s', ) args = parser.parse_args() - run_builds(dists=args.dist, jobs=args.jobs) + run_builds(dists=args.dist, jobs=args.jobs, skip_tests=args.no_check) From a7044e5c0f1ef3c1d3844d4b44458b30ca6b80d4 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Mon, 12 Apr 2021 16:00:28 +0100 Subject: [PATCH 039/619] Drop Python 3.5 from Trove classifier metadata. (#9782) * Drop Python 3.5 from Trove classifier metadata. Signed-off-by: Dan Callahan --- changelog.d/9782.misc | 1 + setup.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/9782.misc diff --git a/changelog.d/9782.misc b/changelog.d/9782.misc new file mode 100644 index 0000000000..ecf49cfee1 --- /dev/null +++ b/changelog.d/9782.misc @@ -0,0 +1 @@ +Synapse now requires Python 3.6 or later. It also requires Postgres 9.6 or later or SQLite 3.22 or later. diff --git a/setup.py b/setup.py index 2eb70d0bb3..4530df348a 100755 --- a/setup.py +++ b/setup.py @@ -129,7 +129,6 @@ def exec_file(path_segments): "Topic :: Communications :: Chat", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 1fc97ee876c6f383a6148897d82dbc58703ea9d1 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 13 Apr 2021 11:26:37 +0200 Subject: [PATCH 040/619] Add an admin API to manage ratelimit for a specific user (#9648) --- changelog.d/9648.feature | 1 + docs/admin_api/user_admin_api.rst | 117 +++++++++- synapse/rest/admin/__init__.py | 2 + synapse/rest/admin/users.py | 111 ++++++++++ synapse/storage/databases/main/room.py | 64 +++++- tests/rest/admin/test_user.py | 284 +++++++++++++++++++++++++ 6 files changed, 573 insertions(+), 6 deletions(-) create mode 100644 changelog.d/9648.feature diff --git a/changelog.d/9648.feature b/changelog.d/9648.feature new file mode 100644 index 0000000000..bc77026039 --- /dev/null +++ b/changelog.d/9648.feature @@ -0,0 +1 @@ +Add an admin API to manage ratelimit for a specific user. \ No newline at end of file diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst index a8a5a2628c..dbce9c90b6 100644 --- a/docs/admin_api/user_admin_api.rst +++ b/docs/admin_api/user_admin_api.rst @@ -202,7 +202,7 @@ The following fields are returned in the JSON response body: - ``users`` - An array of objects, each containing information about an user. User objects contain the following fields: - - ``name`` - string - Fully-qualified user ID (ex. `@user:server.com`). + - ``name`` - string - Fully-qualified user ID (ex. ``@user:server.com``). - ``is_guest`` - bool - Status if that user is a guest account. - ``admin`` - bool - Status if that user is a server administrator. - ``user_type`` - string - Type of the user. Normal users are type ``None``. @@ -864,3 +864,118 @@ The following parameters should be set in the URL: - ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must be local. + +Override ratelimiting for users +=============================== + +This API allows to override or disable ratelimiting for a specific user. +There are specific APIs to set, get and delete a ratelimit. + +Get status of ratelimit +----------------------- + +The API is:: + + GET /_synapse/admin/v1/users//override_ratelimit + +To use it, you will need to authenticate by providing an ``access_token`` for a +server admin: see `README.rst `_. + +A response body like the following is returned: + +.. code:: json + + { + "messages_per_second": 0, + "burst_count": 0 + } + +**Parameters** + +The following parameters should be set in the URL: + +- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must + be local. + +**Response** + +The following fields are returned in the JSON response body: + +- ``messages_per_second`` - integer - The number of actions that can + be performed in a second. `0` mean that ratelimiting is disabled for this user. +- ``burst_count`` - integer - How many actions that can be performed before + being limited. + +If **no** custom ratelimit is set, an empty JSON dict is returned. + +.. code:: json + + {} + +Set ratelimit +------------- + +The API is:: + + POST /_synapse/admin/v1/users//override_ratelimit + +To use it, you will need to authenticate by providing an ``access_token`` for a +server admin: see `README.rst `_. + +A response body like the following is returned: + +.. code:: json + + { + "messages_per_second": 0, + "burst_count": 0 + } + +**Parameters** + +The following parameters should be set in the URL: + +- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must + be local. + +Body parameters: + +- ``messages_per_second`` - positive integer, optional. The number of actions that can + be performed in a second. Defaults to ``0``. +- ``burst_count`` - positive integer, optional. How many actions that can be performed + before being limited. Defaults to ``0``. + +To disable users' ratelimit set both values to ``0``. + +**Response** + +The following fields are returned in the JSON response body: + +- ``messages_per_second`` - integer - The number of actions that can + be performed in a second. +- ``burst_count`` - integer - How many actions that can be performed before + being limited. + +Delete ratelimit +---------------- + +The API is:: + + DELETE /_synapse/admin/v1/users//override_ratelimit + +To use it, you will need to authenticate by providing an ``access_token`` for a +server admin: see `README.rst `_. + +An empty JSON dict is returned. + +.. code:: json + + {} + +**Parameters** + +The following parameters should be set in the URL: + +- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must + be local. + diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 5daa795df1..2dec818a5f 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -54,6 +54,7 @@ AccountValidityRenewServlet, DeactivateAccountRestServlet, PushersRestServlet, + RateLimitRestServlet, ResetPasswordRestServlet, SearchUsersRestServlet, ShadowBanRestServlet, @@ -239,6 +240,7 @@ def register_servlets(hs, http_server): ShadowBanRestServlet(hs).register(http_server) ForwardExtremitiesRestServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) + RateLimitRestServlet(hs).register(http_server) def register_servlets_for_client_rest_resource(hs, http_server): diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 595898c259..04990c71fb 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -981,3 +981,114 @@ async def on_POST( await self.store.set_shadow_banned(UserID.from_string(user_id), True) return 200, {} + + +class RateLimitRestServlet(RestServlet): + """An admin API to override ratelimiting for an user. + + Example: + POST /_synapse/admin/v1/users/@test:example.com/override_ratelimit + { + "messages_per_second": 0, + "burst_count": 0 + } + 200 OK + { + "messages_per_second": 0, + "burst_count": 0 + } + """ + + PATTERNS = admin_patterns("/users/(?P[^/]*)/override_ratelimit") + + def __init__(self, hs: "HomeServer"): + self.hs = hs + self.store = hs.get_datastore() + self.auth = hs.get_auth() + + async def on_GET( + self, request: SynapseRequest, user_id: str + ) -> Tuple[int, JsonDict]: + await assert_requester_is_admin(self.auth, request) + + if not self.hs.is_mine_id(user_id): + raise SynapseError(400, "Can only lookup local users") + + if not await self.store.get_user_by_id(user_id): + raise NotFoundError("User not found") + + ratelimit = await self.store.get_ratelimit_for_user(user_id) + + if ratelimit: + # convert `null` to `0` for consistency + # both values do the same in retelimit handler + ret = { + "messages_per_second": 0 + if ratelimit.messages_per_second is None + else ratelimit.messages_per_second, + "burst_count": 0 + if ratelimit.burst_count is None + else ratelimit.burst_count, + } + else: + ret = {} + + return 200, ret + + async def on_POST( + self, request: SynapseRequest, user_id: str + ) -> Tuple[int, JsonDict]: + await assert_requester_is_admin(self.auth, request) + + if not self.hs.is_mine_id(user_id): + raise SynapseError(400, "Only local users can be ratelimited") + + if not await self.store.get_user_by_id(user_id): + raise NotFoundError("User not found") + + body = parse_json_object_from_request(request, allow_empty_body=True) + + messages_per_second = body.get("messages_per_second", 0) + burst_count = body.get("burst_count", 0) + + if not isinstance(messages_per_second, int) or messages_per_second < 0: + raise SynapseError( + 400, + "%r parameter must be a positive int" % (messages_per_second,), + errcode=Codes.INVALID_PARAM, + ) + + if not isinstance(burst_count, int) or burst_count < 0: + raise SynapseError( + 400, + "%r parameter must be a positive int" % (burst_count,), + errcode=Codes.INVALID_PARAM, + ) + + await self.store.set_ratelimit_for_user( + user_id, messages_per_second, burst_count + ) + ratelimit = await self.store.get_ratelimit_for_user(user_id) + assert ratelimit is not None + + ret = { + "messages_per_second": ratelimit.messages_per_second, + "burst_count": ratelimit.burst_count, + } + + return 200, ret + + async def on_DELETE( + self, request: SynapseRequest, user_id: str + ) -> Tuple[int, JsonDict]: + await assert_requester_is_admin(self.auth, request) + + if not self.hs.is_mine_id(user_id): + raise SynapseError(400, "Only local users can be ratelimited") + + if not await self.store.get_user_by_id(user_id): + raise NotFoundError("User not found") + + await self.store.delete_ratelimit_for_user(user_id) + + return 200, {} diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 9cbcd53026..47fb12f3f6 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -521,13 +521,11 @@ def _get_rooms_paginate_txn(txn): ) @cached(max_entries=10000) - async def get_ratelimit_for_user(self, user_id): - """Check if there are any overrides for ratelimiting for the given - user + async def get_ratelimit_for_user(self, user_id: str) -> Optional[RatelimitOverride]: + """Check if there are any overrides for ratelimiting for the given user Args: - user_id (str) - + user_id: user ID of the user Returns: RatelimitOverride if there is an override, else None. If the contents of RatelimitOverride are None or 0 then ratelimitng has been @@ -549,6 +547,62 @@ async def get_ratelimit_for_user(self, user_id): else: return None + async def set_ratelimit_for_user( + self, user_id: str, messages_per_second: int, burst_count: int + ) -> None: + """Sets whether a user is set an overridden ratelimit. + Args: + user_id: user ID of the user + messages_per_second: The number of actions that can be performed in a second. + burst_count: How many actions that can be performed before being limited. + """ + + def set_ratelimit_txn(txn): + self.db_pool.simple_upsert_txn( + txn, + table="ratelimit_override", + keyvalues={"user_id": user_id}, + values={ + "messages_per_second": messages_per_second, + "burst_count": burst_count, + }, + ) + + self._invalidate_cache_and_stream( + txn, self.get_ratelimit_for_user, (user_id,) + ) + + await self.db_pool.runInteraction("set_ratelimit", set_ratelimit_txn) + + async def delete_ratelimit_for_user(self, user_id: str) -> None: + """Delete an overridden ratelimit for a user. + Args: + user_id: user ID of the user + """ + + def delete_ratelimit_txn(txn): + row = self.db_pool.simple_select_one_txn( + txn, + table="ratelimit_override", + keyvalues={"user_id": user_id}, + retcols=["user_id"], + allow_none=True, + ) + + if not row: + return + + # They are there, delete them. + self.db_pool.simple_delete_one_txn( + txn, "ratelimit_override", keyvalues={"user_id": user_id} + ) + + self._invalidate_cache_and_stream( + txn, self.get_ratelimit_for_user, (user_id,) + ) + + await self.db_pool.runInteraction("delete_ratelimit", delete_ratelimit_txn) + @cached() async def get_retention_policy_for_room(self, room_id): """Get the retention policy for a given room. diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index e47fd3ded8..5070c96984 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -3011,3 +3011,287 @@ def test_success(self): # Ensure the user is shadow-banned (and the cache was cleared). result = self.get_success(self.store.get_user_by_access_token(other_user_token)) self.assertTrue(result.shadow_banned) + + +class RateLimitTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + self.other_user = self.register_user("user", "pass") + self.url = ( + "/_synapse/admin/v1/users/%s/override_ratelimit" + % urllib.parse.quote(self.other_user) + ) + + def test_no_auth(self): + """ + Try to get information of a user without authentication. + """ + channel = self.make_request("GET", self.url, b"{}") + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) + + channel = self.make_request("POST", self.url, b"{}") + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) + + channel = self.make_request("DELETE", self.url, b"{}") + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) + + def test_requester_is_no_admin(self): + """ + If the user is not a server admin, an error is returned. + """ + other_user_token = self.login("user", "pass") + + channel = self.make_request( + "GET", + self.url, + access_token=other_user_token, + ) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + channel = self.make_request( + "POST", + self.url, + access_token=other_user_token, + ) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + channel = self.make_request( + "DELETE", + self.url, + access_token=other_user_token, + ) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_user_does_not_exist(self): + """ + Tests that a lookup for a user that does not exist returns a 404 + """ + url = "/_synapse/admin/v1/users/@unknown_person:test/override_ratelimit" + + channel = self.make_request( + "GET", + url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(404, channel.code, msg=channel.json_body) + self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) + + channel = self.make_request( + "POST", + url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(404, channel.code, msg=channel.json_body) + self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) + + channel = self.make_request( + "DELETE", + url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(404, channel.code, msg=channel.json_body) + self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) + + def test_user_is_not_local(self): + """ + Tests that a lookup for a user that is not a local returns a 400 + """ + url = ( + "/_synapse/admin/v1/users/@unknown_person:unknown_domain/override_ratelimit" + ) + + channel = self.make_request( + "GET", + url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual("Can only lookup local users", channel.json_body["error"]) + + channel = self.make_request( + "POST", + url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual( + "Only local users can be ratelimited", channel.json_body["error"] + ) + + channel = self.make_request( + "DELETE", + url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual( + "Only local users can be ratelimited", channel.json_body["error"] + ) + + def test_invalid_parameter(self): + """ + If parameters are invalid, an error is returned. + """ + # messages_per_second is a string + channel = self.make_request( + "POST", + self.url, + access_token=self.admin_user_tok, + content={"messages_per_second": "string"}, + ) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) + + # messages_per_second is negative + channel = self.make_request( + "POST", + self.url, + access_token=self.admin_user_tok, + content={"messages_per_second": -1}, + ) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) + + # burst_count is a string + channel = self.make_request( + "POST", + self.url, + access_token=self.admin_user_tok, + content={"burst_count": "string"}, + ) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) + + # burst_count is negative + channel = self.make_request( + "POST", + self.url, + access_token=self.admin_user_tok, + content={"burst_count": -1}, + ) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) + + def test_return_zero_when_null(self): + """ + If values in database are `null` API should return an int `0` + """ + + self.get_success( + self.store.db_pool.simple_upsert( + table="ratelimit_override", + keyvalues={"user_id": self.other_user}, + values={ + "messages_per_second": None, + "burst_count": None, + }, + ) + ) + + # request status + channel = self.make_request( + "GET", + self.url, + access_token=self.admin_user_tok, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(0, channel.json_body["messages_per_second"]) + self.assertEqual(0, channel.json_body["burst_count"]) + + def test_success(self): + """ + Rate-limiting (set/update/delete) should succeed for an admin. + """ + # request status + channel = self.make_request( + "GET", + self.url, + access_token=self.admin_user_tok, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertNotIn("messages_per_second", channel.json_body) + self.assertNotIn("burst_count", channel.json_body) + + # set ratelimit + channel = self.make_request( + "POST", + self.url, + access_token=self.admin_user_tok, + content={"messages_per_second": 10, "burst_count": 11}, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(10, channel.json_body["messages_per_second"]) + self.assertEqual(11, channel.json_body["burst_count"]) + + # update ratelimit + channel = self.make_request( + "POST", + self.url, + access_token=self.admin_user_tok, + content={"messages_per_second": 20, "burst_count": 21}, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(20, channel.json_body["messages_per_second"]) + self.assertEqual(21, channel.json_body["burst_count"]) + + # request status + channel = self.make_request( + "GET", + self.url, + access_token=self.admin_user_tok, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(20, channel.json_body["messages_per_second"]) + self.assertEqual(21, channel.json_body["burst_count"]) + + # delete ratelimit + channel = self.make_request( + "DELETE", + self.url, + access_token=self.admin_user_tok, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertNotIn("messages_per_second", channel.json_body) + self.assertNotIn("burst_count", channel.json_body) + + # request status + channel = self.make_request( + "GET", + self.url, + access_token=self.admin_user_tok, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertNotIn("messages_per_second", channel.json_body) + self.assertNotIn("burst_count", channel.json_body) From 1d5f0e3529ec5acd889037c8ebcca2820ad003d5 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Tue, 13 Apr 2021 10:41:34 +0100 Subject: [PATCH 041/619] Bump black configuration to target py36 (#9781) Signed-off-by: Dan Callahan --- changelog.d/9781.misc | 1 + pyproject.toml | 2 +- synapse/config/tls.py | 2 +- synapse/handlers/presence.py | 2 +- synapse/http/matrixfederationclient.py | 2 +- synapse/http/site.py | 2 +- synapse/storage/database.py | 8 ++++---- tests/replication/slave/storage/test_events.py | 2 +- tests/test_state.py | 2 +- tests/test_utils/event_injection.py | 6 +++--- tests/utils.py | 2 +- 11 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 changelog.d/9781.misc diff --git a/changelog.d/9781.misc b/changelog.d/9781.misc new file mode 100644 index 0000000000..d1c73fc741 --- /dev/null +++ b/changelog.d/9781.misc @@ -0,0 +1 @@ +Update Black configuration to target Python 3.6. diff --git a/pyproject.toml b/pyproject.toml index cd880d4e39..8bca1fa4ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ showcontent = true [tool.black] -target-version = ['py35'] +target-version = ['py36'] exclude = ''' ( diff --git a/synapse/config/tls.py b/synapse/config/tls.py index ad37b93c02..85b5db4c40 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -270,7 +270,7 @@ def generate_config_section( tls_certificate_path, tls_private_key_path, acme_domain, - **kwargs + **kwargs, ): """If the acme_domain is specified acme will be enabled. If the TLS paths are not specified the default will be certs in the diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index c817f2952d..0047907cd9 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -1071,7 +1071,7 @@ async def get_new_events( room_ids=None, include_offline=True, explicit_room_id=None, - **kwargs + **kwargs, ) -> Tuple[List[UserPresenceState], int]: # The process for getting presence events are: # 1. Get the rooms the user is in. diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 5f01ebd3d4..ab47dec8f2 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -272,7 +272,7 @@ async def _send_request_with_optional_trailing_slash( self, request: MatrixFederationRequest, try_trailing_slash_on_400: bool = False, - **send_request_args + **send_request_args, ) -> IResponse: """Wrapper for _send_request which can optionally retry the request upon receiving a combination of a 400 HTTP response code and a diff --git a/synapse/http/site.py b/synapse/http/site.py index c0c873ce32..32b5e19c09 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -497,7 +497,7 @@ def __init__( resource, server_version_string, *args, - **kwargs + **kwargs, ): Site.__init__(self, resource, *args, **kwargs) diff --git a/synapse/storage/database.py b/synapse/storage/database.py index fa15b0ce5b..77ef29ec71 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -488,7 +488,7 @@ def new_transaction( exception_callbacks: List[_CallbackListEntry], func: "Callable[..., R]", *args: Any, - **kwargs: Any + **kwargs: Any, ) -> R: """Start a new database transaction with the given connection. @@ -622,7 +622,7 @@ async def runInteraction( func: "Callable[..., R]", *args: Any, db_autocommit: bool = False, - **kwargs: Any + **kwargs: Any, ) -> R: """Starts a transaction on the database and runs a given function @@ -682,7 +682,7 @@ async def runWithConnection( func: "Callable[..., R]", *args: Any, db_autocommit: bool = False, - **kwargs: Any + **kwargs: Any, ) -> R: """Wraps the .runWithConnection() method on the underlying db_pool. @@ -775,7 +775,7 @@ async def execute( desc: str, decoder: Optional[Callable[[Cursor], R]], query: str, - *args: Any + *args: Any, ) -> R: """Runs a single query for a result set. diff --git a/tests/replication/slave/storage/test_events.py b/tests/replication/slave/storage/test_events.py index 333374b183..db80a0bdbd 100644 --- a/tests/replication/slave/storage/test_events.py +++ b/tests/replication/slave/storage/test_events.py @@ -340,7 +340,7 @@ def build_event( prev_state: Optional[list] = None, redacts=None, push_actions: Iterable = frozenset(), - **content + **content, ): prev_events = prev_events or [] auth_events = auth_events or [] diff --git a/tests/test_state.py b/tests/test_state.py index 83383d8872..0d626f49f6 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -38,7 +38,7 @@ def create_event( depth=2, event_id=None, prev_events: Optional[List[str]] = None, - **kwargs + **kwargs, ): global _next_event_id diff --git a/tests/test_utils/event_injection.py b/tests/test_utils/event_injection.py index c3c4a93e1f..3dfbf8f8a9 100644 --- a/tests/test_utils/event_injection.py +++ b/tests/test_utils/event_injection.py @@ -33,7 +33,7 @@ async def inject_member_event( membership: str, target: Optional[str] = None, extra_content: Optional[dict] = None, - **kwargs + **kwargs, ) -> EventBase: """Inject a membership event into a room.""" if target is None: @@ -58,7 +58,7 @@ async def inject_event( hs: synapse.server.HomeServer, room_version: Optional[str] = None, prev_event_ids: Optional[List[str]] = None, - **kwargs + **kwargs, ) -> EventBase: """Inject a generic event into a room @@ -83,7 +83,7 @@ async def create_event( hs: synapse.server.HomeServer, room_version: Optional[str] = None, prev_event_ids: Optional[List[str]] = None, - **kwargs + **kwargs, ) -> Tuple[EventBase, EventContext]: if room_version is None: room_version = await hs.get_datastore().get_room_version_id(kwargs["room_id"]) diff --git a/tests/utils.py b/tests/utils.py index 2e34fad11c..c78d3e5ba7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -190,7 +190,7 @@ def setup_test_homeserver( config=None, reactor=None, homeserver_to_use: Type[HomeServer] = TestHomeServer, - **kwargs + **kwargs, ): """ Setup a homeserver suitable for running tests against. Keyword arguments From c1dbe84c3dcd643f4acedba346046b25a117e3c3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 13 Apr 2021 11:51:10 +0100 Subject: [PATCH 042/619] Add release helper script (#9713) Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/9713.misc | 1 + scripts-dev/release.py | 244 +++++++++++++++++++++++++++++++++++++++++ setup.py | 7 ++ 3 files changed, 252 insertions(+) create mode 100644 changelog.d/9713.misc create mode 100755 scripts-dev/release.py diff --git a/changelog.d/9713.misc b/changelog.d/9713.misc new file mode 100644 index 0000000000..908e7a2459 --- /dev/null +++ b/changelog.d/9713.misc @@ -0,0 +1 @@ +Add release helper script for automating part of the Synapse release process. diff --git a/scripts-dev/release.py b/scripts-dev/release.py new file mode 100755 index 0000000000..1042fa48bc --- /dev/null +++ b/scripts-dev/release.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""An interactive script for doing a release. See `run()` below. +""" + +import subprocess +import sys +from typing import Optional + +import click +import git +from packaging import version +from redbaron import RedBaron + + +@click.command() +def run(): + """An interactive script to walk through the initial stages of creating a + release, including creating release branch, updating changelog and pushing to + GitHub. + + Requires the dev dependencies be installed, which can be done via: + + pip install -e .[dev] + + """ + + # Make sure we're in a git repo. + try: + repo = git.Repo() + except git.InvalidGitRepositoryError: + raise click.ClickException("Not in Synapse repo.") + + if repo.is_dirty(): + raise click.ClickException("Uncommitted changes exist.") + + click.secho("Updating git repo...") + repo.remote().fetch() + + # Parse the AST and load the `__version__` node so that we can edit it + # later. + with open("synapse/__init__.py") as f: + red = RedBaron(f.read()) + + version_node = None + for node in red: + if node.type != "assignment": + continue + + if node.target.type != "name": + continue + + if node.target.value != "__version__": + continue + + version_node = node + break + + if not version_node: + print("Failed to find '__version__' definition in synapse/__init__.py") + sys.exit(1) + + # Parse the current version. + current_version = version.parse(version_node.value.value.strip('"')) + assert isinstance(current_version, version.Version) + + # Figure out what sort of release we're doing and calcuate the new version. + rc = click.confirm("RC", default=True) + if current_version.pre: + # If the current version is an RC we don't need to bump any of the + # version numbers (other than the RC number). + base_version = "{}.{}.{}".format( + current_version.major, + current_version.minor, + current_version.micro, + ) + + if rc: + new_version = "{}.{}.{}rc{}".format( + current_version.major, + current_version.minor, + current_version.micro, + current_version.pre[1] + 1, + ) + else: + new_version = base_version + else: + # If this is a new release cycle then we need to know if its a major + # version bump or a hotfix. + release_type = click.prompt( + "Release type", + type=click.Choice(("major", "hotfix")), + show_choices=True, + default="major", + ) + + if release_type == "major": + base_version = new_version = "{}.{}.{}".format( + current_version.major, + current_version.minor + 1, + 0, + ) + if rc: + new_version = "{}.{}.{}rc1".format( + current_version.major, + current_version.minor + 1, + 0, + ) + + else: + base_version = new_version = "{}.{}.{}".format( + current_version.major, + current_version.minor, + current_version.micro + 1, + ) + if rc: + new_version = "{}.{}.{}rc1".format( + current_version.major, + current_version.minor, + current_version.micro + 1, + ) + + # Confirm the calculated version is OK. + if not click.confirm(f"Create new version: {new_version}?", default=True): + click.get_current_context().abort() + + # Switch to the release branch. + release_branch_name = f"release-v{base_version}" + release_branch = find_ref(repo, release_branch_name) + if release_branch: + if release_branch.is_remote(): + # If the release branch only exists on the remote we check it out + # locally. + repo.git.checkout(release_branch_name) + release_branch = repo.active_branch + else: + # If a branch doesn't exist we create one. We ask which one branch it + # should be based off, defaulting to sensible values depending on the + # release type. + if current_version.is_prerelease: + default = release_branch_name + elif release_type == "major": + default = "develop" + else: + default = "master" + + branch_name = click.prompt( + "Which branch should the release be based on?", default=default + ) + + base_branch = find_ref(repo, branch_name) + if not base_branch: + print(f"Could not find base branch {branch_name}!") + click.get_current_context().abort() + + # Check out the base branch and ensure it's up to date + repo.head.reference = base_branch + repo.head.reset(index=True, working_tree=True) + if not base_branch.is_remote(): + update_branch(repo) + + # Create the new release branch + release_branch = repo.create_head(release_branch_name, commit=base_branch) + + # Switch to the release branch and ensure its up to date. + repo.git.checkout(release_branch_name) + update_branch(repo) + + # Update the `__version__` variable and write it back to the file. + version_node.value = '"' + new_version + '"' + with open("synapse/__init__.py", "w") as f: + f.write(red.dumps()) + + # Generate changelogs + subprocess.run("python3 -m towncrier", shell=True) + + # Generate debian changelogs if its not an RC. + if not rc: + subprocess.run( + f'dch -M -v {new_version} "New synapse release {new_version}."', shell=True + ) + subprocess.run('dch -M -r -D stable ""', shell=True) + + # Show the user the changes and ask if they want to edit the change log. + repo.git.add("-u") + subprocess.run("git diff --cached", shell=True) + + if click.confirm("Edit changelog?", default=False): + click.edit(filename="CHANGES.md") + + # Commit the changes. + repo.git.add("-u") + repo.git.commit(f"-m {new_version}") + + # We give the option to bail here in case the user wants to make sure things + # are OK before pushing. + if not click.confirm("Push branch to github?", default=True): + print("") + print("Run when ready to push:") + print("") + print(f"\tgit push -u {repo.remote().name} {repo.active_branch.name}") + print("") + sys.exit(0) + + # Otherwise, push and open the changelog in the browser. + repo.git.push("-u", repo.remote().name, repo.active_branch.name) + + click.launch( + f"https://github.com/matrix-org/synapse/blob/{repo.active_branch.name}/CHANGES.md" + ) + + +def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]: + """Find the branch/ref, looking first locally then in the remote.""" + if ref_name in repo.refs: + return repo.refs[ref_name] + elif ref_name in repo.remote().refs: + return repo.remote().refs[ref_name] + else: + return None + + +def update_branch(repo: git.Repo): + """Ensure branch is up to date if it has a remote""" + if repo.active_branch.tracking_branch(): + repo.git.merge(repo.active_branch.tracking_branch().name) + + +if __name__ == "__main__": + run() diff --git a/setup.py b/setup.py index 4530df348a..e2e488761d 100755 --- a/setup.py +++ b/setup.py @@ -103,6 +103,13 @@ def exec_file(path_segments): "flake8", ] +CONDITIONAL_REQUIREMENTS["dev"] = CONDITIONAL_REQUIREMENTS["lint"] + [ + # The following are used by the release script + "click==7.1.2", + "redbaron==0.9.2", + "GitPython==3.1.14", +] + CONDITIONAL_REQUIREMENTS["mypy"] = ["mypy==0.812", "mypy-zope==0.2.13"] # Dependencies which are exclusively required by unit test code. This is From 3efd98aa1cb0ff8106cebb0d76fd5761c25d5250 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 13 Apr 2021 14:23:43 +0100 Subject: [PATCH 043/619] 1.32.0rc1 --- CHANGES.md | 72 ++++++++++++++++++++++++++++++++++++++++ changelog.d/8926.bugfix | 1 - changelog.d/9401.removal | 1 - changelog.d/9491.feature | 1 - changelog.d/9548.removal | 1 - changelog.d/9648.feature | 1 - changelog.d/9654.feature | 1 - changelog.d/9661.misc | 1 - changelog.d/9682.misc | 1 - changelog.d/9685.misc | 1 - changelog.d/9686.misc | 1 - changelog.d/9691.feature | 1 - changelog.d/9700.feature | 1 - changelog.d/9710.feature | 1 - changelog.d/9711.bugfix | 1 - changelog.d/9713.misc | 1 - changelog.d/9717.feature | 1 - changelog.d/9718.removal | 1 - changelog.d/9719.doc | 1 - changelog.d/9725.bugfix | 1 - changelog.d/9730.misc | 1 - changelog.d/9735.feature | 1 - changelog.d/9736.misc | 1 - changelog.d/9742.misc | 1 - changelog.d/9743.misc | 1 - changelog.d/9753.misc | 1 - changelog.d/9765.docker | 1 - changelog.d/9766.feature | 1 - changelog.d/9769.misc | 1 - changelog.d/9770.bugfix | 1 - changelog.d/9771.misc | 1 - changelog.d/9772.misc | 1 - changelog.d/9780.bugfix | 1 - changelog.d/9781.misc | 1 - changelog.d/9782.misc | 1 - changelog.d/9793.misc | 1 - synapse/__init__.py | 2 +- 37 files changed, 73 insertions(+), 36 deletions(-) delete mode 100644 changelog.d/8926.bugfix delete mode 100644 changelog.d/9401.removal delete mode 100644 changelog.d/9491.feature delete mode 100644 changelog.d/9548.removal delete mode 100644 changelog.d/9648.feature delete mode 100644 changelog.d/9654.feature delete mode 100644 changelog.d/9661.misc delete mode 100644 changelog.d/9682.misc delete mode 100644 changelog.d/9685.misc delete mode 100644 changelog.d/9686.misc delete mode 100644 changelog.d/9691.feature delete mode 100644 changelog.d/9700.feature delete mode 100644 changelog.d/9710.feature delete mode 100644 changelog.d/9711.bugfix delete mode 100644 changelog.d/9713.misc delete mode 100644 changelog.d/9717.feature delete mode 100644 changelog.d/9718.removal delete mode 100644 changelog.d/9719.doc delete mode 100644 changelog.d/9725.bugfix delete mode 100644 changelog.d/9730.misc delete mode 100644 changelog.d/9735.feature delete mode 100644 changelog.d/9736.misc delete mode 100644 changelog.d/9742.misc delete mode 100644 changelog.d/9743.misc delete mode 100644 changelog.d/9753.misc delete mode 100644 changelog.d/9765.docker delete mode 100644 changelog.d/9766.feature delete mode 100644 changelog.d/9769.misc delete mode 100644 changelog.d/9770.bugfix delete mode 100644 changelog.d/9771.misc delete mode 100644 changelog.d/9772.misc delete mode 100644 changelog.d/9780.bugfix delete mode 100644 changelog.d/9781.misc delete mode 100644 changelog.d/9782.misc delete mode 100644 changelog.d/9793.misc diff --git a/CHANGES.md b/CHANGES.md index 27483532d0..6e7cc5374a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,75 @@ +Synapse 1.32.0rc1 (2021-04-13) +============================== + +**Note:** This release requires Python 3.6+ and Postgres 9.6+ or SQLite 3.22+. + +This release removes the deprecated `GET /_synapse/admin/v1/users/` admin API. Please use the [v2 API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/user_admin_api.rst#query-user-account) instead, which has improved capabilities. + +This release requires Application Services to use type `m.login.application_services` when registering users via the `/_matrix/client/r0/register` endpoint to comply with the spec. Please ensure your Application Services are up to date. + +Features +-------- + +- Add a Synapse module for routing presence updates between users. ([\#9491](https://github.com/matrix-org/synapse/issues/9491)) +- Add an admin API to manage ratelimit for a specific user. ([\#9648](https://github.com/matrix-org/synapse/issues/9648)) +- Include request information in structured logging output. ([\#9654](https://github.com/matrix-org/synapse/issues/9654)) +- Add `order_by` to the admin API `GET /_synapse/admin/v2/users`. Contributed by @dklimpel. ([\#9691](https://github.com/matrix-org/synapse/issues/9691)) +- Replace the `room_invite_state_types` configuration setting with `room_prejoin_state`. ([\#9700](https://github.com/matrix-org/synapse/issues/9700)) +- Add experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. ([\#9717](https://github.com/matrix-org/synapse/issues/9717), [\#9735](https://github.com/matrix-org/synapse/issues/9735)) +- Update experimental support for Spaces: include `m.room.create` in the room state sent with room-invites. ([\#9710](https://github.com/matrix-org/synapse/issues/9710)) +- Synapse now requires Python 3.6 or later. It also requires Postgres 9.6 or later or SQLite 3.22 or later. ([\#9766](https://github.com/matrix-org/synapse/issues/9766)) + + +Bugfixes +-------- + +- Prevent `synapse_forward_extremities` and `synapse_excess_extremity_events` Prometheus metrics from initially reporting zero-values after startup. ([\#8926](https://github.com/matrix-org/synapse/issues/8926)) +- Fix recently added ratelimits to correctly honour the application service `rate_limited` flag. ([\#9711](https://github.com/matrix-org/synapse/issues/9711)) +- Fix longstanding bug which caused `duplicate key value violates unique constraint "remote_media_cache_thumbnails_media_origin_media_id_thumbna_key"` errors. ([\#9725](https://github.com/matrix-org/synapse/issues/9725)) +- Fix bug where sharded federation senders could get stuck repeatedly querying the DB in a loop, using lots of CPU. ([\#9770](https://github.com/matrix-org/synapse/issues/9770)) +- Fix duplicate logging of exceptions thrown during federation transaction processing. ([\#9780](https://github.com/matrix-org/synapse/issues/9780)) + + +Updates to the Docker image +--------------------------- + +- Move opencontainers labels to the final Docker image such that users can inspect them. ([\#9765](https://github.com/matrix-org/synapse/issues/9765)) + + +Improved Documentation +---------------------- + +- Make the `allowed_local_3pids` regex example in the sample config stricter. ([\#9719](https://github.com/matrix-org/synapse/issues/9719)) + + +Deprecations and Removals +------------------------- + +- Remove old admin API `GET /_synapse/admin/v1/users/`. ([\#9401](https://github.com/matrix-org/synapse/issues/9401)) +- Make `/_matrix/client/r0/register` expect a type of `m.login.application_service` when an Application Service registers a user, to align with [the relevant spec](https://spec.matrix.org/unstable/application-service-api/#server-admin-style-permissions). ([\#9548](https://github.com/matrix-org/synapse/issues/9548)) +- Replace deprecated `imp` module with successor `importlib`. Contributed by Cristina Muñoz. ([\#9718](https://github.com/matrix-org/synapse/issues/9718)) + + +Internal Changes +---------------- + +- Experiment with GitHub Actions for CI. ([\#9661](https://github.com/matrix-org/synapse/issues/9661)) +- Introduce flake8-bugbear to the test suite and fix some of its lint violations. ([\#9682](https://github.com/matrix-org/synapse/issues/9682)) +- Update `scripts-dev/complement.sh` to use a local checkout of Complement, allow running a subset of tests and have it use Synapse's Complement test blacklist. ([\#9685](https://github.com/matrix-org/synapse/issues/9685)) +- Improve Jaeger tracing for `to_device` messages. ([\#9686](https://github.com/matrix-org/synapse/issues/9686)) +- Add release helper script for automating part of the Synapse release process. ([\#9713](https://github.com/matrix-org/synapse/issues/9713)) +- Add type hints to expiring cache. ([\#9730](https://github.com/matrix-org/synapse/issues/9730)) +- Convert various testcases to `HomeserverTestCase`. ([\#9736](https://github.com/matrix-org/synapse/issues/9736)) +- Start linting mypy with `no_implicit_optional`. ([\#9742](https://github.com/matrix-org/synapse/issues/9742)) +- Add missing type hints to federation handler and server. ([\#9743](https://github.com/matrix-org/synapse/issues/9743)) +- Check that a `ConfigError` is raised, rather than simply `Exception`, when appropriate in homeserver config file generation tests. ([\#9753](https://github.com/matrix-org/synapse/issues/9753)) +- Fix incompatibility with `tox` 2.5. ([\#9769](https://github.com/matrix-org/synapse/issues/9769)) +- Enable Complement tests for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary API. ([\#9771](https://github.com/matrix-org/synapse/issues/9771)) +- Use mock from the standard library instead of a separate package. ([\#9772](https://github.com/matrix-org/synapse/issues/9772)) +- Update Black configuration to target Python 3.6. ([\#9781](https://github.com/matrix-org/synapse/issues/9781)) +- Add option to skip unit tests when building Debian packages. ([\#9793](https://github.com/matrix-org/synapse/issues/9793)) + + Synapse 1.31.0 (2021-04-06) =========================== diff --git a/changelog.d/8926.bugfix b/changelog.d/8926.bugfix deleted file mode 100644 index aad7bd83ce..0000000000 --- a/changelog.d/8926.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent `synapse_forward_extremities` and `synapse_excess_extremity_events` Prometheus metrics from initially reporting zero-values after startup. diff --git a/changelog.d/9401.removal b/changelog.d/9401.removal deleted file mode 100644 index 9c813e0215..0000000000 --- a/changelog.d/9401.removal +++ /dev/null @@ -1 +0,0 @@ -Remove old admin API `GET /_synapse/admin/v1/users/`. \ No newline at end of file diff --git a/changelog.d/9491.feature b/changelog.d/9491.feature deleted file mode 100644 index 8b56a95a44..0000000000 --- a/changelog.d/9491.feature +++ /dev/null @@ -1 +0,0 @@ -Add a Synapse module for routing presence updates between users. diff --git a/changelog.d/9548.removal b/changelog.d/9548.removal deleted file mode 100644 index 1fb88236c6..0000000000 --- a/changelog.d/9548.removal +++ /dev/null @@ -1 +0,0 @@ -Make `/_matrix/client/r0/register` expect a type of `m.login.application_service` when an Application Service registers a user, to align with [the relevant spec](https://spec.matrix.org/unstable/application-service-api/#server-admin-style-permissions). diff --git a/changelog.d/9648.feature b/changelog.d/9648.feature deleted file mode 100644 index bc77026039..0000000000 --- a/changelog.d/9648.feature +++ /dev/null @@ -1 +0,0 @@ -Add an admin API to manage ratelimit for a specific user. \ No newline at end of file diff --git a/changelog.d/9654.feature b/changelog.d/9654.feature deleted file mode 100644 index a54c96cf19..0000000000 --- a/changelog.d/9654.feature +++ /dev/null @@ -1 +0,0 @@ -Include request information in structured logging output. diff --git a/changelog.d/9661.misc b/changelog.d/9661.misc deleted file mode 100644 index b5beb4626c..0000000000 --- a/changelog.d/9661.misc +++ /dev/null @@ -1 +0,0 @@ -Experiment with GitHub Actions for CI. diff --git a/changelog.d/9682.misc b/changelog.d/9682.misc deleted file mode 100644 index 428a466fac..0000000000 --- a/changelog.d/9682.misc +++ /dev/null @@ -1 +0,0 @@ -Introduce flake8-bugbear to the test suite and fix some of its lint violations. diff --git a/changelog.d/9685.misc b/changelog.d/9685.misc deleted file mode 100644 index 0506d8af0c..0000000000 --- a/changelog.d/9685.misc +++ /dev/null @@ -1 +0,0 @@ -Update `scripts-dev/complement.sh` to use a local checkout of Complement, allow running a subset of tests and have it use Synapse's Complement test blacklist. \ No newline at end of file diff --git a/changelog.d/9686.misc b/changelog.d/9686.misc deleted file mode 100644 index bb2335acf9..0000000000 --- a/changelog.d/9686.misc +++ /dev/null @@ -1 +0,0 @@ -Improve Jaeger tracing for `to_device` messages. diff --git a/changelog.d/9691.feature b/changelog.d/9691.feature deleted file mode 100644 index 3c711db4f5..0000000000 --- a/changelog.d/9691.feature +++ /dev/null @@ -1 +0,0 @@ -Add `order_by` to the admin API `GET /_synapse/admin/v2/users`. Contributed by @dklimpel. \ No newline at end of file diff --git a/changelog.d/9700.feature b/changelog.d/9700.feature deleted file mode 100644 index 037de8367f..0000000000 --- a/changelog.d/9700.feature +++ /dev/null @@ -1 +0,0 @@ -Replace the `room_invite_state_types` configuration setting with `room_prejoin_state`. diff --git a/changelog.d/9710.feature b/changelog.d/9710.feature deleted file mode 100644 index fce308cc41..0000000000 --- a/changelog.d/9710.feature +++ /dev/null @@ -1 +0,0 @@ -Experimental Spaces support: include `m.room.create` in the room state sent with room-invites. diff --git a/changelog.d/9711.bugfix b/changelog.d/9711.bugfix deleted file mode 100644 index 4ca3438d46..0000000000 --- a/changelog.d/9711.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix recently added ratelimits to correctly honour the application service `rate_limited` flag. diff --git a/changelog.d/9713.misc b/changelog.d/9713.misc deleted file mode 100644 index 908e7a2459..0000000000 --- a/changelog.d/9713.misc +++ /dev/null @@ -1 +0,0 @@ -Add release helper script for automating part of the Synapse release process. diff --git a/changelog.d/9717.feature b/changelog.d/9717.feature deleted file mode 100644 index c2c74f13d5..0000000000 --- a/changelog.d/9717.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/changelog.d/9718.removal b/changelog.d/9718.removal deleted file mode 100644 index 6de7814217..0000000000 --- a/changelog.d/9718.removal +++ /dev/null @@ -1 +0,0 @@ -Replace deprecated `imp` module with successor `importlib`. Contributed by Cristina Muñoz. diff --git a/changelog.d/9719.doc b/changelog.d/9719.doc deleted file mode 100644 index f018606dd6..0000000000 --- a/changelog.d/9719.doc +++ /dev/null @@ -1 +0,0 @@ -Make the allowed_local_3pids regex example in the sample config stricter. diff --git a/changelog.d/9725.bugfix b/changelog.d/9725.bugfix deleted file mode 100644 index 71283685c8..0000000000 --- a/changelog.d/9725.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix longstanding bug which caused `duplicate key value violates unique constraint "remote_media_cache_thumbnails_media_origin_media_id_thumbna_key"` errors. diff --git a/changelog.d/9730.misc b/changelog.d/9730.misc deleted file mode 100644 index 8063059b0b..0000000000 --- a/changelog.d/9730.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to expiring cache. diff --git a/changelog.d/9735.feature b/changelog.d/9735.feature deleted file mode 100644 index c2c74f13d5..0000000000 --- a/changelog.d/9735.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/changelog.d/9736.misc b/changelog.d/9736.misc deleted file mode 100644 index 1e445e4344..0000000000 --- a/changelog.d/9736.misc +++ /dev/null @@ -1 +0,0 @@ -Convert various testcases to `HomeserverTestCase`. diff --git a/changelog.d/9742.misc b/changelog.d/9742.misc deleted file mode 100644 index 681ab04df8..0000000000 --- a/changelog.d/9742.misc +++ /dev/null @@ -1 +0,0 @@ -Start linting mypy with `no_implicit_optional`. \ No newline at end of file diff --git a/changelog.d/9743.misc b/changelog.d/9743.misc deleted file mode 100644 index c2f75c1df9..0000000000 --- a/changelog.d/9743.misc +++ /dev/null @@ -1 +0,0 @@ -Add missing type hints to federation handler and server. diff --git a/changelog.d/9753.misc b/changelog.d/9753.misc deleted file mode 100644 index 31184fe0bd..0000000000 --- a/changelog.d/9753.misc +++ /dev/null @@ -1 +0,0 @@ -Check that a `ConfigError` is raised, rather than simply `Exception`, when appropriate in homeserver config file generation tests. \ No newline at end of file diff --git a/changelog.d/9765.docker b/changelog.d/9765.docker deleted file mode 100644 index f170a36714..0000000000 --- a/changelog.d/9765.docker +++ /dev/null @@ -1 +0,0 @@ -Move opencontainers labels to the final Docker image such that users can inspect them. diff --git a/changelog.d/9766.feature b/changelog.d/9766.feature deleted file mode 100644 index ecf49cfee1..0000000000 --- a/changelog.d/9766.feature +++ /dev/null @@ -1 +0,0 @@ -Synapse now requires Python 3.6 or later. It also requires Postgres 9.6 or later or SQLite 3.22 or later. diff --git a/changelog.d/9769.misc b/changelog.d/9769.misc deleted file mode 100644 index 042a50615f..0000000000 --- a/changelog.d/9769.misc +++ /dev/null @@ -1 +0,0 @@ -Fix incompatibility with `tox` 2.5. diff --git a/changelog.d/9770.bugfix b/changelog.d/9770.bugfix deleted file mode 100644 index baf93138de..0000000000 --- a/changelog.d/9770.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where sharded federation senders could get stuck repeatedly querying the DB in a loop, using lots of CPU. diff --git a/changelog.d/9771.misc b/changelog.d/9771.misc deleted file mode 100644 index 42d651d4cc..0000000000 --- a/changelog.d/9771.misc +++ /dev/null @@ -1 +0,0 @@ -Enable Complement tests for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary API. diff --git a/changelog.d/9772.misc b/changelog.d/9772.misc deleted file mode 100644 index ec7d94cc25..0000000000 --- a/changelog.d/9772.misc +++ /dev/null @@ -1 +0,0 @@ -Use mock from the standard library instead of a separate package. diff --git a/changelog.d/9780.bugfix b/changelog.d/9780.bugfix deleted file mode 100644 index 70985a050f..0000000000 --- a/changelog.d/9780.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix duplicate logging of exceptions thrown during federation transaction processing. diff --git a/changelog.d/9781.misc b/changelog.d/9781.misc deleted file mode 100644 index d1c73fc741..0000000000 --- a/changelog.d/9781.misc +++ /dev/null @@ -1 +0,0 @@ -Update Black configuration to target Python 3.6. diff --git a/changelog.d/9782.misc b/changelog.d/9782.misc deleted file mode 100644 index ecf49cfee1..0000000000 --- a/changelog.d/9782.misc +++ /dev/null @@ -1 +0,0 @@ -Synapse now requires Python 3.6 or later. It also requires Postgres 9.6 or later or SQLite 3.22 or later. diff --git a/changelog.d/9793.misc b/changelog.d/9793.misc deleted file mode 100644 index 6334689d26..0000000000 --- a/changelog.d/9793.misc +++ /dev/null @@ -1 +0,0 @@ -Add option to skip unit tests when building Debian packages. diff --git a/synapse/__init__.py b/synapse/__init__.py index 1d2883acf6..125a73d378 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -48,7 +48,7 @@ except ImportError: pass -__version__ = "1.31.0" +__version__ = "1.32.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From d9bd181a3fd1431af784c525e7fe552e0bcad48e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 13 Apr 2021 14:39:06 +0100 Subject: [PATCH 044/619] Update changelog for v1.32.0 --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6e7cc5374a..41908f84be 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,12 +47,12 @@ Deprecations and Removals - Remove old admin API `GET /_synapse/admin/v1/users/`. ([\#9401](https://github.com/matrix-org/synapse/issues/9401)) - Make `/_matrix/client/r0/register` expect a type of `m.login.application_service` when an Application Service registers a user, to align with [the relevant spec](https://spec.matrix.org/unstable/application-service-api/#server-admin-style-permissions). ([\#9548](https://github.com/matrix-org/synapse/issues/9548)) -- Replace deprecated `imp` module with successor `importlib`. Contributed by Cristina Muñoz. ([\#9718](https://github.com/matrix-org/synapse/issues/9718)) Internal Changes ---------------- +- Replace deprecated `imp` module with successor `importlib`. Contributed by Cristina Muñoz. ([\#9718](https://github.com/matrix-org/synapse/issues/9718)) - Experiment with GitHub Actions for CI. ([\#9661](https://github.com/matrix-org/synapse/issues/9661)) - Introduce flake8-bugbear to the test suite and fix some of its lint violations. ([\#9682](https://github.com/matrix-org/synapse/issues/9682)) - Update `scripts-dev/complement.sh` to use a local checkout of Complement, allow running a subset of tests and have it use Synapse's Complement test blacklist. ([\#9685](https://github.com/matrix-org/synapse/issues/9685)) From f16c6cf59aad485cac83ce9d6d87842ae53adedf Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 14 Apr 2021 12:06:19 +0100 Subject: [PATCH 045/619] Add note to docker docs explaining platform support (#9801) Context is in https://github.com/matrix-org/synapse/issues/9764#issuecomment-818615894. I struggled to find a more official link for this. The problem occurs when using WSL1 instead of WSL2, which some Windows platforms (at least Server 2019) still don't have. Docker have updated their documentation to paint a much happier picture now given WSL2's support. The last sentence here can probably be removed once WSL1 is no longer around... though that will likely not be for a very long time. --- changelog.d/9801.doc | 1 + docker/README.md | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9801.doc diff --git a/changelog.d/9801.doc b/changelog.d/9801.doc new file mode 100644 index 0000000000..8b8b9d01d4 --- /dev/null +++ b/changelog.d/9801.doc @@ -0,0 +1 @@ +Add a note to the docker docs mentioning that we mirror upstream's supported Docker platforms. diff --git a/docker/README.md b/docker/README.md index 3a7dc585e7..b65bcea636 100644 --- a/docker/README.md +++ b/docker/README.md @@ -2,9 +2,12 @@ This Docker image will run Synapse as a single process. By default it uses a sqlite database; for production use you should connect it to a separate -postgres database. +postgres database. The image also does *not* provide a TURN server. -The image also does *not* provide a TURN server. +This image should work on all platforms that are supported by Docker upstream. +Note that Docker's WS1-backend Linux Containers on Windows +platform is [experimental](https://github.com/docker/for-win/issues/6470) and +is not supported by this image. ## Volumes @@ -208,4 +211,4 @@ healthcheck: ## Using jemalloc Jemalloc is embedded in the image and will be used instead of the default allocator. -You can read about jemalloc by reading the Synapse [README](../README.md) \ No newline at end of file +You can read about jemalloc by reading the Synapse [README](../README.md) From 7e460ec2a566b19bbcda63bc04b1e422127a99b3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 14 Apr 2021 13:54:49 +0100 Subject: [PATCH 046/619] Add a dockerfile for running a set of Synapse worker processes (#9162) This PR adds a Dockerfile and some supporting files to the `docker/` directory. The Dockerfile's intention is to spin up a container with: * A Synapse main process. * Any desired worker processes, defined by a `SYNAPSE_WORKERS` environment variable supplied at runtime. * A redis for worker communication. * A nginx for routing traffic. * A supervisord to start all worker processes and monitor them if any go down. Note that **this is not currently intended to be used in production**. If you'd like to use Synapse workers with Docker, instead make use of the official image, with one worker per container. The purpose of this dockerfile is currently to allow testing Synapse in worker mode with the [Complement](https://github.com/matrix-org/complement/) test suite. `configure_workers_and_start.py` is where most of the magic happens in this PR. It reads from environment variables (documented in the file) and creates all necessary config files for the processes. It is the entrypoint of the Dockerfile, and thus is run any time the docker container is spun up, recreating all config files in case you want to use a different set of workers. One can specify which workers they'd like to use by setting the `SYNAPSE_WORKERS` environment variable (as a comma-separated list of arbitrary worker names) or by setting it to `*` for all worker processes. We will be using the latter in CI. Huge thanks to @MatMaul for helping get this all working :tada: This PR is paired with its equivalent on the Complement side: https://github.com/matrix-org/complement/pull/62. Note, for the purpose of testing this PR before it's merged: You'll need to (re)build the base Synapse docker image for everything to work (`matrixdotorg/synapse:latest`). Then build the worker-based docker image on top (`matrixdotorg/synapse:workers`). --- changelog.d/9162.misc | 1 + docker/Dockerfile-workers | 23 + docker/README-testing.md | 140 ++++++ docker/README.md | 12 +- docker/conf-workers/nginx.conf.j2 | 27 ++ docker/conf-workers/shared.yaml.j2 | 9 + docker/conf-workers/supervisord.conf.j2 | 41 ++ docker/conf-workers/worker.yaml.j2 | 26 ++ docker/conf/homeserver.yaml | 4 +- docker/conf/log.config | 32 +- docker/configure_workers_and_start.py | 558 ++++++++++++++++++++++++ 11 files changed, 867 insertions(+), 6 deletions(-) create mode 100644 changelog.d/9162.misc create mode 100644 docker/Dockerfile-workers create mode 100644 docker/README-testing.md create mode 100644 docker/conf-workers/nginx.conf.j2 create mode 100644 docker/conf-workers/shared.yaml.j2 create mode 100644 docker/conf-workers/supervisord.conf.j2 create mode 100644 docker/conf-workers/worker.yaml.j2 create mode 100755 docker/configure_workers_and_start.py diff --git a/changelog.d/9162.misc b/changelog.d/9162.misc new file mode 100644 index 0000000000..1083da8a7a --- /dev/null +++ b/changelog.d/9162.misc @@ -0,0 +1 @@ +Add a dockerfile for running Synapse in worker-mode under Complement. \ No newline at end of file diff --git a/docker/Dockerfile-workers b/docker/Dockerfile-workers new file mode 100644 index 0000000000..969cf97286 --- /dev/null +++ b/docker/Dockerfile-workers @@ -0,0 +1,23 @@ +# Inherit from the official Synapse docker image +FROM matrixdotorg/synapse + +# Install deps +RUN apt-get update +RUN apt-get install -y supervisor redis nginx + +# Remove the default nginx sites +RUN rm /etc/nginx/sites-enabled/default + +# Copy Synapse worker, nginx and supervisord configuration template files +COPY ./docker/conf-workers/* /conf/ + +# Expose nginx listener port +EXPOSE 8080/tcp + +# Volume for user-editable config files, logs etc. +VOLUME ["/data"] + +# A script to read environment variables and create the necessary +# files to run the desired worker configuration. Will start supervisord. +COPY ./docker/configure_workers_and_start.py /configure_workers_and_start.py +ENTRYPOINT ["/configure_workers_and_start.py"] diff --git a/docker/README-testing.md b/docker/README-testing.md new file mode 100644 index 0000000000..6a5baf9e28 --- /dev/null +++ b/docker/README-testing.md @@ -0,0 +1,140 @@ +# Running tests against a dockerised Synapse + +It's possible to run integration tests against Synapse +using [Complement](https://github.com/matrix-org/complement). Complement is a Matrix Spec +compliance test suite for homeservers, and supports any homeserver docker image configured +to listen on ports 8008/8448. This document contains instructions for building Synapse +docker images that can be run inside Complement for testing purposes. + +Note that running Synapse's unit tests from within the docker image is not supported. + +## Testing with SQLite and single-process Synapse + +> Note that `scripts-dev/complement.sh` is a script that will automatically build +> and run an SQLite-based, single-process of Synapse against Complement. + +The instructions below will set up Complement testing for a single-process, +SQLite-based Synapse deployment. + +Start by building the base Synapse docker image. If you wish to run tests with the latest +release of Synapse, instead of your current checkout, you can skip this step. From the +root of the repository: + +```sh +docker build -t matrixdotorg/synapse -f docker/Dockerfile . +``` + +This will build an image with the tag `matrixdotorg/synapse`. + +Next, build the Synapse image for Complement. You will need a local checkout +of Complement. Change to the root of your Complement checkout and run: + +```sh +docker build -t complement-synapse -f "dockerfiles/Synapse.Dockerfile" dockerfiles +``` + +This will build an image with the tag `complement-synapse`, which can be handed to +Complement for testing via the `COMPLEMENT_BASE_IMAGE` environment variable. Refer to +[Complement's documentation](https://github.com/matrix-org/complement/#running) for +how to run the tests, as well as the various available command line flags. + +## Testing with PostgreSQL and single or multi-process Synapse + +The above docker image only supports running Synapse with SQLite and in a +single-process topology. The following instructions are used to build a Synapse image for +Complement that supports either single or multi-process topology with a PostgreSQL +database backend. + +As with the single-process image, build the base Synapse docker image. If you wish to run +tests with the latest release of Synapse, instead of your current checkout, you can skip +this step. From the root of the repository: + +```sh +docker build -t matrixdotorg/synapse -f docker/Dockerfile . +``` + +This will build an image with the tag `matrixdotorg/synapse`. + +Next, we build a new image with worker support based on `matrixdotorg/synapse:latest`. +Again, from the root of the repository: + +```sh +docker build -t matrixdotorg/synapse-workers -f docker/Dockerfile-workers . +``` + +This will build an image with the tag` matrixdotorg/synapse-workers`. + +It's worth noting at this point that this image is fully functional, and +can be used for testing against locally. See instructions for using the container +under +[Running the Dockerfile-worker image standalone](#running-the-dockerfile-worker-image-standalone) +below. + +Finally, build the Synapse image for Complement, which is based on +`matrixdotorg/synapse-workers`. You will need a local checkout of Complement. Change to +the root of your Complement checkout and run: + +```sh +docker build -t matrixdotorg/complement-synapse-workers -f dockerfiles/SynapseWorkers.Dockerfile dockerfiles +``` + +This will build an image with the tag `complement-synapse`, which can be handed to +Complement for testing via the `COMPLEMENT_BASE_IMAGE` environment variable. Refer to +[Complement's documentation](https://github.com/matrix-org/complement/#running) for +how to run the tests, as well as the various available command line flags. + +## Running the Dockerfile-worker image standalone + +For manual testing of a multi-process Synapse instance in Docker, +[Dockerfile-workers](Dockerfile-workers) is a Dockerfile that will produce an image +bundling all necessary components together for a workerised homeserver instance. + +This includes any desired Synapse worker processes, a nginx to route traffic accordingly, +a redis for worker communication and a supervisord instance to start up and monitor all +processes. You will need to provide your own postgres container to connect to, and TLS +is not handled by the container. + +Once you've built the image using the above instructions, you can run it. Be sure +you've set up a volume according to the [usual Synapse docker instructions](README.md). +Then run something along the lines of: + +``` +docker run -d --name synapse \ + --mount type=volume,src=synapse-data,dst=/data \ + -p 8008:8008 \ + -e SYNAPSE_SERVER_NAME=my.matrix.host \ + -e SYNAPSE_REPORT_STATS=no \ + -e POSTGRES_HOST=postgres \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=somesecret \ + -e SYNAPSE_WORKER_TYPES=synchrotron,media_repository,user_dir \ + -e SYNAPSE_WORKERS_WRITE_LOGS_TO_DISK=1 \ + matrixdotorg/synapse-workers +``` + +...substituting `POSTGRES*` variables for those that match a postgres host you have +available (usually a running postgres docker container). + +The `SYNAPSE_WORKER_TYPES` environment variable is a comma-separated list of workers to +use when running the container. All possible worker names are defined by the keys of the +`WORKERS_CONFIG` variable in [this script](configure_workers_and_start.py), which the +Dockerfile makes use of to generate appropriate worker, nginx and supervisord config +files. + +Sharding is supported for a subset of workers, in line with the +[worker documentation](../docs/workers.md). To run multiple instances of a given worker +type, simply specify the type multiple times in `SYNAPSE_WORKER_TYPES` +(e.g `SYNAPSE_WORKER_TYPES=event_creator,event_creator...`). + +Otherwise, `SYNAPSE_WORKER_TYPES` can either be left empty or unset to spawn no workers +(leaving only the main process). The container is configured to use redis-based worker +mode. + +Logs for workers and the main process are logged to stdout and can be viewed with +standard `docker logs` tooling. Worker logs contain their worker name +after the timestamp. + +Setting `SYNAPSE_WORKERS_WRITE_LOGS_TO_DISK=1` will cause worker logs to be written to +`/logs/.log`. Logs are kept for 1 week and rotate every day at 00: +00, according to the container's clock. Logging for the main process must still be +configured by modifying the homeserver's log config in your Synapse data volume. diff --git a/docker/README.md b/docker/README.md index b65bcea636..a7d1e670fe 100644 --- a/docker/README.md +++ b/docker/README.md @@ -11,7 +11,7 @@ is not supported by this image. ## Volumes -By default, the image expects a single volume, located at ``/data``, that will hold: +By default, the image expects a single volume, located at `/data`, that will hold: * configuration files; * uploaded media and thumbnails; @@ -19,11 +19,11 @@ By default, the image expects a single volume, located at ``/data``, that will h * the appservices configuration. You are free to use separate volumes depending on storage endpoints at your -disposal. For instance, ``/data/media`` could be stored on a large but low +disposal. For instance, `/data/media` could be stored on a large but low performance hdd storage while other files could be stored on high performance endpoints. -In order to setup an application service, simply create an ``appservices`` +In order to setup an application service, simply create an `appservices` directory in the data volume and write the application service Yaml configuration file there. Multiple application services are supported. @@ -56,6 +56,8 @@ The following environment variables are supported in `generate` mode: * `SYNAPSE_SERVER_NAME` (mandatory): the server public hostname. * `SYNAPSE_REPORT_STATS` (mandatory, `yes` or `no`): whether to enable anonymous statistics reporting. +* `SYNAPSE_HTTP_PORT`: the port Synapse should listen on for http traffic. + Defaults to `8008`. * `SYNAPSE_CONFIG_DIR`: where additional config files (such as the log config and event signing key) will be stored. Defaults to `/data`. * `SYNAPSE_CONFIG_PATH`: path to the file to be generated. Defaults to @@ -76,6 +78,8 @@ docker run -d --name synapse \ matrixdotorg/synapse:latest ``` +(assuming 8008 is the port Synapse is configured to listen on for http traffic.) + You can then check that it has started correctly with: ``` @@ -211,4 +215,4 @@ healthcheck: ## Using jemalloc Jemalloc is embedded in the image and will be used instead of the default allocator. -You can read about jemalloc by reading the Synapse [README](../README.md) +You can read about jemalloc by reading the Synapse [README](../README.md). diff --git a/docker/conf-workers/nginx.conf.j2 b/docker/conf-workers/nginx.conf.j2 new file mode 100644 index 0000000000..1081979e06 --- /dev/null +++ b/docker/conf-workers/nginx.conf.j2 @@ -0,0 +1,27 @@ +# This file contains the base config for the reverse proxy, as part of ../Dockerfile-workers. +# configure_workers_and_start.py uses and amends to this file depending on the workers +# that have been selected. + +{{ upstream_directives }} + +server { + # Listen on an unoccupied port number + listen 8008; + listen [::]:8008; + + server_name localhost; + + # Nginx by default only allows file uploads up to 1M in size + # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml + client_max_body_size 100M; + +{{ worker_locations }} + + # Send all other traffic to the main process + location ~* ^(\\/_matrix|\\/_synapse) { + proxy_pass http://localhost:8080; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + } +} diff --git a/docker/conf-workers/shared.yaml.j2 b/docker/conf-workers/shared.yaml.j2 new file mode 100644 index 0000000000..f94b8c6aca --- /dev/null +++ b/docker/conf-workers/shared.yaml.j2 @@ -0,0 +1,9 @@ +# This file contains the base for the shared homeserver config file between Synapse workers, +# as part of ./Dockerfile-workers. +# configure_workers_and_start.py uses and amends to this file depending on the workers +# that have been selected. + +redis: + enabled: true + +{{ shared_worker_config }} \ No newline at end of file diff --git a/docker/conf-workers/supervisord.conf.j2 b/docker/conf-workers/supervisord.conf.j2 new file mode 100644 index 0000000000..0de2c6143b --- /dev/null +++ b/docker/conf-workers/supervisord.conf.j2 @@ -0,0 +1,41 @@ +# This file contains the base config for supervisord, as part of ../Dockerfile-workers. +# configure_workers_and_start.py uses and amends to this file depending on the workers +# that have been selected. +[supervisord] +nodaemon=true +user=root + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +priority=500 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +username=www-data +autorestart=true + +[program:redis] +command=/usr/bin/redis-server /etc/redis/redis.conf --daemonize no +priority=1 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +username=redis +autorestart=true + +[program:synapse_main] +command=/usr/local/bin/python -m synapse.app.homeserver --config-path="{{ main_config_path }}" --config-path=/conf/workers/shared.yaml +priority=10 +# Log startup failures to supervisord's stdout/err +# Regular synapse logs will still go in the configured data directory +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=unexpected +exitcodes=0 + +# Additional process blocks +{{ worker_config }} \ No newline at end of file diff --git a/docker/conf-workers/worker.yaml.j2 b/docker/conf-workers/worker.yaml.j2 new file mode 100644 index 0000000000..42131afc95 --- /dev/null +++ b/docker/conf-workers/worker.yaml.j2 @@ -0,0 +1,26 @@ +# This is a configuration template for a single worker instance, and is +# used by Dockerfile-workers. +# Values will be change depending on whichever workers are selected when +# running that image. + +worker_app: "{{ app }}" +worker_name: "{{ name }}" + +# The replication listener on the main synapse process. +worker_replication_host: 127.0.0.1 +worker_replication_http_port: 9093 + +worker_listeners: + - type: http + port: {{ port }} +{% if listener_resources %} + resources: + - names: +{%- for resource in listener_resources %} + - {{ resource }} +{%- endfor %} +{% endif %} + +worker_log_config: {{ worker_log_config_filepath }} + +{{ worker_extra_conf }} diff --git a/docker/conf/homeserver.yaml b/docker/conf/homeserver.yaml index a792899540..2b23d7f428 100644 --- a/docker/conf/homeserver.yaml +++ b/docker/conf/homeserver.yaml @@ -40,7 +40,9 @@ listeners: compress: false {% endif %} - - port: 8008 + # Allow configuring in case we want to reverse proxy 8008 + # using another process in the same container + - port: {{ SYNAPSE_HTTP_PORT or 8008 }} tls: false bind_addresses: ['::'] type: http diff --git a/docker/conf/log.config b/docker/conf/log.config index 491bbcc87a..34572bc0f3 100644 --- a/docker/conf/log.config +++ b/docker/conf/log.config @@ -2,9 +2,34 @@ version: 1 formatters: precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' +{% if worker_name %} + format: '%(asctime)s - worker:{{ worker_name }} - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' +{% else %} + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' +{% endif %} handlers: + file: + class: logging.handlers.TimedRotatingFileHandler + formatter: precise + filename: {{ LOG_FILE_PATH or "homeserver.log" }} + when: "midnight" + backupCount: 6 # Does not include the current log file. + encoding: utf8 + + # Default to buffering writes to log file for efficiency. This means that + # there will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR + # logs will still be flushed immediately. + buffer: + class: logging.handlers.MemoryHandler + target: file + # The capacity is the number of log lines that are buffered before + # being written to disk. Increasing this will lead to better + # performance, at the expensive of it taking longer for log lines to + # be written to disk. + capacity: 10 + flushLevel: 30 # Flush for WARNING logs as well + console: class: logging.StreamHandler formatter: precise @@ -17,6 +42,11 @@ loggers: root: level: {{ SYNAPSE_LOG_LEVEL or "INFO" }} + +{% if LOG_FILE_PATH %} + handlers: [console, buffer] +{% else %} handlers: [console] +{% endif %} disable_existing_loggers: false diff --git a/docker/configure_workers_and_start.py b/docker/configure_workers_and_start.py new file mode 100755 index 0000000000..4be6afc65d --- /dev/null +++ b/docker/configure_workers_and_start.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script reads environment variables and generates a shared Synapse worker, +# nginx and supervisord configs depending on the workers requested. +# +# The environment variables it reads are: +# * SYNAPSE_SERVER_NAME: The desired server_name of the homeserver. +# * SYNAPSE_REPORT_STATS: Whether to report stats. +# * SYNAPSE_WORKER_TYPES: A comma separated list of worker names as specified in WORKER_CONFIG +# below. Leave empty for no workers, or set to '*' for all possible workers. +# +# NOTE: According to Complement's ENTRYPOINT expectations for a homeserver image (as defined +# in the project's README), this script may be run multiple times, and functionality should +# continue to work if so. + +import os +import subprocess +import sys + +import jinja2 +import yaml + +MAIN_PROCESS_HTTP_LISTENER_PORT = 8080 + + +WORKERS_CONFIG = { + "pusher": { + "app": "synapse.app.pusher", + "listener_resources": [], + "endpoint_patterns": [], + "shared_extra_conf": {"start_pushers": False}, + "worker_extra_conf": "", + }, + "user_dir": { + "app": "synapse.app.user_dir", + "listener_resources": ["client"], + "endpoint_patterns": [ + "^/_matrix/client/(api/v1|r0|unstable)/user_directory/search$" + ], + "shared_extra_conf": {"update_user_directory": False}, + "worker_extra_conf": "", + }, + "media_repository": { + "app": "synapse.app.media_repository", + "listener_resources": ["media"], + "endpoint_patterns": [ + "^/_matrix/media/", + "^/_synapse/admin/v1/purge_media_cache$", + "^/_synapse/admin/v1/room/.*/media.*$", + "^/_synapse/admin/v1/user/.*/media.*$", + "^/_synapse/admin/v1/media/.*$", + "^/_synapse/admin/v1/quarantine_media/.*$", + ], + "shared_extra_conf": {"enable_media_repo": False}, + "worker_extra_conf": "enable_media_repo: true", + }, + "appservice": { + "app": "synapse.app.appservice", + "listener_resources": [], + "endpoint_patterns": [], + "shared_extra_conf": {"notify_appservices": False}, + "worker_extra_conf": "", + }, + "federation_sender": { + "app": "synapse.app.federation_sender", + "listener_resources": [], + "endpoint_patterns": [], + "shared_extra_conf": {"send_federation": False}, + "worker_extra_conf": "", + }, + "synchrotron": { + "app": "synapse.app.generic_worker", + "listener_resources": ["client"], + "endpoint_patterns": [ + "^/_matrix/client/(v2_alpha|r0)/sync$", + "^/_matrix/client/(api/v1|v2_alpha|r0)/events$", + "^/_matrix/client/(api/v1|r0)/initialSync$", + "^/_matrix/client/(api/v1|r0)/rooms/[^/]+/initialSync$", + ], + "shared_extra_conf": {}, + "worker_extra_conf": "", + }, + "federation_reader": { + "app": "synapse.app.generic_worker", + "listener_resources": ["federation"], + "endpoint_patterns": [ + "^/_matrix/federation/(v1|v2)/event/", + "^/_matrix/federation/(v1|v2)/state/", + "^/_matrix/federation/(v1|v2)/state_ids/", + "^/_matrix/federation/(v1|v2)/backfill/", + "^/_matrix/federation/(v1|v2)/get_missing_events/", + "^/_matrix/federation/(v1|v2)/publicRooms", + "^/_matrix/federation/(v1|v2)/query/", + "^/_matrix/federation/(v1|v2)/make_join/", + "^/_matrix/federation/(v1|v2)/make_leave/", + "^/_matrix/federation/(v1|v2)/send_join/", + "^/_matrix/federation/(v1|v2)/send_leave/", + "^/_matrix/federation/(v1|v2)/invite/", + "^/_matrix/federation/(v1|v2)/query_auth/", + "^/_matrix/federation/(v1|v2)/event_auth/", + "^/_matrix/federation/(v1|v2)/exchange_third_party_invite/", + "^/_matrix/federation/(v1|v2)/user/devices/", + "^/_matrix/federation/(v1|v2)/get_groups_publicised$", + "^/_matrix/key/v2/query", + ], + "shared_extra_conf": {}, + "worker_extra_conf": "", + }, + "federation_inbound": { + "app": "synapse.app.generic_worker", + "listener_resources": ["federation"], + "endpoint_patterns": ["/_matrix/federation/(v1|v2)/send/"], + "shared_extra_conf": {}, + "worker_extra_conf": "", + }, + "event_persister": { + "app": "synapse.app.generic_worker", + "listener_resources": ["replication"], + "endpoint_patterns": [], + "shared_extra_conf": {}, + "worker_extra_conf": "", + }, + "background_worker": { + "app": "synapse.app.generic_worker", + "listener_resources": [], + "endpoint_patterns": [], + # This worker cannot be sharded. Therefore there should only ever be one background + # worker, and it should be named background_worker1 + "shared_extra_conf": {"run_background_tasks_on": "background_worker1"}, + "worker_extra_conf": "", + }, + "event_creator": { + "app": "synapse.app.generic_worker", + "listener_resources": ["client"], + "endpoint_patterns": [ + "^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/redact", + "^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/send", + "^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/(join|invite|leave|ban|unban|kick)$", + "^/_matrix/client/(api/v1|r0|unstable)/join/", + "^/_matrix/client/(api/v1|r0|unstable)/profile/", + ], + "shared_extra_conf": {}, + "worker_extra_conf": "", + }, + "frontend_proxy": { + "app": "synapse.app.frontend_proxy", + "listener_resources": ["client", "replication"], + "endpoint_patterns": ["^/_matrix/client/(api/v1|r0|unstable)/keys/upload"], + "shared_extra_conf": {}, + "worker_extra_conf": ( + "worker_main_http_uri: http://127.0.0.1:%d" + % (MAIN_PROCESS_HTTP_LISTENER_PORT,), + ), + }, +} + +# Templates for sections that may be inserted multiple times in config files +SUPERVISORD_PROCESS_CONFIG_BLOCK = """ +[program:synapse_{name}] +command=/usr/local/bin/python -m {app} \ + --config-path="{config_path}" \ + --config-path=/conf/workers/shared.yaml \ + --config-path=/conf/workers/{name}.yaml +autorestart=unexpected +priority=500 +exitcodes=0 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +""" + +NGINX_LOCATION_CONFIG_BLOCK = """ + location ~* {endpoint} { + proxy_pass {upstream}; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + } +""" + +NGINX_UPSTREAM_CONFIG_BLOCK = """ +upstream {upstream_worker_type} { +{body} +} +""" + + +# Utility functions +def log(txt: str): + """Log something to the stdout. + + Args: + txt: The text to log. + """ + print(txt) + + +def error(txt: str): + """Log something and exit with an error code. + + Args: + txt: The text to log in error. + """ + log(txt) + sys.exit(2) + + +def convert(src: str, dst: str, **template_vars): + """Generate a file from a template + + Args: + src: Path to the input file. + dst: Path to write to. + template_vars: The arguments to replace placeholder variables in the template with. + """ + # Read the template file + with open(src) as infile: + template = infile.read() + + # Generate a string from the template. We disable autoescape to prevent template + # variables from being escaped. + rendered = jinja2.Template(template, autoescape=False).render(**template_vars) + + # Write the generated contents to a file + # + # We use append mode in case the files have already been written to by something else + # (for instance, as part of the instructions in a dockerfile). + with open(dst, "a") as outfile: + # In case the existing file doesn't end with a newline + outfile.write("\n") + + outfile.write(rendered) + + +def add_sharding_to_shared_config( + shared_config: dict, + worker_type: str, + worker_name: str, + worker_port: int, +) -> None: + """Given a dictionary representing a config file shared across all workers, + append sharded worker information to it for the current worker_type instance. + + Args: + shared_config: The config dict that all worker instances share (after being converted to YAML) + worker_type: The type of worker (one of those defined in WORKERS_CONFIG). + worker_name: The name of the worker instance. + worker_port: The HTTP replication port that the worker instance is listening on. + """ + # The instance_map config field marks the workers that write to various replication streams + instance_map = shared_config.setdefault("instance_map", {}) + + # Worker-type specific sharding config + if worker_type == "pusher": + shared_config.setdefault("pusher_instances", []).append(worker_name) + + elif worker_type == "federation_sender": + shared_config.setdefault("federation_sender_instances", []).append(worker_name) + + elif worker_type == "event_persister": + # Event persisters write to the events stream, so we need to update + # the list of event stream writers + shared_config.setdefault("stream_writers", {}).setdefault("events", []).append( + worker_name + ) + + # Map of stream writer instance names to host/ports combos + instance_map[worker_name] = { + "host": "localhost", + "port": worker_port, + } + + elif worker_type == "media_repository": + # The first configured media worker will run the media background jobs + shared_config.setdefault("media_instance_running_background_jobs", worker_name) + + +def generate_base_homeserver_config(): + """Starts Synapse and generates a basic homeserver config, which will later be + modified for worker support. + + Raises: CalledProcessError if calling start.py returned a non-zero exit code. + """ + # start.py already does this for us, so just call that. + # note that this script is copied in in the official, monolith dockerfile + os.environ["SYNAPSE_HTTP_PORT"] = str(MAIN_PROCESS_HTTP_LISTENER_PORT) + subprocess.check_output(["/usr/local/bin/python", "/start.py", "migrate_config"]) + + +def generate_worker_files(environ, config_path: str, data_dir: str): + """Read the desired list of workers from environment variables and generate + shared homeserver, nginx and supervisord configs. + + Args: + environ: _Environ[str] + config_path: Where to output the generated Synapse main worker config file. + data_dir: The location of the synapse data directory. Where log and + user-facing config files live. + """ + # Note that yaml cares about indentation, so care should be taken to insert lines + # into files at the correct indentation below. + + # shared_config is the contents of a Synapse config file that will be shared amongst + # the main Synapse process as well as all workers. + # It is intended mainly for disabling functionality when certain workers are spun up, + # and adding a replication listener. + + # First read the original config file and extract the listeners block. Then we'll add + # another listener for replication. Later we'll write out the result. + listeners = [ + { + "port": 9093, + "bind_address": "127.0.0.1", + "type": "http", + "resources": [{"names": ["replication"]}], + } + ] + with open(config_path) as file_stream: + original_config = yaml.safe_load(file_stream) + original_listeners = original_config.get("listeners") + if original_listeners: + listeners += original_listeners + + # The shared homeserver config. The contents of which will be inserted into the + # base shared worker jinja2 template. + # + # This config file will be passed to all workers, included Synapse's main process. + shared_config = {"listeners": listeners} + + # The supervisord config. The contents of which will be inserted into the + # base supervisord jinja2 template. + # + # Supervisord will be in charge of running everything, from redis to nginx to Synapse + # and all of its worker processes. Load the config template, which defines a few + # services that are necessary to run. + supervisord_config = "" + + # Upstreams for load-balancing purposes. This dict takes the form of a worker type to the + # ports of each worker. For example: + # { + # worker_type: {1234, 1235, ...}} + # } + # and will be used to construct 'upstream' nginx directives. + nginx_upstreams = {} + + # A map of: {"endpoint": "upstream"}, where "upstream" is a str representing what will be + # placed after the proxy_pass directive. The main benefit to representing this data as a + # dict over a str is that we can easily deduplicate endpoints across multiple instances + # of the same worker. + # + # An nginx site config that will be amended to depending on the workers that are + # spun up. To be placed in /etc/nginx/conf.d. + nginx_locations = {} + + # Read the desired worker configuration from the environment + worker_types = environ.get("SYNAPSE_WORKER_TYPES") + if worker_types is None: + # No workers, just the main process + worker_types = [] + else: + # Split type names by comma + worker_types = worker_types.split(",") + + # Create the worker configuration directory if it doesn't already exist + os.makedirs("/conf/workers", exist_ok=True) + + # Start worker ports from this arbitrary port + worker_port = 18009 + + # A counter of worker_type -> int. Used for determining the name for a given + # worker type when generating its config file, as each worker's name is just + # worker_type + instance # + worker_type_counter = {} + + # For each worker type specified by the user, create config values + for worker_type in worker_types: + worker_type = worker_type.strip() + + worker_config = WORKERS_CONFIG.get(worker_type) + if worker_config: + worker_config = worker_config.copy() + else: + log(worker_type + " is an unknown worker type! It will be ignored") + continue + + new_worker_count = worker_type_counter.setdefault(worker_type, 0) + 1 + worker_type_counter[worker_type] = new_worker_count + + # Name workers by their type concatenated with an incrementing number + # e.g. federation_reader1 + worker_name = worker_type + str(new_worker_count) + worker_config.update( + {"name": worker_name, "port": worker_port, "config_path": config_path} + ) + + # Update the shared config with any worker-type specific options + shared_config.update(worker_config["shared_extra_conf"]) + + # Check if more than one instance of this worker type has been specified + worker_type_total_count = worker_types.count(worker_type) + if worker_type_total_count > 1: + # Update the shared config with sharding-related options if necessary + add_sharding_to_shared_config( + shared_config, worker_type, worker_name, worker_port + ) + + # Enable the worker in supervisord + supervisord_config += SUPERVISORD_PROCESS_CONFIG_BLOCK.format_map(worker_config) + + # Add nginx location blocks for this worker's endpoints (if any are defined) + for pattern in worker_config["endpoint_patterns"]: + # Determine whether we need to load-balance this worker + if worker_type_total_count > 1: + # Create or add to a load-balanced upstream for this worker + nginx_upstreams.setdefault(worker_type, set()).add(worker_port) + + # Upstreams are named after the worker_type + upstream = "http://" + worker_type + else: + upstream = "http://localhost:%d" % (worker_port,) + + # Note that this endpoint should proxy to this upstream + nginx_locations[pattern] = upstream + + # Write out the worker's logging config file + + # Check whether we should write worker logs to disk, in addition to the console + extra_log_template_args = {} + if environ.get("SYNAPSE_WORKERS_WRITE_LOGS_TO_DISK"): + extra_log_template_args["LOG_FILE_PATH"] = "{dir}/logs/{name}.log".format( + dir=data_dir, name=worker_name + ) + + # Render and write the file + log_config_filepath = "/conf/workers/{name}.log.config".format(name=worker_name) + convert( + "/conf/log.config", + log_config_filepath, + worker_name=worker_name, + **extra_log_template_args, + ) + + # Then a worker config file + convert( + "/conf/worker.yaml.j2", + "/conf/workers/{name}.yaml".format(name=worker_name), + **worker_config, + worker_log_config_filepath=log_config_filepath, + ) + + worker_port += 1 + + # Build the nginx location config blocks + nginx_location_config = "" + for endpoint, upstream in nginx_locations.items(): + nginx_location_config += NGINX_LOCATION_CONFIG_BLOCK.format( + endpoint=endpoint, + upstream=upstream, + ) + + # Determine the load-balancing upstreams to configure + nginx_upstream_config = "" + for upstream_worker_type, upstream_worker_ports in nginx_upstreams.items(): + body = "" + for port in upstream_worker_ports: + body += " server localhost:%d;\n" % (port,) + + # Add to the list of configured upstreams + nginx_upstream_config += NGINX_UPSTREAM_CONFIG_BLOCK.format( + upstream_worker_type=upstream_worker_type, + body=body, + ) + + # Finally, we'll write out the config files. + + # Shared homeserver config + convert( + "/conf/shared.yaml.j2", + "/conf/workers/shared.yaml", + shared_worker_config=yaml.dump(shared_config), + ) + + # Nginx config + convert( + "/conf/nginx.conf.j2", + "/etc/nginx/conf.d/matrix-synapse.conf", + worker_locations=nginx_location_config, + upstream_directives=nginx_upstream_config, + ) + + # Supervisord config + convert( + "/conf/supervisord.conf.j2", + "/etc/supervisor/conf.d/supervisord.conf", + main_config_path=config_path, + worker_config=supervisord_config, + ) + + # Ensure the logging directory exists + log_dir = data_dir + "/logs" + if not os.path.exists(log_dir): + os.mkdir(log_dir) + + +def start_supervisord(): + """Starts up supervisord which then starts and monitors all other necessary processes + + Raises: CalledProcessError if calling start.py return a non-zero exit code. + """ + subprocess.run(["/usr/bin/supervisord"], stdin=subprocess.PIPE) + + +def main(args, environ): + config_dir = environ.get("SYNAPSE_CONFIG_DIR", "/data") + config_path = environ.get("SYNAPSE_CONFIG_PATH", config_dir + "/homeserver.yaml") + data_dir = environ.get("SYNAPSE_DATA_DIR", "/data") + + # override SYNAPSE_NO_TLS, we don't support TLS in worker mode, + # this needs to be handled by a frontend proxy + environ["SYNAPSE_NO_TLS"] = "yes" + + # Generate the base homeserver config if one does not yet exist + if not os.path.exists(config_path): + log("Generating base homeserver config") + generate_base_homeserver_config() + + # This script may be run multiple times (mostly by Complement, see note at top of file). + # Don't re-configure workers in this instance. + mark_filepath = "/conf/workers_have_been_configured" + if not os.path.exists(mark_filepath): + # Always regenerate all other config files + generate_worker_files(environ, config_path, data_dir) + + # Mark workers as being configured + with open(mark_filepath, "w") as f: + f.write("") + + # Start supervisord, which will start Synapse, all of the configured worker + # processes, redis, nginx etc. according to the config we created above. + start_supervisord() + + +if __name__ == "__main__": + main(sys.argv, os.environ) From 4b965c862dc66c0da5d3240add70e9b5f0aa720b Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Wed, 14 Apr 2021 16:34:27 +0200 Subject: [PATCH 047/619] Remove redundant "coding: utf-8" lines (#9786) Part of #9744 Removes all redundant `# -*- coding: utf-8 -*-` lines from files, as python 3 automatically reads source code as utf-8 now. `Signed-off-by: Jonathan de Jong ` --- .buildkite/scripts/create_postgres_db.py | 1 - changelog.d/9786.misc | 1 + contrib/cmdclient/http.py | 1 - contrib/experiments/test_messaging.py | 1 - scripts-dev/mypy_synapse_plugin.py | 1 - scripts-dev/sign_json | 1 - scripts-dev/update_database | 1 - scripts/export_signing_key | 1 - scripts/generate_log_config | 1 - scripts/generate_signing_key.py | 1 - scripts/move_remote_media_to_new_store.py | 1 - scripts/register_new_matrix_user | 1 - scripts/synapse_port_db | 1 - stubs/frozendict.pyi | 1 - stubs/txredisapi.pyi | 1 - synapse/__init__.py | 1 - synapse/_scripts/register_new_matrix_user.py | 1 - synapse/api/__init__.py | 1 - synapse/api/auth.py | 1 - synapse/api/auth_blocking.py | 1 - synapse/api/constants.py | 1 - synapse/api/errors.py | 1 - synapse/api/filtering.py | 1 - synapse/api/presence.py | 1 - synapse/api/room_versions.py | 1 - synapse/api/urls.py | 1 - synapse/app/__init__.py | 1 - synapse/app/_base.py | 1 - synapse/app/admin_cmd.py | 1 - synapse/app/appservice.py | 1 - synapse/app/client_reader.py | 1 - synapse/app/event_creator.py | 1 - synapse/app/federation_reader.py | 1 - synapse/app/federation_sender.py | 1 - synapse/app/frontend_proxy.py | 1 - synapse/app/generic_worker.py | 1 - synapse/app/homeserver.py | 1 - synapse/app/media_repository.py | 1 - synapse/app/pusher.py | 1 - synapse/app/synchrotron.py | 1 - synapse/app/user_dir.py | 1 - synapse/appservice/__init__.py | 1 - synapse/appservice/api.py | 1 - synapse/appservice/scheduler.py | 1 - synapse/config/__init__.py | 1 - synapse/config/__main__.py | 1 - synapse/config/_base.py | 1 - synapse/config/_util.py | 1 - synapse/config/auth.py | 1 - synapse/config/cache.py | 1 - synapse/config/cas.py | 1 - synapse/config/consent_config.py | 1 - synapse/config/database.py | 1 - synapse/config/emailconfig.py | 1 - synapse/config/experimental.py | 1 - synapse/config/federation.py | 1 - synapse/config/groups.py | 1 - synapse/config/homeserver.py | 1 - synapse/config/jwt_config.py | 1 - synapse/config/key.py | 1 - synapse/config/logger.py | 1 - synapse/config/metrics.py | 1 - synapse/config/oidc_config.py | 1 - synapse/config/password_auth_providers.py | 1 - synapse/config/push.py | 1 - synapse/config/redis.py | 1 - synapse/config/registration.py | 1 - synapse/config/repository.py | 1 - synapse/config/room.py | 1 - synapse/config/room_directory.py | 1 - synapse/config/saml2_config.py | 1 - synapse/config/server.py | 1 - synapse/config/server_notices_config.py | 1 - synapse/config/spam_checker.py | 1 - synapse/config/sso.py | 1 - synapse/config/stats.py | 1 - synapse/config/third_party_event_rules.py | 1 - synapse/config/tls.py | 1 - synapse/config/tracer.py | 1 - synapse/config/user_directory.py | 1 - synapse/config/workers.py | 1 - synapse/crypto/__init__.py | 1 - synapse/crypto/event_signing.py | 1 - synapse/crypto/keyring.py | 1 - synapse/event_auth.py | 1 - synapse/events/__init__.py | 1 - synapse/events/builder.py | 1 - synapse/events/presence_router.py | 1 - synapse/events/snapshot.py | 1 - synapse/events/spamcheck.py | 1 - synapse/events/third_party_rules.py | 1 - synapse/events/utils.py | 1 - synapse/events/validator.py | 1 - synapse/federation/__init__.py | 1 - synapse/federation/federation_base.py | 1 - synapse/federation/federation_client.py | 1 - synapse/federation/federation_server.py | 1 - synapse/federation/persistence.py | 1 - synapse/federation/send_queue.py | 1 - synapse/federation/sender/__init__.py | 1 - synapse/federation/sender/per_destination_queue.py | 1 - synapse/federation/sender/transaction_manager.py | 1 - synapse/federation/transport/__init__.py | 1 - synapse/federation/transport/client.py | 1 - synapse/federation/transport/server.py | 1 - synapse/federation/units.py | 1 - synapse/groups/attestations.py | 1 - synapse/groups/groups_server.py | 1 - synapse/handlers/__init__.py | 1 - synapse/handlers/_base.py | 1 - synapse/handlers/account_data.py | 1 - synapse/handlers/account_validity.py | 1 - synapse/handlers/acme.py | 1 - synapse/handlers/acme_issuing_service.py | 1 - synapse/handlers/admin.py | 1 - synapse/handlers/appservice.py | 1 - synapse/handlers/auth.py | 1 - synapse/handlers/cas_handler.py | 1 - synapse/handlers/deactivate_account.py | 1 - synapse/handlers/device.py | 1 - synapse/handlers/devicemessage.py | 1 - synapse/handlers/directory.py | 1 - synapse/handlers/e2e_keys.py | 1 - synapse/handlers/e2e_room_keys.py | 1 - synapse/handlers/events.py | 1 - synapse/handlers/federation.py | 1 - synapse/handlers/groups_local.py | 1 - synapse/handlers/identity.py | 1 - synapse/handlers/initial_sync.py | 1 - synapse/handlers/message.py | 1 - synapse/handlers/oidc_handler.py | 1 - synapse/handlers/pagination.py | 1 - synapse/handlers/password_policy.py | 1 - synapse/handlers/presence.py | 1 - synapse/handlers/profile.py | 1 - synapse/handlers/read_marker.py | 1 - synapse/handlers/receipts.py | 1 - synapse/handlers/register.py | 1 - synapse/handlers/room.py | 1 - synapse/handlers/room_list.py | 1 - synapse/handlers/room_member.py | 1 - synapse/handlers/room_member_worker.py | 1 - synapse/handlers/saml_handler.py | 1 - synapse/handlers/search.py | 1 - synapse/handlers/set_password.py | 1 - synapse/handlers/space_summary.py | 1 - synapse/handlers/sso.py | 1 - synapse/handlers/state_deltas.py | 1 - synapse/handlers/stats.py | 1 - synapse/handlers/sync.py | 1 - synapse/handlers/typing.py | 1 - synapse/handlers/ui_auth/__init__.py | 1 - synapse/handlers/ui_auth/checkers.py | 1 - synapse/handlers/user_directory.py | 1 - synapse/http/__init__.py | 1 - synapse/http/additional_resource.py | 1 - synapse/http/client.py | 1 - synapse/http/connectproxyclient.py | 1 - synapse/http/federation/__init__.py | 1 - synapse/http/federation/matrix_federation_agent.py | 1 - synapse/http/federation/srv_resolver.py | 1 - synapse/http/federation/well_known_resolver.py | 1 - synapse/http/matrixfederationclient.py | 1 - synapse/http/proxyagent.py | 1 - synapse/http/request_metrics.py | 1 - synapse/http/server.py | 1 - synapse/http/servlet.py | 1 - synapse/logging/__init__.py | 1 - synapse/logging/_remote.py | 1 - synapse/logging/_structured.py | 1 - synapse/logging/_terse_json.py | 1 - synapse/logging/filter.py | 1 - synapse/logging/formatter.py | 1 - synapse/logging/opentracing.py | 1 - synapse/logging/scopecontextmanager.py | 1 - synapse/logging/utils.py | 1 - synapse/metrics/__init__.py | 1 - synapse/metrics/_exposition.py | 1 - synapse/metrics/background_process_metrics.py | 1 - synapse/module_api/__init__.py | 1 - synapse/module_api/errors.py | 1 - synapse/notifier.py | 1 - synapse/push/__init__.py | 1 - synapse/push/action_generator.py | 1 - synapse/push/bulk_push_rule_evaluator.py | 1 - synapse/push/clientformat.py | 1 - synapse/push/emailpusher.py | 1 - synapse/push/httppusher.py | 1 - synapse/push/mailer.py | 1 - synapse/push/presentable_names.py | 1 - synapse/push/push_rule_evaluator.py | 1 - synapse/push/push_tools.py | 1 - synapse/push/pusher.py | 1 - synapse/push/pusherpool.py | 1 - synapse/replication/__init__.py | 1 - synapse/replication/http/__init__.py | 1 - synapse/replication/http/_base.py | 1 - synapse/replication/http/account_data.py | 1 - synapse/replication/http/devices.py | 1 - synapse/replication/http/federation.py | 1 - synapse/replication/http/login.py | 1 - synapse/replication/http/membership.py | 1 - synapse/replication/http/presence.py | 1 - synapse/replication/http/push.py | 1 - synapse/replication/http/register.py | 1 - synapse/replication/http/send_event.py | 1 - synapse/replication/http/streams.py | 1 - synapse/replication/slave/__init__.py | 1 - synapse/replication/slave/storage/__init__.py | 1 - synapse/replication/slave/storage/_base.py | 1 - synapse/replication/slave/storage/_slaved_id_tracker.py | 1 - synapse/replication/slave/storage/account_data.py | 1 - synapse/replication/slave/storage/appservice.py | 1 - synapse/replication/slave/storage/client_ips.py | 1 - synapse/replication/slave/storage/deviceinbox.py | 1 - synapse/replication/slave/storage/devices.py | 1 - synapse/replication/slave/storage/directory.py | 1 - synapse/replication/slave/storage/events.py | 1 - synapse/replication/slave/storage/filtering.py | 1 - synapse/replication/slave/storage/groups.py | 1 - synapse/replication/slave/storage/keys.py | 1 - synapse/replication/slave/storage/presence.py | 1 - synapse/replication/slave/storage/profile.py | 1 - synapse/replication/slave/storage/push_rule.py | 1 - synapse/replication/slave/storage/pushers.py | 1 - synapse/replication/slave/storage/receipts.py | 1 - synapse/replication/slave/storage/registration.py | 1 - synapse/replication/slave/storage/room.py | 1 - synapse/replication/slave/storage/transactions.py | 1 - synapse/replication/tcp/__init__.py | 1 - synapse/replication/tcp/client.py | 1 - synapse/replication/tcp/commands.py | 1 - synapse/replication/tcp/external_cache.py | 1 - synapse/replication/tcp/handler.py | 1 - synapse/replication/tcp/protocol.py | 1 - synapse/replication/tcp/redis.py | 1 - synapse/replication/tcp/resource.py | 1 - synapse/replication/tcp/streams/__init__.py | 1 - synapse/replication/tcp/streams/_base.py | 1 - synapse/replication/tcp/streams/events.py | 1 - synapse/replication/tcp/streams/federation.py | 1 - synapse/rest/__init__.py | 1 - synapse/rest/admin/__init__.py | 1 - synapse/rest/admin/_base.py | 1 - synapse/rest/admin/devices.py | 1 - synapse/rest/admin/event_reports.py | 1 - synapse/rest/admin/groups.py | 1 - synapse/rest/admin/media.py | 1 - synapse/rest/admin/purge_room_servlet.py | 1 - synapse/rest/admin/rooms.py | 1 - synapse/rest/admin/server_notice_servlet.py | 1 - synapse/rest/admin/statistics.py | 1 - synapse/rest/admin/users.py | 1 - synapse/rest/client/__init__.py | 1 - synapse/rest/client/transactions.py | 1 - synapse/rest/client/v1/__init__.py | 1 - synapse/rest/client/v1/directory.py | 1 - synapse/rest/client/v1/events.py | 1 - synapse/rest/client/v1/initial_sync.py | 1 - synapse/rest/client/v1/login.py | 1 - synapse/rest/client/v1/logout.py | 1 - synapse/rest/client/v1/presence.py | 1 - synapse/rest/client/v1/profile.py | 1 - synapse/rest/client/v1/push_rule.py | 1 - synapse/rest/client/v1/pusher.py | 1 - synapse/rest/client/v1/room.py | 1 - synapse/rest/client/v1/voip.py | 1 - synapse/rest/client/v2_alpha/__init__.py | 1 - synapse/rest/client/v2_alpha/_base.py | 1 - synapse/rest/client/v2_alpha/account.py | 1 - synapse/rest/client/v2_alpha/account_data.py | 1 - synapse/rest/client/v2_alpha/account_validity.py | 1 - synapse/rest/client/v2_alpha/auth.py | 1 - synapse/rest/client/v2_alpha/capabilities.py | 1 - synapse/rest/client/v2_alpha/devices.py | 1 - synapse/rest/client/v2_alpha/filter.py | 1 - synapse/rest/client/v2_alpha/groups.py | 1 - synapse/rest/client/v2_alpha/keys.py | 1 - synapse/rest/client/v2_alpha/notifications.py | 1 - synapse/rest/client/v2_alpha/openid.py | 1 - synapse/rest/client/v2_alpha/password_policy.py | 1 - synapse/rest/client/v2_alpha/read_marker.py | 1 - synapse/rest/client/v2_alpha/receipts.py | 1 - synapse/rest/client/v2_alpha/register.py | 1 - synapse/rest/client/v2_alpha/relations.py | 1 - synapse/rest/client/v2_alpha/report_event.py | 1 - synapse/rest/client/v2_alpha/room_keys.py | 1 - synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py | 1 - synapse/rest/client/v2_alpha/sendtodevice.py | 1 - synapse/rest/client/v2_alpha/shared_rooms.py | 1 - synapse/rest/client/v2_alpha/sync.py | 1 - synapse/rest/client/v2_alpha/tags.py | 1 - synapse/rest/client/v2_alpha/thirdparty.py | 1 - synapse/rest/client/v2_alpha/tokenrefresh.py | 1 - synapse/rest/client/v2_alpha/user_directory.py | 1 - synapse/rest/client/versions.py | 1 - synapse/rest/consent/consent_resource.py | 1 - synapse/rest/health.py | 1 - synapse/rest/key/__init__.py | 1 - synapse/rest/key/v2/__init__.py | 1 - synapse/rest/key/v2/local_key_resource.py | 1 - synapse/rest/media/v1/__init__.py | 1 - synapse/rest/media/v1/_base.py | 1 - synapse/rest/media/v1/config_resource.py | 1 - synapse/rest/media/v1/download_resource.py | 1 - synapse/rest/media/v1/filepath.py | 1 - synapse/rest/media/v1/media_repository.py | 1 - synapse/rest/media/v1/media_storage.py | 1 - synapse/rest/media/v1/preview_url_resource.py | 1 - synapse/rest/media/v1/storage_provider.py | 1 - synapse/rest/media/v1/thumbnail_resource.py | 1 - synapse/rest/media/v1/thumbnailer.py | 1 - synapse/rest/media/v1/upload_resource.py | 1 - synapse/rest/synapse/__init__.py | 1 - synapse/rest/synapse/client/__init__.py | 1 - synapse/rest/synapse/client/new_user_consent.py | 1 - synapse/rest/synapse/client/oidc/__init__.py | 1 - synapse/rest/synapse/client/oidc/callback_resource.py | 1 - synapse/rest/synapse/client/password_reset.py | 1 - synapse/rest/synapse/client/pick_idp.py | 1 - synapse/rest/synapse/client/pick_username.py | 1 - synapse/rest/synapse/client/saml2/__init__.py | 1 - synapse/rest/synapse/client/saml2/metadata_resource.py | 1 - synapse/rest/synapse/client/saml2/response_resource.py | 1 - synapse/rest/synapse/client/sso_register.py | 1 - synapse/rest/well_known.py | 1 - synapse/secrets.py | 1 - synapse/server.py | 1 - synapse/server_notices/consent_server_notices.py | 1 - synapse/server_notices/resource_limits_server_notices.py | 1 - synapse/server_notices/server_notices_manager.py | 1 - synapse/server_notices/server_notices_sender.py | 1 - synapse/server_notices/worker_server_notices_sender.py | 1 - synapse/spam_checker_api/__init__.py | 1 - synapse/state/__init__.py | 1 - synapse/state/v1.py | 1 - synapse/state/v2.py | 1 - synapse/storage/__init__.py | 1 - synapse/storage/_base.py | 1 - synapse/storage/background_updates.py | 1 - synapse/storage/database.py | 1 - synapse/storage/databases/__init__.py | 1 - synapse/storage/databases/main/__init__.py | 1 - synapse/storage/databases/main/account_data.py | 1 - synapse/storage/databases/main/appservice.py | 1 - synapse/storage/databases/main/cache.py | 1 - synapse/storage/databases/main/censor_events.py | 1 - synapse/storage/databases/main/client_ips.py | 1 - synapse/storage/databases/main/deviceinbox.py | 1 - synapse/storage/databases/main/devices.py | 1 - synapse/storage/databases/main/directory.py | 1 - synapse/storage/databases/main/e2e_room_keys.py | 1 - synapse/storage/databases/main/end_to_end_keys.py | 1 - synapse/storage/databases/main/event_federation.py | 1 - synapse/storage/databases/main/event_push_actions.py | 1 - synapse/storage/databases/main/events.py | 1 - synapse/storage/databases/main/events_bg_updates.py | 1 - synapse/storage/databases/main/events_forward_extremities.py | 1 - synapse/storage/databases/main/events_worker.py | 1 - synapse/storage/databases/main/filtering.py | 1 - synapse/storage/databases/main/group_server.py | 1 - synapse/storage/databases/main/keys.py | 1 - synapse/storage/databases/main/media_repository.py | 1 - synapse/storage/databases/main/metrics.py | 1 - synapse/storage/databases/main/monthly_active_users.py | 1 - synapse/storage/databases/main/presence.py | 1 - synapse/storage/databases/main/profile.py | 1 - synapse/storage/databases/main/purge_events.py | 1 - synapse/storage/databases/main/push_rule.py | 1 - synapse/storage/databases/main/pusher.py | 1 - synapse/storage/databases/main/receipts.py | 1 - synapse/storage/databases/main/registration.py | 1 - synapse/storage/databases/main/rejections.py | 1 - synapse/storage/databases/main/relations.py | 1 - synapse/storage/databases/main/room.py | 1 - synapse/storage/databases/main/roommember.py | 1 - .../main/schema/delta/50/make_event_content_nullable.py | 1 - .../databases/main/schema/delta/57/local_current_membership.py | 1 - synapse/storage/databases/main/search.py | 1 - synapse/storage/databases/main/signatures.py | 1 - synapse/storage/databases/main/state.py | 1 - synapse/storage/databases/main/state_deltas.py | 1 - synapse/storage/databases/main/stats.py | 1 - synapse/storage/databases/main/stream.py | 1 - synapse/storage/databases/main/tags.py | 1 - synapse/storage/databases/main/transactions.py | 1 - synapse/storage/databases/main/ui_auth.py | 1 - synapse/storage/databases/main/user_directory.py | 1 - synapse/storage/databases/main/user_erasure_store.py | 1 - synapse/storage/databases/state/__init__.py | 1 - synapse/storage/databases/state/bg_updates.py | 1 - synapse/storage/databases/state/store.py | 1 - synapse/storage/engines/__init__.py | 1 - synapse/storage/engines/_base.py | 1 - synapse/storage/engines/postgres.py | 1 - synapse/storage/engines/sqlite.py | 1 - synapse/storage/keys.py | 1 - synapse/storage/persist_events.py | 1 - synapse/storage/prepare_database.py | 1 - synapse/storage/purge_events.py | 1 - synapse/storage/push_rule.py | 1 - synapse/storage/relations.py | 1 - synapse/storage/roommember.py | 1 - synapse/storage/state.py | 1 - synapse/storage/types.py | 1 - synapse/storage/util/__init__.py | 1 - synapse/storage/util/id_generators.py | 1 - synapse/storage/util/sequence.py | 1 - synapse/streams/__init__.py | 1 - synapse/streams/config.py | 1 - synapse/streams/events.py | 1 - synapse/types.py | 1 - synapse/util/__init__.py | 1 - synapse/util/async_helpers.py | 1 - synapse/util/caches/__init__.py | 1 - synapse/util/caches/cached_call.py | 1 - synapse/util/caches/deferred_cache.py | 1 - synapse/util/caches/descriptors.py | 1 - synapse/util/caches/dictionary_cache.py | 1 - synapse/util/caches/expiringcache.py | 1 - synapse/util/caches/lrucache.py | 1 - synapse/util/caches/response_cache.py | 1 - synapse/util/caches/stream_change_cache.py | 1 - synapse/util/caches/ttlcache.py | 1 - synapse/util/daemonize.py | 1 - synapse/util/distributor.py | 1 - synapse/util/file_consumer.py | 1 - synapse/util/frozenutils.py | 1 - synapse/util/hash.py | 2 -- synapse/util/iterutils.py | 1 - synapse/util/jsonobject.py | 1 - synapse/util/macaroons.py | 1 - synapse/util/metrics.py | 1 - synapse/util/module_loader.py | 1 - synapse/util/msisdn.py | 1 - synapse/util/patch_inline_callbacks.py | 1 - synapse/util/ratelimitutils.py | 1 - synapse/util/retryutils.py | 1 - synapse/util/rlimit.py | 1 - synapse/util/stringutils.py | 1 - synapse/util/templates.py | 1 - synapse/util/threepids.py | 1 - synapse/util/versionstring.py | 1 - synapse/util/wheel_timer.py | 1 - synapse/visibility.py | 1 - synctl | 1 - synmark/__init__.py | 1 - synmark/__main__.py | 1 - synmark/suites/logging.py | 1 - synmark/suites/lrucache.py | 1 - synmark/suites/lrucache_evict.py | 1 - tests/__init__.py | 1 - tests/api/test_auth.py | 1 - tests/api/test_filtering.py | 1 - tests/app/test_frontend_proxy.py | 1 - tests/app/test_openid_listener.py | 1 - tests/appservice/__init__.py | 1 - tests/appservice/test_appservice.py | 1 - tests/appservice/test_scheduler.py | 1 - tests/config/__init__.py | 1 - tests/config/test_base.py | 1 - tests/config/test_cache.py | 1 - tests/config/test_database.py | 1 - tests/config/test_generate.py | 1 - tests/config/test_load.py | 1 - tests/config/test_ratelimiting.py | 1 - tests/config/test_room_directory.py | 1 - tests/config/test_server.py | 1 - tests/config/test_tls.py | 1 - tests/config/test_util.py | 1 - tests/crypto/__init__.py | 1 - tests/crypto/test_event_signing.py | 1 - tests/crypto/test_keyring.py | 1 - tests/events/test_presence_router.py | 1 - tests/events/test_snapshot.py | 1 - tests/events/test_utils.py | 1 - tests/federation/test_complexity.py | 1 - tests/federation/test_federation_sender.py | 1 - tests/federation/test_federation_server.py | 1 - tests/federation/transport/test_server.py | 1 - tests/handlers/test_admin.py | 1 - tests/handlers/test_appservice.py | 1 - tests/handlers/test_auth.py | 1 - tests/handlers/test_device.py | 1 - tests/handlers/test_directory.py | 1 - tests/handlers/test_e2e_keys.py | 1 - tests/handlers/test_e2e_room_keys.py | 1 - tests/handlers/test_federation.py | 1 - tests/handlers/test_message.py | 1 - tests/handlers/test_oidc.py | 1 - tests/handlers/test_password_providers.py | 1 - tests/handlers/test_presence.py | 1 - tests/handlers/test_profile.py | 1 - tests/handlers/test_register.py | 1 - tests/handlers/test_stats.py | 1 - tests/handlers/test_sync.py | 1 - tests/handlers/test_typing.py | 1 - tests/handlers/test_user_directory.py | 1 - tests/http/__init__.py | 1 - tests/http/federation/__init__.py | 1 - tests/http/federation/test_matrix_federation_agent.py | 1 - tests/http/federation/test_srv_resolver.py | 1 - tests/http/test_additional_resource.py | 1 - tests/http/test_endpoint.py | 1 - tests/http/test_fedclient.py | 1 - tests/http/test_proxyagent.py | 1 - tests/http/test_servlet.py | 1 - tests/http/test_simple_client.py | 1 - tests/logging/__init__.py | 1 - tests/logging/test_remote_handler.py | 1 - tests/logging/test_terse_json.py | 1 - tests/module_api/test_api.py | 1 - tests/push/test_email.py | 1 - tests/push/test_http.py | 1 - tests/push/test_push_rule_evaluator.py | 1 - tests/replication/__init__.py | 1 - tests/replication/_base.py | 1 - tests/replication/slave/__init__.py | 1 - tests/replication/slave/storage/__init__.py | 1 - tests/replication/tcp/__init__.py | 1 - tests/replication/tcp/streams/__init__.py | 1 - tests/replication/tcp/streams/test_account_data.py | 1 - tests/replication/tcp/streams/test_events.py | 1 - tests/replication/tcp/streams/test_federation.py | 1 - tests/replication/tcp/streams/test_receipts.py | 1 - tests/replication/tcp/streams/test_typing.py | 1 - tests/replication/tcp/test_commands.py | 1 - tests/replication/tcp/test_remote_server_up.py | 1 - tests/replication/test_auth.py | 1 - tests/replication/test_client_reader_shard.py | 1 - tests/replication/test_federation_ack.py | 1 - tests/replication/test_federation_sender_shard.py | 1 - tests/replication/test_multi_media_repo.py | 1 - tests/replication/test_pusher_shard.py | 1 - tests/replication/test_sharded_event_persister.py | 1 - tests/rest/__init__.py | 1 - tests/rest/admin/__init__.py | 1 - tests/rest/admin/test_admin.py | 1 - tests/rest/admin/test_device.py | 1 - tests/rest/admin/test_event_reports.py | 1 - tests/rest/admin/test_media.py | 1 - tests/rest/admin/test_room.py | 1 - tests/rest/admin/test_statistics.py | 1 - tests/rest/admin/test_user.py | 1 - tests/rest/client/__init__.py | 1 - tests/rest/client/test_consent.py | 1 - tests/rest/client/test_ephemeral_message.py | 1 - tests/rest/client/test_identity.py | 1 - tests/rest/client/test_power_levels.py | 1 - tests/rest/client/test_redactions.py | 1 - tests/rest/client/test_retention.py | 1 - tests/rest/client/test_third_party_rules.py | 1 - tests/rest/client/v1/__init__.py | 1 - tests/rest/client/v1/test_directory.py | 1 - tests/rest/client/v1/test_events.py | 1 - tests/rest/client/v1/test_login.py | 1 - tests/rest/client/v1/test_presence.py | 1 - tests/rest/client/v1/test_profile.py | 1 - tests/rest/client/v1/test_push_rule_attrs.py | 1 - tests/rest/client/v1/test_rooms.py | 1 - tests/rest/client/v1/test_typing.py | 1 - tests/rest/client/v1/utils.py | 1 - tests/rest/client/v2_alpha/test_account.py | 1 - tests/rest/client/v2_alpha/test_auth.py | 1 - tests/rest/client/v2_alpha/test_capabilities.py | 1 - tests/rest/client/v2_alpha/test_filter.py | 1 - tests/rest/client/v2_alpha/test_password_policy.py | 1 - tests/rest/client/v2_alpha/test_register.py | 1 - tests/rest/client/v2_alpha/test_relations.py | 1 - tests/rest/client/v2_alpha/test_shared_rooms.py | 1 - tests/rest/client/v2_alpha/test_sync.py | 1 - tests/rest/client/v2_alpha/test_upgrade_room.py | 1 - tests/rest/key/v2/test_remote_key_resource.py | 1 - tests/rest/media/__init__.py | 1 - tests/rest/media/v1/__init__.py | 1 - tests/rest/media/v1/test_base.py | 1 - tests/rest/media/v1/test_media_storage.py | 1 - tests/rest/media/v1/test_url_preview.py | 1 - tests/rest/test_health.py | 1 - tests/rest/test_well_known.py | 1 - tests/scripts/test_new_matrix_user.py | 1 - tests/server_notices/test_consent.py | 1 - tests/server_notices/test_resource_limits_server_notices.py | 1 - tests/state/test_v2.py | 1 - tests/storage/test__base.py | 1 - tests/storage/test_account_data.py | 1 - tests/storage/test_appservice.py | 1 - tests/storage/test_base.py | 1 - tests/storage/test_cleanup_extrems.py | 1 - tests/storage/test_client_ips.py | 1 - tests/storage/test_database.py | 1 - tests/storage/test_devices.py | 1 - tests/storage/test_directory.py | 1 - tests/storage/test_e2e_room_keys.py | 1 - tests/storage/test_end_to_end_keys.py | 1 - tests/storage/test_event_chain.py | 1 - tests/storage/test_event_federation.py | 1 - tests/storage/test_event_metrics.py | 1 - tests/storage/test_event_push_actions.py | 1 - tests/storage/test_events.py | 1 - tests/storage/test_id_generators.py | 1 - tests/storage/test_keys.py | 1 - tests/storage/test_main.py | 1 - tests/storage/test_monthly_active_users.py | 1 - tests/storage/test_profile.py | 1 - tests/storage/test_purge.py | 1 - tests/storage/test_redaction.py | 1 - tests/storage/test_registration.py | 1 - tests/storage/test_room.py | 1 - tests/storage/test_roommember.py | 1 - tests/storage/test_state.py | 1 - tests/storage/test_transactions.py | 1 - tests/storage/test_user_directory.py | 1 - tests/test_distributor.py | 1 - tests/test_event_auth.py | 1 - tests/test_federation.py | 1 - tests/test_mau.py | 1 - tests/test_metrics.py | 1 - tests/test_phone_home.py | 1 - tests/test_preview.py | 1 - tests/test_state.py | 1 - tests/test_test_utils.py | 1 - tests/test_types.py | 1 - tests/test_utils/__init__.py | 1 - tests/test_utils/event_injection.py | 1 - tests/test_utils/html_parsers.py | 1 - tests/test_utils/logging_setup.py | 1 - tests/test_visibility.py | 1 - tests/unittest.py | 1 - tests/util/__init__.py | 1 - tests/util/caches/__init__.py | 1 - tests/util/caches/test_cached_call.py | 1 - tests/util/caches/test_deferred_cache.py | 1 - tests/util/caches/test_descriptors.py | 1 - tests/util/caches/test_ttlcache.py | 1 - tests/util/test_async_utils.py | 1 - tests/util/test_dict_cache.py | 1 - tests/util/test_expiring_cache.py | 1 - tests/util/test_file_consumer.py | 1 - tests/util/test_itertools.py | 1 - tests/util/test_linearizer.py | 1 - tests/util/test_logformatter.py | 1 - tests/util/test_lrucache.py | 1 - tests/util/test_ratelimitutils.py | 1 - tests/util/test_retryutils.py | 1 - tests/util/test_rwlock.py | 1 - tests/util/test_stringutils.py | 1 - tests/util/test_threepids.py | 1 - tests/util/test_treecache.py | 1 - tests/util/test_wheel_timer.py | 1 - tests/utils.py | 1 - 651 files changed, 1 insertion(+), 651 deletions(-) create mode 100644 changelog.d/9786.misc diff --git a/.buildkite/scripts/create_postgres_db.py b/.buildkite/scripts/create_postgres_db.py index 956339de5c..cc829db216 100755 --- a/.buildkite/scripts/create_postgres_db.py +++ b/.buildkite/scripts/create_postgres_db.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/changelog.d/9786.misc b/changelog.d/9786.misc new file mode 100644 index 0000000000..cf265db749 --- /dev/null +++ b/changelog.d/9786.misc @@ -0,0 +1 @@ +Apply `pyupgrade` across the codebase. \ No newline at end of file diff --git a/contrib/cmdclient/http.py b/contrib/cmdclient/http.py index 1cf913756e..1310f078e3 100644 --- a/contrib/cmdclient/http.py +++ b/contrib/cmdclient/http.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/contrib/experiments/test_messaging.py b/contrib/experiments/test_messaging.py index 7fbc7d8fc6..31b8a68225 100644 --- a/contrib/experiments/test_messaging.py +++ b/contrib/experiments/test_messaging.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts-dev/mypy_synapse_plugin.py b/scripts-dev/mypy_synapse_plugin.py index 18df68305b..1217e14874 100644 --- a/scripts-dev/mypy_synapse_plugin.py +++ b/scripts-dev/mypy_synapse_plugin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts-dev/sign_json b/scripts-dev/sign_json index 44553fb79a..4a43d3f2b0 100755 --- a/scripts-dev/sign_json +++ b/scripts-dev/sign_json @@ -1,6 +1,5 @@ #!/usr/bin/env python # -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts-dev/update_database b/scripts-dev/update_database index 56365e2b58..87f709b6ed 100755 --- a/scripts-dev/update_database +++ b/scripts-dev/update_database @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts/export_signing_key b/scripts/export_signing_key index 8aec9d802b..0ed167ea85 100755 --- a/scripts/export_signing_key +++ b/scripts/export_signing_key @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts/generate_log_config b/scripts/generate_log_config index a13a5634a3..e72a0dafb7 100755 --- a/scripts/generate_log_config +++ b/scripts/generate_log_config @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts/generate_signing_key.py b/scripts/generate_signing_key.py index 16d7c4f382..07df25a809 100755 --- a/scripts/generate_signing_key.py +++ b/scripts/generate_signing_key.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts/move_remote_media_to_new_store.py b/scripts/move_remote_media_to_new_store.py index 8477955a90..875aa4781f 100755 --- a/scripts/move_remote_media_to_new_store.py +++ b/scripts/move_remote_media_to_new_store.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts/register_new_matrix_user b/scripts/register_new_matrix_user index 8b9d30877d..00104b9d62 100755 --- a/scripts/register_new_matrix_user +++ b/scripts/register_new_matrix_user @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 58edf6af6c..b7c1ffc956 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/stubs/frozendict.pyi b/stubs/frozendict.pyi index 0368ba4703..24c6f3af77 100644 --- a/stubs/frozendict.pyi +++ b/stubs/frozendict.pyi @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/stubs/txredisapi.pyi b/stubs/txredisapi.pyi index 080ca40287..c1a06ae022 100644 --- a/stubs/txredisapi.pyi +++ b/stubs/txredisapi.pyi @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/__init__.py b/synapse/__init__.py index 125a73d378..c2709c2ad4 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018-9 New Vector Ltd # diff --git a/synapse/_scripts/register_new_matrix_user.py b/synapse/_scripts/register_new_matrix_user.py index dfe26dea6d..dae986c788 100644 --- a/synapse/_scripts/register_new_matrix_user.py +++ b/synapse/_scripts/register_new_matrix_user.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector # diff --git a/synapse/api/__init__.py b/synapse/api/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/synapse/api/__init__.py +++ b/synapse/api/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 7d9930ae7b..6c13f53957 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/api/auth_blocking.py b/synapse/api/auth_blocking.py index d8088f524a..a8df60cb89 100644 --- a/synapse/api/auth_blocking.py +++ b/synapse/api/auth_blocking.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/api/constants.py b/synapse/api/constants.py index a8ae41de48..31a59bceec 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018-2019 New Vector Ltd diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 2a789ea3e8..0231c79079 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index 5caf336fd0..ce49a0ad58 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018-2019 New Vector Ltd diff --git a/synapse/api/presence.py b/synapse/api/presence.py index b9a8e29460..a3bf0348d1 100644 --- a/synapse/api/presence.py +++ b/synapse/api/presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 87038d436d..c9f9596ada 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/api/urls.py b/synapse/api/urls.py index 6379c86dde..4b1f213c75 100644 --- a/synapse/api/urls.py +++ b/synapse/api/urls.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/app/__init__.py b/synapse/app/__init__.py index d1a2cd5e19..f9940491e8 100644 --- a/synapse/app/__init__.py +++ b/synapse/app/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 3912c8994c..2113c4f370 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # Copyright 2019-2021 The Matrix.org Foundation C.I.C # diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 9f99651aa2..eb256db749 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2019 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/appservice.py b/synapse/app/appservice.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/appservice.py +++ b/synapse/app/appservice.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/event_creator.py b/synapse/app/event_creator.py index e9c098c4e7..57af28f10a 100644 --- a/synapse/app/event_creator.py +++ b/synapse/app/event_creator.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/frontend_proxy.py b/synapse/app/frontend_proxy.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/frontend_proxy.py +++ b/synapse/app/frontend_proxy.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index d1c2079233..e35e17492c 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 3bfe9d507f..679b7f4289 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/app/media_repository.py b/synapse/app/media_repository.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/media_repository.py +++ b/synapse/app/media_repository.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index add43147b3..2d50060ffb 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index 503d44f687..a368efb354 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/appservice/__init__.py b/synapse/appservice/__init__.py index 0bfc5e445f..6504c6bd3f 100644 --- a/synapse/appservice/__init__.py +++ b/synapse/appservice/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index 9d3bbe3b8b..fe04d7a672 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/appservice/scheduler.py b/synapse/appservice/scheduler.py index 5203ffe90f..6a2ce99b55 100644 --- a/synapse/appservice/scheduler.py +++ b/synapse/appservice/scheduler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/__init__.py b/synapse/config/__init__.py index 1e76e9559d..d2f889159e 100644 --- a/synapse/config/__init__.py +++ b/synapse/config/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/__main__.py b/synapse/config/__main__.py index 65043d5b5b..b5b6735a8f 100644 --- a/synapse/config/__main__.py +++ b/synapse/config/__main__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/_base.py b/synapse/config/_base.py index ba9cd63cf2..08e2c2c543 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/config/_util.py b/synapse/config/_util.py index 8fce7f6bb1..3edb4b7106 100644 --- a/synapse/config/_util.py +++ b/synapse/config/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/auth.py b/synapse/config/auth.py index 9aabaadf9e..e10d641a96 100644 --- a/synapse/config/auth.py +++ b/synapse/config/auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/config/cache.py b/synapse/config/cache.py index 4e8abbf88a..41b9b3f51f 100644 --- a/synapse/config/cache.py +++ b/synapse/config/cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/cas.py b/synapse/config/cas.py index dbf5085965..901f4123e1 100644 --- a/synapse/config/cas.py +++ b/synapse/config/cas.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/consent_config.py b/synapse/config/consent_config.py index c47f364b14..30d07cc219 100644 --- a/synapse/config/consent_config.py +++ b/synapse/config/consent_config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/database.py b/synapse/config/database.py index e7889b9c20..79a02706b4 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 52505ac5d2..c587939c7a 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index eb96ecda74..a693fba877 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/federation.py b/synapse/config/federation.py index 55e4db5442..090ba047fa 100644 --- a/synapse/config/federation.py +++ b/synapse/config/federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/groups.py b/synapse/config/groups.py index 7b7860ea71..15c2e64bda 100644 --- a/synapse/config/groups.py +++ b/synapse/config/groups.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 64a2429f77..1309535068 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/config/jwt_config.py b/synapse/config/jwt_config.py index f30330abb6..9e07e73008 100644 --- a/synapse/config/jwt_config.py +++ b/synapse/config/jwt_config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 Niklas Riekenbrauck # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/key.py b/synapse/config/key.py index 350ff1d665..94a9063043 100644 --- a/synapse/config/key.py +++ b/synapse/config/key.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 999aecce5c..b174e0df6d 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/metrics.py b/synapse/config/metrics.py index 2b289f4208..7ac82edb0e 100644 --- a/synapse/config/metrics.py +++ b/synapse/config/metrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/config/oidc_config.py b/synapse/config/oidc_config.py index 05733ec41d..5fb94376fd 100644 --- a/synapse/config/oidc_config.py +++ b/synapse/config/oidc_config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Quentin Gliech # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/config/password_auth_providers.py b/synapse/config/password_auth_providers.py index 85d07c4f8f..1cf69734bb 100644 --- a/synapse/config/password_auth_providers.py +++ b/synapse/config/password_auth_providers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 Openmarket # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/push.py b/synapse/config/push.py index 7831a2ef79..6ef8491caf 100644 --- a/synapse/config/push.py +++ b/synapse/config/push.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 New Vector Ltd # diff --git a/synapse/config/redis.py b/synapse/config/redis.py index 1373302335..33104af734 100644 --- a/synapse/config/redis.py +++ b/synapse/config/redis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/registration.py b/synapse/config/registration.py index f27d1e14ac..f8a2768af8 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/repository.py b/synapse/config/repository.py index 061c4ec83f..146bc55d6f 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014, 2015 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/room.py b/synapse/config/room.py index 692d7a1936..d889d90dbc 100644 --- a/synapse/config/room.py +++ b/synapse/config/room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/room_directory.py b/synapse/config/room_directory.py index 2dd719c388..56981cac79 100644 --- a/synapse/config/room_directory.py +++ b/synapse/config/room_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py index 6db9cb5ced..55a7838b10 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2_config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/config/server.py b/synapse/config/server.py index 8decc9d10d..02b86b11a5 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/config/server_notices_config.py b/synapse/config/server_notices_config.py index 57f69dc8e2..48bf3241b6 100644 --- a/synapse/config/server_notices_config.py +++ b/synapse/config/server_notices_config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py index 3d05abc158..447ba3303b 100644 --- a/synapse/config/spam_checker.py +++ b/synapse/config/spam_checker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 243cc681e8..af645c930d 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/stats.py b/synapse/config/stats.py index 2258329a52..3d44b51201 100644 --- a/synapse/config/stats.py +++ b/synapse/config/stats.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/third_party_event_rules.py b/synapse/config/third_party_event_rules.py index c04e1c4e07..f502ff539e 100644 --- a/synapse/config/third_party_event_rules.py +++ b/synapse/config/third_party_event_rules.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 85b5db4c40..b041869758 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/tracer.py b/synapse/config/tracer.py index 727a1e7008..db22b5b19f 100644 --- a/synapse/config/tracer.py +++ b/synapse/config/tracer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C.d # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/user_directory.py b/synapse/config/user_directory.py index 8d05ef173c..4cbf79eeed 100644 --- a/synapse/config/user_directory.py +++ b/synapse/config/user_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/workers.py b/synapse/config/workers.py index ac92375a85..b2540163d1 100644 --- a/synapse/config/workers.py +++ b/synapse/config/workers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/crypto/__init__.py b/synapse/crypto/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/synapse/crypto/__init__.py +++ b/synapse/crypto/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/crypto/event_signing.py b/synapse/crypto/event_signing.py index 8fb116ae18..0f2b632e47 100644 --- a/synapse/crypto/event_signing.py +++ b/synapse/crypto/event_signing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index d5fb51513b..40073dc7c2 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017, 2018 New Vector Ltd # diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 9863953f5c..5234e3f81e 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index f9032e3697..c8b52cbc7a 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/events/builder.py b/synapse/events/builder.py index c1c0426f6e..5793553a88 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/events/presence_router.py b/synapse/events/presence_router.py index 24cd389d80..6c37c8a7a4 100644 --- a/synapse/events/presence_router.py +++ b/synapse/events/presence_router.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/events/snapshot.py b/synapse/events/snapshot.py index 7295df74fe..f8d898c3b1 100644 --- a/synapse/events/snapshot.py +++ b/synapse/events/snapshot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index a9185987a2..c727b48c1e 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 9767d23940..f7944fd834 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/events/utils.py b/synapse/events/utils.py index 0f8a3b5ad8..7d7cd9aaee 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/events/validator.py b/synapse/events/validator.py index f8f3b1a31e..fa6987d7cb 100644 --- a/synapse/events/validator.py +++ b/synapse/events/validator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/__init__.py b/synapse/federation/__init__.py index f5f0bdfca3..46300cba25 100644 --- a/synapse/federation/__init__.py +++ b/synapse/federation/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 383737520a..949dcd4614 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 55533d7501..f93335edaa 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index b9f8d966a6..3ff6479cfb 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # Copyright 2019 Matrix.org Federation C.I.C diff --git a/synapse/federation/persistence.py b/synapse/federation/persistence.py index ce5fc758f0..2f9c9bc2cd 100644 --- a/synapse/federation/persistence.py +++ b/synapse/federation/persistence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index 0c18c49abb..e3f0bc2471 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index d821dcbf6a..155161685d 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index e9c8a9f20a..3b053ebcfb 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/federation/sender/transaction_manager.py b/synapse/federation/sender/transaction_manager.py index 07b740c2f2..12fe3a719b 100644 --- a/synapse/federation/sender/transaction_manager.py +++ b/synapse/federation/sender/transaction_manager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/transport/__init__.py b/synapse/federation/transport/__init__.py index 5db733af98..3c9a0f6944 100644 --- a/synapse/federation/transport/__init__.py +++ b/synapse/federation/transport/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 6aee47c431..ada322a81e 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index a9c1391d27..a3759bdda1 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/federation/units.py b/synapse/federation/units.py index 0f8bf000ac..c83a261918 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/groups/attestations.py b/synapse/groups/attestations.py index 368c44708d..d2fc8be5f5 100644 --- a/synapse/groups/attestations.py +++ b/synapse/groups/attestations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py index 4b16a4ac29..a06d060ebf 100644 --- a/synapse/groups/groups_server.py +++ b/synapse/groups/groups_server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2018 New Vector Ltd # Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index fb899aa90d..d800e16912 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/account_data.py b/synapse/handlers/account_data.py index 1ce6d697ed..affb54e0ee 100644 --- a/synapse/handlers/account_data.py +++ b/synapse/handlers/account_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index bee1447c2e..66ce7e8b83 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py index 2a25af6288..16ab93f580 100644 --- a/synapse/handlers/acme.py +++ b/synapse/handlers/acme.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/acme_issuing_service.py b/synapse/handlers/acme_issuing_service.py index ae2a9dd9c2..a972d3fa0a 100644 --- a/synapse/handlers/acme_issuing_service.py +++ b/synapse/handlers/acme_issuing_service.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index c494de49a3..f72ded038e 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py index 9fb7ee335d..d7bc4e23ed 100644 --- a/synapse/handlers/appservice.py +++ b/synapse/handlers/appservice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 08e413bc98..b8a37b6477 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2019 - 2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index 5060936f94..7346ccfe93 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index 2bcd8f5435..3f6f9f7f3d 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017, 2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 7e76db3e2a..d75edb184b 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # Copyright 2019,2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index c971eeb4d2..c5d631de07 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index abcf86352d..90932316f3 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 92b18378fc..974487800d 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018-2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/handlers/e2e_room_keys.py b/synapse/handlers/e2e_room_keys.py index a910d246d6..31742236a9 100644 --- a/synapse/handlers/e2e_room_keys.py +++ b/synapse/handlers/e2e_room_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017, 2018 New Vector Ltd # Copyright 2019 Matrix.org Foundation C.I.C. # diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index f46cab7325..d82144d7fa 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 67888898ff..fe1d83f6b8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/handlers/groups_local.py b/synapse/handlers/groups_local.py index a41ca5df9c..157f2ff218 100644 --- a/synapse/handlers/groups_local.py +++ b/synapse/handlers/groups_local.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index d89fa5fb30..87a8b89237 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018 New Vector Ltd diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 13f8152283..76242865ae 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 125dae6d25..ec8eb21674 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/handlers/oidc_handler.py b/synapse/handlers/oidc_handler.py index 6624212d6f..b156196a70 100644 --- a/synapse/handlers/oidc_handler.py +++ b/synapse/handlers/oidc_handler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Quentin Gliech # Copyright 2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 66dc886c81..1e1186c29e 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # Copyright 2017 - 2018 New Vector Ltd # diff --git a/synapse/handlers/password_policy.py b/synapse/handlers/password_policy.py index 92cefa11aa..cd21efdcc6 100644 --- a/synapse/handlers/password_policy.py +++ b/synapse/handlers/password_policy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 0047907cd9..251b48148d 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index a755363c3f..05b4a97b59 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/read_marker.py b/synapse/handlers/read_marker.py index a54fe1968e..c679a8303e 100644 --- a/synapse/handlers/read_marker.py +++ b/synapse/handlers/read_marker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index dbfe9bfaca..f782d9db32 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 3b6660c873..007fb12840 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 4b3d0d72e3..5a888b7941 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # Copyright 2018-2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index 924b81db7c..141c9c0444 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 894ef859f4..2bbfac6471 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016-2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/room_member_worker.py b/synapse/handlers/room_member_worker.py index 3a90fc0c16..3e89dd2315 100644 --- a/synapse/handlers/room_member_worker.py +++ b/synapse/handlers/room_member_worker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index ec2ba11c75..80ba65b9e0 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index d742dfbd53..4e718d3f63 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/set_password.py b/synapse/handlers/set_password.py index f98a338ec5..a63fac8283 100644 --- a/synapse/handlers/set_password.py +++ b/synapse/handlers/set_password.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 5d9418969d..01e3e050f9 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py index 415b1c2d17..8d00ffdc73 100644 --- a/synapse/handlers/sso.py +++ b/synapse/handlers/sso.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/state_deltas.py b/synapse/handlers/state_deltas.py index ee8f87e59a..077c7c0649 100644 --- a/synapse/handlers/state_deltas.py +++ b/synapse/handlers/state_deltas.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/stats.py b/synapse/handlers/stats.py index 8730f99d03..383e34026e 100644 --- a/synapse/handlers/stats.py +++ b/synapse/handlers/stats.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index f8d88ef77b..dc8ee8cd17 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018, 2019 New Vector Ltd # diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index bb35af099d..e22393adc4 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/ui_auth/__init__.py b/synapse/handlers/ui_auth/__init__.py index a68d5e790e..4c3b669fae 100644 --- a/synapse/handlers/ui_auth/__init__.py +++ b/synapse/handlers/ui_auth/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/ui_auth/checkers.py b/synapse/handlers/ui_auth/checkers.py index 3d66bf305e..0eeb7c03f2 100644 --- a/synapse/handlers/ui_auth/checkers.py +++ b/synapse/handlers/ui_auth/checkers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py index b121286d95..9b1e6d5c18 100644 --- a/synapse/handlers/user_directory.py +++ b/synapse/handlers/user_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/http/__init__.py b/synapse/http/__init__.py index 142b007d01..ed4671b7de 100644 --- a/synapse/http/__init__.py +++ b/synapse/http/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/http/additional_resource.py b/synapse/http/additional_resource.py index 479746c9c5..55ea97a07f 100644 --- a/synapse/http/additional_resource.py +++ b/synapse/http/additional_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/http/client.py b/synapse/http/client.py index f7a07f0466..1730187ffa 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/http/connectproxyclient.py b/synapse/http/connectproxyclient.py index b797e3ce80..17e1c5abb1 100644 --- a/synapse/http/connectproxyclient.py +++ b/synapse/http/connectproxyclient.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/http/federation/__init__.py b/synapse/http/federation/__init__.py index 1453d04571..743fb9904a 100644 --- a/synapse/http/federation/__init__.py +++ b/synapse/http/federation/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/http/federation/matrix_federation_agent.py b/synapse/http/federation/matrix_federation_agent.py index 5935a125fd..950770201a 100644 --- a/synapse/http/federation/matrix_federation_agent.py +++ b/synapse/http/federation/matrix_federation_agent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/http/federation/srv_resolver.py b/synapse/http/federation/srv_resolver.py index d9620032d2..b8ed4ec905 100644 --- a/synapse/http/federation/srv_resolver.py +++ b/synapse/http/federation/srv_resolver.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/http/federation/well_known_resolver.py b/synapse/http/federation/well_known_resolver.py index ce4079f15c..20d39a4ea6 100644 --- a/synapse/http/federation/well_known_resolver.py +++ b/synapse/http/federation/well_known_resolver.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index ab47dec8f2..d48721a4e2 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py index ea5ad14cb0..7dfae8b786 100644 --- a/synapse/http/proxyagent.py +++ b/synapse/http/proxyagent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/http/request_metrics.py b/synapse/http/request_metrics.py index 0ec5d941b8..602f93c497 100644 --- a/synapse/http/request_metrics.py +++ b/synapse/http/request_metrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/http/server.py b/synapse/http/server.py index fa89260850..845651e606 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 0e637f4701..31897546a9 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/__init__.py b/synapse/logging/__init__.py index b28b7b2ef7..e00969f8b1 100644 --- a/synapse/logging/__init__.py +++ b/synapse/logging/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/_remote.py b/synapse/logging/_remote.py index 643492ceaf..4e8b0f8d10 100644 --- a/synapse/logging/_remote.py +++ b/synapse/logging/_remote.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/_structured.py b/synapse/logging/_structured.py index 3e054f615c..c7a971a9d6 100644 --- a/synapse/logging/_structured.py +++ b/synapse/logging/_structured.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/_terse_json.py b/synapse/logging/_terse_json.py index 2fbf5549a1..8002a250a2 100644 --- a/synapse/logging/_terse_json.py +++ b/synapse/logging/_terse_json.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/filter.py b/synapse/logging/filter.py index 1baf8dd679..ed51a4726c 100644 --- a/synapse/logging/filter.py +++ b/synapse/logging/filter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/formatter.py b/synapse/logging/formatter.py index 11f60a77f7..c0f12ecd15 100644 --- a/synapse/logging/formatter.py +++ b/synapse/logging/formatter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index bfe9136fd8..fba2fa3904 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/scopecontextmanager.py b/synapse/logging/scopecontextmanager.py index 7b9c657456..b1e8e08fe9 100644 --- a/synapse/logging/scopecontextmanager.py +++ b/synapse/logging/scopecontextmanager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/logging/utils.py b/synapse/logging/utils.py index fd3543ab04..08895e72ee 100644 --- a/synapse/logging/utils.py +++ b/synapse/logging/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index 13a5bc4558..31b7b3c256 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/metrics/_exposition.py b/synapse/metrics/_exposition.py index 71320a1402..8002be56e0 100644 --- a/synapse/metrics/_exposition.py +++ b/synapse/metrics/_exposition.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015-2019 Prometheus Python Client Developers # Copyright 2019 Matrix.org Foundation C.I.C. # diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index e8a9096c03..cbd0894e57 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index ca1bd4cdc9..b7dbbfc27c 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/module_api/errors.py b/synapse/module_api/errors.py index b15441772c..d24864c549 100644 --- a/synapse/module_api/errors.py +++ b/synapse/module_api/errors.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/notifier.py b/synapse/notifier.py index 7ce34380af..d5ab77058d 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/__init__.py b/synapse/push/__init__.py index 9fc3da49a2..2c23afe8e3 100644 --- a/synapse/push/__init__.py +++ b/synapse/push/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/action_generator.py b/synapse/push/action_generator.py index 38a47a600f..60758df016 100644 --- a/synapse/push/action_generator.py +++ b/synapse/push/action_generator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 1897f59153..50b470c310 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 OpenMarket Ltd # Copyright 2017 New Vector Ltd # diff --git a/synapse/push/clientformat.py b/synapse/push/clientformat.py index 0cadba761a..2ee0ccd58a 100644 --- a/synapse/push/clientformat.py +++ b/synapse/push/clientformat.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index c0968dc7a1..cd89b54305 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 26af5309c1..06bf5f8ada 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 New Vector Ltd # diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 2e5161de2c..c4b43b0d3f 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/presentable_names.py b/synapse/push/presentable_names.py index 04c2c1482c..412941393f 100644 --- a/synapse/push/presentable_names.py +++ b/synapse/push/presentable_names.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/push_rule_evaluator.py b/synapse/push/push_rule_evaluator.py index ba1877adcd..49ecb38522 100644 --- a/synapse/push/push_rule_evaluator.py +++ b/synapse/push/push_rule_evaluator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 New Vector Ltd # diff --git a/synapse/push/push_tools.py b/synapse/push/push_tools.py index df34103224..9c85200c0f 100644 --- a/synapse/push/push_tools.py +++ b/synapse/push/push_tools.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index cb94127850..c51938b8cf 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 4c7f5fecee..564a5ed0df 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/__init__.py b/synapse/replication/__init__.py index b7df13c9ee..f43a360a80 100644 --- a/synapse/replication/__init__.py +++ b/synapse/replication/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/__init__.py b/synapse/replication/http/__init__.py index cb4a52dbe9..ba8114ac9e 100644 --- a/synapse/replication/http/__init__.py +++ b/synapse/replication/http/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index b7aa0c280f..ece03467b5 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/account_data.py b/synapse/replication/http/account_data.py index 60899b6ad6..70e951af63 100644 --- a/synapse/replication/http/account_data.py +++ b/synapse/replication/http/account_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/devices.py b/synapse/replication/http/devices.py index 807b85d2e1..5a5818ef61 100644 --- a/synapse/replication/http/devices.py +++ b/synapse/replication/http/devices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/federation.py b/synapse/replication/http/federation.py index 82ea3b895f..79cadb7b57 100644 --- a/synapse/replication/http/federation.py +++ b/synapse/replication/http/federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/login.py b/synapse/replication/http/login.py index 4ec1bfa6ea..c2e8c00293 100644 --- a/synapse/replication/http/login.py +++ b/synapse/replication/http/login.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/membership.py b/synapse/replication/http/membership.py index c10992ff51..289a397d68 100644 --- a/synapse/replication/http/membership.py +++ b/synapse/replication/http/membership.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/presence.py b/synapse/replication/http/presence.py index bc9aa82cb4..f25307620d 100644 --- a/synapse/replication/http/presence.py +++ b/synapse/replication/http/presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/push.py b/synapse/replication/http/push.py index 054ed64d34..139427cb1f 100644 --- a/synapse/replication/http/push.py +++ b/synapse/replication/http/push.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/register.py b/synapse/replication/http/register.py index 73d7477854..d6dd7242eb 100644 --- a/synapse/replication/http/register.py +++ b/synapse/replication/http/register.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/send_event.py b/synapse/replication/http/send_event.py index a4c5b44292..fae5ffa451 100644 --- a/synapse/replication/http/send_event.py +++ b/synapse/replication/http/send_event.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/http/streams.py b/synapse/replication/http/streams.py index 309159e304..9afa147d00 100644 --- a/synapse/replication/http/streams.py +++ b/synapse/replication/http/streams.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/__init__.py b/synapse/replication/slave/__init__.py index b7df13c9ee..f43a360a80 100644 --- a/synapse/replication/slave/__init__.py +++ b/synapse/replication/slave/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/__init__.py b/synapse/replication/slave/storage/__init__.py index b7df13c9ee..f43a360a80 100644 --- a/synapse/replication/slave/storage/__init__.py +++ b/synapse/replication/slave/storage/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index 693c9ab901..faa99387a7 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/_slaved_id_tracker.py b/synapse/replication/slave/storage/_slaved_id_tracker.py index 0d39a93ed2..2cb7489047 100644 --- a/synapse/replication/slave/storage/_slaved_id_tracker.py +++ b/synapse/replication/slave/storage/_slaved_id_tracker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/account_data.py b/synapse/replication/slave/storage/account_data.py index 21afe5f155..ee74ee7d85 100644 --- a/synapse/replication/slave/storage/account_data.py +++ b/synapse/replication/slave/storage/account_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/replication/slave/storage/appservice.py b/synapse/replication/slave/storage/appservice.py index 0f8d7037bd..29f50c0add 100644 --- a/synapse/replication/slave/storage/appservice.py +++ b/synapse/replication/slave/storage/appservice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/replication/slave/storage/client_ips.py b/synapse/replication/slave/storage/client_ips.py index 0f5b7adef7..8730966380 100644 --- a/synapse/replication/slave/storage/client_ips.py +++ b/synapse/replication/slave/storage/client_ips.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/deviceinbox.py b/synapse/replication/slave/storage/deviceinbox.py index 1260f6d141..e940751084 100644 --- a/synapse/replication/slave/storage/deviceinbox.py +++ b/synapse/replication/slave/storage/deviceinbox.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index e0d86240dd..70207420a6 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/directory.py b/synapse/replication/slave/storage/directory.py index 1945bcf9a8..71fde0c96c 100644 --- a/synapse/replication/slave/storage/directory.py +++ b/synapse/replication/slave/storage/directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/events.py b/synapse/replication/slave/storage/events.py index fbffe6d85c..d4d3f8c448 100644 --- a/synapse/replication/slave/storage/events.py +++ b/synapse/replication/slave/storage/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/replication/slave/storage/filtering.py b/synapse/replication/slave/storage/filtering.py index 6a23252861..37875bc973 100644 --- a/synapse/replication/slave/storage/filtering.py +++ b/synapse/replication/slave/storage/filtering.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/groups.py b/synapse/replication/slave/storage/groups.py index 30955bcbfe..e9bdc38470 100644 --- a/synapse/replication/slave/storage/groups.py +++ b/synapse/replication/slave/storage/groups.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/keys.py b/synapse/replication/slave/storage/keys.py index 961579751c..a00b38c512 100644 --- a/synapse/replication/slave/storage/keys.py +++ b/synapse/replication/slave/storage/keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/presence.py b/synapse/replication/slave/storage/presence.py index 55620c03d8..57327d910d 100644 --- a/synapse/replication/slave/storage/presence.py +++ b/synapse/replication/slave/storage/presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/profile.py b/synapse/replication/slave/storage/profile.py index f85b20a071..99f4a22642 100644 --- a/synapse/replication/slave/storage/profile.py +++ b/synapse/replication/slave/storage/profile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/push_rule.py b/synapse/replication/slave/storage/push_rule.py index de904c943c..4d5f862862 100644 --- a/synapse/replication/slave/storage/push_rule.py +++ b/synapse/replication/slave/storage/push_rule.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/replication/slave/storage/pushers.py b/synapse/replication/slave/storage/pushers.py index 93161c3dfb..2672a2c94b 100644 --- a/synapse/replication/slave/storage/pushers.py +++ b/synapse/replication/slave/storage/pushers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/replication/slave/storage/receipts.py b/synapse/replication/slave/storage/receipts.py index 3dfdd9961d..3826b87dec 100644 --- a/synapse/replication/slave/storage/receipts.py +++ b/synapse/replication/slave/storage/receipts.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/replication/slave/storage/registration.py b/synapse/replication/slave/storage/registration.py index a40f064e2b..5dae35a960 100644 --- a/synapse/replication/slave/storage/registration.py +++ b/synapse/replication/slave/storage/registration.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/room.py b/synapse/replication/slave/storage/room.py index 109ac6bea1..8cc6de3f46 100644 --- a/synapse/replication/slave/storage/room.py +++ b/synapse/replication/slave/storage/room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/slave/storage/transactions.py b/synapse/replication/slave/storage/transactions.py index 2091ac0df6..a59e543924 100644 --- a/synapse/replication/slave/storage/transactions.py +++ b/synapse/replication/slave/storage/transactions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/__init__.py b/synapse/replication/tcp/__init__.py index 1b8718b11d..1fa60af8e6 100644 --- a/synapse/replication/tcp/__init__.py +++ b/synapse/replication/tcp/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 3455839d67..ced69ee904 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index 8abed1f52d..505d450e19 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/external_cache.py b/synapse/replication/tcp/external_cache.py index d89a36f25a..1a3b051e3c 100644 --- a/synapse/replication/tcp/external_cache.py +++ b/synapse/replication/tcp/external_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index a8894beadf..2ce1b9f222 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index d10d574246..6860576e78 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/redis.py b/synapse/replication/tcp/redis.py index 98bdeb0ec6..6a2c2655e4 100644 --- a/synapse/replication/tcp/redis.py +++ b/synapse/replication/tcp/redis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/resource.py b/synapse/replication/tcp/resource.py index 2018f9f29e..bd47d84258 100644 --- a/synapse/replication/tcp/resource.py +++ b/synapse/replication/tcp/resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/replication/tcp/streams/__init__.py b/synapse/replication/tcp/streams/__init__.py index d1a61c3314..fb74ac4e98 100644 --- a/synapse/replication/tcp/streams/__init__.py +++ b/synapse/replication/tcp/streams/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 3dfee76743..520c45f151 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/replication/tcp/streams/events.py b/synapse/replication/tcp/streams/events.py index fa5e37ba7b..e7e87bac92 100644 --- a/synapse/replication/tcp/streams/events.py +++ b/synapse/replication/tcp/streams/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/replication/tcp/streams/federation.py b/synapse/replication/tcp/streams/federation.py index 9bb8e9e177..096a85d363 100644 --- a/synapse/replication/tcp/streams/federation.py +++ b/synapse/replication/tcp/streams/federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 40f5c32db2..79d52d2dcb 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 2dec818a5f..9cb9a9f6aa 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018-2019 New Vector Ltd # Copyright 2020, 2021 The Matrix.org Foundation C.I.C. diff --git a/synapse/rest/admin/_base.py b/synapse/rest/admin/_base.py index 7681e55b58..f203f6fdc6 100644 --- a/synapse/rest/admin/_base.py +++ b/synapse/rest/admin/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/devices.py b/synapse/rest/admin/devices.py index 5996de11c3..5715190a78 100644 --- a/synapse/rest/admin/devices.py +++ b/synapse/rest/admin/devices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/event_reports.py b/synapse/rest/admin/event_reports.py index 381c3fe685..bbfcaf723b 100644 --- a/synapse/rest/admin/event_reports.py +++ b/synapse/rest/admin/event_reports.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/groups.py b/synapse/rest/admin/groups.py index ebc587aa06..3b3ffde0b6 100644 --- a/synapse/rest/admin/groups.py +++ b/synapse/rest/admin/groups.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index 40646ef241..24dd46113a 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018-2019 New Vector Ltd # diff --git a/synapse/rest/admin/purge_room_servlet.py b/synapse/rest/admin/purge_room_servlet.py index 49966ee3e0..2365ff7a0f 100644 --- a/synapse/rest/admin/purge_room_servlet.py +++ b/synapse/rest/admin/purge_room_servlet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index cfe1bebb91..d0cf121743 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/server_notice_servlet.py b/synapse/rest/admin/server_notice_servlet.py index f495666f4a..cc3ab5854b 100644 --- a/synapse/rest/admin/server_notice_servlet.py +++ b/synapse/rest/admin/server_notice_servlet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/statistics.py b/synapse/rest/admin/statistics.py index f2490e382d..948de94ccd 100644 --- a/synapse/rest/admin/statistics.py +++ b/synapse/rest/admin/statistics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 04990c71fb..edda7861fa 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/__init__.py b/synapse/rest/client/__init__.py index fe0ac3f8e9..629e2df74a 100644 --- a/synapse/rest/client/__init__.py +++ b/synapse/rest/client/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/transactions.py b/synapse/rest/client/transactions.py index 7be5c0fb88..94ff3719ce 100644 --- a/synapse/rest/client/transactions.py +++ b/synapse/rest/client/transactions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/__init__.py b/synapse/rest/client/v1/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/synapse/rest/client/v1/__init__.py +++ b/synapse/rest/client/v1/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/v1/directory.py index e5af26b176..ae92a3df8e 100644 --- a/synapse/rest/client/v1/directory.py +++ b/synapse/rest/client/v1/directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/v1/events.py index 6de4078290..ee7454996e 100644 --- a/synapse/rest/client/v1/events.py +++ b/synapse/rest/client/v1/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/initial_sync.py b/synapse/rest/client/v1/initial_sync.py index 91da0ee573..bef1edc838 100644 --- a/synapse/rest/client/v1/initial_sync.py +++ b/synapse/rest/client/v1/initial_sync.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 3151e72d4f..42e709ec14 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/logout.py b/synapse/rest/client/v1/logout.py index ad8cea49c6..5aa7908d73 100644 --- a/synapse/rest/client/v1/logout.py +++ b/synapse/rest/client/v1/logout.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py index 23a529f8e3..c232484f29 100644 --- a/synapse/rest/client/v1/presence.py +++ b/synapse/rest/client/v1/presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/v1/profile.py index 717c5f2b10..f42f4b3567 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/v1/profile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/v1/push_rule.py index 241e535917..be29a0b39e 100644 --- a/synapse/rest/client/v1/push_rule.py +++ b/synapse/rest/client/v1/push_rule.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index 0c148a213d..18102eca6c 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 525efdf221..5cab4d3c7b 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/rest/client/v1/voip.py b/synapse/rest/client/v1/voip.py index d07ca2c47c..c780ffded5 100644 --- a/synapse/rest/client/v1/voip.py +++ b/synapse/rest/client/v1/voip.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/synapse/rest/client/v2_alpha/__init__.py +++ b/synapse/rest/client/v2_alpha/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index f016b4f1bd..0443f4571c 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 411fb57c47..3aad15132d 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018 New Vector Ltd diff --git a/synapse/rest/client/v2_alpha/account_data.py b/synapse/rest/client/v2_alpha/account_data.py index 3f28c0bc3e..7517e9304e 100644 --- a/synapse/rest/client/v2_alpha/account_data.py +++ b/synapse/rest/client/v2_alpha/account_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/account_validity.py b/synapse/rest/client/v2_alpha/account_validity.py index bd7f9ae203..0ad07fb895 100644 --- a/synapse/rest/client/v2_alpha/account_validity.py +++ b/synapse/rest/client/v2_alpha/account_validity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 75ece1c911..6ea1b50a62 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/capabilities.py b/synapse/rest/client/v2_alpha/capabilities.py index 44ccf10ed4..6a24021484 100644 --- a/synapse/rest/client/v2_alpha/capabilities.py +++ b/synapse/rest/client/v2_alpha/capabilities.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/v2_alpha/devices.py index 3d07aadd39..9af05f9b11 100644 --- a/synapse/rest/client/v2_alpha/devices.py +++ b/synapse/rest/client/v2_alpha/devices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/v2_alpha/filter.py index 7cc692643b..411667a9c8 100644 --- a/synapse/rest/client/v2_alpha/filter.py +++ b/synapse/rest/client/v2_alpha/filter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/groups.py b/synapse/rest/client/v2_alpha/groups.py index 08fb6b2b06..6285680c00 100644 --- a/synapse/rest/client/v2_alpha/groups.py +++ b/synapse/rest/client/v2_alpha/groups.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index f092e5b3a2..a57ccbb5e5 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/rest/client/v2_alpha/notifications.py b/synapse/rest/client/v2_alpha/notifications.py index 87063ec8b1..0ede643c2d 100644 --- a/synapse/rest/client/v2_alpha/notifications.py +++ b/synapse/rest/client/v2_alpha/notifications.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/openid.py b/synapse/rest/client/v2_alpha/openid.py index 5b996e2d63..d3322acc38 100644 --- a/synapse/rest/client/v2_alpha/openid.py +++ b/synapse/rest/client/v2_alpha/openid.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/password_policy.py b/synapse/rest/client/v2_alpha/password_policy.py index 68b27ff23a..a83927aee6 100644 --- a/synapse/rest/client/v2_alpha/password_policy.py +++ b/synapse/rest/client/v2_alpha/password_policy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index 55c6688f52..5988fa47e5 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 6f7246a394..8cf4aebdbe 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 4a064849c1..b26aad7b34 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 - 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # diff --git a/synapse/rest/client/v2_alpha/relations.py b/synapse/rest/client/v2_alpha/relations.py index fe765da23c..c7da6759db 100644 --- a/synapse/rest/client/v2_alpha/relations.py +++ b/synapse/rest/client/v2_alpha/relations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/report_event.py b/synapse/rest/client/v2_alpha/report_event.py index 215d619ca1..2c169abbf3 100644 --- a/synapse/rest/client/v2_alpha/report_event.py +++ b/synapse/rest/client/v2_alpha/report_event.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/room_keys.py b/synapse/rest/client/v2_alpha/room_keys.py index 53de97923f..263596be86 100644 --- a/synapse/rest/client/v2_alpha/room_keys.py +++ b/synapse/rest/client/v2_alpha/room_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017, 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py index 147920767f..6d1b083acb 100644 --- a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py +++ b/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/sendtodevice.py b/synapse/rest/client/v2_alpha/sendtodevice.py index 79c1b526ee..f8dcee603c 100644 --- a/synapse/rest/client/v2_alpha/sendtodevice.py +++ b/synapse/rest/client/v2_alpha/sendtodevice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/shared_rooms.py b/synapse/rest/client/v2_alpha/shared_rooms.py index c866d5151c..d2e7f04b40 100644 --- a/synapse/rest/client/v2_alpha/shared_rooms.py +++ b/synapse/rest/client/v2_alpha/shared_rooms.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Half-Shot # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index 3481770c83..95ee3f1b84 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/tags.py b/synapse/rest/client/v2_alpha/tags.py index a97cd66c52..c14f83be18 100644 --- a/synapse/rest/client/v2_alpha/tags.py +++ b/synapse/rest/client/v2_alpha/tags.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/thirdparty.py b/synapse/rest/client/v2_alpha/thirdparty.py index 0c127a1b5f..b5c67c9bb6 100644 --- a/synapse/rest/client/v2_alpha/thirdparty.py +++ b/synapse/rest/client/v2_alpha/thirdparty.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/tokenrefresh.py b/synapse/rest/client/v2_alpha/tokenrefresh.py index 79317c74ba..b2f858545c 100644 --- a/synapse/rest/client/v2_alpha/tokenrefresh.py +++ b/synapse/rest/client/v2_alpha/tokenrefresh.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/v2_alpha/user_directory.py b/synapse/rest/client/v2_alpha/user_directory.py index ad598cefe0..7e8912f0b9 100644 --- a/synapse/rest/client/v2_alpha/user_directory.py +++ b/synapse/rest/client/v2_alpha/user_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 3e3d8839f4..4582c274c7 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018-2019 New Vector Ltd diff --git a/synapse/rest/consent/consent_resource.py b/synapse/rest/consent/consent_resource.py index 8b9ef26cf2..c4550d3cf0 100644 --- a/synapse/rest/consent/consent_resource.py +++ b/synapse/rest/consent/consent_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/health.py b/synapse/rest/health.py index 0170950bf3..4487b54abf 100644 --- a/synapse/rest/health.py +++ b/synapse/rest/health.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/key/__init__.py b/synapse/rest/key/__init__.py index fe0ac3f8e9..629e2df74a 100644 --- a/synapse/rest/key/__init__.py +++ b/synapse/rest/key/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/key/v2/__init__.py b/synapse/rest/key/v2/__init__.py index cb5abcf826..c6c63073ea 100644 --- a/synapse/rest/key/v2/__init__.py +++ b/synapse/rest/key/v2/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/key/v2/local_key_resource.py b/synapse/rest/key/v2/local_key_resource.py index d8e8e48c1c..e8dbe240d8 100644 --- a/synapse/rest/key/v2/local_key_resource.py +++ b/synapse/rest/key/v2/local_key_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/media/v1/__init__.py b/synapse/rest/media/v1/__init__.py index 3b8c96e267..d20186bbd0 100644 --- a/synapse/rest/media/v1/__init__.py +++ b/synapse/rest/media/v1/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/media/v1/_base.py b/synapse/rest/media/v1/_base.py index 6366947071..0fb4cd81f1 100644 --- a/synapse/rest/media/v1/_base.py +++ b/synapse/rest/media/v1/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/config_resource.py b/synapse/rest/media/v1/config_resource.py index c41a7ab412..b20c29f007 100644 --- a/synapse/rest/media/v1/config_resource.py +++ b/synapse/rest/media/v1/config_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 Will Hunt # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/download_resource.py b/synapse/rest/media/v1/download_resource.py index 5dadaeaf57..cd2468f9c5 100644 --- a/synapse/rest/media/v1/download_resource.py +++ b/synapse/rest/media/v1/download_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/filepath.py b/synapse/rest/media/v1/filepath.py index 7792f26e78..4088e7a059 100644 --- a/synapse/rest/media/v1/filepath.py +++ b/synapse/rest/media/v1/filepath.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index 0c041b542d..87e3645ddc 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/media_storage.py b/synapse/rest/media/v1/media_storage.py index b1b1c9e6ec..c7fd97c46c 100644 --- a/synapse/rest/media/v1/media_storage.py +++ b/synapse/rest/media/v1/media_storage.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 814145a04a..0adfb1a70f 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/storage_provider.py b/synapse/rest/media/v1/storage_provider.py index 031947557d..0ff6ad3c0c 100644 --- a/synapse/rest/media/v1/storage_provider.py +++ b/synapse/rest/media/v1/storage_provider.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/media/v1/thumbnail_resource.py b/synapse/rest/media/v1/thumbnail_resource.py index af802bc0b1..a029d426f0 100644 --- a/synapse/rest/media/v1/thumbnail_resource.py +++ b/synapse/rest/media/v1/thumbnail_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/thumbnailer.py b/synapse/rest/media/v1/thumbnailer.py index 988f52c78f..37fe582390 100644 --- a/synapse/rest/media/v1/thumbnailer.py +++ b/synapse/rest/media/v1/thumbnailer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/media/v1/upload_resource.py b/synapse/rest/media/v1/upload_resource.py index 0138b2e2d1..80f017a4dd 100644 --- a/synapse/rest/media/v1/upload_resource.py +++ b/synapse/rest/media/v1/upload_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/rest/synapse/__init__.py b/synapse/rest/synapse/__init__.py index c0b733488b..6ef4fbe8f7 100644 --- a/synapse/rest/synapse/__init__.py +++ b/synapse/rest/synapse/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/__init__.py b/synapse/rest/synapse/client/__init__.py index 9eeb970580..47a2f72b32 100644 --- a/synapse/rest/synapse/client/__init__.py +++ b/synapse/rest/synapse/client/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/new_user_consent.py b/synapse/rest/synapse/client/new_user_consent.py index 78ee0b5e88..e5634f9679 100644 --- a/synapse/rest/synapse/client/new_user_consent.py +++ b/synapse/rest/synapse/client/new_user_consent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/oidc/__init__.py b/synapse/rest/synapse/client/oidc/__init__.py index 64c0deb75d..36ba401656 100644 --- a/synapse/rest/synapse/client/oidc/__init__.py +++ b/synapse/rest/synapse/client/oidc/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Quentin Gliech # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/oidc/callback_resource.py b/synapse/rest/synapse/client/oidc/callback_resource.py index 1af33f0a45..7785f17e90 100644 --- a/synapse/rest/synapse/client/oidc/callback_resource.py +++ b/synapse/rest/synapse/client/oidc/callback_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Quentin Gliech # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/password_reset.py b/synapse/rest/synapse/client/password_reset.py index d26ce46efc..f2800bf2db 100644 --- a/synapse/rest/synapse/client/password_reset.py +++ b/synapse/rest/synapse/client/password_reset.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/pick_idp.py b/synapse/rest/synapse/client/pick_idp.py index 9550b82998..d3a94a9349 100644 --- a/synapse/rest/synapse/client/pick_idp.py +++ b/synapse/rest/synapse/client/pick_idp.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/pick_username.py b/synapse/rest/synapse/client/pick_username.py index d9ffe84489..9b002cc15e 100644 --- a/synapse/rest/synapse/client/pick_username.py +++ b/synapse/rest/synapse/client/pick_username.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/saml2/__init__.py b/synapse/rest/synapse/client/saml2/__init__.py index 3e8235ee1e..781ccb237c 100644 --- a/synapse/rest/synapse/client/saml2/__init__.py +++ b/synapse/rest/synapse/client/saml2/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/saml2/metadata_resource.py b/synapse/rest/synapse/client/saml2/metadata_resource.py index 1e8526e22e..b37c7083dc 100644 --- a/synapse/rest/synapse/client/saml2/metadata_resource.py +++ b/synapse/rest/synapse/client/saml2/metadata_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/synapse/client/saml2/response_resource.py b/synapse/rest/synapse/client/saml2/response_resource.py index 4dfadf1bfb..774ccd870f 100644 --- a/synapse/rest/synapse/client/saml2/response_resource.py +++ b/synapse/rest/synapse/client/saml2/response_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Copyright 2018 New Vector Ltd # diff --git a/synapse/rest/synapse/client/sso_register.py b/synapse/rest/synapse/client/sso_register.py index f2acce2437..70cd148a76 100644 --- a/synapse/rest/synapse/client/sso_register.py +++ b/synapse/rest/synapse/client/sso_register.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/rest/well_known.py b/synapse/rest/well_known.py index f591cc6c5c..19ac3af337 100644 --- a/synapse/rest/well_known.py +++ b/synapse/rest/well_known.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/secrets.py b/synapse/secrets.py index 7939db75e7..bf829251fd 100644 --- a/synapse/secrets.py +++ b/synapse/secrets.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/server.py b/synapse/server.py index cfb55c230d..6c35ae6e50 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/server_notices/consent_server_notices.py b/synapse/server_notices/consent_server_notices.py index a9349bf9a1..e65f6f88fe 100644 --- a/synapse/server_notices/consent_server_notices.py +++ b/synapse/server_notices/consent_server_notices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/server_notices/resource_limits_server_notices.py b/synapse/server_notices/resource_limits_server_notices.py index a18a2e76c9..e4b0bc5c72 100644 --- a/synapse/server_notices/resource_limits_server_notices.py +++ b/synapse/server_notices/resource_limits_server_notices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/server_notices/server_notices_manager.py b/synapse/server_notices/server_notices_manager.py index 144e1da78e..f19075b760 100644 --- a/synapse/server_notices/server_notices_manager.py +++ b/synapse/server_notices/server_notices_manager.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/server_notices/server_notices_sender.py b/synapse/server_notices/server_notices_sender.py index 965c645889..c875b15b32 100644 --- a/synapse/server_notices/server_notices_sender.py +++ b/synapse/server_notices/server_notices_sender.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/server_notices/worker_server_notices_sender.py b/synapse/server_notices/worker_server_notices_sender.py index c76bd57460..cc53318491 100644 --- a/synapse/server_notices/worker_server_notices_sender.py +++ b/synapse/server_notices/worker_server_notices_sender.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/spam_checker_api/__init__.py b/synapse/spam_checker_api/__init__.py index 3ce25bb012..73018f2d00 100644 --- a/synapse/spam_checker_api/__init__.py +++ b/synapse/spam_checker_api/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index c0f79ffdc8..c7ee731154 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/state/v1.py b/synapse/state/v1.py index ce255da6fd..318e998813 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/state/v2.py b/synapse/state/v2.py index e73a548ee4..32671ddbde 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 0b9007e51f..105e4e1fec 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018,2019 New Vector Ltd # diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 240905329f..56dd3a4861 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index ccb06aab39..142787fdfd 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 77ef29ec71..9a6d2b21f9 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/databases/__init__.py b/synapse/storage/databases/__init__.py index 379c78bb83..20b755056b 100644 --- a/synapse/storage/databases/__init__.py +++ b/synapse/storage/databases/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index b3d16ca7ac..5c50f5f950 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # Copyright 2019-2021 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/databases/main/account_data.py b/synapse/storage/databases/main/account_data.py index a277a1ef13..1d02795f43 100644 --- a/synapse/storage/databases/main/account_data.py +++ b/synapse/storage/databases/main/account_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/appservice.py b/synapse/storage/databases/main/appservice.py index 85bb853d33..9f182c2a89 100644 --- a/synapse/storage/databases/main/appservice.py +++ b/synapse/storage/databases/main/appservice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/cache.py b/synapse/storage/databases/main/cache.py index 1e7637a6f5..ecc1f935e2 100644 --- a/synapse/storage/databases/main/cache.py +++ b/synapse/storage/databases/main/cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/censor_events.py b/synapse/storage/databases/main/censor_events.py index 3e26d5ba87..f22c1f241b 100644 --- a/synapse/storage/databases/main/censor_events.py +++ b/synapse/storage/databases/main/censor_events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/client_ips.py b/synapse/storage/databases/main/client_ips.py index ea3c15fd0e..d60010e942 100644 --- a/synapse/storage/databases/main/client_ips.py +++ b/synapse/storage/databases/main/client_ips.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py index 691080ce74..7c9d1f744e 100644 --- a/synapse/storage/databases/main/deviceinbox.py +++ b/synapse/storage/databases/main/deviceinbox.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index 9bf8ba888f..b204875580 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # Copyright 2019,2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/databases/main/directory.py b/synapse/storage/databases/main/directory.py index 267b948397..86075bc55b 100644 --- a/synapse/storage/databases/main/directory.py +++ b/synapse/storage/databases/main/directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/e2e_room_keys.py b/synapse/storage/databases/main/e2e_room_keys.py index 12cecceec2..b15fb71e62 100644 --- a/synapse/storage/databases/main/e2e_room_keys.py +++ b/synapse/storage/databases/main/e2e_room_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # Copyright 2019 Matrix.org Foundation C.I.C. # diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index f1e7859d26..88afe97c41 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # Copyright 2019,2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index a956be491a..32ce70a396 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/event_push_actions.py b/synapse/storage/databases/main/event_push_actions.py index 78245ad5bd..5845322118 100644 --- a/synapse/storage/databases/main/event_push_actions.py +++ b/synapse/storage/databases/main/event_push_actions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index ad17123915..bed4326d11 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018-2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index 79e7df6ca9..cbe4be1437 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/events_forward_extremities.py b/synapse/storage/databases/main/events_forward_extremities.py index b3703ae161..6d2688d711 100644 --- a/synapse/storage/databases/main/events_forward_extremities.py +++ b/synapse/storage/databases/main/events_forward_extremities.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index c00780969f..64d70785b8 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/filtering.py b/synapse/storage/databases/main/filtering.py index d2f5b9a502..bb244a03c0 100644 --- a/synapse/storage/databases/main/filtering.py +++ b/synapse/storage/databases/main/filtering.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/group_server.py b/synapse/storage/databases/main/group_server.py index bd7826f4e9..66ad363bfb 100644 --- a/synapse/storage/databases/main/group_server.py +++ b/synapse/storage/databases/main/group_server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/keys.py b/synapse/storage/databases/main/keys.py index d504323b03..0e86807834 100644 --- a/synapse/storage/databases/main/keys.py +++ b/synapse/storage/databases/main/keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd. # diff --git a/synapse/storage/databases/main/media_repository.py b/synapse/storage/databases/main/media_repository.py index b7820ac7ff..c584868188 100644 --- a/synapse/storage/databases/main/media_repository.py +++ b/synapse/storage/databases/main/media_repository.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020-2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/storage/databases/main/metrics.py b/synapse/storage/databases/main/metrics.py index 614a418a15..c3f551d377 100644 --- a/synapse/storage/databases/main/metrics.py +++ b/synapse/storage/databases/main/metrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/monthly_active_users.py b/synapse/storage/databases/main/monthly_active_users.py index 757da3d55d..fe25638289 100644 --- a/synapse/storage/databases/main/monthly_active_users.py +++ b/synapse/storage/databases/main/monthly_active_users.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/presence.py b/synapse/storage/databases/main/presence.py index 0ff693a310..c207d917b1 100644 --- a/synapse/storage/databases/main/presence.py +++ b/synapse/storage/databases/main/presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/profile.py b/synapse/storage/databases/main/profile.py index ba01d3108a..9b4e95e134 100644 --- a/synapse/storage/databases/main/profile.py +++ b/synapse/storage/databases/main/profile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/purge_events.py b/synapse/storage/databases/main/purge_events.py index 41f4fe7f95..8f83748b5e 100644 --- a/synapse/storage/databases/main/purge_events.py +++ b/synapse/storage/databases/main/purge_events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/push_rule.py b/synapse/storage/databases/main/push_rule.py index 9e58dc0e6a..db52176337 100644 --- a/synapse/storage/databases/main/push_rule.py +++ b/synapse/storage/databases/main/push_rule.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index c65558c280..b48fe086d4 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/receipts.py b/synapse/storage/databases/main/receipts.py index 43c852c96c..3647276acb 100644 --- a/synapse/storage/databases/main/receipts.py +++ b/synapse/storage/databases/main/receipts.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index 90a8f664ef..833214b7e0 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019,2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/databases/main/rejections.py b/synapse/storage/databases/main/rejections.py index 1e361aaa9a..167318b314 100644 --- a/synapse/storage/databases/main/rejections.py +++ b/synapse/storage/databases/main/rejections.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py index 5cd61547f7..2bbf6d6a95 100644 --- a/synapse/storage/databases/main/relations.py +++ b/synapse/storage/databases/main/relations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 47fb12f3f6..5f38634f48 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index a9216ca9ae..ef5587f87a 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/schema/delta/50/make_event_content_nullable.py b/synapse/storage/databases/main/schema/delta/50/make_event_content_nullable.py index b1684a8441..acd6ad1e1f 100644 --- a/synapse/storage/databases/main/schema/delta/50/make_event_content_nullable.py +++ b/synapse/storage/databases/main/schema/delta/50/make_event_content_nullable.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/schema/delta/57/local_current_membership.py b/synapse/storage/databases/main/schema/delta/57/local_current_membership.py index 44917f0a2e..66989222e6 100644 --- a/synapse/storage/databases/main/schema/delta/57/local_current_membership.py +++ b/synapse/storage/databases/main/schema/delta/57/local_current_membership.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/search.py b/synapse/storage/databases/main/search.py index f5e7d9ef98..0276f30656 100644 --- a/synapse/storage/databases/main/search.py +++ b/synapse/storage/databases/main/search.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/signatures.py b/synapse/storage/databases/main/signatures.py index c8c67953e4..ab2159c2d3 100644 --- a/synapse/storage/databases/main/signatures.py +++ b/synapse/storage/databases/main/signatures.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/state.py b/synapse/storage/databases/main/state.py index 93431efe00..1757064a68 100644 --- a/synapse/storage/databases/main/state.py +++ b/synapse/storage/databases/main/state.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/storage/databases/main/state_deltas.py b/synapse/storage/databases/main/state_deltas.py index 0dbb501f16..bff7d0404f 100644 --- a/synapse/storage/databases/main/state_deltas.py +++ b/synapse/storage/databases/main/state_deltas.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index bce8946c21..ae9f880965 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018, 2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/storage/databases/main/stream.py b/synapse/storage/databases/main/stream.py index 91f8abb67d..db5ce4ea01 100644 --- a/synapse/storage/databases/main/stream.py +++ b/synapse/storage/databases/main/stream.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018-2019 New Vector Ltd diff --git a/synapse/storage/databases/main/tags.py b/synapse/storage/databases/main/tags.py index 50067eabfc..1d62c6140f 100644 --- a/synapse/storage/databases/main/tags.py +++ b/synapse/storage/databases/main/tags.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/databases/main/transactions.py b/synapse/storage/databases/main/transactions.py index b7072f1f5e..82335e7a9d 100644 --- a/synapse/storage/databases/main/transactions.py +++ b/synapse/storage/databases/main/transactions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/ui_auth.py b/synapse/storage/databases/main/ui_auth.py index 5473ec1485..22c05cdde7 100644 --- a/synapse/storage/databases/main/ui_auth.py +++ b/synapse/storage/databases/main/ui_auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/user_directory.py b/synapse/storage/databases/main/user_directory.py index 1026f321e5..7a082fdd21 100644 --- a/synapse/storage/databases/main/user_directory.py +++ b/synapse/storage/databases/main/user_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/main/user_erasure_store.py b/synapse/storage/databases/main/user_erasure_store.py index f9575b1f1f..acf6b2fb64 100644 --- a/synapse/storage/databases/main/user_erasure_store.py +++ b/synapse/storage/databases/main/user_erasure_store.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/state/__init__.py b/synapse/storage/databases/state/__init__.py index c90d022899..e5100d6108 100644 --- a/synapse/storage/databases/state/__init__.py +++ b/synapse/storage/databases/state/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/state/bg_updates.py b/synapse/storage/databases/state/bg_updates.py index 75c09b3687..c2891cb07f 100644 --- a/synapse/storage/databases/state/bg_updates.py +++ b/synapse/storage/databases/state/bg_updates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/databases/state/store.py b/synapse/storage/databases/state/store.py index dfcf89d91c..e38461adbc 100644 --- a/synapse/storage/databases/state/store.py +++ b/synapse/storage/databases/state/store.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/engines/__init__.py b/synapse/storage/engines/__init__.py index d15ccfacde..9abc02046e 100644 --- a/synapse/storage/engines/__init__.py +++ b/synapse/storage/engines/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/engines/_base.py b/synapse/storage/engines/_base.py index 21db1645d3..1882bfd9cf 100644 --- a/synapse/storage/engines/_base.py +++ b/synapse/storage/engines/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index dba8cc51d3..21411c5fea 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index f4f16456f2..5fe1b205e1 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/keys.py b/synapse/storage/keys.py index c03871f393..540adb8781 100644 --- a/synapse/storage/keys.py +++ b/synapse/storage/keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd. # diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 3a0d6fb32e..87e040b014 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018-2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index c7f0b8ccb5..05a9355974 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/purge_events.py b/synapse/storage/purge_events.py index ad954990a7..30669beb7c 100644 --- a/synapse/storage/purge_events.py +++ b/synapse/storage/purge_events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/push_rule.py b/synapse/storage/push_rule.py index f47cec0d86..2d5c21ef72 100644 --- a/synapse/storage/push_rule.py +++ b/synapse/storage/push_rule.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/relations.py b/synapse/storage/relations.py index 2564f34b47..c552dbf04c 100644 --- a/synapse/storage/relations.py +++ b/synapse/storage/relations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index d2ff4da6b9..c34fbf21bc 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/storage/state.py b/synapse/storage/state.py index c1c147c62a..cfafba22c5 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/types.py b/synapse/storage/types.py index 17291c9d5e..57f4883bf4 100644 --- a/synapse/storage/types.py +++ b/synapse/storage/types.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/util/__init__.py b/synapse/storage/util/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/synapse/storage/util/__init__.py +++ b/synapse/storage/util/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/util/id_generators.py b/synapse/storage/util/id_generators.py index 32d6cc16b9..b1bd3a52d9 100644 --- a/synapse/storage/util/id_generators.py +++ b/synapse/storage/util/id_generators.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/storage/util/sequence.py b/synapse/storage/util/sequence.py index 36a67e7019..30b6b8e0ca 100644 --- a/synapse/storage/util/sequence.py +++ b/synapse/storage/util/sequence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/streams/__init__.py b/synapse/streams/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/synapse/streams/__init__.py +++ b/synapse/streams/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/streams/config.py b/synapse/streams/config.py index fdda21d165..13d300588b 100644 --- a/synapse/streams/config.py +++ b/synapse/streams/config.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 92fd5d489f..20fceaa935 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/types.py b/synapse/types.py index b08ce90140..21654ae686 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/synapse/util/__init__.py b/synapse/util/__init__.py index 517686f0a6..0f84fa3f4e 100644 --- a/synapse/util/__init__.py +++ b/synapse/util/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index c3b2d981ea..5c55bb0125 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/util/caches/__init__.py b/synapse/util/caches/__init__.py index 48f64eeb38..46af7fa473 100644 --- a/synapse/util/caches/__init__.py +++ b/synapse/util/caches/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2019, 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/util/caches/cached_call.py b/synapse/util/caches/cached_call.py index 3ee0f2317a..a301c9e89b 100644 --- a/synapse/util/caches/cached_call.py +++ b/synapse/util/caches/cached_call.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/caches/deferred_cache.py b/synapse/util/caches/deferred_cache.py index dd392cf694..484097a48a 100644 --- a/synapse/util/caches/deferred_cache.py +++ b/synapse/util/caches/deferred_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index 4e84379914..ac4a078b26 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synapse/util/caches/dictionary_cache.py b/synapse/util/caches/dictionary_cache.py index b3b413b02c..56d94d96ce 100644 --- a/synapse/util/caches/dictionary_cache.py +++ b/synapse/util/caches/dictionary_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/caches/expiringcache.py b/synapse/util/caches/expiringcache.py index 4dc3477e89..ac47a31cd7 100644 --- a/synapse/util/caches/expiringcache.py +++ b/synapse/util/caches/expiringcache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index 20c8e2d9f5..a21d34fcb4 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/caches/response_cache.py b/synapse/util/caches/response_cache.py index 46ea8e0964..2529845c9e 100644 --- a/synapse/util/caches/response_cache.py +++ b/synapse/util/caches/response_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/caches/stream_change_cache.py b/synapse/util/caches/stream_change_cache.py index 644e9e778a..0469e7d120 100644 --- a/synapse/util/caches/stream_change_cache.py +++ b/synapse/util/caches/stream_change_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/caches/ttlcache.py b/synapse/util/caches/ttlcache.py index 96a8274940..c276107d56 100644 --- a/synapse/util/caches/ttlcache.py +++ b/synapse/util/caches/ttlcache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/daemonize.py b/synapse/util/daemonize.py index 23393cf49b..31b24dd188 100644 --- a/synapse/util/daemonize.py +++ b/synapse/util/daemonize.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2012, 2013, 2014 Ilya Otyutskiy # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/util/distributor.py b/synapse/util/distributor.py index 3c47285d05..1f803aef6d 100644 --- a/synapse/util/distributor.py +++ b/synapse/util/distributor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/file_consumer.py b/synapse/util/file_consumer.py index 68dc632491..e946189f9a 100644 --- a/synapse/util/file_consumer.py +++ b/synapse/util/file_consumer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/frozenutils.py b/synapse/util/frozenutils.py index 5ca2e71e60..2ac7c2913c 100644 --- a/synapse/util/frozenutils.py +++ b/synapse/util/frozenutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/hash.py b/synapse/util/hash.py index 359168704e..ba676e1762 100644 --- a/synapse/util/hash.py +++ b/synapse/util/hash.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/iterutils.py b/synapse/util/iterutils.py index 98707c119d..6f73b1d56d 100644 --- a/synapse/util/iterutils.py +++ b/synapse/util/iterutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/util/jsonobject.py b/synapse/util/jsonobject.py index e3a8ed5b2f..abc12f0837 100644 --- a/synapse/util/jsonobject.py +++ b/synapse/util/jsonobject.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/macaroons.py b/synapse/util/macaroons.py index 12cdd53327..f6ebfd7e7d 100644 --- a/synapse/util/macaroons.py +++ b/synapse/util/macaroons.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Quentin Gliech # Copyright 2021 The Matrix.org Foundation C.I.C. # diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 1023c856d1..6ed7179e8c 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/module_loader.py b/synapse/util/module_loader.py index d184e2a90c..8acbe276e4 100644 --- a/synapse/util/module_loader.py +++ b/synapse/util/module_loader.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/msisdn.py b/synapse/util/msisdn.py index c8bcbe297a..bbbdebf264 100644 --- a/synapse/util/msisdn.py +++ b/synapse/util/msisdn.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/patch_inline_callbacks.py b/synapse/util/patch_inline_callbacks.py index d9f9ae99d6..eed0291cae 100644 --- a/synapse/util/patch_inline_callbacks.py +++ b/synapse/util/patch_inline_callbacks.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/ratelimitutils.py b/synapse/util/ratelimitutils.py index 70d11e1ec3..a654c69684 100644 --- a/synapse/util/ratelimitutils.py +++ b/synapse/util/ratelimitutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/retryutils.py b/synapse/util/retryutils.py index 4ab379e429..f9c370a814 100644 --- a/synapse/util/retryutils.py +++ b/synapse/util/retryutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/rlimit.py b/synapse/util/rlimit.py index 207cd17c2a..bf812ab516 100644 --- a/synapse/util/rlimit.py +++ b/synapse/util/rlimit.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index 9ce7873ab5..c0e6fb9a60 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/util/templates.py b/synapse/util/templates.py index 392dae4a40..38543dd1ea 100644 --- a/synapse/util/templates.py +++ b/synapse/util/templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/threepids.py b/synapse/util/threepids.py index 43c2e0ac23..281c5be4fb 100644 --- a/synapse/util/threepids.py +++ b/synapse/util/threepids.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/versionstring.py b/synapse/util/versionstring.py index ab7d03af3a..dfa30a6229 100644 --- a/synapse/util/versionstring.py +++ b/synapse/util/versionstring.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/wheel_timer.py b/synapse/util/wheel_timer.py index be3b22469d..61814aff24 100644 --- a/synapse/util/wheel_timer.py +++ b/synapse/util/wheel_timer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/visibility.py b/synapse/visibility.py index ff53a49b3a..490fb26e81 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 - 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synctl b/synctl index 56c0e3940f..ccf404accb 100755 --- a/synctl +++ b/synctl @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/synmark/__init__.py b/synmark/__init__.py index 3d4ec3e184..2cc00b0f03 100644 --- a/synmark/__init__.py +++ b/synmark/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synmark/__main__.py b/synmark/__main__.py index f55968a5a4..35a59e347a 100644 --- a/synmark/__main__.py +++ b/synmark/__main__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synmark/suites/logging.py b/synmark/suites/logging.py index b3abc6b254..9419892e95 100644 --- a/synmark/suites/logging.py +++ b/synmark/suites/logging.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synmark/suites/lrucache.py b/synmark/suites/lrucache.py index 69ab042ccc..9b4a424149 100644 --- a/synmark/suites/lrucache.py +++ b/synmark/suites/lrucache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synmark/suites/lrucache_evict.py b/synmark/suites/lrucache_evict.py index 532b1cc702..0ee202ed36 100644 --- a/synmark/suites/lrucache_evict.py +++ b/synmark/suites/lrucache_evict.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/__init__.py b/tests/__init__.py index ed805db1c2..5fced5cc4c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 28d77f0ca2..c0ed64f784 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 - 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/api/test_filtering.py b/tests/api/test_filtering.py index ab7d290724..f44c91a373 100644 --- a/tests/api/test_filtering.py +++ b/tests/api/test_filtering.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018-2019 New Vector Ltd diff --git a/tests/app/test_frontend_proxy.py b/tests/app/test_frontend_proxy.py index e0ca288829..3d45da38ab 100644 --- a/tests/app/test_frontend_proxy.py +++ b/tests/app/test_frontend_proxy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/app/test_openid_listener.py b/tests/app/test_openid_listener.py index 33a37fe35e..276f09015e 100644 --- a/tests/app/test_openid_listener.py +++ b/tests/app/test_openid_listener.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/appservice/__init__.py b/tests/appservice/__init__.py index fe0ac3f8e9..629e2df74a 100644 --- a/tests/appservice/__init__.py +++ b/tests/appservice/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/appservice/test_appservice.py b/tests/appservice/test_appservice.py index 03a7440eec..f386b5e128 100644 --- a/tests/appservice/test_appservice.py +++ b/tests/appservice/test_appservice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/appservice/test_scheduler.py b/tests/appservice/test_scheduler.py index 3c27d797fb..a2b5ed2030 100644 --- a/tests/appservice/test_scheduler.py +++ b/tests/appservice/test_scheduler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/__init__.py b/tests/config/__init__.py index b7df13c9ee..f43a360a80 100644 --- a/tests/config/__init__.py +++ b/tests/config/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_base.py b/tests/config/test_base.py index 42ee5f56d9..84ae3b88ae 100644 --- a/tests/config/test_base.py +++ b/tests/config/test_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_cache.py b/tests/config/test_cache.py index 2b7f09c14b..857d9cd096 100644 --- a/tests/config/test_cache.py +++ b/tests/config/test_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_database.py b/tests/config/test_database.py index f675bde68e..9eca10bbe9 100644 --- a/tests/config/test_database.py +++ b/tests/config/test_database.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_generate.py b/tests/config/test_generate.py index 463855ecc8..fdfbb0e38e 100644 --- a/tests/config/test_generate.py +++ b/tests/config/test_generate.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_load.py b/tests/config/test_load.py index c109425671..ebe2c05165 100644 --- a/tests/config/test_load.py +++ b/tests/config/test_load.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_ratelimiting.py b/tests/config/test_ratelimiting.py index 13ab282384..3c7bb32e07 100644 --- a/tests/config/test_ratelimiting.py +++ b/tests/config/test_ratelimiting.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_room_directory.py b/tests/config/test_room_directory.py index 0ec10019b3..db745815ef 100644 --- a/tests/config/test_room_directory.py +++ b/tests/config/test_room_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_server.py b/tests/config/test_server.py index 98af7aa675..6f2b9e997d 100644 --- a/tests/config/test_server.py +++ b/tests/config/test_server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/config/test_tls.py b/tests/config/test_tls.py index ec32d4b1ca..183034f7d4 100644 --- a/tests/config/test_tls.py +++ b/tests/config/test_tls.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # Copyright 2019 Matrix.org Foundation C.I.C. # diff --git a/tests/config/test_util.py b/tests/config/test_util.py index 10363e3765..3d4929daac 100644 --- a/tests/config/test_util.py +++ b/tests/config/test_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/crypto/__init__.py b/tests/crypto/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/tests/crypto/__init__.py +++ b/tests/crypto/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/crypto/test_event_signing.py b/tests/crypto/test_event_signing.py index 62f639a18d..1c920157f5 100644 --- a/tests/crypto/test_event_signing.py +++ b/tests/crypto/test_event_signing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index a56063315b..2775dfd880 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/events/test_presence_router.py b/tests/events/test_presence_router.py index c996ecc221..01d257307c 100644 --- a/tests/events/test_presence_router.py +++ b/tests/events/test_presence_router.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the 'License'); diff --git a/tests/events/test_snapshot.py b/tests/events/test_snapshot.py index ec85324c0c..48e98aac79 100644 --- a/tests/events/test_snapshot.py +++ b/tests/events/test_snapshot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index 8ba36c6074..9274ce4c39 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the 'License'); diff --git a/tests/federation/test_complexity.py b/tests/federation/test_complexity.py index 701fa8379f..1a809b2a6a 100644 --- a/tests/federation/test_complexity.py +++ b/tests/federation/test_complexity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Matrix.org Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index deb12433cf..b00dd143d6 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/federation/test_federation_server.py b/tests/federation/test_federation_server.py index cfeccc0577..8508b6bd3b 100644 --- a/tests/federation/test_federation_server.py +++ b/tests/federation/test_federation_server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # Copyright 2019 Matrix.org Federation C.I.C # diff --git a/tests/federation/transport/test_server.py b/tests/federation/transport/test_server.py index 85500e169c..84fa72b9ff 100644 --- a/tests/federation/transport/test_server.py +++ b/tests/federation/transport/test_server.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_admin.py b/tests/handlers/test_admin.py index 32669ae9ce..18a734daf4 100644 --- a/tests/handlers/test_admin.py +++ b/tests/handlers/test_admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py index 6e325b24ce..b037b12a0f 100644 --- a/tests/handlers/test_appservice.py +++ b/tests/handlers/test_appservice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_auth.py b/tests/handlers/test_auth.py index 321c5ba045..fe7e9484fd 100644 --- a/tests/handlers/test_auth.py +++ b/tests/handlers/test_auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_device.py b/tests/handlers/test_device.py index 821629bc38..84c38b295d 100644 --- a/tests/handlers/test_device.py +++ b/tests/handlers/test_device.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 6ae9d4f865..1908d3c2c6 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 6915ac0205..61a00130b8 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/tests/handlers/test_e2e_room_keys.py b/tests/handlers/test_e2e_room_keys.py index 07893302ec..9b7e7a8e9a 100644 --- a/tests/handlers/test_e2e_room_keys.py +++ b/tests/handlers/test_e2e_room_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2017 New Vector Ltd # Copyright 2019 Matrix.org Foundation C.I.C. diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index 3af361195b..c7b0975a19 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_message.py b/tests/handlers/test_message.py index a0d1ebdbe3..a8a9fc5b62 100644 --- a/tests/handlers/test_message.py +++ b/tests/handlers/test_message.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_oidc.py b/tests/handlers/test_oidc.py index 8702ee70e0..34d2fc1dfb 100644 --- a/tests/handlers/test_oidc.py +++ b/tests/handlers/test_oidc.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Quentin Gliech # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_password_providers.py b/tests/handlers/test_password_providers.py index e28e4159eb..32651db096 100644 --- a/tests/handlers/test_password_providers.py +++ b/tests/handlers/test_password_providers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 9f16cc65fc..2d12e82897 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index d8b1bcac8b..5330a9b34e 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index 69279a5ce9..608f8f3d33 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_stats.py b/tests/handlers/test_stats.py index 312c0a0d41..c9d4fd9336 100644 --- a/tests/handlers/test_stats.py +++ b/tests/handlers/test_stats.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index 8e950f25c5..c8b43305f4 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 9fa231a37a..0c89487eaf 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index c68cb830af..daac37abd8 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/__init__.py b/tests/http/__init__.py index 3e5a856584..e74f7f5b48 100644 --- a/tests/http/__init__.py +++ b/tests/http/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/federation/__init__.py b/tests/http/federation/__init__.py index 1453d04571..743fb9904a 100644 --- a/tests/http/federation/__init__.py +++ b/tests/http/federation/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index ae9d4504a8..e45980316b 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/federation/test_srv_resolver.py b/tests/http/federation/test_srv_resolver.py index 466ce722d9..c49be33b9f 100644 --- a/tests/http/federation/test_srv_resolver.py +++ b/tests/http/federation/test_srv_resolver.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # diff --git a/tests/http/test_additional_resource.py b/tests/http/test_additional_resource.py index 453391a5a5..768c2ba4ea 100644 --- a/tests/http/test_additional_resource.py +++ b/tests/http/test_additional_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/test_endpoint.py b/tests/http/test_endpoint.py index d06ea518ce..1f9a2f9b1d 100644 --- a/tests/http/test_endpoint.py +++ b/tests/http/test_endpoint.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/test_fedclient.py b/tests/http/test_fedclient.py index 21c1297171..9e97185507 100644 --- a/tests/http/test_fedclient.py +++ b/tests/http/test_fedclient.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/test_proxyagent.py b/tests/http/test_proxyagent.py index 3ea8b5bec7..fefc8099c9 100644 --- a/tests/http/test_proxyagent.py +++ b/tests/http/test_proxyagent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/test_servlet.py b/tests/http/test_servlet.py index f979c96f7c..a80bfb9f4e 100644 --- a/tests/http/test_servlet.py +++ b/tests/http/test_servlet.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/http/test_simple_client.py b/tests/http/test_simple_client.py index cc4cae320d..c85a3665c1 100644 --- a/tests/http/test_simple_client.py +++ b/tests/http/test_simple_client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/logging/__init__.py b/tests/logging/__init__.py index a58d51441c..1acf5666a8 100644 --- a/tests/logging/__init__.py +++ b/tests/logging/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/logging/test_remote_handler.py b/tests/logging/test_remote_handler.py index 4bc27a1d7d..b0d046fe00 100644 --- a/tests/logging/test_remote_handler.py +++ b/tests/logging/test_remote_handler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/logging/test_terse_json.py b/tests/logging/test_terse_json.py index 215fd8b0f9..6cddb95c30 100644 --- a/tests/logging/test_terse_json.py +++ b/tests/logging/test_terse_json.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/module_api/test_api.py b/tests/module_api/test_api.py index 349f93560e..742ad14b8c 100644 --- a/tests/module_api/test_api.py +++ b/tests/module_api/test_api.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/push/test_email.py b/tests/push/test_email.py index 941cf42429..e04bc5c9a6 100644 --- a/tests/push/test_email.py +++ b/tests/push/test_email.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/push/test_http.py b/tests/push/test_http.py index 4074ade87a..ffd75b1491 100644 --- a/tests/push/test_http.py +++ b/tests/push/test_http.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/push/test_push_rule_evaluator.py b/tests/push/test_push_rule_evaluator.py index 4a841f5bb8..45906ce720 100644 --- a/tests/push/test_push_rule_evaluator.py +++ b/tests/push/test_push_rule_evaluator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/__init__.py b/tests/replication/__init__.py index b7df13c9ee..f43a360a80 100644 --- a/tests/replication/__init__.py +++ b/tests/replication/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/_base.py b/tests/replication/_base.py index aff19d9fb3..36138d69aa 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/slave/__init__.py b/tests/replication/slave/__init__.py index b7df13c9ee..f43a360a80 100644 --- a/tests/replication/slave/__init__.py +++ b/tests/replication/slave/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/slave/storage/__init__.py b/tests/replication/slave/storage/__init__.py index b7df13c9ee..f43a360a80 100644 --- a/tests/replication/slave/storage/__init__.py +++ b/tests/replication/slave/storage/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/__init__.py b/tests/replication/tcp/__init__.py index 1453d04571..743fb9904a 100644 --- a/tests/replication/tcp/__init__.py +++ b/tests/replication/tcp/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/streams/__init__.py b/tests/replication/tcp/streams/__init__.py index 1453d04571..743fb9904a 100644 --- a/tests/replication/tcp/streams/__init__.py +++ b/tests/replication/tcp/streams/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/streams/test_account_data.py b/tests/replication/tcp/streams/test_account_data.py index 153634d4ee..cdd052001b 100644 --- a/tests/replication/tcp/streams/test_account_data.py +++ b/tests/replication/tcp/streams/test_account_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/streams/test_events.py b/tests/replication/tcp/streams/test_events.py index 77856fc304..323237c1bb 100644 --- a/tests/replication/tcp/streams/test_events.py +++ b/tests/replication/tcp/streams/test_events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/streams/test_federation.py b/tests/replication/tcp/streams/test_federation.py index aa4bf1c7e3..ffec06a0d6 100644 --- a/tests/replication/tcp/streams/test_federation.py +++ b/tests/replication/tcp/streams/test_federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/streams/test_receipts.py b/tests/replication/tcp/streams/test_receipts.py index 7d848e41ff..7f5d932f0b 100644 --- a/tests/replication/tcp/streams/test_receipts.py +++ b/tests/replication/tcp/streams/test_receipts.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/streams/test_typing.py b/tests/replication/tcp/streams/test_typing.py index 4a0b342264..ecd360c2d0 100644 --- a/tests/replication/tcp/streams/test_typing.py +++ b/tests/replication/tcp/streams/test_typing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/test_commands.py b/tests/replication/tcp/test_commands.py index 60c10a441a..cca7ebb719 100644 --- a/tests/replication/tcp/test_commands.py +++ b/tests/replication/tcp/test_commands.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/tcp/test_remote_server_up.py b/tests/replication/tcp/test_remote_server_up.py index 1fe9d5b4d0..262c35cef3 100644 --- a/tests/replication/tcp/test_remote_server_up.py +++ b/tests/replication/tcp/test_remote_server_up.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/test_auth.py b/tests/replication/test_auth.py index f8fd8a843c..1346e0e160 100644 --- a/tests/replication/test_auth.py +++ b/tests/replication/test_auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/test_client_reader_shard.py b/tests/replication/test_client_reader_shard.py index 5da1d5dc4d..b9751efdc5 100644 --- a/tests/replication/test_client_reader_shard.py +++ b/tests/replication/test_client_reader_shard.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/test_federation_ack.py b/tests/replication/test_federation_ack.py index 44ad5eec57..04a869e295 100644 --- a/tests/replication/test_federation_ack.py +++ b/tests/replication/test_federation_ack.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/test_federation_sender_shard.py b/tests/replication/test_federation_sender_shard.py index 8ca595c3ee..48ab3aa4e3 100644 --- a/tests/replication/test_federation_sender_shard.py +++ b/tests/replication/test_federation_sender_shard.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/test_multi_media_repo.py b/tests/replication/test_multi_media_repo.py index b0800f9840..76e6644353 100644 --- a/tests/replication/test_multi_media_repo.py +++ b/tests/replication/test_multi_media_repo.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/test_pusher_shard.py b/tests/replication/test_pusher_shard.py index 1f12bde1aa..1e4e3821b9 100644 --- a/tests/replication/test_pusher_shard.py +++ b/tests/replication/test_pusher_shard.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/replication/test_sharded_event_persister.py b/tests/replication/test_sharded_event_persister.py index 6c2e1674cb..d739eb6b17 100644 --- a/tests/replication/test_sharded_event_persister.py +++ b/tests/replication/test_sharded_event_persister.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py index fe0ac3f8e9..629e2df74a 100644 --- a/tests/rest/__init__.py +++ b/tests/rest/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/__init__.py b/tests/rest/admin/__init__.py index 1453d04571..743fb9904a 100644 --- a/tests/rest/admin/__init__.py +++ b/tests/rest/admin/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 4abcbe3f55..2f7090e554 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/test_device.py b/tests/rest/admin/test_device.py index 2a1bcf1760..ecbee30bb5 100644 --- a/tests/rest/admin/test_device.py +++ b/tests/rest/admin/test_device.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/test_event_reports.py b/tests/rest/admin/test_event_reports.py index e30ffe4fa0..8c66da3af4 100644 --- a/tests/rest/admin/test_event_reports.py +++ b/tests/rest/admin/test_event_reports.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/test_media.py b/tests/rest/admin/test_media.py index 31db472cd3..ac7b219700 100644 --- a/tests/rest/admin/test_media.py +++ b/tests/rest/admin/test_media.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 85f77c0a65..6bcd997085 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/test_statistics.py b/tests/rest/admin/test_statistics.py index 1f1d11f527..363bdeeb2d 100644 --- a/tests/rest/admin/test_statistics.py +++ b/tests/rest/admin/test_statistics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 5070c96984..2844c493fc 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/__init__.py b/tests/rest/client/__init__.py index fe0ac3f8e9..629e2df74a 100644 --- a/tests/rest/client/__init__.py +++ b/tests/rest/client/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/test_consent.py b/tests/rest/client/test_consent.py index c74693e9b2..5cc62a910a 100644 --- a/tests/rest/client/test_consent.py +++ b/tests/rest/client/test_consent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/test_ephemeral_message.py b/tests/rest/client/test_ephemeral_message.py index 56937dcd2e..eec0fc01f9 100644 --- a/tests/rest/client/test_ephemeral_message.py +++ b/tests/rest/client/test_ephemeral_message.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/test_identity.py b/tests/rest/client/test_identity.py index c0a9fc6925..478296ba0e 100644 --- a/tests/rest/client/test_identity.py +++ b/tests/rest/client/test_identity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/test_power_levels.py b/tests/rest/client/test_power_levels.py index 5256c11fe6..ba5ad47df5 100644 --- a/tests/rest/client/test_power_levels.py +++ b/tests/rest/client/test_power_levels.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/test_redactions.py b/tests/rest/client/test_redactions.py index e0c74591b6..dfd85221d0 100644 --- a/tests/rest/client/test_redactions.py +++ b/tests/rest/client/test_redactions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py index f892a71228..e1a6e73e17 100644 --- a/tests/rest/client/test_retention.py +++ b/tests/rest/client/test_retention.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index a7ebe0c3e9..e1fe72fc5d 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the 'License'); diff --git a/tests/rest/client/v1/__init__.py b/tests/rest/client/v1/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/tests/rest/client/v1/__init__.py +++ b/tests/rest/client/v1/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/test_directory.py b/tests/rest/client/v1/test_directory.py index edd1d184f8..8ed470490b 100644 --- a/tests/rest/client/v1/test_directory.py +++ b/tests/rest/client/v1/test_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py index 87a18d2cb9..852bda408c 100644 --- a/tests/rest/client/v1/test_events.py +++ b/tests/rest/client/v1/test_events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index c7b79ab8a7..605b952316 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index c136827f79..3a050659ca 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py index f3448c94dd..165ad33fb7 100644 --- a/tests/rest/client/v1/test_profile.py +++ b/tests/rest/client/v1/test_profile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/test_push_rule_attrs.py b/tests/rest/client/v1/test_push_rule_attrs.py index 2bc512d75e..d077616082 100644 --- a/tests/rest/client/v1/test_push_rule_attrs.py +++ b/tests/rest/client/v1/test_push_rule_attrs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 4df20c90fd..92babf65e0 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018-2019 New Vector Ltd diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 0b8f565121..0aad48a162 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector # diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index a6a292b20c..ed55a640af 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017 Vector Creations Ltd # Copyright 2018-2019 New Vector Ltd diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index e72b61963d..4ef19145d1 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/tests/rest/client/v2_alpha/test_auth.py b/tests/rest/client/v2_alpha/test_auth.py index ed433d9333..485e3650c3 100644 --- a/tests/rest/client/v2_alpha/test_auth.py +++ b/tests/rest/client/v2_alpha/test_auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # Copyright 2020-2021 The Matrix.org Foundation C.I.C # diff --git a/tests/rest/client/v2_alpha/test_capabilities.py b/tests/rest/client/v2_alpha/test_capabilities.py index 287a1a485c..874052c61c 100644 --- a/tests/rest/client/v2_alpha/test_capabilities.py +++ b/tests/rest/client/v2_alpha/test_capabilities.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py index f761c44936..c7e47725b7 100644 --- a/tests/rest/client/v2_alpha/test_filter.py +++ b/tests/rest/client/v2_alpha/test_filter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v2_alpha/test_password_policy.py b/tests/rest/client/v2_alpha/test_password_policy.py index 5ebc5707a5..6f07ff6cbb 100644 --- a/tests/rest/client/v2_alpha/test_password_policy.py +++ b/tests/rest/client/v2_alpha/test_password_policy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index cd60ea7081..054d4e4140 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. diff --git a/tests/rest/client/v2_alpha/test_relations.py b/tests/rest/client/v2_alpha/test_relations.py index 21ee436b91..856aa8682f 100644 --- a/tests/rest/client/v2_alpha/test_relations.py +++ b/tests/rest/client/v2_alpha/test_relations.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v2_alpha/test_shared_rooms.py b/tests/rest/client/v2_alpha/test_shared_rooms.py index dd83a1f8ff..cedb9614a8 100644 --- a/tests/rest/client/v2_alpha/test_shared_rooms.py +++ b/tests/rest/client/v2_alpha/test_shared_rooms.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Half-Shot # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 2dbf42397a..dbcbdf159a 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2019 New Vector Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/tests/rest/client/v2_alpha/test_upgrade_room.py b/tests/rest/client/v2_alpha/test_upgrade_room.py index d890d11863..5f3f15fc57 100644 --- a/tests/rest/client/v2_alpha/test_upgrade_room.py +++ b/tests/rest/client/v2_alpha/test_upgrade_room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/key/v2/test_remote_key_resource.py b/tests/rest/key/v2/test_remote_key_resource.py index eb8687ce68..3b275bc23b 100644 --- a/tests/rest/key/v2/test_remote_key_resource.py +++ b/tests/rest/key/v2/test_remote_key_resource.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/media/__init__.py b/tests/rest/media/__init__.py index a354d38ca8..b1ee10cfcc 100644 --- a/tests/rest/media/__init__.py +++ b/tests/rest/media/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/media/v1/__init__.py b/tests/rest/media/v1/__init__.py index a354d38ca8..b1ee10cfcc 100644 --- a/tests/rest/media/v1/__init__.py +++ b/tests/rest/media/v1/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/media/v1/test_base.py b/tests/rest/media/v1/test_base.py index ebd7869208..f761e23f1b 100644 --- a/tests/rest/media/v1/test_base.py +++ b/tests/rest/media/v1/test_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index 375f0b7977..4a213d13dd 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/media/v1/test_url_preview.py b/tests/rest/media/v1/test_url_preview.py index 9067463e54..d3ef7bb4c6 100644 --- a/tests/rest/media/v1/test_url_preview.py +++ b/tests/rest/media/v1/test_url_preview.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/test_health.py b/tests/rest/test_health.py index 32acd93dc1..01d48c3860 100644 --- a/tests/rest/test_health.py +++ b/tests/rest/test_health.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/rest/test_well_known.py b/tests/rest/test_well_known.py index 14de0921be..ac0e427752 100644 --- a/tests/rest/test_well_known.py +++ b/tests/rest/test_well_known.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/scripts/test_new_matrix_user.py b/tests/scripts/test_new_matrix_user.py index 885b95a51f..6f3c365c9a 100644 --- a/tests/scripts/test_new_matrix_user.py +++ b/tests/scripts/test_new_matrix_user.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/server_notices/test_consent.py b/tests/server_notices/test_consent.py index 4dd5a36178..ac98259b7e 100644 --- a/tests/server_notices/test_consent.py +++ b/tests/server_notices/test_consent.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index 450b4ec710..d46521ccdc 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018, 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 66e3cafe8e..43fc79ca74 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test__base.py b/tests/storage/test__base.py index 1ac4ebc61d..6339a43f0c 100644 --- a/tests/storage/test__base.py +++ b/tests/storage/test__base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # diff --git a/tests/storage/test_account_data.py b/tests/storage/test_account_data.py index 38444e48e2..01af49a16b 100644 --- a/tests/storage/test_account_data.py +++ b/tests/storage/test_account_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_appservice.py b/tests/storage/test_appservice.py index e755a4db62..666bffe257 100644 --- a/tests/storage/test_appservice.py +++ b/tests/storage/test_appservice.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index 54e9e7f6fe..3b45a7efd8 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_cleanup_extrems.py b/tests/storage/test_cleanup_extrems.py index b02fb32ced..aa20588bbe 100644 --- a/tests/storage/test_cleanup_extrems.py +++ b/tests/storage/test_cleanup_extrems.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index f7f75320ba..e57fce9694 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/tests/storage/test_database.py b/tests/storage/test_database.py index a906d30e73..6fbac0ab14 100644 --- a/tests/storage/test_database.py +++ b/tests/storage/test_database.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_devices.py b/tests/storage/test_devices.py index ef4cf8d0f1..6790aa5242 100644 --- a/tests/storage/test_devices.py +++ b/tests/storage/test_devices.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py index 0db233fd68..41bef62ca8 100644 --- a/tests/storage/test_directory.py +++ b/tests/storage/test_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_e2e_room_keys.py b/tests/storage/test_e2e_room_keys.py index 3d7760d5d9..9b6b425425 100644 --- a/tests/storage/test_e2e_room_keys.py +++ b/tests/storage/test_e2e_room_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_end_to_end_keys.py b/tests/storage/test_end_to_end_keys.py index 1e54b940fd..3bf6e337f4 100644 --- a/tests/storage/test_end_to_end_keys.py +++ b/tests/storage/test_end_to_end_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_event_chain.py b/tests/storage/test_event_chain.py index 16daa66cc9..d87f124c26 100644 --- a/tests/storage/test_event_chain.py +++ b/tests/storage/test_event_chain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the 'License'); diff --git a/tests/storage/test_event_federation.py b/tests/storage/test_event_federation.py index d597d712d6..a0e2259478 100644 --- a/tests/storage/test_event_federation.py +++ b/tests/storage/test_event_federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the 'License'); diff --git a/tests/storage/test_event_metrics.py b/tests/storage/test_event_metrics.py index 7691f2d790..397e68fe0a 100644 --- a/tests/storage/test_event_metrics.py +++ b/tests/storage/test_event_metrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the 'License'); diff --git a/tests/storage/test_event_push_actions.py b/tests/storage/test_event_push_actions.py index 0289942f88..1930b37eda 100644 --- a/tests/storage/test_event_push_actions.py +++ b/tests/storage/test_event_push_actions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_events.py b/tests/storage/test_events.py index ed898b8dbb..617bc8091f 100644 --- a/tests/storage/test_events.py +++ b/tests/storage/test_events.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_id_generators.py b/tests/storage/test_id_generators.py index 6c389fe9ac..792b1c44c1 100644 --- a/tests/storage/test_id_generators.py +++ b/tests/storage/test_id_generators.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_keys.py b/tests/storage/test_keys.py index 95f309fbbc..a94b5fd721 100644 --- a/tests/storage/test_keys.py +++ b/tests/storage/test_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_main.py b/tests/storage/test_main.py index e9e3bca3bf..d2b7b89952 100644 --- a/tests/storage/test_main.py +++ b/tests/storage/test_main.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Awesome Technologies Innovationslabor GmbH # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_monthly_active_users.py b/tests/storage/test_monthly_active_users.py index 47556791f4..944dbc34a2 100644 --- a/tests/storage/test_monthly_active_users.py +++ b/tests/storage/test_monthly_active_users.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py index d18ceb41a9..8a446da848 100644 --- a/tests/storage/test_profile.py +++ b/tests/storage/test_profile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_purge.py b/tests/storage/test_purge.py index 41af8c4847..54c5b470c7 100644 --- a/tests/storage/test_purge.py +++ b/tests/storage/test_purge.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index 2d2f58903c..bb31ab756d 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index c82cf15bc2..9748065282 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index 0089d33c93..70257bf210 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index d2aed66f6d..9fa968f6bb 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 The Matrix.org Foundation C.I.C. # diff --git a/tests/storage/test_state.py b/tests/storage/test_state.py index f06b452fa9..8695264595 100644 --- a/tests/storage/test_state.py +++ b/tests/storage/test_state.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_transactions.py b/tests/storage/test_transactions.py index 8e817e2c7f..b7f7eae8d0 100644 --- a/tests/storage/test_transactions.py +++ b/tests/storage/test_transactions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/storage/test_user_directory.py b/tests/storage/test_user_directory.py index 019c5b7b14..222e5d129d 100644 --- a/tests/storage/test_user_directory.py +++ b/tests/storage/test_user_directory.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_distributor.py b/tests/test_distributor.py index 6a6cf709f6..f8341041ee 100644 --- a/tests/test_distributor.py +++ b/tests/test_distributor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index b5f18344dc..88888319cc 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_federation.py b/tests/test_federation.py index 8928597d17..86a44a13da 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_mau.py b/tests/test_mau.py index 7d92a16a8d..fa6ef92b3b 100644 --- a/tests/test_mau.py +++ b/tests/test_mau.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_metrics.py b/tests/test_metrics.py index f696fcf89e..b4574b2ffe 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # Copyright 2019 Matrix.org Foundation C.I.C. # diff --git a/tests/test_phone_home.py b/tests/test_phone_home.py index 0f800a075b..09707a74d7 100644 --- a/tests/test_phone_home.py +++ b/tests/test_phone_home.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_preview.py b/tests/test_preview.py index ea83299918..cac3d81ac1 100644 --- a/tests/test_preview.py +++ b/tests/test_preview.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_state.py b/tests/test_state.py index 0d626f49f6..62f7095873 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_test_utils.py b/tests/test_test_utils.py index b921ac52c0..f2ef1c6051 100644 --- a/tests/test_test_utils.py +++ b/tests/test_test_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_types.py b/tests/test_types.py index acdeea7a09..d7881021d3 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py index b557ffd692..be6302d170 100644 --- a/tests/test_utils/__init__.py +++ b/tests/test_utils/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # Copyright 2020 The Matrix.org Foundation C.I.C # diff --git a/tests/test_utils/event_injection.py b/tests/test_utils/event_injection.py index 3dfbf8f8a9..e9ec9e085b 100644 --- a/tests/test_utils/event_injection.py +++ b/tests/test_utils/event_injection.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # Copyright 2020 The Matrix.org Foundation C.I.C # diff --git a/tests/test_utils/html_parsers.py b/tests/test_utils/html_parsers.py index ad563eb3f0..1fbb38f4be 100644 --- a/tests/test_utils/html_parsers.py +++ b/tests/test_utils/html_parsers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_utils/logging_setup.py b/tests/test_utils/logging_setup.py index 74568b34f8..51a197a8c6 100644 --- a/tests/test_utils/logging_setup.py +++ b/tests/test_utils/logging_setup.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_visibility.py b/tests/test_visibility.py index e502ac197e..94b19788d7 100644 --- a/tests/test_visibility.py +++ b/tests/test_visibility.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/unittest.py b/tests/unittest.py index 92764434bd..d890ad981f 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018 New Vector # Copyright 2019 Matrix.org Federation C.I.C diff --git a/tests/util/__init__.py b/tests/util/__init__.py index bfebb0f644..5e83dba2ed 100644 --- a/tests/util/__init__.py +++ b/tests/util/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/caches/__init__.py b/tests/util/caches/__init__.py index 451dae3b6c..830e2dfe91 100644 --- a/tests/util/caches/__init__.py +++ b/tests/util/caches/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/caches/test_cached_call.py b/tests/util/caches/test_cached_call.py index f349b5ced0..80b97167ba 100644 --- a/tests/util/caches/test_cached_call.py +++ b/tests/util/caches/test_cached_call.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/caches/test_deferred_cache.py b/tests/util/caches/test_deferred_cache.py index c24c33ee91..54a88a8325 100644 --- a/tests/util/caches/test_deferred_cache.py +++ b/tests/util/caches/test_deferred_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index 2d1f9360e0..40cd98e2d8 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/tests/util/caches/test_ttlcache.py b/tests/util/caches/test_ttlcache.py index 23018081e5..fe8314057d 100644 --- a/tests/util/caches/test_ttlcache.py +++ b/tests/util/caches/test_ttlcache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_async_utils.py b/tests/util/test_async_utils.py index 17fd86d02d..069f875962 100644 --- a/tests/util/test_async_utils.py +++ b/tests/util/test_async_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_dict_cache.py b/tests/util/test_dict_cache.py index 2f41333f4c..bee66dee43 100644 --- a/tests/util/test_dict_cache.py +++ b/tests/util/test_dict_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_expiring_cache.py b/tests/util/test_expiring_cache.py index 49ffeebd0e..e6e13ba06c 100644 --- a/tests/util/test_expiring_cache.py +++ b/tests/util/test_expiring_cache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_file_consumer.py b/tests/util/test_file_consumer.py index d1372f6bc2..3bb4695405 100644 --- a/tests/util/test_file_consumer.py +++ b/tests/util/test_file_consumer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_itertools.py b/tests/util/test_itertools.py index e931a7ec18..1bd0b45d94 100644 --- a/tests/util/test_itertools.py +++ b/tests/util/test_itertools.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_linearizer.py b/tests/util/test_linearizer.py index 0e52811948..c4a3917b23 100644 --- a/tests/util/test_linearizer.py +++ b/tests/util/test_linearizer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # Copyright 2018 New Vector Ltd # diff --git a/tests/util/test_logformatter.py b/tests/util/test_logformatter.py index 0fb60caacb..a2e08281e6 100644 --- a/tests/util/test_logformatter.py +++ b/tests/util/test_logformatter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_lrucache.py b/tests/util/test_lrucache.py index ce4f1cc30a..df3e27779f 100644 --- a/tests/util/test_lrucache.py +++ b/tests/util/test_lrucache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_ratelimitutils.py b/tests/util/test_ratelimitutils.py index 3fed55090a..34aaffe859 100644 --- a/tests/util/test_ratelimitutils.py +++ b/tests/util/test_ratelimitutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_retryutils.py b/tests/util/test_retryutils.py index 5f46ed0cef..9b2be83a43 100644 --- a/tests/util/test_retryutils.py +++ b/tests/util/test_retryutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_rwlock.py b/tests/util/test_rwlock.py index d3dea3b52a..a10071c70f 100644 --- a/tests/util/test_rwlock.py +++ b/tests/util/test_rwlock.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_stringutils.py b/tests/util/test_stringutils.py index 8491f7cc83..f7fecd9cf3 100644 --- a/tests/util/test_stringutils.py +++ b/tests/util/test_stringutils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_threepids.py b/tests/util/test_threepids.py index 5513724d87..d957b953bb 100644 --- a/tests/util/test_threepids.py +++ b/tests/util/test_threepids.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Dirk Klimpel # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_treecache.py b/tests/util/test_treecache.py index a5f2261208..3b077af27e 100644 --- a/tests/util/test_treecache.py +++ b/tests/util/test_treecache.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/util/test_wheel_timer.py b/tests/util/test_wheel_timer.py index 03201a4d9b..0d5039de04 100644 --- a/tests/util/test_wheel_timer.py +++ b/tests/util/test_wheel_timer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/utils.py b/tests/utils.py index c78d3e5ba7..af6b32fc66 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2016 OpenMarket Ltd # Copyright 2018-2019 New Vector Ltd # From c9a2b5d4022feab97fa1bce1d67360d09a6e3dcc Mon Sep 17 00:00:00 2001 From: rkfg Date: Wed, 14 Apr 2021 18:30:59 +0300 Subject: [PATCH 048/619] More robust handling of the Content-Type header for thumbnail generation (#9788) Signed-off-by: Sergey Shpikin --- changelog.d/9788.bugfix | 1 + synapse/config/repository.py | 1 + synapse/rest/media/v1/media_repository.py | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 changelog.d/9788.bugfix diff --git a/changelog.d/9788.bugfix b/changelog.d/9788.bugfix new file mode 100644 index 0000000000..edb58fbd5b --- /dev/null +++ b/changelog.d/9788.bugfix @@ -0,0 +1 @@ +Fix thumbnail generation for some sites with non-standard content types. Contributed by @rkfg. diff --git a/synapse/config/repository.py b/synapse/config/repository.py index 146bc55d6f..c78a83abe1 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -70,6 +70,7 @@ def parse_thumbnail_requirements(thumbnail_sizes): jpeg_thumbnail = ThumbnailRequirement(width, height, method, "image/jpeg") png_thumbnail = ThumbnailRequirement(width, height, method, "image/png") requirements.setdefault("image/jpeg", []).append(jpeg_thumbnail) + requirements.setdefault("image/jpg", []).append(jpeg_thumbnail) requirements.setdefault("image/webp", []).append(jpeg_thumbnail) requirements.setdefault("image/gif", []).append(png_thumbnail) requirements.setdefault("image/png", []).append(png_thumbnail) diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index 87e3645ddc..e8a875b900 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -467,6 +467,9 @@ async def _download_remote_file( return media_info def _get_thumbnail_requirements(self, media_type): + scpos = media_type.find(";") + if scpos > 0: + media_type = media_type[:scpos] return self.thumbnail_requirements.get(media_type, ()) def _generate_thumbnail( From 00a6db967655daf1d6db290b7e0d2bb53827ade9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 14 Apr 2021 17:06:06 +0100 Subject: [PATCH 049/619] Move some replication processing out of generic_worker (#9796) Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- changelog.d/9796.misc | 1 + synapse/app/generic_worker.py | 470 +----------------------------- synapse/handlers/presence.py | 246 ++++++++++++++++ synapse/replication/tcp/client.py | 231 ++++++++++++++- synapse/server.py | 13 +- tests/replication/_base.py | 8 +- 6 files changed, 486 insertions(+), 483 deletions(-) create mode 100644 changelog.d/9796.misc diff --git a/changelog.d/9796.misc b/changelog.d/9796.misc new file mode 100644 index 0000000000..59bb1813c3 --- /dev/null +++ b/changelog.d/9796.misc @@ -0,0 +1 @@ +Move some replication processing out of `generic_worker`. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index e35e17492c..28e3b1aa3c 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -13,12 +13,9 @@ # 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 contextlib import logging import sys -from typing import Dict, Iterable, Optional, Set - -from typing_extensions import ContextManager +from typing import Dict, Iterable, Optional from twisted.internet import address from twisted.web.resource import IResource @@ -40,24 +37,13 @@ from synapse.config.homeserver import HomeServerConfig from synapse.config.logger import setup_logging from synapse.config.server import ListenerConfig -from synapse.federation import send_queue from synapse.federation.transport.server import TransportLayerServer -from synapse.handlers.presence import ( - BasePresenceHandler, - PresenceState, - get_interested_parties, -) from synapse.http.server import JsonResource, OptionsResource from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.http.site import SynapseSite from synapse.logging.context import LoggingContext from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy -from synapse.metrics.background_process_metrics import run_as_background_process from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource -from synapse.replication.http.presence import ( - ReplicationBumpPresenceActiveTime, - ReplicationPresenceSetState, -) from synapse.replication.slave.storage._base import BaseSlavedStore from synapse.replication.slave.storage.account_data import SlavedAccountDataStore from synapse.replication.slave.storage.appservice import SlavedApplicationServiceStore @@ -77,19 +63,6 @@ from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.slave.storage.room import RoomStore from synapse.replication.slave.storage.transactions import SlavedTransactionStore -from synapse.replication.tcp.client import ReplicationDataHandler -from synapse.replication.tcp.commands import ClearUserSyncsCommand -from synapse.replication.tcp.streams import ( - AccountDataStream, - DeviceListsStream, - GroupServerStream, - PresenceStream, - PushersStream, - PushRulesStream, - ReceiptsStream, - TagAccountDataStream, - ToDeviceStream, -) from synapse.rest.admin import register_servlets_for_media_repo from synapse.rest.client.v1 import events, login, room from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet @@ -128,7 +101,7 @@ from synapse.rest.health import HealthResource from synapse.rest.key.v2 import KeyApiV2Resource from synapse.rest.synapse.client import build_synapse_client_resource_tree -from synapse.server import HomeServer, cache_in_self +from synapse.server import HomeServer from synapse.storage.databases.main.censor_events import CensorEventsStore from synapse.storage.databases.main.client_ips import ClientIpWorkerStore from synapse.storage.databases.main.e2e_room_keys import EndToEndRoomKeyStore @@ -137,14 +110,11 @@ from synapse.storage.databases.main.monthly_active_users import ( MonthlyActiveUsersWorkerStore, ) -from synapse.storage.databases.main.presence import UserPresenceState from synapse.storage.databases.main.search import SearchWorkerStore from synapse.storage.databases.main.stats import StatsStore from synapse.storage.databases.main.transactions import TransactionWorkerStore from synapse.storage.databases.main.ui_auth import UIAuthWorkerStore from synapse.storage.databases.main.user_directory import UserDirectoryStore -from synapse.types import ReadReceipt -from synapse.util.async_helpers import Linearizer from synapse.util.httpresourcetree import create_resource_tree from synapse.util.versionstring import get_version_string @@ -264,214 +234,6 @@ async def on_POST(self, request: Request, device_id: Optional[str]): return 200, {"one_time_key_counts": result} -class _NullContextManager(ContextManager[None]): - """A context manager which does nothing.""" - - def __exit__(self, exc_type, exc_val, exc_tb): - pass - - -UPDATE_SYNCING_USERS_MS = 10 * 1000 - - -class GenericWorkerPresence(BasePresenceHandler): - def __init__(self, hs): - super().__init__(hs) - self.hs = hs - self.is_mine_id = hs.is_mine_id - - self.presence_router = hs.get_presence_router() - self._presence_enabled = hs.config.use_presence - - # The number of ongoing syncs on this process, by user id. - # Empty if _presence_enabled is false. - self._user_to_num_current_syncs = {} # type: Dict[str, int] - - self.notifier = hs.get_notifier() - self.instance_id = hs.get_instance_id() - - # user_id -> last_sync_ms. Lists the users that have stopped syncing - # but we haven't notified the master of that yet - self.users_going_offline = {} - - self._bump_active_client = ReplicationBumpPresenceActiveTime.make_client(hs) - self._set_state_client = ReplicationPresenceSetState.make_client(hs) - - self._send_stop_syncing_loop = self.clock.looping_call( - self.send_stop_syncing, UPDATE_SYNCING_USERS_MS - ) - - self._busy_presence_enabled = hs.config.experimental.msc3026_enabled - - hs.get_reactor().addSystemEventTrigger( - "before", - "shutdown", - run_as_background_process, - "generic_presence.on_shutdown", - self._on_shutdown, - ) - - def _on_shutdown(self): - if self._presence_enabled: - self.hs.get_tcp_replication().send_command( - ClearUserSyncsCommand(self.instance_id) - ) - - def send_user_sync(self, user_id, is_syncing, last_sync_ms): - if self._presence_enabled: - self.hs.get_tcp_replication().send_user_sync( - self.instance_id, user_id, is_syncing, last_sync_ms - ) - - def mark_as_coming_online(self, user_id): - """A user has started syncing. Send a UserSync to the master, unless they - had recently stopped syncing. - - Args: - user_id (str) - """ - going_offline = self.users_going_offline.pop(user_id, None) - if not going_offline: - # Safe to skip because we haven't yet told the master they were offline - self.send_user_sync(user_id, True, self.clock.time_msec()) - - def mark_as_going_offline(self, user_id): - """A user has stopped syncing. We wait before notifying the master as - its likely they'll come back soon. This allows us to avoid sending - a stopped syncing immediately followed by a started syncing notification - to the master - - Args: - user_id (str) - """ - self.users_going_offline[user_id] = self.clock.time_msec() - - def send_stop_syncing(self): - """Check if there are any users who have stopped syncing a while ago - and haven't come back yet. If there are poke the master about them. - """ - now = self.clock.time_msec() - for user_id, last_sync_ms in list(self.users_going_offline.items()): - if now - last_sync_ms > UPDATE_SYNCING_USERS_MS: - self.users_going_offline.pop(user_id, None) - self.send_user_sync(user_id, False, last_sync_ms) - - async def user_syncing( - self, user_id: str, affect_presence: bool - ) -> ContextManager[None]: - """Record that a user is syncing. - - Called by the sync and events servlets to record that a user has connected to - this worker and is waiting for some events. - """ - if not affect_presence or not self._presence_enabled: - return _NullContextManager() - - curr_sync = self._user_to_num_current_syncs.get(user_id, 0) - self._user_to_num_current_syncs[user_id] = curr_sync + 1 - - # If we went from no in flight sync to some, notify replication - if self._user_to_num_current_syncs[user_id] == 1: - self.mark_as_coming_online(user_id) - - def _end(): - # We check that the user_id is in user_to_num_current_syncs because - # user_to_num_current_syncs may have been cleared if we are - # shutting down. - if user_id in self._user_to_num_current_syncs: - self._user_to_num_current_syncs[user_id] -= 1 - - # If we went from one in flight sync to non, notify replication - if self._user_to_num_current_syncs[user_id] == 0: - self.mark_as_going_offline(user_id) - - @contextlib.contextmanager - def _user_syncing(): - try: - yield - finally: - _end() - - return _user_syncing() - - async def notify_from_replication(self, states, stream_id): - parties = await get_interested_parties(self.store, self.presence_router, states) - room_ids_to_states, users_to_states = parties - - self.notifier.on_new_event( - "presence_key", - stream_id, - rooms=room_ids_to_states.keys(), - users=users_to_states.keys(), - ) - - async def process_replication_rows(self, token, rows): - states = [ - UserPresenceState( - row.user_id, - row.state, - row.last_active_ts, - row.last_federation_update_ts, - row.last_user_sync_ts, - row.status_msg, - row.currently_active, - ) - for row in rows - ] - - for state in states: - self.user_to_current_state[state.user_id] = state - - stream_id = token - await self.notify_from_replication(states, stream_id) - - def get_currently_syncing_users_for_replication(self) -> Iterable[str]: - return [ - user_id - for user_id, count in self._user_to_num_current_syncs.items() - if count > 0 - ] - - async def set_state(self, target_user, state, ignore_status_msg=False): - """Set the presence state of the user.""" - presence = state["presence"] - - valid_presence = ( - PresenceState.ONLINE, - PresenceState.UNAVAILABLE, - PresenceState.OFFLINE, - PresenceState.BUSY, - ) - - if presence not in valid_presence or ( - presence == PresenceState.BUSY and not self._busy_presence_enabled - ): - raise SynapseError(400, "Invalid presence state") - - user_id = target_user.to_string() - - # If presence is disabled, no-op - if not self.hs.config.use_presence: - return - - # Proxy request to master - await self._set_state_client( - user_id=user_id, state=state, ignore_status_msg=ignore_status_msg - ) - - async def bump_presence_active_time(self, user): - """We've seen the user do something that indicates they're interacting - with the app. - """ - # If presence is disabled, no-op - if not self.hs.config.use_presence: - return - - # Proxy request to master - user_id = user.to_string() - await self._bump_active_client(user_id=user_id) - - class GenericWorkerSlavedStore( # FIXME(#3714): We need to add UserDirectoryStore as we write directly # rather than going via the correct worker. @@ -657,234 +419,6 @@ def start_listening(self, listeners: Iterable[ListenerConfig]): self.get_tcp_replication().start_replication(self) - @cache_in_self - def get_replication_data_handler(self): - return GenericWorkerReplicationHandler(self) - - @cache_in_self - def get_presence_handler(self): - return GenericWorkerPresence(self) - - -class GenericWorkerReplicationHandler(ReplicationDataHandler): - def __init__(self, hs): - super().__init__(hs) - - self.store = hs.get_datastore() - self.presence_handler = hs.get_presence_handler() # type: GenericWorkerPresence - self.notifier = hs.get_notifier() - - self.notify_pushers = hs.config.start_pushers - self.pusher_pool = hs.get_pusherpool() - - self.send_handler = None # type: Optional[FederationSenderHandler] - if hs.config.send_federation: - self.send_handler = FederationSenderHandler(hs) - - async def on_rdata(self, stream_name, instance_name, token, rows): - await super().on_rdata(stream_name, instance_name, token, rows) - await self._process_and_notify(stream_name, instance_name, token, rows) - - async def _process_and_notify(self, stream_name, instance_name, token, rows): - try: - if self.send_handler: - await self.send_handler.process_replication_rows( - stream_name, token, rows - ) - - if stream_name == PushRulesStream.NAME: - self.notifier.on_new_event( - "push_rules_key", 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] - ) - elif stream_name == ReceiptsStream.NAME: - self.notifier.on_new_event( - "receipt_key", 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} - ) - 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) - elif stream_name == DeviceListsStream.NAME: - all_room_ids = set() # type: Set[str] - 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) - elif stream_name == PresenceStream.NAME: - await self.presence_handler.process_replication_rows(token, rows) - elif stream_name == GroupServerStream.NAME: - self.notifier.on_new_event( - "groups_key", token, users=[row.user_id for row in rows] - ) - elif stream_name == PushersStream.NAME: - for row in rows: - if row.deleted: - self.stop_pusher(row.user_id, row.app_id, row.pushkey) - else: - await self.start_pusher(row.user_id, row.app_id, row.pushkey) - except Exception: - logger.exception("Error processing replication") - - async def on_position(self, stream_name: str, instance_name: str, token: int): - await super().on_position(stream_name, instance_name, token) - # Also call on_rdata to ensure that stream positions are properly reset. - await self.on_rdata(stream_name, instance_name, token, []) - - def stop_pusher(self, user_id, app_id, pushkey): - if not self.notify_pushers: - return - - key = "%s:%s" % (app_id, pushkey) - pushers_for_user = self.pusher_pool.pushers.get(user_id, {}) - pusher = pushers_for_user.pop(key, None) - if pusher is None: - return - logger.info("Stopping pusher %r / %r", user_id, key) - pusher.on_stop() - - async def start_pusher(self, user_id, app_id, pushkey): - if not self.notify_pushers: - return - - key = "%s:%s" % (app_id, pushkey) - logger.info("Starting pusher %r / %r", user_id, key) - return await self.pusher_pool.start_pusher_by_id(app_id, pushkey, user_id) - - def on_remote_server_up(self, server: str): - """Called when get a new REMOTE_SERVER_UP command.""" - - # Let's wake up the transaction queue for the server in case we have - # pending stuff to send to it. - if self.send_handler: - self.send_handler.wake_destination(server) - - -class FederationSenderHandler: - """Processes the fedration replication stream - - This class is only instantiate on the worker responsible for sending outbound - federation transactions. It receives rows from the replication stream and forwards - the appropriate entries to the FederationSender class. - """ - - def __init__(self, hs: GenericWorkerServer): - self.store = hs.get_datastore() - self._is_mine_id = hs.is_mine_id - self.federation_sender = hs.get_federation_sender() - self._hs = hs - - # Stores the latest position in the federation stream we've gotten up - # to. This is always set before we use it. - self.federation_position = None - - self._fed_position_linearizer = Linearizer(name="_fed_position_linearizer") - - def wake_destination(self, server: str): - self.federation_sender.wake_destination(server) - - async def process_replication_rows(self, stream_name, token, rows): - # The federation stream contains things that we want to send out, e.g. - # presence, typing, etc. - if stream_name == "federation": - send_queue.process_rows_for_federation(self.federation_sender, rows) - await self.update_token(token) - - # ... and when new receipts happen - elif stream_name == ReceiptsStream.NAME: - await self._on_new_receipts(rows) - - # ... as well as device updates and messages - elif stream_name == DeviceListsStream.NAME: - # The entities are either user IDs (starting with '@') whose devices - # have changed, or remote servers that we need to tell about - # changes. - hosts = {row.entity for row in rows if not row.entity.startswith("@")} - for host in hosts: - self.federation_sender.send_device_messages(host) - - elif stream_name == ToDeviceStream.NAME: - # The to_device stream includes stuff to be pushed to both local - # clients and remote servers, so we ignore entities that start with - # '@' (since they'll be local users rather than destinations). - hosts = {row.entity for row in rows if not row.entity.startswith("@")} - for host in hosts: - self.federation_sender.send_device_messages(host) - - async def _on_new_receipts(self, rows): - """ - Args: - rows (Iterable[synapse.replication.tcp.streams.ReceiptsStream.ReceiptsStreamRow]): - new receipts to be processed - """ - for receipt in rows: - # we only want to send on receipts for our own users - if not self._is_mine_id(receipt.user_id): - continue - receipt_info = ReadReceipt( - receipt.room_id, - receipt.receipt_type, - receipt.user_id, - [receipt.event_id], - receipt.data, - ) - await self.federation_sender.send_read_receipt(receipt_info) - - async def update_token(self, token): - """Update the record of where we have processed to in the federation stream. - - Called after we have processed a an update received over replication. Sends - a FEDERATION_ACK back to the master, and stores the token that we have processed - in `federation_stream_position` so that we can restart where we left off. - """ - self.federation_position = token - - # We save and send the ACK to master asynchronously, so we don't block - # processing on persistence. We don't need to do this operation for - # every single RDATA we receive, we just need to do it periodically. - - if self._fed_position_linearizer.is_queued(None): - # There is already a task queued up to save and send the token, so - # no need to queue up another task. - return - - run_as_background_process("_save_and_send_ack", self._save_and_send_ack) - - async def _save_and_send_ack(self): - """Save the current federation position in the database and send an ACK - to master with where we're up to. - """ - try: - # We linearize here to ensure we don't have races updating the token - # - # XXX this appears to be redundant, since the ReplicationCommandHandler - # has a linearizer which ensures that we only process one line of - # replication data at a time. Should we remove it, or is it doing useful - # service for robustness? Or could we replace it with an assertion that - # we're not being re-entered? - - with (await self._fed_position_linearizer.queue(None)): - # We persist and ack the same position, so we take a copy of it - # here as otherwise it can get modified from underneath us. - current_position = self.federation_position - - await self.store.update_federation_out_pos( - "federation", current_position - ) - - # We ACK this token over replication so that the master can drop - # its in memory queues - self._hs.get_tcp_replication().send_federation_ack(current_position) - except Exception: - logger.exception("Error updating federation stream position") - def start(config_options): try: diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 251b48148d..e120dd1f48 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -22,6 +22,7 @@ - should_notify """ import abc +import contextlib import logging from contextlib import contextmanager from typing import ( @@ -48,6 +49,11 @@ from synapse.logging.utils import log_function from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.replication.http.presence import ( + ReplicationBumpPresenceActiveTime, + ReplicationPresenceSetState, +) +from synapse.replication.tcp.commands import ClearUserSyncsCommand from synapse.state import StateHandler from synapse.storage.databases.main import DataStore from synapse.types import Collection, JsonDict, UserID, get_domain_from_id @@ -104,6 +110,10 @@ # are dead. EXTERNAL_PROCESS_EXPIRY = 5 * 60 * 1000 +# Delay before a worker tells the presence handler that a user has stopped +# syncing. +UPDATE_SYNCING_USERS_MS = 10 * 1000 + assert LAST_ACTIVE_GRANULARITY < IDLE_TIMER @@ -208,6 +218,242 @@ async def bump_presence_active_time(self, user: UserID): with the app. """ + async def update_external_syncs_row( + self, process_id, user_id, is_syncing, sync_time_msec + ): + """Update the syncing users for an external process as a delta. + + This is a no-op when presence is handled by a different worker. + + Args: + process_id (str): An identifier for the process the users are + syncing against. This allows synapse to process updates + as user start and stop syncing against a given process. + user_id (str): The user who has started or stopped syncing + is_syncing (bool): Whether or not the user is now syncing + sync_time_msec(int): Time in ms when the user was last syncing + """ + pass + + async def update_external_syncs_clear(self, process_id): + """Marks all users that had been marked as syncing by a given process + as offline. + + Used when the process has stopped/disappeared. + + This is a no-op when presence is handled by a different worker. + """ + pass + + async def process_replication_rows(self, token, rows): + """Process presence stream rows received over replication.""" + pass + + +class _NullContextManager(ContextManager[None]): + """A context manager which does nothing.""" + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +class WorkerPresenceHandler(BasePresenceHandler): + def __init__(self, hs): + super().__init__(hs) + self.hs = hs + self.is_mine_id = hs.is_mine_id + + self.presence_router = hs.get_presence_router() + self._presence_enabled = hs.config.use_presence + + # The number of ongoing syncs on this process, by user id. + # Empty if _presence_enabled is false. + self._user_to_num_current_syncs = {} # type: Dict[str, int] + + self.notifier = hs.get_notifier() + self.instance_id = hs.get_instance_id() + + # user_id -> last_sync_ms. Lists the users that have stopped syncing + # but we haven't notified the master of that yet + self.users_going_offline = {} + + self._bump_active_client = ReplicationBumpPresenceActiveTime.make_client(hs) + self._set_state_client = ReplicationPresenceSetState.make_client(hs) + + self._send_stop_syncing_loop = self.clock.looping_call( + self.send_stop_syncing, UPDATE_SYNCING_USERS_MS + ) + + self._busy_presence_enabled = hs.config.experimental.msc3026_enabled + + hs.get_reactor().addSystemEventTrigger( + "before", + "shutdown", + run_as_background_process, + "generic_presence.on_shutdown", + self._on_shutdown, + ) + + def _on_shutdown(self): + if self._presence_enabled: + self.hs.get_tcp_replication().send_command( + ClearUserSyncsCommand(self.instance_id) + ) + + def send_user_sync(self, user_id, is_syncing, last_sync_ms): + if self._presence_enabled: + self.hs.get_tcp_replication().send_user_sync( + self.instance_id, user_id, is_syncing, last_sync_ms + ) + + def mark_as_coming_online(self, user_id): + """A user has started syncing. Send a UserSync to the master, unless they + had recently stopped syncing. + + Args: + user_id (str) + """ + going_offline = self.users_going_offline.pop(user_id, None) + if not going_offline: + # Safe to skip because we haven't yet told the master they were offline + self.send_user_sync(user_id, True, self.clock.time_msec()) + + def mark_as_going_offline(self, user_id): + """A user has stopped syncing. We wait before notifying the master as + its likely they'll come back soon. This allows us to avoid sending + a stopped syncing immediately followed by a started syncing notification + to the master + + Args: + user_id (str) + """ + self.users_going_offline[user_id] = self.clock.time_msec() + + def send_stop_syncing(self): + """Check if there are any users who have stopped syncing a while ago + and haven't come back yet. If there are poke the master about them. + """ + now = self.clock.time_msec() + for user_id, last_sync_ms in list(self.users_going_offline.items()): + if now - last_sync_ms > UPDATE_SYNCING_USERS_MS: + self.users_going_offline.pop(user_id, None) + self.send_user_sync(user_id, False, last_sync_ms) + + async def user_syncing( + self, user_id: str, affect_presence: bool + ) -> ContextManager[None]: + """Record that a user is syncing. + + Called by the sync and events servlets to record that a user has connected to + this worker and is waiting for some events. + """ + if not affect_presence or not self._presence_enabled: + return _NullContextManager() + + curr_sync = self._user_to_num_current_syncs.get(user_id, 0) + self._user_to_num_current_syncs[user_id] = curr_sync + 1 + + # If we went from no in flight sync to some, notify replication + if self._user_to_num_current_syncs[user_id] == 1: + self.mark_as_coming_online(user_id) + + def _end(): + # We check that the user_id is in user_to_num_current_syncs because + # user_to_num_current_syncs may have been cleared if we are + # shutting down. + if user_id in self._user_to_num_current_syncs: + self._user_to_num_current_syncs[user_id] -= 1 + + # If we went from one in flight sync to non, notify replication + if self._user_to_num_current_syncs[user_id] == 0: + self.mark_as_going_offline(user_id) + + @contextlib.contextmanager + def _user_syncing(): + try: + yield + finally: + _end() + + return _user_syncing() + + async def notify_from_replication(self, states, stream_id): + parties = await get_interested_parties(self.store, self.presence_router, states) + room_ids_to_states, users_to_states = parties + + self.notifier.on_new_event( + "presence_key", + stream_id, + rooms=room_ids_to_states.keys(), + users=users_to_states.keys(), + ) + + async def process_replication_rows(self, token, rows): + states = [ + UserPresenceState( + row.user_id, + row.state, + row.last_active_ts, + row.last_federation_update_ts, + row.last_user_sync_ts, + row.status_msg, + row.currently_active, + ) + for row in rows + ] + + for state in states: + self.user_to_current_state[state.user_id] = state + + stream_id = token + await self.notify_from_replication(states, stream_id) + + def get_currently_syncing_users_for_replication(self) -> Iterable[str]: + return [ + user_id + for user_id, count in self._user_to_num_current_syncs.items() + if count > 0 + ] + + async def set_state(self, target_user, state, ignore_status_msg=False): + """Set the presence state of the user.""" + presence = state["presence"] + + valid_presence = ( + PresenceState.ONLINE, + PresenceState.UNAVAILABLE, + PresenceState.OFFLINE, + PresenceState.BUSY, + ) + + if presence not in valid_presence or ( + presence == PresenceState.BUSY and not self._busy_presence_enabled + ): + raise SynapseError(400, "Invalid presence state") + + user_id = target_user.to_string() + + # If presence is disabled, no-op + if not self.hs.config.use_presence: + return + + # Proxy request to master + await self._set_state_client( + user_id=user_id, state=state, ignore_status_msg=ignore_status_msg + ) + + async def bump_presence_active_time(self, user): + """We've seen the user do something that indicates they're interacting + with the app. + """ + # If presence is disabled, no-op + if not self.hs.config.use_presence: + return + + # Proxy request to master + user_id = user.to_string() + await self._bump_active_client(user_id=user_id) + class PresenceHandler(BasePresenceHandler): def __init__(self, hs: "HomeServer"): diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index ced69ee904..ce5d651cb8 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -14,22 +14,36 @@ """A replication client for use by synapse workers. """ import logging -from typing import TYPE_CHECKING, Dict, List, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from twisted.internet.defer import Deferred from twisted.internet.protocol import ReconnectingClientFactory from synapse.api.constants import EventTypes +from synapse.federation import send_queue +from synapse.federation.sender import FederationSender from synapse.logging.context import PreserveLoggingContext, make_deferred_yieldable +from synapse.metrics.background_process_metrics import run_as_background_process from synapse.replication.tcp.protocol import ClientReplicationStreamProtocol -from synapse.replication.tcp.streams import TypingStream +from synapse.replication.tcp.streams import ( + AccountDataStream, + DeviceListsStream, + GroupServerStream, + PresenceStream, + PushersStream, + PushRulesStream, + ReceiptsStream, + TagAccountDataStream, + ToDeviceStream, + TypingStream, +) from synapse.replication.tcp.streams.events import ( EventsStream, EventsStreamEventRow, EventsStreamRow, ) -from synapse.types import PersistedEventPosition, UserID -from synapse.util.async_helpers import timeout_deferred +from synapse.types import PersistedEventPosition, ReadReceipt, UserID +from synapse.util.async_helpers import Linearizer, timeout_deferred from synapse.util.metrics import Measure if TYPE_CHECKING: @@ -105,6 +119,14 @@ def __init__(self, hs: "HomeServer"): self._instance_name = hs.get_instance_name() self._typing_handler = hs.get_typing_handler() + self._notify_pushers = hs.config.start_pushers + self._pusher_pool = hs.get_pusherpool() + self._presence_handler = hs.get_presence_handler() + + self.send_handler = None # type: Optional[FederationSenderHandler] + if hs.should_send_federation(): + self.send_handler = FederationSenderHandler(hs) + # Map from stream to list of deferreds waiting for the stream to # arrive at a particular position. The lists are sorted by stream position. self._streams_to_waiters = {} # type: Dict[str, List[Tuple[int, Deferred]]] @@ -125,13 +147,53 @@ async def on_rdata( """ self.store.process_replication_rows(stream_name, instance_name, token, rows) + if self.send_handler: + await self.send_handler.process_replication_rows(stream_name, token, rows) + 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] ) - - if stream_name == EventsStream.NAME: + elif stream_name == PushRulesStream.NAME: + self.notifier.on_new_event( + "push_rules_key", 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] + ) + elif stream_name == ReceiptsStream.NAME: + self.notifier.on_new_event( + "receipt_key", 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} + ) + 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) + elif stream_name == DeviceListsStream.NAME: + all_room_ids = set() # type: Set[str] + 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) + elif stream_name == GroupServerStream.NAME: + self.notifier.on_new_event( + "groups_key", token, users=[row.user_id for row in rows] + ) + elif stream_name == PushersStream.NAME: + for row in rows: + if row.deleted: + self.stop_pusher(row.user_id, row.app_id, row.pushkey) + else: + await self.start_pusher(row.user_id, row.app_id, row.pushkey) + elif stream_name == PresenceStream.NAME: + await self._presence_handler.process_replication_rows(token, rows) + elif stream_name == EventsStream.NAME: # We shouldn't get multiple rows per token for events stream, so # we don't need to optimise this for multiple rows. for row in rows: @@ -190,7 +252,7 @@ async def on_rdata( waiting_list[:] = waiting_list[index_of_first_deferred_not_called:] async def on_position(self, stream_name: str, instance_name: str, token: int): - self.store.process_replication_rows(stream_name, instance_name, token, []) + await self.on_rdata(stream_name, instance_name, token, []) # We poke the generic "replication" notifier to wake anything up that # may be streaming. @@ -199,6 +261,11 @@ async def on_position(self, stream_name: str, instance_name: str, token: int): def on_remote_server_up(self, server: str): """Called when get a new REMOTE_SERVER_UP command.""" + # Let's wake up the transaction queue for the server in case we have + # pending stuff to send to it. + if self.send_handler: + self.send_handler.wake_destination(server) + async def wait_for_stream_position( self, instance_name: str, stream_name: str, position: int ): @@ -235,3 +302,153 @@ async def wait_for_stream_position( logger.info( "Finished waiting for repl stream %r to reach %s", stream_name, position ) + + def stop_pusher(self, user_id, app_id, pushkey): + if not self._notify_pushers: + return + + key = "%s:%s" % (app_id, pushkey) + pushers_for_user = self._pusher_pool.pushers.get(user_id, {}) + pusher = pushers_for_user.pop(key, None) + if pusher is None: + return + logger.info("Stopping pusher %r / %r", user_id, key) + pusher.on_stop() + + async def start_pusher(self, user_id, app_id, pushkey): + if not self._notify_pushers: + return + + key = "%s:%s" % (app_id, pushkey) + logger.info("Starting pusher %r / %r", user_id, key) + return await self._pusher_pool.start_pusher_by_id(app_id, pushkey, user_id) + + +class FederationSenderHandler: + """Processes the fedration replication stream + + This class is only instantiate on the worker responsible for sending outbound + federation transactions. It receives rows from the replication stream and forwards + the appropriate entries to the FederationSender class. + """ + + def __init__(self, hs: "HomeServer"): + assert hs.should_send_federation() + + self.store = hs.get_datastore() + self._is_mine_id = hs.is_mine_id + self._hs = hs + + # We need to make a temporary value to ensure that mypy picks up the + # right type. We know we should have a federation sender instance since + # `should_send_federation` is True. + sender = hs.get_federation_sender() + assert isinstance(sender, FederationSender) + self.federation_sender = sender + + # Stores the latest position in the federation stream we've gotten up + # to. This is always set before we use it. + self.federation_position = None # type: Optional[int] + + self._fed_position_linearizer = Linearizer(name="_fed_position_linearizer") + + def wake_destination(self, server: str): + self.federation_sender.wake_destination(server) + + async def process_replication_rows(self, stream_name, token, rows): + # The federation stream contains things that we want to send out, e.g. + # presence, typing, etc. + if stream_name == "federation": + send_queue.process_rows_for_federation(self.federation_sender, rows) + await self.update_token(token) + + # ... and when new receipts happen + elif stream_name == ReceiptsStream.NAME: + await self._on_new_receipts(rows) + + # ... as well as device updates and messages + elif stream_name == DeviceListsStream.NAME: + # The entities are either user IDs (starting with '@') whose devices + # have changed, or remote servers that we need to tell about + # changes. + hosts = {row.entity for row in rows if not row.entity.startswith("@")} + for host in hosts: + self.federation_sender.send_device_messages(host) + + elif stream_name == ToDeviceStream.NAME: + # The to_device stream includes stuff to be pushed to both local + # clients and remote servers, so we ignore entities that start with + # '@' (since they'll be local users rather than destinations). + hosts = {row.entity for row in rows if not row.entity.startswith("@")} + for host in hosts: + self.federation_sender.send_device_messages(host) + + async def _on_new_receipts(self, rows): + """ + Args: + rows (Iterable[synapse.replication.tcp.streams.ReceiptsStream.ReceiptsStreamRow]): + new receipts to be processed + """ + for receipt in rows: + # we only want to send on receipts for our own users + if not self._is_mine_id(receipt.user_id): + continue + receipt_info = ReadReceipt( + receipt.room_id, + receipt.receipt_type, + receipt.user_id, + [receipt.event_id], + receipt.data, + ) + await self.federation_sender.send_read_receipt(receipt_info) + + async def update_token(self, token): + """Update the record of where we have processed to in the federation stream. + + Called after we have processed a an update received over replication. Sends + a FEDERATION_ACK back to the master, and stores the token that we have processed + in `federation_stream_position` so that we can restart where we left off. + """ + self.federation_position = token + + # We save and send the ACK to master asynchronously, so we don't block + # processing on persistence. We don't need to do this operation for + # every single RDATA we receive, we just need to do it periodically. + + if self._fed_position_linearizer.is_queued(None): + # There is already a task queued up to save and send the token, so + # no need to queue up another task. + return + + run_as_background_process("_save_and_send_ack", self._save_and_send_ack) + + async def _save_and_send_ack(self): + """Save the current federation position in the database and send an ACK + to master with where we're up to. + """ + # We should only be calling this once we've got a token. + assert self.federation_position is not None + + try: + # We linearize here to ensure we don't have races updating the token + # + # XXX this appears to be redundant, since the ReplicationCommandHandler + # has a linearizer which ensures that we only process one line of + # replication data at a time. Should we remove it, or is it doing useful + # service for robustness? Or could we replace it with an assertion that + # we're not being re-entered? + + with (await self._fed_position_linearizer.queue(None)): + # We persist and ack the same position, so we take a copy of it + # here as otherwise it can get modified from underneath us. + current_position = self.federation_position + + await self.store.update_federation_out_pos( + "federation", current_position + ) + + # We ACK this token over replication so that the master can drop + # its in memory queues + self._hs.get_tcp_replication().send_federation_ack(current_position) + except Exception: + logger.exception("Error updating federation stream position") diff --git a/synapse/server.py b/synapse/server.py index 6c35ae6e50..95a2cd2e5d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -85,7 +85,11 @@ from synapse.handlers.message import EventCreationHandler, MessageHandler from synapse.handlers.pagination import PaginationHandler from synapse.handlers.password_policy import PasswordPolicyHandler -from synapse.handlers.presence import PresenceHandler +from synapse.handlers.presence import ( + BasePresenceHandler, + PresenceHandler, + WorkerPresenceHandler, +) from synapse.handlers.profile import ProfileHandler from synapse.handlers.read_marker import ReadMarkerHandler from synapse.handlers.receipts import ReceiptsHandler @@ -415,8 +419,11 @@ def get_state_resolution_handler(self) -> StateResolutionHandler: return StateResolutionHandler(self) @cache_in_self - def get_presence_handler(self) -> PresenceHandler: - return PresenceHandler(self) + def get_presence_handler(self) -> BasePresenceHandler: + if self.config.worker_app: + return WorkerPresenceHandler(self) + else: + return PresenceHandler(self) @cache_in_self def get_typing_writer_handler(self) -> TypingWriterHandler: diff --git a/tests/replication/_base.py b/tests/replication/_base.py index 36138d69aa..c9d04aef29 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -21,13 +21,11 @@ from twisted.web.resource import Resource from twisted.web.server import Request, Site -from synapse.app.generic_worker import ( - GenericWorkerReplicationHandler, - GenericWorkerServer, -) +from synapse.app.generic_worker import GenericWorkerServer from synapse.http.server import JsonResource from synapse.http.site import SynapseRequest, SynapseSite from synapse.replication.http import ReplicationRestResource +from synapse.replication.tcp.client import ReplicationDataHandler from synapse.replication.tcp.handler import ReplicationCommandHandler from synapse.replication.tcp.protocol import ClientReplicationStreamProtocol from synapse.replication.tcp.resource import ( @@ -431,7 +429,7 @@ def connect_any_redis_attempts(self): server_protocol.makeConnection(server_to_client_transport) -class TestReplicationDataHandler(GenericWorkerReplicationHandler): +class TestReplicationDataHandler(ReplicationDataHandler): """Drop-in for ReplicationDataHandler which just collects RDATA rows""" def __init__(self, hs: HomeServer): From 05e8c70c059f8ebb066e029bc3aa3e0cefef1019 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Wed, 14 Apr 2021 18:19:02 +0200 Subject: [PATCH 050/619] Experimental Federation Speedup (#9702) This basically speeds up federation by "squeezing" each individual dual database call (to destinations and destination_rooms), which previously happened per every event, into one call for an entire batch (100 max). Signed-off-by: Jonathan de Jong --- changelog.d/9702.misc | 1 + contrib/experiments/test_messaging.py | 42 +++--- synapse/federation/sender/__init__.py | 140 +++++++++++------- .../sender/per_destination_queue.py | 15 +- .../storage/databases/main/transactions.py | 28 ++-- 5 files changed, 129 insertions(+), 97 deletions(-) create mode 100644 changelog.d/9702.misc diff --git a/changelog.d/9702.misc b/changelog.d/9702.misc new file mode 100644 index 0000000000..c6e63450a9 --- /dev/null +++ b/changelog.d/9702.misc @@ -0,0 +1 @@ +Speed up federation transmission by using fewer database calls. Contributed by @ShadowJonathan. diff --git a/contrib/experiments/test_messaging.py b/contrib/experiments/test_messaging.py index 31b8a68225..5dd172052b 100644 --- a/contrib/experiments/test_messaging.py +++ b/contrib/experiments/test_messaging.py @@ -224,14 +224,16 @@ def send_message(self, room_name, sender, body): destinations = yield self.get_servers_for_context(room_name) try: - yield self.replication_layer.send_pdu( - Pdu.create_new( - context=room_name, - pdu_type="sy.room.message", - content={"sender": sender, "body": body}, - origin=self.server_name, - destinations=destinations, - ) + yield self.replication_layer.send_pdus( + [ + Pdu.create_new( + context=room_name, + pdu_type="sy.room.message", + content={"sender": sender, "body": body}, + origin=self.server_name, + destinations=destinations, + ) + ] ) except Exception as e: logger.exception(e) @@ -253,7 +255,7 @@ def join_room(self, room_name, sender, joinee): origin=self.server_name, destinations=destinations, ) - yield self.replication_layer.send_pdu(pdu) + yield self.replication_layer.send_pdus([pdu]) except Exception as e: logger.exception(e) @@ -265,16 +267,18 @@ def invite_to_room(self, room_name, sender, invitee): destinations = yield self.get_servers_for_context(room_name) try: - yield self.replication_layer.send_pdu( - Pdu.create_new( - context=room_name, - is_state=True, - pdu_type="sy.room.member", - state_key=invitee, - content={"membership": "invite"}, - origin=self.server_name, - destinations=destinations, - ) + yield self.replication_layer.send_pdus( + [ + Pdu.create_new( + context=room_name, + is_state=True, + pdu_type="sy.room.member", + state_key=invitee, + content={"membership": "invite"}, + origin=self.server_name, + destinations=destinations, + ) + ] ) except Exception as e: logger.exception(e) diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 155161685d..952ad39f8c 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -18,8 +18,6 @@ from prometheus_client import Counter -from twisted.internet import defer - import synapse.metrics from synapse.api.presence import UserPresenceState from synapse.events import EventBase @@ -27,11 +25,7 @@ from synapse.federation.sender.transaction_manager import TransactionManager from synapse.federation.units import Edu from synapse.handlers.presence import get_interested_remotes -from synapse.logging.context import ( - make_deferred_yieldable, - preserve_fn, - run_in_background, -) +from synapse.logging.context import preserve_fn from synapse.metrics import ( LaterGauge, event_processing_loop_counter, @@ -39,7 +33,7 @@ events_processed_counter, ) from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.types import JsonDict, ReadReceipt, RoomStreamToken +from synapse.types import Collection, JsonDict, ReadReceipt, RoomStreamToken from synapse.util.metrics import Measure, measure_func if TYPE_CHECKING: @@ -276,15 +270,27 @@ async def _process_event_queue_loop(self) -> None: if not events and next_token >= self._last_poked_id: break - async def handle_event(event: EventBase) -> None: + async def get_destinations_for_event( + event: EventBase, + ) -> Collection[str]: + """Computes the destinations to which this event must be sent. + + This returns an empty tuple when there are no destinations to send to, + or if this event is not from this homeserver and it is not sending + it on behalf of another server. + + Will also filter out destinations which this sender is not responsible for, + if multiple federation senders exist. + """ + # Only send events for this server. send_on_behalf_of = event.internal_metadata.get_send_on_behalf_of() is_mine = self.is_mine_id(event.sender) if not is_mine and send_on_behalf_of is None: - return + return () if not event.internal_metadata.should_proactively_send(): - return + return () destinations = None # type: Optional[Set[str]] if not event.prev_event_ids(): @@ -319,7 +325,7 @@ async def handle_event(event: EventBase) -> None: "Failed to calculate hosts in room for event: %s", event.event_id, ) - return + return () destinations = { d @@ -329,17 +335,15 @@ async def handle_event(event: EventBase) -> None: ) } + destinations.discard(self.server_name) + if send_on_behalf_of is not None: # If we are sending the event on behalf of another server # then it already has the event and there is no reason to # send the event to it. destinations.discard(send_on_behalf_of) - logger.debug("Sending %s to %r", event, destinations) - if destinations: - await self._send_pdu(event, destinations) - now = self.clock.time_msec() ts = await self.store.get_received_ts(event.event_id) @@ -347,24 +351,29 @@ async def handle_event(event: EventBase) -> None: "federation_sender" ).observe((now - ts) / 1000) - async def handle_room_events(events: Iterable[EventBase]) -> None: - with Measure(self.clock, "handle_room_events"): - for event in events: - await handle_event(event) - - events_by_room = {} # type: Dict[str, List[EventBase]] - for event in events: - events_by_room.setdefault(event.room_id, []).append(event) - - await make_deferred_yieldable( - defer.gatherResults( - [ - run_in_background(handle_room_events, evs) - for evs in events_by_room.values() - ], - consumeErrors=True, - ) - ) + return destinations + return () + + async def get_federatable_events_and_destinations( + events: Iterable[EventBase], + ) -> List[Tuple[EventBase, Collection[str]]]: + with Measure(self.clock, "get_destinations_for_events"): + # Fetch federation destinations per event, + # skip if get_destinations_for_event returns an empty collection, + # return list of event->destinations pairs. + return [ + (event, dests) + for (event, dests) in [ + (event, await get_destinations_for_event(event)) + for event in events + ] + if dests + ] + + events_and_dests = await get_federatable_events_and_destinations(events) + + # Send corresponding events to each destination queue + await self._distribute_events(events_and_dests) await self.store.update_federation_out_pos("events", next_token) @@ -382,7 +391,7 @@ async def handle_room_events(events: Iterable[EventBase]) -> None: events_processed_counter.inc(len(events)) event_processing_loop_room_count.labels("federation_sender").inc( - len(events_by_room) + len({event.room_id for event in events}) ) event_processing_loop_counter.labels("federation_sender").inc() @@ -394,34 +403,53 @@ async def handle_room_events(events: Iterable[EventBase]) -> None: finally: self._is_processing = False - async def _send_pdu(self, pdu: EventBase, destinations: Iterable[str]) -> None: - # We loop through all destinations to see whether we already have - # a transaction in progress. If we do, stick it in the pending_pdus - # table and we'll get back to it later. + async def _distribute_events( + self, + events_and_dests: Iterable[Tuple[EventBase, Collection[str]]], + ) -> None: + """Distribute events to the respective per_destination queues. - destinations = set(destinations) - destinations.discard(self.server_name) - logger.debug("Sending to: %s", str(destinations)) + Also persists last-seen per-room stream_ordering to 'destination_rooms'. - if not destinations: - return + Args: + events_and_dests: A list of tuples, which are (event: EventBase, destinations: Collection[str]). + Every event is paired with its intended destinations (in federation). + """ + # Tuples of room_id + destination to their max-seen stream_ordering + room_with_dest_stream_ordering = {} # type: Dict[Tuple[str, str], int] - sent_pdus_destination_dist_total.inc(len(destinations)) - sent_pdus_destination_dist_count.inc() + # List of events to send to each destination + events_by_dest = {} # type: Dict[str, List[EventBase]] - assert pdu.internal_metadata.stream_ordering + # For each event-destinations pair... + for event, destinations in events_and_dests: - # track the fact that we have a PDU for these destinations, - # to allow us to perform catch-up later on if the remote is unreachable - # for a while. - await self.store.store_destination_rooms_entries( - destinations, - pdu.room_id, - pdu.internal_metadata.stream_ordering, + # (we got this from the database, it's filled) + assert event.internal_metadata.stream_ordering + + sent_pdus_destination_dist_total.inc(len(destinations)) + sent_pdus_destination_dist_count.inc() + + # ...iterate over those destinations.. + for destination in destinations: + # ...update their stream-ordering... + room_with_dest_stream_ordering[(event.room_id, destination)] = max( + event.internal_metadata.stream_ordering, + room_with_dest_stream_ordering.get((event.room_id, destination), 0), + ) + + # ...and add the event to each destination queue. + events_by_dest.setdefault(destination, []).append(event) + + # Bulk-store destination_rooms stream_ids + await self.store.bulk_store_destination_rooms_entries( + room_with_dest_stream_ordering ) - for destination in destinations: - self._get_per_destination_queue(destination).send_pdu(pdu) + for destination, pdus in events_by_dest.items(): + logger.debug("Sending %d pdus to %s", len(pdus), destination) + + self._get_per_destination_queue(destination).send_pdus(pdus) async def send_read_receipt(self, receipt: ReadReceipt) -> None: """Send a RR to any other servers in the room diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index 3b053ebcfb..3bb66bce32 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -154,19 +154,22 @@ def pending_edu_count(self) -> int: + len(self._pending_edus_keyed) ) - def send_pdu(self, pdu: EventBase) -> None: - """Add a PDU to the queue, and start the transmission loop if necessary + def send_pdus(self, pdus: Iterable[EventBase]) -> None: + """Add PDUs to the queue, and start the transmission loop if necessary Args: - pdu: pdu to send + pdus: pdus to send """ if not self._catching_up or self._last_successful_stream_ordering is None: # only enqueue the PDU if we are not catching up (False) or do not # yet know if we have anything to catch up (None) - self._pending_pdus.append(pdu) + self._pending_pdus.extend(pdus) else: - assert pdu.internal_metadata.stream_ordering - self._catchup_last_skipped = pdu.internal_metadata.stream_ordering + self._catchup_last_skipped = max( + pdu.internal_metadata.stream_ordering + for pdu in pdus + if pdu.internal_metadata.stream_ordering is not None + ) self.attempt_new_transaction() diff --git a/synapse/storage/databases/main/transactions.py b/synapse/storage/databases/main/transactions.py index 82335e7a9d..b28ca61f80 100644 --- a/synapse/storage/databases/main/transactions.py +++ b/synapse/storage/databases/main/transactions.py @@ -14,7 +14,7 @@ import logging from collections import namedtuple -from typing import Iterable, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple from canonicaljson import encode_canonical_json @@ -295,37 +295,33 @@ def _set_destination_retry_timings_emulated( }, ) - async def store_destination_rooms_entries( - self, - destinations: Iterable[str], - room_id: str, - stream_ordering: int, - ) -> None: + async def bulk_store_destination_rooms_entries( + self, room_and_destination_to_ordering: Dict[Tuple[str, str], int] + ): """ - Updates or creates `destination_rooms` entries in batch for a single event. + Updates or creates `destination_rooms` entries for a number of events. Args: - destinations: list of destinations - room_id: the room_id of the event - stream_ordering: the stream_ordering of the event + room_and_destination_to_ordering: A mapping of (room, destination) -> stream_id """ await self.db_pool.simple_upsert_many( table="destinations", key_names=("destination",), - key_values=[(d,) for d in destinations], + key_values={(d,) for _, d in room_and_destination_to_ordering.keys()}, value_names=[], value_values=[], desc="store_destination_rooms_entries_dests", ) - rows = [(destination, room_id) for destination in destinations] await self.db_pool.simple_upsert_many( table="destination_rooms", - key_names=("destination", "room_id"), - key_values=rows, + key_names=("room_id", "destination"), + key_values=list(room_and_destination_to_ordering.keys()), value_names=["stream_ordering"], - value_values=[(stream_ordering,)] * len(rows), + value_values=[ + (stream_id,) for stream_id in room_and_destination_to_ordering.values() + ], desc="store_destination_rooms_entries_rooms", ) From cc51aaaa7adb0ec2235e027b5184ebda9b660ec4 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 14 Apr 2021 12:32:20 -0400 Subject: [PATCH 051/619] Check for space membership during a remote join of a restricted room. (#9763) When receiving a /send_join request for a room with join rules set to 'restricted', check if the user is a member of the spaces defined in the 'allow' key of the join rules. This only applies to an experimental room version, as defined in MSC3083. --- changelog.d/9763.feature | 1 + changelog.d/9800.feature | 1 + synapse/handlers/event_auth.py | 82 ++++++++++++ synapse/handlers/federation.py | 212 +++++++++++++++++++++----------- synapse/handlers/room_member.py | 62 +--------- synapse/server.py | 5 + tests/test_federation.py | 6 +- 7 files changed, 238 insertions(+), 131 deletions(-) create mode 100644 changelog.d/9763.feature create mode 100644 changelog.d/9800.feature create mode 100644 synapse/handlers/event_auth.py diff --git a/changelog.d/9763.feature b/changelog.d/9763.feature new file mode 100644 index 0000000000..9404ad2fc0 --- /dev/null +++ b/changelog.d/9763.feature @@ -0,0 +1 @@ +Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/changelog.d/9800.feature b/changelog.d/9800.feature new file mode 100644 index 0000000000..9404ad2fc0 --- /dev/null +++ b/changelog.d/9800.feature @@ -0,0 +1 @@ +Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py new file mode 100644 index 0000000000..06da1a93d9 --- /dev/null +++ b/synapse/handlers/event_auth.py @@ -0,0 +1,82 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TYPE_CHECKING + +from synapse.api.constants import EventTypes, JoinRules +from synapse.api.room_versions import RoomVersion +from synapse.types import StateMap + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +class EventAuthHandler: + def __init__(self, hs: "HomeServer"): + self._store = hs.get_datastore() + + async def can_join_without_invite( + self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str + ) -> bool: + """ + Check whether a user can join a room without an invite. + + When joining a room with restricted joined rules (as defined in MSC3083), + the membership of spaces must be checked during join. + + Args: + state_ids: The state of the room as it currently is. + room_version: The room version of the room being joined. + user_id: The user joining the room. + + Returns: + True if the user can join the room, false otherwise. + """ + # This only applies to room versions which support the new join rule. + if not room_version.msc3083_join_rules: + return True + + # If there's no join rule, then it defaults to public (so this doesn't apply). + join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) + if not join_rules_event_id: + return True + + # If the join rule is not restricted, this doesn't apply. + join_rules_event = await self._store.get_event(join_rules_event_id) + if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: + return True + + # If allowed is of the wrong form, then only allow invited users. + allowed_spaces = join_rules_event.content.get("allow", []) + if not isinstance(allowed_spaces, list): + return False + + # Get the list of joined rooms and see if there's an overlap. + joined_rooms = await self._store.get_rooms_for_user(user_id) + + # Pull out the other room IDs, invalid data gets filtered. + for space in allowed_spaces: + if not isinstance(space, dict): + continue + + space_id = space.get("space") + if not isinstance(space_id, str): + continue + + # The user was joined to one of the spaces specified, they can join + # this room! + if space_id in joined_rooms: + return True + + # The user was not in any of the required spaces. + return False diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index fe1d83f6b8..0c9bdf51a4 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -103,7 +103,7 @@ @attr.s(slots=True) class _NewEventInfo: - """Holds information about a received event, ready for passing to _handle_new_events + """Holds information about a received event, ready for passing to _auth_and_persist_events Attributes: event: the received event @@ -146,6 +146,7 @@ def __init__(self, hs: "HomeServer"): self.is_mine_id = hs.is_mine_id self.spam_checker = hs.get_spam_checker() 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._server_notices_mxid = hs.config.server_notices_mxid self.config = hs.config @@ -807,7 +808,10 @@ async def _process_received_pdu( logger.debug("Processing event: %s", event) try: - await self._handle_new_event(origin, event, state=state) + context = await self.state_handler.compute_event_context( + event, old_state=state + ) + await self._auth_and_persist_event(origin, event, context, state=state) except AuthError as e: raise FederationError("ERROR", e.code, e.msg, affected=event.event_id) @@ -1010,7 +1014,9 @@ async def backfill( ) if ev_infos: - await self._handle_new_events(dest, room_id, ev_infos, backfilled=True) + await self._auth_and_persist_events( + dest, room_id, ev_infos, backfilled=True + ) # Step 2: Persist the rest of the events in the chunk one by one events.sort(key=lambda e: e.depth) @@ -1023,10 +1029,12 @@ async def backfill( # non-outliers assert not event.internal_metadata.is_outlier() + context = await self.state_handler.compute_event_context(event) + # We store these one at a time since each event depends on the # previous to work out the state. # TODO: We can probably do something more clever here. - await self._handle_new_event(dest, event, backfilled=True) + await self._auth_and_persist_event(dest, event, context, backfilled=True) return events @@ -1360,7 +1368,7 @@ async def get_event(event_id: str): event_infos.append(_NewEventInfo(event, None, auth)) - await self._handle_new_events( + await self._auth_and_persist_events( destination, room_id, event_infos, @@ -1666,16 +1674,47 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: # would introduce the danger of backwards-compatibility problems. event.internal_metadata.send_on_behalf_of = origin - context = await self._handle_new_event(origin, event) + # Calculate the event context. + context = await self.state_handler.compute_event_context(event) + + # Get the state before the new event. + prev_state_ids = await context.get_prev_state_ids() + + # Check if the user is already in the room or invited to the room. + user_id = event.state_key + prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None) + newly_joined = True + is_invite = False + if prev_member_event_id: + prev_member_event = await self.store.get_event(prev_member_event_id) + newly_joined = prev_member_event.membership != Membership.JOIN + is_invite = prev_member_event.membership == Membership.INVITE + + # If the member is not already in the room, and not invited, check if + # they should be allowed access via membership in a space. + if ( + newly_joined + and not is_invite + and not await self.event_auth_handler.can_join_without_invite( + prev_state_ids, + event.room_version, + user_id, + ) + ): + raise SynapseError( + 400, + "You do not belong to any of the required spaces to join this room.", + ) + + # Persist the event. + await self._auth_and_persist_event(origin, event, context) logger.debug( - "on_send_join_request: After _handle_new_event: %s, sigs: %s", + "on_send_join_request: After _auth_and_persist_event: %s, sigs: %s", event.event_id, event.signatures, ) - prev_state_ids = await context.get_prev_state_ids() - state_ids = list(prev_state_ids.values()) auth_chain = await self.store.get_auth_chain(event.room_id, state_ids) @@ -1878,10 +1917,11 @@ async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: event.internal_metadata.outlier = False - await self._handle_new_event(origin, event) + context = await self.state_handler.compute_event_context(event) + await self._auth_and_persist_event(origin, event, context) logger.debug( - "on_send_leave_request: After _handle_new_event: %s, sigs: %s", + "on_send_leave_request: After _auth_and_persist_event: %s, sigs: %s", event.event_id, event.signatures, ) @@ -1989,16 +2029,44 @@ async def get_persisted_pdu( async def get_min_depth_for_context(self, context: str) -> int: return await self.store.get_min_depth(context) - async def _handle_new_event( + async def _auth_and_persist_event( self, origin: str, event: EventBase, + context: EventContext, state: Optional[Iterable[EventBase]] = None, auth_events: Optional[MutableStateMap[EventBase]] = None, backfilled: bool = False, - ) -> EventContext: - context = await self._prep_event( - origin, event, state=state, auth_events=auth_events, backfilled=backfilled + ) -> None: + """ + Process an event by performing auth checks and then persisting to the database. + + Args: + origin: The host the event originates from. + event: The event itself. + context: + The event context. + + NB that this function potentially modifies it. + state: + The state events used to auth the event. If this is not provided + the current state events will be used. + auth_events: + Map from (event_type, state_key) to event + + Normally, our calculated auth_events based on the state of the room + at the event's position in the DAG, though occasionally (eg if the + event is an outlier), may be the auth events claimed by the remote + server. + backfilled: True if the event was backfilled. + """ + context = await self._check_event_auth( + origin, + event, + context, + state=state, + auth_events=auth_events, + backfilled=backfilled, ) try: @@ -2020,9 +2088,7 @@ async def _handle_new_event( ) raise - return context - - async def _handle_new_events( + async def _auth_and_persist_events( self, origin: str, room_id: str, @@ -2040,9 +2106,13 @@ async def _handle_new_events( async def prep(ev_info: _NewEventInfo): event = ev_info.event with nested_logging_context(suffix=event.event_id): - res = await self._prep_event( + res = await self.state_handler.compute_event_context( + event, old_state=ev_info.state + ) + res = await self._check_event_auth( origin, event, + res, state=ev_info.state, auth_events=ev_info.auth_events, backfilled=backfilled, @@ -2177,49 +2247,6 @@ async def _persist_auth_tree( room_id, [(event, new_event_context)] ) - async def _prep_event( - self, - origin: str, - event: EventBase, - state: Optional[Iterable[EventBase]], - auth_events: Optional[MutableStateMap[EventBase]], - backfilled: bool, - ) -> EventContext: - context = await self.state_handler.compute_event_context(event, old_state=state) - - if not auth_events: - prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self.auth.compute_auth_events( - event, prev_state_ids, for_verification=True - ) - auth_events_x = await self.store.get_events(auth_events_ids) - auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()} - - # This is a hack to fix some old rooms where the initial join event - # didn't reference the create event in its auth events. - if event.type == EventTypes.Member and not event.auth_event_ids(): - if len(event.prev_event_ids()) == 1 and event.depth < 5: - c = await self.store.get_event( - event.prev_event_ids()[0], allow_none=True - ) - if c and c.type == EventTypes.Create: - auth_events[(c.type, c.state_key)] = c - - context = await self.do_auth(origin, event, context, auth_events=auth_events) - - if not context.rejected: - await self._check_for_soft_fail(event, state, backfilled) - - if event.type == EventTypes.GuestAccess and not context.rejected: - await self.maybe_kick_guest_users(event) - - # If we are going to send this event over federation we precaclculate - # the joined hosts. - if event.internal_metadata.get_send_on_behalf_of(): - await self.event_creation_handler.cache_joined_hosts_for_event(event) - - return context - async def _check_for_soft_fail( self, event: EventBase, state: Optional[Iterable[EventBase]], backfilled: bool ) -> None: @@ -2330,19 +2357,28 @@ async def on_get_missing_events( return missing_events - async def do_auth( + async def _check_event_auth( self, origin: str, event: EventBase, context: EventContext, - auth_events: MutableStateMap[EventBase], + state: Optional[Iterable[EventBase]], + auth_events: Optional[MutableStateMap[EventBase]], + backfilled: bool, ) -> EventContext: """ + Checks whether an event should be rejected (for failing auth checks). Args: - origin: - event: + origin: The host the event originates from. + event: The event itself. context: + The event context. + + NB that this function potentially modifies it. + state: + The state events used to auth the event. If this is not provided + the current state events will be used. auth_events: Map from (event_type, state_key) to event @@ -2352,12 +2388,32 @@ async def do_auth( server. Also NB that this function adds entries to it. + backfilled: True if the event was backfilled. + Returns: - updated context object + The updated context object. """ room_version = await self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] + if not auth_events: + prev_state_ids = await context.get_prev_state_ids() + auth_events_ids = self.auth.compute_auth_events( + event, prev_state_ids, for_verification=True + ) + auth_events_x = await self.store.get_events(auth_events_ids) + auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()} + + # This is a hack to fix some old rooms where the initial join event + # didn't reference the create event in its auth events. + if event.type == EventTypes.Member and not event.auth_event_ids(): + if len(event.prev_event_ids()) == 1 and event.depth < 5: + c = await self.store.get_event( + event.prev_event_ids()[0], allow_none=True + ) + if c and c.type == EventTypes.Create: + auth_events[(c.type, c.state_key)] = c + try: context = await self._update_auth_events_and_context_for_auth( origin, event, context, auth_events @@ -2379,6 +2435,17 @@ async def do_auth( logger.warning("Failed auth resolution for %r because %s", event, e) context.rejected = RejectedReason.AUTH_ERROR + if not context.rejected: + await self._check_for_soft_fail(event, state, backfilled) + + if event.type == EventTypes.GuestAccess and not context.rejected: + await self.maybe_kick_guest_users(event) + + # If we are going to send this event over federation we precaclculate + # the joined hosts. + if event.internal_metadata.get_send_on_behalf_of(): + await self.event_creation_handler.cache_joined_hosts_for_event(event) + return context async def _update_auth_events_and_context_for_auth( @@ -2388,7 +2455,7 @@ async def _update_auth_events_and_context_for_auth( context: EventContext, auth_events: MutableStateMap[EventBase], ) -> EventContext: - """Helper for do_auth. See there for docs. + """Helper for _check_event_auth. See there for docs. Checks whether a given event has the expected auth events. If it doesn't then we talk to the remote server to compare state to see if @@ -2468,9 +2535,14 @@ async def _update_auth_events_and_context_for_auth( e.internal_metadata.outlier = True logger.debug( - "do_auth %s missing_auth: %s", event.event_id, e.event_id + "_check_event_auth %s missing_auth: %s", + event.event_id, + e.event_id, + ) + context = await self.state_handler.compute_event_context(e) + await self._auth_and_persist_event( + origin, e, context, auth_events=auth ) - await self._handle_new_event(origin, e, auth_events=auth) if e.event_id in event_auth_events: auth_events[(e.type, e.state_key)] = e diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 2bbfac6471..2c5bada1d8 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple from synapse import types -from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules, Membership +from synapse.api.constants import AccountDataTypes, EventTypes, Membership from synapse.api.errors import ( AuthError, Codes, @@ -28,7 +28,6 @@ SynapseError, ) from synapse.api.ratelimiting import Ratelimiter -from synapse.api.room_versions import RoomVersion from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID @@ -64,6 +63,7 @@ def __init__(self, hs: "HomeServer"): self.profile_handler = hs.get_profile_handler() self.event_creation_handler = hs.get_event_creation_handler() self.account_data_handler = hs.get_account_data_handler() + self.event_auth_handler = hs.get_event_auth_handler() self.member_linearizer = Linearizer(name="member") @@ -178,62 +178,6 @@ async def ratelimit_invite( await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id) - async def _can_join_without_invite( - self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str - ) -> bool: - """ - Check whether a user can join a room without an invite. - - When joining a room with restricted joined rules (as defined in MSC3083), - the membership of spaces must be checked during join. - - Args: - state_ids: The state of the room as it currently is. - room_version: The room version of the room being joined. - user_id: The user joining the room. - - Returns: - True if the user can join the room, false otherwise. - """ - # This only applies to room versions which support the new join rule. - if not room_version.msc3083_join_rules: - return True - - # If there's no join rule, then it defaults to public (so this doesn't apply). - join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) - if not join_rules_event_id: - return True - - # If the join rule is not restricted, this doesn't apply. - join_rules_event = await self.store.get_event(join_rules_event_id) - if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: - return True - - # If allowed is of the wrong form, then only allow invited users. - allowed_spaces = join_rules_event.content.get("allow", []) - if not isinstance(allowed_spaces, list): - return False - - # Get the list of joined rooms and see if there's an overlap. - joined_rooms = await self.store.get_rooms_for_user(user_id) - - # Pull out the other room IDs, invalid data gets filtered. - for space in allowed_spaces: - if not isinstance(space, dict): - continue - - space_id = space.get("space") - if not isinstance(space_id, str): - continue - - # The user was joined to one of the spaces specified, they can join - # this room! - if space_id in joined_rooms: - return True - - # The user was not in any of the required spaces. - return False - async def _local_membership_update( self, requester: Requester, @@ -302,7 +246,7 @@ async def _local_membership_update( if ( newly_joined and not user_is_invited - and not await self._can_join_without_invite( + and not await self.event_auth_handler.can_join_without_invite( prev_state_ids, event.room_version, user_id ) ): diff --git a/synapse/server.py b/synapse/server.py index 95a2cd2e5d..045b8f3fca 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -77,6 +77,7 @@ from synapse.handlers.directory import DirectoryHandler from synapse.handlers.e2e_keys import E2eKeysHandler from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler +from synapse.handlers.event_auth import EventAuthHandler from synapse.handlers.events import EventHandler, EventStreamHandler from synapse.handlers.federation import FederationHandler from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler @@ -749,6 +750,10 @@ def get_account_data_handler(self) -> AccountDataHandler: def get_space_summary_handler(self) -> SpaceSummaryHandler: return SpaceSummaryHandler(self) + @cache_in_self + def get_event_auth_handler(self) -> EventAuthHandler: + return EventAuthHandler(self) + @cache_in_self def get_external_cache(self) -> ExternalCache: return ExternalCache(self) diff --git a/tests/test_federation.py b/tests/test_federation.py index 86a44a13da..0a3a996ec1 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -75,8 +75,10 @@ def setUp(self): ) self.handler = self.homeserver.get_federation_handler() - self.handler.do_auth = lambda origin, event, context, auth_events: succeed( - context + self.handler._check_event_auth = ( + lambda origin, event, context, state, auth_events, backfilled: succeed( + context + ) ) self.client = self.homeserver.get_federation_client() self.client._check_sigs_and_hash_and_fetch = lambda dest, pdus, **k: succeed( From e8816c6aced208cdf1310393a212b5109892ffbf Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 14 Apr 2021 12:33:37 -0400 Subject: [PATCH 052/619] Revert "Check for space membership during a remote join of a restricted room. (#9763)" This reverts commit cc51aaaa7adb0ec2235e027b5184ebda9b660ec4. The PR was prematurely merged and not yet approved. --- changelog.d/9763.feature | 1 - changelog.d/9800.feature | 1 - synapse/handlers/event_auth.py | 82 ------------ synapse/handlers/federation.py | 212 +++++++++++--------------------- synapse/handlers/room_member.py | 62 +++++++++- synapse/server.py | 5 - tests/test_federation.py | 6 +- 7 files changed, 131 insertions(+), 238 deletions(-) delete mode 100644 changelog.d/9763.feature delete mode 100644 changelog.d/9800.feature delete mode 100644 synapse/handlers/event_auth.py diff --git a/changelog.d/9763.feature b/changelog.d/9763.feature deleted file mode 100644 index 9404ad2fc0..0000000000 --- a/changelog.d/9763.feature +++ /dev/null @@ -1 +0,0 @@ -Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/changelog.d/9800.feature b/changelog.d/9800.feature deleted file mode 100644 index 9404ad2fc0..0000000000 --- a/changelog.d/9800.feature +++ /dev/null @@ -1 +0,0 @@ -Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py deleted file mode 100644 index 06da1a93d9..0000000000 --- a/synapse/handlers/event_auth.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2021 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from typing import TYPE_CHECKING - -from synapse.api.constants import EventTypes, JoinRules -from synapse.api.room_versions import RoomVersion -from synapse.types import StateMap - -if TYPE_CHECKING: - from synapse.server import HomeServer - - -class EventAuthHandler: - def __init__(self, hs: "HomeServer"): - self._store = hs.get_datastore() - - async def can_join_without_invite( - self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str - ) -> bool: - """ - Check whether a user can join a room without an invite. - - When joining a room with restricted joined rules (as defined in MSC3083), - the membership of spaces must be checked during join. - - Args: - state_ids: The state of the room as it currently is. - room_version: The room version of the room being joined. - user_id: The user joining the room. - - Returns: - True if the user can join the room, false otherwise. - """ - # This only applies to room versions which support the new join rule. - if not room_version.msc3083_join_rules: - return True - - # If there's no join rule, then it defaults to public (so this doesn't apply). - join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) - if not join_rules_event_id: - return True - - # If the join rule is not restricted, this doesn't apply. - join_rules_event = await self._store.get_event(join_rules_event_id) - if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: - return True - - # If allowed is of the wrong form, then only allow invited users. - allowed_spaces = join_rules_event.content.get("allow", []) - if not isinstance(allowed_spaces, list): - return False - - # Get the list of joined rooms and see if there's an overlap. - joined_rooms = await self._store.get_rooms_for_user(user_id) - - # Pull out the other room IDs, invalid data gets filtered. - for space in allowed_spaces: - if not isinstance(space, dict): - continue - - space_id = space.get("space") - if not isinstance(space_id, str): - continue - - # The user was joined to one of the spaces specified, they can join - # this room! - if space_id in joined_rooms: - return True - - # The user was not in any of the required spaces. - return False diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 0c9bdf51a4..fe1d83f6b8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -103,7 +103,7 @@ @attr.s(slots=True) class _NewEventInfo: - """Holds information about a received event, ready for passing to _auth_and_persist_events + """Holds information about a received event, ready for passing to _handle_new_events Attributes: event: the received event @@ -146,7 +146,6 @@ def __init__(self, hs: "HomeServer"): self.is_mine_id = hs.is_mine_id self.spam_checker = hs.get_spam_checker() 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._server_notices_mxid = hs.config.server_notices_mxid self.config = hs.config @@ -808,10 +807,7 @@ async def _process_received_pdu( logger.debug("Processing event: %s", event) try: - context = await self.state_handler.compute_event_context( - event, old_state=state - ) - await self._auth_and_persist_event(origin, event, context, state=state) + await self._handle_new_event(origin, event, state=state) except AuthError as e: raise FederationError("ERROR", e.code, e.msg, affected=event.event_id) @@ -1014,9 +1010,7 @@ async def backfill( ) if ev_infos: - await self._auth_and_persist_events( - dest, room_id, ev_infos, backfilled=True - ) + await self._handle_new_events(dest, room_id, ev_infos, backfilled=True) # Step 2: Persist the rest of the events in the chunk one by one events.sort(key=lambda e: e.depth) @@ -1029,12 +1023,10 @@ async def backfill( # non-outliers assert not event.internal_metadata.is_outlier() - context = await self.state_handler.compute_event_context(event) - # We store these one at a time since each event depends on the # previous to work out the state. # TODO: We can probably do something more clever here. - await self._auth_and_persist_event(dest, event, context, backfilled=True) + await self._handle_new_event(dest, event, backfilled=True) return events @@ -1368,7 +1360,7 @@ async def get_event(event_id: str): event_infos.append(_NewEventInfo(event, None, auth)) - await self._auth_and_persist_events( + await self._handle_new_events( destination, room_id, event_infos, @@ -1674,47 +1666,16 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: # would introduce the danger of backwards-compatibility problems. event.internal_metadata.send_on_behalf_of = origin - # Calculate the event context. - context = await self.state_handler.compute_event_context(event) - - # Get the state before the new event. - prev_state_ids = await context.get_prev_state_ids() - - # Check if the user is already in the room or invited to the room. - user_id = event.state_key - prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None) - newly_joined = True - is_invite = False - if prev_member_event_id: - prev_member_event = await self.store.get_event(prev_member_event_id) - newly_joined = prev_member_event.membership != Membership.JOIN - is_invite = prev_member_event.membership == Membership.INVITE - - # If the member is not already in the room, and not invited, check if - # they should be allowed access via membership in a space. - if ( - newly_joined - and not is_invite - and not await self.event_auth_handler.can_join_without_invite( - prev_state_ids, - event.room_version, - user_id, - ) - ): - raise SynapseError( - 400, - "You do not belong to any of the required spaces to join this room.", - ) - - # Persist the event. - await self._auth_and_persist_event(origin, event, context) + context = await self._handle_new_event(origin, event) logger.debug( - "on_send_join_request: After _auth_and_persist_event: %s, sigs: %s", + "on_send_join_request: After _handle_new_event: %s, sigs: %s", event.event_id, event.signatures, ) + prev_state_ids = await context.get_prev_state_ids() + state_ids = list(prev_state_ids.values()) auth_chain = await self.store.get_auth_chain(event.room_id, state_ids) @@ -1917,11 +1878,10 @@ async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: event.internal_metadata.outlier = False - context = await self.state_handler.compute_event_context(event) - await self._auth_and_persist_event(origin, event, context) + await self._handle_new_event(origin, event) logger.debug( - "on_send_leave_request: After _auth_and_persist_event: %s, sigs: %s", + "on_send_leave_request: After _handle_new_event: %s, sigs: %s", event.event_id, event.signatures, ) @@ -2029,44 +1989,16 @@ async def get_persisted_pdu( async def get_min_depth_for_context(self, context: str) -> int: return await self.store.get_min_depth(context) - async def _auth_and_persist_event( + async def _handle_new_event( self, origin: str, event: EventBase, - context: EventContext, state: Optional[Iterable[EventBase]] = None, auth_events: Optional[MutableStateMap[EventBase]] = None, backfilled: bool = False, - ) -> None: - """ - Process an event by performing auth checks and then persisting to the database. - - Args: - origin: The host the event originates from. - event: The event itself. - context: - The event context. - - NB that this function potentially modifies it. - state: - The state events used to auth the event. If this is not provided - the current state events will be used. - auth_events: - Map from (event_type, state_key) to event - - Normally, our calculated auth_events based on the state of the room - at the event's position in the DAG, though occasionally (eg if the - event is an outlier), may be the auth events claimed by the remote - server. - backfilled: True if the event was backfilled. - """ - context = await self._check_event_auth( - origin, - event, - context, - state=state, - auth_events=auth_events, - backfilled=backfilled, + ) -> EventContext: + context = await self._prep_event( + origin, event, state=state, auth_events=auth_events, backfilled=backfilled ) try: @@ -2088,7 +2020,9 @@ async def _auth_and_persist_event( ) raise - async def _auth_and_persist_events( + return context + + async def _handle_new_events( self, origin: str, room_id: str, @@ -2106,13 +2040,9 @@ async def _auth_and_persist_events( async def prep(ev_info: _NewEventInfo): event = ev_info.event with nested_logging_context(suffix=event.event_id): - res = await self.state_handler.compute_event_context( - event, old_state=ev_info.state - ) - res = await self._check_event_auth( + res = await self._prep_event( origin, event, - res, state=ev_info.state, auth_events=ev_info.auth_events, backfilled=backfilled, @@ -2247,6 +2177,49 @@ async def _persist_auth_tree( room_id, [(event, new_event_context)] ) + async def _prep_event( + self, + origin: str, + event: EventBase, + state: Optional[Iterable[EventBase]], + auth_events: Optional[MutableStateMap[EventBase]], + backfilled: bool, + ) -> EventContext: + context = await self.state_handler.compute_event_context(event, old_state=state) + + if not auth_events: + prev_state_ids = await context.get_prev_state_ids() + auth_events_ids = self.auth.compute_auth_events( + event, prev_state_ids, for_verification=True + ) + auth_events_x = await self.store.get_events(auth_events_ids) + auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()} + + # This is a hack to fix some old rooms where the initial join event + # didn't reference the create event in its auth events. + if event.type == EventTypes.Member and not event.auth_event_ids(): + if len(event.prev_event_ids()) == 1 and event.depth < 5: + c = await self.store.get_event( + event.prev_event_ids()[0], allow_none=True + ) + if c and c.type == EventTypes.Create: + auth_events[(c.type, c.state_key)] = c + + context = await self.do_auth(origin, event, context, auth_events=auth_events) + + if not context.rejected: + await self._check_for_soft_fail(event, state, backfilled) + + if event.type == EventTypes.GuestAccess and not context.rejected: + await self.maybe_kick_guest_users(event) + + # If we are going to send this event over federation we precaclculate + # the joined hosts. + if event.internal_metadata.get_send_on_behalf_of(): + await self.event_creation_handler.cache_joined_hosts_for_event(event) + + return context + async def _check_for_soft_fail( self, event: EventBase, state: Optional[Iterable[EventBase]], backfilled: bool ) -> None: @@ -2357,28 +2330,19 @@ async def on_get_missing_events( return missing_events - async def _check_event_auth( + async def do_auth( self, origin: str, event: EventBase, context: EventContext, - state: Optional[Iterable[EventBase]], - auth_events: Optional[MutableStateMap[EventBase]], - backfilled: bool, + auth_events: MutableStateMap[EventBase], ) -> EventContext: """ - Checks whether an event should be rejected (for failing auth checks). Args: - origin: The host the event originates from. - event: The event itself. + origin: + event: context: - The event context. - - NB that this function potentially modifies it. - state: - The state events used to auth the event. If this is not provided - the current state events will be used. auth_events: Map from (event_type, state_key) to event @@ -2388,32 +2352,12 @@ async def _check_event_auth( server. Also NB that this function adds entries to it. - backfilled: True if the event was backfilled. - Returns: - The updated context object. + updated context object """ room_version = await self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] - if not auth_events: - prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self.auth.compute_auth_events( - event, prev_state_ids, for_verification=True - ) - auth_events_x = await self.store.get_events(auth_events_ids) - auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()} - - # This is a hack to fix some old rooms where the initial join event - # didn't reference the create event in its auth events. - if event.type == EventTypes.Member and not event.auth_event_ids(): - if len(event.prev_event_ids()) == 1 and event.depth < 5: - c = await self.store.get_event( - event.prev_event_ids()[0], allow_none=True - ) - if c and c.type == EventTypes.Create: - auth_events[(c.type, c.state_key)] = c - try: context = await self._update_auth_events_and_context_for_auth( origin, event, context, auth_events @@ -2435,17 +2379,6 @@ async def _check_event_auth( logger.warning("Failed auth resolution for %r because %s", event, e) context.rejected = RejectedReason.AUTH_ERROR - if not context.rejected: - await self._check_for_soft_fail(event, state, backfilled) - - if event.type == EventTypes.GuestAccess and not context.rejected: - await self.maybe_kick_guest_users(event) - - # If we are going to send this event over federation we precaclculate - # the joined hosts. - if event.internal_metadata.get_send_on_behalf_of(): - await self.event_creation_handler.cache_joined_hosts_for_event(event) - return context async def _update_auth_events_and_context_for_auth( @@ -2455,7 +2388,7 @@ async def _update_auth_events_and_context_for_auth( context: EventContext, auth_events: MutableStateMap[EventBase], ) -> EventContext: - """Helper for _check_event_auth. See there for docs. + """Helper for do_auth. See there for docs. Checks whether a given event has the expected auth events. If it doesn't then we talk to the remote server to compare state to see if @@ -2535,14 +2468,9 @@ async def _update_auth_events_and_context_for_auth( e.internal_metadata.outlier = True logger.debug( - "_check_event_auth %s missing_auth: %s", - event.event_id, - e.event_id, - ) - context = await self.state_handler.compute_event_context(e) - await self._auth_and_persist_event( - origin, e, context, auth_events=auth + "do_auth %s missing_auth: %s", event.event_id, e.event_id ) + await self._handle_new_event(origin, e, auth_events=auth) if e.event_id in event_auth_events: auth_events[(e.type, e.state_key)] = e diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 2c5bada1d8..2bbfac6471 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple from synapse import types -from synapse.api.constants import AccountDataTypes, EventTypes, Membership +from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules, Membership from synapse.api.errors import ( AuthError, Codes, @@ -28,6 +28,7 @@ SynapseError, ) from synapse.api.ratelimiting import Ratelimiter +from synapse.api.room_versions import RoomVersion from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID @@ -63,7 +64,6 @@ def __init__(self, hs: "HomeServer"): self.profile_handler = hs.get_profile_handler() self.event_creation_handler = hs.get_event_creation_handler() self.account_data_handler = hs.get_account_data_handler() - self.event_auth_handler = hs.get_event_auth_handler() self.member_linearizer = Linearizer(name="member") @@ -178,6 +178,62 @@ async def ratelimit_invite( await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id) + async def _can_join_without_invite( + self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str + ) -> bool: + """ + Check whether a user can join a room without an invite. + + When joining a room with restricted joined rules (as defined in MSC3083), + the membership of spaces must be checked during join. + + Args: + state_ids: The state of the room as it currently is. + room_version: The room version of the room being joined. + user_id: The user joining the room. + + Returns: + True if the user can join the room, false otherwise. + """ + # This only applies to room versions which support the new join rule. + if not room_version.msc3083_join_rules: + return True + + # If there's no join rule, then it defaults to public (so this doesn't apply). + join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) + if not join_rules_event_id: + return True + + # If the join rule is not restricted, this doesn't apply. + join_rules_event = await self.store.get_event(join_rules_event_id) + if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: + return True + + # If allowed is of the wrong form, then only allow invited users. + allowed_spaces = join_rules_event.content.get("allow", []) + if not isinstance(allowed_spaces, list): + return False + + # Get the list of joined rooms and see if there's an overlap. + joined_rooms = await self.store.get_rooms_for_user(user_id) + + # Pull out the other room IDs, invalid data gets filtered. + for space in allowed_spaces: + if not isinstance(space, dict): + continue + + space_id = space.get("space") + if not isinstance(space_id, str): + continue + + # The user was joined to one of the spaces specified, they can join + # this room! + if space_id in joined_rooms: + return True + + # The user was not in any of the required spaces. + return False + async def _local_membership_update( self, requester: Requester, @@ -246,7 +302,7 @@ async def _local_membership_update( if ( newly_joined and not user_is_invited - and not await self.event_auth_handler.can_join_without_invite( + and not await self._can_join_without_invite( prev_state_ids, event.room_version, user_id ) ): diff --git a/synapse/server.py b/synapse/server.py index 045b8f3fca..95a2cd2e5d 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -77,7 +77,6 @@ from synapse.handlers.directory import DirectoryHandler from synapse.handlers.e2e_keys import E2eKeysHandler from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler -from synapse.handlers.event_auth import EventAuthHandler from synapse.handlers.events import EventHandler, EventStreamHandler from synapse.handlers.federation import FederationHandler from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler @@ -750,10 +749,6 @@ def get_account_data_handler(self) -> AccountDataHandler: def get_space_summary_handler(self) -> SpaceSummaryHandler: return SpaceSummaryHandler(self) - @cache_in_self - def get_event_auth_handler(self) -> EventAuthHandler: - return EventAuthHandler(self) - @cache_in_self def get_external_cache(self) -> ExternalCache: return ExternalCache(self) diff --git a/tests/test_federation.py b/tests/test_federation.py index 0a3a996ec1..86a44a13da 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -75,10 +75,8 @@ def setUp(self): ) self.handler = self.homeserver.get_federation_handler() - self.handler._check_event_auth = ( - lambda origin, event, context, state, auth_events, backfilled: succeed( - context - ) + self.handler.do_auth = lambda origin, event, context, auth_events: succeed( + context ) self.client = self.homeserver.get_federation_client() self.client._check_sigs_and_hash_and_fetch = lambda dest, pdus, **k: succeed( From 936e69825ab684ca580edb6038e86fdb2e561776 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 14 Apr 2021 12:35:28 -0400 Subject: [PATCH 053/619] Separate creating an event context from persisting it in the federation handler (#9800) This refactoring allows adding logic that uses the event context before persisting it. --- changelog.d/9800.feature | 1 + synapse/handlers/federation.py | 178 +++++++++++++++++++++------------ tests/test_federation.py | 6 +- 3 files changed, 118 insertions(+), 67 deletions(-) create mode 100644 changelog.d/9800.feature diff --git a/changelog.d/9800.feature b/changelog.d/9800.feature new file mode 100644 index 0000000000..9404ad2fc0 --- /dev/null +++ b/changelog.d/9800.feature @@ -0,0 +1 @@ +Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index fe1d83f6b8..4b3730aa3b 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -103,7 +103,7 @@ @attr.s(slots=True) class _NewEventInfo: - """Holds information about a received event, ready for passing to _handle_new_events + """Holds information about a received event, ready for passing to _auth_and_persist_events Attributes: event: the received event @@ -807,7 +807,10 @@ async def _process_received_pdu( logger.debug("Processing event: %s", event) try: - await self._handle_new_event(origin, event, state=state) + context = await self.state_handler.compute_event_context( + event, old_state=state + ) + await self._auth_and_persist_event(origin, event, context, state=state) except AuthError as e: raise FederationError("ERROR", e.code, e.msg, affected=event.event_id) @@ -1010,7 +1013,9 @@ async def backfill( ) if ev_infos: - await self._handle_new_events(dest, room_id, ev_infos, backfilled=True) + await self._auth_and_persist_events( + dest, room_id, ev_infos, backfilled=True + ) # Step 2: Persist the rest of the events in the chunk one by one events.sort(key=lambda e: e.depth) @@ -1023,10 +1028,12 @@ async def backfill( # non-outliers assert not event.internal_metadata.is_outlier() + context = await self.state_handler.compute_event_context(event) + # We store these one at a time since each event depends on the # previous to work out the state. # TODO: We can probably do something more clever here. - await self._handle_new_event(dest, event, backfilled=True) + await self._auth_and_persist_event(dest, event, context, backfilled=True) return events @@ -1360,7 +1367,7 @@ async def get_event(event_id: str): event_infos.append(_NewEventInfo(event, None, auth)) - await self._handle_new_events( + await self._auth_and_persist_events( destination, room_id, event_infos, @@ -1666,10 +1673,11 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: # would introduce the danger of backwards-compatibility problems. event.internal_metadata.send_on_behalf_of = origin - context = await self._handle_new_event(origin, event) + context = await self.state_handler.compute_event_context(event) + context = await self._auth_and_persist_event(origin, event, context) logger.debug( - "on_send_join_request: After _handle_new_event: %s, sigs: %s", + "on_send_join_request: After _auth_and_persist_event: %s, sigs: %s", event.event_id, event.signatures, ) @@ -1878,10 +1886,11 @@ async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: event.internal_metadata.outlier = False - await self._handle_new_event(origin, event) + context = await self.state_handler.compute_event_context(event) + await self._auth_and_persist_event(origin, event, context) logger.debug( - "on_send_leave_request: After _handle_new_event: %s, sigs: %s", + "on_send_leave_request: After _auth_and_persist_event: %s, sigs: %s", event.event_id, event.signatures, ) @@ -1989,16 +1998,47 @@ async def get_persisted_pdu( async def get_min_depth_for_context(self, context: str) -> int: return await self.store.get_min_depth(context) - async def _handle_new_event( + async def _auth_and_persist_event( self, origin: str, event: EventBase, + context: EventContext, state: Optional[Iterable[EventBase]] = None, auth_events: Optional[MutableStateMap[EventBase]] = None, backfilled: bool = False, ) -> EventContext: - context = await self._prep_event( - origin, event, state=state, auth_events=auth_events, backfilled=backfilled + """ + Process an event by performing auth checks and then persisting to the database. + + Args: + origin: The host the event originates from. + event: The event itself. + context: + The event context. + + NB that this function potentially modifies it. + state: + The state events used to check the event for soft-fail. If this is + not provided the current state events will be used. + auth_events: + Map from (event_type, state_key) to event + + Normally, our calculated auth_events based on the state of the room + at the event's position in the DAG, though occasionally (eg if the + event is an outlier), may be the auth events claimed by the remote + server. + backfilled: True if the event was backfilled. + + Returns: + The event context. + """ + context = await self._check_event_auth( + origin, + event, + context, + state=state, + auth_events=auth_events, + backfilled=backfilled, ) try: @@ -2022,7 +2062,7 @@ async def _handle_new_event( return context - async def _handle_new_events( + async def _auth_and_persist_events( self, origin: str, room_id: str, @@ -2040,9 +2080,13 @@ async def _handle_new_events( async def prep(ev_info: _NewEventInfo): event = ev_info.event with nested_logging_context(suffix=event.event_id): - res = await self._prep_event( + res = await self.state_handler.compute_event_context( + event, old_state=ev_info.state + ) + res = await self._check_event_auth( origin, event, + res, state=ev_info.state, auth_events=ev_info.auth_events, backfilled=backfilled, @@ -2177,49 +2221,6 @@ async def _persist_auth_tree( room_id, [(event, new_event_context)] ) - async def _prep_event( - self, - origin: str, - event: EventBase, - state: Optional[Iterable[EventBase]], - auth_events: Optional[MutableStateMap[EventBase]], - backfilled: bool, - ) -> EventContext: - context = await self.state_handler.compute_event_context(event, old_state=state) - - if not auth_events: - prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self.auth.compute_auth_events( - event, prev_state_ids, for_verification=True - ) - auth_events_x = await self.store.get_events(auth_events_ids) - auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()} - - # This is a hack to fix some old rooms where the initial join event - # didn't reference the create event in its auth events. - if event.type == EventTypes.Member and not event.auth_event_ids(): - if len(event.prev_event_ids()) == 1 and event.depth < 5: - c = await self.store.get_event( - event.prev_event_ids()[0], allow_none=True - ) - if c and c.type == EventTypes.Create: - auth_events[(c.type, c.state_key)] = c - - context = await self.do_auth(origin, event, context, auth_events=auth_events) - - if not context.rejected: - await self._check_for_soft_fail(event, state, backfilled) - - if event.type == EventTypes.GuestAccess and not context.rejected: - await self.maybe_kick_guest_users(event) - - # If we are going to send this event over federation we precaclculate - # the joined hosts. - if event.internal_metadata.get_send_on_behalf_of(): - await self.event_creation_handler.cache_joined_hosts_for_event(event) - - return context - async def _check_for_soft_fail( self, event: EventBase, state: Optional[Iterable[EventBase]], backfilled: bool ) -> None: @@ -2330,19 +2331,28 @@ async def on_get_missing_events( return missing_events - async def do_auth( + async def _check_event_auth( self, origin: str, event: EventBase, context: EventContext, - auth_events: MutableStateMap[EventBase], + state: Optional[Iterable[EventBase]], + auth_events: Optional[MutableStateMap[EventBase]], + backfilled: bool, ) -> EventContext: """ + Checks whether an event should be rejected (for failing auth checks). Args: - origin: - event: + origin: The host the event originates from. + event: The event itself. context: + The event context. + + NB that this function potentially modifies it. + state: + The state events used to check the event for soft-fail. If this is + not provided the current state events will be used. auth_events: Map from (event_type, state_key) to event @@ -2352,12 +2362,34 @@ async def do_auth( server. Also NB that this function adds entries to it. + + If this is not provided, it is calculated from the previous state IDs. + backfilled: True if the event was backfilled. + Returns: - updated context object + The updated context object. """ room_version = await self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] + if not auth_events: + prev_state_ids = await context.get_prev_state_ids() + auth_events_ids = self.auth.compute_auth_events( + event, prev_state_ids, for_verification=True + ) + auth_events_x = await self.store.get_events(auth_events_ids) + auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()} + + # This is a hack to fix some old rooms where the initial join event + # didn't reference the create event in its auth events. + if event.type == EventTypes.Member and not event.auth_event_ids(): + if len(event.prev_event_ids()) == 1 and event.depth < 5: + c = await self.store.get_event( + event.prev_event_ids()[0], allow_none=True + ) + if c and c.type == EventTypes.Create: + auth_events[(c.type, c.state_key)] = c + try: context = await self._update_auth_events_and_context_for_auth( origin, event, context, auth_events @@ -2379,6 +2411,17 @@ async def do_auth( logger.warning("Failed auth resolution for %r because %s", event, e) context.rejected = RejectedReason.AUTH_ERROR + if not context.rejected: + await self._check_for_soft_fail(event, state, backfilled) + + if event.type == EventTypes.GuestAccess and not context.rejected: + await self.maybe_kick_guest_users(event) + + # If we are going to send this event over federation we precaclculate + # the joined hosts. + if event.internal_metadata.get_send_on_behalf_of(): + await self.event_creation_handler.cache_joined_hosts_for_event(event) + return context async def _update_auth_events_and_context_for_auth( @@ -2388,7 +2431,7 @@ async def _update_auth_events_and_context_for_auth( context: EventContext, auth_events: MutableStateMap[EventBase], ) -> EventContext: - """Helper for do_auth. See there for docs. + """Helper for _check_event_auth. See there for docs. Checks whether a given event has the expected auth events. If it doesn't then we talk to the remote server to compare state to see if @@ -2468,9 +2511,14 @@ async def _update_auth_events_and_context_for_auth( e.internal_metadata.outlier = True logger.debug( - "do_auth %s missing_auth: %s", event.event_id, e.event_id + "_check_event_auth %s missing_auth: %s", + event.event_id, + e.event_id, + ) + context = await self.state_handler.compute_event_context(e) + await self._auth_and_persist_event( + origin, e, context, auth_events=auth ) - await self._handle_new_event(origin, e, auth_events=auth) if e.event_id in event_auth_events: auth_events[(e.type, e.state_key)] = e diff --git a/tests/test_federation.py b/tests/test_federation.py index 86a44a13da..0a3a996ec1 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -75,8 +75,10 @@ def setUp(self): ) self.handler = self.homeserver.get_federation_handler() - self.handler.do_auth = lambda origin, event, context, auth_events: succeed( - context + self.handler._check_event_auth = ( + lambda origin, event, context, state, auth_events, backfilled: succeed( + context + ) ) self.client = self.homeserver.get_federation_client() self.client._check_sigs_and_hash_and_fetch = lambda dest, pdus, **k: succeed( From 5a153772c197a689df6c087e49d7bd8beee5dbdd Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 14 Apr 2021 19:09:08 +0100 Subject: [PATCH 054/619] remove `HomeServer.get_config` (#9815) Every single time I want to access the config object, I have to remember whether or not we use `get_config`. Let's just get rid of it. --- changelog.d/9815.misc | 1 + synapse/app/generic_worker.py | 2 +- synapse/app/homeserver.py | 18 +++++++++--------- synapse/crypto/keyring.py | 2 +- synapse/federation/federation_server.py | 2 +- .../federation/sender/transaction_manager.py | 2 +- synapse/rest/media/v1/config_resource.py | 2 +- synapse/server.py | 3 --- tests/app/test_openid_listener.py | 2 +- 9 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 changelog.d/9815.misc diff --git a/changelog.d/9815.misc b/changelog.d/9815.misc new file mode 100644 index 0000000000..e33d012d3d --- /dev/null +++ b/changelog.d/9815.misc @@ -0,0 +1 @@ +Replace `HomeServer.get_config()` with inline references. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 28e3b1aa3c..26c458dbb6 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -405,7 +405,7 @@ def start_listening(self, listeners: Iterable[ListenerConfig]): listener.bind_addresses, listener.port, manhole_globals={"hs": self} ) elif listener.type == "metrics": - if not self.get_config().enable_metrics: + if not self.config.enable_metrics: logger.warning( ( "Metrics listener configured, but " diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 679b7f4289..8be8b520eb 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -191,7 +191,7 @@ def _configure_named_resource(self, name, compress=False): } ) - if self.get_config().threepid_behaviour_email == ThreepidBehaviour.LOCAL: + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: from synapse.rest.synapse.client.password_reset import ( PasswordResetSubmitTokenResource, ) @@ -230,7 +230,7 @@ def _configure_named_resource(self, name, compress=False): ) if name in ["media", "federation", "client"]: - if self.get_config().enable_media_repo: + if self.config.enable_media_repo: media_repo = self.get_media_repository_resource() resources.update( {MEDIA_PREFIX: media_repo, LEGACY_MEDIA_PREFIX: media_repo} @@ -244,7 +244,7 @@ def _configure_named_resource(self, name, compress=False): resources[SERVER_KEY_V2_PREFIX] = KeyApiV2Resource(self) if name == "webclient": - webclient_loc = self.get_config().web_client_location + webclient_loc = self.config.web_client_location if webclient_loc is None: logger.warning( @@ -265,7 +265,7 @@ def _configure_named_resource(self, name, compress=False): # https://twistedmatrix.com/trac/ticket/7678 resources[WEB_CLIENT_PREFIX] = File(webclient_loc) - if name == "metrics" and self.get_config().enable_metrics: + if name == "metrics" and self.config.enable_metrics: resources[METRICS_PREFIX] = MetricsResource(RegistryProxy) if name == "replication": @@ -274,9 +274,7 @@ def _configure_named_resource(self, name, compress=False): return resources def start_listening(self, listeners: Iterable[ListenerConfig]): - config = self.get_config() - - if config.redis_enabled: + if self.config.redis_enabled: # If redis is enabled we connect via the replication command handler # in the same way as the workers (since we're effectively a client # rather than a server). @@ -284,7 +282,9 @@ def start_listening(self, listeners: Iterable[ListenerConfig]): for listener in listeners: if listener.type == "http": - self._listening_services.extend(self._listener_http(config, listener)) + self._listening_services.extend( + self._listener_http(self.config, listener) + ) elif listener.type == "manhole": _base.listen_manhole( listener.bind_addresses, listener.port, manhole_globals={"hs": self} @@ -298,7 +298,7 @@ def start_listening(self, listeners: Iterable[ListenerConfig]): for s in services: reactor.addSystemEventTrigger("before", "shutdown", s.stopListening) elif listener.type == "metrics": - if not self.get_config().enable_metrics: + if not self.config.enable_metrics: logger.warning( ( "Metrics listener configured, but " diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 40073dc7c2..5f18ef7748 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -501,7 +501,7 @@ async def get_keys( class BaseV2KeyFetcher(KeyFetcher): def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() - self.config = hs.get_config() + self.config = hs.config async def process_v2_response( self, from_server: str, response_json: JsonDict, time_added_ms: int diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 3ff6479cfb..b729a69203 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -136,7 +136,7 @@ def __init__(self, hs: "HomeServer"): ) # type: ResponseCache[Tuple[str, str]] self._federation_metrics_domains = ( - hs.get_config().federation.federation_metrics_domains + hs.config.federation.federation_metrics_domains ) async def on_backfill_request( diff --git a/synapse/federation/sender/transaction_manager.py b/synapse/federation/sender/transaction_manager.py index 12fe3a719b..72a635830b 100644 --- a/synapse/federation/sender/transaction_manager.py +++ b/synapse/federation/sender/transaction_manager.py @@ -56,7 +56,7 @@ def __init__(self, hs: "synapse.server.HomeServer"): self._transport_layer = hs.get_federation_transport_client() self._federation_metrics_domains = ( - hs.get_config().federation.federation_metrics_domains + hs.config.federation.federation_metrics_domains ) # HACK to get unique tx id diff --git a/synapse/rest/media/v1/config_resource.py b/synapse/rest/media/v1/config_resource.py index b20c29f007..a1d36e5cf1 100644 --- a/synapse/rest/media/v1/config_resource.py +++ b/synapse/rest/media/v1/config_resource.py @@ -30,7 +30,7 @@ class MediaConfigResource(DirectServeJsonResource): def __init__(self, hs: "HomeServer"): super().__init__() - config = hs.get_config() + config = hs.config self.clock = hs.get_clock() self.auth = hs.get_auth() self.limits_dict = {"m.upload.size": config.max_upload_size} diff --git a/synapse/server.py b/synapse/server.py index 95a2cd2e5d..42d2fad8e8 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -323,9 +323,6 @@ def get_datastores(self) -> Databases: return self.datastores - def get_config(self) -> HomeServerConfig: - return self.config - @cache_in_self def get_distributor(self) -> Distributor: return Distributor() diff --git a/tests/app/test_openid_listener.py b/tests/app/test_openid_listener.py index 276f09015e..264e101082 100644 --- a/tests/app/test_openid_listener.py +++ b/tests/app/test_openid_listener.py @@ -109,7 +109,7 @@ def test_openid_listener(self, names, expectation): } # Listen with the config - self.hs._listener_http(self.hs.get_config(), parse_listener_def(config)) + self.hs._listener_http(self.hs.config, parse_listener_def(config)) # Grab the resource from the site that was told to listen site = self.reactor.tcpServers[0][1] From 601b893352838c1391da083e8edde62904d23208 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 16 Apr 2021 14:44:55 +0100 Subject: [PATCH 055/619] Small speed up joining large remote rooms (#9825) There are a couple of points in `persist_events` where we are doing a query per event in series, which we can replace. --- changelog.d/9825.misc | 1 + synapse/storage/databases/main/events.py | 54 +++++++++++++++--------- 2 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 changelog.d/9825.misc diff --git a/changelog.d/9825.misc b/changelog.d/9825.misc new file mode 100644 index 0000000000..42f3f15619 --- /dev/null +++ b/changelog.d/9825.misc @@ -0,0 +1 @@ +Small speed up for joining large remote rooms. diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index bed4326d11..a362521e20 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -1378,17 +1378,21 @@ def get_internal_metadata(event): ], ) - for event, _ in events_and_contexts: - if not event.internal_metadata.is_redacted(): - # If we're persisting an unredacted event we go and ensure - # that we mark any redactions that reference this event as - # requiring censoring. - self.db_pool.simple_update_txn( - txn, - table="redactions", - keyvalues={"redacts": event.event_id}, - updatevalues={"have_censored": False}, + # If we're persisting an unredacted event we go and ensure + # that we mark any redactions that reference this event as + # requiring censoring. + sql = "UPDATE redactions SET have_censored = ? WHERE redacts = ?" + txn.execute_batch( + sql, + ( + ( + False, + event.event_id, ) + for event, _ in events_and_contexts + if not event.internal_metadata.is_redacted() + ), + ) state_events_and_contexts = [ ec for ec in events_and_contexts if ec[0].is_state() @@ -1881,20 +1885,28 @@ def _set_push_actions_for_event_and_users_txn( ), ) - for event, _ in events_and_contexts: - user_ids = self.db_pool.simple_select_onecol_txn( - txn, - table="event_push_actions_staging", - keyvalues={"event_id": event.event_id}, - retcol="user_id", - ) + room_to_event_ids = {} # type: Dict[str, List[str]] + for e, _ in events_and_contexts: + room_to_event_ids.setdefault(e.room_id, []).append(e.event_id) - for uid in user_ids: - txn.call_after( - self.store.get_unread_event_push_actions_by_room_for_user.invalidate_many, - (event.room_id, uid), + for room_id, event_ids in room_to_event_ids.items(): + rows = self.db_pool.simple_select_many_txn( + txn, + table="event_push_actions_staging", + column="event_id", + iterable=event_ids, + keyvalues={}, + retcols=("user_id",), ) + user_ids = {row["user_id"] for row in rows} + + for user_id in user_ids: + txn.call_after( + self.store.get_unread_event_push_actions_by_room_for_user.invalidate_many, + (room_id, user_id), + ) + # Now we delete the staging area for *all* events that were being # persisted. txn.execute_batch( From c571736c6ca5d1d2d9bf7cd9b717465d446ac7b3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 16 Apr 2021 18:17:18 +0100 Subject: [PATCH 056/619] User directory: use calculated room membership state instead (#9821) Fixes: #9797. Should help reduce CPU usage on the user directory, especially when memberships change in rooms with lots of state history. --- changelog.d/9821.misc | 1 + synapse/handlers/user_directory.py | 15 ++++++----- synapse/storage/databases/main/roommember.py | 27 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 changelog.d/9821.misc diff --git a/changelog.d/9821.misc b/changelog.d/9821.misc new file mode 100644 index 0000000000..03b2d2ed4d --- /dev/null +++ b/changelog.d/9821.misc @@ -0,0 +1 @@ +Reduce CPU usage of the user directory by reusing existing calculated room membership. \ No newline at end of file diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py index 9b1e6d5c18..dacc4f3076 100644 --- a/synapse/handlers/user_directory.py +++ b/synapse/handlers/user_directory.py @@ -44,7 +44,6 @@ def __init__(self, hs: "HomeServer"): super().__init__(hs) self.store = hs.get_datastore() - self.state = hs.get_state_handler() self.server_name = hs.hostname self.clock = hs.get_clock() self.notifier = hs.get_notifier() @@ -302,10 +301,12 @@ async def _handle_room_publicity_change( # ignore the change return - users_with_profile = await self.state.get_current_users_in_room(room_id) + other_users_in_room_with_profiles = ( + await self.store.get_users_in_room_with_profiles(room_id) + ) # Remove every user from the sharing tables for that room. - for user_id in users_with_profile.keys(): + for user_id in other_users_in_room_with_profiles.keys(): await self.store.remove_user_who_share_room(user_id, room_id) # Then, re-add them to the tables. @@ -314,7 +315,7 @@ async def _handle_room_publicity_change( # which when ran over an entire room, will result in the same values # being added multiple times. The batching upserts shouldn't make this # too bad, though. - for user_id, profile in users_with_profile.items(): + for user_id, profile in other_users_in_room_with_profiles.items(): await self._handle_new_user(room_id, user_id, profile) async def _handle_new_user( @@ -336,7 +337,7 @@ async def _handle_new_user( room_id ) # Now we update users who share rooms with users. - users_with_profile = await self.state.get_current_users_in_room(room_id) + other_users_in_room = await self.store.get_users_in_room(room_id) if is_public: await self.store.add_users_in_public_rooms(room_id, (user_id,)) @@ -352,14 +353,14 @@ async def _handle_new_user( # We don't care about appservice users. if not is_appservice: - for other_user_id in users_with_profile: + for other_user_id in other_users_in_room: if user_id == other_user_id: continue to_insert.add((user_id, other_user_id)) # Next we need to update for every local user in the room - for other_user_id in users_with_profile: + for other_user_id in other_users_in_room: if user_id == other_user_id: continue diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index ef5587f87a..fd525dce65 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -173,6 +173,33 @@ def get_users_in_room_txn(self, txn, room_id: str) -> List[str]: txn.execute(sql, (room_id, Membership.JOIN)) return [r[0] for r in txn] + @cached(max_entries=100000, iterable=True) + async def get_users_in_room_with_profiles( + self, room_id: str + ) -> Dict[str, ProfileInfo]: + """Get a mapping from user ID to profile information for all users in a given room. + + Args: + room_id: The ID of the room to retrieve the users of. + + Returns: + A mapping from user ID to ProfileInfo. + """ + + def _get_users_in_room_with_profiles(txn) -> Dict[str, ProfileInfo]: + sql = """ + SELECT user_id, display_name, avatar_url FROM room_memberships + WHERE room_id = ? AND membership = ? + """ + txn.execute(sql, (room_id, Membership.JOIN)) + + return {r[0]: ProfileInfo(display_name=r[1], avatar_url=r[2]) for r in txn} + + return await self.db_pool.runInteraction( + "get_users_in_room_with_profiles", + _get_users_in_room_with_profiles, + ) + @cached(max_entries=100000) async def get_room_summary(self, room_id: str) -> Dict[str, MemberSummary]: """Get the details of a room roughly suitable for use by the room From 2b7dd21655b1ed2db490853d2cdbf6fb38704d81 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 19 Apr 2021 10:50:49 +0100 Subject: [PATCH 057/619] Don't send normal presence updates over federation replication stream (#9828) --- changelog.d/9828.feature | 1 + synapse/federation/send_queue.py | 70 +------------------ synapse/federation/sender/__init__.py | 96 +-------------------------- synapse/handlers/presence.py | 78 +++++++++++++++++----- synapse/module_api/__init__.py | 13 ++-- 5 files changed, 75 insertions(+), 183 deletions(-) create mode 100644 changelog.d/9828.feature diff --git a/changelog.d/9828.feature b/changelog.d/9828.feature new file mode 100644 index 0000000000..f56b0bb3bd --- /dev/null +++ b/changelog.d/9828.feature @@ -0,0 +1 @@ +Add experimental support for handling presence on a worker. diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index e3f0bc2471..d71f04e43e 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -76,9 +76,6 @@ def __init__(self, hs: "HomeServer"): # Pending presence map user_id -> UserPresenceState self.presence_map = {} # type: Dict[str, UserPresenceState] - # Stream position -> list[user_id] - self.presence_changed = SortedDict() # type: SortedDict[int, List[str]] - # Stores the destinations we need to explicitly send presence to about a # given user. # Stream position -> (user_id, destinations) @@ -96,7 +93,7 @@ def __init__(self, hs: "HomeServer"): self.edus = SortedDict() # type: SortedDict[int, Edu] - # stream ID for the next entry into presence_changed/keyed_edu_changed/edus. + # stream ID for the next entry into keyed_edu_changed/edus. self.pos = 1 # map from stream ID to the time that stream entry was generated, so that we @@ -117,7 +114,6 @@ def register(name: str, queue: Sized) -> None: for queue_name in [ "presence_map", - "presence_changed", "keyed_edu", "keyed_edu_changed", "edus", @@ -155,23 +151,12 @@ def _clear_queue_before_pos(self, position_to_delete: int) -> None: """Clear all the queues from before a given position""" with Measure(self.clock, "send_queue._clear"): # Delete things out of presence maps - keys = self.presence_changed.keys() - i = self.presence_changed.bisect_left(position_to_delete) - for key in keys[:i]: - del self.presence_changed[key] - - user_ids = { - user_id for uids in self.presence_changed.values() for user_id in uids - } - keys = self.presence_destinations.keys() i = self.presence_destinations.bisect_left(position_to_delete) for key in keys[:i]: del self.presence_destinations[key] - user_ids.update( - user_id for user_id, _ in self.presence_destinations.values() - ) + user_ids = {user_id for user_id, _ in self.presence_destinations.values()} to_del = [ user_id for user_id in self.presence_map if user_id not in user_ids @@ -244,23 +229,6 @@ async def send_read_receipt(self, receipt: ReadReceipt) -> None: """ # nothing to do here: the replication listener will handle it. - def send_presence(self, states: List[UserPresenceState]) -> None: - """As per FederationSender - - Args: - states - """ - pos = self._next_pos() - - # We only want to send presence for our own users, so lets always just - # filter here just in case. - local_states = [s for s in states if self.is_mine_id(s.user_id)] - - self.presence_map.update({state.user_id: state for state in local_states}) - self.presence_changed[pos] = [state.user_id for state in local_states] - - self.notifier.on_new_replication_data() - def send_presence_to_destinations( self, states: Iterable[UserPresenceState], destinations: Iterable[str] ) -> None: @@ -325,18 +293,6 @@ async def get_replication_rows( # of the federation stream. rows = [] # type: List[Tuple[int, BaseFederationRow]] - # Fetch changed presence - i = self.presence_changed.bisect_right(from_token) - j = self.presence_changed.bisect_right(to_token) + 1 - dest_user_ids = [ - (pos, user_id) - for pos, user_id_list in self.presence_changed.items()[i:j] - for user_id in user_id_list - ] - - for (key, user_id) in dest_user_ids: - rows.append((key, PresenceRow(state=self.presence_map[user_id]))) - # Fetch presence to send to destinations i = self.presence_destinations.bisect_right(from_token) j = self.presence_destinations.bisect_right(to_token) + 1 @@ -427,22 +383,6 @@ def add_to_buffer(self, buff): raise NotImplementedError() -class PresenceRow( - BaseFederationRow, namedtuple("PresenceRow", ("state",)) # UserPresenceState -): - TypeId = "p" - - @staticmethod - def from_data(data): - return PresenceRow(state=UserPresenceState.from_dict(data)) - - def to_data(self): - return self.state.as_dict() - - def add_to_buffer(self, buff): - buff.presence.append(self.state) - - class PresenceDestinationsRow( BaseFederationRow, namedtuple( @@ -506,7 +446,6 @@ def add_to_buffer(self, buff): _rowtypes = ( - PresenceRow, PresenceDestinationsRow, KeyedEduRow, EduRow, @@ -518,7 +457,6 @@ def add_to_buffer(self, buff): ParsedFederationStreamData = namedtuple( "ParsedFederationStreamData", ( - "presence", # list(UserPresenceState) "presence_destinations", # list of tuples of UserPresenceState and destinations "keyed_edus", # dict of destination -> { key -> Edu } "edus", # dict of destination -> [Edu] @@ -543,7 +481,6 @@ def process_rows_for_federation( # them into the appropriate collection and then send them off. buff = ParsedFederationStreamData( - presence=[], presence_destinations=[], keyed_edus={}, edus={}, @@ -559,9 +496,6 @@ def process_rows_for_federation( parsed_row = RowType.from_data(row.data) parsed_row.add_to_buffer(buff) - if buff.presence: - transaction_queue.send_presence(buff.presence) - for state, destinations in buff.presence_destinations: transaction_queue.send_presence_to_destinations( states=[state], destinations=destinations diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 952ad39f8c..6266accaf5 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -24,8 +24,6 @@ from synapse.federation.sender.per_destination_queue import PerDestinationQueue from synapse.federation.sender.transaction_manager import TransactionManager from synapse.federation.units import Edu -from synapse.handlers.presence import get_interested_remotes -from synapse.logging.context import preserve_fn from synapse.metrics import ( LaterGauge, event_processing_loop_counter, @@ -34,7 +32,7 @@ ) from synapse.metrics.background_process_metrics import run_as_background_process from synapse.types import Collection, JsonDict, ReadReceipt, RoomStreamToken -from synapse.util.metrics import Measure, measure_func +from synapse.util.metrics import Measure if TYPE_CHECKING: from synapse.events.presence_router import PresenceRouter @@ -79,15 +77,6 @@ async def send_read_receipt(self, receipt: ReadReceipt) -> None: """ raise NotImplementedError() - @abc.abstractmethod - def send_presence(self, states: List[UserPresenceState]) -> None: - """Send the new presence states to the appropriate destinations. - - This actually queues up the presence states ready for sending and - triggers a background task to process them and send out the transactions. - """ - raise NotImplementedError() - @abc.abstractmethod def send_presence_to_destinations( self, states: Iterable[UserPresenceState], destinations: Iterable[str] @@ -176,11 +165,6 @@ def __init__(self, hs: "HomeServer"): ), ) - # Map of user_id -> UserPresenceState for all the pending presence - # to be sent out by user_id. Entries here get processed and put in - # pending_presence_by_dest - self.pending_presence = {} # type: Dict[str, UserPresenceState] - LaterGauge( "synapse_federation_transaction_queue_pending_pdus", "", @@ -201,8 +185,6 @@ def __init__(self, hs: "HomeServer"): self._is_processing = False self._last_poked_id = -1 - self._processing_pending_presence = False - # map from room_id to a set of PerDestinationQueues which we believe are # awaiting a call to flush_read_receipts_for_room. The presence of an entry # here for a given room means that we are rate-limiting RR flushes to that room, @@ -546,48 +528,6 @@ def _flush_rrs_for_room(self, room_id: str) -> None: for queue in queues: queue.flush_read_receipts_for_room(room_id) - @preserve_fn # the caller should not yield on this - async def send_presence(self, states: List[UserPresenceState]) -> None: - """Send the new presence states to the appropriate destinations. - - This actually queues up the presence states ready for sending and - triggers a background task to process them and send out the transactions. - """ - if not self.hs.config.use_presence: - # No-op if presence is disabled. - return - - # First we queue up the new presence by user ID, so multiple presence - # updates in quick succession are correctly handled. - # We only want to send presence for our own users, so lets always just - # filter here just in case. - self.pending_presence.update( - {state.user_id: state for state in states if self.is_mine_id(state.user_id)} - ) - - # We then handle the new pending presence in batches, first figuring - # out the destinations we need to send each state to and then poking it - # to attempt a new transaction. We linearize this so that we don't - # accidentally mess up the ordering and send multiple presence updates - # in the wrong order - if self._processing_pending_presence: - return - - self._processing_pending_presence = True - try: - while True: - states_map = self.pending_presence - self.pending_presence = {} - - if not states_map: - break - - await self._process_presence_inner(list(states_map.values())) - except Exception: - logger.exception("Error sending presence states to servers") - finally: - self._processing_pending_presence = False - def send_presence_to_destinations( self, states: Iterable[UserPresenceState], destinations: Iterable[str] ) -> None: @@ -608,40 +548,6 @@ def send_presence_to_destinations( continue self._get_per_destination_queue(destination).send_presence(states) - @measure_func("txnqueue._process_presence") - async def _process_presence_inner(self, states: List[UserPresenceState]) -> None: - """Given a list of states populate self.pending_presence_by_dest and - poke to send a new transaction to each destination - """ - # We pull the presence router here instead of __init__ - # to prevent a dependency cycle: - # - # AuthHandler -> Notifier -> FederationSender - # -> PresenceRouter -> ModuleApi -> AuthHandler - if self._presence_router is None: - self._presence_router = self.hs.get_presence_router() - - assert self._presence_router is not None - - hosts_and_states = await get_interested_remotes( - self.store, - self._presence_router, - states, - self.state, - ) - - for destinations, states in hosts_and_states: - for destination in destinations: - if destination == self.server_name: - continue - - if not self._federation_shard_config.should_handle( - self._instance_name, destination - ): - continue - - self._get_per_destination_queue(destination).send_presence(states) - def build_and_send_edu( self, destination: str, diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index e120dd1f48..6460eb9952 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -123,6 +123,14 @@ class BasePresenceHandler(abc.ABC): def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.store = hs.get_datastore() + self.presence_router = hs.get_presence_router() + self.state = hs.get_state_handler() + + self._federation = None + if hs.should_send_federation() or not hs.config.worker_app: + self._federation = hs.get_federation_sender() + + self._send_federation = hs.should_send_federation() self._busy_presence_enabled = hs.config.experimental.msc3026_enabled @@ -249,6 +257,29 @@ async def process_replication_rows(self, token, rows): """Process presence stream rows received over replication.""" pass + async def maybe_send_presence_to_interested_destinations( + self, states: List[UserPresenceState] + ): + """If this instance is a federation sender, send the states to all + destinations that are interested. + """ + + if not self._send_federation: + return + + # If this worker sends federation we must have a FederationSender. + assert self._federation + + hosts_and_states = await get_interested_remotes( + self.store, + self.presence_router, + states, + self.state, + ) + + for destinations, states in hosts_and_states: + self._federation.send_presence_to_destinations(states, destinations) + class _NullContextManager(ContextManager[None]): """A context manager which does nothing.""" @@ -263,7 +294,6 @@ def __init__(self, hs): self.hs = hs self.is_mine_id = hs.is_mine_id - self.presence_router = hs.get_presence_router() self._presence_enabled = hs.config.use_presence # The number of ongoing syncs on this process, by user id. @@ -388,6 +418,9 @@ async def notify_from_replication(self, states, stream_id): users=users_to_states.keys(), ) + # If this is a federation sender, notify about presence updates. + await self.maybe_send_presence_to_interested_destinations(states) + async def process_replication_rows(self, token, rows): states = [ UserPresenceState( @@ -463,9 +496,6 @@ def __init__(self, hs: "HomeServer"): self.server_name = hs.hostname self.wheel_timer = WheelTimer() self.notifier = hs.get_notifier() - self.federation = hs.get_federation_sender() - self.state = hs.get_state_handler() - self.presence_router = hs.get_presence_router() self._presence_enabled = hs.config.use_presence federation_registry = hs.get_federation_registry() @@ -672,6 +702,13 @@ async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None: self.unpersisted_users_changes |= {s.user_id for s in new_states} self.unpersisted_users_changes -= set(to_notify.keys()) + # Check if we need to resend any presence states to remote hosts. We + # only do this for states that haven't been updated in a while to + # ensure that the remote host doesn't time the presence state out. + # + # Note that since these are states that have *not* been updated, + # they won't get sent down the normal presence replication stream, + # and so we have to explicitly send them via the federation stream. to_federation_ping = { user_id: state for user_id, state in to_federation_ping.items() @@ -680,7 +717,19 @@ async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None: if to_federation_ping: federation_presence_out_counter.inc(len(to_federation_ping)) - self._push_to_remotes(to_federation_ping.values()) + hosts_and_states = await get_interested_remotes( + self.store, + self.presence_router, + list(to_federation_ping.values()), + self.state, + ) + + # Since this is master we know that we have a federation sender or + # queue, and so this will be defined. + assert self._federation + + for destinations, states in hosts_and_states: + self._federation.send_presence_to_destinations(states, destinations) async def _handle_timeouts(self): """Checks the presence of users that have timed out and updates as @@ -920,15 +969,10 @@ async def _persist_and_notify(self, states): users=[UserID.from_string(u) for u in users_to_states], ) - self._push_to_remotes(states) - - def _push_to_remotes(self, states): - """Sends state updates to remote servers. - - Args: - states (list(UserPresenceState)) - """ - self.federation.send_presence(states) + # We only want to poke the local federation sender, if any, as other + # workers will receive the presence updates via the presence replication + # stream (which is updated by `store.update_presence`). + await self.maybe_send_presence_to_interested_destinations(states) async def incoming_presence(self, origin, content): """Called when we receive a `m.presence` EDU from a remote server.""" @@ -1164,9 +1208,13 @@ async def _handle_state_delta(self, deltas): user_presence_states ) + # Since this is master we know that we have a federation sender or + # queue, and so this will be defined. + assert self._federation + # Send out user presence updates for each destination for destination, user_state_set in presence_destinations.items(): - self.federation.send_presence_to_destinations( + self._federation.send_presence_to_destinations( destinations=[destination], states=user_state_set ) diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index b7dbbfc27c..a1a2b9aecc 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -50,6 +50,7 @@ def __init__(self, hs, auth_handler): self._auth_handler = auth_handler self._server_name = hs.hostname self._presence_stream = hs.get_event_sources().sources["presence"] + self._state = hs.get_state_handler() # We expose these as properties below in order to attach a helpful docstring. self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient @@ -429,11 +430,13 @@ async def send_local_online_presence_to(self, users: Iterable[str]) -> None: UserID.from_string(user), from_key=None, include_offline=False ) - # Send to remote destinations - await make_deferred_yieldable( - # We pull the federation sender here as we can only do so on workers - # that support sending presence - self._hs.get_federation_sender().send_presence(presence_events) + # Send to remote destinations. + + # We pull out the presence handler here to break a cyclic + # dependency between the presence router and module API. + presence_handler = self._hs.get_presence_handler() + await presence_handler.maybe_send_presence_to_interested_destinations( + presence_events ) From e694a598f8c948ad177e897c5bedaa71a47add29 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Mon, 19 Apr 2021 16:21:46 +0000 Subject: [PATCH 058/619] Sanity check identity server passed to bind/unbind. (#9802) Signed-off-by: Denis Kasak --- changelog.d/9802.bugfix | 1 + synapse/handlers/identity.py | 29 ++++++++++++++++++++++++++--- synapse/util/stringutils.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9802.bugfix diff --git a/changelog.d/9802.bugfix b/changelog.d/9802.bugfix new file mode 100644 index 0000000000..0c72f7be47 --- /dev/null +++ b/changelog.d/9802.bugfix @@ -0,0 +1 @@ +Add some sanity checks to identity server passed to 3PID bind/unbind endpoints. diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 87a8b89237..0b3b1fadb5 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -15,7 +15,6 @@ # limitations under the License. """Utilities for interacting with Identity Servers""" - import logging import urllib.parse from typing import Awaitable, Callable, Dict, List, Optional, Tuple @@ -34,7 +33,11 @@ from synapse.types import JsonDict, Requester from synapse.util import json_decoder from synapse.util.hash import sha256_and_url_safe_base64 -from synapse.util.stringutils import assert_valid_client_secret, random_string +from synapse.util.stringutils import ( + assert_valid_client_secret, + random_string, + valid_id_server_location, +) from ._base import BaseHandler @@ -172,6 +175,11 @@ async def bind_threepid( server with, if necessary. Required if use_v2 is true use_v2: Whether to use v2 Identity Service API endpoints. Defaults to True + Raises: + SynapseError: On any of the following conditions + - the supplied id_server is not a valid identity server name + - we failed to contact the supplied identity server + Returns: The response from the identity server """ @@ -181,6 +189,12 @@ async def bind_threepid( if id_access_token is None: use_v2 = False + if not valid_id_server_location(id_server): + raise SynapseError( + 400, + "id_server must be a valid hostname with optional port and path components", + ) + # Decide which API endpoint URLs to use headers = {} bind_data = {"sid": sid, "client_secret": client_secret, "mxid": mxid} @@ -269,12 +283,21 @@ async def try_unbind_threepid_with_id_server( id_server: Identity server to unbind from Raises: - SynapseError: If we failed to contact the identity server + SynapseError: On any of the following conditions + - the supplied id_server is not a valid identity server name + - we failed to contact the supplied identity server Returns: True on success, otherwise False if the identity server doesn't support unbinding """ + + if not valid_id_server_location(id_server): + raise SynapseError( + 400, + "id_server must be a valid hostname with optional port and path components", + ) + url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,) url_bytes = "/_matrix/identity/api/v1/3pid/unbind".encode("ascii") diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index c0e6fb9a60..cd82777f80 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -132,6 +132,38 @@ def parse_and_validate_server_name(server_name: str) -> Tuple[str, Optional[int] return host, port +def valid_id_server_location(id_server: str) -> bool: + """Check whether an identity server location, such as the one passed as the + `id_server` parameter to `/_matrix/client/r0/account/3pid/bind`, is valid. + + A valid identity server location consists of a valid hostname and optional + port number, optionally followed by any number of `/` delimited path + components, without any fragment or query string parts. + + Args: + id_server: identity server location string to validate + + Returns: + True if valid, False otherwise. + """ + + components = id_server.split("/", 1) + + host = components[0] + + try: + parse_and_validate_server_name(host) + except ValueError: + return False + + if len(components) < 2: + # no path + return True + + path = components[1] + return "#" not in path and "?" not in path + + def parse_and_validate_mxc_uri(mxc: str) -> Tuple[str, Optional[int], str]: """Parse the given string as an MXC URI From 71f0623de968f07292d5a092e9197f7513ab6cde Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 19 Apr 2021 19:16:34 +0100 Subject: [PATCH 059/619] Port "Allow users to click account renewal links multiple times without hitting an 'Invalid Token' page #74" from synapse-dinsic (#9832) This attempts to be a direct port of https://github.com/matrix-org/synapse-dinsic/pull/74 to mainline. There was some fiddling required to deal with the changes that have been made to mainline since (mainly dealing with the split of `RegistrationWorkerStore` from `RegistrationStore`, and the changes made to `self.make_request` in test code). --- UPGRADE.rst | 23 +++ changelog.d/9832.feature | 1 + docs/sample_config.yaml | 148 +++++++++------- synapse/api/auth.py | 6 +- synapse/config/_base.pyi | 2 + synapse/config/account_validity.py | 165 ++++++++++++++++++ synapse/config/emailconfig.py | 2 +- synapse/config/homeserver.py | 3 +- synapse/config/registration.py | 129 -------------- synapse/handlers/account_validity.py | 101 ++++++++--- synapse/handlers/deactivate_account.py | 4 +- synapse/push/pusherpool.py | 8 +- .../templates/account_previously_renewed.html | 1 + synapse/res/templates/account_renewed.html | 2 +- .../rest/client/v2_alpha/account_validity.py | 32 +++- .../storage/databases/main/registration.py | 62 +++++-- .../12account_validity_token_used_ts_ms.sql | 18 ++ tests/rest/client/v2_alpha/test_register.py | 52 ++++-- 18 files changed, 496 insertions(+), 263 deletions(-) create mode 100644 changelog.d/9832.feature create mode 100644 synapse/config/account_validity.py create mode 100644 synapse/res/templates/account_previously_renewed.html create mode 100644 synapse/storage/databases/main/schema/delta/59/12account_validity_token_used_ts_ms.sql diff --git a/UPGRADE.rst b/UPGRADE.rst index 665821d4ef..eff976017d 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -85,6 +85,29 @@ for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb +Upgrading to v1.33.0 +==================== + +Account Validity HTML templates can now display a user's expiration date +------------------------------------------------------------------------ + +This may affect you if you have enabled the account validity feature, and have made use of a +custom HTML template specified by the ``account_validity.template_dir`` or ``account_validity.account_renewed_html_path`` +Synapse config options. + +The template can now accept an ``expiration_ts`` variable, which represents the unix timestamp in milliseconds for the +future date of which their account has been renewed until. See the +`default template `_ +for an example of usage. + +ALso note that a new HTML template, ``account_previously_renewed.html``, has been added. This is is shown to users +when they attempt to renew their account with a valid renewal token that has already been used before. The default +template contents can been found +`here `_, +and can also accept an ``expiration_ts`` variable. This template replaces the error message users would previously see +upon attempting to use a valid renewal token more than once. + + Upgrading to v1.32.0 ==================== diff --git a/changelog.d/9832.feature b/changelog.d/9832.feature new file mode 100644 index 0000000000..e76395fbe8 --- /dev/null +++ b/changelog.d/9832.feature @@ -0,0 +1 @@ +Don't return an error when a user attempts to renew their account multiple times with the same token. Instead, state when their account is set to expire. This change concerns the optional account validity feature. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 9182dcd987..d260d76259 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1175,69 +1175,6 @@ url_preview_accept_language: # #enable_registration: false -# Optional account validity configuration. This allows for accounts to be denied -# any request after a given period. -# -# Once this feature is enabled, Synapse will look for registered users without an -# expiration date at startup and will add one to every account it found using the -# current settings at that time. -# This means that, if a validity period is set, and Synapse is restarted (it will -# then derive an expiration date from the current validity period), and some time -# after that the validity period changes and Synapse is restarted, the users' -# expiration dates won't be updated unless their account is manually renewed. This -# date will be randomly selected within a range [now + period - d ; now + period], -# where d is equal to 10% of the validity period. -# -account_validity: - # The account validity feature is disabled by default. Uncomment the - # following line to enable it. - # - #enabled: true - - # The period after which an account is valid after its registration. When - # renewing the account, its validity period will be extended by this amount - # of time. This parameter is required when using the account validity - # feature. - # - #period: 6w - - # The amount of time before an account's expiry date at which Synapse will - # send an email to the account's email address with a renewal link. By - # default, no such emails are sent. - # - # If you enable this setting, you will also need to fill out the 'email' and - # 'public_baseurl' configuration sections. - # - #renew_at: 1w - - # The subject of the email sent out with the renewal link. '%(app)s' can be - # used as a placeholder for the 'app_name' parameter from the 'email' - # section. - # - # Note that the placeholder must be written '%(app)s', including the - # trailing 's'. - # - # If this is not set, a default value is used. - # - #renew_email_subject: "Renew your %(app)s account" - - # Directory in which Synapse will try to find templates for the HTML files to - # serve to the user when trying to renew an account. If not set, default - # templates from within the Synapse package will be used. - # - #template_dir: "res/templates" - - # File within 'template_dir' giving the HTML to be displayed to the user after - # they successfully renewed their account. If not set, default text is used. - # - #account_renewed_html_path: "account_renewed.html" - - # File within 'template_dir' giving the HTML to be displayed when the user - # tries to renew an account with an invalid renewal token. If not set, - # default text is used. - # - #invalid_token_html_path: "invalid_token.html" - # Time that a user's session remains valid for, after they log in. # # Note that this is not currently compatible with guest logins. @@ -1432,6 +1369,91 @@ account_threepid_delegates: #auto_join_rooms_for_guests: false +## Account Validity ## + +# Optional account validity configuration. This allows for accounts to be denied +# any request after a given period. +# +# Once this feature is enabled, Synapse will look for registered users without an +# expiration date at startup and will add one to every account it found using the +# current settings at that time. +# This means that, if a validity period is set, and Synapse is restarted (it will +# then derive an expiration date from the current validity period), and some time +# after that the validity period changes and Synapse is restarted, the users' +# expiration dates won't be updated unless their account is manually renewed. This +# date will be randomly selected within a range [now + period - d ; now + period], +# where d is equal to 10% of the validity period. +# +account_validity: + # The account validity feature is disabled by default. Uncomment the + # following line to enable it. + # + #enabled: true + + # The period after which an account is valid after its registration. When + # renewing the account, its validity period will be extended by this amount + # of time. This parameter is required when using the account validity + # feature. + # + #period: 6w + + # The amount of time before an account's expiry date at which Synapse will + # send an email to the account's email address with a renewal link. By + # default, no such emails are sent. + # + # If you enable this setting, you will also need to fill out the 'email' and + # 'public_baseurl' configuration sections. + # + #renew_at: 1w + + # The subject of the email sent out with the renewal link. '%(app)s' can be + # used as a placeholder for the 'app_name' parameter from the 'email' + # section. + # + # Note that the placeholder must be written '%(app)s', including the + # trailing 's'. + # + # If this is not set, a default value is used. + # + #renew_email_subject: "Renew your %(app)s account" + + # Directory in which Synapse will try to find templates for the HTML files to + # serve to the user when trying to renew an account. If not set, default + # templates from within the Synapse package will be used. + # + # The currently available templates are: + # + # * account_renewed.html: Displayed to the user after they have successfully + # renewed their account. + # + # * account_previously_renewed.html: Displayed to the user if they attempt to + # renew their account with a token that is valid, but that has already + # been used. In this case the account is not renewed again. + # + # * invalid_token.html: Displayed to the user when they try to renew an account + # with an unknown or invalid renewal token. + # + # See https://github.com/matrix-org/synapse/tree/master/synapse/res/templates for + # default template contents. + # + # The file name of some of these templates can be configured below for legacy + # reasons. + # + #template_dir: "res/templates" + + # A custom file name for the 'account_renewed.html' template. + # + # If not set, the file is assumed to be named "account_renewed.html". + # + #account_renewed_html_path: "account_renewed.html" + + # A custom file name for the 'invalid_token.html' template. + # + # If not set, the file is assumed to be named "invalid_token.html". + # + #invalid_token_html_path: "invalid_token.html" + + ## Metrics ### # Enable collection and rendering of performance metrics diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 6c13f53957..872fd100cd 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -79,7 +79,9 @@ def __init__(self, hs): self._auth_blocking = AuthBlocking(self.hs) - self._account_validity = hs.config.account_validity + self._account_validity_enabled = ( + hs.config.account_validity.account_validity_enabled + ) self._track_appservice_user_ips = hs.config.track_appservice_user_ips self._macaroon_secret_key = hs.config.macaroon_secret_key @@ -222,7 +224,7 @@ async def get_user_by_req( shadow_banned = user_info.shadow_banned # Deny the request if the user account has expired. - if self._account_validity.enabled and not allow_expired: + if self._account_validity_enabled and not allow_expired: if await self.store.is_account_expired( user_info.user_id, self.clock.time_msec() ): diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index e896fd34e2..ddec356a07 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -1,6 +1,7 @@ from typing import Any, Iterable, List, Optional from synapse.config import ( + account_validity, api, appservice, auth, @@ -59,6 +60,7 @@ class RootConfig: captcha: captcha.CaptchaConfig voip: voip.VoipConfig registration: registration.RegistrationConfig + account_validity: account_validity.AccountValidityConfig metrics: metrics.MetricsConfig api: api.ApiConfig appservice: appservice.AppServiceConfig diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py new file mode 100644 index 0000000000..c58a7d95a7 --- /dev/null +++ b/synapse/config/account_validity.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from synapse.config._base import Config, ConfigError + + +class AccountValidityConfig(Config): + section = "account_validity" + + def read_config(self, config, **kwargs): + account_validity_config = config.get("account_validity") or {} + self.account_validity_enabled = account_validity_config.get("enabled", False) + self.account_validity_renew_by_email_enabled = ( + "renew_at" in account_validity_config + ) + + if self.account_validity_enabled: + if "period" in account_validity_config: + self.account_validity_period = self.parse_duration( + account_validity_config["period"] + ) + else: + raise ConfigError("'period' is required when using account validity") + + if "renew_at" in account_validity_config: + self.account_validity_renew_at = self.parse_duration( + account_validity_config["renew_at"] + ) + + if "renew_email_subject" in account_validity_config: + self.account_validity_renew_email_subject = account_validity_config[ + "renew_email_subject" + ] + else: + self.account_validity_renew_email_subject = "Renew your %(app)s account" + + self.account_validity_startup_job_max_delta = ( + self.account_validity_period * 10.0 / 100.0 + ) + + if self.account_validity_renew_by_email_enabled: + if not self.public_baseurl: + raise ConfigError("Can't send renewal emails without 'public_baseurl'") + + # Load account validity templates. + account_validity_template_dir = account_validity_config.get("template_dir") + + account_renewed_template_filename = account_validity_config.get( + "account_renewed_html_path", "account_renewed.html" + ) + invalid_token_template_filename = account_validity_config.get( + "invalid_token_html_path", "invalid_token.html" + ) + + # Read and store template content + ( + self.account_validity_account_renewed_template, + self.account_validity_account_previously_renewed_template, + self.account_validity_invalid_token_template, + ) = self.read_templates( + [ + account_renewed_template_filename, + "account_previously_renewed.html", + invalid_token_template_filename, + ], + account_validity_template_dir, + ) + + def generate_config_section(self, **kwargs): + return """\ + ## Account Validity ## + + # Optional account validity configuration. This allows for accounts to be denied + # any request after a given period. + # + # Once this feature is enabled, Synapse will look for registered users without an + # expiration date at startup and will add one to every account it found using the + # current settings at that time. + # This means that, if a validity period is set, and Synapse is restarted (it will + # then derive an expiration date from the current validity period), and some time + # after that the validity period changes and Synapse is restarted, the users' + # expiration dates won't be updated unless their account is manually renewed. This + # date will be randomly selected within a range [now + period - d ; now + period], + # where d is equal to 10% of the validity period. + # + account_validity: + # The account validity feature is disabled by default. Uncomment the + # following line to enable it. + # + #enabled: true + + # The period after which an account is valid after its registration. When + # renewing the account, its validity period will be extended by this amount + # of time. This parameter is required when using the account validity + # feature. + # + #period: 6w + + # The amount of time before an account's expiry date at which Synapse will + # send an email to the account's email address with a renewal link. By + # default, no such emails are sent. + # + # If you enable this setting, you will also need to fill out the 'email' and + # 'public_baseurl' configuration sections. + # + #renew_at: 1w + + # The subject of the email sent out with the renewal link. '%(app)s' can be + # used as a placeholder for the 'app_name' parameter from the 'email' + # section. + # + # Note that the placeholder must be written '%(app)s', including the + # trailing 's'. + # + # If this is not set, a default value is used. + # + #renew_email_subject: "Renew your %(app)s account" + + # Directory in which Synapse will try to find templates for the HTML files to + # serve to the user when trying to renew an account. If not set, default + # templates from within the Synapse package will be used. + # + # The currently available templates are: + # + # * account_renewed.html: Displayed to the user after they have successfully + # renewed their account. + # + # * account_previously_renewed.html: Displayed to the user if they attempt to + # renew their account with a token that is valid, but that has already + # been used. In this case the account is not renewed again. + # + # * invalid_token.html: Displayed to the user when they try to renew an account + # with an unknown or invalid renewal token. + # + # See https://github.com/matrix-org/synapse/tree/master/synapse/res/templates for + # default template contents. + # + # The file name of some of these templates can be configured below for legacy + # reasons. + # + #template_dir: "res/templates" + + # A custom file name for the 'account_renewed.html' template. + # + # If not set, the file is assumed to be named "account_renewed.html". + # + #account_renewed_html_path: "account_renewed.html" + + # A custom file name for the 'invalid_token.html' template. + # + # If not set, the file is assumed to be named "invalid_token.html". + # + #invalid_token_html_path: "invalid_token.html" + """ diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index c587939c7a..5564d7d097 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -299,7 +299,7 @@ def read_config(self, config, **kwargs): "client_base_url", email_config.get("riot_base_url", None) ) - if self.account_validity.renew_by_email_enabled: + if self.account_validity_renew_by_email_enabled: expiry_template_html = email_config.get( "expiry_template_html", "notice_expiry.html" ) diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 1309535068..58e3bcd511 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -12,8 +12,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - from ._base import RootConfig +from .account_validity import AccountValidityConfig from .api import ApiConfig from .appservice import AppServiceConfig from .auth import AuthConfig @@ -68,6 +68,7 @@ class HomeServerConfig(RootConfig): CaptchaConfig, VoipConfig, RegistrationConfig, + AccountValidityConfig, MetricsConfig, ApiConfig, AppServiceConfig, diff --git a/synapse/config/registration.py b/synapse/config/registration.py index f8a2768af8..e6f52b4f40 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -12,74 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - -import pkg_resources - from synapse.api.constants import RoomCreationPreset from synapse.config._base import Config, ConfigError from synapse.types import RoomAlias, UserID from synapse.util.stringutils import random_string_with_symbols, strtobool -class AccountValidityConfig(Config): - section = "accountvalidity" - - def __init__(self, config, synapse_config): - if config is None: - return - super().__init__() - self.enabled = config.get("enabled", False) - self.renew_by_email_enabled = "renew_at" in config - - if self.enabled: - if "period" in config: - self.period = self.parse_duration(config["period"]) - else: - raise ConfigError("'period' is required when using account validity") - - if "renew_at" in config: - self.renew_at = self.parse_duration(config["renew_at"]) - - if "renew_email_subject" in config: - self.renew_email_subject = config["renew_email_subject"] - else: - self.renew_email_subject = "Renew your %(app)s account" - - self.startup_job_max_delta = self.period * 10.0 / 100.0 - - if self.renew_by_email_enabled: - if "public_baseurl" not in synapse_config: - raise ConfigError("Can't send renewal emails without 'public_baseurl'") - - template_dir = config.get("template_dir") - - if not template_dir: - template_dir = pkg_resources.resource_filename("synapse", "res/templates") - - if "account_renewed_html_path" in config: - file_path = os.path.join(template_dir, config["account_renewed_html_path"]) - - self.account_renewed_html_content = self.read_file( - file_path, "account_validity.account_renewed_html_path" - ) - else: - self.account_renewed_html_content = ( - "Your account has been successfully renewed." - ) - - if "invalid_token_html_path" in config: - file_path = os.path.join(template_dir, config["invalid_token_html_path"]) - - self.invalid_token_html_content = self.read_file( - file_path, "account_validity.invalid_token_html_path" - ) - else: - self.invalid_token_html_content = ( - "Invalid renewal token." - ) - - class RegistrationConfig(Config): section = "registration" @@ -92,10 +30,6 @@ def read_config(self, config, **kwargs): str(config["disable_registration"]) ) - self.account_validity = AccountValidityConfig( - config.get("account_validity") or {}, config - ) - self.registrations_require_3pid = config.get("registrations_require_3pid", []) self.allowed_local_3pids = config.get("allowed_local_3pids", []) self.enable_3pid_lookup = config.get("enable_3pid_lookup", True) @@ -207,69 +141,6 @@ def generate_config_section(self, generate_secrets=False, **kwargs): # #enable_registration: false - # Optional account validity configuration. This allows for accounts to be denied - # any request after a given period. - # - # Once this feature is enabled, Synapse will look for registered users without an - # expiration date at startup and will add one to every account it found using the - # current settings at that time. - # This means that, if a validity period is set, and Synapse is restarted (it will - # then derive an expiration date from the current validity period), and some time - # after that the validity period changes and Synapse is restarted, the users' - # expiration dates won't be updated unless their account is manually renewed. This - # date will be randomly selected within a range [now + period - d ; now + period], - # where d is equal to 10%% of the validity period. - # - account_validity: - # The account validity feature is disabled by default. Uncomment the - # following line to enable it. - # - #enabled: true - - # The period after which an account is valid after its registration. When - # renewing the account, its validity period will be extended by this amount - # of time. This parameter is required when using the account validity - # feature. - # - #period: 6w - - # The amount of time before an account's expiry date at which Synapse will - # send an email to the account's email address with a renewal link. By - # default, no such emails are sent. - # - # If you enable this setting, you will also need to fill out the 'email' and - # 'public_baseurl' configuration sections. - # - #renew_at: 1w - - # The subject of the email sent out with the renewal link. '%%(app)s' can be - # used as a placeholder for the 'app_name' parameter from the 'email' - # section. - # - # Note that the placeholder must be written '%%(app)s', including the - # trailing 's'. - # - # If this is not set, a default value is used. - # - #renew_email_subject: "Renew your %%(app)s account" - - # Directory in which Synapse will try to find templates for the HTML files to - # serve to the user when trying to renew an account. If not set, default - # templates from within the Synapse package will be used. - # - #template_dir: "res/templates" - - # File within 'template_dir' giving the HTML to be displayed to the user after - # they successfully renewed their account. If not set, default text is used. - # - #account_renewed_html_path: "account_renewed.html" - - # File within 'template_dir' giving the HTML to be displayed when the user - # tries to renew an account with an invalid renewal token. If not set, - # default text is used. - # - #invalid_token_html_path: "invalid_token.html" - # Time that a user's session remains valid for, after they log in. # # Note that this is not currently compatible with guest logins. diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index 66ce7e8b83..5b927f10b3 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -17,7 +17,7 @@ import logging from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List, Optional, Tuple from synapse.api.errors import StoreError, SynapseError from synapse.logging.context import make_deferred_yieldable @@ -39,28 +39,44 @@ def __init__(self, hs: "HomeServer"): self.sendmail = self.hs.get_sendmail() self.clock = self.hs.get_clock() - self._account_validity = self.hs.config.account_validity + self._account_validity_enabled = ( + hs.config.account_validity.account_validity_enabled + ) + self._account_validity_renew_by_email_enabled = ( + hs.config.account_validity.account_validity_renew_by_email_enabled + ) + + self._account_validity_period = None + if self._account_validity_enabled: + self._account_validity_period = ( + hs.config.account_validity.account_validity_period + ) if ( - self._account_validity.enabled - and self._account_validity.renew_by_email_enabled + self._account_validity_enabled + and self._account_validity_renew_by_email_enabled ): # Don't do email-specific configuration if renewal by email is disabled. - self._template_html = self.config.account_validity_template_html - self._template_text = self.config.account_validity_template_text + self._template_html = ( + hs.config.account_validity.account_validity_template_html + ) + self._template_text = ( + hs.config.account_validity.account_validity_template_text + ) + account_validity_renew_email_subject = ( + hs.config.account_validity.account_validity_renew_email_subject + ) try: - app_name = self.hs.config.email_app_name + app_name = hs.config.email_app_name - self._subject = self._account_validity.renew_email_subject % { - "app": app_name - } + self._subject = account_validity_renew_email_subject % {"app": app_name} - self._from_string = self.hs.config.email_notif_from % {"app": app_name} + self._from_string = hs.config.email_notif_from % {"app": app_name} except Exception: # If substitution failed, fall back to the bare strings. - self._subject = self._account_validity.renew_email_subject - self._from_string = self.hs.config.email_notif_from + self._subject = account_validity_renew_email_subject + self._from_string = hs.config.email_notif_from self._raw_from = email.utils.parseaddr(self._from_string)[1] @@ -220,50 +236,87 @@ async def _get_renewal_token(self, user_id: str) -> str: attempts += 1 raise StoreError(500, "Couldn't generate a unique string as refresh string.") - async def renew_account(self, renewal_token: str) -> bool: + async def renew_account(self, renewal_token: str) -> Tuple[bool, bool, int]: """Renews the account attached to a given renewal token by pushing back the expiration date by the current validity period in the server's configuration. + If it turns out that the token is valid but has already been used, then the + token is considered stale. A token is stale if the 'token_used_ts_ms' db column + is non-null. + Args: renewal_token: Token sent with the renewal request. Returns: - Whether the provided token is valid. + A tuple containing: + * A bool representing whether the token is valid and unused. + * A bool which is `True` if the token is valid, but stale. + * An int representing the user's expiry timestamp as milliseconds since the + epoch, or 0 if the token was invalid. """ try: - user_id = await self.store.get_user_from_renewal_token(renewal_token) + ( + user_id, + current_expiration_ts, + token_used_ts, + ) = await self.store.get_user_from_renewal_token(renewal_token) except StoreError: - return False + return False, False, 0 + + # Check whether this token has already been used. + if token_used_ts: + logger.info( + "User '%s' attempted to use previously used token '%s' to renew account", + user_id, + renewal_token, + ) + return False, True, current_expiration_ts logger.debug("Renewing an account for user %s", user_id) - await self.renew_account_for_user(user_id) - return True + # Renew the account. Pass the renewal_token here so that it is not cleared. + # We want to keep the token around in case the user attempts to renew their + # account with the same token twice (clicking the email link twice). + # + # In that case, the token will be accepted, but the account's expiration ts + # will remain unchanged. + new_expiration_ts = await self.renew_account_for_user( + user_id, renewal_token=renewal_token + ) + + return True, False, new_expiration_ts async def renew_account_for_user( self, user_id: str, expiration_ts: Optional[int] = None, email_sent: bool = False, + renewal_token: Optional[str] = None, ) -> int: """Renews the account attached to a given user by pushing back the expiration date by the current validity period in the server's configuration. Args: - renewal_token: Token sent with the renewal request. + user_id: The ID of the user to renew. expiration_ts: New expiration date. Defaults to now + validity period. - email_sen: Whether an email has been sent for this validity period. - Defaults to False. + email_sent: Whether an email has been sent for this validity period. + renewal_token: Token sent with the renewal request. The user's token + will be cleared if this is None. Returns: New expiration date for this account, as a timestamp in milliseconds since epoch. """ + now = self.clock.time_msec() if expiration_ts is None: - expiration_ts = self.clock.time_msec() + self._account_validity.period + expiration_ts = now + self._account_validity_period await self.store.set_account_validity_for_user( - user_id=user_id, expiration_ts=expiration_ts, email_sent=email_sent + user_id=user_id, + expiration_ts=expiration_ts, + email_sent=email_sent, + renewal_token=renewal_token, + token_used_ts=now, ) return expiration_ts diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py index 3f6f9f7f3d..45d2404dde 100644 --- a/synapse/handlers/deactivate_account.py +++ b/synapse/handlers/deactivate_account.py @@ -49,7 +49,9 @@ def __init__(self, hs: "HomeServer"): if hs.config.run_background_tasks: hs.get_reactor().callWhenRunning(self._start_user_parting) - self._account_validity_enabled = hs.config.account_validity.enabled + self._account_validity_enabled = ( + hs.config.account_validity.account_validity_enabled + ) async def deactivate_account( self, diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 564a5ed0df..579fcdf472 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -62,7 +62,9 @@ def __init__(self, hs: "HomeServer"): self.store = self.hs.get_datastore() self.clock = self.hs.get_clock() - self._account_validity = hs.config.account_validity + self._account_validity_enabled = ( + hs.config.account_validity.account_validity_enabled + ) # We shard the handling of push notifications by user ID. self._pusher_shard_config = hs.config.push.pusher_shard_config @@ -236,7 +238,7 @@ async def _on_new_notifications(self, max_token: RoomStreamToken) -> None: for u in users_affected: # Don't push if the user account has expired - if self._account_validity.enabled: + if self._account_validity_enabled: expired = await self.store.is_account_expired( u, self.clock.time_msec() ) @@ -266,7 +268,7 @@ async def on_new_receipts( for u in users_affected: # Don't push if the user account has expired - if self._account_validity.enabled: + if self._account_validity_enabled: expired = await self.store.is_account_expired( u, self.clock.time_msec() ) diff --git a/synapse/res/templates/account_previously_renewed.html b/synapse/res/templates/account_previously_renewed.html new file mode 100644 index 0000000000..b751359bdf --- /dev/null +++ b/synapse/res/templates/account_previously_renewed.html @@ -0,0 +1 @@ +Your account is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}. diff --git a/synapse/res/templates/account_renewed.html b/synapse/res/templates/account_renewed.html index 894da030af..e8c0f52f05 100644 --- a/synapse/res/templates/account_renewed.html +++ b/synapse/res/templates/account_renewed.html @@ -1 +1 @@ -Your account has been successfully renewed. +Your account has been successfully renewed and is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}. diff --git a/synapse/rest/client/v2_alpha/account_validity.py b/synapse/rest/client/v2_alpha/account_validity.py index 0ad07fb895..2d1ad3d3fb 100644 --- a/synapse/rest/client/v2_alpha/account_validity.py +++ b/synapse/rest/client/v2_alpha/account_validity.py @@ -36,24 +36,40 @@ def __init__(self, hs): self.hs = hs self.account_activity_handler = hs.get_account_validity_handler() self.auth = hs.get_auth() - self.success_html = hs.config.account_validity.account_renewed_html_content - self.failure_html = hs.config.account_validity.invalid_token_html_content + self.account_renewed_template = ( + hs.config.account_validity.account_validity_account_renewed_template + ) + self.account_previously_renewed_template = ( + hs.config.account_validity.account_validity_account_previously_renewed_template + ) + self.invalid_token_template = ( + hs.config.account_validity.account_validity_invalid_token_template + ) async def on_GET(self, request): if b"token" not in request.args: raise SynapseError(400, "Missing renewal token") renewal_token = request.args[b"token"][0] - token_valid = await self.account_activity_handler.renew_account( + ( + token_valid, + token_stale, + expiration_ts, + ) = await self.account_activity_handler.renew_account( renewal_token.decode("utf8") ) if token_valid: status_code = 200 - response = self.success_html + response = self.account_renewed_template.render(expiration_ts=expiration_ts) + elif token_stale: + status_code = 200 + response = self.account_previously_renewed_template.render( + expiration_ts=expiration_ts + ) else: status_code = 404 - response = self.failure_html + response = self.invalid_token_template.render(expiration_ts=expiration_ts) respond_with_html(request, status_code, response) @@ -71,10 +87,12 @@ def __init__(self, hs): self.hs = hs self.account_activity_handler = hs.get_account_validity_handler() self.auth = hs.get_auth() - self.account_validity = self.hs.config.account_validity + self.account_validity_renew_by_email_enabled = ( + hs.config.account_validity.account_validity_renew_by_email_enabled + ) async def on_POST(self, request): - if not self.account_validity.renew_by_email_enabled: + if not self.account_validity_renew_by_email_enabled: raise AuthError( 403, "Account renewal via email is disabled on this server." ) diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index 833214b7e0..6e5ee557d2 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -91,13 +91,25 @@ def __init__( id_column=None, ) - self._account_validity = hs.config.account_validity - if hs.config.run_background_tasks and self._account_validity.enabled: - self._clock.call_later( - 0.0, - self._set_expiration_date_when_missing, + self._account_validity_enabled = ( + hs.config.account_validity.account_validity_enabled + ) + self._account_validity_period = None + self._account_validity_startup_job_max_delta = None + if self._account_validity_enabled: + self._account_validity_period = ( + hs.config.account_validity.account_validity_period + ) + self._account_validity_startup_job_max_delta = ( + hs.config.account_validity.account_validity_startup_job_max_delta ) + if hs.config.run_background_tasks: + self._clock.call_later( + 0.0, + self._set_expiration_date_when_missing, + ) + # Create a background job for culling expired 3PID validity tokens if hs.config.run_background_tasks: self._clock.looping_call( @@ -194,6 +206,7 @@ async def set_account_validity_for_user( expiration_ts: int, email_sent: bool, renewal_token: Optional[str] = None, + token_used_ts: Optional[int] = None, ) -> None: """Updates the account validity properties of the given account, with the given values. @@ -207,6 +220,8 @@ async def set_account_validity_for_user( period. renewal_token: Renewal token the user can use to extend the validity of their account. Defaults to no token. + token_used_ts: A timestamp of when the current token was used to renew + the account. """ def set_account_validity_for_user_txn(txn): @@ -218,6 +233,7 @@ def set_account_validity_for_user_txn(txn): "expiration_ts_ms": expiration_ts, "email_sent": email_sent, "renewal_token": renewal_token, + "token_used_ts_ms": token_used_ts, }, ) self._invalidate_cache_and_stream( @@ -231,7 +247,7 @@ def set_account_validity_for_user_txn(txn): async def set_renewal_token_for_user( self, user_id: str, renewal_token: str ) -> None: - """Defines a renewal token for a given user. + """Defines a renewal token for a given user, and clears the token_used timestamp. Args: user_id: ID of the user to set the renewal token for. @@ -244,26 +260,40 @@ async def set_renewal_token_for_user( await self.db_pool.simple_update_one( table="account_validity", keyvalues={"user_id": user_id}, - updatevalues={"renewal_token": renewal_token}, + updatevalues={"renewal_token": renewal_token, "token_used_ts_ms": None}, desc="set_renewal_token_for_user", ) - async def get_user_from_renewal_token(self, renewal_token: str) -> str: - """Get a user ID from a renewal token. + async def get_user_from_renewal_token( + self, renewal_token: str + ) -> Tuple[str, int, Optional[int]]: + """Get a user ID and renewal status from a renewal token. Args: renewal_token: The renewal token to perform the lookup with. Returns: - The ID of the user to which the token belongs. + A tuple of containing the following values: + * The ID of a user to which the token belongs. + * An int representing the user's expiry timestamp as milliseconds since the + epoch, or 0 if the token was invalid. + * An optional int representing the timestamp of when the user renewed their + account timestamp as milliseconds since the epoch. None if the account + has not been renewed using the current token yet. """ - return await self.db_pool.simple_select_one_onecol( + ret_dict = await self.db_pool.simple_select_one( table="account_validity", keyvalues={"renewal_token": renewal_token}, - retcol="user_id", + retcols=["user_id", "expiration_ts_ms", "token_used_ts_ms"], desc="get_user_from_renewal_token", ) + return ( + ret_dict["user_id"], + ret_dict["expiration_ts_ms"], + ret_dict["token_used_ts_ms"], + ) + async def get_renewal_token_for_user(self, user_id: str) -> str: """Get the renewal token associated with a given user ID. @@ -302,7 +332,7 @@ def select_users_txn(txn, now_ms, renew_at): "get_users_expiring_soon", select_users_txn, self._clock.time_msec(), - self.config.account_validity.renew_at, + self.config.account_validity_renew_at, ) async def set_renewal_mail_status(self, user_id: str, email_sent: bool) -> None: @@ -964,11 +994,11 @@ def set_expiration_date_for_user_txn(self, txn, user_id, use_delta=False): delta equal to 10% of the validity period. """ now_ms = self._clock.time_msec() - expiration_ts = now_ms + self._account_validity.period + expiration_ts = now_ms + self._account_validity_period if use_delta: expiration_ts = self.rand.randrange( - expiration_ts - self._account_validity.startup_job_max_delta, + expiration_ts - self._account_validity_startup_job_max_delta, expiration_ts, ) @@ -1412,7 +1442,7 @@ def _register_user( except self.database_engine.module.IntegrityError: raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE) - if self._account_validity.enabled: + if self._account_validity_enabled: self.set_expiration_date_for_user_txn(txn, user_id) if create_profile_with_displayname: diff --git a/synapse/storage/databases/main/schema/delta/59/12account_validity_token_used_ts_ms.sql b/synapse/storage/databases/main/schema/delta/59/12account_validity_token_used_ts_ms.sql new file mode 100644 index 0000000000..4836dac16e --- /dev/null +++ b/synapse/storage/databases/main/schema/delta/59/12account_validity_token_used_ts_ms.sql @@ -0,0 +1,18 @@ +/* Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Track when users renew their account using the value of the 'renewal_token' column. +-- This field should be set to NULL after a fresh token is generated. +ALTER TABLE account_validity ADD token_used_ts_ms BIGINT; diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index 054d4e4140..98695b05d5 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -492,8 +492,8 @@ def test_renewal_email(self): (user_id, tok) = self.create_user() - # Move 6 days forward. This should trigger a renewal email to be sent. - self.reactor.advance(datetime.timedelta(days=6).total_seconds()) + # Move 5 days forward. This should trigger a renewal email to be sent. + self.reactor.advance(datetime.timedelta(days=5).total_seconds()) self.assertEqual(len(self.email_attempts), 1) # Retrieving the URL from the email is too much pain for now, so we @@ -504,14 +504,32 @@ def test_renewal_email(self): self.assertEquals(channel.result["code"], b"200", channel.result) # Check that we're getting HTML back. - content_type = None - for header in channel.result.get("headers", []): - if header[0] == b"Content-Type": - content_type = header[1] - self.assertEqual(content_type, b"text/html; charset=utf-8", channel.result) + content_type = channel.headers.getRawHeaders(b"Content-Type") + self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result) # Check that the HTML we're getting is the one we expect on a successful renewal. - expected_html = self.hs.config.account_validity.account_renewed_html_content + expiration_ts = self.get_success(self.store.get_expiration_ts_for_user(user_id)) + expected_html = self.hs.config.account_validity.account_validity_account_renewed_template.render( + expiration_ts=expiration_ts + ) + self.assertEqual( + channel.result["body"], expected_html.encode("utf8"), channel.result + ) + + # Move 1 day forward. Try to renew with the same token again. + url = "/_matrix/client/unstable/account_validity/renew?token=%s" % renewal_token + channel = self.make_request(b"GET", url) + self.assertEquals(channel.result["code"], b"200", channel.result) + + # Check that we're getting HTML back. + content_type = channel.headers.getRawHeaders(b"Content-Type") + self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result) + + # Check that the HTML we're getting is the one we expect when reusing a + # token. The account expiration date should not have changed. + expected_html = self.hs.config.account_validity.account_validity_account_previously_renewed_template.render( + expiration_ts=expiration_ts + ) self.assertEqual( channel.result["body"], expected_html.encode("utf8"), channel.result ) @@ -531,15 +549,14 @@ def test_renewal_invalid_token(self): self.assertEquals(channel.result["code"], b"404", channel.result) # Check that we're getting HTML back. - content_type = None - for header in channel.result.get("headers", []): - if header[0] == b"Content-Type": - content_type = header[1] - self.assertEqual(content_type, b"text/html; charset=utf-8", channel.result) + content_type = channel.headers.getRawHeaders(b"Content-Type") + self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result) # Check that the HTML we're getting is the one we expect when using an # invalid/unknown token. - expected_html = self.hs.config.account_validity.invalid_token_html_content + expected_html = ( + self.hs.config.account_validity.account_validity_invalid_token_template.render() + ) self.assertEqual( channel.result["body"], expected_html.encode("utf8"), channel.result ) @@ -647,7 +664,12 @@ def make_homeserver(self, reactor, clock): config["account_validity"] = {"enabled": False} self.hs = self.setup_test_homeserver(config=config) - self.hs.config.account_validity.period = self.validity_period + + # We need to set these directly, instead of in the homeserver config dict above. + # This is due to account validity-related config options not being read by + # Synapse when account_validity.enabled is False. + self.hs.get_datastore()._account_validity_period = self.validity_period + self.hs.get_datastore()._account_validity_startup_job_max_delta = self.max_delta self.store = self.hs.get_datastore() From 495b214f4f8f45d16ffee851c8ab7a380dd0e2b2 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 20 Apr 2021 12:50:49 +0200 Subject: [PATCH 060/619] Fix (final) Bugbear violations (#9838) --- changelog.d/9838.misc | 1 + scripts-dev/definitions.py | 2 +- scripts-dev/list_url_patterns.py | 2 +- setup.cfg | 3 +-- synapse/event_auth.py | 2 +- synapse/federation/send_queue.py | 4 ++-- synapse/handlers/auth.py | 2 +- synapse/handlers/device.py | 13 +++++-------- synapse/handlers/federation.py | 2 +- synapse/logging/_remote.py | 4 ++-- synapse/rest/key/v2/remote_key_resource.py | 4 ++-- synapse/storage/databases/main/events.py | 10 +++++----- tests/handlers/test_federation.py | 2 +- tests/replication/tcp/streams/test_events.py | 4 ++-- tests/rest/admin/test_device.py | 4 ++-- tests/rest/admin/test_event_reports.py | 8 ++++---- tests/rest/admin/test_room.py | 8 ++++---- tests/rest/admin/test_statistics.py | 2 +- tests/rest/admin/test_user.py | 4 ++-- tests/rest/client/v1/test_rooms.py | 6 +++--- tests/storage/test_event_metrics.py | 4 ++-- tests/unittest.py | 2 +- tests/utils.py | 2 +- 23 files changed, 46 insertions(+), 49 deletions(-) create mode 100644 changelog.d/9838.misc diff --git a/changelog.d/9838.misc b/changelog.d/9838.misc new file mode 100644 index 0000000000..b98ce56309 --- /dev/null +++ b/changelog.d/9838.misc @@ -0,0 +1 @@ +Introduce flake8-bugbear to the test suite and fix some of its lint violations. \ No newline at end of file diff --git a/scripts-dev/definitions.py b/scripts-dev/definitions.py index 313860df13..c82ddd9677 100755 --- a/scripts-dev/definitions.py +++ b/scripts-dev/definitions.py @@ -140,7 +140,7 @@ def used_names(prefix, item, defs, names): definitions = {} for directory in args.directories: - for root, dirs, files in os.walk(directory): + for root, _, files in os.walk(directory): for filename in files: if filename.endswith(".py"): filepath = os.path.join(root, filename) diff --git a/scripts-dev/list_url_patterns.py b/scripts-dev/list_url_patterns.py index 26ad7c67f4..e85420dea8 100755 --- a/scripts-dev/list_url_patterns.py +++ b/scripts-dev/list_url_patterns.py @@ -48,7 +48,7 @@ def find_patterns_in_file(filepath): for directory in args.directories: - for root, dirs, files in os.walk(directory): + for root, _, files in os.walk(directory): for filename in files: if filename.endswith(".py"): filepath = os.path.join(root, filename) diff --git a/setup.cfg b/setup.cfg index 33601b71d5..e5ceb7ed19 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,8 +18,7 @@ ignore = # E203: whitespace before ':' (which is contrary to pep8?) # E731: do not assign a lambda expression, use a def # E501: Line too long (black enforces this for us) -# B007: Subsection of the bugbear suite (TODO: add in remaining fixes) -ignore=W503,W504,E203,E731,E501,B007 +ignore=W503,W504,E203,E731,E501 [isort] line_length = 88 diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 5234e3f81e..c831d9f73c 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -670,7 +670,7 @@ def _verify_third_party_invite(event: EventBase, auth_events: StateMap[EventBase public_key = public_key_object["public_key"] try: for server, signature_block in signed["signatures"].items(): - for key_name, encoded_signature in signature_block.items(): + for key_name in signature_block.keys(): if not key_name.startswith("ed25519:"): continue verify_key = decode_verify_key_bytes( diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index d71f04e43e..65d76ea974 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -501,10 +501,10 @@ def process_rows_for_federation( states=[state], destinations=destinations ) - for destination, edu_map in buff.keyed_edus.items(): + for edu_map in buff.keyed_edus.values(): for key, edu in edu_map.items(): transaction_queue.send_edu(edu, key) - for destination, edu_list in buff.edus.items(): + for edu_list in buff.edus.values(): for edu in edu_list: transaction_queue.send_edu(edu, None) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index b8a37b6477..36f2450e2e 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -1248,7 +1248,7 @@ async def delete_access_tokens_for_user( # see if any of our auth providers want to know about this for provider in self.password_providers: - for token, token_id, device_id in tokens_and_devices: + for token, _, device_id in tokens_and_devices: await provider.on_logged_out( user_id=user_id, device_id=device_id, access_token=token ) diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index d75edb184b..c1d7800981 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -156,8 +156,7 @@ async def get_user_ids_changed( # The user may have left the room # TODO: Check if they actually did or if we were just invited. if room_id not in room_ids: - for key, event_id in current_state_ids.items(): - etype, state_key = key + for etype, state_key in current_state_ids.keys(): if etype != EventTypes.Member: continue possibly_left.add(state_key) @@ -179,8 +178,7 @@ async def get_user_ids_changed( log_kv( {"event": "encountered empty previous state", "room_id": room_id} ) - for key, event_id in current_state_ids.items(): - etype, state_key = key + for etype, state_key in current_state_ids.keys(): if etype != EventTypes.Member: continue possibly_changed.add(state_key) @@ -198,8 +196,7 @@ async def get_user_ids_changed( for state_dict in prev_state_ids.values(): member_event = state_dict.get((EventTypes.Member, user_id), None) if not member_event or member_event != current_member_id: - for key, event_id in current_state_ids.items(): - etype, state_key = key + for etype, state_key in current_state_ids.keys(): if etype != EventTypes.Member: continue possibly_changed.add(state_key) @@ -714,7 +711,7 @@ async def _handle_device_updates(self, user_id: str) -> None: # This can happen since we batch updates return - for device_id, stream_id, prev_ids, content in pending_updates: + for device_id, stream_id, prev_ids, _ in pending_updates: logger.debug( "Handling update %r/%r, ID: %r, prev: %r ", user_id, @@ -740,7 +737,7 @@ async def _handle_device_updates(self, user_id: str) -> None: else: # Simply update the single device, since we know that is the only # change (because of the single prev_id matching the current cache) - for device_id, stream_id, prev_ids, content in pending_updates: + for device_id, stream_id, _, content in pending_updates: await self.store.update_remote_device_list_cache_entry( user_id, device_id, content, stream_id ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 4b3730aa3b..dbdd7d2db3 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2956,7 +2956,7 @@ async def _check_signature(self, event: EventBase, context: EventContext) -> Non try: # for each sig on the third_party_invite block of the actual invite for server, signature_block in signed["signatures"].items(): - for key_name, encoded_signature in signature_block.items(): + for key_name in signature_block.keys(): if not key_name.startswith("ed25519:"): continue diff --git a/synapse/logging/_remote.py b/synapse/logging/_remote.py index 4e8b0f8d10..c515690b38 100644 --- a/synapse/logging/_remote.py +++ b/synapse/logging/_remote.py @@ -226,11 +226,11 @@ def _handle_pressure(self) -> None: old_buffer = self._buffer self._buffer = deque() - for i in range(buffer_split): + for _ in range(buffer_split): self._buffer.append(old_buffer.popleft()) end_buffer = [] - for i in range(buffer_split): + for _ in range(buffer_split): end_buffer.append(old_buffer.pop()) self._buffer.extend(reversed(end_buffer)) diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index c57ac22e58..f648678b09 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -144,7 +144,7 @@ async def query_keys(self, request, query, query_remote_on_cache_miss=False): # Note that the value is unused. cache_misses = {} # type: Dict[str, Dict[str, int]] - for (server_name, key_id, from_server), results in cached.items(): + for (server_name, key_id, _), results in cached.items(): results = [(result["ts_added_ms"], result) for result in results] if not results and key_id is not None: @@ -206,7 +206,7 @@ async def query_keys(self, request, query, query_remote_on_cache_miss=False): # Cast to bytes since postgresql returns a memoryview. json_results.add(bytes(most_recent_result["key_json"])) else: - for ts_added, result in results: + for _, result in results: # Cast to bytes since postgresql returns a memoryview. json_results.add(bytes(result["key_json"])) diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index a362521e20..fd25c8112d 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -170,7 +170,7 @@ async def _persist_events_and_state_updates( ) async with stream_ordering_manager as stream_orderings: - for (event, context), stream in zip(events_and_contexts, stream_orderings): + for (event, _), stream in zip(events_and_contexts, stream_orderings): event.internal_metadata.stream_ordering = stream await self.db_pool.runInteraction( @@ -297,7 +297,7 @@ def _get_prevs_before_rejected_txn(txn, batch): txn.execute(sql + clause, args) to_recursively_check = [] - for event_id, prev_event_id, metadata, rejected in txn: + for _, prev_event_id, metadata, rejected in txn: if prev_event_id in existing_prevs: continue @@ -1127,7 +1127,7 @@ def _upsert_room_version_txn(self, txn: LoggingTransaction, room_id: str): def _update_forward_extremities_txn( self, txn, new_forward_extremities, max_stream_order ): - for room_id, new_extrem in new_forward_extremities.items(): + for room_id in new_forward_extremities.keys(): self.db_pool.simple_delete_txn( txn, table="event_forward_extremities", keyvalues={"room_id": room_id} ) @@ -1399,7 +1399,7 @@ def get_internal_metadata(event): ] state_values = [] - for event, context in state_events_and_contexts: + for event, _ in state_events_and_contexts: vals = { "event_id": event.event_id, "room_id": event.room_id, @@ -1468,7 +1468,7 @@ def _update_metadata_tables_txn( # nothing to do here return - for event, context in events_and_contexts: + for event, _ in events_and_contexts: if event.type == EventTypes.Redaction and event.redacts is not None: # Remove the entries in the event_push_actions table for the # redacted event. diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index c7b0975a19..8796af45ed 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -222,7 +222,7 @@ def create_invite(): room_version, ) - for i in range(3): + for _ in range(3): event = create_invite() self.get_success( self.handler.on_invite_request( diff --git a/tests/replication/tcp/streams/test_events.py b/tests/replication/tcp/streams/test_events.py index 323237c1bb..f51fa0a79e 100644 --- a/tests/replication/tcp/streams/test_events.py +++ b/tests/replication/tcp/streams/test_events.py @@ -239,7 +239,7 @@ def test_update_function_huge_state_change(self): # the state rows are unsorted state_rows = [] # type: List[EventsStreamCurrentStateRow] - for stream_name, token, row in received_rows: + for stream_name, _, row in received_rows: self.assertEqual("events", stream_name) self.assertIsInstance(row, EventsStreamRow) self.assertEqual(row.type, "state") @@ -356,7 +356,7 @@ def test_update_function_state_row_limit(self): # the state rows are unsorted state_rows = [] # type: List[EventsStreamCurrentStateRow] - for j in range(STATES_PER_USER + 1): + for _ in range(STATES_PER_USER + 1): stream_name, token, row = received_rows.pop(0) self.assertEqual("events", stream_name) self.assertIsInstance(row, EventsStreamRow) diff --git a/tests/rest/admin/test_device.py b/tests/rest/admin/test_device.py index ecbee30bb5..120730b764 100644 --- a/tests/rest/admin/test_device.py +++ b/tests/rest/admin/test_device.py @@ -430,7 +430,7 @@ def test_get_devices(self): """ # Create devices number_devices = 5 - for n in range(number_devices): + for _ in range(number_devices): self.login("user", "pass") # Get devices @@ -547,7 +547,7 @@ def test_delete_devices(self): # Create devices number_devices = 5 - for n in range(number_devices): + for _ in range(number_devices): self.login("user", "pass") # Get devices diff --git a/tests/rest/admin/test_event_reports.py b/tests/rest/admin/test_event_reports.py index 8c66da3af4..29341bc6e9 100644 --- a/tests/rest/admin/test_event_reports.py +++ b/tests/rest/admin/test_event_reports.py @@ -48,22 +48,22 @@ def prepare(self, reactor, clock, hs): self.helper.join(self.room_id2, user=self.admin_user, tok=self.admin_user_tok) # Two rooms and two users. Every user sends and reports every room event - for i in range(5): + for _ in range(5): self._create_event_and_report( room_id=self.room_id1, user_tok=self.other_user_tok, ) - for i in range(5): + for _ in range(5): self._create_event_and_report( room_id=self.room_id2, user_tok=self.other_user_tok, ) - for i in range(5): + for _ in range(5): self._create_event_and_report( room_id=self.room_id1, user_tok=self.admin_user_tok, ) - for i in range(5): + for _ in range(5): self._create_event_and_report( room_id=self.room_id2, user_tok=self.admin_user_tok, diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 6bcd997085..6b84188120 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -615,7 +615,7 @@ def test_list_rooms(self): # Create 3 test rooms total_rooms = 3 room_ids = [] - for x in range(total_rooms): + for _ in range(total_rooms): room_id = self.helper.create_room_as( self.admin_user, tok=self.admin_user_tok ) @@ -679,7 +679,7 @@ def test_list_rooms_pagination(self): # Create 5 test rooms total_rooms = 5 room_ids = [] - for x in range(total_rooms): + for _ in range(total_rooms): room_id = self.helper.create_room_as( self.admin_user, tok=self.admin_user_tok ) @@ -1577,7 +1577,7 @@ def test_context_as_admin(self): channel.json_body["event"]["event_id"], events[midway]["event_id"] ) - for i, found_event in enumerate(channel.json_body["events_before"]): + for found_event in channel.json_body["events_before"]: for j, posted_event in enumerate(events): if found_event["event_id"] == posted_event["event_id"]: self.assertTrue(j < midway) @@ -1585,7 +1585,7 @@ def test_context_as_admin(self): else: self.fail("Event %s from events_before not found" % j) - for i, found_event in enumerate(channel.json_body["events_after"]): + for found_event in channel.json_body["events_after"]: for j, posted_event in enumerate(events): if found_event["event_id"] == posted_event["event_id"]: self.assertTrue(j > midway) diff --git a/tests/rest/admin/test_statistics.py b/tests/rest/admin/test_statistics.py index 363bdeeb2d..79cac4266b 100644 --- a/tests/rest/admin/test_statistics.py +++ b/tests/rest/admin/test_statistics.py @@ -467,7 +467,7 @@ def _create_media(self, user_token: str, number_media: int): number_media: Number of media to be created for the user """ upload_resource = self.media_repo.children[b"upload"] - for i in range(number_media): + for _ in range(number_media): # file size is 67 Byte image_data = unhexlify( b"89504e470d0a1a0a0000000d4948445200000001000000010806" diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 2844c493fc..b3afd51522 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -1937,7 +1937,7 @@ def test_get_rooms(self): # Create rooms and join other_user_tok = self.login("user", "pass") number_rooms = 5 - for n in range(number_rooms): + for _ in range(number_rooms): self.helper.create_room_as(self.other_user, tok=other_user_tok) # Get rooms @@ -2517,7 +2517,7 @@ def _create_media_for_user(self, user_token: str, number_media: int): user_token: Access token of the user number_media: Number of media to be created for the user """ - for i in range(number_media): + for _ in range(number_media): # file size is 67 Byte image_data = unhexlify( b"89504e470d0a1a0a0000000d4948445200000001000000010806" diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 92babf65e0..a3694f3d02 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -646,7 +646,7 @@ def test_invites_by_rooms_ratelimit(self): def test_invites_by_users_ratelimit(self): """Tests that invites to a specific user are actually rate-limited.""" - for i in range(3): + for _ in range(3): room_id = self.helper.create_room_as(self.user_id) self.helper.invite(room_id, self.user_id, "@other-users:red") @@ -668,7 +668,7 @@ class RoomJoinRatelimitTestCase(RoomBase): ) def test_join_local_ratelimit(self): """Tests that local joins are actually rate-limited.""" - for i in range(3): + for _ in range(3): self.helper.create_room_as(self.user_id) self.helper.create_room_as(self.user_id, expect_code=429) @@ -733,7 +733,7 @@ def test_join_local_ratelimit_idempotent(self): for path in paths_to_test: # Make sure we send more requests than the rate-limiting config would allow # if all of these requests ended up joining the user to a room. - for i in range(4): + for _ in range(4): channel = self.make_request("POST", path % room_id, {}) self.assertEquals(channel.code, 200) diff --git a/tests/storage/test_event_metrics.py b/tests/storage/test_event_metrics.py index 397e68fe0a..088fbb247b 100644 --- a/tests/storage/test_event_metrics.py +++ b/tests/storage/test_event_metrics.py @@ -38,12 +38,12 @@ def test_exposed_to_prometheus(self): last_event = None # Make a real event chain - for i in range(event_count): + for _ in range(event_count): ev = self.create_and_send_event(room_id, user, False, last_event) last_event = [ev] # Sprinkle in some extremities - for i in range(extrems): + for _ in range(extrems): ev = self.create_and_send_event(room_id, user, False, last_event) # Let it run for a while, then pull out the statistics from the diff --git a/tests/unittest.py b/tests/unittest.py index d890ad981f..ee22a53849 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -133,7 +133,7 @@ def tearDown(orig): def assertObjectHasAttributes(self, attrs, obj): """Asserts that the given object has each of the attributes given, and that the value of each matches according to assertEquals.""" - for (key, value) in attrs.items(): + for key in attrs.keys(): if not hasattr(obj, key): raise AssertionError("Expected obj to have a '.%s'" % key) try: diff --git a/tests/utils.py b/tests/utils.py index af6b32fc66..63d52b9140 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -303,7 +303,7 @@ def cleanup(): # database for a few more seconds due to flakiness, preventing # us from dropping it when the test is over. If we can't drop # it, warn and move on. - for x in range(5): + for _ in range(5): try: cur.execute("DROP DATABASE IF EXISTS %s;" % (test_db,)) db_conn.commit() From db70435de740b534936df75c435290a37dcc015f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 20 Apr 2021 13:37:54 +0100 Subject: [PATCH 061/619] Fix bug where we sent remote presence states to remote servers (#9850) --- changelog.d/9850.feature | 1 + synapse/federation/sender/__init__.py | 4 ++++ synapse/handlers/presence.py | 11 ++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9850.feature diff --git a/changelog.d/9850.feature b/changelog.d/9850.feature new file mode 100644 index 0000000000..f56b0bb3bd --- /dev/null +++ b/changelog.d/9850.feature @@ -0,0 +1 @@ +Add experimental support for handling presence on a worker. diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 6266accaf5..b00a55324c 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -539,6 +539,10 @@ def send_presence_to_destinations( # No-op if presence is disabled. return + # Ensure we only send out presence states for local users. + for state in states: + assert self.is_mine_id(state.user_id) + for destination in destinations: if destination == self.server_name: continue diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 6460eb9952..bd2382193f 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -125,6 +125,7 @@ def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() self.presence_router = hs.get_presence_router() self.state = hs.get_state_handler() + self.is_mine_id = hs.is_mine_id self._federation = None if hs.should_send_federation() or not hs.config.worker_app: @@ -261,7 +262,8 @@ async def maybe_send_presence_to_interested_destinations( self, states: List[UserPresenceState] ): """If this instance is a federation sender, send the states to all - destinations that are interested. + destinations that are interested. Filters out any states for remote + users. """ if not self._send_federation: @@ -270,6 +272,11 @@ async def maybe_send_presence_to_interested_destinations( # If this worker sends federation we must have a FederationSender. assert self._federation + states = [s for s in states if self.is_mine_id(s.user_id)] + + if not states: + return + hosts_and_states = await get_interested_remotes( self.store, self.presence_router, @@ -292,7 +299,6 @@ class WorkerPresenceHandler(BasePresenceHandler): def __init__(self, hs): super().__init__(hs) self.hs = hs - self.is_mine_id = hs.is_mine_id self._presence_enabled = hs.config.use_presence @@ -492,7 +498,6 @@ class PresenceHandler(BasePresenceHandler): def __init__(self, hs: "HomeServer"): super().__init__(hs) self.hs = hs - self.is_mine_id = hs.is_mine_id self.server_name = hs.hostname self.wheel_timer = WheelTimer() self.notifier = hs.get_notifier() From de0d088adc0cf3d5bbd80238b88143426cd6eaca Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 20 Apr 2021 14:11:24 +0100 Subject: [PATCH 062/619] Add presence federation stream (#9819) --- changelog.d/9819.feature | 1 + synapse/handlers/presence.py | 243 ++++++++++++++++++-- synapse/replication/tcp/client.py | 7 +- synapse/replication/tcp/streams/__init__.py | 3 + synapse/replication/tcp/streams/_base.py | 24 ++ tests/handlers/test_presence.py | 179 +++++++++++++- 6 files changed, 426 insertions(+), 31 deletions(-) create mode 100644 changelog.d/9819.feature diff --git a/changelog.d/9819.feature b/changelog.d/9819.feature new file mode 100644 index 0000000000..f56b0bb3bd --- /dev/null +++ b/changelog.d/9819.feature @@ -0,0 +1 @@ +Add experimental support for handling presence on a worker. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index bd2382193f..598466c9bd 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -24,6 +24,7 @@ import abc import contextlib import logging +from bisect import bisect from contextlib import contextmanager from typing import ( TYPE_CHECKING, @@ -53,7 +54,9 @@ ReplicationBumpPresenceActiveTime, ReplicationPresenceSetState, ) +from synapse.replication.http.streams import ReplicationGetStreamUpdates from synapse.replication.tcp.commands import ClearUserSyncsCommand +from synapse.replication.tcp.streams import PresenceFederationStream, PresenceStream from synapse.state import StateHandler from synapse.storage.databases.main import DataStore from synapse.types import Collection, JsonDict, UserID, get_domain_from_id @@ -128,10 +131,10 @@ def __init__(self, hs: "HomeServer"): self.is_mine_id = hs.is_mine_id self._federation = None - if hs.should_send_federation() or not hs.config.worker_app: + if hs.should_send_federation(): self._federation = hs.get_federation_sender() - self._send_federation = hs.should_send_federation() + self._federation_queue = PresenceFederationQueue(hs, self) self._busy_presence_enabled = hs.config.experimental.msc3026_enabled @@ -254,9 +257,17 @@ async def update_external_syncs_clear(self, process_id): """ pass - async def process_replication_rows(self, token, rows): - """Process presence stream rows received over replication.""" - pass + async def process_replication_rows( + self, stream_name: str, instance_name: str, token: int, rows: list + ): + """Process streams received over replication.""" + await self._federation_queue.process_replication_rows( + stream_name, instance_name, token, rows + ) + + def get_federation_queue(self) -> "PresenceFederationQueue": + """Get the presence federation queue.""" + return self._federation_queue async def maybe_send_presence_to_interested_destinations( self, states: List[UserPresenceState] @@ -266,12 +277,9 @@ async def maybe_send_presence_to_interested_destinations( users. """ - if not self._send_federation: + if not self._federation: return - # If this worker sends federation we must have a FederationSender. - assert self._federation - states = [s for s in states if self.is_mine_id(s.user_id)] if not states: @@ -427,7 +435,14 @@ async def notify_from_replication(self, states, stream_id): # If this is a federation sender, notify about presence updates. await self.maybe_send_presence_to_interested_destinations(states) - async def process_replication_rows(self, token, rows): + async def process_replication_rows( + self, stream_name: str, instance_name: str, token: int, rows: list + ): + await super().process_replication_rows(stream_name, instance_name, token, rows) + + if stream_name != PresenceStream.NAME: + return + states = [ UserPresenceState( row.user_id, @@ -729,12 +744,10 @@ async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None: self.state, ) - # Since this is master we know that we have a federation sender or - # queue, and so this will be defined. - assert self._federation - for destinations, states in hosts_and_states: - self._federation.send_presence_to_destinations(states, destinations) + self._federation_queue.send_presence_to_destinations( + states, destinations + ) async def _handle_timeouts(self): """Checks the presence of users that have timed out and updates as @@ -1213,13 +1226,9 @@ async def _handle_state_delta(self, deltas): user_presence_states ) - # Since this is master we know that we have a federation sender or - # queue, and so this will be defined. - assert self._federation - # Send out user presence updates for each destination for destination, user_state_set in presence_destinations.items(): - self._federation.send_presence_to_destinations( + self._federation_queue.send_presence_to_destinations( destinations=[destination], states=user_state_set ) @@ -1864,3 +1873,197 @@ async def get_interested_remotes( hosts_and_states.append(([host], states)) return hosts_and_states + + +class PresenceFederationQueue: + """Handles sending ad hoc presence updates over federation, which are *not* + due to state updates (that get handled via the presence stream), e.g. + federation pings and sending existing present states to newly joined hosts. + + Only the last N minutes will be queued, so if a federation sender instance + is down for longer then some updates will be dropped. This is OK as presence + is ephemeral, and so it will self correct eventually. + + On workers the class tracks the last received position of the stream from + replication, and handles querying for missed updates over HTTP replication, + c.f. `get_current_token` and `get_replication_rows`. + """ + + # How long to keep entries in the queue for. Workers that are down for + # longer than this duration will miss out on older updates. + _KEEP_ITEMS_IN_QUEUE_FOR_MS = 5 * 60 * 1000 + + # How often to check if we can expire entries from the queue. + _CLEAR_ITEMS_EVERY_MS = 60 * 1000 + + def __init__(self, hs: "HomeServer", presence_handler: BasePresenceHandler): + self._clock = hs.get_clock() + self._notifier = hs.get_notifier() + self._instance_name = hs.get_instance_name() + self._presence_handler = presence_handler + self._repl_client = ReplicationGetStreamUpdates.make_client(hs) + + # Should we keep a queue of recent presence updates? We only bother if + # another process may be handling federation sending. + self._queue_presence_updates = True + + # Whether this instance is a presence writer. + self._presence_writer = hs.config.worker.worker_app is None + + # The FederationSender instance, if this process sends federation traffic directly. + self._federation = None + + if hs.should_send_federation(): + self._federation = hs.get_federation_sender() + + # We don't bother queuing up presence states if only this instance + # is sending federation. + if hs.config.worker.federation_shard_config.instances == [ + self._instance_name + ]: + self._queue_presence_updates = False + + # The queue of recently queued updates as tuples of: `(timestamp, + # stream_id, destinations, user_ids)`. We don't store the full states + # for efficiency, and remote workers will already have the full states + # cached. + self._queue = [] # type: List[Tuple[int, int, Collection[str], Set[str]]] + + self._next_id = 1 + + # Map from instance name to current token + self._current_tokens = {} # type: Dict[str, int] + + if self._queue_presence_updates: + self._clock.looping_call(self._clear_queue, self._CLEAR_ITEMS_EVERY_MS) + + def _clear_queue(self): + """Clear out older entries from the queue.""" + clear_before = self._clock.time_msec() - self._KEEP_ITEMS_IN_QUEUE_FOR_MS + + # The queue is sorted by timestamp, so we can bisect to find the right + # place to purge before. Note that we are searching using a 1-tuple with + # the time, which does The Right Thing since the queue is a tuple where + # the first item is a timestamp. + index = bisect(self._queue, (clear_before,)) + self._queue = self._queue[index:] + + def send_presence_to_destinations( + self, states: Collection[UserPresenceState], destinations: Collection[str] + ) -> None: + """Send the presence states to the given destinations. + + Will forward to the local federation sender (if there is one) and queue + to send over replication (if there are other federation sender instances.). + + Must only be called on the master process. + """ + + # This should only be called on a presence writer. + assert self._presence_writer + + if self._federation: + self._federation.send_presence_to_destinations( + states=states, + destinations=destinations, + ) + + if not self._queue_presence_updates: + return + + now = self._clock.time_msec() + + stream_id = self._next_id + self._next_id += 1 + + self._queue.append((now, stream_id, destinations, {s.user_id for s in states})) + + self._notifier.notify_replication() + + def get_current_token(self, instance_name: str) -> int: + """Get the current position of the stream. + + On workers this returns the last stream ID received from replication. + """ + if instance_name == self._instance_name: + return self._next_id - 1 + else: + return self._current_tokens.get(instance_name, 0) + + async def get_replication_rows( + self, + instance_name: str, + from_token: int, + upto_token: int, + target_row_count: int, + ) -> Tuple[List[Tuple[int, Tuple[str, str]]], int, bool]: + """Get all the updates between the two tokens. + + We return rows in the form of `(destination, user_id)` to keep the size + of each row bounded (rather than returning the sets in a row). + + On workers this will query the master process via HTTP replication. + """ + if instance_name != self._instance_name: + # If not local we query over http replication from the master + result = await self._repl_client( + instance_name=instance_name, + stream_name=PresenceFederationStream.NAME, + from_token=from_token, + upto_token=upto_token, + ) + return result["updates"], result["upto_token"], result["limited"] + + # We can find the correct position in the queue by noting that there is + # exactly one entry per stream ID, and that the last entry has an ID of + # `self._next_id - 1`, so we can count backwards from the end. + # + # Since the start of the queue is periodically truncated we need to + # handle the case where `from_token` stream ID has already been dropped. + start_idx = max(from_token - self._next_id, -len(self._queue)) + + to_send = [] # type: List[Tuple[int, Tuple[str, str]]] + limited = False + new_id = upto_token + for _, stream_id, destinations, user_ids in self._queue[start_idx:]: + if stream_id > upto_token: + break + + new_id = stream_id + + to_send.extend( + (stream_id, (destination, user_id)) + for destination in destinations + for user_id in user_ids + ) + + if len(to_send) > target_row_count: + limited = True + break + + return to_send, new_id, limited + + async def process_replication_rows( + self, stream_name: str, instance_name: str, token: int, rows: list + ): + if stream_name != PresenceFederationStream.NAME: + return + + # We keep track of the current tokens (so that we can catch up with anything we missed after a disconnect) + self._current_tokens[instance_name] = token + + # If we're a federation sender we pull out the presence states to send + # and forward them on. + if not self._federation: + return + + hosts_to_users = {} # type: Dict[str, Set[str]] + for row in rows: + hosts_to_users.setdefault(row.destination, set()).add(row.user_id) + + for host, user_ids in hosts_to_users.items(): + states = await self._presence_handler.current_state_for_users(user_ids) + self._federation.send_presence_to_destinations( + states=states.values(), + destinations=[host], + ) diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index ce5d651cb8..4f3c6a18b6 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -29,7 +29,6 @@ AccountDataStream, DeviceListsStream, GroupServerStream, - PresenceStream, PushersStream, PushRulesStream, ReceiptsStream, @@ -191,8 +190,6 @@ async def on_rdata( self.stop_pusher(row.user_id, row.app_id, row.pushkey) else: await self.start_pusher(row.user_id, row.app_id, row.pushkey) - elif stream_name == PresenceStream.NAME: - await self._presence_handler.process_replication_rows(token, rows) elif stream_name == EventsStream.NAME: # We shouldn't get multiple rows per token for events stream, so # we don't need to optimise this for multiple rows. @@ -221,6 +218,10 @@ async def on_rdata( membership=row.data.membership, ) + await self._presence_handler.process_replication_rows( + stream_name, instance_name, token, rows + ) + # Notify any waiting deferreds. The list is ordered by position so we # just iterate through the list until we reach a position that is # greater than the received row position. diff --git a/synapse/replication/tcp/streams/__init__.py b/synapse/replication/tcp/streams/__init__.py index fb74ac4e98..4c0023c68a 100644 --- a/synapse/replication/tcp/streams/__init__.py +++ b/synapse/replication/tcp/streams/__init__.py @@ -30,6 +30,7 @@ CachesStream, DeviceListsStream, GroupServerStream, + PresenceFederationStream, PresenceStream, PublicRoomsStream, PushersStream, @@ -50,6 +51,7 @@ EventsStream, BackfillStream, PresenceStream, + PresenceFederationStream, TypingStream, ReceiptsStream, PushRulesStream, @@ -71,6 +73,7 @@ "Stream", "BackfillStream", "PresenceStream", + "PresenceFederationStream", "TypingStream", "ReceiptsStream", "PushRulesStream", diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 520c45f151..9d75a89f1c 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -290,6 +290,30 @@ def __init__(self, hs): ) +class PresenceFederationStream(Stream): + """A stream used to send ad hoc presence updates over federation. + + Streams the remote destination and the user ID of the presence state to + send. + """ + + @attr.s(slots=True, auto_attribs=True) + class PresenceFederationStreamRow: + destination: str + user_id: str + + NAME = "presence_federation" + ROW_TYPE = PresenceFederationStreamRow + + def __init__(self, hs: "HomeServer"): + federation_queue = hs.get_presence_handler().get_federation_queue() + super().__init__( + hs.get_instance_name(), + federation_queue.get_current_token, + federation_queue.get_replication_rows, + ) + + class TypingStream(Stream): TypingStreamRow = namedtuple( "TypingStreamRow", ("room_id", "user_ids") # str # list(str) diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 2d12e82897..61271cd084 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -21,6 +21,7 @@ from synapse.api.presence import UserPresenceState from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.events.builder import EventBuilder +from synapse.federation.sender import FederationSender from synapse.handlers.presence import ( EXTERNAL_PROCESS_EXPIRY, FEDERATION_PING_INTERVAL, @@ -471,6 +472,168 @@ def test_external_process_timeout(self): self.assertEqual(state.state, PresenceState.OFFLINE) +class PresenceFederationQueueTestCase(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, hs): + self.presence_handler = hs.get_presence_handler() + self.clock = hs.get_clock() + self.instance_name = hs.get_instance_name() + + self.queue = self.presence_handler.get_federation_queue() + + def test_send_and_get(self): + state1 = UserPresenceState.default("@user1:test") + state2 = UserPresenceState.default("@user2:test") + state3 = UserPresenceState.default("@user3:test") + + prev_token = self.queue.get_current_token(self.instance_name) + + self.queue.send_presence_to_destinations((state1, state2), ("dest1", "dest2")) + self.queue.send_presence_to_destinations((state3,), ("dest3",)) + + now_token = self.queue.get_current_token(self.instance_name) + + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", prev_token, now_token, 10) + ) + + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + + expected_rows = [ + (1, ("dest1", "@user1:test")), + (1, ("dest2", "@user1:test")), + (1, ("dest1", "@user2:test")), + (1, ("dest2", "@user2:test")), + (2, ("dest3", "@user3:test")), + ] + + self.assertCountEqual(rows, expected_rows) + + def test_send_and_get_split(self): + state1 = UserPresenceState.default("@user1:test") + state2 = UserPresenceState.default("@user2:test") + state3 = UserPresenceState.default("@user3:test") + + prev_token = self.queue.get_current_token(self.instance_name) + + self.queue.send_presence_to_destinations((state1, state2), ("dest1", "dest2")) + + now_token = self.queue.get_current_token(self.instance_name) + + self.queue.send_presence_to_destinations((state3,), ("dest3",)) + + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", prev_token, now_token, 10) + ) + + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + + expected_rows = [ + (1, ("dest1", "@user1:test")), + (1, ("dest2", "@user1:test")), + (1, ("dest1", "@user2:test")), + (1, ("dest2", "@user2:test")), + ] + + self.assertCountEqual(rows, expected_rows) + + def test_clear_queue_all(self): + state1 = UserPresenceState.default("@user1:test") + state2 = UserPresenceState.default("@user2:test") + state3 = UserPresenceState.default("@user3:test") + + prev_token = self.queue.get_current_token(self.instance_name) + + self.queue.send_presence_to_destinations((state1, state2), ("dest1", "dest2")) + self.queue.send_presence_to_destinations((state3,), ("dest3",)) + + self.reactor.advance(10 * 60 * 1000) + + now_token = self.queue.get_current_token(self.instance_name) + + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", prev_token, now_token, 10) + ) + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + self.assertCountEqual(rows, []) + + prev_token = self.queue.get_current_token(self.instance_name) + + self.queue.send_presence_to_destinations((state1, state2), ("dest1", "dest2")) + self.queue.send_presence_to_destinations((state3,), ("dest3",)) + + now_token = self.queue.get_current_token(self.instance_name) + + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", prev_token, now_token, 10) + ) + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + + expected_rows = [ + (3, ("dest1", "@user1:test")), + (3, ("dest2", "@user1:test")), + (3, ("dest1", "@user2:test")), + (3, ("dest2", "@user2:test")), + (4, ("dest3", "@user3:test")), + ] + + self.assertCountEqual(rows, expected_rows) + + def test_partially_clear_queue(self): + state1 = UserPresenceState.default("@user1:test") + state2 = UserPresenceState.default("@user2:test") + state3 = UserPresenceState.default("@user3:test") + + prev_token = self.queue.get_current_token(self.instance_name) + + self.queue.send_presence_to_destinations((state1, state2), ("dest1", "dest2")) + + self.reactor.advance(2 * 60 * 1000) + + self.queue.send_presence_to_destinations((state3,), ("dest3",)) + + self.reactor.advance(4 * 60 * 1000) + + now_token = self.queue.get_current_token(self.instance_name) + + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", prev_token, now_token, 10) + ) + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + + expected_rows = [ + (2, ("dest3", "@user3:test")), + ] + self.assertCountEqual(rows, []) + + prev_token = self.queue.get_current_token(self.instance_name) + + self.queue.send_presence_to_destinations((state1, state2), ("dest1", "dest2")) + self.queue.send_presence_to_destinations((state3,), ("dest3",)) + + now_token = self.queue.get_current_token(self.instance_name) + + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", prev_token, now_token, 10) + ) + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + + expected_rows = [ + (3, ("dest1", "@user1:test")), + (3, ("dest2", "@user1:test")), + (3, ("dest1", "@user2:test")), + (3, ("dest2", "@user2:test")), + (4, ("dest3", "@user3:test")), + ] + + self.assertCountEqual(rows, expected_rows) + + class PresenceJoinTestCase(unittest.HomeserverTestCase): """Tests remote servers get told about presence of users in the room when they join and when new local users join. @@ -482,10 +645,17 @@ class PresenceJoinTestCase(unittest.HomeserverTestCase): def make_homeserver(self, reactor, clock): hs = self.setup_test_homeserver( - "server", federation_http_client=None, federation_sender=Mock() + "server", + federation_http_client=None, + federation_sender=Mock(spec=FederationSender), ) return hs + def default_config(self): + config = super().default_config() + config["send_federation"] = True + return config + def prepare(self, reactor, clock, hs): self.federation_sender = hs.get_federation_sender() self.event_builder_factory = hs.get_event_builder_factory() @@ -529,9 +699,6 @@ def test_remote_joins(self): # Add a new remote server to the room self._add_new_user(room_id, "@alice:server2") - # We shouldn't have sent out any local presence *updates* - self.federation_sender.send_presence.assert_not_called() - # When new server is joined we send it the local users presence states. # We expect to only see user @test2:server, as @test:server is offline # and has a zero last_active_ts @@ -550,7 +717,6 @@ def test_remote_joins(self): self.federation_sender.reset_mock() self._add_new_user(room_id, "@bob:server3") - self.federation_sender.send_presence.assert_not_called() self.federation_sender.send_presence_to_destinations.assert_called_once_with( destinations=["server3"], states={expected_state} ) @@ -595,9 +761,6 @@ def test_remote_gets_presence_when_local_user_joins(self): self.reactor.pump([0]) # Wait for presence updates to be handled - # We shouldn't have sent out any local presence *updates* - self.federation_sender.send_presence.assert_not_called() - # We expect to only send test2 presence to server2 and server3 expected_state = self.get_success( self.presence_handler.current_state_for_user("@test2:server") From b076bc276e881b262048307b6a226061d96c4a8d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 20 Apr 2021 09:19:00 -0400 Subject: [PATCH 063/619] Always use the name as the log ID. (#9829) As far as I can tell our logging contexts are meant to log the request ID, or sometimes the request ID followed by a suffix (this is generally stored in the name field of LoggingContext). There's also code to log the name@memory location, but I'm not sure this is ever used. This simplifies the code paths to require every logging context to have a name and use that in logging. For sub-contexts (created via nested_logging_contexts, defer_to_threadpool, Measure) we use the current context's str (which becomes their name or the string "sentinel") and then potentially modify that (e.g. add a suffix). --- changelog.d/9829.bugfix | 1 + synapse/logging/context.py | 14 ++++---------- synapse/metrics/background_process_metrics.py | 15 ++++----------- synapse/replication/tcp/protocol.py | 2 +- synapse/util/metrics.py | 14 +++++++++----- tests/logging/test_terse_json.py | 6 ++++-- tests/test_federation.py | 2 +- tests/util/caches/test_descriptors.py | 6 ++---- 8 files changed, 26 insertions(+), 34 deletions(-) create mode 100644 changelog.d/9829.bugfix diff --git a/changelog.d/9829.bugfix b/changelog.d/9829.bugfix new file mode 100644 index 0000000000..d0c1e49fd8 --- /dev/null +++ b/changelog.d/9829.bugfix @@ -0,0 +1 @@ +Fix the log lines of nested logging contexts. diff --git a/synapse/logging/context.py b/synapse/logging/context.py index e78343f554..dbd7d3a33a 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -277,7 +277,7 @@ class LoggingContext: def __init__( self, - name: Optional[str] = None, + name: str, parent_context: "Optional[LoggingContext]" = None, request: Optional[ContextRequest] = None, ) -> None: @@ -315,9 +315,7 @@ def __init__( self.request = request def __str__(self) -> str: - if self.request: - return self.request.request_id - return "%s@%x" % (self.name, id(self)) + return self.name @classmethod def current_context(cls) -> LoggingContextOrSentinel: @@ -694,17 +692,13 @@ def nested_logging_context(suffix: str) -> LoggingContext: "Starting nested logging context from sentinel context: metrics will be lost" ) parent_context = None - prefix = "" - request = None else: assert isinstance(curr_context, LoggingContext) parent_context = curr_context - prefix = str(parent_context.name) - request = parent_context.request + prefix = str(curr_context) return LoggingContext( prefix + "-" + suffix, parent_context=parent_context, - request=request, ) @@ -895,7 +889,7 @@ def defer_to_threadpool(reactor, threadpool, f, *args, **kwargs): parent_context = curr_context def g(): - with LoggingContext(parent_context=parent_context): + with LoggingContext(str(curr_context), parent_context=parent_context): return f(*args, **kwargs) return make_deferred_yieldable(threads.deferToThreadPool(reactor, threadpool, g)) diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index e8a9096c03..78e9cfbc26 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -16,7 +16,7 @@ import logging import threading from functools import wraps -from typing import TYPE_CHECKING, Dict, Optional, Set, Union +from typing import TYPE_CHECKING, Dict, Optional, Set from prometheus_client.core import REGISTRY, Counter, Gauge @@ -199,7 +199,7 @@ async def run(): _background_process_start_count.labels(desc).inc() _background_process_in_flight_count.labels(desc).inc() - with BackgroundProcessLoggingContext(desc, count) as context: + with BackgroundProcessLoggingContext("%s-%s" % (desc, count)) as context: try: ctx = noop_context_manager() if bg_start_span: @@ -242,19 +242,12 @@ class BackgroundProcessLoggingContext(LoggingContext): processes. """ - __slots__ = ["_id", "_proc"] + __slots__ = ["_proc"] - def __init__(self, name: str, id: Optional[Union[int, str]] = None): + def __init__(self, name: str): super().__init__(name) - self._id = id - self._proc = _BackgroundProcess(name, self) - def __str__(self) -> str: - if self._id is not None: - return "%s-%s" % (self.name, self._id) - return "%s@%x" % (self.name, id(self)) - def start(self, rusage: "Optional[resource._RUsage]"): """Log context has started running (again).""" diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index d10d574246..ba753318bd 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -185,7 +185,7 @@ def __init__(self, clock: Clock, handler: "ReplicationCommandHandler"): # a logcontext which we use for processing incoming commands. We declare it as a # background process so that the CPU stats get reported to prometheus. self._logging_context = BackgroundProcessLoggingContext( - "replication-conn", self.conn_id + "replication-conn-%s" % (self.conn_id,) ) def connectionMade(self): diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 1023c856d1..019cfa17cc 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -105,7 +105,13 @@ class Measure: "start", ] - def __init__(self, clock, name): + def __init__(self, clock, name: str): + """ + Args: + clock: A n object with a "time()" method, which returns the current + time in seconds. + name: The name of the metric to report. + """ self.clock = clock self.name = name curr_context = current_context() @@ -118,10 +124,8 @@ def __init__(self, clock, name): else: assert isinstance(curr_context, LoggingContext) parent_context = curr_context - self._logging_context = LoggingContext( - "Measure[%s]" % (self.name,), parent_context - ) - self.start = None + self._logging_context = LoggingContext(str(curr_context), parent_context) + self.start = None # type: Optional[int] def __enter__(self) -> "Measure": if self.start is not None: diff --git a/tests/logging/test_terse_json.py b/tests/logging/test_terse_json.py index 215fd8b0f9..ecf873e2ab 100644 --- a/tests/logging/test_terse_json.py +++ b/tests/logging/test_terse_json.py @@ -138,7 +138,7 @@ def test_with_context(self): ] self.assertCountEqual(log.keys(), expected_log_keys) self.assertEqual(log["log"], "Hello there, wally!") - self.assertTrue(log["request"].startswith("name@")) + self.assertEqual(log["request"], "name") def test_with_request_context(self): """ @@ -165,7 +165,9 @@ def test_with_request_context(self): # Also set the requester to ensure the processing works. request.requester = "@foo:test" - with LoggingContext(parent_context=request.logcontext): + with LoggingContext( + request.get_request_id(), parent_context=request.logcontext + ): logger.info("Hello there, %s!", "wally") log = self.get_log_line() diff --git a/tests/test_federation.py b/tests/test_federation.py index 8928597d17..382cedbd5d 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -134,7 +134,7 @@ async def post_json(destination, path, data, headers=None, timeout=0): } ) - with LoggingContext(): + with LoggingContext("test-context"): failure = self.get_failure( self.handler.on_receive_pdu( "test.serv", lying_event, sent_to_us_directly=True diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index 2d1f9360e0..8c082e7432 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -231,8 +231,7 @@ def inner_fn(): @defer.inlineCallbacks def do_lookup(): - with LoggingContext() as c1: - c1.name = "c1" + with LoggingContext("c1") as c1: r = yield obj.fn(1) self.assertEqual(current_context(), c1) return r @@ -274,8 +273,7 @@ def inner_fn(): @defer.inlineCallbacks def do_lookup(): - with LoggingContext() as c1: - c1.name = "c1" + with LoggingContext("c1") as c1: try: d = obj.fn(1) self.assertEqual( From 0a88ec0a879d9fcb6f2202b7cff3766ed5f7253b Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 20 Apr 2021 14:19:35 +0100 Subject: [PATCH 064/619] Add Application Service registration type requirement + py35, pg95 deprecation notices to v1.32.0 upgrade notes (#9849) Fixes https://github.com/matrix-org/synapse/issues/9846. Adds important removal information from the top of https://github.com/matrix-org/synapse/releases/tag/v1.32.0rc1 into UPGRADE.rst. --- UPGRADE.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/UPGRADE.rst b/UPGRADE.rst index 665821d4ef..7a9b869055 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -88,6 +88,14 @@ for example: Upgrading to v1.32.0 ==================== +Dropping support for old Python, Postgres and SQLite versions +------------------------------------------------------------- + +In line with our `deprecation policy `_, +we've dropped support for Python 3.5 and PostgreSQL 9.5, as they are no longer supported upstream. + +This release of Synapse requires Python 3.6+ and PostgresSQL 9.6+ or SQLite 3.22+. + Removal of old List Accounts Admin API -------------------------------------- @@ -98,6 +106,16 @@ has been available since Synapse 1.7.0 (2019-12-13), and is accessible under ``G The deprecation of the old endpoint was announced with Synapse 1.28.0 (released on 2021-02-25). +Application Services must use type ``m.login.application_service`` when registering users +----------------------------------------------------------------------------------------- + +In compliance with the +`Application Service spec `_, +Application Services are now required to use the ``m.login.application_service`` type when registering users via the +``/_matrix/client/r0/register`` endpoint. This behaviour was deprecated in Synapse v1.30.0. + +Please ensure your Application Services are up to date. + Upgrading to v1.29.0 ==================== From e031c7e0cca2422aa2c5d3704adc66723d8094e7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Apr 2021 14:31:27 +0100 Subject: [PATCH 065/619] 1.32.0 --- CHANGES.md | 13 +++++++++++-- changelog.d/9829.bugfix | 1 - debian/changelog | 8 ++++++-- synapse/__init__.py | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/9829.bugfix diff --git a/CHANGES.md b/CHANGES.md index 41908f84be..4d48a321c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,5 @@ -Synapse 1.32.0rc1 (2021-04-13) -============================== +Synapse 1.32.0 (2021-04-20) +=========================== **Note:** This release requires Python 3.6+ and Postgres 9.6+ or SQLite 3.22+. @@ -7,6 +7,15 @@ This release removes the deprecated `GET /_synapse/admin/v1/users/` adm This release requires Application Services to use type `m.login.application_services` when registering users via the `/_matrix/client/r0/register` endpoint to comply with the spec. Please ensure your Application Services are up to date. +Bugfixes +-------- + +- Fix the log lines of nested logging contexts. Broke in 1.32.0rc1. ([\#9829](https://github.com/matrix-org/synapse/issues/9829)) + + +Synapse 1.32.0rc1 (2021-04-13) +============================== + Features -------- diff --git a/changelog.d/9829.bugfix b/changelog.d/9829.bugfix deleted file mode 100644 index d0c1e49fd8..0000000000 --- a/changelog.d/9829.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the log lines of nested logging contexts. diff --git a/debian/changelog b/debian/changelog index 5d526316fc..83be4497ec 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,12 @@ -matrix-synapse-py3 (1.31.0+nmu1) UNRELEASED; urgency=medium +matrix-synapse-py3 (1.32.0) stable; urgency=medium + [ Dan Callahan ] * Skip tests when DEB_BUILD_OPTIONS contains "nocheck". - -- Dan Callahan Mon, 12 Apr 2021 13:07:36 +0000 + [ Synapse Packaging team ] + * New synapse release 1.32.0. + + -- Synapse Packaging team Tue, 20 Apr 2021 14:28:39 +0100 matrix-synapse-py3 (1.31.0) stable; urgency=medium diff --git a/synapse/__init__.py b/synapse/__init__.py index 125a73d378..79232c4de1 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -48,7 +48,7 @@ except ImportError: pass -__version__ = "1.32.0rc1" +__version__ = "1.32.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 438a8594cb5a74478da36fe33ba98d86e2ca00fc Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Apr 2021 14:47:17 +0100 Subject: [PATCH 066/619] Update v1.32.0 changelog. It's m.login.application_service, not plural --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4d48a321c6..482863c0e8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Synapse 1.32.0 (2021-04-20) This release removes the deprecated `GET /_synapse/admin/v1/users/` admin API. Please use the [v2 API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/user_admin_api.rst#query-user-account) instead, which has improved capabilities. -This release requires Application Services to use type `m.login.application_services` when registering users via the `/_matrix/client/r0/register` endpoint to comply with the spec. Please ensure your Application Services are up to date. +This release requires Application Services to use type `m.login.application_service` when registering users via the `/_matrix/client/r0/register` endpoint to comply with the spec. Please ensure your Application Services are up to date. Bugfixes -------- From 913f790bb2ef7f1186e03afea85323dfa4da6df8 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Apr 2021 15:33:56 +0100 Subject: [PATCH 067/619] Add note about expired Debian gpg signing keys to CHANGES.md --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 482863c0e8..b0fbc5a452 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,12 @@ This release removes the deprecated `GET /_synapse/admin/v1/users/` adm This release requires Application Services to use type `m.login.application_service` when registering users via the `/_matrix/client/r0/register` endpoint to comply with the spec. Please ensure your Application Services are up to date. +If you are using the `packages.matrix.org` Debian repository for Synapse packages, note that our gpg signing keys have rotated and the old pair have expired. You can pull the latest keys with: + +``` +sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg +``` + Bugfixes -------- From 05fa06834df10966be3f727fa0797424b12660f3 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Apr 2021 15:50:54 +0100 Subject: [PATCH 068/619] Further tweaking on gpg signing key notice --- CHANGES.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b0fbc5a452..170d1e447d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,9 +7,12 @@ This release removes the deprecated `GET /_synapse/admin/v1/users/` adm This release requires Application Services to use type `m.login.application_service` when registering users via the `/_matrix/client/r0/register` endpoint to comply with the spec. Please ensure your Application Services are up to date. -If you are using the `packages.matrix.org` Debian repository for Synapse packages, note that our gpg signing keys have rotated and the old pair have expired. You can pull the latest keys with: +If you are using the `packages.matrix.org` Debian repository for Synapse packages, +note that we have recently updated the expiry date on the gpg signing key. If you see an +error similar to `The following signatures were invalid: EXPKEYSIG F473DD4473365DE1`, you +will need to get a fresh copy of the keys. You can do so with: -``` +```sh sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg ``` From b8c5f6fddbc8c3203c2841500767ef2fc9dc6ff6 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Apr 2021 17:11:36 +0100 Subject: [PATCH 069/619] Mention Prometheus metrics regression in v1.32.0 --- CHANGES.md | 6 ++++++ UPGRADE.rst | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 170d1e447d..7713328f12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Synapse 1.32.0 (2021-04-20) =========================== +**Note:** This release introduces [a regression](https://githubcom/matrix-org/synapse/issues/9853) +that can overwhelm connected Prometheus instances. This issue was not present in +Synapse v1.32.0rc1. It is recommended not to update to this release. If you have +upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be +resolved in a subsequent release version shortly. + **Note:** This release requires Python 3.6+ and Postgres 9.6+ or SQLite 3.22+. This release removes the deprecated `GET /_synapse/admin/v1/users/` admin API. Please use the [v2 API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/user_admin_api.rst#query-user-account) instead, which has improved capabilities. diff --git a/UPGRADE.rst b/UPGRADE.rst index 7a9b869055..c8dce62227 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -88,6 +88,15 @@ for example: Upgrading to v1.32.0 ==================== +Regression causing connected Prometheus instances to become overwhelmed +----------------------------------------------------------------------- + +This release introduces `a regression `_ +that can overwhelm connected Prometheus instances. This issue was not present in +Synapse v1.32.0rc1. It is recommended not to update to this release. If you have +upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be +resolved in a subsequent release version shortly. + Dropping support for old Python, Postgres and SQLite versions ------------------------------------------------------------- From 683d6f75af0e941e9ab3bc0a985aa6ed5cc7a238 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 20 Apr 2021 14:55:20 -0400 Subject: [PATCH 070/619] Rename handler and config modules which end in handler/config. (#9816) --- changelog.d/9816.misc | 1 + docs/sample_config.yaml | 2 +- docs/sso_mapping_providers.md | 4 ++-- synapse/config/_base.pyi | 20 +++++++++---------- .../config/{consent_config.py => consent.py} | 0 synapse/config/homeserver.py | 10 +++++----- synapse/config/{jwt_config.py => jwt.py} | 0 synapse/config/{oidc_config.py => oidc.py} | 7 ++++++- synapse/config/{saml2_config.py => saml2.py} | 7 ++++++- ...er_notices_config.py => server_notices.py} | 0 synapse/handlers/{cas_handler.py => cas.py} | 0 synapse/handlers/{oidc_handler.py => oidc.py} | 5 +---- synapse/handlers/{saml_handler.py => saml.py} | 0 synapse/rest/client/v2_alpha/register.py | 2 +- synapse/server.py | 10 +++++----- tests/handlers/test_cas.py | 2 +- tests/handlers/test_oidc.py | 8 ++++---- 17 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 changelog.d/9816.misc rename synapse/config/{consent_config.py => consent.py} (100%) rename synapse/config/{jwt_config.py => jwt.py} (100%) rename synapse/config/{oidc_config.py => oidc.py} (98%) rename synapse/config/{saml2_config.py => saml2.py} (97%) rename synapse/config/{server_notices_config.py => server_notices.py} (100%) rename synapse/handlers/{cas_handler.py => cas.py} (100%) rename synapse/handlers/{oidc_handler.py => oidc.py} (99%) rename synapse/handlers/{saml_handler.py => saml.py} (100%) diff --git a/changelog.d/9816.misc b/changelog.d/9816.misc new file mode 100644 index 0000000000..d098122500 --- /dev/null +++ b/changelog.d/9816.misc @@ -0,0 +1 @@ +Rename some handlers and config modules to not duplicate the top-level module. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index d260d76259..e0350279ad 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1900,7 +1900,7 @@ saml2_config: # sub-properties: # # module: The class name of a custom mapping module. Default is -# 'synapse.handlers.oidc_handler.JinjaOidcMappingProvider'. +# 'synapse.handlers.oidc.JinjaOidcMappingProvider'. # See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers # for information on implementing a custom mapping provider. # diff --git a/docs/sso_mapping_providers.md b/docs/sso_mapping_providers.md index e1d6ede7ba..50020d1a4a 100644 --- a/docs/sso_mapping_providers.md +++ b/docs/sso_mapping_providers.md @@ -106,7 +106,7 @@ A custom mapping provider must specify the following methods: Synapse has a built-in OpenID mapping provider if a custom provider isn't specified in the config. It is located at -[`synapse.handlers.oidc_handler.JinjaOidcMappingProvider`](../synapse/handlers/oidc_handler.py). +[`synapse.handlers.oidc.JinjaOidcMappingProvider`](../synapse/handlers/oidc.py). ## SAML Mapping Providers @@ -190,4 +190,4 @@ A custom mapping provider must specify the following methods: Synapse has a built-in SAML mapping provider if a custom provider isn't specified in the config. It is located at -[`synapse.handlers.saml_handler.DefaultSamlMappingProvider`](../synapse/handlers/saml_handler.py). +[`synapse.handlers.saml.DefaultSamlMappingProvider`](../synapse/handlers/saml.py). diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index ddec356a07..ff9abbc232 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -7,16 +7,16 @@ from synapse.config import ( auth, captcha, cas, - consent_config, + consent, database, emailconfig, experimental, groups, - jwt_config, + jwt, key, logger, metrics, - oidc_config, + oidc, password_auth_providers, push, ratelimiting, @@ -24,9 +24,9 @@ from synapse.config import ( registration, repository, room_directory, - saml2_config, + saml2, server, - server_notices_config, + server_notices, spam_checker, sso, stats, @@ -65,11 +65,11 @@ class RootConfig: api: api.ApiConfig appservice: appservice.AppServiceConfig key: key.KeyConfig - saml2: saml2_config.SAML2Config + saml2: saml2.SAML2Config cas: cas.CasConfig sso: sso.SSOConfig - oidc: oidc_config.OIDCConfig - jwt: jwt_config.JWTConfig + oidc: oidc.OIDCConfig + jwt: jwt.JWTConfig auth: auth.AuthConfig email: emailconfig.EmailConfig worker: workers.WorkerConfig @@ -78,9 +78,9 @@ class RootConfig: spamchecker: spam_checker.SpamCheckerConfig groups: groups.GroupsConfig userdirectory: user_directory.UserDirectoryConfig - consent: consent_config.ConsentConfig + consent: consent.ConsentConfig stats: stats.StatsConfig - servernotices: server_notices_config.ServerNoticesConfig + servernotices: server_notices.ServerNoticesConfig roomdirectory: room_directory.RoomDirectoryConfig thirdpartyrules: third_party_event_rules.ThirdPartyRulesConfig tracer: tracer.TracerConfig diff --git a/synapse/config/consent_config.py b/synapse/config/consent.py similarity index 100% rename from synapse/config/consent_config.py rename to synapse/config/consent.py diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 58e3bcd511..c23b66c88c 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -20,17 +20,17 @@ from .cache import CacheConfig from .captcha import CaptchaConfig from .cas import CasConfig -from .consent_config import ConsentConfig +from .consent import ConsentConfig from .database import DatabaseConfig from .emailconfig import EmailConfig from .experimental import ExperimentalConfig from .federation import FederationConfig from .groups import GroupsConfig -from .jwt_config import JWTConfig +from .jwt import JWTConfig from .key import KeyConfig from .logger import LoggingConfig from .metrics import MetricsConfig -from .oidc_config import OIDCConfig +from .oidc import OIDCConfig from .password_auth_providers import PasswordAuthProviderConfig from .push import PushConfig from .ratelimiting import RatelimitConfig @@ -39,9 +39,9 @@ from .repository import ContentRepositoryConfig from .room import RoomConfig from .room_directory import RoomDirectoryConfig -from .saml2_config import SAML2Config +from .saml2 import SAML2Config from .server import ServerConfig -from .server_notices_config import ServerNoticesConfig +from .server_notices import ServerNoticesConfig from .spam_checker import SpamCheckerConfig from .sso import SSOConfig from .stats import StatsConfig diff --git a/synapse/config/jwt_config.py b/synapse/config/jwt.py similarity index 100% rename from synapse/config/jwt_config.py rename to synapse/config/jwt.py diff --git a/synapse/config/oidc_config.py b/synapse/config/oidc.py similarity index 98% rename from synapse/config/oidc_config.py rename to synapse/config/oidc.py index 5fb94376fd..72402eb81d 100644 --- a/synapse/config/oidc_config.py +++ b/synapse/config/oidc.py @@ -27,7 +27,10 @@ from ._base import Config, ConfigError, read_file -DEFAULT_USER_MAPPING_PROVIDER = "synapse.handlers.oidc_handler.JinjaOidcMappingProvider" +DEFAULT_USER_MAPPING_PROVIDER = "synapse.handlers.oidc.JinjaOidcMappingProvider" +# The module that JinjaOidcMappingProvider is in was renamed, we want to +# transparently handle both the same. +LEGACY_USER_MAPPING_PROVIDER = "synapse.handlers.oidc_handler.JinjaOidcMappingProvider" class OIDCConfig(Config): @@ -403,6 +406,8 @@ def _parse_oidc_config_dict( """ ump_config = oidc_config.get("user_mapping_provider", {}) ump_config.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER) + if ump_config.get("module") == LEGACY_USER_MAPPING_PROVIDER: + ump_config["module"] = DEFAULT_USER_MAPPING_PROVIDER ump_config.setdefault("config", {}) ( diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2.py similarity index 97% rename from synapse/config/saml2_config.py rename to synapse/config/saml2.py index 55a7838b10..3d1218c8d1 100644 --- a/synapse/config/saml2_config.py +++ b/synapse/config/saml2.py @@ -25,7 +25,10 @@ logger = logging.getLogger(__name__) -DEFAULT_USER_MAPPING_PROVIDER = ( +DEFAULT_USER_MAPPING_PROVIDER = "synapse.handlers.saml.DefaultSamlMappingProvider" +# The module that DefaultSamlMappingProvider is in was renamed, we want to +# transparently handle both the same. +LEGACY_USER_MAPPING_PROVIDER = ( "synapse.handlers.saml_handler.DefaultSamlMappingProvider" ) @@ -97,6 +100,8 @@ def read_config(self, config, **kwargs): # Use the default user mapping provider if not set ump_dict.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER) + if ump_dict.get("module") == LEGACY_USER_MAPPING_PROVIDER: + ump_dict["module"] = DEFAULT_USER_MAPPING_PROVIDER # Ensure a config is present ump_dict["config"] = ump_dict.get("config") or {} diff --git a/synapse/config/server_notices_config.py b/synapse/config/server_notices.py similarity index 100% rename from synapse/config/server_notices_config.py rename to synapse/config/server_notices.py diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas.py similarity index 100% rename from synapse/handlers/cas_handler.py rename to synapse/handlers/cas.py diff --git a/synapse/handlers/oidc_handler.py b/synapse/handlers/oidc.py similarity index 99% rename from synapse/handlers/oidc_handler.py rename to synapse/handlers/oidc.py index b156196a70..45514be50f 100644 --- a/synapse/handlers/oidc_handler.py +++ b/synapse/handlers/oidc.py @@ -37,10 +37,7 @@ from twisted.web.http_headers import Headers from synapse.config import ConfigError -from synapse.config.oidc_config import ( - OidcProviderClientSecretJwtKey, - OidcProviderConfig, -) +from synapse.config.oidc import OidcProviderClientSecretJwtKey, OidcProviderConfig from synapse.handlers.sso import MappingException, UserAttributes from synapse.http.site import SynapseRequest from synapse.logging.context import make_deferred_yieldable diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml.py similarity index 100% rename from synapse/handlers/saml_handler.py rename to synapse/handlers/saml.py diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index b26aad7b34..c5a6800b8a 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -30,7 +30,7 @@ ) from synapse.config import ConfigError from synapse.config.captcha import CaptchaConfig -from synapse.config.consent_config import ConsentConfig +from synapse.config.consent import ConsentConfig from synapse.config.emailconfig import ThreepidBehaviour from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.registration import RegistrationConfig diff --git a/synapse/server.py b/synapse/server.py index 42d2fad8e8..59ae91b503 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -70,7 +70,7 @@ from synapse.handlers.admin import AdminHandler from synapse.handlers.appservice import ApplicationServicesHandler from synapse.handlers.auth import AuthHandler, MacaroonGenerator -from synapse.handlers.cas_handler import CasHandler +from synapse.handlers.cas import CasHandler from synapse.handlers.deactivate_account import DeactivateAccountHandler from synapse.handlers.device import DeviceHandler, DeviceWorkerHandler from synapse.handlers.devicemessage import DeviceMessageHandler @@ -145,8 +145,8 @@ if TYPE_CHECKING: from txredisapi import RedisProtocol - from synapse.handlers.oidc_handler import OidcHandler - from synapse.handlers.saml_handler import SamlHandler + from synapse.handlers.oidc import OidcHandler + from synapse.handlers.saml import SamlHandler T = TypeVar("T", bound=Callable[..., Any]) @@ -696,13 +696,13 @@ def get_cas_handler(self) -> CasHandler: @cache_in_self def get_saml_handler(self) -> "SamlHandler": - from synapse.handlers.saml_handler import SamlHandler + from synapse.handlers.saml import SamlHandler return SamlHandler(self) @cache_in_self def get_oidc_handler(self) -> "OidcHandler": - from synapse.handlers.oidc_handler import OidcHandler + from synapse.handlers.oidc import OidcHandler return OidcHandler(self) diff --git a/tests/handlers/test_cas.py b/tests/handlers/test_cas.py index 0444b26798..b625995d12 100644 --- a/tests/handlers/test_cas.py +++ b/tests/handlers/test_cas.py @@ -13,7 +13,7 @@ # limitations under the License. from unittest.mock import Mock -from synapse.handlers.cas_handler import CasResponse +from synapse.handlers.cas import CasResponse from tests.test_utils import simple_async_mock from tests.unittest import HomeserverTestCase, override_config diff --git a/tests/handlers/test_oidc.py b/tests/handlers/test_oidc.py index 34d2fc1dfb..a25c89bd5b 100644 --- a/tests/handlers/test_oidc.py +++ b/tests/handlers/test_oidc.py @@ -499,7 +499,7 @@ def test_callback(self): self.assertRenderedError("fetch_error") # Handle code exchange failure - from synapse.handlers.oidc_handler import OidcError + from synapse.handlers.oidc import OidcError self.provider._exchange_code = simple_async_mock( raises=OidcError("invalid_request") @@ -583,7 +583,7 @@ def test_exchange_code(self): body=b'{"error": "foo", "error_description": "bar"}', ) ) - from synapse.handlers.oidc_handler import OidcError + from synapse.handlers.oidc import OidcError exc = self.get_failure(self.provider._exchange_code(code), OidcError) self.assertEqual(exc.value.error, "foo") @@ -1126,7 +1126,7 @@ def _generate_oidc_session_token( client_redirect_url: str, ui_auth_session_id: str = "", ) -> str: - from synapse.handlers.oidc_handler import OidcSessionData + from synapse.handlers.oidc import OidcSessionData return self.handler._token_generator.generate_oidc_session_token( state=state, @@ -1152,7 +1152,7 @@ async def _make_callback_with_userinfo( userinfo: the OIDC userinfo dict client_redirect_url: the URL to redirect to on success. """ - from synapse.handlers.oidc_handler import OidcSessionData + from synapse.handlers.oidc import OidcSessionData handler = hs.get_oidc_handler() provider = handler._providers["oidc"] From 5d281c10dd3d4d1f96635e92d803a74e3880d6b7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 21 Apr 2021 10:03:31 +0100 Subject: [PATCH 071/619] Stop BackgroundProcessLoggingContext making new prometheus timeseries (#9854) This undoes part of b076bc276e881b262048307b6a226061d96c4a8d. --- changelog.d/9854.bugfix | 1 + synapse/metrics/background_process_metrics.py | 20 +++++++++++++++---- synapse/replication/tcp/protocol.py | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 changelog.d/9854.bugfix diff --git a/changelog.d/9854.bugfix b/changelog.d/9854.bugfix new file mode 100644 index 0000000000..e39a3f9915 --- /dev/null +++ b/changelog.d/9854.bugfix @@ -0,0 +1 @@ +Fix a regression in Synapse 1.32.0 which caused Synapse to report large numbers of Prometheus time series, potentially overwhelming Prometheus instances. diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index 78e9cfbc26..3f621539f3 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -16,7 +16,7 @@ import logging import threading from functools import wraps -from typing import TYPE_CHECKING, Dict, Optional, Set +from typing import TYPE_CHECKING, Dict, Optional, Set, Union from prometheus_client.core import REGISTRY, Counter, Gauge @@ -199,7 +199,7 @@ async def run(): _background_process_start_count.labels(desc).inc() _background_process_in_flight_count.labels(desc).inc() - with BackgroundProcessLoggingContext("%s-%s" % (desc, count)) as context: + with BackgroundProcessLoggingContext(desc, count) as context: try: ctx = noop_context_manager() if bg_start_span: @@ -244,8 +244,20 @@ class BackgroundProcessLoggingContext(LoggingContext): __slots__ = ["_proc"] - def __init__(self, name: str): - super().__init__(name) + def __init__(self, name: str, instance_id: Optional[Union[int, str]] = None): + """ + + Args: + name: The name of the background process. Each distinct `name` gets a + separate prometheus time series. + + instance_id: an identifer to add to `name` to distinguish this instance of + the named background process in the logs. If this is `None`, one is + made up based on id(self). + """ + if instance_id is None: + instance_id = id(self) + super().__init__("%s-%s" % (name, instance_id)) self._proc = _BackgroundProcess(name, self) def start(self, rusage: "Optional[resource._RUsage]"): diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index ba753318bd..d10d574246 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -185,7 +185,7 @@ def __init__(self, clock: Clock, handler: "ReplicationCommandHandler"): # a logcontext which we use for processing incoming commands. We declare it as a # background process so that the CPU stats get reported to prometheus. self._logging_context = BackgroundProcessLoggingContext( - "replication-conn-%s" % (self.conn_id,) + "replication-conn", self.conn_id ) def connectionMade(self): From 30c94862b4fbaa782e47eac37a673a53feae2bb1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Apr 2021 17:11:36 +0100 Subject: [PATCH 072/619] Mention Prometheus metrics regression in v1.32.0 --- CHANGES.md | 6 ++++++ UPGRADE.rst | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 170d1e447d..7713328f12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Synapse 1.32.0 (2021-04-20) =========================== +**Note:** This release introduces [a regression](https://githubcom/matrix-org/synapse/issues/9853) +that can overwhelm connected Prometheus instances. This issue was not present in +Synapse v1.32.0rc1. It is recommended not to update to this release. If you have +upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be +resolved in a subsequent release version shortly. + **Note:** This release requires Python 3.6+ and Postgres 9.6+ or SQLite 3.22+. This release removes the deprecated `GET /_synapse/admin/v1/users/` admin API. Please use the [v2 API](https://github.com/matrix-org/synapse/blob/develop/docs/admin_api/user_admin_api.rst#query-user-account) instead, which has improved capabilities. diff --git a/UPGRADE.rst b/UPGRADE.rst index 7a9b869055..c8dce62227 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -88,6 +88,15 @@ for example: Upgrading to v1.32.0 ==================== +Regression causing connected Prometheus instances to become overwhelmed +----------------------------------------------------------------------- + +This release introduces `a regression `_ +that can overwhelm connected Prometheus instances. This issue was not present in +Synapse v1.32.0rc1. It is recommended not to update to this release. If you have +upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be +resolved in a subsequent release version shortly. + Dropping support for old Python, Postgres and SQLite versions ------------------------------------------------------------- From a745531c10da75079fcac152935bc2ff505eec14 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Apr 2021 14:01:12 +0100 Subject: [PATCH 073/619] 1.32.1 --- CHANGES.md | 9 +++++++++ changelog.d/9854.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/9854.bugfix diff --git a/CHANGES.md b/CHANGES.md index 7713328f12..65819ee1e1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.32.1 (2021-04-21) +=========================== + +Bugfixes +-------- + +- Fix a regression in Synapse 1.32.0 which caused Synapse to report large numbers of Prometheus time series, potentially overwhelming Prometheus instances. ([\#9854](https://github.com/matrix-org/synapse/issues/9854)) + + Synapse 1.32.0 (2021-04-20) =========================== diff --git a/changelog.d/9854.bugfix b/changelog.d/9854.bugfix deleted file mode 100644 index e39a3f9915..0000000000 --- a/changelog.d/9854.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a regression in Synapse 1.32.0 which caused Synapse to report large numbers of Prometheus time series, potentially overwhelming Prometheus instances. diff --git a/debian/changelog b/debian/changelog index 83be4497ec..b8cf2cac58 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.32.1) stable; urgency=medium + + * New synapse release 1.32.1. + + -- Synapse Packaging team Wed, 21 Apr 2021 14:00:55 +0100 + matrix-synapse-py3 (1.32.0) stable; urgency=medium [ Dan Callahan ] diff --git a/synapse/__init__.py b/synapse/__init__.py index 79232c4de1..a0332d602d 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -48,7 +48,7 @@ except ImportError: pass -__version__ = "1.32.0" +__version__ = "1.32.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 026a66f2b37998a6e62d07267e6a114f87dc9b84 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Apr 2021 14:04:44 +0100 Subject: [PATCH 074/619] Fix typo in link to regression in 1.32.0 upgrade notes --- UPGRADE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UPGRADE.rst b/UPGRADE.rst index c8dce62227..0be6e27cc0 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -91,7 +91,7 @@ Upgrading to v1.32.0 Regression causing connected Prometheus instances to become overwhelmed ----------------------------------------------------------------------- -This release introduces `a regression `_ +This release introduces `a regression `_ that can overwhelm connected Prometheus instances. This issue was not present in Synapse v1.32.0rc1. It is recommended not to update to this release. If you have upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be From 98a1b84631e5fbc227f211821d9bce318dc921d8 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Apr 2021 14:07:51 +0100 Subject: [PATCH 075/619] Add link to fixing prometheus to 1.32.0 upgrade notes; 1.32.1 has a fix --- CHANGES.md | 2 ++ UPGRADE.rst | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 65819ee1e1..d06bcd9ae8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ Synapse 1.32.1 (2021-04-21) =========================== +This release fixes the regression introduced in Synapse + Bugfixes -------- diff --git a/UPGRADE.rst b/UPGRADE.rst index 0be6e27cc0..d4651ec2d3 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -94,8 +94,10 @@ Regression causing connected Prometheus instances to become overwhelmed This release introduces `a regression `_ that can overwhelm connected Prometheus instances. This issue was not present in Synapse v1.32.0rc1. It is recommended not to update to this release. If you have -upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be -resolved in a subsequent release version shortly. +upgraded to v1.32.0 already, please upgrade to v1.31.1 which contains a fix. +If you started Synapse on v1.32.0, you may need to remove excess writeahead logs +in order for Prometheus to recover; instructions for doing so are +`here `_. Dropping support for old Python, Postgres and SQLite versions ------------------------------------------------------------- From acb8c81041aa66e03b22150e457cfd2c7a44d436 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Apr 2021 14:24:16 +0100 Subject: [PATCH 076/619] Add regression notes to CHANGES.md; fix link in 1.32.0 changelog --- CHANGES.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d06bcd9ae8..cc66f2f01d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,11 @@ Synapse 1.32.1 (2021-04-21) =========================== -This release fixes the regression introduced in Synapse +This release fixes [a regression](https://github.com/matrix-org/synapse/issues/9853) +in Synapse 1.32.0 that caused connected Prometheus instances to become unstable. If you +ran Synapse 1.32.0 with Prometheus metrics, first upgrade to Synapse 1.32.1 and follow +[these instructions](https://github.com/matrix-org/synapse/pull/9854#issuecomment-823472183) +to clean up any excess writeahead logs. Bugfixes -------- @@ -12,7 +16,7 @@ Bugfixes Synapse 1.32.0 (2021-04-20) =========================== -**Note:** This release introduces [a regression](https://githubcom/matrix-org/synapse/issues/9853) +**Note:** This release introduces [a regression](https://github.com/matrix-org/synapse/issues/9853) that can overwhelm connected Prometheus instances. This issue was not present in Synapse v1.32.0rc1. It is recommended not to update to this release. If you have upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be From bdb4c20dc1ed4b5c88ac2889d1a02a32a9dd8a1f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Apr 2021 14:44:04 +0100 Subject: [PATCH 077/619] Clarify 1.32.0/1 changelog and upgrade notes --- CHANGES.md | 4 +--- UPGRADE.rst | 11 ++++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cc66f2f01d..7188f94445 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,9 +18,7 @@ Synapse 1.32.0 (2021-04-20) **Note:** This release introduces [a regression](https://github.com/matrix-org/synapse/issues/9853) that can overwhelm connected Prometheus instances. This issue was not present in -Synapse v1.32.0rc1. It is recommended not to update to this release. If you have -upgraded to v1.32.0 already, please downgrade to v1.31.0. This issue will be -resolved in a subsequent release version shortly. +1.32.0rc1, and is fixed in 1.32.1. See the changelog for 1.32.1 above for more information. **Note:** This release requires Python 3.6+ and Postgres 9.6+ or SQLite 3.22+. diff --git a/UPGRADE.rst b/UPGRADE.rst index d4651ec2d3..76d2ee394f 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -92,11 +92,12 @@ Regression causing connected Prometheus instances to become overwhelmed ----------------------------------------------------------------------- This release introduces `a regression `_ -that can overwhelm connected Prometheus instances. This issue was not present in -Synapse v1.32.0rc1. It is recommended not to update to this release. If you have -upgraded to v1.32.0 already, please upgrade to v1.31.1 which contains a fix. -If you started Synapse on v1.32.0, you may need to remove excess writeahead logs -in order for Prometheus to recover; instructions for doing so are +that can overwhelm connected Prometheus instances. This issue is not present in +Synapse v1.32.0rc1, and is fixed in Synapse v1.32.1. + +If you have been affected, please first upgrade to a more recent Synapse version. +You then may need to remove excess writeahead logs in order for Prometheus to recover. +Instructions for doing so are provided `here `_. Dropping support for old Python, Postgres and SQLite versions From d9bd62f9d1a6238b3f485caee07f9fd399b27134 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 21 Apr 2021 16:39:34 +0100 Subject: [PATCH 078/619] Make LoggingContext's name optional (#9857) Fixes https://github.com/matrix-org/synapse-s3-storage-provider/issues/55 --- changelog.d/9857.bugfix | 1 + synapse/logging/context.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9857.bugfix diff --git a/changelog.d/9857.bugfix b/changelog.d/9857.bugfix new file mode 100644 index 0000000000..7eed41594d --- /dev/null +++ b/changelog.d/9857.bugfix @@ -0,0 +1 @@ +Fix a regression in Synapse v1.32.1 which caused `LoggingContext` errors in plugins. diff --git a/synapse/logging/context.py b/synapse/logging/context.py index dbd7d3a33a..7fc11a9ac2 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -258,7 +258,8 @@ class LoggingContext: child to the parent Args: - name (str): Name for the context for debugging. + name: Name for the context for logging. If this is omitted, it is + inherited from the parent context. parent_context (LoggingContext|None): The parent of the new context """ @@ -277,12 +278,11 @@ class LoggingContext: def __init__( self, - name: str, + name: Optional[str] = None, parent_context: "Optional[LoggingContext]" = None, request: Optional[ContextRequest] = None, ) -> None: self.previous_context = current_context() - self.name = name # track the resources used by this context so far self._resource_usage = ContextResourceUsage() @@ -314,6 +314,15 @@ def __init__( # the request param overrides the request from the parent context self.request = request + # if we don't have a `name`, but do have a parent context, use its name. + if self.parent_context and name is None: + name = str(self.parent_context) + if name is None: + raise ValueError( + "LoggingContext must be given either a name or a parent context" + ) + self.name = name + def __str__(self) -> str: return self.name From 0c23aa393cf5f26a9a49267d113767ffcf82d58f Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 21 Apr 2021 18:16:58 +0100 Subject: [PATCH 079/619] Note LoggingContext signature change incompatibility in 1.32.0 (#9859) 1.32.0 also introduced an incompatibility with Synapse modules that make use of `synapse.logging.context.LoggingContext`, such as [synapse-s3-storage-provider](https://github.com/matrix-org/synapse-s3-storage-provider). This PR adds a note to the 1.32.0 changelog and upgrade notes about it. --- CHANGES.md | 17 ++++++++++++----- UPGRADE.rst | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7188f94445..a1349252cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,10 @@ Synapse 1.32.1 (2021-04-21) =========================== This release fixes [a regression](https://github.com/matrix-org/synapse/issues/9853) -in Synapse 1.32.0 that caused connected Prometheus instances to become unstable. If you -ran Synapse 1.32.0 with Prometheus metrics, first upgrade to Synapse 1.32.1 and follow -[these instructions](https://github.com/matrix-org/synapse/pull/9854#issuecomment-823472183) -to clean up any excess writeahead logs. +in Synapse 1.32.0 that caused connected Prometheus instances to become unstable. + +However, as this release is still subject to the `LoggingContext` change in 1.32.0, +it is recommended to remain on or downgrade to 1.31.0. Bugfixes -------- @@ -18,7 +18,14 @@ Synapse 1.32.0 (2021-04-20) **Note:** This release introduces [a regression](https://github.com/matrix-org/synapse/issues/9853) that can overwhelm connected Prometheus instances. This issue was not present in -1.32.0rc1, and is fixed in 1.32.1. See the changelog for 1.32.1 above for more information. +1.32.0rc1. If affected, it is recommended to downgrade to 1.31.0 in the meantime, and +follow [these instructions](https://github.com/matrix-org/synapse/pull/9854#issuecomment-823472183) +to clean up any excess writeahead logs. + +**Note:** This release also mistakenly included a change that may affected Synapse +modules that import `synapse.logging.context.LoggingContext`, such as +[synapse-s3-storage-provider](https://github.com/matrix-org/synapse-s3-storage-provider). +This will be fixed in a later Synapse version. **Note:** This release requires Python 3.6+ and Postgres 9.6+ or SQLite 3.22+. diff --git a/UPGRADE.rst b/UPGRADE.rst index 76d2ee394f..6af35bc38f 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -93,11 +93,11 @@ Regression causing connected Prometheus instances to become overwhelmed This release introduces `a regression `_ that can overwhelm connected Prometheus instances. This issue is not present in -Synapse v1.32.0rc1, and is fixed in Synapse v1.32.1. +Synapse v1.32.0rc1. -If you have been affected, please first upgrade to a more recent Synapse version. -You then may need to remove excess writeahead logs in order for Prometheus to recover. -Instructions for doing so are provided +If you have been affected, please downgrade to 1.31.0. You then may need to +remove excess writeahead logs in order for Prometheus to recover. Instructions +for doing so are provided `here `_. Dropping support for old Python, Postgres and SQLite versions From 55159c48e31129c87b55d15d203df946ca33f884 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Apr 2021 18:45:39 +0100 Subject: [PATCH 080/619] 1.32.2 --- CHANGES.md | 11 +++++++++++ changelog.d/9857.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/9857.bugfix diff --git a/CHANGES.md b/CHANGES.md index a1349252cb..f194f4db30 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +Synapse 1.32.2 (2021-04-21) +=========================== + +This release includes fixes for the two regressions introduced in 1.32.0. + +Bugfixes +-------- + +- Fix a regression in Synapse v1.32.1 which caused `LoggingContext` errors in plugins. ([\#9857](https://github.com/matrix-org/synapse/issues/9857)) + + Synapse 1.32.1 (2021-04-21) =========================== diff --git a/changelog.d/9857.bugfix b/changelog.d/9857.bugfix deleted file mode 100644 index 7eed41594d..0000000000 --- a/changelog.d/9857.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a regression in Synapse v1.32.1 which caused `LoggingContext` errors in plugins. diff --git a/debian/changelog b/debian/changelog index b8cf2cac58..9ebfc3c3f1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.32.2) stable; urgency=medium + + * New synapse release 1.32.2. + + -- Synapse Packaging team Wed, 21 Apr 2021 18:43:52 +0100 + matrix-synapse-py3 (1.32.1) stable; urgency=medium * New synapse release 1.32.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index a0332d602d..781f5ac3a2 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -48,7 +48,7 @@ except ImportError: pass -__version__ = "1.32.1" +__version__ = "1.32.2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From ca380881b16847f61b323424aceb65548180d624 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 21 Apr 2021 18:47:31 +0100 Subject: [PATCH 081/619] Update dates in changelogs --- CHANGES.md | 2 +- debian/changelog | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f194f4db30..7475f7a402 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -Synapse 1.32.2 (2021-04-21) +Synapse 1.32.2 (2021-04-22) =========================== This release includes fixes for the two regressions introduced in 1.32.0. diff --git a/debian/changelog b/debian/changelog index 9ebfc3c3f1..fd33bfda5c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,7 +2,7 @@ matrix-synapse-py3 (1.32.2) stable; urgency=medium * New synapse release 1.32.2. - -- Synapse Packaging team Wed, 21 Apr 2021 18:43:52 +0100 + -- Synapse Packaging team Wed, 22 Apr 2021 12:43:52 +0100 matrix-synapse-py3 (1.32.1) stable; urgency=medium From 79e6d9e4b1d6231ebdb9b4e8d1bd5e382c37f26e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 22 Apr 2021 11:04:51 +0100 Subject: [PATCH 082/619] Note regression was in 1.32.0 and 1.32.1 --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7475f7a402..8381b3112d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,12 @@ Synapse 1.32.2 (2021-04-22) =========================== -This release includes fixes for the two regressions introduced in 1.32.0. +This release includes a fix for a regression introduced in 1.32.0 and 1.32.1. Bugfixes -------- -- Fix a regression in Synapse v1.32.1 which caused `LoggingContext` errors in plugins. ([\#9857](https://github.com/matrix-org/synapse/issues/9857)) +- Fix a regression in Synapse 1.32.1 which caused `LoggingContext` errors in plugins. ([\#9857](https://github.com/matrix-org/synapse/issues/9857)) Synapse 1.32.1 (2021-04-21) From dac44459348bd1d771a2dd6970f2a9e6532ee85f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 22 Apr 2021 11:09:31 +0100 Subject: [PATCH 083/619] A regression can't be introduced twice --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8381b3112d..532b30e232 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,12 @@ Synapse 1.32.2 (2021-04-22) =========================== -This release includes a fix for a regression introduced in 1.32.0 and 1.32.1. +This release includes a fix for a regression introduced in 1.32.0. Bugfixes -------- -- Fix a regression in Synapse 1.32.1 which caused `LoggingContext` errors in plugins. ([\#9857](https://github.com/matrix-org/synapse/issues/9857)) +- Fix a regression in Synapse 1.32.0 and 1.32.1 which caused `LoggingContext` errors in plugins. ([\#9857](https://github.com/matrix-org/synapse/issues/9857)) Synapse 1.32.1 (2021-04-21) From 294c67503300b6bfa7785a5cfa55e25c1e452574 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 22 Apr 2021 16:43:50 +0100 Subject: [PATCH 084/619] Remove `synapse.types.Collection` (#9856) This is no longer required, since we have dropped support for Python 3.5. --- changelog.d/9856.misc | 1 + synapse/config/oidc.py | 4 ++-- synapse/events/spamcheck.py | 3 +-- synapse/federation/sender/__init__.py | 14 ++++++++++++-- synapse/handlers/appservice.py | 4 ++-- synapse/handlers/device.py | 3 +-- synapse/handlers/presence.py | 3 ++- synapse/handlers/sso.py | 3 ++- synapse/handlers/sync.py | 13 +++++++++++-- synapse/notifier.py | 9 ++------- synapse/replication/tcp/protocol.py | 3 +-- synapse/state/__init__.py | 3 ++- synapse/state/v2.py | 3 ++- synapse/storage/_base.py | 4 ++-- synapse/storage/database.py | 2 +- synapse/storage/databases/main/devices.py | 4 ++-- synapse/storage/databases/main/event_federation.py | 3 +-- synapse/storage/databases/main/events_worker.py | 13 +++++++++++-- synapse/storage/databases/main/roommember.py | 14 ++++++++++++-- synapse/storage/databases/main/search.py | 3 +-- synapse/storage/databases/main/stream.py | 4 ++-- synapse/storage/persist_events.py | 3 +-- synapse/storage/prepare_database.py | 3 +-- synapse/types.py | 14 -------------- synapse/util/caches/stream_change_cache.py | 3 +-- synapse/util/iterutils.py | 3 +-- 26 files changed, 77 insertions(+), 62 deletions(-) create mode 100644 changelog.d/9856.misc diff --git a/changelog.d/9856.misc b/changelog.d/9856.misc new file mode 100644 index 0000000000..d67e8c386a --- /dev/null +++ b/changelog.d/9856.misc @@ -0,0 +1 @@ +Remove redundant `synapse.types.Collection` type definition. diff --git a/synapse/config/oidc.py b/synapse/config/oidc.py index 72402eb81d..ea0abf5aa2 100644 --- a/synapse/config/oidc.py +++ b/synapse/config/oidc.py @@ -14,14 +14,14 @@ # limitations under the License. from collections import Counter -from typing import Iterable, List, Mapping, Optional, Tuple, Type +from typing import Collection, Iterable, List, Mapping, Optional, Tuple, Type import attr from synapse.config._util import validate_config from synapse.config.sso import SsoAttributeRequirement from synapse.python_dependencies import DependencyException, check_requirements -from synapse.types import Collection, JsonDict +from synapse.types import JsonDict from synapse.util.module_loader import load_module from synapse.util.stringutils import parse_and_validate_mxc_uri diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index c727b48c1e..7118d5f52d 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -15,12 +15,11 @@ import inspect import logging -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Tuple, Union from synapse.rest.media.v1._base import FileInfo from synapse.rest.media.v1.media_storage import ReadableFileWrapper from synapse.spam_checker_api import RegistrationBehaviour -from synapse.types import Collection from synapse.util.async_helpers import maybe_awaitable if TYPE_CHECKING: diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index b00a55324c..022bbf7dad 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -14,7 +14,17 @@ import abc import logging -from typing import TYPE_CHECKING, Dict, Hashable, Iterable, List, Optional, Set, Tuple +from typing import ( + TYPE_CHECKING, + Collection, + Dict, + Hashable, + Iterable, + List, + Optional, + Set, + Tuple, +) from prometheus_client import Counter @@ -31,7 +41,7 @@ events_processed_counter, ) from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.types import Collection, JsonDict, ReadReceipt, RoomStreamToken +from synapse.types import JsonDict, ReadReceipt, RoomStreamToken from synapse.util.metrics import Measure if TYPE_CHECKING: diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py index d7bc4e23ed..177310f0be 100644 --- a/synapse/handlers/appservice.py +++ b/synapse/handlers/appservice.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import TYPE_CHECKING, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Union from prometheus_client import Counter @@ -33,7 +33,7 @@ wrap_as_background_process, ) from synapse.storage.databases.main.directory import RoomAliasMapping -from synapse.types import Collection, JsonDict, RoomAlias, RoomStreamToken, UserID +from synapse.types import JsonDict, RoomAlias, RoomStreamToken, UserID from synapse.util.metrics import Measure if TYPE_CHECKING: diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index c1d7800981..34d39e3b44 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Collection, Dict, Iterable, List, Optional, Set, Tuple from synapse.api import errors from synapse.api.constants import EventTypes @@ -28,7 +28,6 @@ from synapse.logging.opentracing import log_kv, set_tag, trace from synapse.metrics.background_process_metrics import run_as_background_process from synapse.types import ( - Collection, JsonDict, StreamToken, UserID, diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 598466c9bd..7fd28ffa54 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -28,6 +28,7 @@ from contextlib import contextmanager from typing import ( TYPE_CHECKING, + Collection, Dict, FrozenSet, Iterable, @@ -59,7 +60,7 @@ from synapse.replication.tcp.streams import PresenceFederationStream, PresenceStream from synapse.state import StateHandler from synapse.storage.databases.main import DataStore -from synapse.types import Collection, JsonDict, UserID, get_domain_from_id +from synapse.types import JsonDict, 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 diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py index 8d00ffdc73..044ff06d84 100644 --- a/synapse/handlers/sso.py +++ b/synapse/handlers/sso.py @@ -18,6 +18,7 @@ Any, Awaitable, Callable, + Collection, Dict, Iterable, List, @@ -40,7 +41,7 @@ from synapse.http import get_request_user_agent from synapse.http.server import respond_with_html, respond_with_redirect from synapse.http.site import SynapseRequest -from synapse.types import Collection, JsonDict, UserID, contains_invalid_mxid_characters +from synapse.types import JsonDict, UserID, contains_invalid_mxid_characters from synapse.util.async_helpers import Linearizer from synapse.util.stringutils import random_string diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index dc8ee8cd17..a9a3ee05c3 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -14,7 +14,17 @@ # limitations under the License. import itertools import logging -from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Optional, Set, Tuple +from typing import ( + TYPE_CHECKING, + Any, + Collection, + Dict, + FrozenSet, + List, + Optional, + Set, + Tuple, +) import attr from prometheus_client import Counter @@ -28,7 +38,6 @@ from synapse.storage.roommember import MemberSummary from synapse.storage.state import StateFilter from synapse.types import ( - Collection, JsonDict, MutableStateMap, Requester, diff --git a/synapse/notifier.py b/synapse/notifier.py index d5ab77058d..b9531007e2 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -17,6 +17,7 @@ from typing import ( Awaitable, Callable, + Collection, Dict, Iterable, List, @@ -42,13 +43,7 @@ from synapse.logging.utils import log_function from synapse.metrics import LaterGauge from synapse.streams.config import PaginationConfig -from synapse.types import ( - Collection, - PersistedEventPosition, - RoomStreamToken, - StreamToken, - UserID, -) +from synapse.types import PersistedEventPosition, RoomStreamToken, StreamToken, UserID from synapse.util.async_helpers import ObservableDeferred, timeout_deferred from synapse.util.metrics import Measure from synapse.visibility import filter_events_for_client diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index 6860576e78..6e3705364f 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -49,7 +49,7 @@ import logging import struct from inspect import isawaitable -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Collection, List, Optional from prometheus_client import Counter from zope.interface import Interface, implementer @@ -76,7 +76,6 @@ ServerCommand, parse_command_from_line, ) -from synapse.types import Collection from synapse.util import Clock from synapse.util.stringutils import random_string diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index c7ee731154..b3bd92d37c 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -19,6 +19,7 @@ Any, Awaitable, Callable, + Collection, DefaultDict, Dict, FrozenSet, @@ -46,7 +47,7 @@ from synapse.state import v1, v2 from synapse.storage.databases.main.events_worker import EventRedactBehaviour from synapse.storage.roommember import ProfileInfo -from synapse.types import Collection, StateMap +from synapse.types import StateMap from synapse.util.async_helpers import Linearizer from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.metrics import Measure, measure_func diff --git a/synapse/state/v2.py b/synapse/state/v2.py index 32671ddbde..008644cd98 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -18,6 +18,7 @@ from typing import ( Any, Callable, + Collection, Dict, Generator, Iterable, @@ -37,7 +38,7 @@ from synapse.api.errors import AuthError from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.events import EventBase -from synapse.types import Collection, MutableStateMap, StateMap +from synapse.types import MutableStateMap, StateMap from synapse.util import Clock logger = logging.getLogger(__name__) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 56dd3a4861..d472676acf 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -16,13 +16,13 @@ import logging import random from abc import ABCMeta -from typing import TYPE_CHECKING, Any, Iterable, Optional, Union +from typing import TYPE_CHECKING, Any, Collection, Iterable, Optional, Union from synapse.storage.database import LoggingTransaction # noqa: F401 from synapse.storage.database import make_in_list_sql_clause # noqa: F401 from synapse.storage.database import DatabasePool from synapse.storage.types import Connection -from synapse.types import Collection, StreamToken, get_domain_from_id +from synapse.types import StreamToken, get_domain_from_id from synapse.util import json_decoder if TYPE_CHECKING: diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 9a6d2b21f9..9452368bf0 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -20,6 +20,7 @@ from typing import ( Any, Callable, + Collection, Dict, Iterable, Iterator, @@ -48,7 +49,6 @@ from synapse.storage.background_updates import BackgroundUpdater from synapse.storage.engines import BaseDatabaseEngine, PostgresEngine, Sqlite3Engine from synapse.storage.types import Connection, Cursor -from synapse.types import Collection # python 3 does not have a maximum int value MAX_TXN_ID = 2 ** 63 - 1 diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index b204875580..9be713399f 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -15,7 +15,7 @@ # limitations under the License. import abc import logging -from typing import Any, Dict, Iterable, List, Optional, Set, Tuple +from typing import Any, Collection, Dict, Iterable, List, Optional, Set, Tuple from synapse.api.errors import Codes, StoreError from synapse.logging.opentracing import ( @@ -31,7 +31,7 @@ LoggingTransaction, make_tuple_comparison_clause, ) -from synapse.types import Collection, JsonDict, get_verify_key_from_cross_signing_key +from synapse.types import JsonDict, get_verify_key_from_cross_signing_key from synapse.util import json_decoder, json_encoder from synapse.util.caches.descriptors import cached, cachedList from synapse.util.caches.lrucache import LruCache diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index 32ce70a396..ff81d5cd17 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -14,7 +14,7 @@ import itertools import logging from queue import Empty, PriorityQueue -from typing import Dict, Iterable, List, Set, Tuple +from typing import Collection, Dict, Iterable, List, Set, Tuple from synapse.api.errors import StoreError from synapse.events import EventBase @@ -25,7 +25,6 @@ from synapse.storage.databases.main.signatures import SignatureWorkerStore from synapse.storage.engines import PostgresEngine from synapse.storage.types import Cursor -from synapse.types import Collection from synapse.util.caches.descriptors import cached from synapse.util.caches.lrucache import LruCache from synapse.util.iterutils import batch_iter diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index 64d70785b8..2c823e09cf 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -15,7 +15,16 @@ import logging import threading from collections import namedtuple -from typing import Container, Dict, Iterable, List, Optional, Tuple, overload +from typing import ( + Collection, + Container, + Dict, + Iterable, + List, + Optional, + Tuple, + overload, +) from constantly import NamedConstant, Names from typing_extensions import Literal @@ -45,7 +54,7 @@ from synapse.storage.engines import PostgresEngine from synapse.storage.util.id_generators import MultiWriterIdGenerator, StreamIdGenerator from synapse.storage.util.sequence import build_sequence_generator -from synapse.types import Collection, JsonDict, get_domain_from_id +from synapse.types import JsonDict, get_domain_from_id from synapse.util.caches.descriptors import cached from synapse.util.caches.lrucache import LruCache from synapse.util.iterutils import batch_iter diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index fd525dce65..bd8513cd43 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -13,7 +13,17 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import TYPE_CHECKING, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple +from typing import ( + TYPE_CHECKING, + Collection, + Dict, + FrozenSet, + Iterable, + List, + Optional, + Set, + Tuple, +) from synapse.api.constants import EventTypes, Membership from synapse.events import EventBase @@ -33,7 +43,7 @@ ProfileInfo, RoomsForUser, ) -from synapse.types import Collection, PersistedEventPosition, get_domain_from_id +from synapse.types import PersistedEventPosition, get_domain_from_id from synapse.util.async_helpers import Linearizer from synapse.util.caches import intern_string from synapse.util.caches.descriptors import _CacheContext, cached, cachedList diff --git a/synapse/storage/databases/main/search.py b/synapse/storage/databases/main/search.py index 0276f30656..6480d5a9f5 100644 --- a/synapse/storage/databases/main/search.py +++ b/synapse/storage/databases/main/search.py @@ -15,7 +15,7 @@ import logging import re from collections import namedtuple -from typing import List, Optional, Set +from typing import Collection, List, Optional, Set from synapse.api.errors import SynapseError from synapse.events import EventBase @@ -23,7 +23,6 @@ from synapse.storage.database import DatabasePool from synapse.storage.databases.main.events_worker import EventRedactBehaviour from synapse.storage.engines import PostgresEngine, Sqlite3Engine -from synapse.types import Collection logger = logging.getLogger(__name__) diff --git a/synapse/storage/databases/main/stream.py b/synapse/storage/databases/main/stream.py index db5ce4ea01..7581c7d3ff 100644 --- a/synapse/storage/databases/main/stream.py +++ b/synapse/storage/databases/main/stream.py @@ -37,7 +37,7 @@ import abc import logging from collections import namedtuple -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Set, Tuple from twisted.internet import defer @@ -53,7 +53,7 @@ from synapse.storage.databases.main.events_worker import EventsWorkerStore from synapse.storage.engines import BaseDatabaseEngine, PostgresEngine from synapse.storage.util.id_generators import MultiWriterIdGenerator -from synapse.types import Collection, PersistedEventPosition, RoomStreamToken +from synapse.types import PersistedEventPosition, RoomStreamToken from synapse.util.caches.descriptors import cached from synapse.util.caches.stream_change_cache import StreamChangeCache diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 87e040b014..33dc752d8f 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -17,7 +17,7 @@ import itertools import logging from collections import deque, namedtuple -from typing import Dict, Iterable, List, Optional, Set, Tuple +from typing import Collection, Dict, Iterable, List, Optional, Set, Tuple from prometheus_client import Counter, Histogram @@ -32,7 +32,6 @@ from synapse.storage.databases.main.events import DeltaState from synapse.storage.databases.main.events_worker import EventRedactBehaviour from synapse.types import ( - Collection, PersistedEventPosition, RoomStreamToken, StateMap, diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 05a9355974..7a2cbee426 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -17,7 +17,7 @@ import os import re from collections import Counter -from typing import Generator, Iterable, List, Optional, TextIO, Tuple +from typing import Collection, Generator, Iterable, List, Optional, TextIO, Tuple import attr from typing_extensions import Counter as CounterType @@ -27,7 +27,6 @@ from synapse.storage.engines import BaseDatabaseEngine from synapse.storage.engines.postgres import PostgresEngine from synapse.storage.types import Cursor -from synapse.types import Collection logger = logging.getLogger(__name__) diff --git a/synapse/types.py b/synapse/types.py index 21654ae686..e19f28d543 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -15,13 +15,11 @@ import abc import re import string -import sys from collections import namedtuple from typing import ( TYPE_CHECKING, Any, Dict, - Iterable, Mapping, MutableMapping, Optional, @@ -50,18 +48,6 @@ from synapse.appservice.api import ApplicationService from synapse.storage.databases.main import DataStore -# define a version of typing.Collection that works on python 3.5 -if sys.version_info[:3] >= (3, 6, 0): - from typing import Collection -else: - from typing import Container, Sized - - T_co = TypeVar("T_co", covariant=True) - - class Collection(Iterable[T_co], Container[T_co], Sized): # type: ignore - __slots__ = () - - # Define a state map type from type/state_key to T (usually an event ID or # event) T = TypeVar("T") diff --git a/synapse/util/caches/stream_change_cache.py b/synapse/util/caches/stream_change_cache.py index 0469e7d120..e81e468899 100644 --- a/synapse/util/caches/stream_change_cache.py +++ b/synapse/util/caches/stream_change_cache.py @@ -14,11 +14,10 @@ import logging import math -from typing import Dict, FrozenSet, List, Mapping, Optional, Set, Union +from typing import Collection, Dict, FrozenSet, List, Mapping, Optional, Set, Union from sortedcontainers import SortedDict -from synapse.types import Collection from synapse.util import caches logger = logging.getLogger(__name__) diff --git a/synapse/util/iterutils.py b/synapse/util/iterutils.py index 6f73b1d56d..abfdc29832 100644 --- a/synapse/util/iterutils.py +++ b/synapse/util/iterutils.py @@ -15,6 +15,7 @@ import heapq from itertools import islice from typing import ( + Collection, Dict, Generator, Iterable, @@ -26,8 +27,6 @@ TypeVar, ) -from synapse.types import Collection - T = TypeVar("T") From 69018acbd2d1f331d6a52335b4938c3753b16de6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 22 Apr 2021 16:53:24 +0100 Subject: [PATCH 085/619] Clear the resync bit after resyncing device lists (#9867) Fixes #9866. --- changelog.d/9867.bugfix | 1 + synapse/handlers/device.py | 7 +++++++ synapse/storage/databases/main/devices.py | 19 +++++++++---------- 3 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 changelog.d/9867.bugfix diff --git a/changelog.d/9867.bugfix b/changelog.d/9867.bugfix new file mode 100644 index 0000000000..f236de247d --- /dev/null +++ b/changelog.d/9867.bugfix @@ -0,0 +1 @@ +Fix a bug which could cause Synapse to get stuck in a loop of resyncing device lists. diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 34d39e3b44..95bdc5902a 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -925,6 +925,10 @@ async def user_device_resync( else: cached_devices = await self.store.get_cached_devices_for_user(user_id) if cached_devices == {d["device_id"]: d for d in devices}: + logging.info( + "Skipping device list resync for %s, as our cache matches already", + user_id, + ) devices = [] ignore_devices = True @@ -940,6 +944,9 @@ async def user_device_resync( await self.store.update_remote_device_list_cache( user_id, devices, stream_id ) + # mark the cache as valid, whether or not we actually processed any device + # list updates. + await self.store.mark_remote_user_device_cache_as_valid(user_id) device_ids = [device["device_id"] for device in devices] # Handle cross-signing keys. diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index 9be713399f..c9346de316 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -717,7 +717,15 @@ async def mark_remote_user_device_cache_as_stale(self, user_id: str) -> None: keyvalues={"user_id": user_id}, values={}, insertion_values={"added_ts": self._clock.time_msec()}, - desc="make_remote_user_device_cache_as_stale", + desc="mark_remote_user_device_cache_as_stale", + ) + + async def mark_remote_user_device_cache_as_valid(self, user_id: str) -> None: + # Remove the database entry that says we need to resync devices, after a resync + await self.db_pool.simple_delete( + table="device_lists_remote_resync", + keyvalues={"user_id": user_id}, + desc="mark_remote_user_device_cache_as_valid", ) async def mark_remote_user_device_list_as_unsubscribed(self, user_id: str) -> None: @@ -1289,15 +1297,6 @@ def _update_remote_device_list_cache_txn( lock=False, ) - # If we're replacing the remote user's device list cache presumably - # we've done a full resync, so we remove the entry that says we need - # to resync - self.db_pool.simple_delete_txn( - txn, - table="device_lists_remote_resync", - keyvalues={"user_id": user_id}, - ) - async def add_device_change_to_streams( self, user_id: str, device_ids: Collection[str], hosts: List[str] ): From 177dae270420ee4b4c8fa5e2c74c5081d98da320 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 22 Apr 2021 17:49:11 +0100 Subject: [PATCH 086/619] Limit length of accepted email addresses (#9855) --- changelog.d/9855.misc | 1 + synapse/push/emailpusher.py | 9 +++- synapse/rest/client/v2_alpha/account.py | 8 ++-- synapse/rest/client/v2_alpha/register.py | 8 +++- synapse/util/threepids.py | 30 ++++++++++++ tests/rest/client/v2_alpha/test_register.py | 51 +++++++++++++++++++++ 6 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 changelog.d/9855.misc diff --git a/changelog.d/9855.misc b/changelog.d/9855.misc new file mode 100644 index 0000000000..6a3d700fde --- /dev/null +++ b/changelog.d/9855.misc @@ -0,0 +1 @@ +Limit length of accepted email addresses. diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index cd89b54305..99a18874d1 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -19,8 +19,9 @@ from twisted.internet.interfaces import IDelayedCall from synapse.metrics.background_process_metrics import run_as_background_process -from synapse.push import Pusher, PusherConfig, ThrottleParams +from synapse.push import Pusher, PusherConfig, PusherConfigException, ThrottleParams from synapse.push.mailer import Mailer +from synapse.util.threepids import validate_email if TYPE_CHECKING: from synapse.server import HomeServer @@ -71,6 +72,12 @@ def __init__(self, hs: "HomeServer", pusher_config: PusherConfig, mailer: Mailer self._is_processing = False + # Make sure that the email is valid. + try: + validate_email(self.email) + except ValueError: + raise PusherConfigException("Invalid email") + def on_started(self, should_check_for_notifs: bool) -> None: """Called when this pusher has been started. diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 3aad15132d..085561d3e9 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -39,7 +39,7 @@ from synapse.push.mailer import Mailer from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string -from synapse.util.threepids import canonicalise_email, check_3pid_allowed +from synapse.util.threepids import check_3pid_allowed, validate_email from ._base import client_patterns, interactive_auth_handler @@ -92,7 +92,7 @@ async def on_POST(self, request): # Stored in the database "foo@bar.com" # User requests with "FOO@bar.com" would raise a Not Found error try: - email = canonicalise_email(body["email"]) + email = validate_email(body["email"]) except ValueError as e: raise SynapseError(400, str(e)) send_attempt = body["send_attempt"] @@ -247,7 +247,7 @@ async def on_POST(self, request): # We store all email addresses canonicalised in the DB. # (See add_threepid in synapse/handlers/auth.py) try: - threepid["address"] = canonicalise_email(threepid["address"]) + threepid["address"] = validate_email(threepid["address"]) except ValueError as e: raise SynapseError(400, str(e)) # if using email, we must know about the email they're authing with! @@ -375,7 +375,7 @@ async def on_POST(self, request): # Otherwise the email will be sent to "FOO@bar.com" and stored as # "foo@bar.com" in database. try: - email = canonicalise_email(body["email"]) + email = validate_email(body["email"]) except ValueError as e: raise SynapseError(400, str(e)) send_attempt = body["send_attempt"] diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index c5a6800b8a..a30a5df1b1 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -49,7 +49,11 @@ from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.ratelimitutils import FederationRateLimiter from synapse.util.stringutils import assert_valid_client_secret, random_string -from synapse.util.threepids import canonicalise_email, check_3pid_allowed +from synapse.util.threepids import ( + canonicalise_email, + check_3pid_allowed, + validate_email, +) from ._base import client_patterns, interactive_auth_handler @@ -111,7 +115,7 @@ async def on_POST(self, request): # (See on_POST in EmailThreepidRequestTokenRestServlet # in synapse/rest/client/v2_alpha/account.py) try: - email = canonicalise_email(body["email"]) + email = validate_email(body["email"]) except ValueError as e: raise SynapseError(400, str(e)) send_attempt = body["send_attempt"] diff --git a/synapse/util/threepids.py b/synapse/util/threepids.py index 281c5be4fb..a1cf1960b0 100644 --- a/synapse/util/threepids.py +++ b/synapse/util/threepids.py @@ -18,6 +18,16 @@ logger = logging.getLogger(__name__) +# it's unclear what the maximum length of an email address is. RFC3696 (as corrected +# by errata) says: +# the upper limit on address lengths should normally be considered to be 254. +# +# In practice, mail servers appear to be more tolerant and allow 400 characters +# or so. Let's allow 500, which should be plenty for everyone. +# +MAX_EMAIL_ADDRESS_LENGTH = 500 + + def check_3pid_allowed(hs, medium, address): """Checks whether a given format of 3PID is allowed to be used on this HS @@ -70,3 +80,23 @@ def canonicalise_email(address: str) -> str: raise ValueError("Unable to parse email address") return parts[0].casefold() + "@" + parts[1].lower() + + +def validate_email(address: str) -> str: + """Does some basic validation on an email address. + + Returns the canonicalised email, as returned by `canonicalise_email`. + + Raises a ValueError if the email is invalid. + """ + # First we try canonicalising in case that fails + address = canonicalise_email(address) + + # Email addresses have to be at least 3 characters. + if len(address) < 3: + raise ValueError("Unable to parse email address") + + if len(address) > MAX_EMAIL_ADDRESS_LENGTH: + raise ValueError("Unable to parse email address") + + return address diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index 98695b05d5..1cad5f00eb 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -310,6 +310,57 @@ def test_request_token_existing_email_inhibit_error(self): self.assertIsNotNone(channel.json_body.get("sid")) + @unittest.override_config( + { + "public_baseurl": "https://test_server", + "email": { + "smtp_host": "mail_server", + "smtp_port": 2525, + "notif_from": "sender@host", + }, + } + ) + def test_reject_invalid_email(self): + """Check that bad emails are rejected""" + + # Test for email with multiple @ + channel = self.make_request( + "POST", + b"register/email/requestToken", + {"client_secret": "foobar", "email": "email@@email", "send_attempt": 1}, + ) + self.assertEquals(400, channel.code, channel.result) + # Check error to ensure that we're not erroring due to a bug in the test. + self.assertEquals( + channel.json_body, + {"errcode": "M_UNKNOWN", "error": "Unable to parse email address"}, + ) + + # Test for email with no @ + channel = self.make_request( + "POST", + b"register/email/requestToken", + {"client_secret": "foobar", "email": "email", "send_attempt": 1}, + ) + self.assertEquals(400, channel.code, channel.result) + self.assertEquals( + channel.json_body, + {"errcode": "M_UNKNOWN", "error": "Unable to parse email address"}, + ) + + # Test for super long email + email = "a@" + "a" * 1000 + channel = self.make_request( + "POST", + b"register/email/requestToken", + {"client_secret": "foobar", "email": email, "send_attempt": 1}, + ) + self.assertEquals(400, channel.code, channel.result) + self.assertEquals( + channel.json_body, + {"errcode": "M_UNKNOWN", "error": "Unable to parse email address"}, + ) + class AccountValidityTestCase(unittest.HomeserverTestCase): From c1ddbbde4fb948cf740d4c59869157943d3711c6 Mon Sep 17 00:00:00 2001 From: manuroe Date: Thu, 22 Apr 2021 18:49:42 +0200 Subject: [PATCH 087/619] Handle all new rate limits in demo scripts (#9858) --- changelog.d/9858.misc | 1 + demo/start.sh | 54 +++++++++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 changelog.d/9858.misc diff --git a/changelog.d/9858.misc b/changelog.d/9858.misc new file mode 100644 index 0000000000..f7e286fa69 --- /dev/null +++ b/changelog.d/9858.misc @@ -0,0 +1 @@ +Handle recently added rate limits correctly when using `--no-rate-limit` with the demo scripts. diff --git a/demo/start.sh b/demo/start.sh index 621a5698b8..bc4854091b 100755 --- a/demo/start.sh +++ b/demo/start.sh @@ -96,18 +96,48 @@ for port in 8080 8081 8082; do # Check script parameters if [ $# -eq 1 ]; then if [ $1 = "--no-rate-limit" ]; then - # messages rate limit - echo 'rc_messages_per_second: 1000' >> $DIR/etc/$port.config - echo 'rc_message_burst_count: 1000' >> $DIR/etc/$port.config - - # registration rate limit - printf 'rc_registration:\n per_second: 1000\n burst_count: 1000\n' >> $DIR/etc/$port.config - - # login rate limit - echo 'rc_login:' >> $DIR/etc/$port.config - printf ' address:\n per_second: 1000\n burst_count: 1000\n' >> $DIR/etc/$port.config - printf ' account:\n per_second: 1000\n burst_count: 1000\n' >> $DIR/etc/$port.config - printf ' failed_attempts:\n per_second: 1000\n burst_count: 1000\n' >> $DIR/etc/$port.config + + # Disable any rate limiting + ratelimiting=$(cat <<-RC + rc_message: + per_second: 1000 + burst_count: 1000 + rc_registration: + per_second: 1000 + burst_count: 1000 + rc_login: + address: + per_second: 1000 + burst_count: 1000 + account: + per_second: 1000 + burst_count: 1000 + failed_attempts: + per_second: 1000 + burst_count: 1000 + rc_admin_redaction: + per_second: 1000 + burst_count: 1000 + rc_joins: + local: + per_second: 1000 + burst_count: 1000 + remote: + per_second: 1000 + burst_count: 1000 + 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 + RC + ) + echo "${ratelimiting}" >> $DIR/etc/$port.config fi fi From 51a20914a863ac24387c424ccee14aa877e218f8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 23 Apr 2021 11:08:41 +0100 Subject: [PATCH 088/619] Limit the size of HTTP responses read over federation. (#9833) --- changelog.d/9833.bugfix | 1 + synapse/http/client.py | 15 ++++++- synapse/http/matrixfederationclient.py | 43 ++++++++++++++++--- tests/http/test_fedclient.py | 59 ++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 changelog.d/9833.bugfix diff --git a/changelog.d/9833.bugfix b/changelog.d/9833.bugfix new file mode 100644 index 0000000000..56f9c9626b --- /dev/null +++ b/changelog.d/9833.bugfix @@ -0,0 +1 @@ +Limit the size of HTTP responses read over federation. diff --git a/synapse/http/client.py b/synapse/http/client.py index 1730187ffa..5f40f16e24 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -33,6 +33,7 @@ from canonicaljson import encode_canonical_json from netaddr import AddrFormatError, IPAddress, IPSet from prometheus_client import Counter +from typing_extensions import Protocol from zope.interface import implementer, provider from OpenSSL import SSL @@ -754,6 +755,16 @@ def _timeout_to_request_timed_out_error(f: Failure): return f +class ByteWriteable(Protocol): + """The type of object which must be passed into read_body_with_max_size. + + Typically this is a file object. + """ + + def write(self, data: bytes) -> int: + pass + + class BodyExceededMaxSize(Exception): """The maximum allowed size of the HTTP body was exceeded.""" @@ -790,7 +801,7 @@ class _ReadBodyWithMaxSizeProtocol(protocol.Protocol): transport = None # type: Optional[ITCPTransport] def __init__( - self, stream: BinaryIO, deferred: defer.Deferred, max_size: Optional[int] + self, stream: ByteWriteable, deferred: defer.Deferred, max_size: Optional[int] ): self.stream = stream self.deferred = deferred @@ -830,7 +841,7 @@ def connectionLost(self, reason: Failure = connectionDone) -> None: def read_body_with_max_size( - response: IResponse, stream: BinaryIO, max_size: Optional[int] + response: IResponse, stream: ByteWriteable, max_size: Optional[int] ) -> defer.Deferred: """ Read a HTTP response body to a file-object. Optionally enforcing a maximum file size. diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index d48721a4e2..bb837b7b19 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -1,5 +1,4 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,11 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. import cgi +import codecs import logging import random import sys +import typing import urllib.parse -from io import BytesIO +from io import BytesIO, StringIO from typing import Callable, Dict, List, Optional, Tuple, Union import attr @@ -72,6 +73,9 @@ "synapse_http_matrixfederationclient_responses", "", ["method", "code"] ) +# a federation response can be rather large (eg a big state_ids is 50M or so), so we +# need a generous limit here. +MAX_RESPONSE_SIZE = 100 * 1024 * 1024 MAX_LONG_RETRIES = 10 MAX_SHORT_RETRIES = 3 @@ -167,12 +171,27 @@ async def _handle_json_response( try: check_content_type_is_json(response.headers) - # Use the custom JSON decoder (partially re-implements treq.json_content). - d = treq.text_content(response, encoding="utf-8") - d.addCallback(json_decoder.decode) + buf = StringIO() + d = read_body_with_max_size(response, BinaryIOWrapper(buf), MAX_RESPONSE_SIZE) d = timeout_deferred(d, timeout=timeout_sec, reactor=reactor) + def parse(_len: int): + return json_decoder.decode(buf.getvalue()) + + d.addCallback(parse) + body = await make_deferred_yieldable(d) + except BodyExceededMaxSize as e: + # The response was too big. + logger.warning( + "{%s} [%s] JSON response exceeded max size %i - %s %s", + request.txn_id, + request.destination, + MAX_RESPONSE_SIZE, + request.method, + request.uri.decode("ascii"), + ) + raise RequestSendFailed(e, can_retry=False) from e except ValueError as e: # The JSON content was invalid. logger.warning( @@ -218,6 +237,18 @@ async def _handle_json_response( return body +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"): + self.decoder = codecs.getincrementaldecoder(encoding)(errors) + self.file = file + + def write(self, b: Union[bytes, bytearray]) -> int: + self.file.write(self.decoder.decode(b)) + return len(b) + + class MatrixFederationHttpClient: """HTTP client used to talk to other homeservers over the federation protocol. Send client certificates and signs requests. diff --git a/tests/http/test_fedclient.py b/tests/http/test_fedclient.py index 9e97185507..ed9a884d76 100644 --- a/tests/http/test_fedclient.py +++ b/tests/http/test_fedclient.py @@ -26,6 +26,7 @@ from synapse.api.errors import RequestSendFailed from synapse.http.matrixfederationclient import ( + MAX_RESPONSE_SIZE, MatrixFederationHttpClient, MatrixFederationRequest, ) @@ -560,3 +561,61 @@ def test_json_error(self, return_value): f = self.failureResultOf(test_d) self.assertIsInstance(f.value, RequestSendFailed) + + def test_too_big(self): + """ + Test what happens if a huge response is returned from the remote endpoint. + """ + + test_d = defer.ensureDeferred(self.cl.get_json("testserv:8008", "foo/bar")) + + self.pump() + + # Nothing happened yet + self.assertNoResult(test_d) + + # Make sure treq is trying to connect + clients = self.reactor.tcpClients + self.assertEqual(len(clients), 1) + (host, port, factory, _timeout, _bindAddress) = clients[0] + self.assertEqual(host, "1.2.3.4") + self.assertEqual(port, 8008) + + # complete the connection and wire it up to a fake transport + protocol = factory.buildProtocol(None) + transport = StringTransport() + protocol.makeConnection(transport) + + # that should have made it send the request to the transport + self.assertRegex(transport.value(), b"^GET /foo/bar") + self.assertRegex(transport.value(), b"Host: testserv:8008") + + # Deferred is still without a result + self.assertNoResult(test_d) + + # Send it a huge HTTP response + protocol.dataReceived( + b"HTTP/1.1 200 OK\r\n" + b"Server: Fake\r\n" + b"Content-Type: application/json\r\n" + b"\r\n" + ) + + self.pump() + + # should still be waiting + self.assertNoResult(test_d) + + sent = 0 + chunk_size = 1024 * 512 + while not test_d.called: + protocol.dataReceived(b"a" * chunk_size) + sent += chunk_size + self.assertLessEqual(sent, MAX_RESPONSE_SIZE) + + self.assertEqual(sent, MAX_RESPONSE_SIZE) + + f = self.failureResultOf(test_d) + self.assertIsInstance(f.value, RequestSendFailed) + + self.assertTrue(transport.disconnecting) From 3853a7edfcee1c00ba4df04b06821397e1155257 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 23 Apr 2021 11:47:07 +0100 Subject: [PATCH 089/619] Only store data in caches, not "smart" objects (#9845) --- changelog.d/9845.misc | 1 + synapse/push/bulk_push_rule_evaluator.py | 161 +++++++++++-------- synapse/storage/databases/main/roommember.py | 161 ++++++++++--------- 3 files changed, 182 insertions(+), 141 deletions(-) create mode 100644 changelog.d/9845.misc diff --git a/changelog.d/9845.misc b/changelog.d/9845.misc new file mode 100644 index 0000000000..875dd6d131 --- /dev/null +++ b/changelog.d/9845.misc @@ -0,0 +1 @@ +Only store the raw data in the in-memory caches, rather than objects that include references to e.g. the data stores. diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 50b470c310..350646f458 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -106,6 +106,10 @@ def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() self.auth = hs.get_auth() + # Used by `RulesForRoom` to ensure only one thing mutates the cache at a + # time. Keyed off room_id. + self._rules_linearizer = Linearizer(name="rules_for_room") + self.room_push_rule_cache_metrics = register_cache( "cache", "room_push_rule_cache", @@ -123,7 +127,16 @@ async def _get_rules_for_event( dict of user_id -> push_rules """ room_id = event.room_id - rules_for_room = self._get_rules_for_room(room_id) + + rules_for_room_data = self._get_rules_for_room(room_id) + rules_for_room = RulesForRoom( + hs=self.hs, + room_id=room_id, + rules_for_room_cache=self._get_rules_for_room.cache, + room_push_rule_cache_metrics=self.room_push_rule_cache_metrics, + linearizer=self._rules_linearizer, + cached_data=rules_for_room_data, + ) rules_by_user = await rules_for_room.get_rules(event, context) @@ -142,17 +155,12 @@ async def _get_rules_for_event( return rules_by_user @lru_cache() - def _get_rules_for_room(self, room_id: str) -> "RulesForRoom": - """Get the current RulesForRoom object for the given room id""" - # It's important that RulesForRoom gets added to self._get_rules_for_room.cache + def _get_rules_for_room(self, room_id: str) -> "RulesForRoomData": + """Get the current RulesForRoomData object for the given room id""" + # It's important that the RulesForRoomData object gets added to self._get_rules_for_room.cache # before any lookup methods get called on it as otherwise there may be # a race if invalidate_all gets called (which assumes its in the cache) - return RulesForRoom( - self.hs, - room_id, - self._get_rules_for_room.cache, - self.room_push_rule_cache_metrics, - ) + return RulesForRoomData() async def _get_power_levels_and_sender_level( self, event: EventBase, context: EventContext @@ -282,11 +290,49 @@ def _condition_checker( return True +@attr.s(slots=True) +class RulesForRoomData: + """The data stored in the cache by `RulesForRoom`. + + We don't store `RulesForRoom` directly in the cache as we want our caches to + *only* include data, and not references to e.g. the data stores. + """ + + # event_id -> (user_id, state) + member_map = attr.ib(type=Dict[str, Tuple[str, str]], factory=dict) + # user_id -> rules + rules_by_user = attr.ib(type=Dict[str, List[Dict[str, dict]]], factory=dict) + + # The last state group we updated the caches for. If the state_group of + # a new event comes along, we know that we can just return the cached + # result. + # On invalidation of the rules themselves (if the user changes them), + # we invalidate everything and set state_group to `object()` + state_group = attr.ib(type=Union[object, int], factory=object) + + # A sequence number to keep track of when we're allowed to update the + # cache. We bump the sequence number when we invalidate the cache. If + # the sequence number changes while we're calculating stuff we should + # not update the cache with it. + sequence = attr.ib(type=int, default=0) + + # A cache of user_ids that we *know* aren't interesting, e.g. user_ids + # owned by AS's, or remote users, etc. (I.e. users we will never need to + # calculate push for) + # These never need to be invalidated as we will never set up push for + # them. + uninteresting_user_set = attr.ib(type=Set[str], factory=set) + + class RulesForRoom: """Caches push rules for users in a room. This efficiently handles users joining/leaving the room by not invalidating the entire cache for the room. + + A new instance is constructed for each call to + `BulkPushRuleEvaluator._get_rules_for_event`, with the cached data from + previous calls passed in. """ def __init__( @@ -295,6 +341,8 @@ def __init__( room_id: str, rules_for_room_cache: LruCache, room_push_rule_cache_metrics: CacheMetric, + linearizer: Linearizer, + cached_data: RulesForRoomData, ): """ Args: @@ -303,38 +351,21 @@ def __init__( rules_for_room_cache: The cache object that caches these RoomsForUser objects. room_push_rule_cache_metrics: The metrics object + linearizer: The linearizer used to ensure only one thing mutates + the cache at a time. Keyed off room_id + cached_data: Cached data from previous calls to `self.get_rules`, + can be mutated. """ self.room_id = room_id self.is_mine_id = hs.is_mine_id self.store = hs.get_datastore() self.room_push_rule_cache_metrics = room_push_rule_cache_metrics - self.linearizer = Linearizer(name="rules_for_room") - - # event_id -> (user_id, state) - self.member_map = {} # type: Dict[str, Tuple[str, str]] - # user_id -> rules - self.rules_by_user = {} # type: Dict[str, List[Dict[str, dict]]] - - # The last state group we updated the caches for. If the state_group of - # a new event comes along, we know that we can just return the cached - # result. - # On invalidation of the rules themselves (if the user changes them), - # we invalidate everything and set state_group to `object()` - self.state_group = object() - - # A sequence number to keep track of when we're allowed to update the - # cache. We bump the sequence number when we invalidate the cache. If - # the sequence number changes while we're calculating stuff we should - # not update the cache with it. - self.sequence = 0 - - # A cache of user_ids that we *know* aren't interesting, e.g. user_ids - # owned by AS's, or remote users, etc. (I.e. users we will never need to - # calculate push for) - # These never need to be invalidated as we will never set up push for - # them. - self.uninteresting_user_set = set() # type: Set[str] + # Used to ensure only one thing mutates the cache at a time. Keyed off + # room_id. + self.linearizer = linearizer + + self.data = cached_data # We need to be clever on the invalidating caches callbacks, as # otherwise the invalidation callback holds a reference to the object, @@ -352,25 +383,25 @@ async def get_rules( """ state_group = context.state_group - if state_group and self.state_group == state_group: + if state_group and self.data.state_group == state_group: logger.debug("Using cached rules for %r", self.room_id) self.room_push_rule_cache_metrics.inc_hits() - return self.rules_by_user + return self.data.rules_by_user - with (await self.linearizer.queue(())): - if state_group and self.state_group == state_group: + with (await self.linearizer.queue(self.room_id)): + if state_group and self.data.state_group == state_group: logger.debug("Using cached rules for %r", self.room_id) self.room_push_rule_cache_metrics.inc_hits() - return self.rules_by_user + return self.data.rules_by_user self.room_push_rule_cache_metrics.inc_misses() ret_rules_by_user = {} missing_member_event_ids = {} - if state_group and self.state_group == context.prev_group: + if state_group and self.data.state_group == context.prev_group: # If we have a simple delta then we can reuse most of the previous # results. - ret_rules_by_user = self.rules_by_user + ret_rules_by_user = self.data.rules_by_user current_state_ids = context.delta_ids push_rules_delta_state_cache_metric.inc_hits() @@ -393,24 +424,24 @@ async def get_rules( if typ != EventTypes.Member: continue - if user_id in self.uninteresting_user_set: + if user_id in self.data.uninteresting_user_set: continue if not self.is_mine_id(user_id): - self.uninteresting_user_set.add(user_id) + self.data.uninteresting_user_set.add(user_id) continue if self.store.get_if_app_services_interested_in_user(user_id): - self.uninteresting_user_set.add(user_id) + self.data.uninteresting_user_set.add(user_id) continue event_id = current_state_ids[key] - res = self.member_map.get(event_id, None) + res = self.data.member_map.get(event_id, None) if res: user_id, state = res if state == Membership.JOIN: - rules = self.rules_by_user.get(user_id, None) + rules = self.data.rules_by_user.get(user_id, None) if rules: ret_rules_by_user[user_id] = rules continue @@ -430,7 +461,7 @@ async def get_rules( else: # The push rules didn't change but lets update the cache anyway self.update_cache( - self.sequence, + self.data.sequence, members={}, # There were no membership changes rules_by_user=ret_rules_by_user, state_group=state_group, @@ -461,7 +492,7 @@ async def _update_rules_with_member_event_ids( for. Used when updating the cache. event: The event we are currently computing push rules for. """ - sequence = self.sequence + sequence = self.data.sequence rows = await self.store.get_membership_from_event_ids(member_event_ids.values()) @@ -501,23 +532,11 @@ async def _update_rules_with_member_event_ids( self.update_cache(sequence, members, ret_rules_by_user, state_group) - def invalidate_all(self) -> None: - # Note: Don't hand this function directly to an invalidation callback - # as it keeps a reference to self and will stop this instance from being - # GC'd if it gets dropped from the rules_to_user cache. Instead use - # `self.invalidate_all_cb` - logger.debug("Invalidating RulesForRoom for %r", self.room_id) - self.sequence += 1 - self.state_group = object() - self.member_map = {} - self.rules_by_user = {} - push_rules_invalidation_counter.inc() - def update_cache(self, sequence, members, rules_by_user, state_group) -> None: - if sequence == self.sequence: - self.member_map.update(members) - self.rules_by_user = rules_by_user - self.state_group = state_group + if sequence == self.data.sequence: + self.data.member_map.update(members) + self.data.rules_by_user = rules_by_user + self.data.state_group = state_group @attr.attrs(slots=True, frozen=True) @@ -535,6 +554,10 @@ class _Invalidation: room_id = attr.ib(type=str) def __call__(self) -> None: - rules = self.cache.get(self.room_id, None, update_metrics=False) - if rules: - rules.invalidate_all() + rules_data = self.cache.get(self.room_id, None, update_metrics=False) + if rules_data: + rules_data.sequence += 1 + rules_data.state_group = object() + rules_data.member_map = {} + rules_data.rules_by_user = {} + push_rules_invalidation_counter.inc() diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index bd8513cd43..2a8532f8c1 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -23,8 +23,11 @@ Optional, Set, Tuple, + Union, ) +import attr + from synapse.api.constants import EventTypes, Membership from synapse.events import EventBase from synapse.events.snapshot import EventContext @@ -43,7 +46,7 @@ ProfileInfo, RoomsForUser, ) -from synapse.types import PersistedEventPosition, get_domain_from_id +from synapse.types import PersistedEventPosition, StateMap, get_domain_from_id from synapse.util.async_helpers import Linearizer from synapse.util.caches import intern_string from synapse.util.caches.descriptors import _CacheContext, cached, cachedList @@ -63,6 +66,10 @@ class RoomMemberWorkerStore(EventsWorkerStore): def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) + # Used by `_get_joined_hosts` to ensure only one thing mutates the cache + # at a time. Keyed by room_id. + self._joined_host_linearizer = Linearizer("_JoinedHostsCache") + # Is the current_state_events.membership up to date? Or is the # background update still running? self._current_state_events_membership_up_to_date = False @@ -740,19 +747,82 @@ async def get_joined_hosts(self, room_id: str, state_entry): @cached(num_args=2, max_entries=10000, iterable=True) async def _get_joined_hosts( - self, room_id, state_group, current_state_ids, state_entry - ): - # We don't use `state_group`, its there so that we can cache based - # on it. However, its important that its never None, since two current_state's - # with a state_group of None are likely to be different. + self, + room_id: str, + state_group: int, + current_state_ids: StateMap[str], + state_entry: "_StateCacheEntry", + ) -> FrozenSet[str]: + # We don't use `state_group`, its there so that we can cache based on + # it. However, its important that its never None, since two + # current_state's with a state_group of None are likely to be different. + # + # The `state_group` must match the `state_entry.state_group` (if not None). assert state_group is not None - + assert state_entry.state_group is None or state_entry.state_group == state_group + + # We use a secondary cache of previous work to allow us to build up the + # joined hosts for the given state group based on previous state groups. + # + # We cache one object per room containing the results of the last state + # group we got joined hosts for. The idea is that generally + # `get_joined_hosts` is called with the "current" state group for the + # room, and so consecutive calls will be for consecutive state groups + # which point to the previous state group. cache = await self._get_joined_hosts_cache(room_id) - return await cache.get_destinations(state_entry) + + # If the state group in the cache matches, we already have the data we need. + if state_entry.state_group == cache.state_group: + return frozenset(cache.hosts_to_joined_users) + + # Since we'll mutate the cache we need to lock. + with (await self._joined_host_linearizer.queue(room_id)): + if state_entry.state_group == cache.state_group: + # Same state group, so nothing to do. We've already checked for + # this above, but the cache may have changed while waiting on + # the lock. + pass + elif state_entry.prev_group == cache.state_group: + # The cached work is for the previous state group, so we work out + # the delta. + for (typ, state_key), event_id in state_entry.delta_ids.items(): + if typ != EventTypes.Member: + continue + + host = intern_string(get_domain_from_id(state_key)) + user_id = state_key + known_joins = cache.hosts_to_joined_users.setdefault(host, set()) + + event = await self.get_event(event_id) + if event.membership == Membership.JOIN: + known_joins.add(user_id) + else: + known_joins.discard(user_id) + + if not known_joins: + cache.hosts_to_joined_users.pop(host, None) + else: + # The cache doesn't match the state group or prev state group, + # so we calculate the result from first principles. + joined_users = await self.get_joined_users_from_state( + room_id, state_entry + ) + + cache.hosts_to_joined_users = {} + for user_id in joined_users: + host = intern_string(get_domain_from_id(user_id)) + cache.hosts_to_joined_users.setdefault(host, set()).add(user_id) + + if state_entry.state_group: + cache.state_group = state_entry.state_group + else: + cache.state_group = object() + + return frozenset(cache.hosts_to_joined_users) @cached(max_entries=10000) def _get_joined_hosts_cache(self, room_id: str) -> "_JoinedHostsCache": - return _JoinedHostsCache(self, room_id) + return _JoinedHostsCache() @cached(num_args=2) async def did_forget(self, user_id: str, room_id: str) -> bool: @@ -1062,71 +1132,18 @@ def f(txn): await self.db_pool.runInteraction("forget_membership", f) +@attr.s(slots=True) class _JoinedHostsCache: - """Cache for joined hosts in a room that is optimised to handle updates - via state deltas. - """ - - def __init__(self, store, room_id): - self.store = store - self.room_id = room_id + """The cached data used by the `_get_joined_hosts_cache`.""" - self.hosts_to_joined_users = {} + # Dict of host to the set of their users in the room at the state group. + hosts_to_joined_users = attr.ib(type=Dict[str, Set[str]], factory=dict) - self.state_group = object() - - self.linearizer = Linearizer("_JoinedHostsCache") - - self._len = 0 - - async def get_destinations(self, state_entry: "_StateCacheEntry") -> Set[str]: - """Get set of destinations for a state entry - - Args: - state_entry - - Returns: - The destinations as a set. - """ - if state_entry.state_group == self.state_group: - return frozenset(self.hosts_to_joined_users) - - with (await self.linearizer.queue(())): - if state_entry.state_group == self.state_group: - pass - elif state_entry.prev_group == self.state_group: - for (typ, state_key), event_id in state_entry.delta_ids.items(): - if typ != EventTypes.Member: - continue - - host = intern_string(get_domain_from_id(state_key)) - user_id = state_key - known_joins = self.hosts_to_joined_users.setdefault(host, set()) - - event = await self.store.get_event(event_id) - if event.membership == Membership.JOIN: - known_joins.add(user_id) - else: - known_joins.discard(user_id) - - if not known_joins: - self.hosts_to_joined_users.pop(host, None) - else: - joined_users = await self.store.get_joined_users_from_state( - self.room_id, state_entry - ) - - self.hosts_to_joined_users = {} - for user_id in joined_users: - host = intern_string(get_domain_from_id(user_id)) - self.hosts_to_joined_users.setdefault(host, set()).add(user_id) - - if state_entry.state_group: - self.state_group = state_entry.state_group - else: - self.state_group = object() - self._len = sum(len(v) for v in self.hosts_to_joined_users.values()) - return frozenset(self.hosts_to_joined_users) + # The state group `hosts_to_joined_users` is derived from. Will be an object + # if the instance is newly created or if the state is not based on a state + # group. (An object is used as a sentinel value to ensure that it never is + # equal to anything else). + state_group = attr.ib(type=Union[object, int], factory=object) def __len__(self): - return self._len + return sum(len(v) for v in self.hosts_to_joined_users.values()) From d924827da1db5d210eb06db2247a1403ed4c8b9a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 23 Apr 2021 07:05:51 -0400 Subject: [PATCH 090/619] Check for space membership during a remote join of a restricted room (#9814) When receiving a /send_join request for a room with join rules set to 'restricted', check if the user is a member of the spaces defined in the 'allow' key of the join rules. This only applies to an experimental room version, as defined in MSC3083. --- changelog.d/9814.feature | 1 + synapse/api/auth.py | 1 + synapse/handlers/event_auth.py | 86 +++++++++++++++++++++++++++++++++ synapse/handlers/federation.py | 44 +++++++++++++---- synapse/handlers/room_member.py | 62 ++---------------------- synapse/server.py | 5 ++ 6 files changed, 131 insertions(+), 68 deletions(-) create mode 100644 changelog.d/9814.feature create mode 100644 synapse/handlers/event_auth.py diff --git a/changelog.d/9814.feature b/changelog.d/9814.feature new file mode 100644 index 0000000000..9404ad2fc0 --- /dev/null +++ b/changelog.d/9814.feature @@ -0,0 +1 @@ +Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 872fd100cd..2d845d0d5c 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -65,6 +65,7 @@ class Auth: """ FIXME: This class contains a mix of functions for authenticating users of our client-server API and authenticating events added to room graphs. + The latter should be moved to synapse.handlers.event_auth.EventAuthHandler. """ def __init__(self, hs): diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py new file mode 100644 index 0000000000..eff639f407 --- /dev/null +++ b/synapse/handlers/event_auth.py @@ -0,0 +1,86 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TYPE_CHECKING + +from synapse.api.constants import EventTypes, JoinRules +from synapse.api.room_versions import RoomVersion +from synapse.types import StateMap + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +class EventAuthHandler: + """ + This class contains methods for authenticating events added to room graphs. + """ + + def __init__(self, hs: "HomeServer"): + self._store = hs.get_datastore() + + async def can_join_without_invite( + self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str + ) -> bool: + """ + Check whether a user can join a room without an invite. + + When joining a room with restricted joined rules (as defined in MSC3083), + the membership of spaces must be checked during join. + + Args: + state_ids: The state of the room as it currently is. + room_version: The room version of the room being joined. + user_id: The user joining the room. + + Returns: + True if the user can join the room, false otherwise. + """ + # This only applies to room versions which support the new join rule. + if not room_version.msc3083_join_rules: + return True + + # If there's no join rule, then it defaults to invite (so this doesn't apply). + join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) + if not join_rules_event_id: + return True + + # If the join rule is not restricted, this doesn't apply. + join_rules_event = await self._store.get_event(join_rules_event_id) + if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: + return True + + # If allowed is of the wrong form, then only allow invited users. + allowed_spaces = join_rules_event.content.get("allow", []) + if not isinstance(allowed_spaces, list): + return False + + # Get the list of joined rooms and see if there's an overlap. + joined_rooms = await self._store.get_rooms_for_user(user_id) + + # Pull out the other room IDs, invalid data gets filtered. + for space in allowed_spaces: + if not isinstance(space, dict): + continue + + space_id = space.get("space") + if not isinstance(space_id, str): + continue + + # The user was joined to one of the spaces specified, they can join + # this room! + if space_id in joined_rooms: + return True + + # The user was not in any of the required spaces. + return False diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index dbdd7d2db3..9d867aaf4d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -146,6 +146,7 @@ def __init__(self, hs: "HomeServer"): self.is_mine_id = hs.is_mine_id self.spam_checker = hs.get_spam_checker() 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._server_notices_mxid = hs.config.server_notices_mxid self.config = hs.config @@ -1673,8 +1674,40 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: # would introduce the danger of backwards-compatibility problems. event.internal_metadata.send_on_behalf_of = origin + # Calculate the event context. context = await self.state_handler.compute_event_context(event) - context = await self._auth_and_persist_event(origin, event, context) + + # Get the state before the new event. + prev_state_ids = await context.get_prev_state_ids() + + # Check if the user is already in the room or invited to the room. + user_id = event.state_key + prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None) + newly_joined = True + user_is_invited = False + if prev_member_event_id: + prev_member_event = await self.store.get_event(prev_member_event_id) + newly_joined = prev_member_event.membership != Membership.JOIN + user_is_invited = prev_member_event.membership == Membership.INVITE + + # If the member is not already in the room, and not invited, check if + # they should be allowed access via membership in a space. + if ( + newly_joined + and not user_is_invited + and not await self._event_auth_handler.can_join_without_invite( + prev_state_ids, + event.room_version, + user_id, + ) + ): + raise AuthError( + 403, + "You do not belong to any of the required spaces to join this room.", + ) + + # Persist the event. + await self._auth_and_persist_event(origin, event, context) logger.debug( "on_send_join_request: After _auth_and_persist_event: %s, sigs: %s", @@ -1682,8 +1715,6 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: event.signatures, ) - prev_state_ids = await context.get_prev_state_ids() - state_ids = list(prev_state_ids.values()) auth_chain = await self.store.get_auth_chain(event.room_id, state_ids) @@ -2006,7 +2037,7 @@ async def _auth_and_persist_event( state: Optional[Iterable[EventBase]] = None, auth_events: Optional[MutableStateMap[EventBase]] = None, backfilled: bool = False, - ) -> EventContext: + ) -> None: """ Process an event by performing auth checks and then persisting to the database. @@ -2028,9 +2059,6 @@ async def _auth_and_persist_event( event is an outlier), may be the auth events claimed by the remote server. backfilled: True if the event was backfilled. - - Returns: - The event context. """ context = await self._check_event_auth( origin, @@ -2060,8 +2088,6 @@ async def _auth_and_persist_event( ) raise - return context - async def _auth_and_persist_events( self, origin: str, diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 2bbfac6471..2c5bada1d8 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple from synapse import types -from synapse.api.constants import AccountDataTypes, EventTypes, JoinRules, Membership +from synapse.api.constants import AccountDataTypes, EventTypes, Membership from synapse.api.errors import ( AuthError, Codes, @@ -28,7 +28,6 @@ SynapseError, ) from synapse.api.ratelimiting import Ratelimiter -from synapse.api.room_versions import RoomVersion from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID @@ -64,6 +63,7 @@ def __init__(self, hs: "HomeServer"): self.profile_handler = hs.get_profile_handler() self.event_creation_handler = hs.get_event_creation_handler() self.account_data_handler = hs.get_account_data_handler() + self.event_auth_handler = hs.get_event_auth_handler() self.member_linearizer = Linearizer(name="member") @@ -178,62 +178,6 @@ async def ratelimit_invite( await self._invites_per_user_limiter.ratelimit(requester, invitee_user_id) - async def _can_join_without_invite( - self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str - ) -> bool: - """ - Check whether a user can join a room without an invite. - - When joining a room with restricted joined rules (as defined in MSC3083), - the membership of spaces must be checked during join. - - Args: - state_ids: The state of the room as it currently is. - room_version: The room version of the room being joined. - user_id: The user joining the room. - - Returns: - True if the user can join the room, false otherwise. - """ - # This only applies to room versions which support the new join rule. - if not room_version.msc3083_join_rules: - return True - - # If there's no join rule, then it defaults to public (so this doesn't apply). - join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) - if not join_rules_event_id: - return True - - # If the join rule is not restricted, this doesn't apply. - join_rules_event = await self.store.get_event(join_rules_event_id) - if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: - return True - - # If allowed is of the wrong form, then only allow invited users. - allowed_spaces = join_rules_event.content.get("allow", []) - if not isinstance(allowed_spaces, list): - return False - - # Get the list of joined rooms and see if there's an overlap. - joined_rooms = await self.store.get_rooms_for_user(user_id) - - # Pull out the other room IDs, invalid data gets filtered. - for space in allowed_spaces: - if not isinstance(space, dict): - continue - - space_id = space.get("space") - if not isinstance(space_id, str): - continue - - # The user was joined to one of the spaces specified, they can join - # this room! - if space_id in joined_rooms: - return True - - # The user was not in any of the required spaces. - return False - async def _local_membership_update( self, requester: Requester, @@ -302,7 +246,7 @@ async def _local_membership_update( if ( newly_joined and not user_is_invited - and not await self._can_join_without_invite( + and not await self.event_auth_handler.can_join_without_invite( prev_state_ids, event.room_version, user_id ) ): diff --git a/synapse/server.py b/synapse/server.py index 59ae91b503..67598fffe3 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -77,6 +77,7 @@ from synapse.handlers.directory import DirectoryHandler from synapse.handlers.e2e_keys import E2eKeysHandler from synapse.handlers.e2e_room_keys import E2eRoomKeysHandler +from synapse.handlers.event_auth import EventAuthHandler from synapse.handlers.events import EventHandler, EventStreamHandler from synapse.handlers.federation import FederationHandler from synapse.handlers.groups_local import GroupsLocalHandler, GroupsLocalWorkerHandler @@ -746,6 +747,10 @@ def get_account_data_handler(self) -> AccountDataHandler: def get_space_summary_handler(self) -> SpaceSummaryHandler: return SpaceSummaryHandler(self) + @cache_in_self + def get_event_auth_handler(self) -> EventAuthHandler: + return EventAuthHandler(self) + @cache_in_self def get_external_cache(self) -> ExternalCache: return ExternalCache(self) From 9d25a0ae65ce8728d0fda1eebaf0b469316f84d7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 23 Apr 2021 12:21:55 +0100 Subject: [PATCH 091/619] Split presence out of master (#9820) --- changelog.d/9820.feature | 1 + scripts/synapse_port_db | 7 +- synapse/app/generic_worker.py | 31 +------ synapse/config/workers.py | 27 +++++- synapse/handlers/presence.py | 56 +++++++---- synapse/replication/http/_base.py | 5 +- synapse/replication/slave/storage/presence.py | 50 ---------- synapse/replication/tcp/handler.py | 18 +++- synapse/replication/tcp/streams/_base.py | 17 +++- synapse/rest/client/v1/presence.py | 7 +- synapse/server.py | 6 +- synapse/storage/databases/main/__init__.py | 47 +--------- synapse/storage/databases/main/presence.py | 92 ++++++++++++++++++- .../delta/59/12presence_stream_instance.sql | 18 ++++ ...2presence_stream_instance_seq.sql.postgres | 20 ++++ tests/app/test_frontend_proxy.py | 83 ----------------- tests/rest/client/v1/test_presence.py | 5 +- 17 files changed, 245 insertions(+), 245 deletions(-) create mode 100644 changelog.d/9820.feature delete mode 100644 synapse/replication/slave/storage/presence.py create mode 100644 synapse/storage/databases/main/schema/delta/59/12presence_stream_instance.sql create mode 100644 synapse/storage/databases/main/schema/delta/59/12presence_stream_instance_seq.sql.postgres delete mode 100644 tests/app/test_frontend_proxy.py diff --git a/changelog.d/9820.feature b/changelog.d/9820.feature new file mode 100644 index 0000000000..f56b0bb3bd --- /dev/null +++ b/changelog.d/9820.feature @@ -0,0 +1 @@ +Add experimental support for handling presence on a worker. diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index b7c1ffc956..f0c93d5226 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -634,8 +634,11 @@ class Porter(object): "device_inbox_sequence", ("device_inbox", "device_federation_outbox") ) await self._setup_sequence( - "account_data_sequence", ("room_account_data", "room_tags_revisions", "account_data")) - await self._setup_sequence("receipts_sequence", ("receipts_linearized", )) + "account_data_sequence", + ("room_account_data", "room_tags_revisions", "account_data"), + ) + await self._setup_sequence("receipts_sequence", ("receipts_linearized",)) + await self._setup_sequence("presence_stream_sequence", ("presence_stream",)) await self._setup_auth_chain_sequence() # Step 3. Get tables. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 26c458dbb6..7b2ac3ca64 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -55,7 +55,6 @@ from synapse.replication.slave.storage.filtering import SlavedFilteringStore from synapse.replication.slave.storage.groups import SlavedGroupServerStore from synapse.replication.slave.storage.keys import SlavedKeyStore -from synapse.replication.slave.storage.presence import SlavedPresenceStore from synapse.replication.slave.storage.profile import SlavedProfileStore from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore from synapse.replication.slave.storage.pushers import SlavedPusherStore @@ -64,7 +63,7 @@ from synapse.replication.slave.storage.room import RoomStore from synapse.replication.slave.storage.transactions import SlavedTransactionStore from synapse.rest.admin import register_servlets_for_media_repo -from synapse.rest.client.v1 import events, login, room +from synapse.rest.client.v1 import events, login, presence, room from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet from synapse.rest.client.v1.profile import ( ProfileAvatarURLRestServlet, @@ -110,6 +109,7 @@ from synapse.storage.databases.main.monthly_active_users import ( MonthlyActiveUsersWorkerStore, ) +from synapse.storage.databases.main.presence import PresenceStore from synapse.storage.databases.main.search import SearchWorkerStore from synapse.storage.databases.main.stats import StatsStore from synapse.storage.databases.main.transactions import TransactionWorkerStore @@ -121,26 +121,6 @@ logger = logging.getLogger("synapse.app.generic_worker") -class PresenceStatusStubServlet(RestServlet): - """If presence is disabled this servlet can be used to stub out setting - presence status. - """ - - PATTERNS = client_patterns("/presence/(?P[^/]*)/status") - - def __init__(self, hs): - super().__init__() - self.auth = hs.get_auth() - - async def on_GET(self, request, user_id): - await self.auth.get_user_by_req(request) - return 200, {"presence": "offline"} - - async def on_PUT(self, request, user_id): - await self.auth.get_user_by_req(request) - return 200, {} - - class KeyUploadServlet(RestServlet): """An implementation of the `KeyUploadServlet` that responds to read only requests, but otherwise proxies through to the master instance. @@ -241,6 +221,7 @@ class GenericWorkerSlavedStore( StatsStore, UIAuthWorkerStore, EndToEndRoomKeyStore, + PresenceStore, SlavedDeviceInboxStore, SlavedDeviceStore, SlavedReceiptsStore, @@ -259,7 +240,6 @@ class GenericWorkerSlavedStore( SlavedTransactionStore, SlavedProfileStore, SlavedClientIpStore, - SlavedPresenceStore, SlavedFilteringStore, MonthlyActiveUsersWorkerStore, MediaRepositoryStore, @@ -327,10 +307,7 @@ def _listen_http(self, listener_config: ListenerConfig): user_directory.register_servlets(self, resource) - # If presence is disabled, use the stub servlet that does - # not allow sending presence - if not self.config.use_presence: - PresenceStatusStubServlet(self).register(resource) + presence.register_servlets(self, resource) groups.register_servlets(self, resource) diff --git a/synapse/config/workers.py b/synapse/config/workers.py index b2540163d1..462630201d 100644 --- a/synapse/config/workers.py +++ b/synapse/config/workers.py @@ -64,6 +64,14 @@ class WriterLocations: Attributes: events: The instances that write to the event and backfill streams. typing: The instance that writes to the typing stream. + to_device: The instances that write to the to_device stream. Currently + can only be a single instance. + account_data: The instances that write to the account data streams. Currently + can only be a single instance. + receipts: The instances that write to the receipts stream. Currently + can only be a single instance. + presence: The instances that write to the presence stream. Currently + can only be a single instance. """ events = attr.ib( @@ -85,6 +93,11 @@ class WriterLocations: type=List[str], converter=_instance_to_list_converter, ) + presence = attr.ib( + default=["master"], + type=List[str], + converter=_instance_to_list_converter, + ) class WorkerConfig(Config): @@ -188,7 +201,14 @@ def read_config(self, config, **kwargs): # Check that the configured writers for events and typing also appears in # `instance_map`. - for stream in ("events", "typing", "to_device", "account_data", "receipts"): + for stream in ( + "events", + "typing", + "to_device", + "account_data", + "receipts", + "presence", + ): instances = _instance_to_list_converter(getattr(self.writers, stream)) for instance in instances: if instance != "master" and instance not in self.instance_map: @@ -215,6 +235,11 @@ def read_config(self, config, **kwargs): if len(self.writers.events) == 0: raise ConfigError("Must specify at least one instance to handle `events`.") + if len(self.writers.presence) != 1: + raise ConfigError( + "Must only specify one instance to handle `presence` messages." + ) + self.events_shard_config = RoutableShardedWorkerHandlingConfig( self.writers.events ) diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 7fd28ffa54..9938be3821 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -122,7 +122,8 @@ class BasePresenceHandler(abc.ABC): - """Parts of the PresenceHandler that are shared between workers and master""" + """Parts of the PresenceHandler that are shared between workers and presence + writer""" def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() @@ -309,8 +310,16 @@ def __init__(self, hs): super().__init__(hs) self.hs = hs + self._presence_writer_instance = hs.config.worker.writers.presence[0] + self._presence_enabled = hs.config.use_presence + # Route presence EDUs to the right worker + hs.get_federation_registry().register_instances_for_edu( + "m.presence", + hs.config.worker.writers.presence, + ) + # The number of ongoing syncs on this process, by user id. # Empty if _presence_enabled is false. self._user_to_num_current_syncs = {} # type: Dict[str, int] @@ -318,8 +327,8 @@ def __init__(self, hs): self.notifier = hs.get_notifier() self.instance_id = hs.get_instance_id() - # user_id -> last_sync_ms. Lists the users that have stopped syncing - # but we haven't notified the master of that yet + # user_id -> last_sync_ms. Lists the users that have stopped syncing but + # we haven't notified the presence writer of that yet self.users_going_offline = {} self._bump_active_client = ReplicationBumpPresenceActiveTime.make_client(hs) @@ -352,22 +361,23 @@ def send_user_sync(self, user_id, is_syncing, last_sync_ms): ) def mark_as_coming_online(self, user_id): - """A user has started syncing. Send a UserSync to the master, unless they - had recently stopped syncing. + """A user has started syncing. Send a UserSync to the presence writer, + unless they had recently stopped syncing. Args: user_id (str) """ going_offline = self.users_going_offline.pop(user_id, None) if not going_offline: - # Safe to skip because we haven't yet told the master they were offline + # Safe to skip because we haven't yet told the presence writer they + # were offline self.send_user_sync(user_id, True, self.clock.time_msec()) def mark_as_going_offline(self, user_id): - """A user has stopped syncing. We wait before notifying the master as - its likely they'll come back soon. This allows us to avoid sending - a stopped syncing immediately followed by a started syncing notification - to the master + """A user has stopped syncing. We wait before notifying the presence + writer as its likely they'll come back soon. This allows us to avoid + sending a stopped syncing immediately followed by a started syncing + notification to the presence writer Args: user_id (str) @@ -375,8 +385,8 @@ def mark_as_going_offline(self, user_id): self.users_going_offline[user_id] = self.clock.time_msec() def send_stop_syncing(self): - """Check if there are any users who have stopped syncing a while ago - and haven't come back yet. If there are poke the master about them. + """Check if there are any users who have stopped syncing a while ago and + haven't come back yet. If there are poke the presence writer about them. """ now = self.clock.time_msec() for user_id, last_sync_ms in list(self.users_going_offline.items()): @@ -492,9 +502,12 @@ async def set_state(self, target_user, state, ignore_status_msg=False): if not self.hs.config.use_presence: return - # Proxy request to master + # Proxy request to instance that writes presence await self._set_state_client( - user_id=user_id, state=state, ignore_status_msg=ignore_status_msg + instance_name=self._presence_writer_instance, + user_id=user_id, + state=state, + ignore_status_msg=ignore_status_msg, ) async def bump_presence_active_time(self, user): @@ -505,9 +518,11 @@ async def bump_presence_active_time(self, user): if not self.hs.config.use_presence: return - # Proxy request to master + # Proxy request to instance that writes presence user_id = user.to_string() - await self._bump_active_client(user_id=user_id) + await self._bump_active_client( + instance_name=self._presence_writer_instance, user_id=user_id + ) class PresenceHandler(BasePresenceHandler): @@ -1909,7 +1924,7 @@ def __init__(self, hs: "HomeServer", presence_handler: BasePresenceHandler): self._queue_presence_updates = True # Whether this instance is a presence writer. - self._presence_writer = hs.config.worker.worker_app is None + self._presence_writer = self._instance_name in hs.config.worker.writers.presence # The FederationSender instance, if this process sends federation traffic directly. self._federation = None @@ -1957,7 +1972,7 @@ def send_presence_to_destinations( Will forward to the local federation sender (if there is one) and queue to send over replication (if there are other federation sender instances.). - Must only be called on the master process. + Must only be called on the presence writer process. """ # This should only be called on a presence writer. @@ -2003,10 +2018,11 @@ async def get_replication_rows( We return rows in the form of `(destination, user_id)` to keep the size of each row bounded (rather than returning the sets in a row). - On workers this will query the master process via HTTP replication. + On workers this will query the presence writer process via HTTP replication. """ if instance_name != self._instance_name: - # If not local we query over http replication from the master + # If not local we query over http replication from the presence + # writer result = await self._repl_client( instance_name=instance_name, stream_name=PresenceFederationStream.NAME, diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index ece03467b5..5685cf2121 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -158,7 +158,10 @@ async def _handle_request(self, request, **kwargs): def make_client(cls, hs): """Create a client that makes requests. - Returns a callable that accepts the same parameters as `_serialize_payload`. + Returns a callable that accepts the same parameters as + `_serialize_payload`, and also accepts an optional `instance_name` + parameter to specify which instance to hit (the instance must be in + the `instance_map` config). """ clock = hs.get_clock() client = hs.get_simple_http_client() diff --git a/synapse/replication/slave/storage/presence.py b/synapse/replication/slave/storage/presence.py deleted file mode 100644 index 57327d910d..0000000000 --- a/synapse/replication/slave/storage/presence.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2016 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. - -from synapse.replication.tcp.streams import PresenceStream -from synapse.storage import DataStore -from synapse.storage.database import DatabasePool -from synapse.storage.databases.main.presence import PresenceStore -from synapse.util.caches.stream_change_cache import StreamChangeCache - -from ._base import BaseSlavedStore -from ._slaved_id_tracker import SlavedIdTracker - - -class SlavedPresenceStore(BaseSlavedStore): - def __init__(self, database: DatabasePool, db_conn, hs): - super().__init__(database, db_conn, hs) - self._presence_id_gen = SlavedIdTracker(db_conn, "presence_stream", "stream_id") - - self._presence_on_startup = self._get_active_presence(db_conn) # type: ignore - - self.presence_stream_cache = StreamChangeCache( - "PresenceStreamChangeCache", self._presence_id_gen.get_current_token() - ) - - _get_active_presence = DataStore._get_active_presence - take_presence_startup_info = DataStore.take_presence_startup_info - _get_presence_for_user = PresenceStore.__dict__["_get_presence_for_user"] - get_presence_for_users = PresenceStore.__dict__["get_presence_for_users"] - - def get_current_presence_token(self): - return self._presence_id_gen.get_current_token() - - def process_replication_rows(self, stream_name, instance_name, token, rows): - if stream_name == PresenceStream.NAME: - self._presence_id_gen.advance(instance_name, token) - for row in rows: - self.presence_stream_cache.entity_has_changed(row.user_id, token) - self._get_presence_for_user.invalidate((row.user_id,)) - return super().process_replication_rows(stream_name, instance_name, token, rows) diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index 2ce1b9f222..7ced4c543c 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -55,6 +55,8 @@ CachesStream, EventsStream, FederationStream, + PresenceFederationStream, + PresenceStream, ReceiptsStream, Stream, TagAccountDataStream, @@ -99,6 +101,10 @@ def __init__(self, hs: "HomeServer"): self._instance_id = hs.get_instance_id() self._instance_name = hs.get_instance_name() + self._is_presence_writer = ( + hs.get_instance_name() in hs.config.worker.writers.presence + ) + self._streams = { stream.NAME: stream(hs) for stream in STREAMS_MAP.values() } # type: Dict[str, Stream] @@ -153,6 +159,14 @@ def __init__(self, hs: "HomeServer"): continue + if isinstance(stream, (PresenceStream, PresenceFederationStream)): + # Only add PresenceStream as a source on the instance in charge + # of presence. + if self._is_presence_writer: + self._streams_to_replicate.append(stream) + + continue + # Only add any other streams if we're on master. if hs.config.worker_app is not None: continue @@ -350,7 +364,7 @@ def on_USER_SYNC( ) -> Optional[Awaitable[None]]: user_sync_counter.inc() - if self._is_master: + if self._is_presence_writer: return self._presence_handler.update_external_syncs_row( cmd.instance_id, cmd.user_id, cmd.is_syncing, cmd.last_sync_ms ) @@ -360,7 +374,7 @@ def on_USER_SYNC( def on_CLEAR_USER_SYNC( self, conn: IReplicationConnection, cmd: ClearUserSyncsCommand ) -> Optional[Awaitable[None]]: - if self._is_master: + if self._is_presence_writer: return self._presence_handler.update_external_syncs_clear(cmd.instance_id) else: return None diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 9d75a89f1c..b03824925a 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -272,15 +272,22 @@ class PresenceStream(Stream): NAME = "presence" ROW_TYPE = PresenceStreamRow - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): store = hs.get_datastore() - if hs.config.worker_app is None: - # on the master, query the presence handler + if hs.get_instance_name() in hs.config.worker.writers.presence: + # on the presence writer, query the presence handler presence_handler = hs.get_presence_handler() - update_function = presence_handler.get_all_presence_updates + + from synapse.handlers.presence import PresenceHandler + + assert isinstance(presence_handler, PresenceHandler) + + update_function = ( + presence_handler.get_all_presence_updates + ) # type: UpdateFunction else: - # Query master process + # Query presence writer process update_function = make_http_update_function(hs, self.NAME) super().__init__( diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/v1/presence.py index c232484f29..2b24fe5aa6 100644 --- a/synapse/rest/client/v1/presence.py +++ b/synapse/rest/client/v1/presence.py @@ -35,10 +35,15 @@ def __init__(self, hs): self.clock = hs.get_clock() self.auth = hs.get_auth() + self._use_presence = hs.config.server.use_presence + async def on_GET(self, request, user_id): requester = await self.auth.get_user_by_req(request) user = UserID.from_string(user_id) + if not self._use_presence: + return 200, {"presence": "offline"} + if requester.user != user: allowed = await self.presence_handler.is_visible( observed_user=user, observer_user=requester.user @@ -80,7 +85,7 @@ async def on_PUT(self, request, user_id): except Exception: raise SynapseError(400, "Unable to parse state") - if self.hs.config.use_presence: + if self._use_presence: await self.presence_handler.set_state(user, state) return 200, {} diff --git a/synapse/server.py b/synapse/server.py index 67598fffe3..8c147be2b3 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -418,10 +418,10 @@ def get_state_resolution_handler(self) -> StateResolutionHandler: @cache_in_self def get_presence_handler(self) -> BasePresenceHandler: - if self.config.worker_app: - return WorkerPresenceHandler(self) - else: + if self.get_instance_name() in self.config.worker.writers.presence: return PresenceHandler(self) + else: + return WorkerPresenceHandler(self) @cache_in_self def get_typing_writer_handler(self) -> TypingWriterHandler: diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index 5c50f5f950..49c7606d51 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -17,7 +17,6 @@ import logging from typing import List, Optional, Tuple -from synapse.api.constants import PresenceState from synapse.config.homeserver import HomeServerConfig from synapse.storage.database import DatabasePool from synapse.storage.databases.main.stats import UserSortOrder @@ -51,7 +50,7 @@ from .metrics import ServerMetricsStore from .monthly_active_users import MonthlyActiveUsersStore from .openid import OpenIdStore -from .presence import PresenceStore, UserPresenceState +from .presence import PresenceStore from .profile import ProfileStore from .purge_events import PurgeEventsStore from .push_rule import PushRuleStore @@ -126,9 +125,6 @@ def __init__(self, database: DatabasePool, db_conn, hs): self._clock = hs.get_clock() self.database_engine = database.engine - self._presence_id_gen = StreamIdGenerator( - db_conn, "presence_stream", "stream_id" - ) self._public_room_id_gen = StreamIdGenerator( db_conn, "public_room_list_stream", "stream_id" ) @@ -177,21 +173,6 @@ def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) - self._presence_on_startup = self._get_active_presence(db_conn) - - presence_cache_prefill, min_presence_val = self.db_pool.get_cache_dict( - db_conn, - "presence_stream", - entity_column="user_id", - stream_column="stream_id", - max_value=self._presence_id_gen.get_current_token(), - ) - self.presence_stream_cache = StreamChangeCache( - "PresenceStreamChangeCache", - min_presence_val, - prefilled_cache=presence_cache_prefill, - ) - device_list_max = self._device_list_id_gen.get_current_token() self._device_list_stream_cache = StreamChangeCache( "DeviceListStreamChangeCache", device_list_max @@ -238,32 +219,6 @@ def __init__(self, database: DatabasePool, db_conn, hs): def get_device_stream_token(self) -> int: return self._device_list_id_gen.get_current_token() - def take_presence_startup_info(self): - active_on_startup = self._presence_on_startup - self._presence_on_startup = None - return active_on_startup - - def _get_active_presence(self, db_conn): - """Fetch non-offline presence from the database so that we can register - the appropriate time outs. - """ - - sql = ( - "SELECT user_id, state, last_active_ts, last_federation_update_ts," - " last_user_sync_ts, status_msg, currently_active FROM presence_stream" - " WHERE state != ?" - ) - - txn = db_conn.cursor() - txn.execute(sql, (PresenceState.OFFLINE,)) - rows = self.db_pool.cursor_to_dict(txn) - txn.close() - - for row in rows: - row["currently_active"] = bool(row["currently_active"]) - - return [UserPresenceState(**row) for row in rows] - async def get_users(self) -> List[JsonDict]: """Function to retrieve a list of users in users table. diff --git a/synapse/storage/databases/main/presence.py b/synapse/storage/databases/main/presence.py index c207d917b1..db22fab23e 100644 --- a/synapse/storage/databases/main/presence.py +++ b/synapse/storage/databases/main/presence.py @@ -12,16 +12,69 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List, Tuple +from typing import TYPE_CHECKING, Dict, List, Tuple -from synapse.api.presence import UserPresenceState +from synapse.api.presence import PresenceState, UserPresenceState +from synapse.replication.tcp.streams import PresenceStream from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage.database import DatabasePool +from synapse.storage.engines import PostgresEngine +from synapse.storage.types import Connection +from synapse.storage.util.id_generators import MultiWriterIdGenerator, StreamIdGenerator from synapse.util.caches.descriptors import cached, cachedList +from synapse.util.caches.stream_change_cache import StreamChangeCache from synapse.util.iterutils import batch_iter +if TYPE_CHECKING: + from synapse.server import HomeServer + class PresenceStore(SQLBaseStore): + def __init__( + self, + database: DatabasePool, + db_conn: Connection, + hs: "HomeServer", + ): + super().__init__(database, db_conn, hs) + + self._can_persist_presence = ( + hs.get_instance_name() in hs.config.worker.writers.presence + ) + + if isinstance(database.engine, PostgresEngine): + self._presence_id_gen = MultiWriterIdGenerator( + db_conn=db_conn, + db=database, + stream_name="presence_stream", + instance_name=self._instance_name, + tables=[("presence_stream", "instance_name", "stream_id")], + sequence_name="presence_stream_sequence", + writers=hs.config.worker.writers.to_device, + ) + else: + self._presence_id_gen = StreamIdGenerator( + db_conn, "presence_stream", "stream_id" + ) + + self._presence_on_startup = self._get_active_presence(db_conn) + + presence_cache_prefill, min_presence_val = self.db_pool.get_cache_dict( + db_conn, + "presence_stream", + entity_column="user_id", + stream_column="stream_id", + max_value=self._presence_id_gen.get_current_token(), + ) + self.presence_stream_cache = StreamChangeCache( + "PresenceStreamChangeCache", + min_presence_val, + prefilled_cache=presence_cache_prefill, + ) + async def update_presence(self, presence_states): + assert self._can_persist_presence + stream_ordering_manager = self._presence_id_gen.get_next_mult( len(presence_states) ) @@ -57,6 +110,7 @@ def _update_presence_txn(self, txn, stream_orderings, presence_states): "last_user_sync_ts": state.last_user_sync_ts, "status_msg": state.status_msg, "currently_active": state.currently_active, + "instance_name": self._instance_name, } for stream_id, state in zip(stream_orderings, presence_states) ], @@ -216,3 +270,37 @@ async def get_presence_for_all_users( def get_current_presence_token(self): return self._presence_id_gen.get_current_token() + + def _get_active_presence(self, db_conn: Connection): + """Fetch non-offline presence from the database so that we can register + the appropriate time outs. + """ + + sql = ( + "SELECT user_id, state, last_active_ts, last_federation_update_ts," + " last_user_sync_ts, status_msg, currently_active FROM presence_stream" + " WHERE state != ?" + ) + + txn = db_conn.cursor() + txn.execute(sql, (PresenceState.OFFLINE,)) + rows = self.db_pool.cursor_to_dict(txn) + txn.close() + + for row in rows: + row["currently_active"] = bool(row["currently_active"]) + + return [UserPresenceState(**row) for row in rows] + + def take_presence_startup_info(self): + active_on_startup = self._presence_on_startup + self._presence_on_startup = None + return active_on_startup + + def process_replication_rows(self, stream_name, instance_name, token, rows): + if stream_name == PresenceStream.NAME: + self._presence_id_gen.advance(instance_name, token) + for row in rows: + self.presence_stream_cache.entity_has_changed(row.user_id, token) + self._get_presence_for_user.invalidate((row.user_id,)) + return super().process_replication_rows(stream_name, instance_name, token, rows) diff --git a/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance.sql b/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance.sql new file mode 100644 index 0000000000..b6ba0bda1a --- /dev/null +++ b/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance.sql @@ -0,0 +1,18 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Add a column to specify which instance wrote the row. Historic rows have +-- `NULL`, which indicates that the master instance wrote them. +ALTER TABLE presence_stream ADD COLUMN instance_name TEXT; diff --git a/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance_seq.sql.postgres b/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance_seq.sql.postgres new file mode 100644 index 0000000000..02b182adf9 --- /dev/null +++ b/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance_seq.sql.postgres @@ -0,0 +1,20 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CREATE SEQUENCE IF NOT EXISTS presence_stream_sequence; + +SELECT setval('presence_stream_sequence', ( + SELECT COALESCE(MAX(stream_id), 1) FROM presence_stream +)); diff --git a/tests/app/test_frontend_proxy.py b/tests/app/test_frontend_proxy.py deleted file mode 100644 index 3d45da38ab..0000000000 --- a/tests/app/test_frontend_proxy.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2018 New Vector 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. - -from synapse.app.generic_worker import GenericWorkerServer - -from tests.server import make_request -from tests.unittest import HomeserverTestCase - - -class FrontendProxyTests(HomeserverTestCase): - def make_homeserver(self, reactor, clock): - - hs = self.setup_test_homeserver( - federation_http_client=None, homeserver_to_use=GenericWorkerServer - ) - - return hs - - def default_config(self): - c = super().default_config() - c["worker_app"] = "synapse.app.frontend_proxy" - - c["worker_listeners"] = [ - { - "type": "http", - "port": 8080, - "bind_addresses": ["0.0.0.0"], - "resources": [{"names": ["client"]}], - } - ] - - return c - - def test_listen_http_with_presence_enabled(self): - """ - When presence is on, the stub servlet will not register. - """ - # Presence is on - self.hs.config.use_presence = True - - # Listen with the config - self.hs._listen_http(self.hs.config.worker.worker_listeners[0]) - - # Grab the resource from the site that was told to listen - self.assertEqual(len(self.reactor.tcpServers), 1) - site = self.reactor.tcpServers[0][1] - - channel = make_request(self.reactor, site, "PUT", "presence/a/status") - - # 400 + unrecognised, because nothing is registered - self.assertEqual(channel.code, 400) - self.assertEqual(channel.json_body["errcode"], "M_UNRECOGNIZED") - - def test_listen_http_with_presence_disabled(self): - """ - When presence is off, the stub servlet will register. - """ - # Presence is off - self.hs.config.use_presence = False - - # Listen with the config - self.hs._listen_http(self.hs.config.worker.worker_listeners[0]) - - # Grab the resource from the site that was told to listen - self.assertEqual(len(self.reactor.tcpServers), 1) - site = self.reactor.tcpServers[0][1] - - channel = make_request(self.reactor, site, "PUT", "presence/a/status") - - # 401, because the stub servlet still checks authentication - self.assertEqual(channel.code, 401) - self.assertEqual(channel.json_body["errcode"], "M_MISSING_TOKEN") diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 3a050659ca..409f3949dc 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -16,6 +16,7 @@ from twisted.internet import defer +from synapse.handlers.presence import PresenceHandler from synapse.rest.client.v1 import presence from synapse.types import UserID @@ -32,7 +33,7 @@ class PresenceTestCase(unittest.HomeserverTestCase): def make_homeserver(self, reactor, clock): - presence_handler = Mock() + presence_handler = Mock(spec=PresenceHandler) presence_handler.set_state.return_value = defer.succeed(None) hs = self.setup_test_homeserver( @@ -59,12 +60,12 @@ def test_put_presence(self): self.assertEqual(channel.code, 200) self.assertEqual(self.hs.get_presence_handler().set_state.call_count, 1) + @unittest.override_config({"use_presence": False}) def test_put_presence_disabled(self): """ PUT to the status endpoint with use_presence disabled will NOT call set_state on the presence handler. """ - self.hs.config.use_presence = False body = {"presence": "here", "status_msg": "beep boop"} channel = self.make_request( From ceaa76970fa0092bbdc35055c6f32dc63dd59960 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 23 Apr 2021 13:37:48 +0100 Subject: [PATCH 092/619] Remove room and user invite ratelimits in default unit test config (#9871) --- changelog.d/9871.misc | 1 + tests/utils.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog.d/9871.misc diff --git a/changelog.d/9871.misc b/changelog.d/9871.misc new file mode 100644 index 0000000000..b19acfab62 --- /dev/null +++ b/changelog.d/9871.misc @@ -0,0 +1 @@ +Disable invite rate-limiting by default when running the unit tests. \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py index 63d52b9140..6bd008dcfe 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -153,6 +153,10 @@ def default_config(name, parse=False): "local": {"per_second": 10000, "burst_count": 10000}, "remote": {"per_second": 10000, "burst_count": 10000}, }, + "rc_invites": { + "per_room": {"per_second": 10000, "burst_count": 10000}, + "per_user": {"per_second": 10000, "burst_count": 10000}, + }, "rc_3pid_validation": {"per_second": 10000, "burst_count": 10000}, "saml2_enabled": False, "public_baseurl": None, From a15c003e5b0bff8bf78a675f3b719d3f25fe8bde Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 23 Apr 2021 15:46:29 +0100 Subject: [PATCH 093/619] Make DomainSpecificString an attrs class (#9875) --- changelog.d/9875.misc | 1 + synapse/handlers/oidc.py | 5 +++++ synapse/rest/synapse/client/new_user_consent.py | 9 +++++++++ synapse/types.py | 17 +++++++++-------- 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 changelog.d/9875.misc diff --git a/changelog.d/9875.misc b/changelog.d/9875.misc new file mode 100644 index 0000000000..9345c0bf45 --- /dev/null +++ b/changelog.d/9875.misc @@ -0,0 +1 @@ +Make `DomainSpecificString` an `attrs` class. diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py index 45514be50f..1c4a43be0a 100644 --- a/synapse/handlers/oidc.py +++ b/synapse/handlers/oidc.py @@ -957,6 +957,11 @@ async def grandfather_existing_users() -> Optional[str]: # and attempt to match it. attributes = await oidc_response_to_user_attributes(failures=0) + if attributes.localpart is None: + # If no localpart is returned then we will generate one, so + # there is no need to search for existing users. + return None + user_id = UserID(attributes.localpart, self._server_name).to_string() users = await self._store.get_users_by_id_case_insensitive(user_id) if users: diff --git a/synapse/rest/synapse/client/new_user_consent.py b/synapse/rest/synapse/client/new_user_consent.py index e5634f9679..488b97b32e 100644 --- a/synapse/rest/synapse/client/new_user_consent.py +++ b/synapse/rest/synapse/client/new_user_consent.py @@ -61,6 +61,15 @@ async def _async_render_GET(self, request: Request) -> None: self._sso_handler.render_error(request, "bad_session", e.msg, code=e.code) return + # It should be impossible to get here without having first been through + # the pick-a-username step, which ensures chosen_localpart gets set. + if not session.chosen_localpart: + logger.warning("Session has no user name selected") + self._sso_handler.render_error( + request, "no_user", "No user name has been selected.", code=400 + ) + return + user_id = UserID(session.chosen_localpart, self._server_name) user_profile = { "display_name": session.display_name, diff --git a/synapse/types.py b/synapse/types.py index e19f28d543..e52cd7ffd4 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -199,9 +199,8 @@ def get_localpart_from_id(string): DS = TypeVar("DS", bound="DomainSpecificString") -class DomainSpecificString( - namedtuple("DomainSpecificString", ("localpart", "domain")), metaclass=abc.ABCMeta -): +@attr.s(slots=True, frozen=True, repr=False) +class DomainSpecificString(metaclass=abc.ABCMeta): """Common base class among ID/name strings that have a local part and a domain name, prefixed with a sigil. @@ -213,11 +212,8 @@ class DomainSpecificString( SIGIL = abc.abstractproperty() # type: str # type: ignore - # Deny iteration because it will bite you if you try to create a singleton - # set by: - # users = set(user) - def __iter__(self): - raise ValueError("Attempted to iterate a %s" % (type(self).__name__,)) + localpart = attr.ib(type=str) + domain = attr.ib(type=str) # Because this class is a namedtuple of strings and booleans, it is deeply # immutable. @@ -272,30 +268,35 @@ def is_valid(cls: Type[DS], s: str) -> bool: __repr__ = to_string +@attr.s(slots=True, frozen=True, repr=False) class UserID(DomainSpecificString): """Structure representing a user ID.""" SIGIL = "@" +@attr.s(slots=True, frozen=True, repr=False) class RoomAlias(DomainSpecificString): """Structure representing a room name.""" SIGIL = "#" +@attr.s(slots=True, frozen=True, repr=False) class RoomID(DomainSpecificString): """Structure representing a room id. """ SIGIL = "!" +@attr.s(slots=True, frozen=True, repr=False) class EventID(DomainSpecificString): """Structure representing an event id. """ SIGIL = "$" +@attr.s(slots=True, frozen=True, repr=False) class GroupID(DomainSpecificString): """Structure representing a group ID.""" From e83627926fb5373b383129b99a5039e8a2e329af Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 23 Apr 2021 12:02:16 -0400 Subject: [PATCH 094/619] Add type hints to auth and auth_blocking. (#9876) --- changelog.d/9876.misc | 1 + synapse/api/auth.py | 78 ++++++++++++++++++------------------ synapse/api/auth_blocking.py | 9 +++-- synapse/event_auth.py | 4 +- 4 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 changelog.d/9876.misc diff --git a/changelog.d/9876.misc b/changelog.d/9876.misc new file mode 100644 index 0000000000..28390e32e6 --- /dev/null +++ b/changelog.d/9876.misc @@ -0,0 +1 @@ +Add type hints to `synapse.api.auth` and `synapse.api.auth_blocking` modules. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 2d845d0d5c..efc926d094 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple import pymacaroons from netaddr import IPAddress from twisted.web.server import Request -import synapse.types from synapse import event_auth from synapse.api.auth_blocking import AuthBlocking from synapse.api.constants import EventTypes, HistoryVisibility, Membership @@ -36,11 +35,14 @@ from synapse.http.site import SynapseRequest from synapse.logging import opentracing as opentracing from synapse.storage.databases.main.registration import TokenLookupResult -from synapse.types import StateMap, UserID +from synapse.types import Requester, StateMap, UserID, create_requester from synapse.util.caches.lrucache import LruCache from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry from synapse.util.metrics import Measure +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) @@ -68,7 +70,7 @@ class Auth: The latter should be moved to synapse.handlers.event_auth.EventAuthHandler. """ - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.hs = hs self.clock = hs.get_clock() self.store = hs.get_datastore() @@ -88,13 +90,13 @@ def __init__(self, hs): async def check_from_context( self, room_version: str, event, context, do_sig_check=True - ): + ) -> None: prev_state_ids = await context.get_prev_state_ids() auth_events_ids = self.compute_auth_events( event, prev_state_ids, for_verification=True ) - auth_events = await self.store.get_events(auth_events_ids) - auth_events = {(e.type, e.state_key): e for e in auth_events.values()} + auth_events_by_id = await self.store.get_events(auth_events_ids) + auth_events = {(e.type, e.state_key): e for e in auth_events_by_id.values()} room_version_obj = KNOWN_ROOM_VERSIONS[room_version] event_auth.check( @@ -151,17 +153,11 @@ async def check_user_in_room( raise AuthError(403, "User %s not in room %s" % (user_id, room_id)) - async def check_host_in_room(self, room_id, host): + async def check_host_in_room(self, room_id: str, host: str) -> bool: with Measure(self.clock, "check_host_in_room"): - latest_event_ids = await self.store.is_host_joined(room_id, host) - return latest_event_ids - - def can_federate(self, event, auth_events): - creation_event = auth_events.get((EventTypes.Create, "")) + return await self.store.is_host_joined(room_id, host) - return creation_event.content.get("m.federate", True) is True - - def get_public_keys(self, invite_event): + def get_public_keys(self, invite_event: EventBase) -> List[Dict[str, Any]]: return event_auth.get_public_keys(invite_event) async def get_user_by_req( @@ -170,7 +166,7 @@ async def get_user_by_req( allow_guest: bool = False, rights: str = "access", allow_expired: bool = False, - ) -> synapse.types.Requester: + ) -> Requester: """Get a registered user's ID. Args: @@ -196,7 +192,7 @@ async def get_user_by_req( access_token = self.get_access_token_from_request(request) user_id, app_service = await self._get_appservice_user_id(request) - if user_id: + if user_id and app_service: if ip_addr and self._track_appservice_user_ips: await self.store.insert_client_ip( user_id=user_id, @@ -206,9 +202,7 @@ async def get_user_by_req( device_id="dummy-device", # stubbed ) - requester = synapse.types.create_requester( - user_id, app_service=app_service - ) + requester = create_requester(user_id, app_service=app_service) request.requester = user_id opentracing.set_tag("authenticated_entity", user_id) @@ -251,7 +245,7 @@ async def get_user_by_req( errcode=Codes.GUEST_ACCESS_FORBIDDEN, ) - requester = synapse.types.create_requester( + requester = create_requester( user_info.user_id, token_id, is_guest, @@ -271,7 +265,9 @@ async def get_user_by_req( except KeyError: raise MissingClientTokenError() - async def _get_appservice_user_id(self, request): + async def _get_appservice_user_id( + self, request: Request + ) -> Tuple[Optional[str], Optional[ApplicationService]]: app_service = self.store.get_app_service_by_token( self.get_access_token_from_request(request) ) @@ -283,6 +279,9 @@ async def _get_appservice_user_id(self, request): if ip_address not in app_service.ip_range_whitelist: return None, None + # This will always be set by the time Twisted calls us. + assert request.args is not None + if b"user_id" not in request.args: return app_service.sender, app_service @@ -387,7 +386,9 @@ async def get_user_by_access_token( logger.warning("Invalid macaroon in auth: %s %s", type(e), e) raise InvalidClientTokenError("Invalid macaroon passed.") - def _parse_and_validate_macaroon(self, token, rights="access"): + def _parse_and_validate_macaroon( + self, token: str, rights: str = "access" + ) -> Tuple[str, bool]: """Takes a macaroon and tries to parse and validate it. This is cached if and only if rights == access and there isn't an expiry. @@ -432,15 +433,16 @@ def _parse_and_validate_macaroon(self, token, rights="access"): return user_id, guest - def validate_macaroon(self, macaroon, type_string, user_id): + def validate_macaroon( + self, macaroon: pymacaroons.Macaroon, type_string: str, user_id: str + ) -> None: """ validate that a Macaroon is understood by and was signed by this server. Args: - macaroon(pymacaroons.Macaroon): The macaroon to validate - type_string(str): The kind of token required (e.g. "access", - "delete_pusher") - user_id (str): The user_id required + macaroon: The macaroon to validate + type_string: The kind of token required (e.g. "access", "delete_pusher") + user_id: The user_id required """ v = pymacaroons.Verifier() @@ -465,9 +467,7 @@ def get_appservice_by_req(self, request: SynapseRequest) -> ApplicationService: if not service: logger.warning("Unrecognised appservice access token.") raise InvalidClientTokenError() - request.requester = synapse.types.create_requester( - service.sender, app_service=service - ) + request.requester = create_requester(service.sender, app_service=service) return service async def is_server_admin(self, user: UserID) -> bool: @@ -519,7 +519,7 @@ def compute_auth_events( return auth_ids - async def check_can_change_room_list(self, room_id: str, user: UserID): + async def check_can_change_room_list(self, room_id: str, user: UserID) -> bool: """Determine whether the user is allowed to edit the room's entry in the published room list. @@ -554,11 +554,11 @@ async def check_can_change_room_list(self, room_id: str, user: UserID): return user_level >= send_level @staticmethod - def has_access_token(request: Request): + def has_access_token(request: Request) -> bool: """Checks if the request has an access_token. Returns: - bool: False if no access_token was given, True otherwise. + False if no access_token was given, True otherwise. """ # This will always be set by the time Twisted calls us. assert request.args is not None @@ -568,13 +568,13 @@ def has_access_token(request: Request): return bool(query_params) or bool(auth_headers) @staticmethod - def get_access_token_from_request(request: Request): + def get_access_token_from_request(request: Request) -> str: """Extracts the access_token from the request. Args: request: The http request. Returns: - unicode: The access_token + The access_token Raises: MissingClientTokenError: If there isn't a single access_token in the request @@ -649,5 +649,5 @@ async def check_user_in_room_or_world_readable( % (user_id, room_id), ) - def check_auth_blocking(self, *args, **kwargs): - return self._auth_blocking.check_auth_blocking(*args, **kwargs) + async def check_auth_blocking(self, *args, **kwargs) -> None: + await self._auth_blocking.check_auth_blocking(*args, **kwargs) diff --git a/synapse/api/auth_blocking.py b/synapse/api/auth_blocking.py index a8df60cb89..e6bced93d5 100644 --- a/synapse/api/auth_blocking.py +++ b/synapse/api/auth_blocking.py @@ -13,18 +13,21 @@ # limitations under the License. import logging -from typing import Optional +from typing import TYPE_CHECKING, Optional from synapse.api.constants import LimitBlockingTypes, UserTypes from synapse.api.errors import Codes, ResourceLimitError from synapse.config.server import is_threepid_reserved from synapse.types import Requester +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) class AuthBlocking: - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() self._server_notices_mxid = hs.config.server_notices_mxid @@ -43,7 +46,7 @@ async def check_auth_blocking( threepid: Optional[dict] = None, user_type: Optional[str] = None, requester: Optional[Requester] = None, - ): + ) -> None: """Checks if the user should be rejected for some external reason, such as monthly active user limiting or global disable flag diff --git a/synapse/event_auth.py b/synapse/event_auth.py index c831d9f73c..afc2bc8267 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -14,7 +14,7 @@ # limitations under the License. import logging -from typing import List, Optional, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple from canonicaljson import encode_canonical_json from signedjson.key import decode_verify_key_bytes @@ -688,7 +688,7 @@ def _verify_third_party_invite(event: EventBase, auth_events: StateMap[EventBase return False -def get_public_keys(invite_event): +def get_public_keys(invite_event: EventBase) -> List[Dict[str, Any]]: public_keys = [] if "public_key" in invite_event.content: o = {"public_key": invite_event.content["public_key"]} From 59d24c5bef4e05fa7be0cad1f7e63f0a0097374b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 23 Apr 2021 17:06:47 +0100 Subject: [PATCH 095/619] pass a reactor into SynapseSite (#9874) --- changelog.d/9874.misc | 1 + synapse/app/generic_worker.py | 1 + synapse/app/homeserver.py | 25 ++++++++++------------- synapse/http/site.py | 37 ++++++++++++++++++++++++++--------- tests/replication/_base.py | 1 + tests/test_server.py | 1 + tests/unittest.py | 1 + 7 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 changelog.d/9874.misc diff --git a/changelog.d/9874.misc b/changelog.d/9874.misc new file mode 100644 index 0000000000..ba1097e65e --- /dev/null +++ b/changelog.d/9874.misc @@ -0,0 +1 @@ +Pass a reactor into `SynapseSite` to make testing easier. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 7b2ac3ca64..70e07d0574 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -367,6 +367,7 @@ def _listen_http(self, listener_config: ListenerConfig): listener_config, root_resource, self.version_string, + reactor=self.get_reactor(), ), reactor=self.get_reactor(), ) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 8be8b520eb..140f6bcdee 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -126,19 +126,20 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf else: root_resource = OptionsResource() - root_resource = create_resource_tree(resources, root_resource) + site = SynapseSite( + "synapse.access.%s.%s" % ("https" if tls else "http", site_tag), + site_tag, + listener_config, + create_resource_tree(resources, root_resource), + self.version_string, + reactor=self.get_reactor(), + ) if tls: ports = listen_ssl( bind_addresses, port, - SynapseSite( - "synapse.access.https.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), + site, self.tls_server_context_factory, reactor=self.get_reactor(), ) @@ -148,13 +149,7 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf ports = listen_tcp( bind_addresses, port, - SynapseSite( - "synapse.access.http.%s" % (site_tag,), - site_tag, - listener_config, - root_resource, - self.version_string, - ), + site, reactor=self.get_reactor(), ) logger.info("Synapse now listening on TCP port %d", port) diff --git a/synapse/http/site.py b/synapse/http/site.py index 32b5e19c09..e911ee4809 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -19,8 +19,9 @@ import attr from zope.interface import implementer -from twisted.internet.interfaces import IAddress +from twisted.internet.interfaces import IAddress, IReactorTime from twisted.python.failure import Failure +from twisted.web.resource import IResource from twisted.web.server import Request, Site from synapse.config.server import ListenerConfig @@ -485,21 +486,39 @@ class _XForwardedForAddress: class SynapseSite(Site): """ - Subclass of a twisted http Site that does access logging with python's - standard logging + Synapse-specific twisted http Site + + This does two main things. + + First, it replaces the requestFactory in use so that we build SynapseRequests + instead of regular t.w.server.Requests. All of the constructor params are really + just parameters for SynapseRequest. + + Second, it inhibits the log() method called by Request.finish, since SynapseRequest + does its own logging. """ def __init__( self, - logger_name, - site_tag, + logger_name: str, + site_tag: str, config: ListenerConfig, - resource, + resource: IResource, server_version_string, - *args, - **kwargs, + reactor: IReactorTime, ): - Site.__init__(self, resource, *args, **kwargs) + """ + + Args: + logger_name: The name of the logger to use for access logs. + site_tag: A tag to use for this site - mostly in access logs. + config: Configuration for the HTTP listener corresponding to this site + resource: The base of the resource tree to be used for serving requests on + this site + server_version_string: A string to present for the Server header + reactor: reactor to be used to manage connection timeouts + """ + Site.__init__(self, resource, reactor=reactor) self.site_tag = site_tag diff --git a/tests/replication/_base.py b/tests/replication/_base.py index c9d04aef29..5cf58d8b60 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -349,6 +349,7 @@ def make_worker_hs( config=worker_hs.config.server.listeners[0], resource=resource, server_version_string="1", + reactor=self.reactor, ) if worker_hs.config.redis.redis_enabled: diff --git a/tests/test_server.py b/tests/test_server.py index 55cde7f62f..45400be367 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -202,6 +202,7 @@ def _make_request(self, method, path): parse_listener_def({"type": "http", "port": 0}), self.resource, "1.0", + reactor=self.reactor, ) # render the request and return the channel diff --git a/tests/unittest.py b/tests/unittest.py index ee22a53849..5353e75c7c 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -247,6 +247,7 @@ def setUp(self): config=self.hs.config.server.listeners[0], resource=self.resource, server_version_string="1", + reactor=self.reactor, ) from tests.rest.client.v1.utils import RestHelper From 695b73c861aa26ab591cad3f378214b2666e806e Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 23 Apr 2021 18:22:47 +0100 Subject: [PATCH 096/619] Allow OIDC cookies to work on non-root public baseurls (#9726) Applied a (slightly modified) patch from https://github.com/matrix-org/synapse/issues/9574. As far as I understand this would allow the cookie set during the OIDC flow to work on deployments using public baseurls that do not sit at the URL path root. --- changelog.d/9726.bugfix | 1 + synapse/config/server.py | 8 ++++---- synapse/handlers/oidc.py | 22 +++++++++++++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 changelog.d/9726.bugfix diff --git a/changelog.d/9726.bugfix b/changelog.d/9726.bugfix new file mode 100644 index 0000000000..4ba0b24327 --- /dev/null +++ b/changelog.d/9726.bugfix @@ -0,0 +1 @@ +Fixes the OIDC SSO flow when using a `public_baseurl` value including a non-root URL path. \ No newline at end of file diff --git a/synapse/config/server.py b/synapse/config/server.py index 02b86b11a5..21ca7b33e3 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -235,7 +235,11 @@ def read_config(self, config, **kwargs): self.print_pidfile = config.get("print_pidfile") self.user_agent_suffix = config.get("user_agent_suffix") self.use_frozen_dicts = config.get("use_frozen_dicts", False) + self.public_baseurl = config.get("public_baseurl") + if self.public_baseurl is not None: + if self.public_baseurl[-1] != "/": + self.public_baseurl += "/" # Whether to enable user presence. presence_config = config.get("presence") or {} @@ -407,10 +411,6 @@ def read_config(self, config, **kwargs): config_path=("federation_ip_range_blacklist",), ) - if self.public_baseurl is not None: - if self.public_baseurl[-1] != "/": - self.public_baseurl += "/" - # (undocumented) option for torturing the worker-mode replication a bit, # for testing. The value defines the number of milliseconds to pause before # sending out any replication updates. diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py index 1c4a43be0a..ee6e41c0e4 100644 --- a/synapse/handlers/oidc.py +++ b/synapse/handlers/oidc.py @@ -15,7 +15,7 @@ import inspect import logging from typing import TYPE_CHECKING, Dict, Generic, List, Optional, TypeVar, Union -from urllib.parse import urlencode +from urllib.parse import urlencode, urlparse import attr import pymacaroons @@ -68,8 +68,8 @@ # # Here we have the names of the cookies, and the options we use to set them. _SESSION_COOKIES = [ - (b"oidc_session", b"Path=/_synapse/client/oidc; HttpOnly; Secure; SameSite=None"), - (b"oidc_session_no_samesite", b"Path=/_synapse/client/oidc; HttpOnly"), + (b"oidc_session", b"HttpOnly; Secure; SameSite=None"), + (b"oidc_session_no_samesite", b"HttpOnly"), ] #: A token exchanged from the token endpoint, as per RFC6749 sec 5.1. and @@ -279,6 +279,13 @@ def __init__( self._config = provider self._callback_url = hs.config.oidc_callback_url # type: str + # Calculate the prefix for OIDC callback paths based on the public_baseurl. + # We'll insert this into the Path= parameter of any session cookies we set. + public_baseurl_path = urlparse(hs.config.server.public_baseurl).path + self._callback_path_prefix = ( + public_baseurl_path.encode("utf-8") + b"_synapse/client/oidc" + ) + self._oidc_attribute_requirements = provider.attribute_requirements self._scopes = provider.scopes self._user_profile_method = provider.user_profile_method @@ -779,8 +786,13 @@ async def handle_redirect_request( for cookie_name, options in _SESSION_COOKIES: request.cookies.append( - b"%s=%s; Max-Age=3600; %s" - % (cookie_name, cookie.encode("utf-8"), options) + b"%s=%s; Max-Age=3600; Path=%s; %s" + % ( + cookie_name, + cookie.encode("utf-8"), + self._callback_path_prefix, + options, + ) ) metadata = await self.load_metadata() From 84936e22648d3c9f6b76028b08c33f0267f5e3a0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 23 Apr 2021 18:40:57 +0100 Subject: [PATCH 097/619] Kill off `_PushHTTPChannel`. (#9878) First of all, a fixup to `FakeChannel` which is needed to make it work with the default HTTP channel implementation. Secondly, it looks like we no longer need `_PushHTTPChannel`, because as of #8013, the producer that gets attached to the `HTTPChannel` is now an `IPushProducer`. This is good, because it means we can remove a whole load of test-specific boilerplate which causes variation between tests and production. --- changelog.d/9878.misc | 1 + tests/replication/_base.py | 134 ++++++------------------------------- tests/server.py | 6 -- 3 files changed, 20 insertions(+), 121 deletions(-) create mode 100644 changelog.d/9878.misc diff --git a/changelog.d/9878.misc b/changelog.d/9878.misc new file mode 100644 index 0000000000..927876852d --- /dev/null +++ b/changelog.d/9878.misc @@ -0,0 +1 @@ +Remove redundant `_PushHTTPChannel` test class. diff --git a/tests/replication/_base.py b/tests/replication/_base.py index 5cf58d8b60..dc3519ea13 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -12,14 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Type +from typing import Any, Callable, Dict, List, Optional, Tuple -from twisted.internet.interfaces import IConsumer, IPullProducer, IReactorTime from twisted.internet.protocol import Protocol -from twisted.internet.task import LoopingCall -from twisted.web.http import HTTPChannel from twisted.web.resource import Resource -from twisted.web.server import Request, Site from synapse.app.generic_worker import GenericWorkerServer from synapse.http.server import JsonResource @@ -33,7 +29,6 @@ ServerReplicationStreamProtocol, ) from synapse.server import HomeServer -from synapse.util import Clock from tests import unittest from tests.server import FakeTransport @@ -154,7 +149,19 @@ def handle_http_replication_attempt(self) -> SynapseRequest: client_protocol = client_factory.buildProtocol(None) # Set up the server side protocol - channel = _PushHTTPChannel(self.reactor, SynapseRequest, self.site) + channel = self.site.buildProtocol(None) + + # hook into the channel's request factory so that we can keep a record + # of the requests + requests: List[SynapseRequest] = [] + real_request_factory = channel.requestFactory + + def request_factory(*args, **kwargs): + request = real_request_factory(*args, **kwargs) + requests.append(request) + return request + + channel.requestFactory = request_factory # Connect client to server and vice versa. client_to_server_transport = FakeTransport( @@ -176,7 +183,10 @@ def handle_http_replication_attempt(self) -> SynapseRequest: server_to_client_transport.loseConnection() client_to_server_transport.loseConnection() - return channel.request + # there should have been exactly one request + self.assertEqual(len(requests), 1) + + return requests[0] def assert_request_is_get_repl_stream_updates( self, request: SynapseRequest, stream_name: str @@ -387,7 +397,7 @@ def _handle_http_replication_attempt(self, hs, repl_port): client_protocol = client_factory.buildProtocol(None) # Set up the server side protocol - channel = _PushHTTPChannel(self.reactor, SynapseRequest, self._hs_to_site[hs]) + channel = self._hs_to_site[hs].buildProtocol(None) # Connect client to server and vice versa. client_to_server_transport = FakeTransport( @@ -445,112 +455,6 @@ async def on_rdata(self, stream_name, instance_name, token, rows): self.received_rdata_rows.append((stream_name, token, r)) -class _PushHTTPChannel(HTTPChannel): - """A HTTPChannel that wraps pull producers to push producers. - - This is a hack to get around the fact that HTTPChannel transparently wraps a - pull producer (which is what Synapse uses to reply to requests) with - `_PullToPush` to convert it to a push producer. Unfortunately `_PullToPush` - uses the standard reactor rather than letting us use our test reactor, which - makes it very hard to test. - """ - - def __init__( - self, reactor: IReactorTime, request_factory: Type[Request], site: Site - ): - super().__init__() - self.reactor = reactor - self.requestFactory = request_factory - self.site = site - - self._pull_to_push_producer = None # type: Optional[_PullToPushProducer] - - def registerProducer(self, producer, streaming): - # Convert pull producers to push producer. - if not streaming: - self._pull_to_push_producer = _PullToPushProducer( - self.reactor, producer, self - ) - producer = self._pull_to_push_producer - - super().registerProducer(producer, True) - - def unregisterProducer(self): - if self._pull_to_push_producer: - # We need to manually stop the _PullToPushProducer. - self._pull_to_push_producer.stop() - - def checkPersistence(self, request, version): - """Check whether the connection can be re-used""" - # We hijack this to always say no for ease of wiring stuff up in - # `handle_http_replication_attempt`. - request.responseHeaders.setRawHeaders(b"connection", [b"close"]) - return False - - def requestDone(self, request): - # Store the request for inspection. - self.request = request - super().requestDone(request) - - -class _PullToPushProducer: - """A push producer that wraps a pull producer.""" - - def __init__( - self, reactor: IReactorTime, producer: IPullProducer, consumer: IConsumer - ): - self._clock = Clock(reactor) - self._producer = producer - self._consumer = consumer - - # While running we use a looping call with a zero delay to call - # resumeProducing on given producer. - self._looping_call = None # type: Optional[LoopingCall] - - # We start writing next reactor tick. - self._start_loop() - - def _start_loop(self): - """Start the looping call to""" - - if not self._looping_call: - # Start a looping call which runs every tick. - self._looping_call = self._clock.looping_call(self._run_once, 0) - - def stop(self): - """Stops calling resumeProducing.""" - if self._looping_call: - self._looping_call.stop() - self._looping_call = None - - def pauseProducing(self): - """Implements IPushProducer""" - self.stop() - - def resumeProducing(self): - """Implements IPushProducer""" - self._start_loop() - - def stopProducing(self): - """Implements IPushProducer""" - self.stop() - self._producer.stopProducing() - - def _run_once(self): - """Calls resumeProducing on producer once.""" - - try: - self._producer.resumeProducing() - except Exception: - logger.exception("Failed to call resumeProducing") - try: - self._consumer.unregisterProducer() - except Exception: - pass - - self.stopProducing() - - class FakeRedisPubSubServer: """A fake Redis server for pub/sub.""" diff --git a/tests/server.py b/tests/server.py index b535a5d886..9df8cda24f 100644 --- a/tests/server.py +++ b/tests/server.py @@ -603,12 +603,6 @@ def flush(self, maxbytes=None): if self.disconnected: return - if not hasattr(self.other, "transport"): - # the other has no transport yet; reschedule - if self.autoflush: - self._reactor.callLater(0.0, self.flush) - return - if maxbytes is not None: to_write = self.buffer[:maxbytes] else: From 3ff225175462dde8376aa584e3a47c43b1f0e790 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 23 Apr 2021 19:20:44 +0100 Subject: [PATCH 098/619] Improved validation for received requests (#9817) * Simplify `start_listening` callpath * Correctly check the size of uploaded files --- changelog.d/9817.misc | 1 + synapse/api/constants.py | 3 + synapse/app/_base.py | 30 +++++++-- synapse/app/admin_cmd.py | 8 +-- synapse/app/generic_worker.py | 11 ++-- synapse/app/homeserver.py | 17 +++-- synapse/config/logger.py | 3 +- synapse/event_auth.py | 4 +- synapse/http/site.py | 32 +++++++-- synapse/rest/media/v1/upload_resource.py | 2 - synapse/server.py | 8 +++ tests/http/test_site.py | 83 ++++++++++++++++++++++++ tests/replication/_base.py | 1 + tests/test_server.py | 1 + tests/unittest.py | 1 + 15 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 changelog.d/9817.misc create mode 100644 tests/http/test_site.py diff --git a/changelog.d/9817.misc b/changelog.d/9817.misc new file mode 100644 index 0000000000..8aa8895f05 --- /dev/null +++ b/changelog.d/9817.misc @@ -0,0 +1 @@ +Fix a long-standing bug which caused `max_upload_size` to not be correctly enforced. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 31a59bceec..936b6534b4 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -17,6 +17,9 @@ """Contains constants from the specification.""" +# the max size of a (canonical-json-encoded) event +MAX_PDU_SIZE = 65536 + # the "depth" field on events is limited to 2**63 - 1 MAX_DEPTH = 2 ** 63 - 1 diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 2113c4f370..638e01c1b2 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -30,9 +30,10 @@ from twisted.protocols.tls import TLSMemoryBIOFactory import synapse +from synapse.api.constants import MAX_PDU_SIZE from synapse.app import check_bind_error from synapse.app.phone_stats_home import start_phone_stats_home -from synapse.config.server import ListenerConfig +from synapse.config.homeserver import HomeServerConfig from synapse.crypto import context_factory from synapse.logging.context import PreserveLoggingContext from synapse.metrics.background_process_metrics import wrap_as_background_process @@ -288,7 +289,7 @@ def refresh_certificate(hs): logger.info("Context factories updated.") -async def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerConfig]): +async def start(hs: "synapse.server.HomeServer"): """ Start a Synapse server or worker. @@ -300,7 +301,6 @@ async def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerCon Args: hs: homeserver instance - listeners: Listener configuration ('listeners' in homeserver.yaml) """ # Set up the SIGHUP machinery. if hasattr(signal, "SIGHUP"): @@ -336,7 +336,7 @@ def run_sighup(*args, **kwargs): synapse.logging.opentracing.init_tracer(hs) # type: ignore[attr-defined] # noqa # It is now safe to start your Synapse. - hs.start_listening(listeners) + hs.start_listening() hs.get_datastore().db_pool.start_profiling() hs.get_pusherpool().start() @@ -530,3 +530,25 @@ def sdnotify(state): # this is a bit surprising, since we don't expect to have a NOTIFY_SOCKET # unless systemd is expecting us to notify it. logger.warning("Unable to send notification to systemd: %s", e) + + +def max_request_body_size(config: HomeServerConfig) -> int: + """Get a suitable maximum size for incoming HTTP requests""" + + # Other than media uploads, the biggest request we expect to see is a fully-loaded + # /federation/v1/send request. + # + # The main thing in such a request is up to 50 PDUs, and up to 100 EDUs. PDUs are + # limited to 65536 bytes (possibly slightly more if the sender didn't use canonical + # json encoding); there is no specced limit to EDUs (see + # https://github.com/matrix-org/matrix-doc/issues/3121). + # + # in short, we somewhat arbitrarily limit requests to 200 * 64K (about 12.5M) + # + max_request_size = 200 * MAX_PDU_SIZE + + # if we have a media repo enabled, we may need to allow larger uploads than that + if config.media.can_load_media_repo: + max_request_size = max(max_request_size, config.media.max_upload_size) + + return max_request_size diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index eb256db749..68ae19c977 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -70,12 +70,6 @@ class AdminCmdSlavedStore( class AdminCmdServer(HomeServer): DATASTORE_CLASS = AdminCmdSlavedStore - def _listen_http(self, listener_config): - pass - - def start_listening(self, listeners): - pass - async def export_data_command(hs, args): """Export data for a user. @@ -232,7 +226,7 @@ def start(config_options): async def run(): with LoggingContext("command"): - _base.start(ss, []) + _base.start(ss) await args.func(ss, args) _base.start_worker_reactor( diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 70e07d0574..1a15ceee81 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -15,7 +15,7 @@ # limitations under the License. import logging import sys -from typing import Dict, Iterable, Optional +from typing import Dict, Optional from twisted.internet import address from twisted.web.resource import IResource @@ -32,7 +32,7 @@ SERVER_KEY_V2_PREFIX, ) from synapse.app import _base -from synapse.app._base import register_start +from synapse.app._base import max_request_body_size, register_start from synapse.config._base import ConfigError from synapse.config.homeserver import HomeServerConfig from synapse.config.logger import setup_logging @@ -367,6 +367,7 @@ def _listen_http(self, listener_config: ListenerConfig): listener_config, root_resource, self.version_string, + max_request_body_size=max_request_body_size(self.config), reactor=self.get_reactor(), ), reactor=self.get_reactor(), @@ -374,8 +375,8 @@ def _listen_http(self, listener_config: ListenerConfig): logger.info("Synapse worker now listening on port %d", port) - def start_listening(self, listeners: Iterable[ListenerConfig]): - for listener in listeners: + def start_listening(self): + for listener in self.config.worker_listeners: if listener.type == "http": self._listen_http(listener) elif listener.type == "manhole": @@ -468,7 +469,7 @@ def start(config_options): # streams. Will no-op if no streams can be written to by this worker. hs.get_replication_streamer() - register_start(_base.start, hs, config.worker_listeners) + register_start(_base.start, hs) _base.start_worker_reactor("synapse-generic-worker", config) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 140f6bcdee..8e78134bbe 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -17,7 +17,7 @@ import logging import os import sys -from typing import Iterable, Iterator +from typing import Iterator from twisted.internet import reactor from twisted.web.resource import EncodingResourceWrapper, IResource @@ -36,7 +36,13 @@ WEB_CLIENT_PREFIX, ) from synapse.app import _base -from synapse.app._base import listen_ssl, listen_tcp, quit_with_error, register_start +from synapse.app._base import ( + listen_ssl, + listen_tcp, + max_request_body_size, + quit_with_error, + register_start, +) from synapse.config._base import ConfigError from synapse.config.emailconfig import ThreepidBehaviour from synapse.config.homeserver import HomeServerConfig @@ -132,6 +138,7 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf listener_config, create_resource_tree(resources, root_resource), self.version_string, + max_request_body_size=max_request_body_size(self.config), reactor=self.get_reactor(), ) @@ -268,14 +275,14 @@ def _configure_named_resource(self, name, compress=False): return resources - def start_listening(self, listeners: Iterable[ListenerConfig]): + def start_listening(self): if self.config.redis_enabled: # If redis is enabled we connect via the replication command handler # in the same way as the workers (since we're effectively a client # rather than a server). self.get_tcp_replication().start_replication(self) - for listener in listeners: + for listener in self.config.server.listeners: if listener.type == "http": self._listening_services.extend( self._listener_http(self.config, listener) @@ -407,7 +414,7 @@ async def start(): # Loading the provider metadata also ensures the provider config is valid. await oidc.load_metadata() - await _base.start(hs, config.listeners) + await _base.start(hs) hs.get_datastore().db_pool.updates.start_doing_background_updates() diff --git a/synapse/config/logger.py b/synapse/config/logger.py index b174e0df6d..813076dfe2 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -31,7 +31,6 @@ ) import synapse -from synapse.app import _base as appbase from synapse.logging._structured import setup_structured_logging from synapse.logging.context import LoggingContextFilter from synapse.logging.filter import MetadataFilter @@ -318,6 +317,8 @@ def setup_logging( # Perform one-time logging configuration. _setup_stdlib_logging(config, log_config_path, logBeginner=logBeginner) # Add a SIGHUP handler to reload the logging configuration, if one is available. + from synapse.app import _base as appbase + appbase.register_sighup(_reload_logging_config, log_config_path) # Log immediately so we can grep backwards. diff --git a/synapse/event_auth.py b/synapse/event_auth.py index afc2bc8267..70c556566e 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -21,7 +21,7 @@ from signedjson.sign import SignatureVerifyException, verify_signed_json from unpaddedbase64 import decode_base64 -from synapse.api.constants import EventTypes, JoinRules, Membership +from synapse.api.constants import MAX_PDU_SIZE, EventTypes, JoinRules, Membership from synapse.api.errors import AuthError, EventSizeError, SynapseError from synapse.api.room_versions import ( KNOWN_ROOM_VERSIONS, @@ -205,7 +205,7 @@ def too_big(field): too_big("type") if len(event.event_id) > 255: too_big("event_id") - if len(encode_canonical_json(event.get_pdu_json())) > 65536: + if len(encode_canonical_json(event.get_pdu_json())) > MAX_PDU_SIZE: too_big("event") diff --git a/synapse/http/site.py b/synapse/http/site.py index e911ee4809..671fd3fbcc 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -14,7 +14,7 @@ import contextlib import logging import time -from typing import Optional, Tuple, Type, Union +from typing import Optional, Tuple, Union import attr from zope.interface import implementer @@ -50,6 +50,7 @@ class SynapseRequest(Request): * Redaction of access_token query-params in __repr__ * Logging at start and end * Metrics to record CPU, wallclock and DB time by endpoint. + * A limit to the size of request which will be accepted It also provides a method `processing`, which returns a context manager. If this method is called, the request won't be logged until the context manager is closed; @@ -60,8 +61,9 @@ class SynapseRequest(Request): logcontext: the log context for this request """ - def __init__(self, channel, *args, **kw): + def __init__(self, channel, *args, max_request_body_size=1024, **kw): Request.__init__(self, channel, *args, **kw) + self._max_request_body_size = max_request_body_size self.site = channel.site # type: SynapseSite self._channel = channel # this is used by the tests self.start_time = 0.0 @@ -98,6 +100,18 @@ def __repr__(self): self.site.site_tag, ) + def handleContentChunk(self, data): + # we should have a `content` by now. + assert self.content, "handleContentChunk() called before gotLength()" + if self.content.tell() + len(data) > self._max_request_body_size: + logger.warning( + "Aborting connection from %s because the request exceeds maximum size", + self.client, + ) + self.transport.abortConnection() + return + super().handleContentChunk(data) + @property def requester(self) -> Optional[Union[Requester, str]]: return self._requester @@ -505,6 +519,7 @@ def __init__( config: ListenerConfig, resource: IResource, server_version_string, + max_request_body_size: int, reactor: IReactorTime, ): """ @@ -516,6 +531,8 @@ def __init__( resource: The base of the resource tree to be used for serving requests on this site server_version_string: A string to present for the Server header + max_request_body_size: Maximum request body length to allow before + dropping the connection reactor: reactor to be used to manage connection timeouts """ Site.__init__(self, resource, reactor=reactor) @@ -524,9 +541,14 @@ def __init__( assert config.http_options is not None proxied = config.http_options.x_forwarded - self.requestFactory = ( - XForwardedForRequest if proxied else SynapseRequest - ) # type: Type[Request] + request_class = XForwardedForRequest if proxied else SynapseRequest + + def request_factory(channel, queued) -> Request: + return request_class( + channel, max_request_body_size=max_request_body_size, queued=queued + ) + + self.requestFactory = request_factory # type: ignore self.access_logger = logging.getLogger(logger_name) self.server_version_string = server_version_string.encode("ascii") diff --git a/synapse/rest/media/v1/upload_resource.py b/synapse/rest/media/v1/upload_resource.py index 80f017a4dd..024a105bf2 100644 --- a/synapse/rest/media/v1/upload_resource.py +++ b/synapse/rest/media/v1/upload_resource.py @@ -51,8 +51,6 @@ async def _async_render_OPTIONS(self, request: Request) -> None: async def _async_render_POST(self, request: SynapseRequest) -> None: requester = await self.auth.get_user_by_req(request) - # TODO: The checks here are a bit late. The content will have - # already been uploaded to a tmp file at this point content_length = request.getHeader("Content-Length") if content_length is None: raise SynapseError(msg="Request must specify a Content-Length", code=400) diff --git a/synapse/server.py b/synapse/server.py index 8c147be2b3..06570bb1ce 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -287,6 +287,14 @@ def setup(self) -> None: if self.config.run_background_tasks: self.setup_background_tasks() + def start_listening(self) -> None: + """Start the HTTP, manhole, metrics, etc listeners + + Does nothing in this base class; overridden in derived classes to start the + appropriate listeners. + """ + pass + def setup_background_tasks(self) -> None: """ Some handlers have side effects on instantiation (like registering diff --git a/tests/http/test_site.py b/tests/http/test_site.py new file mode 100644 index 0000000000..8c13b4f693 --- /dev/null +++ b/tests/http/test_site.py @@ -0,0 +1,83 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet.address import IPv6Address +from twisted.test.proto_helpers import StringTransport + +from synapse.app.homeserver import SynapseHomeServer + +from tests.unittest import HomeserverTestCase + + +class SynapseRequestTestCase(HomeserverTestCase): + def make_homeserver(self, reactor, clock): + return self.setup_test_homeserver(homeserver_to_use=SynapseHomeServer) + + def test_large_request(self): + """overlarge HTTP requests should be rejected""" + self.hs.start_listening() + + # find the HTTP server which is configured to listen on port 0 + (port, factory, _backlog, interface) = self.reactor.tcpServers[0] + self.assertEqual(interface, "::") + self.assertEqual(port, 0) + + # as a control case, first send a regular request. + + # complete the connection and wire it up to a fake transport + client_address = IPv6Address("TCP", "::1", "2345") + protocol = factory.buildProtocol(client_address) + transport = StringTransport() + protocol.makeConnection(transport) + + protocol.dataReceived( + b"POST / HTTP/1.1\r\n" + b"Connection: close\r\n" + b"Transfer-Encoding: chunked\r\n" + b"\r\n" + b"0\r\n" + b"\r\n" + ) + + while not transport.disconnecting: + self.reactor.advance(1) + + # we should get a 404 + self.assertRegex(transport.value().decode(), r"^HTTP/1\.1 404 ") + + # now send an oversized request + protocol = factory.buildProtocol(client_address) + transport = StringTransport() + protocol.makeConnection(transport) + + protocol.dataReceived( + b"POST / HTTP/1.1\r\n" + b"Connection: close\r\n" + b"Transfer-Encoding: chunked\r\n" + b"\r\n" + ) + + # we deliberately send all the data in one big chunk, to ensure that + # twisted isn't buffering the data in the chunked transfer decoder. + # we start with the chunk size, in hex. (We won't actually send this much) + protocol.dataReceived(b"10000000\r\n") + sent = 0 + while not transport.disconnected: + self.assertLess(sent, 0x10000000, "connection did not drop") + protocol.dataReceived(b"\0" * 1024) + sent += 1024 + + # default max upload size is 50M, so it should drop on the next buffer after + # that. + self.assertEqual(sent, 50 * 1024 * 1024 + 1024) diff --git a/tests/replication/_base.py b/tests/replication/_base.py index dc3519ea13..624bd1b927 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -359,6 +359,7 @@ def make_worker_hs( config=worker_hs.config.server.listeners[0], resource=resource, server_version_string="1", + max_request_body_size=4096, reactor=self.reactor, ) diff --git a/tests/test_server.py b/tests/test_server.py index 45400be367..407e172e41 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -202,6 +202,7 @@ def _make_request(self, method, path): parse_listener_def({"type": "http", "port": 0}), self.resource, "1.0", + max_request_body_size=1234, reactor=self.reactor, ) diff --git a/tests/unittest.py b/tests/unittest.py index 5353e75c7c..9bd02bd9c4 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -247,6 +247,7 @@ def setUp(self): config=self.hs.config.server.listeners[0], resource=self.resource, server_version_string="1", + max_request_body_size=1234, reactor=self.reactor, ) From 0ffa5fb935ac9285217d957403861d2e3327e109 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 27 Apr 2021 10:09:41 +0100 Subject: [PATCH 099/619] Use current state table for `presence.get_interested_remotes` (#9887) This should be a lot quicker than asking the state handler. --- changelog.d/9887.misc | 1 + synapse/handlers/presence.py | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) create mode 100644 changelog.d/9887.misc diff --git a/changelog.d/9887.misc b/changelog.d/9887.misc new file mode 100644 index 0000000000..650ebf85e6 --- /dev/null +++ b/changelog.d/9887.misc @@ -0,0 +1 @@ +Small performance improvement around handling new local presence updates. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 9938be3821..969c73c1e7 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -58,7 +58,6 @@ from synapse.replication.http.streams import ReplicationGetStreamUpdates from synapse.replication.tcp.commands import ClearUserSyncsCommand from synapse.replication.tcp.streams import PresenceFederationStream, PresenceStream -from synapse.state import StateHandler from synapse.storage.databases.main import DataStore from synapse.types import JsonDict, UserID, get_domain_from_id from synapse.util.async_helpers import Linearizer @@ -291,7 +290,6 @@ async def maybe_send_presence_to_interested_destinations( self.store, self.presence_router, states, - self.state, ) for destinations, states in hosts_and_states: @@ -757,7 +755,6 @@ async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None: self.store, self.presence_router, list(to_federation_ping.values()), - self.state, ) for destinations, states in hosts_and_states: @@ -1384,7 +1381,6 @@ def __init__(self, hs: "HomeServer"): self.get_presence_router = hs.get_presence_router self.clock = hs.get_clock() self.store = hs.get_datastore() - self.state = hs.get_state_handler() @log_function async def get_new_events( @@ -1853,7 +1849,6 @@ async def get_interested_remotes( store: DataStore, presence_router: PresenceRouter, states: List[UserPresenceState], - state_handler: StateHandler, ) -> List[Tuple[Collection[str], List[UserPresenceState]]]: """Given a list of presence states figure out which remote servers should be sent which. @@ -1864,7 +1859,6 @@ async def get_interested_remotes( store: The homeserver's data store. presence_router: A module for augmenting the destinations for presence updates. states: A list of incoming user presence updates. - state_handler: Returns: A list of 2-tuples of destinations and states, where for @@ -1881,7 +1875,8 @@ async def get_interested_remotes( ) for room_id, states in room_ids_to_states.items(): - hosts = await state_handler.get_current_hosts_in_room(room_id) + user_ids = await store.get_users_in_room(room_id) + hosts = {get_domain_from_id(user_id) for user_id in user_ids} hosts_and_states.append((hosts, states)) for user_id, states in users_to_states.items(): From 1350b053da45c94722cd8acf9cfd367db787259c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 27 Apr 2021 07:30:34 -0400 Subject: [PATCH 100/619] Pass errors back to the client when trying multiple federation destinations. (#9868) This ensures that something like an auth error (403) will be returned to the requester instead of attempting to try more servers, which will likely result in the same error, and then passing back a generic 400 error. --- changelog.d/9868.bugfix | 1 + synapse/federation/federation_client.py | 118 ++++++++++++------------ 2 files changed, 61 insertions(+), 58 deletions(-) create mode 100644 changelog.d/9868.bugfix diff --git a/changelog.d/9868.bugfix b/changelog.d/9868.bugfix new file mode 100644 index 0000000000..e2b4f97ad5 --- /dev/null +++ b/changelog.d/9868.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where errors from federation did not propagate to the client. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index f93335edaa..a5b6a61195 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -451,6 +451,28 @@ async def get_event_auth( return signed_auth + def _is_unknown_endpoint( + self, e: HttpResponseException, synapse_error: Optional[SynapseError] = None + ) -> bool: + """ + Returns true if the response was due to an endpoint being unimplemented. + + Args: + e: The error response received from the remote server. + synapse_error: The above error converted to a SynapseError. This is + automatically generated if not provided. + + """ + if synapse_error is None: + synapse_error = e.to_synapse_error() + # There is no good way to detect an "unknown" endpoint. + # + # Dendrite returns a 404 (with no body); synapse returns a 400 + # with M_UNRECOGNISED. + return e.code == 404 or ( + e.code == 400 and synapse_error.errcode == Codes.UNRECOGNIZED + ) + async def _try_destination_list( self, description: str, @@ -468,9 +490,9 @@ async def _try_destination_list( callback: Function to run for each server. Passed a single argument: the server_name to try. - If the callback raises a CodeMessageException with a 300/400 code, - attempts to perform the operation stop immediately and the exception is - reraised. + If the callback raises a CodeMessageException with a 300/400 code or + an UnsupportedRoomVersionError, attempts to perform the operation + stop immediately and the exception is reraised. Otherwise, if the callback raises an Exception the error is logged and the next server tried. Normally the stacktrace is logged but this is @@ -492,8 +514,7 @@ async def _try_destination_list( continue try: - res = await callback(destination) - return res + return await callback(destination) except InvalidResponseError as e: logger.warning("Failed to %s via %s: %s", description, destination, e) except UnsupportedRoomVersionError: @@ -502,17 +523,15 @@ async def _try_destination_list( synapse_error = e.to_synapse_error() failover = False + # Failover on an internal server error, or if the destination + # doesn't implemented the endpoint for some reason. if 500 <= e.code < 600: failover = True - elif failover_on_unknown_endpoint: - # there is no good way to detect an "unknown" endpoint. Dendrite - # returns a 404 (with no body); synapse returns a 400 - # with M_UNRECOGNISED. - if e.code == 404 or ( - e.code == 400 and synapse_error.errcode == Codes.UNRECOGNIZED - ): - failover = True + elif failover_on_unknown_endpoint and self._is_unknown_endpoint( + e, synapse_error + ): + failover = True if not failover: raise synapse_error from e @@ -570,9 +589,8 @@ async def make_membership_event( UnsupportedRoomVersionError: if remote responds with a room version we don't understand. - SynapseError: if the chosen remote server returns a 300/400 code. - - RuntimeError: if no servers were reachable. + SynapseError: if the chosen remote server returns a 300/400 code, or + no servers successfully handle the request. """ valid_memberships = {Membership.JOIN, Membership.LEAVE} if membership not in valid_memberships: @@ -642,9 +660,8 @@ async def send_join( ``auth_chain``. Raises: - SynapseError: if the chosen remote server returns a 300/400 code. - - RuntimeError: if no servers were reachable. + SynapseError: if the chosen remote server returns a 300/400 code, or + no servers successfully handle the request. """ async def send_request(destination) -> Dict[str, Any]: @@ -673,7 +690,7 @@ async def send_request(destination) -> Dict[str, Any]: if create_event is None: # If the state doesn't have a create event then the room is # invalid, and it would fail auth checks anyway. - raise SynapseError(400, "No create event in state") + raise InvalidResponseError("No create event in state") # the room version should be sane. create_room_version = create_event.content.get( @@ -746,16 +763,11 @@ async def _do_send_join(self, destination: str, pdu: EventBase) -> JsonDict: content=pdu.get_pdu_json(time_now), ) except HttpResponseException as e: - if e.code in [400, 404]: - err = e.to_synapse_error() - - # If we receive an error response that isn't a generic error, or an - # unrecognised endpoint error, we assume that the remote understands - # the v2 invite API and this is a legitimate error. - if err.errcode not in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: - raise err - else: - raise e.to_synapse_error() + # If an error is received that is due to an unrecognised endpoint, + # fallback to the v1 endpoint. Otherwise consider it a legitmate error + # and raise. + if not self._is_unknown_endpoint(e): + raise logger.debug("Couldn't send_join with the v2 API, falling back to the v1 API") @@ -802,6 +814,11 @@ async def _do_send_invite( Returns: The event as a dict as returned by the remote server + + Raises: + SynapseError: if the remote server returns an error or if the server + only supports the v1 endpoint and a room version other than "1" + or "2" is requested. """ time_now = self._clock.time_msec() @@ -817,28 +834,19 @@ async def _do_send_invite( }, ) except HttpResponseException as e: - if e.code in [400, 404]: - err = e.to_synapse_error() - - # If we receive an error response that isn't a generic error, we - # assume that the remote understands the v2 invite API and this - # is a legitimate error. - if err.errcode != Codes.UNKNOWN: - raise err - - # Otherwise, we assume that the remote server doesn't understand - # the v2 invite API. That's ok provided the room uses old-style event - # IDs. + # If an error is received that is due to an unrecognised endpoint, + # fallback to the v1 endpoint if the room uses old-style event IDs. + # Otherwise consider it a legitmate error and raise. + err = e.to_synapse_error() + if self._is_unknown_endpoint(e, err): if room_version.event_format != EventFormatVersions.V1: raise SynapseError( 400, "User's homeserver does not support this room version", Codes.UNSUPPORTED_ROOM_VERSION, ) - elif e.code in (403, 429): - raise e.to_synapse_error() else: - raise + raise err # Didn't work, try v1 API. # Note the v1 API returns a tuple of `(200, content)` @@ -865,9 +873,8 @@ async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None: pdu: event to be sent Raises: - SynapseError if the chosen remote server returns a 300/400 code. - - RuntimeError if no servers were reachable. + SynapseError: if the chosen remote server returns a 300/400 code, or + no servers successfully handle the request. """ async def send_request(destination: str) -> None: @@ -889,16 +896,11 @@ async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict: content=pdu.get_pdu_json(time_now), ) except HttpResponseException as e: - if e.code in [400, 404]: - err = e.to_synapse_error() - - # If we receive an error response that isn't a generic error, or an - # unrecognised endpoint error, we assume that the remote understands - # the v2 invite API and this is a legitimate error. - if err.errcode not in [Codes.UNKNOWN, Codes.UNRECOGNIZED]: - raise err - else: - raise e.to_synapse_error() + # If an error is received that is due to an unrecognised endpoint, + # fallback to the v1 endpoint. Otherwise consider it a legitmate error + # and raise. + if not self._is_unknown_endpoint(e): + raise logger.debug("Couldn't send_leave with the v2 API, falling back to the v1 API") From fe604a022a7142157da7e90a40330beb2a11af7a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 27 Apr 2021 13:13:07 +0100 Subject: [PATCH 101/619] Remove various bits of compatibility code for Python <3.6 (#9879) I went through and removed a bunch of cruft that was lying around for compatibility with old Python versions. This PR also will now prevent Synapse from starting unless you're running Python 3.6+. --- changelog.d/9879.misc | 1 + mypy.ini | 1 - synapse/__init__.py | 4 +-- synapse/python_dependencies.py | 9 ++--- synapse/rest/admin/users.py | 3 +- synapse/rest/consent/consent_resource.py | 10 +----- synapse/rest/media/v1/filepath.py | 2 +- synapse/secrets.py | 44 ------------------------ synapse/server.py | 5 --- synapse/storage/_base.py | 2 +- synapse/storage/database.py | 15 ++++---- synapse/util/caches/response_cache.py | 2 +- tests/rest/admin/test_user.py | 15 ++++---- tests/storage/test__base.py | 3 +- tests/unittest.py | 2 +- tox.ini | 9 ++--- 16 files changed, 29 insertions(+), 98 deletions(-) create mode 100644 changelog.d/9879.misc delete mode 100644 synapse/secrets.py diff --git a/changelog.d/9879.misc b/changelog.d/9879.misc new file mode 100644 index 0000000000..c9ca37cf48 --- /dev/null +++ b/changelog.d/9879.misc @@ -0,0 +1 @@ +Remove backwards-compatibility code for Python versions < 3.6. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 32e6197409..a40f705b76 100644 --- a/mypy.ini +++ b/mypy.ini @@ -41,7 +41,6 @@ files = synapse/push, synapse/replication, synapse/rest, - synapse/secrets.py, synapse/server.py, synapse/server_notices, synapse/spam_checker_api, diff --git a/synapse/__init__.py b/synapse/__init__.py index 837e938f56..fbd49a93e1 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -21,8 +21,8 @@ import sys # Check that we're not running on an unsupported Python version. -if sys.version_info < (3, 5): - print("Synapse requires Python 3.5 or above.") +if sys.version_info < (3, 6): + print("Synapse requires Python 3.6 or above.") sys.exit(1) # Twisted and canonicaljson will fail to import when this file is executed to diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 2a1c925ee8..2de946f464 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -85,7 +85,7 @@ "typing-extensions>=3.7.4", # We enforce that we have a `cryptography` version that bundles an `openssl` # with the latest security patches. - "cryptography>=3.4.7;python_version>='3.6'", + "cryptography>=3.4.7", ] CONDITIONAL_REQUIREMENTS = { @@ -100,14 +100,9 @@ # that use the protocol, such as Let's Encrypt. "acme": [ "txacme>=0.9.2", - # txacme depends on eliot. Eliot 1.8.0 is incompatible with - # python 3.5.2, as per https://github.com/itamarst/eliot/issues/418 - "eliot<1.8.0;python_version<'3.5.3'", ], "saml2": [ - # pysaml2 6.4.0 is incompatible with Python 3.5 (see https://github.com/IdentityPython/pysaml2/issues/749) - "pysaml2>=4.5.0,<6.4.0;python_version<'3.6'", - "pysaml2>=4.5.0;python_version>='3.6'", + "pysaml2>=4.5.0", ], "oidc": ["authlib>=0.14.0"], # systemd-python is necessary for logging to the systemd journal via diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index edda7861fa..8c9d21d3ea 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -14,6 +14,7 @@ import hashlib import hmac import logging +import secrets from http import HTTPStatus from typing import TYPE_CHECKING, Dict, List, Optional, Tuple @@ -375,7 +376,7 @@ def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: """ self._clear_old_nonces() - nonce = self.hs.get_secrets().token_hex(64) + nonce = secrets.token_hex(64) self.nonces[nonce] = int(self.reactor.seconds()) return 200, {"nonce": nonce} diff --git a/synapse/rest/consent/consent_resource.py b/synapse/rest/consent/consent_resource.py index c4550d3cf0..b19cd8afc5 100644 --- a/synapse/rest/consent/consent_resource.py +++ b/synapse/rest/consent/consent_resource.py @@ -32,14 +32,6 @@ logger = logging.getLogger(__name__) -# use hmac.compare_digest if we have it (python 2.7.7), else just use equality -if hasattr(hmac, "compare_digest"): - compare_digest = hmac.compare_digest -else: - - def compare_digest(a, b): - return a == b - class ConsentResource(DirectServeHtmlResource): """A twisted Resource to display a privacy policy and gather consent to it @@ -209,5 +201,5 @@ def _check_hash(self, userid, userhmac): .encode("ascii") ) - if not compare_digest(want_mac, userhmac): + if not hmac.compare_digest(want_mac, userhmac): raise SynapseError(HTTPStatus.FORBIDDEN, "HMAC incorrect") diff --git a/synapse/rest/media/v1/filepath.py b/synapse/rest/media/v1/filepath.py index 4088e7a059..09531ebf54 100644 --- a/synapse/rest/media/v1/filepath.py +++ b/synapse/rest/media/v1/filepath.py @@ -21,7 +21,7 @@ NEW_FORMAT_ID_RE = re.compile(r"^\d\d\d\d-\d\d-\d\d") -def _wrap_in_base_path(func: "Callable[..., str]") -> "Callable[..., str]": +def _wrap_in_base_path(func: Callable[..., str]) -> Callable[..., str]: """Takes a function that returns a relative path and turns it into an absolute path based on the location of the primary media store """ diff --git a/synapse/secrets.py b/synapse/secrets.py deleted file mode 100644 index bf829251fd..0000000000 --- a/synapse/secrets.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2018 New Vector 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. - -""" -Injectable secrets module for Synapse. - -See https://docs.python.org/3/library/secrets.html#module-secrets for the API -used in Python 3.6, and the API emulated in Python 2.7. -""" -import sys - -# secrets is available since python 3.6 -if sys.version_info[0:2] >= (3, 6): - import secrets - - class Secrets: - def token_bytes(self, nbytes: int = 32) -> bytes: - return secrets.token_bytes(nbytes) - - def token_hex(self, nbytes: int = 32) -> str: - return secrets.token_hex(nbytes) - - -else: - import binascii - import os - - class Secrets: - def token_bytes(self, nbytes: int = 32) -> bytes: - return os.urandom(nbytes) - - def token_hex(self, nbytes: int = 32) -> str: - return binascii.hexlify(self.token_bytes(nbytes)).decode("ascii") diff --git a/synapse/server.py b/synapse/server.py index 06570bb1ce..2337d2d9b4 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -126,7 +126,6 @@ MediaRepository, MediaRepositoryResource, ) -from synapse.secrets import Secrets from synapse.server_notices.server_notices_manager import ServerNoticesManager from synapse.server_notices.server_notices_sender import ServerNoticesSender from synapse.server_notices.worker_server_notices_sender import ( @@ -641,10 +640,6 @@ def get_groups_attestation_signing(self) -> GroupAttestationSigning: def get_groups_attestation_renewer(self) -> GroupAttestionRenewer: return GroupAttestionRenewer(self) - @cache_in_self - def get_secrets(self) -> Secrets: - return Secrets() - @cache_in_self def get_stats_handler(self) -> StatsHandler: return StatsHandler(self) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index d472676acf..6b68d8720c 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -114,7 +114,7 @@ def db_to_json(db_content: Union[memoryview, bytes, bytearray, str]) -> Any: db_content = db_content.tobytes() # Decode it to a Unicode string before feeding it to the JSON decoder, since - # Python 3.5 does not support deserializing bytes. + # it only supports handling strings if isinstance(db_content, (bytes, bytearray)): db_content = db_content.decode("utf8") diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 9452368bf0..bd39c095af 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -171,10 +171,7 @@ def __getattr__(self, name): # The type of entry which goes on our after_callbacks and exception_callbacks lists. -# -# Python 3.5.2 doesn't support Callable with an ellipsis, so we wrap it in quotes so -# that mypy sees the type but the runtime python doesn't. -_CallbackListEntry = Tuple["Callable[..., None]", Iterable[Any], Dict[str, Any]] +_CallbackListEntry = Tuple[Callable[..., None], Iterable[Any], Dict[str, Any]] R = TypeVar("R") @@ -221,7 +218,7 @@ def __init__( self.after_callbacks = after_callbacks self.exception_callbacks = exception_callbacks - def call_after(self, callback: "Callable[..., None]", *args: Any, **kwargs: Any): + def call_after(self, callback: Callable[..., None], *args: Any, **kwargs: Any): """Call the given callback on the main twisted thread after the transaction has finished. Used to invalidate the caches on the correct thread. @@ -233,7 +230,7 @@ def call_after(self, callback: "Callable[..., None]", *args: Any, **kwargs: Any) self.after_callbacks.append((callback, args, kwargs)) def call_on_exception( - self, callback: "Callable[..., None]", *args: Any, **kwargs: Any + self, callback: Callable[..., None], *args: Any, **kwargs: Any ): # if self.exception_callbacks is None, that means that whatever constructed the # LoggingTransaction isn't expecting there to be any callbacks; assert that @@ -485,7 +482,7 @@ def new_transaction( desc: str, after_callbacks: List[_CallbackListEntry], exception_callbacks: List[_CallbackListEntry], - func: "Callable[..., R]", + func: Callable[..., R], *args: Any, **kwargs: Any, ) -> R: @@ -618,7 +615,7 @@ def new_transaction( async def runInteraction( self, desc: str, - func: "Callable[..., R]", + func: Callable[..., R], *args: Any, db_autocommit: bool = False, **kwargs: Any, @@ -678,7 +675,7 @@ async def runInteraction( async def runWithConnection( self, - func: "Callable[..., R]", + func: Callable[..., R], *args: Any, db_autocommit: bool = False, **kwargs: Any, diff --git a/synapse/util/caches/response_cache.py b/synapse/util/caches/response_cache.py index 2529845c9e..25ea1bcc91 100644 --- a/synapse/util/caches/response_cache.py +++ b/synapse/util/caches/response_cache.py @@ -110,7 +110,7 @@ def remove(r): return result.observe() def wrap( - self, key: T, callback: "Callable[..., Any]", *args: Any, **kwargs: Any + self, key: T, callback: Callable[..., Any], *args: Any, **kwargs: Any ) -> defer.Deferred: """Wrap together a *get* and *set* call, taking care of logcontexts diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index b3afd51522..d599a4c984 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -18,7 +18,7 @@ import urllib.parse from binascii import unhexlify from typing import List, Optional -from unittest.mock import Mock +from unittest.mock import Mock, patch import synapse.rest.admin from synapse.api.constants import UserTypes @@ -54,8 +54,6 @@ def make_homeserver(self, reactor, clock): self.datastore = Mock(return_value=Mock()) self.datastore.get_current_state_deltas = Mock(return_value=(0, [])) - self.secrets = Mock() - self.hs = self.setup_test_homeserver() self.hs.config.registration_shared_secret = "shared" @@ -84,14 +82,13 @@ def test_get_nonce(self): Calling GET on the endpoint will return a randomised nonce, using the homeserver's secrets provider. """ - secrets = Mock() - secrets.token_hex = Mock(return_value="abcd") - - self.hs.get_secrets = Mock(return_value=secrets) + with patch("secrets.token_hex") as token_hex: + # Patch secrets.token_hex for the duration of this context + token_hex.return_value = "abcd" - channel = self.make_request("GET", self.url) + channel = self.make_request("GET", self.url) - self.assertEqual(channel.json_body, {"nonce": "abcd"}) + self.assertEqual(channel.json_body, {"nonce": "abcd"}) def test_expired_nonce(self): """ diff --git a/tests/storage/test__base.py b/tests/storage/test__base.py index 6339a43f0c..200b9198f9 100644 --- a/tests/storage/test__base.py +++ b/tests/storage/test__base.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import secrets from tests import unittest @@ -21,7 +22,7 @@ class UpsertManyTests(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): self.storage = hs.get_datastore() - self.table_name = "table_" + hs.get_secrets().token_hex(6) + self.table_name = "table_" + secrets.token_hex(6) self.get_success( self.storage.db_pool.runInteraction( "create", diff --git a/tests/unittest.py b/tests/unittest.py index 9bd02bd9c4..74db7c08f1 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -18,6 +18,7 @@ import hmac import inspect import logging +import secrets import time from typing import Callable, Dict, Iterable, Optional, Tuple, Type, TypeVar, Union from unittest.mock import Mock, patch @@ -626,7 +627,6 @@ def create_and_send_event( str: The new event's ID. """ event_creator = self.hs.get_event_creation_handler() - secrets = self.hs.get_secrets() requester = create_requester(user) event, context = self.get_success( diff --git a/tox.ini b/tox.ini index 998b04b224..ecd609271d 100644 --- a/tox.ini +++ b/tox.ini @@ -21,13 +21,11 @@ deps = # installed on that). # # anyway, make sure that we have a recent enough setuptools. - setuptools>=18.5 ; python_version >= '3.6' - setuptools>=18.5,<51.0.0 ; python_version < '3.6' + setuptools>=18.5 # we also need a semi-recent version of pip, because old ones fail to # install the "enum34" dependency of cryptography. - pip>=10 ; python_version >= '3.6' - pip>=10,<21.0 ; python_version < '3.6' + pip>=10 # directories/files we run the linters on. # if you update this list, make sure to do the same in scripts-dev/lint.sh @@ -168,8 +166,7 @@ skip_install = true usedevelop = false deps = coverage - pip>=10 ; python_version >= '3.6' - pip>=10,<21.0 ; python_version < '3.6' + pip>=10 commands= coverage combine coverage report From dd2d32dcdb3238735aeeeaff18e5c754b1d50be9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 28 Apr 2021 11:07:47 +0100 Subject: [PATCH 102/619] Add type hints to presence handler (#9885) --- changelog.d/9885.misc | 1 + synapse/handlers/presence.py | 159 ++++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 70 deletions(-) create mode 100644 changelog.d/9885.misc diff --git a/changelog.d/9885.misc b/changelog.d/9885.misc new file mode 100644 index 0000000000..492fccea46 --- /dev/null +++ b/changelog.d/9885.misc @@ -0,0 +1 @@ +Add type hints to presence handler. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 969c73c1e7..e9f618bb5a 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -28,6 +28,7 @@ from contextlib import contextmanager from typing import ( TYPE_CHECKING, + Callable, Collection, Dict, FrozenSet, @@ -232,23 +233,23 @@ async def bump_presence_active_time(self, user: UserID): """ async def update_external_syncs_row( - self, process_id, user_id, is_syncing, sync_time_msec - ): + self, process_id: str, user_id: str, is_syncing: bool, sync_time_msec: int + ) -> None: """Update the syncing users for an external process as a delta. This is a no-op when presence is handled by a different worker. Args: - process_id (str): An identifier for the process the users are + process_id: An identifier for the process the users are syncing against. This allows synapse to process updates as user start and stop syncing against a given process. - user_id (str): The user who has started or stopped syncing - is_syncing (bool): Whether or not the user is now syncing - sync_time_msec(int): Time in ms when the user was last syncing + user_id: The user who has started or stopped syncing + is_syncing: Whether or not the user is now syncing + sync_time_msec: Time in ms when the user was last syncing """ pass - async def update_external_syncs_clear(self, process_id): + async def update_external_syncs_clear(self, process_id: str) -> None: """Marks all users that had been marked as syncing by a given process as offline. @@ -304,7 +305,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): class WorkerPresenceHandler(BasePresenceHandler): - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__(hs) self.hs = hs @@ -327,7 +328,7 @@ def __init__(self, hs): # user_id -> last_sync_ms. Lists the users that have stopped syncing but # we haven't notified the presence writer of that yet - self.users_going_offline = {} + self.users_going_offline = {} # type: Dict[str, int] self._bump_active_client = ReplicationBumpPresenceActiveTime.make_client(hs) self._set_state_client = ReplicationPresenceSetState.make_client(hs) @@ -346,24 +347,21 @@ def __init__(self, hs): self._on_shutdown, ) - def _on_shutdown(self): + def _on_shutdown(self) -> None: if self._presence_enabled: self.hs.get_tcp_replication().send_command( ClearUserSyncsCommand(self.instance_id) ) - def send_user_sync(self, user_id, is_syncing, last_sync_ms): + def send_user_sync(self, user_id: str, is_syncing: bool, last_sync_ms: int) -> None: if self._presence_enabled: self.hs.get_tcp_replication().send_user_sync( self.instance_id, user_id, is_syncing, last_sync_ms ) - def mark_as_coming_online(self, user_id): + def mark_as_coming_online(self, user_id: str) -> None: """A user has started syncing. Send a UserSync to the presence writer, unless they had recently stopped syncing. - - Args: - user_id (str) """ going_offline = self.users_going_offline.pop(user_id, None) if not going_offline: @@ -371,18 +369,15 @@ def mark_as_coming_online(self, user_id): # were offline self.send_user_sync(user_id, True, self.clock.time_msec()) - def mark_as_going_offline(self, user_id): + def mark_as_going_offline(self, user_id: str) -> None: """A user has stopped syncing. We wait before notifying the presence writer as its likely they'll come back soon. This allows us to avoid sending a stopped syncing immediately followed by a started syncing notification to the presence writer - - Args: - user_id (str) """ self.users_going_offline[user_id] = self.clock.time_msec() - def send_stop_syncing(self): + def send_stop_syncing(self) -> None: """Check if there are any users who have stopped syncing a while ago and haven't come back yet. If there are poke the presence writer about them. """ @@ -430,7 +425,9 @@ def _user_syncing(): return _user_syncing() - async def notify_from_replication(self, states, stream_id): + async def notify_from_replication( + self, states: List[UserPresenceState], stream_id: int + ) -> None: parties = await get_interested_parties(self.store, self.presence_router, states) room_ids_to_states, users_to_states = parties @@ -478,7 +475,12 @@ def get_currently_syncing_users_for_replication(self) -> Iterable[str]: if count > 0 ] - async def set_state(self, target_user, state, ignore_status_msg=False): + async def set_state( + self, + target_user: UserID, + state: JsonDict, + ignore_status_msg: bool = False, + ) -> None: """Set the presence state of the user.""" presence = state["presence"] @@ -508,7 +510,7 @@ async def set_state(self, target_user, state, ignore_status_msg=False): ignore_status_msg=ignore_status_msg, ) - async def bump_presence_active_time(self, user): + async def bump_presence_active_time(self, user: UserID) -> None: """We've seen the user do something that indicates they're interacting with the app. """ @@ -592,8 +594,8 @@ def __init__(self, hs: "HomeServer"): # we assume that all the sync requests on that process have stopped. # Stored as a dict from process_id to set of user_id, and a dict of # process_id to millisecond timestamp last updated. - self.external_process_to_current_syncs = {} # type: Dict[int, Set[str]] - self.external_process_last_updated_ms = {} # type: Dict[int, int] + self.external_process_to_current_syncs = {} # type: Dict[str, Set[str]] + self.external_process_last_updated_ms = {} # type: Dict[str, int] self.external_sync_linearizer = Linearizer(name="external_sync_linearizer") @@ -633,7 +635,7 @@ def run_persister(): self._event_pos = self.store.get_current_events_token() self._event_processing = False - async def _on_shutdown(self): + async def _on_shutdown(self) -> None: """Gets called when shutting down. This lets us persist any updates that we haven't yet persisted, e.g. updates that only changes some internal timers. This allows changes to persist across startup without having to @@ -662,7 +664,7 @@ async def _on_shutdown(self): ) logger.info("Finished _on_shutdown") - async def _persist_unpersisted_changes(self): + async def _persist_unpersisted_changes(self) -> None: """We periodically persist the unpersisted changes, as otherwise they may stack up and slow down shutdown times. """ @@ -762,7 +764,7 @@ async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None: states, destinations ) - async def _handle_timeouts(self): + async def _handle_timeouts(self) -> None: """Checks the presence of users that have timed out and updates as appropriate. """ @@ -814,7 +816,7 @@ async def _handle_timeouts(self): return await self._update_states(changes) - async def bump_presence_active_time(self, user): + async def bump_presence_active_time(self, user: UserID) -> None: """We've seen the user do something that indicates they're interacting with the app. """ @@ -911,17 +913,17 @@ def get_currently_syncing_users_for_replication(self) -> Iterable[str]: return [] async def update_external_syncs_row( - self, process_id, user_id, is_syncing, sync_time_msec - ): + self, process_id: str, user_id: str, is_syncing: bool, sync_time_msec: int + ) -> None: """Update the syncing users for an external process as a delta. Args: - process_id (str): An identifier for the process the users are + process_id: An identifier for the process the users are syncing against. This allows synapse to process updates as user start and stop syncing against a given process. - user_id (str): The user who has started or stopped syncing - is_syncing (bool): Whether or not the user is now syncing - sync_time_msec(int): Time in ms when the user was last syncing + user_id: The user who has started or stopped syncing + is_syncing: Whether or not the user is now syncing + sync_time_msec: Time in ms when the user was last syncing """ with (await self.external_sync_linearizer.queue(process_id)): prev_state = await self.current_state_for_user(user_id) @@ -958,7 +960,7 @@ async def update_external_syncs_row( self.external_process_last_updated_ms[process_id] = self.clock.time_msec() - async def update_external_syncs_clear(self, process_id): + async def update_external_syncs_clear(self, process_id: str) -> None: """Marks all users that had been marked as syncing by a given process as offline. @@ -979,12 +981,12 @@ async def update_external_syncs_clear(self, process_id): ) self.external_process_last_updated_ms.pop(process_id, None) - async def current_state_for_user(self, user_id): + async def current_state_for_user(self, user_id: str) -> UserPresenceState: """Get the current presence state for a user.""" res = await self.current_state_for_users([user_id]) return res[user_id] - async def _persist_and_notify(self, states): + async def _persist_and_notify(self, states: List[UserPresenceState]) -> None: """Persist states in the database, poke the notifier and send to interested remote servers """ @@ -1005,7 +1007,7 @@ async def _persist_and_notify(self, states): # stream (which is updated by `store.update_presence`). await self.maybe_send_presence_to_interested_destinations(states) - async def incoming_presence(self, origin, content): + async def incoming_presence(self, origin: str, content: JsonDict) -> None: """Called when we receive a `m.presence` EDU from a remote server.""" if not self._presence_enabled: return @@ -1055,7 +1057,9 @@ async def incoming_presence(self, origin, content): federation_presence_counter.inc(len(updates)) await self._update_states(updates) - async def set_state(self, target_user, state, ignore_status_msg=False): + async def set_state( + self, target_user: UserID, state: JsonDict, ignore_status_msg: bool = False + ) -> None: """Set the presence state of the user.""" status_msg = state.get("status_msg", None) presence = state["presence"] @@ -1089,7 +1093,7 @@ async def set_state(self, target_user, state, ignore_status_msg=False): await self._update_states([prev_state.copy_and_replace(**new_fields)]) - async def is_visible(self, observed_user, observer_user): + async def is_visible(self, observed_user: UserID, observer_user: UserID) -> bool: """Returns whether a user can see another user's presence.""" observer_room_ids = await self.store.get_rooms_for_user( observer_user.to_string() @@ -1144,7 +1148,7 @@ async def get_all_presence_updates( ) return rows - def notify_new_event(self): + def notify_new_event(self) -> None: """Called when new events have happened. Handles users and servers joining rooms and require being sent presence. """ @@ -1163,7 +1167,7 @@ async def _process_presence(): run_as_background_process("presence.notify_new_event", _process_presence) - async def _unsafe_process(self): + async def _unsafe_process(self) -> None: # Loop round handling deltas until we're up to date while True: with Measure(self.clock, "presence_delta"): @@ -1188,7 +1192,7 @@ async def _unsafe_process(self): max_pos ) - async def _handle_state_delta(self, deltas): + async def _handle_state_delta(self, deltas: List[JsonDict]) -> None: """Process current state deltas to find new joins that need to be handled. """ @@ -1311,7 +1315,7 @@ async def _on_user_joined_room( return [remote_host], states -def should_notify(old_state, new_state): +def should_notify(old_state: UserPresenceState, new_state: UserPresenceState) -> bool: """Decides if a presence state change should be sent to interested parties.""" if old_state == new_state: return False @@ -1347,7 +1351,9 @@ def should_notify(old_state, new_state): return False -def format_user_presence_state(state, now, include_user_id=True): +def format_user_presence_state( + state: UserPresenceState, now: int, include_user_id: bool = True +) -> JsonDict: """Convert UserPresenceState to a format that can be sent down to clients and to other servers. @@ -1385,11 +1391,11 @@ def __init__(self, hs: "HomeServer"): @log_function async def get_new_events( self, - user, - from_key, - room_ids=None, - include_offline=True, - explicit_room_id=None, + user: UserID, + from_key: Optional[int], + room_ids: Optional[List[str]] = None, + include_offline: bool = True, + explicit_room_id: Optional[str] = None, **kwargs, ) -> Tuple[List[UserPresenceState], int]: # The process for getting presence events are: @@ -1594,7 +1600,7 @@ def _filter_offline_presence_state( if update.state != PresenceState.OFFLINE ] - def get_current_key(self): + def get_current_key(self) -> int: return self.store.get_current_presence_token() @cached(num_args=2, cache_context=True) @@ -1654,15 +1660,20 @@ async def _get_interested_in( return users_interested_in -def handle_timeouts(user_states, is_mine_fn, syncing_user_ids, now): +def handle_timeouts( + user_states: List[UserPresenceState], + is_mine_fn: Callable[[str], bool], + syncing_user_ids: Set[str], + now: int, +) -> List[UserPresenceState]: """Checks the presence of users that have timed out and updates as appropriate. Args: - user_states(list): List of UserPresenceState's to check. - is_mine_fn (fn): Function that returns if a user_id is ours - syncing_user_ids (set): Set of user_ids with active syncs. - now (int): Current time in ms. + user_states: List of UserPresenceState's to check. + is_mine_fn: Function that returns if a user_id is ours + syncing_user_ids: Set of user_ids with active syncs. + now: Current time in ms. Returns: List of UserPresenceState updates @@ -1679,14 +1690,16 @@ def handle_timeouts(user_states, is_mine_fn, syncing_user_ids, now): return list(changes.values()) -def handle_timeout(state, is_mine, syncing_user_ids, now): +def handle_timeout( + state: UserPresenceState, is_mine: bool, syncing_user_ids: Set[str], now: int +) -> Optional[UserPresenceState]: """Checks the presence of the user to see if any of the timers have elapsed Args: - state (UserPresenceState) - is_mine (bool): Whether the user is ours - syncing_user_ids (set): Set of user_ids with active syncs. - now (int): Current time in ms. + state + is_mine: Whether the user is ours + syncing_user_ids: Set of user_ids with active syncs. + now: Current time in ms. Returns: A UserPresenceState update or None if no update. @@ -1738,23 +1751,29 @@ def handle_timeout(state, is_mine, syncing_user_ids, now): return state if changed else None -def handle_update(prev_state, new_state, is_mine, wheel_timer, now): +def handle_update( + prev_state: UserPresenceState, + new_state: UserPresenceState, + is_mine: bool, + wheel_timer: WheelTimer, + now: int, +) -> Tuple[UserPresenceState, bool, bool]: """Given a presence update: 1. Add any appropriate timers. 2. Check if we should notify anyone. Args: - prev_state (UserPresenceState) - new_state (UserPresenceState) - is_mine (bool): Whether the user is ours - wheel_timer (WheelTimer) - now (int): Time now in ms + prev_state + new_state + is_mine: Whether the user is ours + wheel_timer + now: Time now in ms Returns: 3-tuple: `(new_state, persist_and_notify, federation_ping)` where: - new_state: is the state to actually persist - - persist_and_notify (bool): whether to persist and notify people - - federation_ping (bool): whether we should send a ping over federation + - persist_and_notify: whether to persist and notify people + - federation_ping: whether we should send a ping over federation """ user_id = new_state.user_id From 4e0fd35bc918b6901fcd29371ab6d89db8ce1b5e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 28 Apr 2021 11:04:38 +0100 Subject: [PATCH 103/619] Revert "Experimental Federation Speedup (#9702)" This reverts commit 05e8c70c059f8ebb066e029bc3aa3e0cefef1019. --- changelog.d/9702.misc | 1 - contrib/experiments/test_messaging.py | 42 +++-- synapse/federation/sender/__init__.py | 145 +++++++----------- .../sender/per_destination_queue.py | 15 +- .../storage/databases/main/transactions.py | 28 ++-- 5 files changed, 93 insertions(+), 138 deletions(-) delete mode 100644 changelog.d/9702.misc diff --git a/changelog.d/9702.misc b/changelog.d/9702.misc deleted file mode 100644 index c6e63450a9..0000000000 --- a/changelog.d/9702.misc +++ /dev/null @@ -1 +0,0 @@ -Speed up federation transmission by using fewer database calls. Contributed by @ShadowJonathan. diff --git a/contrib/experiments/test_messaging.py b/contrib/experiments/test_messaging.py index 5dd172052b..31b8a68225 100644 --- a/contrib/experiments/test_messaging.py +++ b/contrib/experiments/test_messaging.py @@ -224,16 +224,14 @@ def send_message(self, room_name, sender, body): destinations = yield self.get_servers_for_context(room_name) try: - yield self.replication_layer.send_pdus( - [ - Pdu.create_new( - context=room_name, - pdu_type="sy.room.message", - content={"sender": sender, "body": body}, - origin=self.server_name, - destinations=destinations, - ) - ] + yield self.replication_layer.send_pdu( + Pdu.create_new( + context=room_name, + pdu_type="sy.room.message", + content={"sender": sender, "body": body}, + origin=self.server_name, + destinations=destinations, + ) ) except Exception as e: logger.exception(e) @@ -255,7 +253,7 @@ def join_room(self, room_name, sender, joinee): origin=self.server_name, destinations=destinations, ) - yield self.replication_layer.send_pdus([pdu]) + yield self.replication_layer.send_pdu(pdu) except Exception as e: logger.exception(e) @@ -267,18 +265,16 @@ def invite_to_room(self, room_name, sender, invitee): destinations = yield self.get_servers_for_context(room_name) try: - yield self.replication_layer.send_pdus( - [ - Pdu.create_new( - context=room_name, - is_state=True, - pdu_type="sy.room.member", - state_key=invitee, - content={"membership": "invite"}, - origin=self.server_name, - destinations=destinations, - ) - ] + yield self.replication_layer.send_pdu( + Pdu.create_new( + context=room_name, + is_state=True, + pdu_type="sy.room.member", + state_key=invitee, + content={"membership": "invite"}, + origin=self.server_name, + destinations=destinations, + ) ) except Exception as e: logger.exception(e) diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 022bbf7dad..deb40f4610 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -14,26 +14,19 @@ import abc import logging -from typing import ( - TYPE_CHECKING, - Collection, - Dict, - Hashable, - Iterable, - List, - Optional, - Set, - Tuple, -) +from typing import TYPE_CHECKING, Dict, Hashable, Iterable, List, Optional, Set, Tuple from prometheus_client import Counter +from twisted.internet import defer + import synapse.metrics from synapse.api.presence import UserPresenceState from synapse.events import EventBase from synapse.federation.sender.per_destination_queue import PerDestinationQueue from synapse.federation.sender.transaction_manager import TransactionManager from synapse.federation.units import Edu +from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.metrics import ( LaterGauge, event_processing_loop_counter, @@ -262,27 +255,15 @@ async def _process_event_queue_loop(self) -> None: if not events and next_token >= self._last_poked_id: break - async def get_destinations_for_event( - event: EventBase, - ) -> Collection[str]: - """Computes the destinations to which this event must be sent. - - This returns an empty tuple when there are no destinations to send to, - or if this event is not from this homeserver and it is not sending - it on behalf of another server. - - Will also filter out destinations which this sender is not responsible for, - if multiple federation senders exist. - """ - + async def handle_event(event: EventBase) -> None: # Only send events for this server. send_on_behalf_of = event.internal_metadata.get_send_on_behalf_of() is_mine = self.is_mine_id(event.sender) if not is_mine and send_on_behalf_of is None: - return () + return if not event.internal_metadata.should_proactively_send(): - return () + return destinations = None # type: Optional[Set[str]] if not event.prev_event_ids(): @@ -317,7 +298,7 @@ async def get_destinations_for_event( "Failed to calculate hosts in room for event: %s", event.event_id, ) - return () + return destinations = { d @@ -327,15 +308,17 @@ async def get_destinations_for_event( ) } - destinations.discard(self.server_name) - if send_on_behalf_of is not None: # If we are sending the event on behalf of another server # then it already has the event and there is no reason to # send the event to it. destinations.discard(send_on_behalf_of) + logger.debug("Sending %s to %r", event, destinations) + if destinations: + await self._send_pdu(event, destinations) + now = self.clock.time_msec() ts = await self.store.get_received_ts(event.event_id) @@ -343,29 +326,24 @@ async def get_destinations_for_event( "federation_sender" ).observe((now - ts) / 1000) - return destinations - return () - - async def get_federatable_events_and_destinations( - events: Iterable[EventBase], - ) -> List[Tuple[EventBase, Collection[str]]]: - with Measure(self.clock, "get_destinations_for_events"): - # Fetch federation destinations per event, - # skip if get_destinations_for_event returns an empty collection, - # return list of event->destinations pairs. - return [ - (event, dests) - for (event, dests) in [ - (event, await get_destinations_for_event(event)) - for event in events - ] - if dests - ] - - events_and_dests = await get_federatable_events_and_destinations(events) - - # Send corresponding events to each destination queue - await self._distribute_events(events_and_dests) + async def handle_room_events(events: Iterable[EventBase]) -> None: + with Measure(self.clock, "handle_room_events"): + for event in events: + await handle_event(event) + + events_by_room = {} # type: Dict[str, List[EventBase]] + for event in events: + events_by_room.setdefault(event.room_id, []).append(event) + + await make_deferred_yieldable( + defer.gatherResults( + [ + run_in_background(handle_room_events, evs) + for evs in events_by_room.values() + ], + consumeErrors=True, + ) + ) await self.store.update_federation_out_pos("events", next_token) @@ -383,7 +361,7 @@ async def get_federatable_events_and_destinations( events_processed_counter.inc(len(events)) event_processing_loop_room_count.labels("federation_sender").inc( - len({event.room_id for event in events}) + len(events_by_room) ) event_processing_loop_counter.labels("federation_sender").inc() @@ -395,53 +373,34 @@ async def get_federatable_events_and_destinations( finally: self._is_processing = False - async def _distribute_events( - self, - events_and_dests: Iterable[Tuple[EventBase, Collection[str]]], - ) -> None: - """Distribute events to the respective per_destination queues. - - Also persists last-seen per-room stream_ordering to 'destination_rooms'. - - Args: - events_and_dests: A list of tuples, which are (event: EventBase, destinations: Collection[str]). - Every event is paired with its intended destinations (in federation). - """ - # Tuples of room_id + destination to their max-seen stream_ordering - room_with_dest_stream_ordering = {} # type: Dict[Tuple[str, str], int] - - # List of events to send to each destination - events_by_dest = {} # type: Dict[str, List[EventBase]] + async def _send_pdu(self, pdu: EventBase, destinations: Iterable[str]) -> None: + # We loop through all destinations to see whether we already have + # a transaction in progress. If we do, stick it in the pending_pdus + # table and we'll get back to it later. - # For each event-destinations pair... - for event, destinations in events_and_dests: + destinations = set(destinations) + destinations.discard(self.server_name) + logger.debug("Sending to: %s", str(destinations)) - # (we got this from the database, it's filled) - assert event.internal_metadata.stream_ordering - - sent_pdus_destination_dist_total.inc(len(destinations)) - sent_pdus_destination_dist_count.inc() + if not destinations: + return - # ...iterate over those destinations.. - for destination in destinations: - # ...update their stream-ordering... - room_with_dest_stream_ordering[(event.room_id, destination)] = max( - event.internal_metadata.stream_ordering, - room_with_dest_stream_ordering.get((event.room_id, destination), 0), - ) + sent_pdus_destination_dist_total.inc(len(destinations)) + sent_pdus_destination_dist_count.inc() - # ...and add the event to each destination queue. - events_by_dest.setdefault(destination, []).append(event) + assert pdu.internal_metadata.stream_ordering - # Bulk-store destination_rooms stream_ids - await self.store.bulk_store_destination_rooms_entries( - room_with_dest_stream_ordering + # track the fact that we have a PDU for these destinations, + # to allow us to perform catch-up later on if the remote is unreachable + # for a while. + await self.store.store_destination_rooms_entries( + destinations, + pdu.room_id, + pdu.internal_metadata.stream_ordering, ) - for destination, pdus in events_by_dest.items(): - logger.debug("Sending %d pdus to %s", len(pdus), destination) - - self._get_per_destination_queue(destination).send_pdus(pdus) + for destination in destinations: + self._get_per_destination_queue(destination).send_pdu(pdu) async def send_read_receipt(self, receipt: ReadReceipt) -> None: """Send a RR to any other servers in the room diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index 3bb66bce32..3b053ebcfb 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -154,22 +154,19 @@ def pending_edu_count(self) -> int: + len(self._pending_edus_keyed) ) - def send_pdus(self, pdus: Iterable[EventBase]) -> None: - """Add PDUs to the queue, and start the transmission loop if necessary + def send_pdu(self, pdu: EventBase) -> None: + """Add a PDU to the queue, and start the transmission loop if necessary Args: - pdus: pdus to send + pdu: pdu to send """ if not self._catching_up or self._last_successful_stream_ordering is None: # only enqueue the PDU if we are not catching up (False) or do not # yet know if we have anything to catch up (None) - self._pending_pdus.extend(pdus) + self._pending_pdus.append(pdu) else: - self._catchup_last_skipped = max( - pdu.internal_metadata.stream_ordering - for pdu in pdus - if pdu.internal_metadata.stream_ordering is not None - ) + assert pdu.internal_metadata.stream_ordering + self._catchup_last_skipped = pdu.internal_metadata.stream_ordering self.attempt_new_transaction() diff --git a/synapse/storage/databases/main/transactions.py b/synapse/storage/databases/main/transactions.py index b28ca61f80..82335e7a9d 100644 --- a/synapse/storage/databases/main/transactions.py +++ b/synapse/storage/databases/main/transactions.py @@ -14,7 +14,7 @@ import logging from collections import namedtuple -from typing import Dict, List, Optional, Tuple +from typing import Iterable, List, Optional, Tuple from canonicaljson import encode_canonical_json @@ -295,33 +295,37 @@ def _set_destination_retry_timings_emulated( }, ) - async def bulk_store_destination_rooms_entries( - self, room_and_destination_to_ordering: Dict[Tuple[str, str], int] - ): + async def store_destination_rooms_entries( + self, + destinations: Iterable[str], + room_id: str, + stream_ordering: int, + ) -> None: """ - Updates or creates `destination_rooms` entries for a number of events. + Updates or creates `destination_rooms` entries in batch for a single event. Args: - room_and_destination_to_ordering: A mapping of (room, destination) -> stream_id + destinations: list of destinations + room_id: the room_id of the event + stream_ordering: the stream_ordering of the event """ await self.db_pool.simple_upsert_many( table="destinations", key_names=("destination",), - key_values={(d,) for _, d in room_and_destination_to_ordering.keys()}, + key_values=[(d,) for d in destinations], value_names=[], value_values=[], desc="store_destination_rooms_entries_dests", ) + rows = [(destination, room_id) for destination in destinations] await self.db_pool.simple_upsert_many( table="destination_rooms", - key_names=("room_id", "destination"), - key_values=list(room_and_destination_to_ordering.keys()), + key_names=("destination", "room_id"), + key_values=rows, value_names=["stream_ordering"], - value_values=[ - (stream_id,) for stream_id in room_and_destination_to_ordering.values() - ], + value_values=[(stream_ordering,)] * len(rows), desc="store_destination_rooms_entries_rooms", ) From 787de3190f70d952b0d6589e9335aa16cacc41f2 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 28 Apr 2021 11:43:33 +0100 Subject: [PATCH 104/619] 1.33.0rc1 --- CHANGES.md | 53 ++++++++++++++++++++++++++++++++++++++++ changelog.d/9162.misc | 1 - changelog.d/9726.bugfix | 1 - changelog.d/9786.misc | 1 - changelog.d/9788.bugfix | 1 - changelog.d/9796.misc | 1 - changelog.d/9800.feature | 1 - changelog.d/9801.doc | 1 - changelog.d/9802.bugfix | 1 - changelog.d/9814.feature | 1 - changelog.d/9815.misc | 1 - changelog.d/9816.misc | 1 - changelog.d/9817.misc | 1 - changelog.d/9819.feature | 1 - changelog.d/9820.feature | 1 - changelog.d/9821.misc | 1 - changelog.d/9825.misc | 1 - changelog.d/9828.feature | 1 - changelog.d/9832.feature | 1 - changelog.d/9833.bugfix | 1 - changelog.d/9838.misc | 1 - changelog.d/9845.misc | 1 - changelog.d/9850.feature | 1 - changelog.d/9855.misc | 1 - changelog.d/9856.misc | 1 - changelog.d/9858.misc | 1 - changelog.d/9867.bugfix | 1 - changelog.d/9868.bugfix | 1 - changelog.d/9871.misc | 1 - changelog.d/9874.misc | 1 - changelog.d/9875.misc | 1 - changelog.d/9876.misc | 1 - changelog.d/9878.misc | 1 - changelog.d/9879.misc | 1 - changelog.d/9887.misc | 1 - synapse/__init__.py | 2 +- 36 files changed, 54 insertions(+), 35 deletions(-) delete mode 100644 changelog.d/9162.misc delete mode 100644 changelog.d/9726.bugfix delete mode 100644 changelog.d/9786.misc delete mode 100644 changelog.d/9788.bugfix delete mode 100644 changelog.d/9796.misc delete mode 100644 changelog.d/9800.feature delete mode 100644 changelog.d/9801.doc delete mode 100644 changelog.d/9802.bugfix delete mode 100644 changelog.d/9814.feature delete mode 100644 changelog.d/9815.misc delete mode 100644 changelog.d/9816.misc delete mode 100644 changelog.d/9817.misc delete mode 100644 changelog.d/9819.feature delete mode 100644 changelog.d/9820.feature delete mode 100644 changelog.d/9821.misc delete mode 100644 changelog.d/9825.misc delete mode 100644 changelog.d/9828.feature delete mode 100644 changelog.d/9832.feature delete mode 100644 changelog.d/9833.bugfix delete mode 100644 changelog.d/9838.misc delete mode 100644 changelog.d/9845.misc delete mode 100644 changelog.d/9850.feature delete mode 100644 changelog.d/9855.misc delete mode 100644 changelog.d/9856.misc delete mode 100644 changelog.d/9858.misc delete mode 100644 changelog.d/9867.bugfix delete mode 100644 changelog.d/9868.bugfix delete mode 100644 changelog.d/9871.misc delete mode 100644 changelog.d/9874.misc delete mode 100644 changelog.d/9875.misc delete mode 100644 changelog.d/9876.misc delete mode 100644 changelog.d/9878.misc delete mode 100644 changelog.d/9879.misc delete mode 100644 changelog.d/9887.misc diff --git a/CHANGES.md b/CHANGES.md index 532b30e232..a1f5376ff2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,56 @@ +Synapse 1.33.0rc1 (2021-04-28) +============================== + +Features +-------- + +- Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. ([\#9800](https://github.com/matrix-org/synapse/issues/9800), [\#9814](https://github.com/matrix-org/synapse/issues/9814)) +- Add experimental support for handling presence on a worker. ([\#9819](https://github.com/matrix-org/synapse/issues/9819), [\#9820](https://github.com/matrix-org/synapse/issues/9820), [\#9828](https://github.com/matrix-org/synapse/issues/9828), [\#9850](https://github.com/matrix-org/synapse/issues/9850)) +- Don't return an error when a user attempts to renew their account multiple times with the same token. Instead, state when their account is set to expire. This change concerns the optional account validity feature. ([\#9832](https://github.com/matrix-org/synapse/issues/9832)) + + +Bugfixes +-------- + +- Fixes the OIDC SSO flow when using a `public_baseurl` value including a non-root URL path. ([\#9726](https://github.com/matrix-org/synapse/issues/9726)) +- Fix thumbnail generation for some sites with non-standard content types. Contributed by @rkfg. ([\#9788](https://github.com/matrix-org/synapse/issues/9788)) +- Add some sanity checks to identity server passed to 3PID bind/unbind endpoints. ([\#9802](https://github.com/matrix-org/synapse/issues/9802)) +- Limit the size of HTTP responses read over federation. ([\#9833](https://github.com/matrix-org/synapse/issues/9833)) +- Fix a bug which could cause Synapse to get stuck in a loop of resyncing device lists. ([\#9867](https://github.com/matrix-org/synapse/issues/9867)) +- Fix a long-standing bug where errors from federation did not propagate to the client. ([\#9868](https://github.com/matrix-org/synapse/issues/9868)) + + +Improved Documentation +---------------------- + +- Add a note to the docker docs mentioning that we mirror upstream's supported Docker platforms. ([\#9801](https://github.com/matrix-org/synapse/issues/9801)) + + +Internal Changes +---------------- + +- Add a dockerfile for running Synapse in worker-mode under Complement. ([\#9162](https://github.com/matrix-org/synapse/issues/9162)) +- Apply `pyupgrade` across the codebase. ([\#9786](https://github.com/matrix-org/synapse/issues/9786)) +- Move some replication processing out of `generic_worker`. ([\#9796](https://github.com/matrix-org/synapse/issues/9796)) +- Replace `HomeServer.get_config()` with inline references. ([\#9815](https://github.com/matrix-org/synapse/issues/9815)) +- Rename some handlers and config modules to not duplicate the top-level module. ([\#9816](https://github.com/matrix-org/synapse/issues/9816)) +- Fix a long-standing bug which caused `max_upload_size` to not be correctly enforced. ([\#9817](https://github.com/matrix-org/synapse/issues/9817)) +- Reduce CPU usage of the user directory by reusing existing calculated room membership. ([\#9821](https://github.com/matrix-org/synapse/issues/9821)) +- Small speed up for joining large remote rooms. ([\#9825](https://github.com/matrix-org/synapse/issues/9825)) +- Introduce flake8-bugbear to the test suite and fix some of its lint violations. ([\#9838](https://github.com/matrix-org/synapse/issues/9838)) +- Only store the raw data in the in-memory caches, rather than objects that include references to e.g. the data stores. ([\#9845](https://github.com/matrix-org/synapse/issues/9845)) +- Limit length of accepted email addresses. ([\#9855](https://github.com/matrix-org/synapse/issues/9855)) +- Remove redundant `synapse.types.Collection` type definition. ([\#9856](https://github.com/matrix-org/synapse/issues/9856)) +- Handle recently added rate limits correctly when using `--no-rate-limit` with the demo scripts. ([\#9858](https://github.com/matrix-org/synapse/issues/9858)) +- Disable invite rate-limiting by default when running the unit tests. ([\#9871](https://github.com/matrix-org/synapse/issues/9871)) +- Pass a reactor into `SynapseSite` to make testing easier. ([\#9874](https://github.com/matrix-org/synapse/issues/9874)) +- Make `DomainSpecificString` an `attrs` class. ([\#9875](https://github.com/matrix-org/synapse/issues/9875)) +- Add type hints to `synapse.api.auth` and `synapse.api.auth_blocking` modules. ([\#9876](https://github.com/matrix-org/synapse/issues/9876)) +- Remove redundant `_PushHTTPChannel` test class. ([\#9878](https://github.com/matrix-org/synapse/issues/9878)) +- Remove backwards-compatibility code for Python versions < 3.6. ([\#9879](https://github.com/matrix-org/synapse/issues/9879)) +- Small performance improvement around handling new local presence updates. ([\#9887](https://github.com/matrix-org/synapse/issues/9887)) + + Synapse 1.32.2 (2021-04-22) =========================== diff --git a/changelog.d/9162.misc b/changelog.d/9162.misc deleted file mode 100644 index 1083da8a7a..0000000000 --- a/changelog.d/9162.misc +++ /dev/null @@ -1 +0,0 @@ -Add a dockerfile for running Synapse in worker-mode under Complement. \ No newline at end of file diff --git a/changelog.d/9726.bugfix b/changelog.d/9726.bugfix deleted file mode 100644 index 4ba0b24327..0000000000 --- a/changelog.d/9726.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixes the OIDC SSO flow when using a `public_baseurl` value including a non-root URL path. \ No newline at end of file diff --git a/changelog.d/9786.misc b/changelog.d/9786.misc deleted file mode 100644 index cf265db749..0000000000 --- a/changelog.d/9786.misc +++ /dev/null @@ -1 +0,0 @@ -Apply `pyupgrade` across the codebase. \ No newline at end of file diff --git a/changelog.d/9788.bugfix b/changelog.d/9788.bugfix deleted file mode 100644 index edb58fbd5b..0000000000 --- a/changelog.d/9788.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix thumbnail generation for some sites with non-standard content types. Contributed by @rkfg. diff --git a/changelog.d/9796.misc b/changelog.d/9796.misc deleted file mode 100644 index 59bb1813c3..0000000000 --- a/changelog.d/9796.misc +++ /dev/null @@ -1 +0,0 @@ -Move some replication processing out of `generic_worker`. diff --git a/changelog.d/9800.feature b/changelog.d/9800.feature deleted file mode 100644 index 9404ad2fc0..0000000000 --- a/changelog.d/9800.feature +++ /dev/null @@ -1 +0,0 @@ -Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/changelog.d/9801.doc b/changelog.d/9801.doc deleted file mode 100644 index 8b8b9d01d4..0000000000 --- a/changelog.d/9801.doc +++ /dev/null @@ -1 +0,0 @@ -Add a note to the docker docs mentioning that we mirror upstream's supported Docker platforms. diff --git a/changelog.d/9802.bugfix b/changelog.d/9802.bugfix deleted file mode 100644 index 0c72f7be47..0000000000 --- a/changelog.d/9802.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add some sanity checks to identity server passed to 3PID bind/unbind endpoints. diff --git a/changelog.d/9814.feature b/changelog.d/9814.feature deleted file mode 100644 index 9404ad2fc0..0000000000 --- a/changelog.d/9814.feature +++ /dev/null @@ -1 +0,0 @@ -Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. diff --git a/changelog.d/9815.misc b/changelog.d/9815.misc deleted file mode 100644 index e33d012d3d..0000000000 --- a/changelog.d/9815.misc +++ /dev/null @@ -1 +0,0 @@ -Replace `HomeServer.get_config()` with inline references. diff --git a/changelog.d/9816.misc b/changelog.d/9816.misc deleted file mode 100644 index d098122500..0000000000 --- a/changelog.d/9816.misc +++ /dev/null @@ -1 +0,0 @@ -Rename some handlers and config modules to not duplicate the top-level module. diff --git a/changelog.d/9817.misc b/changelog.d/9817.misc deleted file mode 100644 index 8aa8895f05..0000000000 --- a/changelog.d/9817.misc +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug which caused `max_upload_size` to not be correctly enforced. diff --git a/changelog.d/9819.feature b/changelog.d/9819.feature deleted file mode 100644 index f56b0bb3bd..0000000000 --- a/changelog.d/9819.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for handling presence on a worker. diff --git a/changelog.d/9820.feature b/changelog.d/9820.feature deleted file mode 100644 index f56b0bb3bd..0000000000 --- a/changelog.d/9820.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for handling presence on a worker. diff --git a/changelog.d/9821.misc b/changelog.d/9821.misc deleted file mode 100644 index 03b2d2ed4d..0000000000 --- a/changelog.d/9821.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce CPU usage of the user directory by reusing existing calculated room membership. \ No newline at end of file diff --git a/changelog.d/9825.misc b/changelog.d/9825.misc deleted file mode 100644 index 42f3f15619..0000000000 --- a/changelog.d/9825.misc +++ /dev/null @@ -1 +0,0 @@ -Small speed up for joining large remote rooms. diff --git a/changelog.d/9828.feature b/changelog.d/9828.feature deleted file mode 100644 index f56b0bb3bd..0000000000 --- a/changelog.d/9828.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for handling presence on a worker. diff --git a/changelog.d/9832.feature b/changelog.d/9832.feature deleted file mode 100644 index e76395fbe8..0000000000 --- a/changelog.d/9832.feature +++ /dev/null @@ -1 +0,0 @@ -Don't return an error when a user attempts to renew their account multiple times with the same token. Instead, state when their account is set to expire. This change concerns the optional account validity feature. \ No newline at end of file diff --git a/changelog.d/9833.bugfix b/changelog.d/9833.bugfix deleted file mode 100644 index 56f9c9626b..0000000000 --- a/changelog.d/9833.bugfix +++ /dev/null @@ -1 +0,0 @@ -Limit the size of HTTP responses read over federation. diff --git a/changelog.d/9838.misc b/changelog.d/9838.misc deleted file mode 100644 index b98ce56309..0000000000 --- a/changelog.d/9838.misc +++ /dev/null @@ -1 +0,0 @@ -Introduce flake8-bugbear to the test suite and fix some of its lint violations. \ No newline at end of file diff --git a/changelog.d/9845.misc b/changelog.d/9845.misc deleted file mode 100644 index 875dd6d131..0000000000 --- a/changelog.d/9845.misc +++ /dev/null @@ -1 +0,0 @@ -Only store the raw data in the in-memory caches, rather than objects that include references to e.g. the data stores. diff --git a/changelog.d/9850.feature b/changelog.d/9850.feature deleted file mode 100644 index f56b0bb3bd..0000000000 --- a/changelog.d/9850.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for handling presence on a worker. diff --git a/changelog.d/9855.misc b/changelog.d/9855.misc deleted file mode 100644 index 6a3d700fde..0000000000 --- a/changelog.d/9855.misc +++ /dev/null @@ -1 +0,0 @@ -Limit length of accepted email addresses. diff --git a/changelog.d/9856.misc b/changelog.d/9856.misc deleted file mode 100644 index d67e8c386a..0000000000 --- a/changelog.d/9856.misc +++ /dev/null @@ -1 +0,0 @@ -Remove redundant `synapse.types.Collection` type definition. diff --git a/changelog.d/9858.misc b/changelog.d/9858.misc deleted file mode 100644 index f7e286fa69..0000000000 --- a/changelog.d/9858.misc +++ /dev/null @@ -1 +0,0 @@ -Handle recently added rate limits correctly when using `--no-rate-limit` with the demo scripts. diff --git a/changelog.d/9867.bugfix b/changelog.d/9867.bugfix deleted file mode 100644 index f236de247d..0000000000 --- a/changelog.d/9867.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which could cause Synapse to get stuck in a loop of resyncing device lists. diff --git a/changelog.d/9868.bugfix b/changelog.d/9868.bugfix deleted file mode 100644 index e2b4f97ad5..0000000000 --- a/changelog.d/9868.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where errors from federation did not propagate to the client. diff --git a/changelog.d/9871.misc b/changelog.d/9871.misc deleted file mode 100644 index b19acfab62..0000000000 --- a/changelog.d/9871.misc +++ /dev/null @@ -1 +0,0 @@ -Disable invite rate-limiting by default when running the unit tests. \ No newline at end of file diff --git a/changelog.d/9874.misc b/changelog.d/9874.misc deleted file mode 100644 index ba1097e65e..0000000000 --- a/changelog.d/9874.misc +++ /dev/null @@ -1 +0,0 @@ -Pass a reactor into `SynapseSite` to make testing easier. diff --git a/changelog.d/9875.misc b/changelog.d/9875.misc deleted file mode 100644 index 9345c0bf45..0000000000 --- a/changelog.d/9875.misc +++ /dev/null @@ -1 +0,0 @@ -Make `DomainSpecificString` an `attrs` class. diff --git a/changelog.d/9876.misc b/changelog.d/9876.misc deleted file mode 100644 index 28390e32e6..0000000000 --- a/changelog.d/9876.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to `synapse.api.auth` and `synapse.api.auth_blocking` modules. diff --git a/changelog.d/9878.misc b/changelog.d/9878.misc deleted file mode 100644 index 927876852d..0000000000 --- a/changelog.d/9878.misc +++ /dev/null @@ -1 +0,0 @@ -Remove redundant `_PushHTTPChannel` test class. diff --git a/changelog.d/9879.misc b/changelog.d/9879.misc deleted file mode 100644 index c9ca37cf48..0000000000 --- a/changelog.d/9879.misc +++ /dev/null @@ -1 +0,0 @@ -Remove backwards-compatibility code for Python versions < 3.6. \ No newline at end of file diff --git a/changelog.d/9887.misc b/changelog.d/9887.misc deleted file mode 100644 index 650ebf85e6..0000000000 --- a/changelog.d/9887.misc +++ /dev/null @@ -1 +0,0 @@ -Small performance improvement around handling new local presence updates. diff --git a/synapse/__init__.py b/synapse/__init__.py index fbd49a93e1..5bbaa62de2 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.32.2" +__version__ = "1.33.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 391bfe9a7b7b22c3dbee9f9e02071fd5c1730ab5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 28 Apr 2021 11:59:28 +0100 Subject: [PATCH 105/619] Reduce memory footprint of caches (#9886) --- changelog.d/9886.misc | 1 + synapse/util/caches/lrucache.py | 77 +++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 changelog.d/9886.misc diff --git a/changelog.d/9886.misc b/changelog.d/9886.misc new file mode 100644 index 0000000000..8ff869e659 --- /dev/null +++ b/changelog.d/9886.misc @@ -0,0 +1 @@ +Reduce memory usage of the LRU caches. diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index a21d34fcb4..10b0ec6b75 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -17,8 +17,10 @@ from typing import ( Any, Callable, + Collection, Generic, Iterable, + List, Optional, Type, TypeVar, @@ -57,13 +59,56 @@ class _Node: __slots__ = ["prev_node", "next_node", "key", "value", "callbacks"] def __init__( - self, prev_node, next_node, key, value, callbacks: Optional[set] = None + self, + prev_node, + next_node, + key, + value, + callbacks: Collection[Callable[[], None]] = (), ): self.prev_node = prev_node self.next_node = next_node self.key = key self.value = value - self.callbacks = callbacks or set() + + # Set of callbacks to run when the node gets deleted. We store as a list + # rather than a set to keep memory usage down (and since we expect few + # entries per node, the performance of checking for duplication in a + # list vs using a set is negligible). + # + # Note that we store this as an optional list to keep the memory + # footprint down. Storing `None` is free as its a singleton, while empty + # lists are 56 bytes (and empty sets are 216 bytes, if we did the naive + # thing and used sets). + self.callbacks = None # type: Optional[List[Callable[[], None]]] + + self.add_callbacks(callbacks) + + def add_callbacks(self, callbacks: Collection[Callable[[], None]]) -> None: + """Add to stored list of callbacks, removing duplicates.""" + + if not callbacks: + return + + if not self.callbacks: + self.callbacks = [] + + for callback in callbacks: + if callback not in self.callbacks: + self.callbacks.append(callback) + + def run_and_clear_callbacks(self) -> None: + """Run all callbacks and clear the stored list of callbacks. Used when + the node is being deleted. + """ + + if not self.callbacks: + return + + for callback in self.callbacks: + callback() + + self.callbacks = None class LruCache(Generic[KT, VT]): @@ -177,10 +222,10 @@ def cache_len(): self.len = synchronized(cache_len) - def add_node(key, value, callbacks: Optional[set] = None): + def add_node(key, value, callbacks: Collection[Callable[[], None]] = ()): prev_node = list_root next_node = prev_node.next_node - node = _Node(prev_node, next_node, key, value, callbacks or set()) + node = _Node(prev_node, next_node, key, value, callbacks) prev_node.next_node = node next_node.prev_node = node cache[key] = node @@ -211,16 +256,15 @@ def delete_node(node): deleted_len = size_callback(node.value) cached_cache_len[0] -= deleted_len - for cb in node.callbacks: - cb() - node.callbacks.clear() + node.run_and_clear_callbacks() + return deleted_len @overload def cache_get( key: KT, default: Literal[None] = None, - callbacks: Iterable[Callable[[], None]] = ..., + callbacks: Collection[Callable[[], None]] = ..., update_metrics: bool = ..., ) -> Optional[VT]: ... @@ -229,7 +273,7 @@ def cache_get( def cache_get( key: KT, default: T, - callbacks: Iterable[Callable[[], None]] = ..., + callbacks: Collection[Callable[[], None]] = ..., update_metrics: bool = ..., ) -> Union[T, VT]: ... @@ -238,13 +282,13 @@ def cache_get( def cache_get( key: KT, default: Optional[T] = None, - callbacks: Iterable[Callable[[], None]] = (), + callbacks: Collection[Callable[[], None]] = (), update_metrics: bool = True, ): node = cache.get(key, None) if node is not None: move_node_to_front(node) - node.callbacks.update(callbacks) + node.add_callbacks(callbacks) if update_metrics and metrics: metrics.inc_hits() return node.value @@ -260,10 +304,8 @@ def cache_set(key: KT, value: VT, callbacks: Iterable[Callable[[], None]] = ()): # We sometimes store large objects, e.g. dicts, which cause # the inequality check to take a long time. So let's only do # the check if we have some callbacks to call. - if node.callbacks and value != node.value: - for cb in node.callbacks: - cb() - node.callbacks.clear() + if value != node.value: + node.run_and_clear_callbacks() # We don't bother to protect this by value != node.value as # generally size_callback will be cheap compared with equality @@ -273,7 +315,7 @@ def cache_set(key: KT, value: VT, callbacks: Iterable[Callable[[], None]] = ()): cached_cache_len[0] -= size_callback(node.value) cached_cache_len[0] += size_callback(value) - node.callbacks.update(callbacks) + node.add_callbacks(callbacks) move_node_to_front(node) node.value = value @@ -326,8 +368,7 @@ def cache_clear() -> None: list_root.next_node = list_root list_root.prev_node = list_root for node in cache.values(): - for cb in node.callbacks: - cb() + node.run_and_clear_callbacks() cache.clear() if size_callback: cached_cache_len[0] = 0 From 8ba086980dbe4272a6ad2f529ae7b955b93bb9b0 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 28 Apr 2021 12:07:49 +0100 Subject: [PATCH 106/619] Reword account validity template change to sound less like a bugfix --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a1f5376ff2..9a41607679 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Features - Update experimental support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083): restricting room access via group membership. ([\#9800](https://github.com/matrix-org/synapse/issues/9800), [\#9814](https://github.com/matrix-org/synapse/issues/9814)) - Add experimental support for handling presence on a worker. ([\#9819](https://github.com/matrix-org/synapse/issues/9819), [\#9820](https://github.com/matrix-org/synapse/issues/9820), [\#9828](https://github.com/matrix-org/synapse/issues/9828), [\#9850](https://github.com/matrix-org/synapse/issues/9850)) -- Don't return an error when a user attempts to renew their account multiple times with the same token. Instead, state when their account is set to expire. This change concerns the optional account validity feature. ([\#9832](https://github.com/matrix-org/synapse/issues/9832)) +- Return a new template when an user attempts to renew their account multiple times with the same token, stating that their account is set to expire. This replaces the invalid token template that would previously be shown in this case. This change concerns the optional account validity feature. ([\#9832](https://github.com/matrix-org/synapse/issues/9832)) Bugfixes From 10a08ab88ad423bfca86983808c47f34a601ec9c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 28 Apr 2021 07:44:52 -0400 Subject: [PATCH 107/619] Use the parent's logging context name for runWithConnection. (#9895) This fixes a regression where the logging context for runWithConnection was reported as runWithConnection instead of the connection name, e.g. "POST-XYZ". --- changelog.d/9895.bugfix | 1 + synapse/storage/database.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9895.bugfix diff --git a/changelog.d/9895.bugfix b/changelog.d/9895.bugfix new file mode 100644 index 0000000000..1053f975bf --- /dev/null +++ b/changelog.d/9895.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.32.0 where the associated connection was improperly logged for SQL logging statements. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index bd39c095af..a761ad603b 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -715,7 +715,9 @@ def inner_func(conn, *args, **kwargs): # pool). assert not self.engine.in_transaction(conn) - with LoggingContext("runWithConnection", parent_context) as context: + with LoggingContext( + str(curr_context), parent_context=parent_context + ) as context: sched_duration_sec = monotonic_time() - start_time sql_scheduling_timer.observe(sched_duration_sec) context.add_database_scheduled(sched_duration_sec) From e4ab8676b4b5a3336ef49bb68a0e6dabbf030df4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 28 Apr 2021 14:42:50 +0100 Subject: [PATCH 108/619] Fix tight loop handling presence replication. (#9900) Only affects workers. Introduced in #9819. Fixes #9899. --- changelog.d/9900.bugfix | 1 + synapse/handlers/presence.py | 24 +++++++++++++++++++++++- tests/handlers/test_presence.py | 22 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9900.bugfix diff --git a/changelog.d/9900.bugfix b/changelog.d/9900.bugfix new file mode 100644 index 0000000000..a8470fca3f --- /dev/null +++ b/changelog.d/9900.bugfix @@ -0,0 +1 @@ +Fix tight loop handling presence replication when using workers. Introduced in v1.33.0rc1. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 969c73c1e7..12df35f26e 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -2026,18 +2026,40 @@ async def get_replication_rows( ) return result["updates"], result["upto_token"], result["limited"] + # If the from_token is the current token then there's nothing to return + # and we can trivially no-op. + if from_token == self._next_id - 1: + return [], upto_token, False + # We can find the correct position in the queue by noting that there is # exactly one entry per stream ID, and that the last entry has an ID of # `self._next_id - 1`, so we can count backwards from the end. # + # Since we are returning all states in the range `from_token < stream_id + # <= upto_token` we look for the index with a `stream_id` of `from_token + # + 1`. + # # Since the start of the queue is periodically truncated we need to # handle the case where `from_token` stream ID has already been dropped. - start_idx = max(from_token - self._next_id, -len(self._queue)) + start_idx = max(from_token + 1 - self._next_id, -len(self._queue)) to_send = [] # type: List[Tuple[int, Tuple[str, str]]] limited = False new_id = upto_token for _, stream_id, destinations, user_ids in self._queue[start_idx:]: + if stream_id <= from_token: + # Paranoia check that we are actually only sending states that + # are have stream_id strictly greater than from_token. We should + # never hit this. + logger.warning( + "Tried returning presence federation stream ID: %d less than from_token: %d (next_id: %d, len: %d)", + stream_id, + from_token, + self._next_id, + len(self._queue), + ) + continue + if stream_id > upto_token: break diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 61271cd084..ce330e79cc 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -509,6 +509,14 @@ def test_send_and_get(self): self.assertCountEqual(rows, expected_rows) + now_token = self.queue.get_current_token(self.instance_name) + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", upto_token, now_token, 10) + ) + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + self.assertCountEqual(rows, []) + def test_send_and_get_split(self): state1 = UserPresenceState.default("@user1:test") state2 = UserPresenceState.default("@user2:test") @@ -538,6 +546,20 @@ def test_send_and_get_split(self): self.assertCountEqual(rows, expected_rows) + now_token = self.queue.get_current_token(self.instance_name) + rows, upto_token, limited = self.get_success( + self.queue.get_replication_rows("master", upto_token, now_token, 10) + ) + + self.assertEqual(upto_token, now_token) + self.assertFalse(limited) + + expected_rows = [ + (2, ("dest3", "@user3:test")), + ] + + self.assertCountEqual(rows, expected_rows) + def test_clear_queue_all(self): state1 = UserPresenceState.default("@user1:test") state2 = UserPresenceState.default("@user2:test") From 0085dc5abc614579f3adbd9e6d2cbdd41facef00 Mon Sep 17 00:00:00 2001 From: ThibF Date: Thu, 29 Apr 2021 09:31:45 +0000 Subject: [PATCH 109/619] Delete room endpoint (#9889) Support the delete of a room through DELETE request and mark previous request as deprecated through documentation. Signed-off-by: Thibault Ferrante --- changelog.d/9889.feature | 1 + changelog.d/9889.removal | 1 + docs/admin_api/rooms.md | 11 ++- synapse/rest/admin/rooms.py | 134 +++++++++++++++++++++++----------- tests/rest/admin/test_room.py | 45 +++++++----- 5 files changed, 128 insertions(+), 64 deletions(-) create mode 100644 changelog.d/9889.feature create mode 100644 changelog.d/9889.removal diff --git a/changelog.d/9889.feature b/changelog.d/9889.feature new file mode 100644 index 0000000000..74d46f222e --- /dev/null +++ b/changelog.d/9889.feature @@ -0,0 +1 @@ +Add support for `DELETE /_synapse/admin/v1/rooms/`. \ No newline at end of file diff --git a/changelog.d/9889.removal b/changelog.d/9889.removal new file mode 100644 index 0000000000..398b9e129b --- /dev/null +++ b/changelog.d/9889.removal @@ -0,0 +1 @@ +Mark as deprecated `POST /_synapse/admin/v1/rooms//delete`. \ No newline at end of file diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index bc737b30f5..01d3882426 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -427,7 +427,7 @@ the new room. Users on other servers will be unaffected. The API is: ``` -POST /_synapse/admin/v1/rooms//delete +DELETE /_synapse/admin/v1/rooms/ ``` with a body of: @@ -528,6 +528,15 @@ You will have to manually handle, if you so choose, the following: * Users that would have been booted from the room (and will have been force-joined to the Content Violation room). * Removal of the Content Violation room if desired. +## Deprecated endpoint + +The previous deprecated API will be removed in a future release, it was: + +``` +POST /_synapse/admin/v1/rooms//delete +``` + +It behaves the same way than the current endpoint except the path and the method. # Make Room Admin API diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index d0cf121743..f289ffe3d0 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -37,9 +37,11 @@ from synapse.util import json_decoder if TYPE_CHECKING: + from synapse.api.auth import Auth + from synapse.handlers.pagination import PaginationHandler + from synapse.handlers.room import RoomShutdownHandler from synapse.server import HomeServer - logger = logging.getLogger(__name__) @@ -146,50 +148,14 @@ def __init__(self, hs: "HomeServer"): async def on_POST( self, request: SynapseRequest, room_id: str ) -> Tuple[int, JsonDict]: - requester = await self.auth.get_user_by_req(request) - await assert_user_is_admin(self.auth, requester.user) - - content = parse_json_object_from_request(request) - - block = content.get("block", False) - if not isinstance(block, bool): - raise SynapseError( - HTTPStatus.BAD_REQUEST, - "Param 'block' must be a boolean, if given", - Codes.BAD_JSON, - ) - - purge = content.get("purge", True) - if not isinstance(purge, bool): - raise SynapseError( - HTTPStatus.BAD_REQUEST, - "Param 'purge' must be a boolean, if given", - Codes.BAD_JSON, - ) - - force_purge = content.get("force_purge", False) - if not isinstance(force_purge, bool): - raise SynapseError( - HTTPStatus.BAD_REQUEST, - "Param 'force_purge' must be a boolean, if given", - Codes.BAD_JSON, - ) - - ret = await self.room_shutdown_handler.shutdown_room( - room_id=room_id, - new_room_user_id=content.get("new_room_user_id"), - new_room_name=content.get("room_name"), - message=content.get("message"), - requester_user_id=requester.user.to_string(), - block=block, + return await _delete_room( + request, + room_id, + self.auth, + self.room_shutdown_handler, + self.pagination_handler, ) - # Purge room - if purge: - await self.pagination_handler.purge_room(room_id, force=force_purge) - - return (200, ret) - class ListRoomRestServlet(RestServlet): """ @@ -282,7 +248,22 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: class RoomRestServlet(RestServlet): - """Get room details. + """Manage a room. + + On GET : Get details of a room. + + On DELETE : Delete a room from server. + + It is a combination and improvement of shutdown and purge room. + + Shuts down a room by removing all local users from the room. + Blocking all future invites and joins to the room is optional. + + If desired any local aliases will be repointed to a new room + created by `new_room_user_id` and kicked users will be auto- + joined to the new room. + + If 'purge' is true, it will remove all traces of a room from the database. TODO: Add on_POST to allow room creation without joining the room """ @@ -293,6 +274,8 @@ def __init__(self, hs: "HomeServer"): self.hs = hs self.auth = hs.get_auth() self.store = hs.get_datastore() + self.room_shutdown_handler = hs.get_room_shutdown_handler() + self.pagination_handler = hs.get_pagination_handler() async def on_GET( self, request: SynapseRequest, room_id: str @@ -308,6 +291,17 @@ async def on_GET( return (200, ret) + async def on_DELETE( + self, request: SynapseRequest, room_id: str + ) -> Tuple[int, JsonDict]: + return await _delete_room( + request, + room_id, + self.auth, + self.room_shutdown_handler, + self.pagination_handler, + ) + class RoomMembersRestServlet(RestServlet): """ @@ -694,3 +688,55 @@ async def on_GET( ) return 200, results + + +async def _delete_room( + request: SynapseRequest, + room_id: str, + auth: "Auth", + room_shutdown_handler: "RoomShutdownHandler", + pagination_handler: "PaginationHandler", +) -> Tuple[int, JsonDict]: + requester = await auth.get_user_by_req(request) + await assert_user_is_admin(auth, requester.user) + + content = parse_json_object_from_request(request) + + block = content.get("block", False) + if not isinstance(block, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'block' must be a boolean, if given", + Codes.BAD_JSON, + ) + + purge = content.get("purge", True) + if not isinstance(purge, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'purge' must be a boolean, if given", + Codes.BAD_JSON, + ) + + force_purge = content.get("force_purge", False) + if not isinstance(force_purge, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'force_purge' must be a boolean, if given", + Codes.BAD_JSON, + ) + + ret = await room_shutdown_handler.shutdown_room( + room_id=room_id, + new_room_user_id=content.get("new_room_user_id"), + new_room_name=content.get("room_name"), + message=content.get("message"), + requester_user_id=requester.user.to_string(), + block=block, + ) + + # Purge room + if purge: + await pagination_handler.purge_room(room_id, force=force_purge) + + return (200, ret) diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 6b84188120..ee071c2477 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -17,6 +17,8 @@ from typing import List, Optional from unittest.mock import Mock +from parameterized import parameterized_class + import synapse.rest.admin from synapse.api.constants import EventTypes, Membership from synapse.api.errors import Codes @@ -144,6 +146,13 @@ def _assert_peek(self, room_id, expect_code): ) +@parameterized_class( + ("method", "url_template"), + [ + ("POST", "/_synapse/admin/v1/rooms/%s/delete"), + ("DELETE", "/_synapse/admin/v1/rooms/%s"), + ], +) class DeleteRoomTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets, @@ -175,7 +184,7 @@ def prepare(self, reactor, clock, hs): self.room_id = self.helper.create_room_as( self.other_user, tok=self.other_user_tok ) - self.url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id + self.url = self.url_template % self.room_id def test_requester_is_no_admin(self): """ @@ -183,7 +192,7 @@ def test_requester_is_no_admin(self): """ channel = self.make_request( - "POST", + self.method, self.url, json.dumps({}), access_token=self.other_user_tok, @@ -196,10 +205,10 @@ def test_room_does_not_exist(self): """ Check that unknown rooms/server return error 404. """ - url = "/_synapse/admin/v1/rooms/!unknown:test/delete" + url = self.url_template % "!unknown:test" channel = self.make_request( - "POST", + self.method, url, json.dumps({}), access_token=self.admin_user_tok, @@ -212,10 +221,10 @@ def test_room_is_not_valid(self): """ Check that invalid room names, return an error 400. """ - url = "/_synapse/admin/v1/rooms/invalidroom/delete" + url = self.url_template % "invalidroom" channel = self.make_request( - "POST", + self.method, url, json.dumps({}), access_token=self.admin_user_tok, @@ -234,7 +243,7 @@ def test_new_room_user_does_not_exist(self): body = json.dumps({"new_room_user_id": "@unknown:test"}) channel = self.make_request( - "POST", + self.method, self.url, content=body.encode(encoding="utf_8"), access_token=self.admin_user_tok, @@ -253,7 +262,7 @@ def test_new_room_user_is_not_local(self): body = json.dumps({"new_room_user_id": "@not:exist.bla"}) channel = self.make_request( - "POST", + self.method, self.url, content=body.encode(encoding="utf_8"), access_token=self.admin_user_tok, @@ -272,7 +281,7 @@ def test_block_is_not_bool(self): body = json.dumps({"block": "NotBool"}) channel = self.make_request( - "POST", + self.method, self.url, content=body.encode(encoding="utf_8"), access_token=self.admin_user_tok, @@ -288,7 +297,7 @@ def test_purge_is_not_bool(self): body = json.dumps({"purge": "NotBool"}) channel = self.make_request( - "POST", + self.method, self.url, content=body.encode(encoding="utf_8"), access_token=self.admin_user_tok, @@ -314,7 +323,7 @@ def test_purge_room_and_block(self): body = json.dumps({"block": True, "purge": True}) channel = self.make_request( - "POST", + self.method, self.url.encode("ascii"), content=body.encode(encoding="utf_8"), access_token=self.admin_user_tok, @@ -347,7 +356,7 @@ def test_purge_room_and_not_block(self): body = json.dumps({"block": False, "purge": True}) channel = self.make_request( - "POST", + self.method, self.url.encode("ascii"), content=body.encode(encoding="utf_8"), access_token=self.admin_user_tok, @@ -381,7 +390,7 @@ def test_block_room_and_not_purge(self): body = json.dumps({"block": False, "purge": False}) channel = self.make_request( - "POST", + self.method, self.url.encode("ascii"), content=body.encode(encoding="utf_8"), access_token=self.admin_user_tok, @@ -426,10 +435,9 @@ def test_shutdown_room_consent(self): self._is_member(room_id=self.room_id, user_id=self.other_user) # Test that the admin can still send shutdown - url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id channel = self.make_request( - "POST", - url.encode("ascii"), + self.method, + self.url, json.dumps({"new_room_user_id": self.admin_user}), access_token=self.admin_user_tok, ) @@ -473,10 +481,9 @@ def test_shutdown_room_block_peek(self): self._is_member(room_id=self.room_id, user_id=self.other_user) # Test that the admin can still send shutdown - url = "/_synapse/admin/v1/rooms/%s/delete" % self.room_id channel = self.make_request( - "POST", - url.encode("ascii"), + self.method, + self.url, json.dumps({"new_room_user_id": self.admin_user}), access_token=self.admin_user_tok, ) From e9444cc74d73f6367dedcfe406e3f1d9ff3d5414 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 29 Apr 2021 11:45:37 +0100 Subject: [PATCH 110/619] 1.33.0rc2 --- CHANGES.md | 9 +++++++++ changelog.d/9900.bugfix | 1 - synapse/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/9900.bugfix diff --git a/CHANGES.md b/CHANGES.md index 9a41607679..629d4a180d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.33.0rc2 (2021-04-29) +============================== + +Bugfixes +-------- + +- Fix tight loop handling presence replication when using workers. Introduced in v1.33.0rc1. ([\#9900](https://github.com/matrix-org/synapse/issues/9900)) + + Synapse 1.33.0rc1 (2021-04-28) ============================== diff --git a/changelog.d/9900.bugfix b/changelog.d/9900.bugfix deleted file mode 100644 index a8470fca3f..0000000000 --- a/changelog.d/9900.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix tight loop handling presence replication when using workers. Introduced in v1.33.0rc1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 5bbaa62de2..319c52be2c 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.33.0rc1" +__version__ = "1.33.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From bb4b11846f3bdd539a1671eb8f1db8ee1a0bf57a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 29 Apr 2021 07:17:28 -0400 Subject: [PATCH 111/619] Add missing type hints to handlers and fix a Spam Checker type hint. (#9896) The user_may_create_room_alias method on spam checkers declared the room_alias parameter as a str when in reality it is passed a RoomAlias object. --- changelog.d/9896.bugfix | 1 + changelog.d/9896.misc | 1 + synapse/events/spamcheck.py | 5 ++- synapse/handlers/directory.py | 59 ++++++++++++++++------------ synapse/handlers/identity.py | 9 +++-- synapse/handlers/message.py | 24 +++++++---- synapse/handlers/room_member.py | 2 +- synapse/handlers/ui_auth/checkers.py | 35 +++++++++-------- 8 files changed, 82 insertions(+), 54 deletions(-) create mode 100644 changelog.d/9896.bugfix create mode 100644 changelog.d/9896.misc diff --git a/changelog.d/9896.bugfix b/changelog.d/9896.bugfix new file mode 100644 index 0000000000..07a8e87f9f --- /dev/null +++ b/changelog.d/9896.bugfix @@ -0,0 +1 @@ +Correct the type hint for the `user_may_create_room_alias` method of spam checkers. It is provided a `RoomAlias`, not a `str`. diff --git a/changelog.d/9896.misc b/changelog.d/9896.misc new file mode 100644 index 0000000000..e41c7d1f02 --- /dev/null +++ b/changelog.d/9896.misc @@ -0,0 +1 @@ +Add type hints to the `synapse.handlers` module. diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index 7118d5f52d..d5fa195094 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -20,6 +20,7 @@ from synapse.rest.media.v1._base import FileInfo from synapse.rest.media.v1.media_storage import ReadableFileWrapper from synapse.spam_checker_api import RegistrationBehaviour +from synapse.types import RoomAlias from synapse.util.async_helpers import maybe_awaitable if TYPE_CHECKING: @@ -113,7 +114,9 @@ async def user_may_create_room(self, userid: str) -> bool: return True - async def user_may_create_room_alias(self, userid: str, room_alias: str) -> bool: + async def user_may_create_room_alias( + self, userid: str, room_alias: RoomAlias + ) -> bool: """Checks if a given user may create a room alias If this method returns false, the association request will be rejected. diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 90932316f3..de1b14cde3 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -14,7 +14,7 @@ import logging import string -from typing import Iterable, List, Optional +from typing import TYPE_CHECKING, Iterable, List, Optional from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes from synapse.api.errors import ( @@ -27,15 +27,19 @@ SynapseError, ) from synapse.appservice import ApplicationService -from synapse.types import Requester, RoomAlias, UserID, get_domain_from_id +from synapse.storage.databases.main.directory import RoomAliasMapping +from synapse.types import JsonDict, Requester, RoomAlias, UserID, get_domain_from_id from ._base import BaseHandler +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) class DirectoryHandler(BaseHandler): - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__(hs) self.state = hs.get_state_handler() @@ -60,7 +64,7 @@ async def _create_association( room_id: str, servers: Optional[Iterable[str]] = None, creator: Optional[str] = None, - ): + ) -> None: # general association creation for both human users and app services for wchar in string.whitespace: @@ -104,8 +108,9 @@ async def create_association( """ user_id = requester.user.to_string() + room_alias_str = room_alias.to_string() - if len(room_alias.to_string()) > MAX_ALIAS_LENGTH: + if len(room_alias_str) > MAX_ALIAS_LENGTH: raise SynapseError( 400, "Can't create aliases longer than %s characters" % MAX_ALIAS_LENGTH, @@ -114,7 +119,7 @@ async def create_association( service = requester.app_service if service: - if not service.is_interested_in_alias(room_alias.to_string()): + if not service.is_interested_in_alias(room_alias_str): raise SynapseError( 400, "This application service has not reserved this kind of alias.", @@ -138,7 +143,7 @@ async def create_association( raise AuthError(403, "This user is not permitted to create this alias") if not self.config.is_alias_creation_allowed( - user_id, room_id, room_alias.to_string() + user_id, room_id, room_alias_str ): # Lets just return a generic message, as there may be all sorts of # reasons why we said no. TODO: Allow configurable error messages @@ -211,7 +216,7 @@ async def delete_association( async def delete_appservice_association( self, service: ApplicationService, room_alias: RoomAlias - ): + ) -> None: if not service.is_interested_in_alias(room_alias.to_string()): raise SynapseError( 400, @@ -220,7 +225,7 @@ async def delete_appservice_association( ) await self._delete_association(room_alias) - async def _delete_association(self, room_alias: RoomAlias): + async def _delete_association(self, room_alias: RoomAlias) -> str: if not self.hs.is_mine(room_alias): raise SynapseError(400, "Room alias must be local") @@ -228,17 +233,19 @@ async def _delete_association(self, room_alias: RoomAlias): return room_id - async def get_association(self, room_alias: RoomAlias): + async def get_association(self, room_alias: RoomAlias) -> JsonDict: room_id = None if self.hs.is_mine(room_alias): - result = await self.get_association_from_room_alias(room_alias) + result = await self.get_association_from_room_alias( + room_alias + ) # type: Optional[RoomAliasMapping] if result: room_id = result.room_id servers = result.servers else: try: - result = await self.federation.make_query( + fed_result = await self.federation.make_query( destination=room_alias.domain, query_type="directory", args={"room_alias": room_alias.to_string()}, @@ -248,13 +255,13 @@ async def get_association(self, room_alias: RoomAlias): except CodeMessageException as e: logging.warning("Error retrieving alias") if e.code == 404: - result = None + fed_result = None else: raise - if result and "room_id" in result and "servers" in result: - room_id = result["room_id"] - servers = result["servers"] + if fed_result and "room_id" in fed_result and "servers" in fed_result: + room_id = fed_result["room_id"] + servers = fed_result["servers"] if not room_id: raise SynapseError( @@ -275,7 +282,7 @@ async def get_association(self, room_alias: RoomAlias): return {"room_id": room_id, "servers": servers} - async def on_directory_query(self, args): + async def on_directory_query(self, args: JsonDict) -> JsonDict: room_alias = RoomAlias.from_string(args["room_alias"]) if not self.hs.is_mine(room_alias): raise SynapseError(400, "Room Alias is not hosted on this homeserver") @@ -293,7 +300,7 @@ async def on_directory_query(self, args): async def _update_canonical_alias( self, requester: Requester, user_id: str, room_id: str, room_alias: RoomAlias - ): + ) -> None: """ Send an updated canonical alias event if the removed alias was set as the canonical alias or listed in the alt_aliases field. @@ -344,7 +351,9 @@ async def _update_canonical_alias( ratelimit=False, ) - async def get_association_from_room_alias(self, room_alias: RoomAlias): + async def get_association_from_room_alias( + self, room_alias: RoomAlias + ) -> Optional[RoomAliasMapping]: result = await self.store.get_association_from_room_alias(room_alias) if not result: # Query AS to see if it exists @@ -372,7 +381,7 @@ def can_modify_alias(self, alias: RoomAlias, user_id: Optional[str] = None) -> b # either no interested services, or no service with an exclusive lock return True - async def _user_can_delete_alias(self, alias: RoomAlias, user_id: str): + async def _user_can_delete_alias(self, alias: RoomAlias, user_id: str) -> bool: """Determine whether a user can delete an alias. One of the following must be true: @@ -394,14 +403,13 @@ async def _user_can_delete_alias(self, alias: RoomAlias, user_id: str): if not room_id: return False - res = await self.auth.check_can_change_room_list( + return await self.auth.check_can_change_room_list( room_id, UserID.from_string(user_id) ) - return res async def edit_published_room_list( self, requester: Requester, room_id: str, visibility: str - ): + ) -> None: """Edit the entry of the room in the published room list. requester @@ -469,7 +477,7 @@ async def edit_published_room_list( async def edit_published_appservice_room_list( self, appservice_id: str, network_id: str, room_id: str, visibility: str - ): + ) -> None: """Add or remove a room from the appservice/network specific public room list. @@ -499,5 +507,4 @@ async def get_aliases_for_room( room_id, requester.user.to_string() ) - aliases = await self.store.get_aliases_for_room(room_id) - return aliases + return await self.store.get_aliases_for_room(room_id) diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 0b3b1fadb5..33d16fbf9c 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -17,7 +17,7 @@ """Utilities for interacting with Identity Servers""" import logging import urllib.parse -from typing import Awaitable, Callable, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Tuple from synapse.api.errors import ( CodeMessageException, @@ -41,13 +41,16 @@ from ._base import BaseHandler +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) id_server_scheme = "https://" class IdentityHandler(BaseHandler): - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__(hs) # An HTTP client for contacting trusted URLs. @@ -80,7 +83,7 @@ async def ratelimit_request_token_requests( request: SynapseRequest, medium: str, address: str, - ): + ) -> None: """Used to ratelimit requests to `/requestToken` by IP and address. Args: diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index ec8eb21674..49f8aa25ea 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -15,7 +15,7 @@ # limitations under the License. import logging import random -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple from canonicaljson import encode_canonical_json @@ -66,7 +66,7 @@ class MessageHandler: """Contains some read only APIs to get state about a room""" - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.auth = hs.get_auth() self.clock = hs.get_clock() self.state = hs.get_state_handler() @@ -91,7 +91,7 @@ async def get_room_data( room_id: str, event_type: str, state_key: str, - ) -> dict: + ) -> Optional[EventBase]: """Get data from a room. Args: @@ -115,6 +115,10 @@ async def get_room_data( data = await self.state.get_current_state(room_id, event_type, state_key) elif membership == Membership.LEAVE: key = (event_type, state_key) + # If the membership is not JOIN, then the event ID should exist. + assert ( + membership_event_id is not None + ), "check_user_in_room_or_world_readable returned invalid data" room_state = await self.state_store.get_state_for_events( [membership_event_id], StateFilter.from_types([key]) ) @@ -186,10 +190,12 @@ async def get_state_events( event = last_events[0] if visible_events: - room_state = await self.state_store.get_state_for_events( + room_state_events = await self.state_store.get_state_for_events( [event.event_id], state_filter=state_filter ) - room_state = room_state[event.event_id] + room_state = room_state_events[ + event.event_id + ] # type: Mapping[Any, EventBase] else: raise AuthError( 403, @@ -210,10 +216,14 @@ async def get_state_events( ) room_state = await self.store.get_events(state_ids.values()) elif membership == Membership.LEAVE: - room_state = await self.state_store.get_state_for_events( + # If the membership is not JOIN, then the event ID should exist. + assert ( + membership_event_id is not None + ), "check_user_in_room_or_world_readable returned invalid data" + room_state_events = await self.state_store.get_state_for_events( [membership_event_id], state_filter=state_filter ) - room_state = room_state[membership_event_id] + room_state = room_state_events[membership_event_id] now = self.clock.time_msec() events = await self._event_serializer.serialize_events( diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 2c5bada1d8..20700fc5a8 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -1044,7 +1044,7 @@ async def _is_server_notice_room(self, room_id: str) -> bool: class RoomMemberMasterHandler(RoomMemberHandler): - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__(hs) self.distributor = hs.get_distributor() diff --git a/synapse/handlers/ui_auth/checkers.py b/synapse/handlers/ui_auth/checkers.py index 0eeb7c03f2..5414ce77d8 100644 --- a/synapse/handlers/ui_auth/checkers.py +++ b/synapse/handlers/ui_auth/checkers.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from typing import Any +from typing import TYPE_CHECKING, Any from twisted.web.client import PartialDownloadError @@ -22,13 +22,16 @@ from synapse.config.emailconfig import ThreepidBehaviour from synapse.util import json_decoder +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) class UserInteractiveAuthChecker: """Abstract base class for an interactive auth checker""" - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): pass def is_enabled(self) -> bool: @@ -57,10 +60,10 @@ async def check_auth(self, authdict: dict, clientip: str) -> Any: class DummyAuthChecker(UserInteractiveAuthChecker): AUTH_TYPE = LoginType.DUMMY - def is_enabled(self): + def is_enabled(self) -> bool: return True - async def check_auth(self, authdict, clientip): + async def check_auth(self, authdict: dict, clientip: str) -> Any: return True @@ -70,24 +73,24 @@ class TermsAuthChecker(UserInteractiveAuthChecker): def is_enabled(self): return True - async def check_auth(self, authdict, clientip): + async def check_auth(self, authdict: dict, clientip: str) -> Any: return True class RecaptchaAuthChecker(UserInteractiveAuthChecker): AUTH_TYPE = LoginType.RECAPTCHA - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): super().__init__(hs) self._enabled = bool(hs.config.recaptcha_private_key) self._http_client = hs.get_proxied_http_client() self._url = hs.config.recaptcha_siteverify_api self._secret = hs.config.recaptcha_private_key - def is_enabled(self): + def is_enabled(self) -> bool: return self._enabled - async def check_auth(self, authdict, clientip): + async def check_auth(self, authdict: dict, clientip: str) -> Any: try: user_response = authdict["response"] except KeyError: @@ -132,11 +135,11 @@ async def check_auth(self, authdict, clientip): class _BaseThreepidAuthChecker: - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.hs = hs self.store = hs.get_datastore() - async def _check_threepid(self, medium, authdict): + async def _check_threepid(self, medium: str, authdict: dict) -> dict: if "threepid_creds" not in authdict: raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM) @@ -206,31 +209,31 @@ async def _check_threepid(self, medium, authdict): class EmailIdentityAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker): AUTH_TYPE = LoginType.EMAIL_IDENTITY - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): UserInteractiveAuthChecker.__init__(self, hs) _BaseThreepidAuthChecker.__init__(self, hs) - def is_enabled(self): + def is_enabled(self) -> bool: return self.hs.config.threepid_behaviour_email in ( ThreepidBehaviour.REMOTE, ThreepidBehaviour.LOCAL, ) - async def check_auth(self, authdict, clientip): + async def check_auth(self, authdict: dict, clientip: str) -> Any: return await self._check_threepid("email", authdict) class MsisdnAuthChecker(UserInteractiveAuthChecker, _BaseThreepidAuthChecker): AUTH_TYPE = LoginType.MSISDN - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): UserInteractiveAuthChecker.__init__(self, hs) _BaseThreepidAuthChecker.__init__(self, hs) - def is_enabled(self): + def is_enabled(self) -> bool: return bool(self.hs.config.account_threepid_delegate_msisdn) - async def check_auth(self, authdict, clientip): + async def check_auth(self, authdict: dict, clientip: str) -> Any: return await self._check_threepid("msisdn", authdict) From d11f2dfee519a4136def4169cef0ef218ebf1e19 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 29 Apr 2021 14:31:14 +0100 Subject: [PATCH 112/619] typo in changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 629d4a180d..bdeb614b9e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Synapse 1.33.0rc2 (2021-04-29) Bugfixes -------- -- Fix tight loop handling presence replication when using workers. Introduced in v1.33.0rc1. ([\#9900](https://github.com/matrix-org/synapse/issues/9900)) +- Fix tight loop when handling presence replication when using workers. Introduced in v1.33.0rc1. ([\#9900](https://github.com/matrix-org/synapse/issues/9900)) Synapse 1.33.0rc1 (2021-04-28) From 56c4b47df36a99d2fc57a64013a4f482e25ee097 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Fri, 30 Apr 2021 15:36:05 +0100 Subject: [PATCH 113/619] Build Debian packages for Ubuntu 21.04 Hirsute (#9909) Signed-off-by: Dan Callahan --- changelog.d/9909.feature | 1 + scripts-dev/build_debian_packages | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9909.feature diff --git a/changelog.d/9909.feature b/changelog.d/9909.feature new file mode 100644 index 0000000000..41aba5cca6 --- /dev/null +++ b/changelog.d/9909.feature @@ -0,0 +1 @@ +Build Debian packages for Ubuntu 21.04 (Hirsute Hippo). diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index 3bb6e2c7ea..07d018db99 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -21,9 +21,10 @@ DISTS = ( "debian:buster", "debian:bullseye", "debian:sid", - "ubuntu:bionic", - "ubuntu:focal", - "ubuntu:groovy", + "ubuntu:bionic", # 18.04 LTS (our EOL forced by Py36 on 2021-12-23) + "ubuntu:focal", # 20.04 LTS (our EOL forced by Py38 on 2024-10-14) + "ubuntu:groovy", # 20.10 (EOL 2021-07-07) + "ubuntu:hirsute", # 21.04 (EOL 2022-01-05) ) DESC = '''\ From b85821aca2c3cfc1c732dfcc0c1d6758a263487a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 4 May 2021 13:28:59 +0100 Subject: [PATCH 114/619] Add port parameter to the sample config for psycopg2 args (#9911) Adds the `port` option with the default value to the sample config file. --- changelog.d/9911.doc | 1 + docs/sample_config.yaml | 1 + synapse/config/database.py | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/9911.doc diff --git a/changelog.d/9911.doc b/changelog.d/9911.doc new file mode 100644 index 0000000000..f7fd9f1ba9 --- /dev/null +++ b/changelog.d/9911.doc @@ -0,0 +1 @@ +Add `port` argument to the Postgres database sample config section. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index e0350279ad..d013725cdc 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -810,6 +810,7 @@ caches: # password: secretpassword # database: synapse # host: localhost +# port: 5432 # cp_min: 5 # cp_max: 10 # diff --git a/synapse/config/database.py b/synapse/config/database.py index 79a02706b4..c76ef1e1de 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -58,6 +58,7 @@ # password: secretpassword # database: synapse # host: localhost +# port: 5432 # cp_min: 5 # cp_max: 10 # From e3bc4617fcd5c858ff02cf2d443b898db87ae8a5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 4 May 2021 15:14:22 +0100 Subject: [PATCH 115/619] Time external cache response time (#9904) --- changelog.d/9904.misc | 1 + synapse/replication/tcp/external_cache.py | 36 ++++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 changelog.d/9904.misc diff --git a/changelog.d/9904.misc b/changelog.d/9904.misc new file mode 100644 index 0000000000..3db1e625ae --- /dev/null +++ b/changelog.d/9904.misc @@ -0,0 +1 @@ +Time response time for external cache requests. diff --git a/synapse/replication/tcp/external_cache.py b/synapse/replication/tcp/external_cache.py index 1a3b051e3c..b402f82810 100644 --- a/synapse/replication/tcp/external_cache.py +++ b/synapse/replication/tcp/external_cache.py @@ -15,7 +15,7 @@ import logging from typing import TYPE_CHECKING, Any, Optional -from prometheus_client import Counter +from prometheus_client import Counter, Histogram from synapse.logging.context import make_deferred_yieldable from synapse.util import json_decoder, json_encoder @@ -35,6 +35,20 @@ labelnames=["cache_name", "hit"], ) +response_timer = Histogram( + "synapse_external_cache_response_time_seconds", + "Time taken to get a response from Redis for a cache get/set request", + labelnames=["method"], + buckets=( + 0.001, + 0.002, + 0.005, + 0.01, + 0.02, + 0.05, + ), +) + logger = logging.getLogger(__name__) @@ -72,13 +86,14 @@ async def set(self, cache_name: str, key: str, value: Any, expiry_ms: int) -> No logger.debug("Caching %s %s: %r", cache_name, key, encoded_value) - return await make_deferred_yieldable( - self._redis_connection.set( - self._get_redis_key(cache_name, key), - encoded_value, - pexpire=expiry_ms, + with response_timer.labels("set").time(): + return await make_deferred_yieldable( + self._redis_connection.set( + self._get_redis_key(cache_name, key), + encoded_value, + pexpire=expiry_ms, + ) ) - ) async def get(self, cache_name: str, key: str) -> Optional[Any]: """Look up a key/value in the named cache.""" @@ -86,9 +101,10 @@ async def get(self, cache_name: str, key: str) -> Optional[Any]: if self._redis_connection is None: return None - result = await make_deferred_yieldable( - self._redis_connection.get(self._get_redis_key(cache_name, key)) - ) + with response_timer.labels("get").time(): + result = await make_deferred_yieldable( + self._redis_connection.get(self._get_redis_key(cache_name, key)) + ) logger.debug("Got cache result %s %s: %r", cache_name, key, result) From 0644ac0989d06fc0ba5d08a5412d65d75f7d8db2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 5 May 2021 14:15:54 +0100 Subject: [PATCH 116/619] 1.33.0 --- CHANGES.md | 9 +++++++++ changelog.d/9909.feature | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/9909.feature diff --git a/CHANGES.md b/CHANGES.md index bdeb614b9e..206859cff7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.33.0 (2021-05-05) +=========================== + +Features +-------- + +- Build Debian packages for Ubuntu 21.04 (Hirsute Hippo). ([\#9909](https://github.com/matrix-org/synapse/issues/9909)) + + Synapse 1.33.0rc2 (2021-04-29) ============================== diff --git a/changelog.d/9909.feature b/changelog.d/9909.feature deleted file mode 100644 index 41aba5cca6..0000000000 --- a/changelog.d/9909.feature +++ /dev/null @@ -1 +0,0 @@ -Build Debian packages for Ubuntu 21.04 (Hirsute Hippo). diff --git a/debian/changelog b/debian/changelog index fd33bfda5c..b54d3f2bc6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.33.0) stable; urgency=medium + + * New synapse release 1.33.0. + + -- Synapse Packaging team Wed, 05 May 2021 14:15:27 +0100 + matrix-synapse-py3 (1.32.2) stable; urgency=medium * New synapse release 1.32.2. diff --git a/synapse/__init__.py b/synapse/__init__.py index 319c52be2c..5eac40730a 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.33.0rc2" +__version__ = "1.33.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From e9eb3549d32a6f93d07de8dbd5e1ebe54c8d8278 Mon Sep 17 00:00:00 2001 From: "DeepBlueV7.X" Date: Wed, 5 May 2021 13:37:56 +0000 Subject: [PATCH 117/619] Leave out optional keys from /sync (#9919) This leaves out all optional keys from /sync. This should be fine for all clients tested against conduit already, but it may break some clients, as such we should check, that at least most of them don't break horribly and maybe back out some of the individual changes. (We can probably always leave out groups for example, while the others may cause more issues.) Signed-off-by: Nicolas Werner --- changelog.d/9919.feature | 1 + synapse/rest/client/v2_alpha/sync.py | 62 +++++++++++++------ tests/rest/client/v2_alpha/test_sync.py | 30 +-------- .../test_resource_limits_server_notices.py | 8 ++- 4 files changed, 51 insertions(+), 50 deletions(-) create mode 100644 changelog.d/9919.feature diff --git a/changelog.d/9919.feature b/changelog.d/9919.feature new file mode 100644 index 0000000000..07747505d2 --- /dev/null +++ b/changelog.d/9919.feature @@ -0,0 +1 @@ +Omit empty fields from the `/sync` response. Contributed by @deepbluev7. diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index 95ee3f1b84..5f85653330 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -14,6 +14,7 @@ import itertools import logging +from collections import defaultdict from typing import TYPE_CHECKING, Tuple from synapse.api.constants import PresenceState @@ -229,24 +230,49 @@ async def encode_response(self, time_now, sync_result, access_token_id, filter): ) logger.debug("building sync response dict") - return { - "account_data": {"events": sync_result.account_data}, - "to_device": {"events": sync_result.to_device}, - "device_lists": { - "changed": list(sync_result.device_lists.changed), - "left": list(sync_result.device_lists.left), - }, - "presence": SyncRestServlet.encode_presence(sync_result.presence, time_now), - "rooms": {"join": joined, "invite": invited, "leave": archived}, - "groups": { - "join": sync_result.groups.join, - "invite": sync_result.groups.invite, - "leave": sync_result.groups.leave, - }, - "device_one_time_keys_count": sync_result.device_one_time_keys_count, - "org.matrix.msc2732.device_unused_fallback_key_types": sync_result.device_unused_fallback_key_types, - "next_batch": await sync_result.next_batch.to_string(self.store), - } + + response: dict = defaultdict(dict) + response["next_batch"] = await sync_result.next_batch.to_string(self.store) + + if sync_result.account_data: + response["account_data"] = {"events": sync_result.account_data} + if sync_result.presence: + response["presence"] = SyncRestServlet.encode_presence( + sync_result.presence, time_now + ) + + if sync_result.to_device: + response["to_device"] = {"events": sync_result.to_device} + + if sync_result.device_lists.changed: + response["device_lists"]["changed"] = list(sync_result.device_lists.changed) + if sync_result.device_lists.left: + response["device_lists"]["left"] = list(sync_result.device_lists.left) + + if sync_result.device_one_time_keys_count: + response[ + "device_one_time_keys_count" + ] = sync_result.device_one_time_keys_count + if sync_result.device_unused_fallback_key_types: + response[ + "org.matrix.msc2732.device_unused_fallback_key_types" + ] = sync_result.device_unused_fallback_key_types + + if joined: + response["rooms"]["join"] = joined + if invited: + response["rooms"]["invite"] = invited + if archived: + response["rooms"]["leave"] = archived + + if sync_result.groups.join: + response["groups"]["join"] = sync_result.groups.join + if sync_result.groups.invite: + response["groups"]["invite"] = sync_result.groups.invite + if sync_result.groups.leave: + response["groups"]["leave"] = sync_result.groups.leave + + return response @staticmethod def encode_presence(events, time_now): diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index dbcbdf159a..74be5176d0 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -37,35 +37,7 @@ def test_sync_argless(self): channel = self.make_request("GET", "/sync") self.assertEqual(channel.code, 200) - self.assertTrue( - { - "next_batch", - "rooms", - "presence", - "account_data", - "to_device", - "device_lists", - }.issubset(set(channel.json_body.keys())) - ) - - def test_sync_presence_disabled(self): - """ - When presence is disabled, the key does not appear in /sync. - """ - self.hs.config.use_presence = False - - channel = self.make_request("GET", "/sync") - - self.assertEqual(channel.code, 200) - self.assertTrue( - { - "next_batch", - "rooms", - "account_data", - "to_device", - "device_lists", - }.issubset(set(channel.json_body.keys())) - ) + self.assertIn("next_batch", channel.json_body) class SyncFilterTestCase(unittest.HomeserverTestCase): diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index d46521ccdc..3245aa91ca 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -306,8 +306,9 @@ def test_no_invite_without_notice(self): channel = self.make_request("GET", "/sync?timeout=0", access_token=tok) - invites = channel.json_body["rooms"]["invite"] - self.assertEqual(len(invites), 0, invites) + self.assertNotIn( + "rooms", channel.json_body, "Got invites without server notice" + ) def test_invite_with_notice(self): """Tests that, if the MAU limit is hit, the server notices user invites each user @@ -364,7 +365,8 @@ def _trigger_notice_and_join(self): # We could also pick another user and sync with it, which would return an # invite to a system notices room, but it doesn't matter which user we're # using so we use the last one because it saves us an extra sync. - invites = channel.json_body["rooms"]["invite"] + if "rooms" in channel.json_body: + invites = channel.json_body["rooms"]["invite"] # Make sure we have an invite to process. self.assertEqual(len(invites), 1, invites) From d5305000f1c5799ffb6fcd64ad27e7bfd8ba2113 Mon Sep 17 00:00:00 2001 From: Christopher May-Townsend Date: Wed, 5 May 2021 16:33:04 +0100 Subject: [PATCH 118/619] Docker healthcheck timings - add startup delay and changed interval (#9913) * Add healthcheck startup delay by 5secs and reduced interval check to 15s to reduce waiting time for docker aware edge routers bringing an instance online --- changelog.d/9913.docker | 1 + docker/Dockerfile | 2 +- docker/README.md | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 changelog.d/9913.docker diff --git a/changelog.d/9913.docker b/changelog.d/9913.docker new file mode 100644 index 0000000000..93835e14cb --- /dev/null +++ b/changelog.d/9913.docker @@ -0,0 +1 @@ +Added startup_delay to docker healthcheck to reduce waiting time for coming online, updated readme for extra options, contributed by @Maquis196. diff --git a/docker/Dockerfile b/docker/Dockerfile index 4f5cd06d72..2bdc607e66 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -88,5 +88,5 @@ EXPOSE 8008/tcp 8009/tcp 8448/tcp ENTRYPOINT ["/start.py"] -HEALTHCHECK --interval=1m --timeout=5s \ +HEALTHCHECK --start-period=5s --interval=15s --timeout=5s \ CMD curl -fSs http://localhost:8008/health || exit 1 diff --git a/docker/README.md b/docker/README.md index a7d1e670fe..c8d3c4b3da 100644 --- a/docker/README.md +++ b/docker/README.md @@ -191,6 +191,16 @@ whilst running the above `docker run` commands. ``` --no-healthcheck ``` + +## Disabling the healthcheck in docker-compose file + +If you wish to disable the healthcheck via docker-compose, append the following to your service configuration. + +``` + healthcheck: + disable: true +``` + ## Setting custom healthcheck on docker run If you wish to point the healthcheck at a different port with docker command, add the following @@ -202,14 +212,15 @@ If you wish to point the healthcheck at a different port with docker command, ad ## Setting the healthcheck in docker-compose file You can add the following to set a custom healthcheck in a docker compose file. -You will need version >2.1 for this to work. +You will need docker-compose version >2.1 for this to work. ``` healthcheck: test: ["CMD", "curl", "-fSs", "http://localhost:8008/health"] - interval: 1m - timeout: 10s + interval: 15s + timeout: 5s retries: 3 + start_period: 5s ``` ## Using jemalloc From d0aee697ac0587c005bc1048f5036979331f1101 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 5 May 2021 16:49:34 +0100 Subject: [PATCH 119/619] Use get_current_users_in_room from store and not StateHandler (#9910) --- changelog.d/9910.bugfix | 1 + changelog.d/9910.feature | 1 + synapse/handlers/directory.py | 4 ++-- synapse/handlers/events.py | 2 +- synapse/handlers/message.py | 2 +- synapse/handlers/presence.py | 2 +- synapse/handlers/room.py | 2 +- synapse/handlers/sync.py | 6 +++--- synapse/state/__init__.py | 10 +++++++--- synapse/storage/_base.py | 1 + synapse/storage/databases/main/roommember.py | 8 ++++++-- synapse/storage/databases/main/user_directory.py | 4 +--- 12 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 changelog.d/9910.bugfix create mode 100644 changelog.d/9910.feature diff --git a/changelog.d/9910.bugfix b/changelog.d/9910.bugfix new file mode 100644 index 0000000000..06d523fd46 --- /dev/null +++ b/changelog.d/9910.bugfix @@ -0,0 +1 @@ +Fix bug where user directory could get out of sync if room visibility and membership changed in quick succession. diff --git a/changelog.d/9910.feature b/changelog.d/9910.feature new file mode 100644 index 0000000000..54165cce18 --- /dev/null +++ b/changelog.d/9910.feature @@ -0,0 +1 @@ +Improve performance after joining a large room when presence is enabled. diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index de1b14cde3..4064a2b859 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -78,7 +78,7 @@ async def _create_association( # TODO(erikj): Add transactions. # TODO(erikj): Check if there is a current association. if not servers: - users = await self.state.get_current_users_in_room(room_id) + users = await self.store.get_users_in_room(room_id) servers = {get_domain_from_id(u) for u in users} if not servers: @@ -270,7 +270,7 @@ async def get_association(self, room_alias: RoomAlias) -> JsonDict: Codes.NOT_FOUND, ) - users = await self.state.get_current_users_in_room(room_id) + users = await self.store.get_users_in_room(room_id) extra_servers = {get_domain_from_id(u) for u in users} servers = set(extra_servers) | set(servers) diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index d82144d7fa..f134f1e234 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -103,7 +103,7 @@ async def get_stream( # Send down presence. if event.state_key == auth_user_id: # Send down presence for everyone in the room. - users = await self.state.get_current_users_in_room( + users = await self.store.get_users_in_room( event.room_id ) # type: Iterable[str] else: diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 49f8aa25ea..393f17c3a3 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -258,7 +258,7 @@ async def get_joined_members(self, requester: Requester, room_id: str) -> dict: "Getting joined members after leaving is not implemented" ) - users_with_profile = await self.state.get_current_users_in_room(room_id) + users_with_profile = await self.store.get_users_in_room_with_profiles(room_id) # If this is an AS, double check that they are allowed to see the members. # This can either be because the AS user is in the room or because there diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index ebbc234334..8e085dfbec 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -1293,7 +1293,7 @@ async def _on_user_joined_room( remote_host = get_domain_from_id(user_id) - users = await self.state.get_current_users_in_room(room_id) + users = await self.store.get_users_in_room(room_id) user_ids = list(filter(self.is_mine_id, users)) states_d = await self.current_state_for_users(user_ids) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 5a888b7941..fb4823a5cc 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1327,7 +1327,7 @@ async def shutdown_room( new_room_id = None logger.info("Shutting down room %r", room_id) - users = await self.state.get_current_users_in_room(room_id) + users = await self.store.get_users_in_room(room_id) kicked_users = [] failed_to_kick_users = [] for user_id in users: diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index a9a3ee05c3..0fcc1532da 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1190,7 +1190,7 @@ async def _generate_sync_entry_for_device_list( # Step 1b, check for newly joined rooms for room_id in newly_joined_rooms: - joined_users = await self.state.get_current_users_in_room(room_id) + joined_users = await self.store.get_users_in_room(room_id) newly_joined_or_invited_users.update(joined_users) # TODO: Check that these users are actually new, i.e. either they @@ -1206,7 +1206,7 @@ async def _generate_sync_entry_for_device_list( # Now find users that we no longer track for room_id in newly_left_rooms: - left_users = await self.state.get_current_users_in_room(room_id) + left_users = await self.store.get_users_in_room(room_id) newly_left_users.update(left_users) # Remove any users that we still share a room with. @@ -1361,7 +1361,7 @@ async def _generate_sync_entry_for_presence( extra_users_ids = set(newly_joined_or_invited_users) for room_id in newly_joined_rooms: - users = await self.state.get_current_users_in_room(room_id) + users = await self.store.get_users_in_room(room_id) extra_users_ids.update(users) extra_users_ids.discard(user.to_string()) diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index b3bd92d37c..a1770f620e 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -213,19 +213,23 @@ async def get_current_state_ids( return ret.state async def get_current_users_in_room( - self, room_id: str, latest_event_ids: Optional[List[str]] = None + self, room_id: str, latest_event_ids: List[str] ) -> Dict[str, ProfileInfo]: """ Get the users who are currently in a room. + Note: This is much slower than using the equivalent method + `DataStore.get_users_in_room` or `DataStore.get_users_in_room_with_profiles`, + so this should only be used when wanting the users at a particular point + in the room. + Args: room_id: The ID of the room. latest_event_ids: Precomputed list of latest event IDs. Will be computed if None. Returns: Dictionary of user IDs to their profileinfo. """ - if not latest_event_ids: - latest_event_ids = await self.store.get_latest_event_ids_in_room(room_id) + assert latest_event_ids is not None logger.debug("calling resolve_state_groups from get_current_users_in_room") diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 6b68d8720c..3d98d3f5f8 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -69,6 +69,7 @@ def _invalidate_state_caches( self._attempt_to_invalidate_cache("is_host_joined", (room_id, host)) self._attempt_to_invalidate_cache("get_users_in_room", (room_id,)) + self._attempt_to_invalidate_cache("get_users_in_room_with_profiles", (room_id,)) self._attempt_to_invalidate_cache("get_room_summary", (room_id,)) self._attempt_to_invalidate_cache("get_current_state_ids", (room_id,)) diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 2a8532f8c1..5fc3bb5a7d 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -205,8 +205,12 @@ async def get_users_in_room_with_profiles( def _get_users_in_room_with_profiles(txn) -> Dict[str, ProfileInfo]: sql = """ - SELECT user_id, display_name, avatar_url FROM room_memberships - WHERE room_id = ? AND membership = ? + SELECT state_key, display_name, avatar_url FROM room_memberships as m + INNER JOIN current_state_events as c + ON m.event_id = c.event_id + AND m.room_id = c.room_id + AND m.user_id = c.state_key + WHERE c.type = 'm.room.member' AND c.room_id = ? AND m.membership = ? """ txn.execute(sql, (room_id, Membership.JOIN)) diff --git a/synapse/storage/databases/main/user_directory.py b/synapse/storage/databases/main/user_directory.py index 7a082fdd21..a6bfb4902a 100644 --- a/synapse/storage/databases/main/user_directory.py +++ b/synapse/storage/databases/main/user_directory.py @@ -142,8 +142,6 @@ async def _populate_user_directory_process_rooms(self, progress, batch_size): batch_size (int): Maximum number of state events to process per cycle. """ - state = self.hs.get_state_handler() - # If we don't have progress filed, delete everything. if not progress: await self.delete_all_from_user_dir() @@ -197,7 +195,7 @@ def _get_next_batch(txn): room_id ) - users_with_profile = await state.get_current_users_in_room(room_id) + users_with_profile = await self.get_users_in_room_with_profiles(room_id) user_ids = set(users_with_profile) # Update each user in the user directory. From de8f0a03a3cc3a2327dfd3058c99e48067965079 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 5 May 2021 16:53:22 +0100 Subject: [PATCH 120/619] Don't set the external cache if its been done recently (#9905) --- changelog.d/9905.feature | 1 + synapse/handlers/federation.py | 4 +++- synapse/handlers/message.py | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 changelog.d/9905.feature diff --git a/changelog.d/9905.feature b/changelog.d/9905.feature new file mode 100644 index 0000000000..96a0e7f09f --- /dev/null +++ b/changelog.d/9905.feature @@ -0,0 +1 @@ +Improve performance of sending events for worker-based deployments using Redis. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 9d867aaf4d..e8330a2b50 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2446,7 +2446,9 @@ async def _check_event_auth( # If we are going to send this event over federation we precaclculate # the joined hosts. if event.internal_metadata.get_send_on_behalf_of(): - await self.event_creation_handler.cache_joined_hosts_for_event(event) + await self.event_creation_handler.cache_joined_hosts_for_event( + event, context + ) return context diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 393f17c3a3..8729332d4b 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -51,6 +51,7 @@ from synapse.types import Requester, RoomAlias, StreamToken, UserID, create_requester from synapse.util import json_decoder, json_encoder from synapse.util.async_helpers import Linearizer +from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.metrics import measure_func from synapse.visibility import filter_events_for_client @@ -457,6 +458,19 @@ def __init__(self, hs: "HomeServer"): self._external_cache = hs.get_external_cache() + # Stores the state groups we've recently added to the joined hosts + # external cache. Note that the timeout must be significantly less than + # the TTL on the external cache. + self._external_cache_joined_hosts_updates = ( + None + ) # type: Optional[ExpiringCache] + if self._external_cache.is_enabled(): + self._external_cache_joined_hosts_updates = ExpiringCache( + "_external_cache_joined_hosts_updates", + self.clock, + expiry_ms=30 * 60 * 1000, + ) + async def create_event( self, requester: Requester, @@ -967,7 +981,7 @@ async def handle_new_client_event( await self.action_generator.handle_push_actions_for_event(event, context) - await self.cache_joined_hosts_for_event(event) + await self.cache_joined_hosts_for_event(event, context) try: # If we're a worker we need to hit out to the master. @@ -1008,7 +1022,9 @@ async def handle_new_client_event( await self.store.remove_push_actions_from_staging(event.event_id) raise - async def cache_joined_hosts_for_event(self, event: EventBase) -> None: + async def cache_joined_hosts_for_event( + self, event: EventBase, context: EventContext + ) -> None: """Precalculate the joined hosts at the event, when using Redis, so that external federation senders don't have to recalculate it themselves. """ @@ -1016,6 +1032,9 @@ async def cache_joined_hosts_for_event(self, event: EventBase) -> None: if not self._external_cache.is_enabled(): return + # If external cache is enabled we should always have this. + assert self._external_cache_joined_hosts_updates is not None + # We actually store two mappings, event ID -> prev state group, # state group -> joined hosts, which is much more space efficient # than event ID -> joined hosts. @@ -1023,16 +1042,21 @@ async def cache_joined_hosts_for_event(self, event: EventBase) -> None: # Note: We have to cache event ID -> prev state group, as we don't # store that in the DB. # - # Note: We always set the state group -> joined hosts cache, even if - # we already set it, so that the expiry time is reset. + # Note: We set the state group -> joined hosts cache if it hasn't been + # set for a while, so that the expiry time is reset. state_entry = await self.state.resolve_state_groups_for_events( event.room_id, event_ids=event.prev_event_ids() ) if state_entry.state_group: + if state_entry.state_group in self._external_cache_joined_hosts_updates: + return + joined_hosts = await self.store.get_joined_hosts(event.room_id, state_entry) + # Note that the expiry times must be larger than the expiry time in + # _external_cache_joined_hosts_updates. await self._external_cache.set( "event_to_prev_state_group", event.event_id, @@ -1046,6 +1070,8 @@ async def cache_joined_hosts_for_event(self, event: EventBase) -> None: expiry_ms=60 * 60 * 1000, ) + self._external_cache_joined_hosts_updates[state_entry.state_group] = None + async def _validate_canonical_alias( self, directory_handler, room_alias_str: str, expected_room_id: str ) -> None: From 1fb9a2d0bf2506ca6e5343cb340a441585ca1c07 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 5 May 2021 16:53:45 +0100 Subject: [PATCH 121/619] Limit how often GC happens by time. (#9902) Synapse can be quite memory intensive, and unless care is taken to tune the GC thresholds it can end up thrashing, causing noticable performance problems for large servers. We fix this by limiting how often we GC a given generation, regardless of current counts/thresholds. This does not help with the reverse problem where the thresholds are set too high, but that should only happen in situations where they've been manually configured. Adds a `gc_min_seconds_between` config option to override the defaults. Fixes #9890. --- changelog.d/9902.feature | 1 + docs/sample_config.yaml | 10 ++++++++++ synapse/app/generic_worker.py | 3 +++ synapse/app/homeserver.py | 3 +++ synapse/config/server.py | 31 ++++++++++++++++++++++++++++++- synapse/metrics/__init__.py | 18 ++++++++++++++++-- 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9902.feature diff --git a/changelog.d/9902.feature b/changelog.d/9902.feature new file mode 100644 index 0000000000..4d9f324d4e --- /dev/null +++ b/changelog.d/9902.feature @@ -0,0 +1 @@ +Add limits to how often Synapse will GC, ensuring that large servers do not end up GC thrashing if `gc_thresholds` has not been correctly set. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index d013725cdc..f469d6e54f 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -152,6 +152,16 @@ presence: # #gc_thresholds: [700, 10, 10] +# The minimum time in seconds between each GC for a generation, regardless of +# the GC thresholds. This ensures that we don't do GC too frequently. +# +# A value of `[1s, 10s, 30s]` indicates that a second must pass between consecutive +# generation 0 GCs, etc. +# +# Defaults to `[1s, 10s, 30s]`. +# +#gc_min_interval: [0.5s, 30s, 1m] + # Set the limit on the returned events in the timeline in the get # and sync operations. The default value is 100. -1 means no upper limit. # diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 1a15ceee81..a3fe9a3f38 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -455,6 +455,9 @@ def start(config_options): synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts + if config.server.gc_seconds: + synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds + hs = GenericWorkerServer( config.server_name, config=config, diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 8e78134bbe..6a823da10d 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -342,6 +342,9 @@ def setup(config_options): events.USE_FROZEN_DICTS = config.use_frozen_dicts + if config.server.gc_seconds: + synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds + hs = SynapseHomeServer( config.server_name, config=config, diff --git a/synapse/config/server.py b/synapse/config/server.py index 21ca7b33e3..c290a35a92 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -19,7 +19,7 @@ import os.path import re from textwrap import indent -from typing import Any, Dict, Iterable, List, Optional, Set +from typing import Any, Dict, Iterable, List, Optional, Set, Tuple import attr import yaml @@ -572,6 +572,7 @@ def read_config(self, config, **kwargs): _warn_if_webclient_configured(self.listeners) self.gc_thresholds = read_gc_thresholds(config.get("gc_thresholds", None)) + self.gc_seconds = self.read_gc_intervals(config.get("gc_min_interval", None)) @attr.s class LimitRemoteRoomsConfig: @@ -917,6 +918,16 @@ def generate_config_section( # #gc_thresholds: [700, 10, 10] + # The minimum time in seconds between each GC for a generation, regardless of + # the GC thresholds. This ensures that we don't do GC too frequently. + # + # A value of `[1s, 10s, 30s]` indicates that a second must pass between consecutive + # generation 0 GCs, etc. + # + # Defaults to `[1s, 10s, 30s]`. + # + #gc_min_interval: [0.5s, 30s, 1m] + # Set the limit on the returned events in the timeline in the get # and sync operations. The default value is 100. -1 means no upper limit. # @@ -1305,6 +1316,24 @@ def add_arguments(parser): help="Turn on the twisted telnet manhole service on the given port.", ) + def read_gc_intervals(self, durations) -> Optional[Tuple[float, float, float]]: + """Reads the three durations for the GC min interval option, returning seconds.""" + if durations is None: + return None + + try: + if len(durations) != 3: + raise ValueError() + return ( + self.parse_duration(durations[0]) / 1000, + self.parse_duration(durations[1]) / 1000, + self.parse_duration(durations[2]) / 1000, + ) + except Exception: + raise ConfigError( + "Value of `gc_min_interval` must be a list of three durations if set" + ) + def is_threepid_reserved(reserved_threepids, threepid): """Check the threepid against the reserved threepid config diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index 31b7b3c256..e671da26d5 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -535,6 +535,13 @@ def collect(self): REGISTRY.register(ReactorLastSeenMetric()) +# The minimum time in seconds between GCs for each generation, regardless of the current GC +# thresholds and counts. +MIN_TIME_BETWEEN_GCS = (1.0, 10.0, 30.0) + +# The time (in seconds since the epoch) of the last time we did a GC for each generation. +_last_gc = [0.0, 0.0, 0.0] + def runUntilCurrentTimer(reactor, func): @functools.wraps(func) @@ -575,11 +582,16 @@ def f(*args, **kwargs): return ret # Check if we need to do a manual GC (since its been disabled), and do - # one if necessary. + # one if necessary. Note we go in reverse order as e.g. a gen 1 GC may + # promote an object into gen 2, and we don't want to handle the same + # object multiple times. threshold = gc.get_threshold() counts = gc.get_count() for i in (2, 1, 0): - if threshold[i] < counts[i]: + # We check if we need to do one based on a straightforward + # comparison between the threshold and count. We also do an extra + # check to make sure that we don't a GC too often. + if threshold[i] < counts[i] and MIN_TIME_BETWEEN_GCS[i] < end - _last_gc[i]: if i == 0: logger.debug("Collecting gc %d", i) else: @@ -589,6 +601,8 @@ def f(*args, **kwargs): unreachable = gc.collect(i) end = time.time() + _last_gc[i] = end + gc_time.labels(i).observe(end - start) gc_unreachable.labels(i).set(unreachable) From ef889c98a6cde0cfa95f7fdaf7f99ec3c1e9bb7f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 5 May 2021 16:54:36 +0100 Subject: [PATCH 122/619] Optionally track memory usage of each LruCache (#9881) This will double count slightly in the presence of interned strings. It's off by default as it can consume a lot of resources. --- changelog.d/9881.feature | 1 + mypy.ini | 3 +++ synapse/app/generic_worker.py | 1 + synapse/app/homeserver.py | 1 + synapse/config/cache.py | 11 ++++++++ synapse/python_dependencies.py | 2 ++ synapse/util/caches/__init__.py | 31 +++++++++++++++++++++ synapse/util/caches/lrucache.py | 48 ++++++++++++++++++++++++++++++++- 8 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9881.feature diff --git a/changelog.d/9881.feature b/changelog.d/9881.feature new file mode 100644 index 0000000000..088a517e02 --- /dev/null +++ b/changelog.d/9881.feature @@ -0,0 +1 @@ +Add experimental option to track memory usage of the caches. diff --git a/mypy.ini b/mypy.ini index a40f705b76..ea655a0d4d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -171,3 +171,6 @@ ignore_missing_imports = True [mypy-txacme.*] ignore_missing_imports = True + +[mypy-pympler.*] +ignore_missing_imports = True diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index a3fe9a3f38..f730cdbd78 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -454,6 +454,7 @@ def start(config_options): config.server.update_user_directory = False synapse.events.USE_FROZEN_DICTS = config.use_frozen_dicts + synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage if config.server.gc_seconds: synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 6a823da10d..b2501ee4d7 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -341,6 +341,7 @@ def setup(config_options): sys.exit(0) events.USE_FROZEN_DICTS = config.use_frozen_dicts + synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage if config.server.gc_seconds: synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds diff --git a/synapse/config/cache.py b/synapse/config/cache.py index 41b9b3f51f..91165ee1ce 100644 --- a/synapse/config/cache.py +++ b/synapse/config/cache.py @@ -17,6 +17,8 @@ import threading from typing import Callable, Dict +from synapse.python_dependencies import DependencyException, check_requirements + from ._base import Config, ConfigError # The prefix for all cache factor-related environment variables @@ -189,6 +191,15 @@ def read_config(self, config, **kwargs): ) self.cache_factors[cache] = factor + self.track_memory_usage = cache_config.get("track_memory_usage", False) + if self.track_memory_usage: + try: + check_requirements("cache_memory") + except DependencyException as e: + raise ConfigError( + e.message # noqa: B306, DependencyException.message is a property + ) + # Resize all caches (if necessary) with the new factors we've loaded self.resize_all_caches() diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 2de946f464..d58eeeaa74 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -116,6 +116,8 @@ # hiredis is not a *strict* dependency, but it makes things much faster. # (if it is not installed, we fall back to slow code.) "redis": ["txredisapi>=1.4.7", "hiredis"], + # Required to use experimental `caches.track_memory_usage` config option. + "cache_memory": ["pympler"], } ALL_OPTIONAL_REQUIREMENTS = set() # type: Set[str] diff --git a/synapse/util/caches/__init__.py b/synapse/util/caches/__init__.py index 46af7fa473..ca36f07c20 100644 --- a/synapse/util/caches/__init__.py +++ b/synapse/util/caches/__init__.py @@ -24,6 +24,11 @@ logger = logging.getLogger(__name__) + +# Whether to track estimated memory usage of the LruCaches. +TRACK_MEMORY_USAGE = False + + caches_by_name = {} # type: Dict[str, Sized] collectors_by_name = {} # type: Dict[str, CacheMetric] @@ -32,6 +37,11 @@ cache_evicted = Gauge("synapse_util_caches_cache:evicted_size", "", ["name"]) cache_total = Gauge("synapse_util_caches_cache:total", "", ["name"]) cache_max_size = Gauge("synapse_util_caches_cache_max_size", "", ["name"]) +cache_memory_usage = Gauge( + "synapse_util_caches_cache_size_bytes", + "Estimated memory usage of the caches", + ["name"], +) response_cache_size = Gauge("synapse_util_caches_response_cache:size", "", ["name"]) response_cache_hits = Gauge("synapse_util_caches_response_cache:hits", "", ["name"]) @@ -52,6 +62,7 @@ class CacheMetric: hits = attr.ib(default=0) misses = attr.ib(default=0) evicted_size = attr.ib(default=0) + memory_usage = attr.ib(default=None) def inc_hits(self): self.hits += 1 @@ -62,6 +73,19 @@ def inc_misses(self): def inc_evictions(self, size=1): self.evicted_size += size + def inc_memory_usage(self, memory: int): + if self.memory_usage is None: + self.memory_usage = 0 + + self.memory_usage += memory + + def dec_memory_usage(self, memory: int): + self.memory_usage -= memory + + def clear_memory_usage(self): + if self.memory_usage is not None: + self.memory_usage = 0 + def describe(self): return [] @@ -81,6 +105,13 @@ def collect(self): cache_total.labels(self._cache_name).set(self.hits + self.misses) if getattr(self._cache, "max_size", None): cache_max_size.labels(self._cache_name).set(self._cache.max_size) + + if TRACK_MEMORY_USAGE: + # self.memory_usage can be None if nothing has been inserted + # into the cache yet. + cache_memory_usage.labels(self._cache_name).set( + self.memory_usage or 0 + ) if self._collect_callback: self._collect_callback() except Exception as e: diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index 10b0ec6b75..1be675e014 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -32,9 +32,36 @@ from typing_extensions import Literal from synapse.config import cache as cache_config +from synapse.util import caches from synapse.util.caches import CacheMetric, register_cache from synapse.util.caches.treecache import TreeCache +try: + from pympler.asizeof import Asizer + + def _get_size_of(val: Any, *, recurse=True) -> int: + """Get an estimate of the size in bytes of the object. + + Args: + val: The object to size. + recurse: If true will include referenced values in the size, + otherwise only sizes the given object. + """ + # Ignore singleton values when calculating memory usage. + if val in ((), None, ""): + return 0 + + sizer = Asizer() + sizer.exclude_refs((), None, "") + return sizer.asizeof(val, limit=100 if recurse else 0) + + +except ImportError: + + def _get_size_of(val: Any, *, recurse=True) -> int: + return 0 + + # Function type: the type used for invalidation callbacks FT = TypeVar("FT", bound=Callable[..., Any]) @@ -56,7 +83,7 @@ def enumerate_leaves(node, depth): class _Node: - __slots__ = ["prev_node", "next_node", "key", "value", "callbacks"] + __slots__ = ["prev_node", "next_node", "key", "value", "callbacks", "memory"] def __init__( self, @@ -84,6 +111,16 @@ def __init__( self.add_callbacks(callbacks) + self.memory = 0 + if caches.TRACK_MEMORY_USAGE: + self.memory = ( + _get_size_of(key) + + _get_size_of(value) + + _get_size_of(self.callbacks, recurse=False) + + _get_size_of(self, recurse=False) + ) + self.memory += _get_size_of(self.memory, recurse=False) + def add_callbacks(self, callbacks: Collection[Callable[[], None]]) -> None: """Add to stored list of callbacks, removing duplicates.""" @@ -233,6 +270,9 @@ def add_node(key, value, callbacks: Collection[Callable[[], None]] = ()): if size_callback: cached_cache_len[0] += size_callback(node.value) + if caches.TRACK_MEMORY_USAGE and metrics: + metrics.inc_memory_usage(node.memory) + def move_node_to_front(node): prev_node = node.prev_node next_node = node.next_node @@ -258,6 +298,9 @@ def delete_node(node): node.run_and_clear_callbacks() + if caches.TRACK_MEMORY_USAGE and metrics: + metrics.dec_memory_usage(node.memory) + return deleted_len @overload @@ -373,6 +416,9 @@ def cache_clear() -> None: if size_callback: cached_cache_len[0] = 0 + if caches.TRACK_MEMORY_USAGE and metrics: + metrics.clear_memory_usage() + @synchronized def cache_contains(key: KT) -> bool: return key in cache From e2a443550e7b47bf8fe1b5fbd76f9ca95e81cbad Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 5 May 2021 11:56:51 -0400 Subject: [PATCH 123/619] Support stable MSC1772 spaces identifiers. (#9915) Support both the unstable and stable identifiers. A future release will disable the unstable identifiers. --- changelog.d/9915.feature | 1 + synapse/api/constants.py | 3 +++ synapse/handlers/space_summary.py | 8 ++++++-- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9915.feature diff --git a/changelog.d/9915.feature b/changelog.d/9915.feature new file mode 100644 index 0000000000..832916cb01 --- /dev/null +++ b/changelog.d/9915.feature @@ -0,0 +1 @@ +Support stable identifiers from [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 936b6534b4..bff750e5fb 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -110,6 +110,8 @@ class EventTypes: Dummy = "org.matrix.dummy_event" + SpaceChild = "m.space.child" + SpaceParent = "m.space.parent" MSC1772_SPACE_CHILD = "org.matrix.msc1772.space.child" MSC1772_SPACE_PARENT = "org.matrix.msc1772.space.parent" @@ -174,6 +176,7 @@ class EventContentFields: SELF_DESTRUCT_AFTER = "org.matrix.self_destruct_after" # cf https://github.com/matrix-org/matrix-doc/pull/1772 + ROOM_TYPE = "m.type" MSC1772_ROOM_TYPE = "org.matrix.msc1772.type" diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 01e3e050f9..d32452747c 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -288,6 +288,7 @@ async def _summarize_remote_room( ev.data for ev in res.events if ev.event_type == EventTypes.MSC1772_SPACE_CHILD + or ev.event_type == EventTypes.SpaceChild ) async def _is_room_accessible(self, room_id: str, requester: Optional[str]) -> bool: @@ -331,7 +332,9 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: ) # TODO: update once MSC1772 lands - room_type = create_event.content.get(EventContentFields.MSC1772_ROOM_TYPE) + room_type = create_event.content.get(EventContentFields.ROOM_TYPE) + if not room_type: + room_type = create_event.content.get(EventContentFields.MSC1772_ROOM_TYPE) entry = { "room_id": stats["room_id"], @@ -360,8 +363,9 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: [ event_id for key, event_id in current_state_ids.items() - # TODO: update once MSC1772 lands + # TODO: update once MSC1772 has been FCP for a period of time. if key[0] == EventTypes.MSC1772_SPACE_CHILD + or key[0] == EventTypes.SpaceChild ] ) From 37623e33822d4e032ba6d1f523fb09b12fe27aab Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 5 May 2021 17:27:05 +0100 Subject: [PATCH 124/619] Increase perf of handling presence when joining large rooms. (#9916) --- changelog.d/9916.feature | 1 + synapse/handlers/presence.py | 154 +++++++++++++++++--------------- tests/handlers/test_presence.py | 14 +-- 3 files changed, 87 insertions(+), 82 deletions(-) create mode 100644 changelog.d/9916.feature diff --git a/changelog.d/9916.feature b/changelog.d/9916.feature new file mode 100644 index 0000000000..54165cce18 --- /dev/null +++ b/changelog.d/9916.feature @@ -0,0 +1 @@ +Improve performance after joining a large room when presence is enabled. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 8e085dfbec..6fd1f34289 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -1183,7 +1183,16 @@ async def _unsafe_process(self) -> None: max_pos, deltas = await self.store.get_current_state_deltas( self._event_pos, room_max_stream_ordering ) - await self._handle_state_delta(deltas) + + # We may get multiple deltas for different rooms, but we want to + # handle them on a room by room basis, so we batch them up by + # room. + deltas_by_room: Dict[str, List[JsonDict]] = {} + for delta in deltas: + deltas_by_room.setdefault(delta["room_id"], []).append(delta) + + for room_id, deltas_for_room in deltas_by_room.items(): + await self._handle_state_delta(room_id, deltas_for_room) self._event_pos = max_pos @@ -1192,17 +1201,21 @@ async def _unsafe_process(self) -> None: max_pos ) - async def _handle_state_delta(self, deltas: List[JsonDict]) -> None: - """Process current state deltas to find new joins that need to be - handled. + async def _handle_state_delta(self, room_id: str, deltas: List[JsonDict]) -> None: + """Process current state deltas for the room to find new joins that need + to be handled. """ - # A map of destination to a set of user state that they should receive - presence_destinations = {} # type: Dict[str, Set[UserPresenceState]] + + # Sets of newly joined users. Note that if the local server is + # joining a remote room for the first time we'll see both the joining + # user and all remote users as newly joined. + newly_joined_users = set() for delta in deltas: + assert room_id == delta["room_id"] + typ = delta["type"] state_key = delta["state_key"] - room_id = delta["room_id"] event_id = delta["event_id"] prev_event_id = delta["prev_event_id"] @@ -1231,72 +1244,55 @@ async def _handle_state_delta(self, deltas: List[JsonDict]) -> None: # Ignore changes to join events. continue - # Retrieve any user presence state updates that need to be sent as a result, - # and the destinations that need to receive it - destinations, user_presence_states = await self._on_user_joined_room( - room_id, state_key - ) - - # Insert the destinations and respective updates into our destinations dict - for destination in destinations: - presence_destinations.setdefault(destination, set()).update( - user_presence_states - ) - - # Send out user presence updates for each destination - for destination, user_state_set in presence_destinations.items(): - self._federation_queue.send_presence_to_destinations( - destinations=[destination], states=user_state_set - ) - - async def _on_user_joined_room( - self, room_id: str, user_id: str - ) -> Tuple[List[str], List[UserPresenceState]]: - """Called when we detect a user joining the room via the current state - delta stream. Returns the destinations that need to be updated and the - presence updates to send to them. - - Args: - room_id: The ID of the room that the user has joined. - user_id: The ID of the user that has joined the room. - - Returns: - A tuple of destinations and presence updates to send to them. - """ - if self.is_mine_id(user_id): - # If this is a local user then we need to send their presence - # out to hosts in the room (who don't already have it) - - # TODO: We should be able to filter the hosts down to those that - # haven't previously seen the user - - remote_hosts = await self.state.get_current_hosts_in_room(room_id) + newly_joined_users.add(state_key) - # Filter out ourselves. - filtered_remote_hosts = [ - host for host in remote_hosts if host != self.server_name - ] - - state = await self.current_state_for_user(user_id) - return filtered_remote_hosts, [state] - else: - # A remote user has joined the room, so we need to: - # 1. Check if this is a new server in the room - # 2. If so send any presence they don't already have for - # local users in the room. - - # TODO: We should be able to filter the users down to those that - # the server hasn't previously seen - - # TODO: Check that this is actually a new server joining the - # room. - - remote_host = get_domain_from_id(user_id) + if not newly_joined_users: + # If nobody has joined then there's nothing to do. + return - users = await self.store.get_users_in_room(room_id) - user_ids = list(filter(self.is_mine_id, users)) + # We want to send: + # 1. presence states of all local users in the room to newly joined + # remote servers + # 2. presence states of newly joined users to all remote servers in + # the room. + # + # TODO: Only send presence states to remote hosts that don't already + # have them (because they already share rooms). + + # Get all the users who were already in the room, by fetching the + # current users in the room and removing the newly joined users. + users = await self.store.get_users_in_room(room_id) + prev_users = set(users) - newly_joined_users + + # Construct sets for all the local users and remote hosts that were + # already in the room + prev_local_users = [] + prev_remote_hosts = set() + for user_id in prev_users: + if self.is_mine_id(user_id): + prev_local_users.append(user_id) + else: + prev_remote_hosts.add(get_domain_from_id(user_id)) + + # Similarly, construct sets for all the local users and remote hosts + # that were *not* already in the room. Care needs to be taken with the + # calculating the remote hosts, as a host may have already been in the + # room even if there is a newly joined user from that host. + newly_joined_local_users = [] + newly_joined_remote_hosts = set() + for user_id in newly_joined_users: + if self.is_mine_id(user_id): + newly_joined_local_users.append(user_id) + else: + host = get_domain_from_id(user_id) + if host not in prev_remote_hosts: + newly_joined_remote_hosts.add(host) - states_d = await self.current_state_for_users(user_ids) + # Send presence states of all local users in the room to newly joined + # remote servers. (We actually only send states for local users already + # in the room, as we'll send states for newly joined local users below.) + if prev_local_users and newly_joined_remote_hosts: + local_states = await self.current_state_for_users(prev_local_users) # Filter out old presence, i.e. offline presence states where # the user hasn't been active for a week. We can change this @@ -1306,13 +1302,27 @@ async def _on_user_joined_room( now = self.clock.time_msec() states = [ state - for state in states_d.values() + for state in local_states.values() if state.state != PresenceState.OFFLINE or now - state.last_active_ts < 7 * 24 * 60 * 60 * 1000 or state.status_msg is not None ] - return [remote_host], states + self._federation_queue.send_presence_to_destinations( + destinations=newly_joined_remote_hosts, + states=states, + ) + + # Send presence states of newly joined users to all remote servers in + # the room + if newly_joined_local_users and ( + prev_remote_hosts or newly_joined_remote_hosts + ): + local_states = await self.current_state_for_users(newly_joined_local_users) + self._federation_queue.send_presence_to_destinations( + destinations=prev_remote_hosts | newly_joined_remote_hosts, + states=list(local_states.values()), + ) def should_notify(old_state: UserPresenceState, new_state: UserPresenceState) -> bool: diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index ce330e79cc..1ffab709fc 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -729,7 +729,7 @@ def test_remote_joins(self): ) self.assertEqual(expected_state.state, PresenceState.ONLINE) self.federation_sender.send_presence_to_destinations.assert_called_once_with( - destinations=["server2"], states={expected_state} + destinations={"server2"}, states=[expected_state] ) # @@ -740,7 +740,7 @@ def test_remote_joins(self): self._add_new_user(room_id, "@bob:server3") self.federation_sender.send_presence_to_destinations.assert_called_once_with( - destinations=["server3"], states={expected_state} + destinations={"server3"}, states=[expected_state] ) def test_remote_gets_presence_when_local_user_joins(self): @@ -788,14 +788,8 @@ def test_remote_gets_presence_when_local_user_joins(self): self.presence_handler.current_state_for_user("@test2:server") ) self.assertEqual(expected_state.state, PresenceState.ONLINE) - self.assertEqual( - self.federation_sender.send_presence_to_destinations.call_count, 2 - ) - self.federation_sender.send_presence_to_destinations.assert_any_call( - destinations=["server3"], states={expected_state} - ) - self.federation_sender.send_presence_to_destinations.assert_any_call( - destinations=["server2"], states={expected_state} + self.federation_sender.send_presence_to_destinations.assert_called_once_with( + destinations={"server2", "server3"}, states=[expected_state] ) def _add_new_user(self, room_id, user_id): From d783880083733a694ed4c7b15ca53be00e06f8a7 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 5 May 2021 13:33:05 -0400 Subject: [PATCH 125/619] Include the time of the create event in Spaces Summary. (#9928) This is an update based on changes to MSC2946. The origin_server_ts of the m.room.create event is copied into the creation_ts field for each room returned from the spaces summary. --- changelog.d/9928.bugfix | 1 + synapse/handlers/space_summary.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/9928.bugfix diff --git a/changelog.d/9928.bugfix b/changelog.d/9928.bugfix new file mode 100644 index 0000000000..7b74cd9fb6 --- /dev/null +++ b/changelog.d/9928.bugfix @@ -0,0 +1 @@ +Include the `origin_server_ts` property in the experimental [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946) support to allow clients to properly sort rooms. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index d32452747c..2e997841f1 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -347,6 +347,7 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: stats["history_visibility"] == HistoryVisibility.WORLD_READABLE ), "guest_can_join": stats["guest_access"] == "can_join", + "creation_ts": create_event.origin_server_ts, "room_type": room_type, } From 70f0ffd2fcd815a065b4734ac606654a2e11dd28 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 5 May 2021 16:31:16 -0400 Subject: [PATCH 126/619] Follow-up to #9915 to correct the identifier for room types. --- synapse/api/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index bff750e5fb..ab628b2be7 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -176,7 +176,7 @@ class EventContentFields: SELF_DESTRUCT_AFTER = "org.matrix.self_destruct_after" # cf https://github.com/matrix-org/matrix-doc/pull/1772 - ROOM_TYPE = "m.type" + ROOM_TYPE = "type" MSC1772_ROOM_TYPE = "org.matrix.msc1772.type" From 24f07a83e604a7ee93214ef0bf7d7c56a14a534d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 May 2021 14:06:06 +0100 Subject: [PATCH 127/619] Pin attrs to <21.1.0 (#9937) Fixes #9936 --- changelog.d/9937.bugfix | 1 + synapse/python_dependencies.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9937.bugfix diff --git a/changelog.d/9937.bugfix b/changelog.d/9937.bugfix new file mode 100644 index 0000000000..c1cc5ccb43 --- /dev/null +++ b/changelog.d/9937.bugfix @@ -0,0 +1 @@ +Fix bug where `/sync` would break if using the latest version of `attrs` dependency, by pinning to a previous version. diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 2de946f464..7709361f16 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -78,7 +78,8 @@ # we use attr.validators.deep_iterable, which arrived in 19.1.0 (Note: # Fedora 31 only has 19.1, so if we want to upgrade we should wait until 33 # is out in November.) - "attrs>=19.1.0", + # Note: 21.1.0 broke `/sync`, see #9936 + "attrs>=19.1.0,<21.1.0", "netaddr>=0.7.18", "Jinja2>=2.9", "bleach>=1.4.3", From ac88aca7f7acc2ce909db230682f93bb4e2ff73b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 May 2021 14:06:38 +0100 Subject: [PATCH 128/619] 1.33.1 --- CHANGES.md | 9 +++++++++ changelog.d/9937.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/9937.bugfix diff --git a/CHANGES.md b/CHANGES.md index 206859cff7..a41abbefba 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.33.1 (2021-05-06) +=========================== + +Bugfixes +-------- + +- Fix bug where `/sync` would break if using the latest version of `attrs` dependency, by pinning to a previous version. ([\#9937](https://github.com/matrix-org/synapse/issues/9937)) + + Synapse 1.33.0 (2021-05-05) =========================== diff --git a/changelog.d/9937.bugfix b/changelog.d/9937.bugfix deleted file mode 100644 index c1cc5ccb43..0000000000 --- a/changelog.d/9937.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where `/sync` would break if using the latest version of `attrs` dependency, by pinning to a previous version. diff --git a/debian/changelog b/debian/changelog index b54d3f2bc6..de50dd14ea 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.33.1) stable; urgency=medium + + * New synapse release 1.33.1. + + -- Synapse Packaging team Thu, 06 May 2021 14:06:33 +0100 + matrix-synapse-py3 (1.33.0) stable; urgency=medium * New synapse release 1.33.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 5eac40730a..441cd8b339 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.33.0" +__version__ = "1.33.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From eba431c539dbe0ca28794d89962d447d1f75938f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 May 2021 15:06:35 +0100 Subject: [PATCH 129/619] Revert "Leave out optional keys from /sync (#9919)" (#9940) This reverts commit e9eb3549d32a6f93d07de8dbd5e1ebe54c8d8278. --- changelog.d/9919.feature | 1 - synapse/rest/client/v2_alpha/sync.py | 62 ++++++------------- tests/rest/client/v2_alpha/test_sync.py | 30 ++++++++- .../test_resource_limits_server_notices.py | 8 +-- 4 files changed, 50 insertions(+), 51 deletions(-) delete mode 100644 changelog.d/9919.feature diff --git a/changelog.d/9919.feature b/changelog.d/9919.feature deleted file mode 100644 index 07747505d2..0000000000 --- a/changelog.d/9919.feature +++ /dev/null @@ -1 +0,0 @@ -Omit empty fields from the `/sync` response. Contributed by @deepbluev7. diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index 5f85653330..95ee3f1b84 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -14,7 +14,6 @@ import itertools import logging -from collections import defaultdict from typing import TYPE_CHECKING, Tuple from synapse.api.constants import PresenceState @@ -230,49 +229,24 @@ async def encode_response(self, time_now, sync_result, access_token_id, filter): ) logger.debug("building sync response dict") - - response: dict = defaultdict(dict) - response["next_batch"] = await sync_result.next_batch.to_string(self.store) - - if sync_result.account_data: - response["account_data"] = {"events": sync_result.account_data} - if sync_result.presence: - response["presence"] = SyncRestServlet.encode_presence( - sync_result.presence, time_now - ) - - if sync_result.to_device: - response["to_device"] = {"events": sync_result.to_device} - - if sync_result.device_lists.changed: - response["device_lists"]["changed"] = list(sync_result.device_lists.changed) - if sync_result.device_lists.left: - response["device_lists"]["left"] = list(sync_result.device_lists.left) - - if sync_result.device_one_time_keys_count: - response[ - "device_one_time_keys_count" - ] = sync_result.device_one_time_keys_count - if sync_result.device_unused_fallback_key_types: - response[ - "org.matrix.msc2732.device_unused_fallback_key_types" - ] = sync_result.device_unused_fallback_key_types - - if joined: - response["rooms"]["join"] = joined - if invited: - response["rooms"]["invite"] = invited - if archived: - response["rooms"]["leave"] = archived - - if sync_result.groups.join: - response["groups"]["join"] = sync_result.groups.join - if sync_result.groups.invite: - response["groups"]["invite"] = sync_result.groups.invite - if sync_result.groups.leave: - response["groups"]["leave"] = sync_result.groups.leave - - return response + return { + "account_data": {"events": sync_result.account_data}, + "to_device": {"events": sync_result.to_device}, + "device_lists": { + "changed": list(sync_result.device_lists.changed), + "left": list(sync_result.device_lists.left), + }, + "presence": SyncRestServlet.encode_presence(sync_result.presence, time_now), + "rooms": {"join": joined, "invite": invited, "leave": archived}, + "groups": { + "join": sync_result.groups.join, + "invite": sync_result.groups.invite, + "leave": sync_result.groups.leave, + }, + "device_one_time_keys_count": sync_result.device_one_time_keys_count, + "org.matrix.msc2732.device_unused_fallback_key_types": sync_result.device_unused_fallback_key_types, + "next_batch": await sync_result.next_batch.to_string(self.store), + } @staticmethod def encode_presence(events, time_now): diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 74be5176d0..dbcbdf159a 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -37,7 +37,35 @@ def test_sync_argless(self): channel = self.make_request("GET", "/sync") self.assertEqual(channel.code, 200) - self.assertIn("next_batch", channel.json_body) + self.assertTrue( + { + "next_batch", + "rooms", + "presence", + "account_data", + "to_device", + "device_lists", + }.issubset(set(channel.json_body.keys())) + ) + + def test_sync_presence_disabled(self): + """ + When presence is disabled, the key does not appear in /sync. + """ + self.hs.config.use_presence = False + + channel = self.make_request("GET", "/sync") + + self.assertEqual(channel.code, 200) + self.assertTrue( + { + "next_batch", + "rooms", + "account_data", + "to_device", + "device_lists", + }.issubset(set(channel.json_body.keys())) + ) class SyncFilterTestCase(unittest.HomeserverTestCase): diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index 3245aa91ca..d46521ccdc 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -306,9 +306,8 @@ def test_no_invite_without_notice(self): channel = self.make_request("GET", "/sync?timeout=0", access_token=tok) - self.assertNotIn( - "rooms", channel.json_body, "Got invites without server notice" - ) + invites = channel.json_body["rooms"]["invite"] + self.assertEqual(len(invites), 0, invites) def test_invite_with_notice(self): """Tests that, if the MAU limit is hit, the server notices user invites each user @@ -365,8 +364,7 @@ def _trigger_notice_and_join(self): # We could also pick another user and sync with it, which would return an # invite to a system notices room, but it doesn't matter which user we're # using so we use the last one because it saves us an extra sync. - if "rooms" in channel.json_body: - invites = channel.json_body["rooms"]["invite"] + invites = channel.json_body["rooms"]["invite"] # Make sure we have an invite to process. self.assertEqual(len(invites), 1, invites) From 8771b1337da9faa3b60cf0ec0a128a7de856f19e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 6 May 2021 15:54:07 +0100 Subject: [PATCH 130/619] Export jemalloc stats to prometheus when used (#9882) --- changelog.d/9882.misc | 1 + synapse/app/_base.py | 2 + synapse/metrics/__init__.py | 1 + synapse/metrics/jemalloc.py | 196 ++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 changelog.d/9882.misc create mode 100644 synapse/metrics/jemalloc.py diff --git a/changelog.d/9882.misc b/changelog.d/9882.misc new file mode 100644 index 0000000000..facfa31f38 --- /dev/null +++ b/changelog.d/9882.misc @@ -0,0 +1 @@ +Export jemalloc stats to Prometheus if it is being used. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 638e01c1b2..59918d789e 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -37,6 +37,7 @@ from synapse.crypto import context_factory from synapse.logging.context import PreserveLoggingContext from synapse.metrics.background_process_metrics import wrap_as_background_process +from synapse.metrics.jemalloc import setup_jemalloc_stats from synapse.util.async_helpers import Linearizer from synapse.util.daemonize import daemonize_process from synapse.util.rlimit import change_resource_limit @@ -115,6 +116,7 @@ def start_reactor( def run(): logger.info("Running") + setup_jemalloc_stats() change_resource_limit(soft_file_limit) if gc_thresholds: gc.set_threshold(*gc_thresholds) diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index e671da26d5..fef2846669 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -629,6 +629,7 @@ def f(*args, **kwargs): except AttributeError: pass + __all__ = [ "MetricsResource", "generate_latest", diff --git a/synapse/metrics/jemalloc.py b/synapse/metrics/jemalloc.py new file mode 100644 index 0000000000..29ab6c0229 --- /dev/null +++ b/synapse/metrics/jemalloc.py @@ -0,0 +1,196 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ctypes +import logging +import os +import re +from typing import Optional + +from synapse.metrics import REGISTRY, GaugeMetricFamily + +logger = logging.getLogger(__name__) + + +def _setup_jemalloc_stats(): + """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) + + def _mallctl( + name: str, read: bool = True, write: Optional[int] = None + ) -> Optional[int]: + """Wrapper around `mallctl` for reading and writing integers to + jemalloc. + + Args: + name: The name of the option to read from/write to. + read: Whether to try and read the value. + write: The value to write, if given. + + Returns: + The value read if `read` is True, otherwise None. + + Raises: + An exception if `mallctl` returns a non-zero error code. + """ + + input_var = None + input_var_ref = None + input_len_ref = None + if read: + input_var = ctypes.c_size_t(0) + input_len = ctypes.c_size_t(ctypes.sizeof(input_var)) + + input_var_ref = ctypes.byref(input_var) + input_len_ref = ctypes.byref(input_len) + + write_var_ref = None + write_len = ctypes.c_size_t(0) + if write is not None: + write_var = ctypes.c_size_t(write) + write_len = ctypes.c_size_t(ctypes.sizeof(write_var)) + + write_var_ref = ctypes.byref(write_var) + + # The interface is: + # + # int mallctl( + # const char *name, + # void *oldp, + # size_t *oldlenp, + # void *newp, + # size_t newlen + # ) + # + # 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( + name.encode("ascii"), + input_var_ref, + input_len_ref, + write_var_ref, + write_len, + ) + + if result != 0: + raise Exception("Failed to call mallctl") + + if input_var is None: + return None + + return input_var.value + + def _jemalloc_refresh_stats() -> 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) + except Exception as e: + logger.warning("Failed to reload jemalloc stats: %s", e) + + class JemallocCollector: + """Metrics for internal jemalloc stats.""" + + def collect(self): + _jemalloc_refresh_stats() + + g = GaugeMetricFamily( + "jemalloc_stats_app_memory_bytes", + "The stats reported by jemalloc", + labels=["type"], + ) + + # Read the relevant global stats from jemalloc. Note that these may + # not be accurate if python is configured to use its internal small + # object allocator (which is on by default, disable by setting the + # env `PYTHONMALLOC=malloc`). + # + # See the jemalloc manpage for details about what each value means, + # roughly: + # - allocated ─ Total number of bytes allocated by the app + # - active ─ Total number of bytes in active pages allocated by + # the application, this is bigger than `allocated`. + # - resident ─ Maximum number of bytes in physically resident data + # pages mapped by the allocator, comprising all pages dedicated + # to allocator metadata, pages backing active allocations, and + # unused dirty pages. This is bigger than `active`. + # - mapped ─ Total number of bytes in active extents mapped by the + # allocator. + # - metadata ─ Total number of bytes dedicated to jemalloc + # metadata. + for t in ( + "allocated", + "active", + "resident", + "mapped", + "metadata", + ): + try: + value = _mallctl(f"stats.{t}") + except Exception as e: + # There was an error fetching the value, skip. + logger.warning("Failed to read jemalloc stats.%s: %s", t, e) + continue + + g.add_metric([t], value=value) + + yield g + + REGISTRY.register(JemallocCollector()) + + logger.debug("Added jemalloc stats") + + +def setup_jemalloc_stats(): + """Try to setup jemalloc stats, if jemalloc is loaded.""" + + try: + _setup_jemalloc_stats() + except Exception as e: + # This should only happen if we find the loaded jemalloc library, but + # fail to load it somehow (e.g. we somehow picked the wrong version). + logger.info("Failed to setup collector to record jemalloc stats: %s", e) From 25f43faa70f7cc58493b636c2702ae63395779dc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 7 May 2021 10:22:05 +0100 Subject: [PATCH 131/619] Reorganise the database schema directories (#9932) The hope here is that by moving all the schema files into synapse/storage/schema, it gets a bit easier for newcomers to navigate. It certainly got easier for me to write a helpful README. There's more to do on that front, but I'll follow up with other PRs for that. --- changelog.d/9932.misc | 1 + .../main/schema/full_schemas/README.md | 21 -------- synapse/storage/prepare_database.py | 48 ++++++++++--------- synapse/storage/schema/README.md | 37 ++++++++++++++ synapse/storage/schema/__init__.py | 17 +++++++ .../delta/25/00background_updates.sql | 0 .../delta/35/00background_updates_add_col.sql | 0 .../delta/58/00background_update_ordering.sql | 0 .../{ => common}/full_schemas/54/full.sql | 0 .../schema/{ => common}/schema_version.sql | 0 .../schema => schema/main}/delta/12/v12.sql | 0 .../schema => schema/main}/delta/13/v13.sql | 0 .../schema => schema/main}/delta/14/v14.sql | 0 .../main}/delta/15/appservice_txns.sql | 0 .../main}/delta/15/presence_indices.sql | 0 .../schema => schema/main}/delta/15/v15.sql | 0 .../main}/delta/16/events_order_index.sql | 0 .../delta/16/remote_media_cache_index.sql | 0 .../main}/delta/16/remove_duplicates.sql | 0 .../main}/delta/16/room_alias_index.sql | 0 .../main}/delta/16/unique_constraints.sql | 0 .../schema => schema/main}/delta/16/users.sql | 0 .../main}/delta/17/drop_indexes.sql | 0 .../main}/delta/17/server_keys.sql | 0 .../main}/delta/17/user_threepids.sql | 0 .../delta/18/server_keys_bigger_ints.sql | 0 .../main}/delta/19/event_index.sql | 0 .../schema => schema/main}/delta/20/dummy.sql | 0 .../main}/delta/20/pushers.py | 0 .../main}/delta/21/end_to_end_keys.sql | 0 .../main}/delta/21/receipts.sql | 0 .../main}/delta/22/receipts_index.sql | 0 .../main}/delta/22/user_threepids_unique.sql | 0 .../main}/delta/24/stats_reporting.sql | 0 .../schema => schema/main}/delta/25/fts.py | 0 .../main}/delta/25/guest_access.sql | 0 .../main}/delta/25/history_visibility.sql | 0 .../schema => schema/main}/delta/25/tags.sql | 0 .../main}/delta/26/account_data.sql | 0 .../main}/delta/27/account_data.sql | 0 .../main}/delta/27/forgotten_memberships.sql | 0 .../schema => schema/main}/delta/27/ts.py | 0 .../main}/delta/28/event_push_actions.sql | 0 .../main}/delta/28/events_room_stream.sql | 0 .../main}/delta/28/public_roms_index.sql | 0 .../main}/delta/28/receipts_user_id_index.sql | 0 .../main}/delta/28/upgrade_times.sql | 0 .../main}/delta/28/users_is_guest.sql | 0 .../main}/delta/29/push_actions.sql | 0 .../main}/delta/30/alias_creator.sql | 0 .../main}/delta/30/as_users.py | 0 .../main}/delta/30/deleted_pushers.sql | 0 .../main}/delta/30/presence_stream.sql | 0 .../main}/delta/30/public_rooms.sql | 0 .../main}/delta/30/push_rule_stream.sql | 0 .../delta/30/threepid_guest_access_tokens.sql | 0 .../main}/delta/31/invites.sql | 0 .../31/local_media_repository_url_cache.sql | 0 .../main}/delta/31/pushers.py | 0 .../main}/delta/31/pushers_index.sql | 0 .../main}/delta/31/search_update.py | 0 .../main}/delta/32/events.sql | 0 .../main}/delta/32/openid.sql | 0 .../main}/delta/32/pusher_throttle.sql | 0 .../main}/delta/32/remove_indices.sql | 0 .../main}/delta/32/reports.sql | 0 .../delta/33/access_tokens_device_index.sql | 0 .../main}/delta/33/devices.sql | 0 .../main}/delta/33/devices_for_e2e_keys.sql | 0 ...ices_for_e2e_keys_clear_unknown_device.sql | 0 .../main}/delta/33/event_fields.py | 0 .../main}/delta/33/remote_media_ts.py | 0 .../main}/delta/33/user_ips_index.sql | 0 .../main}/delta/34/appservice_stream.sql | 0 .../main}/delta/34/cache_stream.py | 0 .../main}/delta/34/device_inbox.sql | 0 .../delta/34/push_display_name_rename.sql | 0 .../main}/delta/34/received_txn_purge.py | 0 .../main}/delta/35/contains_url.sql | 0 .../main}/delta/35/device_outbox.sql | 0 .../main}/delta/35/device_stream_id.sql | 0 .../delta/35/event_push_actions_index.sql | 0 .../35/public_room_list_change_stream.sql | 0 .../main}/delta/35/stream_order_to_extrem.sql | 0 .../main}/delta/36/readd_public_rooms.sql | 0 .../main}/delta/37/remove_auth_idx.py | 0 .../main}/delta/37/user_threepids.sql | 0 .../main}/delta/38/postgres_fts_gist.sql | 0 .../main}/delta/39/appservice_room_list.sql | 0 .../delta/39/device_federation_stream_idx.sql | 0 .../main}/delta/39/event_push_index.sql | 0 .../delta/39/federation_out_position.sql | 0 .../main}/delta/39/membership_profile.sql | 0 .../main}/delta/40/current_state_idx.sql | 0 .../main}/delta/40/device_inbox.sql | 0 .../main}/delta/40/device_list_streams.sql | 0 .../main}/delta/40/event_push_summary.sql | 0 .../main}/delta/40/pushers.sql | 0 .../main}/delta/41/device_list_stream_idx.sql | 0 .../main}/delta/41/device_outbound_index.sql | 0 .../delta/41/event_search_event_id_idx.sql | 0 .../main}/delta/41/ratelimit.sql | 0 .../main}/delta/42/current_state_delta.sql | 0 .../main}/delta/42/device_list_last_id.sql | 0 .../main}/delta/42/event_auth_state_only.sql | 0 .../main}/delta/42/user_dir.py | 0 .../main}/delta/43/blocked_rooms.sql | 0 .../main}/delta/43/quarantine_media.sql | 0 .../main}/delta/43/url_cache.sql | 0 .../main}/delta/43/user_share.sql | 0 .../main}/delta/44/expire_url_cache.sql | 0 .../main}/delta/45/group_server.sql | 0 .../main}/delta/45/profile_cache.sql | 0 .../main}/delta/46/drop_refresh_tokens.sql | 0 .../delta/46/drop_unique_deleted_pushers.sql | 0 .../main}/delta/46/group_server.sql | 0 .../46/local_media_repository_url_idx.sql | 0 .../main}/delta/46/user_dir_null_room_ids.sql | 0 .../main}/delta/46/user_dir_typos.sql | 0 .../main}/delta/47/last_access_media.sql | 0 .../main}/delta/47/postgres_fts_gin.sql | 0 .../main}/delta/47/push_actions_staging.sql | 0 .../main}/delta/48/add_user_consent.sql | 0 .../delta/48/add_user_ips_last_seen_index.sql | 0 .../main}/delta/48/deactivated_users.sql | 0 .../main}/delta/48/group_unique_indexes.py | 0 .../main}/delta/48/groups_joinable.sql | 0 .../add_user_consent_server_notice_sent.sql | 0 .../main}/delta/49/add_user_daily_visits.sql | 0 .../49/add_user_ips_last_seen_only_index.sql | 0 .../delta/50/add_creation_ts_users_index.sql | 0 .../main}/delta/50/erasure_store.sql | 0 .../delta/50/make_event_content_nullable.py | 0 .../main}/delta/51/e2e_room_keys.sql | 0 .../main}/delta/51/monthly_active_users.sql | 0 .../52/add_event_to_state_group_index.sql | 0 .../52/device_list_streams_unique_idx.sql | 0 .../main}/delta/52/e2e_room_keys.sql | 0 .../main}/delta/53/add_user_type_to_users.sql | 0 .../main}/delta/53/drop_sent_transactions.sql | 0 .../main}/delta/53/event_format_version.sql | 0 .../main}/delta/53/user_dir_populate.sql | 0 .../main}/delta/53/user_ips_index.sql | 0 .../main}/delta/53/user_share.sql | 0 .../main}/delta/53/user_threepid_id.sql | 0 .../main}/delta/53/users_in_public_rooms.sql | 0 .../54/account_validity_with_renewal.sql | 0 .../delta/54/add_validity_to_server_keys.sql | 0 .../delta/54/delete_forward_extremities.sql | 0 .../main}/delta/54/drop_legacy_tables.sql | 0 .../main}/delta/54/drop_presence_list.sql | 0 .../main}/delta/54/relations.sql | 0 .../schema => schema/main}/delta/54/stats.sql | 0 .../main}/delta/54/stats2.sql | 0 .../main}/delta/55/access_token_expiry.sql | 0 .../delta/55/track_threepid_validations.sql | 0 .../delta/55/users_alter_deactivated.sql | 0 .../delta/56/add_spans_to_device_lists.sql | 0 .../56/current_state_events_membership.sql | 0 .../current_state_events_membership_mk2.sql | 0 .../56/delete_keys_from_deleted_backups.sql | 0 .../delta/56/destinations_failure_ts.sql | 0 ...tinations_retry_interval_type.sql.postgres | 0 .../delta/56/device_stream_id_insert.sql | 0 .../main}/delta/56/devices_last_seen.sql | 0 .../delta/56/drop_unused_event_tables.sql | 0 .../main}/delta/56/event_expiry.sql | 0 .../main}/delta/56/event_labels.sql | 0 .../56/event_labels_background_update.sql | 0 .../main}/delta/56/fix_room_keys_index.sql | 0 .../main}/delta/56/hidden_devices.sql | 0 .../delta/56/hidden_devices_fix.sql.sqlite | 0 .../56/nuke_empty_communities_from_db.sql | 0 .../main}/delta/56/public_room_list_idx.sql | 0 .../main}/delta/56/redaction_censor.sql | 0 .../main}/delta/56/redaction_censor2.sql | 0 .../redaction_censor3_fix_update.sql.postgres | 0 .../main}/delta/56/redaction_censor4.sql | 0 ...remove_tombstoned_rooms_from_directory.sql | 0 .../main}/delta/56/room_key_etag.sql | 0 .../main}/delta/56/room_membership_idx.sql | 0 .../main}/delta/56/room_retention.sql | 0 .../main}/delta/56/signing_keys.sql | 0 .../56/signing_keys_nonunique_signatures.sql | 0 .../main}/delta/56/stats_separated.sql | 0 .../delta/56/unique_user_filter_index.py | 0 .../main}/delta/56/user_external_ids.sql | 0 .../delta/56/users_in_public_rooms_idx.sql | 0 .../57/delete_old_current_state_events.sql | 0 .../57/device_list_remote_cache_stale.sql | 0 .../delta/57/local_current_membership.py | 0 .../delta/57/remove_sent_outbound_pokes.sql | 0 .../main}/delta/57/rooms_version_column.sql | 0 .../57/rooms_version_column_2.sql.postgres | 0 .../57/rooms_version_column_2.sql.sqlite | 0 .../57/rooms_version_column_3.sql.postgres | 0 .../57/rooms_version_column_3.sql.sqlite | 0 .../delta/58/02remove_dup_outbound_pokes.sql | 0 .../main}/delta/58/03persist_ui_auth.sql | 0 .../delta/58/05cache_instance.sql.postgres | 0 .../main}/delta/58/06dlols_unique_idx.py | 0 ...ethod_to_thumbnail_constraint.sql.postgres | 0 ..._method_to_thumbnail_constraint.sql.sqlite | 0 .../main}/delta/58/07persist_ui_auth_ips.sql | 0 ...08_media_safe_from_quarantine.sql.postgres | 0 .../08_media_safe_from_quarantine.sql.sqlite | 0 .../main}/delta/58/09shadow_ban.sql | 0 .../10_pushrules_enabled_delete_obsolete.sql | 0 .../58/10drop_local_rejections_stream.sql | 0 .../58/10federation_pos_instance_name.sql | 0 .../main}/delta/58/11dehydration.sql | 0 .../main}/delta/58/11fallback.sql | 0 .../main}/delta/58/11user_id_seq.py | 0 .../main}/delta/58/12room_stats.sql | 0 .../58/13remove_presence_allow_inbound.sql | 0 .../main}/delta/58/14events_instance_name.sql | 0 .../58/14events_instance_name.sql.postgres | 0 .../delta/58/15_catchup_destination_rooms.sql | 0 .../main}/delta/58/15unread_count.sql | 0 .../58/16populate_stats_process_rooms_fix.sql | 0 .../delta/58/17_catchup_last_successful.sql | 0 .../main}/delta/58/18stream_positions.sql | 0 .../delta/58/19instance_map.sql.postgres | 0 .../main}/delta/58/19txn_id.sql | 0 .../delta/58/20instance_name_event_tables.sql | 0 .../main}/delta/58/20user_daily_visits.sql | 0 .../main}/delta/58/21as_device_stream.sql | 0 .../delta/58/21drop_device_max_stream_id.sql | 0 .../main}/delta/58/22puppet_token.sql | 0 .../delta/58/22users_have_local_media.sql | 0 .../delta/58/23e2e_cross_signing_keys_idx.sql | 0 .../delta/58/24drop_event_json_index.sql | 0 .../58/25user_external_ids_user_id_idx.sql | 0 .../58/26access_token_last_validated.sql | 0 .../main}/delta/58/27local_invites.sql | 0 .../58/28drop_last_used_column.sql.postgres | 0 .../58/28drop_last_used_column.sql.sqlite | 0 .../main}/delta/59/01ignored_user.py | 0 .../main}/delta/59/02shard_send_to_device.sql | 0 ...shard_send_to_device_sequence.sql.postgres | 0 .../main}/delta/59/04_event_auth_chains.sql | 0 .../59/04_event_auth_chains.sql.postgres | 0 .../main}/delta/59/04drop_account_data.sql | 0 .../main}/delta/59/05cache_invalidation.sql | 0 .../main}/delta/59/06chain_cover_index.sql | 0 .../main}/delta/59/06shard_account_data.sql | 0 .../59/06shard_account_data.sql.postgres | 0 .../delta/59/07shard_account_data_fix.sql | 0 ...elete_pushers_for_deactivated_accounts.sql | 0 .../main}/delta/59/08delete_stale_pushers.sql | 0 .../delta/59/09rejected_events_metadata.sql | 0 .../delta/59/10delete_purged_chain_cover.sql | 0 .../11drop_thumbnail_constraint.sql.postgres | 0 .../12account_validity_token_used_ts_ms.sql | 0 .../delta/59/12presence_stream_instance.sql | 0 ...2presence_stream_instance_seq.sql.postgres | 0 .../full_schemas/16/application_services.sql | 0 .../main}/full_schemas/16/event_edges.sql | 0 .../full_schemas/16/event_signatures.sql | 0 .../main}/full_schemas/16/im.sql | 0 .../main}/full_schemas/16/keys.sql | 0 .../full_schemas/16/media_repository.sql | 0 .../main}/full_schemas/16/presence.sql | 0 .../main}/full_schemas/16/profiles.sql | 0 .../main}/full_schemas/16/push.sql | 0 .../main}/full_schemas/16/redactions.sql | 0 .../main}/full_schemas/16/room_aliases.sql | 0 .../main}/full_schemas/16/state.sql | 0 .../main}/full_schemas/16/transactions.sql | 0 .../main}/full_schemas/16/users.sql | 0 .../main}/full_schemas/54/full.sql.postgres | 0 .../main}/full_schemas/54/full.sql.sqlite | 0 .../full_schemas/54/stream_positions.sql | 0 .../state}/delta/23/drop_state_index.sql | 0 .../state}/delta/30/state_stream.sql | 0 .../state}/delta/32/remove_state_indices.sql | 0 .../state}/delta/35/add_state_index.sql | 0 .../state}/delta/35/state.sql | 0 .../state}/delta/35/state_dedupe.sql | 0 .../state}/delta/47/state_group_seq.py | 0 .../state}/delta/56/state_group_room_idx.sql | 0 .../state}/full_schemas/54/full.sql | 0 .../full_schemas/54/sequence.sql.postgres | 0 tests/storage/test_cleanup_extrems.py | 4 +- 284 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 changelog.d/9932.misc delete mode 100644 synapse/storage/databases/main/schema/full_schemas/README.md create mode 100644 synapse/storage/schema/README.md create mode 100644 synapse/storage/schema/__init__.py rename synapse/storage/schema/{ => common}/delta/25/00background_updates.sql (100%) rename synapse/storage/schema/{ => common}/delta/35/00background_updates_add_col.sql (100%) rename synapse/storage/schema/{ => common}/delta/58/00background_update_ordering.sql (100%) rename synapse/storage/schema/{ => common}/full_schemas/54/full.sql (100%) rename synapse/storage/schema/{ => common}/schema_version.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/12/v12.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/13/v13.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/14/v14.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/15/appservice_txns.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/15/presence_indices.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/15/v15.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/16/events_order_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/16/remote_media_cache_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/16/remove_duplicates.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/16/room_alias_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/16/unique_constraints.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/16/users.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/17/drop_indexes.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/17/server_keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/17/user_threepids.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/18/server_keys_bigger_ints.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/19/event_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/20/dummy.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/20/pushers.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/21/end_to_end_keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/21/receipts.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/22/receipts_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/22/user_threepids_unique.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/24/stats_reporting.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/25/fts.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/25/guest_access.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/25/history_visibility.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/25/tags.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/26/account_data.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/27/account_data.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/27/forgotten_memberships.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/27/ts.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/28/event_push_actions.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/28/events_room_stream.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/28/public_roms_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/28/receipts_user_id_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/28/upgrade_times.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/28/users_is_guest.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/29/push_actions.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/30/alias_creator.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/30/as_users.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/30/deleted_pushers.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/30/presence_stream.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/30/public_rooms.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/30/push_rule_stream.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/30/threepid_guest_access_tokens.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/31/invites.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/31/local_media_repository_url_cache.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/31/pushers.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/31/pushers_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/31/search_update.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/32/events.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/32/openid.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/32/pusher_throttle.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/32/remove_indices.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/32/reports.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/33/access_tokens_device_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/33/devices.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/33/devices_for_e2e_keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/33/devices_for_e2e_keys_clear_unknown_device.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/33/event_fields.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/33/remote_media_ts.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/33/user_ips_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/34/appservice_stream.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/34/cache_stream.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/34/device_inbox.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/34/push_display_name_rename.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/34/received_txn_purge.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/35/contains_url.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/35/device_outbox.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/35/device_stream_id.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/35/event_push_actions_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/35/public_room_list_change_stream.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/35/stream_order_to_extrem.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/36/readd_public_rooms.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/37/remove_auth_idx.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/37/user_threepids.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/38/postgres_fts_gist.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/39/appservice_room_list.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/39/device_federation_stream_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/39/event_push_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/39/federation_out_position.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/39/membership_profile.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/40/current_state_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/40/device_inbox.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/40/device_list_streams.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/40/event_push_summary.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/40/pushers.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/41/device_list_stream_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/41/device_outbound_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/41/event_search_event_id_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/41/ratelimit.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/42/current_state_delta.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/42/device_list_last_id.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/42/event_auth_state_only.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/42/user_dir.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/43/blocked_rooms.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/43/quarantine_media.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/43/url_cache.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/43/user_share.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/44/expire_url_cache.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/45/group_server.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/45/profile_cache.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/46/drop_refresh_tokens.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/46/drop_unique_deleted_pushers.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/46/group_server.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/46/local_media_repository_url_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/46/user_dir_null_room_ids.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/46/user_dir_typos.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/47/last_access_media.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/47/postgres_fts_gin.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/47/push_actions_staging.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/48/add_user_consent.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/48/add_user_ips_last_seen_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/48/deactivated_users.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/48/group_unique_indexes.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/48/groups_joinable.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/49/add_user_consent_server_notice_sent.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/49/add_user_daily_visits.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/49/add_user_ips_last_seen_only_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/50/add_creation_ts_users_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/50/erasure_store.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/50/make_event_content_nullable.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/51/e2e_room_keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/51/monthly_active_users.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/52/add_event_to_state_group_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/52/device_list_streams_unique_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/52/e2e_room_keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/add_user_type_to_users.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/drop_sent_transactions.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/event_format_version.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/user_dir_populate.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/user_ips_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/user_share.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/user_threepid_id.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/53/users_in_public_rooms.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/account_validity_with_renewal.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/add_validity_to_server_keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/delete_forward_extremities.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/drop_legacy_tables.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/drop_presence_list.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/relations.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/stats.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/54/stats2.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/55/access_token_expiry.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/55/track_threepid_validations.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/55/users_alter_deactivated.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/add_spans_to_device_lists.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/current_state_events_membership.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/current_state_events_membership_mk2.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/delete_keys_from_deleted_backups.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/destinations_failure_ts.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/destinations_retry_interval_type.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/device_stream_id_insert.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/devices_last_seen.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/drop_unused_event_tables.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/event_expiry.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/event_labels.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/event_labels_background_update.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/fix_room_keys_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/hidden_devices.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/hidden_devices_fix.sql.sqlite (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/nuke_empty_communities_from_db.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/public_room_list_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/redaction_censor.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/redaction_censor2.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/redaction_censor3_fix_update.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/redaction_censor4.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/remove_tombstoned_rooms_from_directory.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/room_key_etag.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/room_membership_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/room_retention.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/signing_keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/signing_keys_nonunique_signatures.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/stats_separated.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/unique_user_filter_index.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/user_external_ids.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/56/users_in_public_rooms_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/delete_old_current_state_events.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/device_list_remote_cache_stale.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/local_current_membership.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/remove_sent_outbound_pokes.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/rooms_version_column.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/rooms_version_column_2.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/rooms_version_column_2.sql.sqlite (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/rooms_version_column_3.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/57/rooms_version_column_3.sql.sqlite (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/02remove_dup_outbound_pokes.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/03persist_ui_auth.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/05cache_instance.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/06dlols_unique_idx.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/07add_method_to_thumbnail_constraint.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/07add_method_to_thumbnail_constraint.sql.sqlite (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/07persist_ui_auth_ips.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/08_media_safe_from_quarantine.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/08_media_safe_from_quarantine.sql.sqlite (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/09shadow_ban.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/10_pushrules_enabled_delete_obsolete.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/10drop_local_rejections_stream.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/10federation_pos_instance_name.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/11dehydration.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/11fallback.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/11user_id_seq.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/12room_stats.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/13remove_presence_allow_inbound.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/14events_instance_name.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/14events_instance_name.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/15_catchup_destination_rooms.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/15unread_count.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/16populate_stats_process_rooms_fix.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/17_catchup_last_successful.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/18stream_positions.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/19instance_map.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/19txn_id.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/20instance_name_event_tables.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/20user_daily_visits.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/21as_device_stream.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/21drop_device_max_stream_id.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/22puppet_token.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/22users_have_local_media.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/23e2e_cross_signing_keys_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/24drop_event_json_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/25user_external_ids_user_id_idx.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/26access_token_last_validated.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/27local_invites.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/28drop_last_used_column.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/58/28drop_last_used_column.sql.sqlite (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/01ignored_user.py (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/02shard_send_to_device.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/03shard_send_to_device_sequence.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/04_event_auth_chains.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/04_event_auth_chains.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/04drop_account_data.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/05cache_invalidation.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/06chain_cover_index.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/06shard_account_data.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/06shard_account_data.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/07shard_account_data_fix.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/08delete_pushers_for_deactivated_accounts.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/08delete_stale_pushers.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/09rejected_events_metadata.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/10delete_purged_chain_cover.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/11drop_thumbnail_constraint.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/12account_validity_token_used_ts_ms.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/12presence_stream_instance.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/delta/59/12presence_stream_instance_seq.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/application_services.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/event_edges.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/event_signatures.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/im.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/keys.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/media_repository.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/presence.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/profiles.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/push.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/redactions.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/room_aliases.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/state.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/transactions.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/16/users.sql (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/54/full.sql.postgres (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/54/full.sql.sqlite (100%) rename synapse/storage/{databases/main/schema => schema/main}/full_schemas/54/stream_positions.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/23/drop_state_index.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/30/state_stream.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/32/remove_state_indices.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/35/add_state_index.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/35/state.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/35/state_dedupe.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/47/state_group_seq.py (100%) rename synapse/storage/{databases/state/schema => schema/state}/delta/56/state_group_room_idx.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/full_schemas/54/full.sql (100%) rename synapse/storage/{databases/state/schema => schema/state}/full_schemas/54/sequence.sql.postgres (100%) diff --git a/changelog.d/9932.misc b/changelog.d/9932.misc new file mode 100644 index 0000000000..9e16a36173 --- /dev/null +++ b/changelog.d/9932.misc @@ -0,0 +1 @@ +Move database schema files into a common directory. diff --git a/synapse/storage/databases/main/schema/full_schemas/README.md b/synapse/storage/databases/main/schema/full_schemas/README.md deleted file mode 100644 index c00f287190..0000000000 --- a/synapse/storage/databases/main/schema/full_schemas/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Synapse Database Schemas - -These schemas are used as a basis to create brand new Synapse databases, on both -SQLite3 and Postgres. - -## Building full schema dumps - -If you want to recreate these schemas, they need to be made from a database that -has had all background updates run. - -To do so, use `scripts-dev/make_full_schema.sh`. This will produce new -`full.sql.postgres ` and `full.sql.sqlite` files. - -Ensure postgres is installed and your user has the ability to run bash commands -such as `createdb`, then call - - ./scripts-dev/make_full_schema.sh -p postgres_username -o output_dir/ - -There are currently two folders with full-schema snapshots. `16` is a snapshot -from 2015, for historical reference. The other contains the most recent full -schema snapshot. diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 7a2cbee426..3799d46734 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -26,16 +26,13 @@ from synapse.storage.database import LoggingDatabaseConnection from synapse.storage.engines import BaseDatabaseEngine from synapse.storage.engines.postgres import PostgresEngine +from synapse.storage.schema import SCHEMA_VERSION from synapse.storage.types import Cursor logger = logging.getLogger(__name__) -# Remember to update this number every time a change is made to database -# schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 59 - -dir_path = os.path.abspath(os.path.dirname(__file__)) +schema_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "schema") class PrepareDatabaseException(Exception): @@ -167,7 +164,14 @@ def _setup_new_database( Example directory structure: - schema/ + schema/ + common/ + delta/ + ... + full_schemas/ + 11/ + foo.sql + main/ delta/ ... full_schemas/ @@ -175,15 +179,14 @@ def _setup_new_database( test.sql ... 11/ - foo.sql bar.sql ... In the example foo.sql and bar.sql would be run, and then any delta files for versions strictly greater than 11. - Note: we apply the full schemas and deltas from the top level `schema/` - folder as well those in the data stores specified. + Note: we apply the full schemas and deltas from the `schema/common` + folder as well those in the databases specified. Args: cur: a database cursor @@ -195,12 +198,12 @@ def _setup_new_database( # configured to our liking. database_engine.check_new_database(cur) - current_dir = os.path.join(dir_path, "schema", "full_schemas") + full_schemas_dir = os.path.join(schema_path, "common", "full_schemas") # First we find the highest full schema version we have valid_versions = [] - for filename in os.listdir(current_dir): + for filename in os.listdir(full_schemas_dir): try: ver = int(filename) except ValueError: @@ -218,15 +221,13 @@ def _setup_new_database( logger.debug("Initialising schema v%d", max_current_ver) - # Now lets find all the full schema files, both in the global schema and - # in data store schemas. - directories = [os.path.join(current_dir, str(max_current_ver))] + # Now let's find all the full schema files, both in the common schema and + # in database schemas. + directories = [os.path.join(full_schemas_dir, str(max_current_ver))] directories.extend( os.path.join( - dir_path, - "databases", + schema_path, database, - "schema", "full_schemas", str(max_current_ver), ) @@ -357,6 +358,9 @@ def _upgrade_existing_database( check_database_before_upgrade(cur, database_engine, config) start_ver = current_version + + # if we got to this schema version by running a full_schema rather than a series + # of deltas, we should not run the deltas for this version. if not upgraded: start_ver += 1 @@ -385,12 +389,10 @@ def _upgrade_existing_database( # directories for schema updates. # First we find the directories to search in - delta_dir = os.path.join(dir_path, "schema", "delta", str(v)) + delta_dir = os.path.join(schema_path, "common", "delta", str(v)) directories = [delta_dir] for database in databases: - directories.append( - os.path.join(dir_path, "databases", database, "schema", "delta", str(v)) - ) + directories.append(os.path.join(schema_path, database, "delta", str(v))) # Used to check if we have any duplicate file names file_name_counter = Counter() # type: CounterType[str] @@ -621,8 +623,8 @@ def _get_or_create_schema_state( txn: Cursor, database_engine: BaseDatabaseEngine ) -> Optional[Tuple[int, List[str], bool]]: # Bluntly try creating the schema_version tables. - schema_path = os.path.join(dir_path, "schema", "schema_version.sql") - executescript(txn, schema_path) + sql_path = os.path.join(schema_path, "common", "schema_version.sql") + executescript(txn, sql_path) txn.execute("SELECT version, upgraded FROM schema_version") row = txn.fetchone() diff --git a/synapse/storage/schema/README.md b/synapse/storage/schema/README.md new file mode 100644 index 0000000000..030153db64 --- /dev/null +++ b/synapse/storage/schema/README.md @@ -0,0 +1,37 @@ +# Synapse Database Schemas + +This directory contains the schema files used to build Synapse databases. + +Synapse supports splitting its datastore across multiple physical databases (which can +be useful for large installations), and the schema files are therefore split according +to the logical database they are apply to. + +At the time of writing, the following "logical" databases are supported: + +* `state` - used to store Matrix room state (more specifically, `state_groups`, + their relationships and contents.) +* `main` - stores everything else. + +Addionally, the `common` directory contains schema files for tables which must be +present on *all* physical databases. + +## Full schema dumps + +In the `full_schemas` directories, only the most recently-numbered snapshot is useful +(`54` at the time of writing). Older snapshots (eg, `16`) are present for historical +reference only. + +## Building full schema dumps + +If you want to recreate these schemas, they need to be made from a database that +has had all background updates run. + +To do so, use `scripts-dev/make_full_schema.sh`. This will produce new +`full.sql.postgres` and `full.sql.sqlite` files. + +Ensure postgres is installed, then run: + + ./scripts-dev/make_full_schema.sh -p postgres_username -o output_dir/ + +NB at the time of writing, this script predates the split into separate `state`/`main` +databases so will require updates to handle that correctly. diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py new file mode 100644 index 0000000000..f0d9f23167 --- /dev/null +++ b/synapse/storage/schema/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Remember to update this number every time a change is made to database +# schema files, so the users will be informed on server restarts. +SCHEMA_VERSION = 59 diff --git a/synapse/storage/schema/delta/25/00background_updates.sql b/synapse/storage/schema/common/delta/25/00background_updates.sql similarity index 100% rename from synapse/storage/schema/delta/25/00background_updates.sql rename to synapse/storage/schema/common/delta/25/00background_updates.sql diff --git a/synapse/storage/schema/delta/35/00background_updates_add_col.sql b/synapse/storage/schema/common/delta/35/00background_updates_add_col.sql similarity index 100% rename from synapse/storage/schema/delta/35/00background_updates_add_col.sql rename to synapse/storage/schema/common/delta/35/00background_updates_add_col.sql diff --git a/synapse/storage/schema/delta/58/00background_update_ordering.sql b/synapse/storage/schema/common/delta/58/00background_update_ordering.sql similarity index 100% rename from synapse/storage/schema/delta/58/00background_update_ordering.sql rename to synapse/storage/schema/common/delta/58/00background_update_ordering.sql diff --git a/synapse/storage/schema/full_schemas/54/full.sql b/synapse/storage/schema/common/full_schemas/54/full.sql similarity index 100% rename from synapse/storage/schema/full_schemas/54/full.sql rename to synapse/storage/schema/common/full_schemas/54/full.sql diff --git a/synapse/storage/schema/schema_version.sql b/synapse/storage/schema/common/schema_version.sql similarity index 100% rename from synapse/storage/schema/schema_version.sql rename to synapse/storage/schema/common/schema_version.sql diff --git a/synapse/storage/databases/main/schema/delta/12/v12.sql b/synapse/storage/schema/main/delta/12/v12.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/12/v12.sql rename to synapse/storage/schema/main/delta/12/v12.sql diff --git a/synapse/storage/databases/main/schema/delta/13/v13.sql b/synapse/storage/schema/main/delta/13/v13.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/13/v13.sql rename to synapse/storage/schema/main/delta/13/v13.sql diff --git a/synapse/storage/databases/main/schema/delta/14/v14.sql b/synapse/storage/schema/main/delta/14/v14.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/14/v14.sql rename to synapse/storage/schema/main/delta/14/v14.sql diff --git a/synapse/storage/databases/main/schema/delta/15/appservice_txns.sql b/synapse/storage/schema/main/delta/15/appservice_txns.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/15/appservice_txns.sql rename to synapse/storage/schema/main/delta/15/appservice_txns.sql diff --git a/synapse/storage/databases/main/schema/delta/15/presence_indices.sql b/synapse/storage/schema/main/delta/15/presence_indices.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/15/presence_indices.sql rename to synapse/storage/schema/main/delta/15/presence_indices.sql diff --git a/synapse/storage/databases/main/schema/delta/15/v15.sql b/synapse/storage/schema/main/delta/15/v15.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/15/v15.sql rename to synapse/storage/schema/main/delta/15/v15.sql diff --git a/synapse/storage/databases/main/schema/delta/16/events_order_index.sql b/synapse/storage/schema/main/delta/16/events_order_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/16/events_order_index.sql rename to synapse/storage/schema/main/delta/16/events_order_index.sql diff --git a/synapse/storage/databases/main/schema/delta/16/remote_media_cache_index.sql b/synapse/storage/schema/main/delta/16/remote_media_cache_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/16/remote_media_cache_index.sql rename to synapse/storage/schema/main/delta/16/remote_media_cache_index.sql diff --git a/synapse/storage/databases/main/schema/delta/16/remove_duplicates.sql b/synapse/storage/schema/main/delta/16/remove_duplicates.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/16/remove_duplicates.sql rename to synapse/storage/schema/main/delta/16/remove_duplicates.sql diff --git a/synapse/storage/databases/main/schema/delta/16/room_alias_index.sql b/synapse/storage/schema/main/delta/16/room_alias_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/16/room_alias_index.sql rename to synapse/storage/schema/main/delta/16/room_alias_index.sql diff --git a/synapse/storage/databases/main/schema/delta/16/unique_constraints.sql b/synapse/storage/schema/main/delta/16/unique_constraints.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/16/unique_constraints.sql rename to synapse/storage/schema/main/delta/16/unique_constraints.sql diff --git a/synapse/storage/databases/main/schema/delta/16/users.sql b/synapse/storage/schema/main/delta/16/users.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/16/users.sql rename to synapse/storage/schema/main/delta/16/users.sql diff --git a/synapse/storage/databases/main/schema/delta/17/drop_indexes.sql b/synapse/storage/schema/main/delta/17/drop_indexes.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/17/drop_indexes.sql rename to synapse/storage/schema/main/delta/17/drop_indexes.sql diff --git a/synapse/storage/databases/main/schema/delta/17/server_keys.sql b/synapse/storage/schema/main/delta/17/server_keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/17/server_keys.sql rename to synapse/storage/schema/main/delta/17/server_keys.sql diff --git a/synapse/storage/databases/main/schema/delta/17/user_threepids.sql b/synapse/storage/schema/main/delta/17/user_threepids.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/17/user_threepids.sql rename to synapse/storage/schema/main/delta/17/user_threepids.sql diff --git a/synapse/storage/databases/main/schema/delta/18/server_keys_bigger_ints.sql b/synapse/storage/schema/main/delta/18/server_keys_bigger_ints.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/18/server_keys_bigger_ints.sql rename to synapse/storage/schema/main/delta/18/server_keys_bigger_ints.sql diff --git a/synapse/storage/databases/main/schema/delta/19/event_index.sql b/synapse/storage/schema/main/delta/19/event_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/19/event_index.sql rename to synapse/storage/schema/main/delta/19/event_index.sql diff --git a/synapse/storage/databases/main/schema/delta/20/dummy.sql b/synapse/storage/schema/main/delta/20/dummy.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/20/dummy.sql rename to synapse/storage/schema/main/delta/20/dummy.sql diff --git a/synapse/storage/databases/main/schema/delta/20/pushers.py b/synapse/storage/schema/main/delta/20/pushers.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/20/pushers.py rename to synapse/storage/schema/main/delta/20/pushers.py diff --git a/synapse/storage/databases/main/schema/delta/21/end_to_end_keys.sql b/synapse/storage/schema/main/delta/21/end_to_end_keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/21/end_to_end_keys.sql rename to synapse/storage/schema/main/delta/21/end_to_end_keys.sql diff --git a/synapse/storage/databases/main/schema/delta/21/receipts.sql b/synapse/storage/schema/main/delta/21/receipts.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/21/receipts.sql rename to synapse/storage/schema/main/delta/21/receipts.sql diff --git a/synapse/storage/databases/main/schema/delta/22/receipts_index.sql b/synapse/storage/schema/main/delta/22/receipts_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/22/receipts_index.sql rename to synapse/storage/schema/main/delta/22/receipts_index.sql diff --git a/synapse/storage/databases/main/schema/delta/22/user_threepids_unique.sql b/synapse/storage/schema/main/delta/22/user_threepids_unique.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/22/user_threepids_unique.sql rename to synapse/storage/schema/main/delta/22/user_threepids_unique.sql diff --git a/synapse/storage/databases/main/schema/delta/24/stats_reporting.sql b/synapse/storage/schema/main/delta/24/stats_reporting.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/24/stats_reporting.sql rename to synapse/storage/schema/main/delta/24/stats_reporting.sql diff --git a/synapse/storage/databases/main/schema/delta/25/fts.py b/synapse/storage/schema/main/delta/25/fts.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/25/fts.py rename to synapse/storage/schema/main/delta/25/fts.py diff --git a/synapse/storage/databases/main/schema/delta/25/guest_access.sql b/synapse/storage/schema/main/delta/25/guest_access.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/25/guest_access.sql rename to synapse/storage/schema/main/delta/25/guest_access.sql diff --git a/synapse/storage/databases/main/schema/delta/25/history_visibility.sql b/synapse/storage/schema/main/delta/25/history_visibility.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/25/history_visibility.sql rename to synapse/storage/schema/main/delta/25/history_visibility.sql diff --git a/synapse/storage/databases/main/schema/delta/25/tags.sql b/synapse/storage/schema/main/delta/25/tags.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/25/tags.sql rename to synapse/storage/schema/main/delta/25/tags.sql diff --git a/synapse/storage/databases/main/schema/delta/26/account_data.sql b/synapse/storage/schema/main/delta/26/account_data.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/26/account_data.sql rename to synapse/storage/schema/main/delta/26/account_data.sql diff --git a/synapse/storage/databases/main/schema/delta/27/account_data.sql b/synapse/storage/schema/main/delta/27/account_data.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/27/account_data.sql rename to synapse/storage/schema/main/delta/27/account_data.sql diff --git a/synapse/storage/databases/main/schema/delta/27/forgotten_memberships.sql b/synapse/storage/schema/main/delta/27/forgotten_memberships.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/27/forgotten_memberships.sql rename to synapse/storage/schema/main/delta/27/forgotten_memberships.sql diff --git a/synapse/storage/databases/main/schema/delta/27/ts.py b/synapse/storage/schema/main/delta/27/ts.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/27/ts.py rename to synapse/storage/schema/main/delta/27/ts.py diff --git a/synapse/storage/databases/main/schema/delta/28/event_push_actions.sql b/synapse/storage/schema/main/delta/28/event_push_actions.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/28/event_push_actions.sql rename to synapse/storage/schema/main/delta/28/event_push_actions.sql diff --git a/synapse/storage/databases/main/schema/delta/28/events_room_stream.sql b/synapse/storage/schema/main/delta/28/events_room_stream.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/28/events_room_stream.sql rename to synapse/storage/schema/main/delta/28/events_room_stream.sql diff --git a/synapse/storage/databases/main/schema/delta/28/public_roms_index.sql b/synapse/storage/schema/main/delta/28/public_roms_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/28/public_roms_index.sql rename to synapse/storage/schema/main/delta/28/public_roms_index.sql diff --git a/synapse/storage/databases/main/schema/delta/28/receipts_user_id_index.sql b/synapse/storage/schema/main/delta/28/receipts_user_id_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/28/receipts_user_id_index.sql rename to synapse/storage/schema/main/delta/28/receipts_user_id_index.sql diff --git a/synapse/storage/databases/main/schema/delta/28/upgrade_times.sql b/synapse/storage/schema/main/delta/28/upgrade_times.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/28/upgrade_times.sql rename to synapse/storage/schema/main/delta/28/upgrade_times.sql diff --git a/synapse/storage/databases/main/schema/delta/28/users_is_guest.sql b/synapse/storage/schema/main/delta/28/users_is_guest.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/28/users_is_guest.sql rename to synapse/storage/schema/main/delta/28/users_is_guest.sql diff --git a/synapse/storage/databases/main/schema/delta/29/push_actions.sql b/synapse/storage/schema/main/delta/29/push_actions.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/29/push_actions.sql rename to synapse/storage/schema/main/delta/29/push_actions.sql diff --git a/synapse/storage/databases/main/schema/delta/30/alias_creator.sql b/synapse/storage/schema/main/delta/30/alias_creator.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/30/alias_creator.sql rename to synapse/storage/schema/main/delta/30/alias_creator.sql diff --git a/synapse/storage/databases/main/schema/delta/30/as_users.py b/synapse/storage/schema/main/delta/30/as_users.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/30/as_users.py rename to synapse/storage/schema/main/delta/30/as_users.py diff --git a/synapse/storage/databases/main/schema/delta/30/deleted_pushers.sql b/synapse/storage/schema/main/delta/30/deleted_pushers.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/30/deleted_pushers.sql rename to synapse/storage/schema/main/delta/30/deleted_pushers.sql diff --git a/synapse/storage/databases/main/schema/delta/30/presence_stream.sql b/synapse/storage/schema/main/delta/30/presence_stream.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/30/presence_stream.sql rename to synapse/storage/schema/main/delta/30/presence_stream.sql diff --git a/synapse/storage/databases/main/schema/delta/30/public_rooms.sql b/synapse/storage/schema/main/delta/30/public_rooms.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/30/public_rooms.sql rename to synapse/storage/schema/main/delta/30/public_rooms.sql diff --git a/synapse/storage/databases/main/schema/delta/30/push_rule_stream.sql b/synapse/storage/schema/main/delta/30/push_rule_stream.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/30/push_rule_stream.sql rename to synapse/storage/schema/main/delta/30/push_rule_stream.sql diff --git a/synapse/storage/databases/main/schema/delta/30/threepid_guest_access_tokens.sql b/synapse/storage/schema/main/delta/30/threepid_guest_access_tokens.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/30/threepid_guest_access_tokens.sql rename to synapse/storage/schema/main/delta/30/threepid_guest_access_tokens.sql diff --git a/synapse/storage/databases/main/schema/delta/31/invites.sql b/synapse/storage/schema/main/delta/31/invites.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/31/invites.sql rename to synapse/storage/schema/main/delta/31/invites.sql diff --git a/synapse/storage/databases/main/schema/delta/31/local_media_repository_url_cache.sql b/synapse/storage/schema/main/delta/31/local_media_repository_url_cache.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/31/local_media_repository_url_cache.sql rename to synapse/storage/schema/main/delta/31/local_media_repository_url_cache.sql diff --git a/synapse/storage/databases/main/schema/delta/31/pushers.py b/synapse/storage/schema/main/delta/31/pushers.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/31/pushers.py rename to synapse/storage/schema/main/delta/31/pushers.py diff --git a/synapse/storage/databases/main/schema/delta/31/pushers_index.sql b/synapse/storage/schema/main/delta/31/pushers_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/31/pushers_index.sql rename to synapse/storage/schema/main/delta/31/pushers_index.sql diff --git a/synapse/storage/databases/main/schema/delta/31/search_update.py b/synapse/storage/schema/main/delta/31/search_update.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/31/search_update.py rename to synapse/storage/schema/main/delta/31/search_update.py diff --git a/synapse/storage/databases/main/schema/delta/32/events.sql b/synapse/storage/schema/main/delta/32/events.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/32/events.sql rename to synapse/storage/schema/main/delta/32/events.sql diff --git a/synapse/storage/databases/main/schema/delta/32/openid.sql b/synapse/storage/schema/main/delta/32/openid.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/32/openid.sql rename to synapse/storage/schema/main/delta/32/openid.sql diff --git a/synapse/storage/databases/main/schema/delta/32/pusher_throttle.sql b/synapse/storage/schema/main/delta/32/pusher_throttle.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/32/pusher_throttle.sql rename to synapse/storage/schema/main/delta/32/pusher_throttle.sql diff --git a/synapse/storage/databases/main/schema/delta/32/remove_indices.sql b/synapse/storage/schema/main/delta/32/remove_indices.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/32/remove_indices.sql rename to synapse/storage/schema/main/delta/32/remove_indices.sql diff --git a/synapse/storage/databases/main/schema/delta/32/reports.sql b/synapse/storage/schema/main/delta/32/reports.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/32/reports.sql rename to synapse/storage/schema/main/delta/32/reports.sql diff --git a/synapse/storage/databases/main/schema/delta/33/access_tokens_device_index.sql b/synapse/storage/schema/main/delta/33/access_tokens_device_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/33/access_tokens_device_index.sql rename to synapse/storage/schema/main/delta/33/access_tokens_device_index.sql diff --git a/synapse/storage/databases/main/schema/delta/33/devices.sql b/synapse/storage/schema/main/delta/33/devices.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/33/devices.sql rename to synapse/storage/schema/main/delta/33/devices.sql diff --git a/synapse/storage/databases/main/schema/delta/33/devices_for_e2e_keys.sql b/synapse/storage/schema/main/delta/33/devices_for_e2e_keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/33/devices_for_e2e_keys.sql rename to synapse/storage/schema/main/delta/33/devices_for_e2e_keys.sql diff --git a/synapse/storage/databases/main/schema/delta/33/devices_for_e2e_keys_clear_unknown_device.sql b/synapse/storage/schema/main/delta/33/devices_for_e2e_keys_clear_unknown_device.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/33/devices_for_e2e_keys_clear_unknown_device.sql rename to synapse/storage/schema/main/delta/33/devices_for_e2e_keys_clear_unknown_device.sql diff --git a/synapse/storage/databases/main/schema/delta/33/event_fields.py b/synapse/storage/schema/main/delta/33/event_fields.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/33/event_fields.py rename to synapse/storage/schema/main/delta/33/event_fields.py diff --git a/synapse/storage/databases/main/schema/delta/33/remote_media_ts.py b/synapse/storage/schema/main/delta/33/remote_media_ts.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/33/remote_media_ts.py rename to synapse/storage/schema/main/delta/33/remote_media_ts.py diff --git a/synapse/storage/databases/main/schema/delta/33/user_ips_index.sql b/synapse/storage/schema/main/delta/33/user_ips_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/33/user_ips_index.sql rename to synapse/storage/schema/main/delta/33/user_ips_index.sql diff --git a/synapse/storage/databases/main/schema/delta/34/appservice_stream.sql b/synapse/storage/schema/main/delta/34/appservice_stream.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/34/appservice_stream.sql rename to synapse/storage/schema/main/delta/34/appservice_stream.sql diff --git a/synapse/storage/databases/main/schema/delta/34/cache_stream.py b/synapse/storage/schema/main/delta/34/cache_stream.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/34/cache_stream.py rename to synapse/storage/schema/main/delta/34/cache_stream.py diff --git a/synapse/storage/databases/main/schema/delta/34/device_inbox.sql b/synapse/storage/schema/main/delta/34/device_inbox.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/34/device_inbox.sql rename to synapse/storage/schema/main/delta/34/device_inbox.sql diff --git a/synapse/storage/databases/main/schema/delta/34/push_display_name_rename.sql b/synapse/storage/schema/main/delta/34/push_display_name_rename.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/34/push_display_name_rename.sql rename to synapse/storage/schema/main/delta/34/push_display_name_rename.sql diff --git a/synapse/storage/databases/main/schema/delta/34/received_txn_purge.py b/synapse/storage/schema/main/delta/34/received_txn_purge.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/34/received_txn_purge.py rename to synapse/storage/schema/main/delta/34/received_txn_purge.py diff --git a/synapse/storage/databases/main/schema/delta/35/contains_url.sql b/synapse/storage/schema/main/delta/35/contains_url.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/35/contains_url.sql rename to synapse/storage/schema/main/delta/35/contains_url.sql diff --git a/synapse/storage/databases/main/schema/delta/35/device_outbox.sql b/synapse/storage/schema/main/delta/35/device_outbox.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/35/device_outbox.sql rename to synapse/storage/schema/main/delta/35/device_outbox.sql diff --git a/synapse/storage/databases/main/schema/delta/35/device_stream_id.sql b/synapse/storage/schema/main/delta/35/device_stream_id.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/35/device_stream_id.sql rename to synapse/storage/schema/main/delta/35/device_stream_id.sql diff --git a/synapse/storage/databases/main/schema/delta/35/event_push_actions_index.sql b/synapse/storage/schema/main/delta/35/event_push_actions_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/35/event_push_actions_index.sql rename to synapse/storage/schema/main/delta/35/event_push_actions_index.sql diff --git a/synapse/storage/databases/main/schema/delta/35/public_room_list_change_stream.sql b/synapse/storage/schema/main/delta/35/public_room_list_change_stream.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/35/public_room_list_change_stream.sql rename to synapse/storage/schema/main/delta/35/public_room_list_change_stream.sql diff --git a/synapse/storage/databases/main/schema/delta/35/stream_order_to_extrem.sql b/synapse/storage/schema/main/delta/35/stream_order_to_extrem.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/35/stream_order_to_extrem.sql rename to synapse/storage/schema/main/delta/35/stream_order_to_extrem.sql diff --git a/synapse/storage/databases/main/schema/delta/36/readd_public_rooms.sql b/synapse/storage/schema/main/delta/36/readd_public_rooms.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/36/readd_public_rooms.sql rename to synapse/storage/schema/main/delta/36/readd_public_rooms.sql diff --git a/synapse/storage/databases/main/schema/delta/37/remove_auth_idx.py b/synapse/storage/schema/main/delta/37/remove_auth_idx.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/37/remove_auth_idx.py rename to synapse/storage/schema/main/delta/37/remove_auth_idx.py diff --git a/synapse/storage/databases/main/schema/delta/37/user_threepids.sql b/synapse/storage/schema/main/delta/37/user_threepids.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/37/user_threepids.sql rename to synapse/storage/schema/main/delta/37/user_threepids.sql diff --git a/synapse/storage/databases/main/schema/delta/38/postgres_fts_gist.sql b/synapse/storage/schema/main/delta/38/postgres_fts_gist.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/38/postgres_fts_gist.sql rename to synapse/storage/schema/main/delta/38/postgres_fts_gist.sql diff --git a/synapse/storage/databases/main/schema/delta/39/appservice_room_list.sql b/synapse/storage/schema/main/delta/39/appservice_room_list.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/39/appservice_room_list.sql rename to synapse/storage/schema/main/delta/39/appservice_room_list.sql diff --git a/synapse/storage/databases/main/schema/delta/39/device_federation_stream_idx.sql b/synapse/storage/schema/main/delta/39/device_federation_stream_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/39/device_federation_stream_idx.sql rename to synapse/storage/schema/main/delta/39/device_federation_stream_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/39/event_push_index.sql b/synapse/storage/schema/main/delta/39/event_push_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/39/event_push_index.sql rename to synapse/storage/schema/main/delta/39/event_push_index.sql diff --git a/synapse/storage/databases/main/schema/delta/39/federation_out_position.sql b/synapse/storage/schema/main/delta/39/federation_out_position.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/39/federation_out_position.sql rename to synapse/storage/schema/main/delta/39/federation_out_position.sql diff --git a/synapse/storage/databases/main/schema/delta/39/membership_profile.sql b/synapse/storage/schema/main/delta/39/membership_profile.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/39/membership_profile.sql rename to synapse/storage/schema/main/delta/39/membership_profile.sql diff --git a/synapse/storage/databases/main/schema/delta/40/current_state_idx.sql b/synapse/storage/schema/main/delta/40/current_state_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/40/current_state_idx.sql rename to synapse/storage/schema/main/delta/40/current_state_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/40/device_inbox.sql b/synapse/storage/schema/main/delta/40/device_inbox.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/40/device_inbox.sql rename to synapse/storage/schema/main/delta/40/device_inbox.sql diff --git a/synapse/storage/databases/main/schema/delta/40/device_list_streams.sql b/synapse/storage/schema/main/delta/40/device_list_streams.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/40/device_list_streams.sql rename to synapse/storage/schema/main/delta/40/device_list_streams.sql diff --git a/synapse/storage/databases/main/schema/delta/40/event_push_summary.sql b/synapse/storage/schema/main/delta/40/event_push_summary.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/40/event_push_summary.sql rename to synapse/storage/schema/main/delta/40/event_push_summary.sql diff --git a/synapse/storage/databases/main/schema/delta/40/pushers.sql b/synapse/storage/schema/main/delta/40/pushers.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/40/pushers.sql rename to synapse/storage/schema/main/delta/40/pushers.sql diff --git a/synapse/storage/databases/main/schema/delta/41/device_list_stream_idx.sql b/synapse/storage/schema/main/delta/41/device_list_stream_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/41/device_list_stream_idx.sql rename to synapse/storage/schema/main/delta/41/device_list_stream_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/41/device_outbound_index.sql b/synapse/storage/schema/main/delta/41/device_outbound_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/41/device_outbound_index.sql rename to synapse/storage/schema/main/delta/41/device_outbound_index.sql diff --git a/synapse/storage/databases/main/schema/delta/41/event_search_event_id_idx.sql b/synapse/storage/schema/main/delta/41/event_search_event_id_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/41/event_search_event_id_idx.sql rename to synapse/storage/schema/main/delta/41/event_search_event_id_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/41/ratelimit.sql b/synapse/storage/schema/main/delta/41/ratelimit.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/41/ratelimit.sql rename to synapse/storage/schema/main/delta/41/ratelimit.sql diff --git a/synapse/storage/databases/main/schema/delta/42/current_state_delta.sql b/synapse/storage/schema/main/delta/42/current_state_delta.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/42/current_state_delta.sql rename to synapse/storage/schema/main/delta/42/current_state_delta.sql diff --git a/synapse/storage/databases/main/schema/delta/42/device_list_last_id.sql b/synapse/storage/schema/main/delta/42/device_list_last_id.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/42/device_list_last_id.sql rename to synapse/storage/schema/main/delta/42/device_list_last_id.sql diff --git a/synapse/storage/databases/main/schema/delta/42/event_auth_state_only.sql b/synapse/storage/schema/main/delta/42/event_auth_state_only.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/42/event_auth_state_only.sql rename to synapse/storage/schema/main/delta/42/event_auth_state_only.sql diff --git a/synapse/storage/databases/main/schema/delta/42/user_dir.py b/synapse/storage/schema/main/delta/42/user_dir.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/42/user_dir.py rename to synapse/storage/schema/main/delta/42/user_dir.py diff --git a/synapse/storage/databases/main/schema/delta/43/blocked_rooms.sql b/synapse/storage/schema/main/delta/43/blocked_rooms.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/43/blocked_rooms.sql rename to synapse/storage/schema/main/delta/43/blocked_rooms.sql diff --git a/synapse/storage/databases/main/schema/delta/43/quarantine_media.sql b/synapse/storage/schema/main/delta/43/quarantine_media.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/43/quarantine_media.sql rename to synapse/storage/schema/main/delta/43/quarantine_media.sql diff --git a/synapse/storage/databases/main/schema/delta/43/url_cache.sql b/synapse/storage/schema/main/delta/43/url_cache.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/43/url_cache.sql rename to synapse/storage/schema/main/delta/43/url_cache.sql diff --git a/synapse/storage/databases/main/schema/delta/43/user_share.sql b/synapse/storage/schema/main/delta/43/user_share.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/43/user_share.sql rename to synapse/storage/schema/main/delta/43/user_share.sql diff --git a/synapse/storage/databases/main/schema/delta/44/expire_url_cache.sql b/synapse/storage/schema/main/delta/44/expire_url_cache.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/44/expire_url_cache.sql rename to synapse/storage/schema/main/delta/44/expire_url_cache.sql diff --git a/synapse/storage/databases/main/schema/delta/45/group_server.sql b/synapse/storage/schema/main/delta/45/group_server.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/45/group_server.sql rename to synapse/storage/schema/main/delta/45/group_server.sql diff --git a/synapse/storage/databases/main/schema/delta/45/profile_cache.sql b/synapse/storage/schema/main/delta/45/profile_cache.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/45/profile_cache.sql rename to synapse/storage/schema/main/delta/45/profile_cache.sql diff --git a/synapse/storage/databases/main/schema/delta/46/drop_refresh_tokens.sql b/synapse/storage/schema/main/delta/46/drop_refresh_tokens.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/46/drop_refresh_tokens.sql rename to synapse/storage/schema/main/delta/46/drop_refresh_tokens.sql diff --git a/synapse/storage/databases/main/schema/delta/46/drop_unique_deleted_pushers.sql b/synapse/storage/schema/main/delta/46/drop_unique_deleted_pushers.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/46/drop_unique_deleted_pushers.sql rename to synapse/storage/schema/main/delta/46/drop_unique_deleted_pushers.sql diff --git a/synapse/storage/databases/main/schema/delta/46/group_server.sql b/synapse/storage/schema/main/delta/46/group_server.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/46/group_server.sql rename to synapse/storage/schema/main/delta/46/group_server.sql diff --git a/synapse/storage/databases/main/schema/delta/46/local_media_repository_url_idx.sql b/synapse/storage/schema/main/delta/46/local_media_repository_url_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/46/local_media_repository_url_idx.sql rename to synapse/storage/schema/main/delta/46/local_media_repository_url_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/46/user_dir_null_room_ids.sql b/synapse/storage/schema/main/delta/46/user_dir_null_room_ids.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/46/user_dir_null_room_ids.sql rename to synapse/storage/schema/main/delta/46/user_dir_null_room_ids.sql diff --git a/synapse/storage/databases/main/schema/delta/46/user_dir_typos.sql b/synapse/storage/schema/main/delta/46/user_dir_typos.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/46/user_dir_typos.sql rename to synapse/storage/schema/main/delta/46/user_dir_typos.sql diff --git a/synapse/storage/databases/main/schema/delta/47/last_access_media.sql b/synapse/storage/schema/main/delta/47/last_access_media.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/47/last_access_media.sql rename to synapse/storage/schema/main/delta/47/last_access_media.sql diff --git a/synapse/storage/databases/main/schema/delta/47/postgres_fts_gin.sql b/synapse/storage/schema/main/delta/47/postgres_fts_gin.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/47/postgres_fts_gin.sql rename to synapse/storage/schema/main/delta/47/postgres_fts_gin.sql diff --git a/synapse/storage/databases/main/schema/delta/47/push_actions_staging.sql b/synapse/storage/schema/main/delta/47/push_actions_staging.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/47/push_actions_staging.sql rename to synapse/storage/schema/main/delta/47/push_actions_staging.sql diff --git a/synapse/storage/databases/main/schema/delta/48/add_user_consent.sql b/synapse/storage/schema/main/delta/48/add_user_consent.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/48/add_user_consent.sql rename to synapse/storage/schema/main/delta/48/add_user_consent.sql diff --git a/synapse/storage/databases/main/schema/delta/48/add_user_ips_last_seen_index.sql b/synapse/storage/schema/main/delta/48/add_user_ips_last_seen_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/48/add_user_ips_last_seen_index.sql rename to synapse/storage/schema/main/delta/48/add_user_ips_last_seen_index.sql diff --git a/synapse/storage/databases/main/schema/delta/48/deactivated_users.sql b/synapse/storage/schema/main/delta/48/deactivated_users.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/48/deactivated_users.sql rename to synapse/storage/schema/main/delta/48/deactivated_users.sql diff --git a/synapse/storage/databases/main/schema/delta/48/group_unique_indexes.py b/synapse/storage/schema/main/delta/48/group_unique_indexes.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/48/group_unique_indexes.py rename to synapse/storage/schema/main/delta/48/group_unique_indexes.py diff --git a/synapse/storage/databases/main/schema/delta/48/groups_joinable.sql b/synapse/storage/schema/main/delta/48/groups_joinable.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/48/groups_joinable.sql rename to synapse/storage/schema/main/delta/48/groups_joinable.sql diff --git a/synapse/storage/databases/main/schema/delta/49/add_user_consent_server_notice_sent.sql b/synapse/storage/schema/main/delta/49/add_user_consent_server_notice_sent.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/49/add_user_consent_server_notice_sent.sql rename to synapse/storage/schema/main/delta/49/add_user_consent_server_notice_sent.sql diff --git a/synapse/storage/databases/main/schema/delta/49/add_user_daily_visits.sql b/synapse/storage/schema/main/delta/49/add_user_daily_visits.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/49/add_user_daily_visits.sql rename to synapse/storage/schema/main/delta/49/add_user_daily_visits.sql diff --git a/synapse/storage/databases/main/schema/delta/49/add_user_ips_last_seen_only_index.sql b/synapse/storage/schema/main/delta/49/add_user_ips_last_seen_only_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/49/add_user_ips_last_seen_only_index.sql rename to synapse/storage/schema/main/delta/49/add_user_ips_last_seen_only_index.sql diff --git a/synapse/storage/databases/main/schema/delta/50/add_creation_ts_users_index.sql b/synapse/storage/schema/main/delta/50/add_creation_ts_users_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/50/add_creation_ts_users_index.sql rename to synapse/storage/schema/main/delta/50/add_creation_ts_users_index.sql diff --git a/synapse/storage/databases/main/schema/delta/50/erasure_store.sql b/synapse/storage/schema/main/delta/50/erasure_store.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/50/erasure_store.sql rename to synapse/storage/schema/main/delta/50/erasure_store.sql diff --git a/synapse/storage/databases/main/schema/delta/50/make_event_content_nullable.py b/synapse/storage/schema/main/delta/50/make_event_content_nullable.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/50/make_event_content_nullable.py rename to synapse/storage/schema/main/delta/50/make_event_content_nullable.py diff --git a/synapse/storage/databases/main/schema/delta/51/e2e_room_keys.sql b/synapse/storage/schema/main/delta/51/e2e_room_keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/51/e2e_room_keys.sql rename to synapse/storage/schema/main/delta/51/e2e_room_keys.sql diff --git a/synapse/storage/databases/main/schema/delta/51/monthly_active_users.sql b/synapse/storage/schema/main/delta/51/monthly_active_users.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/51/monthly_active_users.sql rename to synapse/storage/schema/main/delta/51/monthly_active_users.sql diff --git a/synapse/storage/databases/main/schema/delta/52/add_event_to_state_group_index.sql b/synapse/storage/schema/main/delta/52/add_event_to_state_group_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/52/add_event_to_state_group_index.sql rename to synapse/storage/schema/main/delta/52/add_event_to_state_group_index.sql diff --git a/synapse/storage/databases/main/schema/delta/52/device_list_streams_unique_idx.sql b/synapse/storage/schema/main/delta/52/device_list_streams_unique_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/52/device_list_streams_unique_idx.sql rename to synapse/storage/schema/main/delta/52/device_list_streams_unique_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/52/e2e_room_keys.sql b/synapse/storage/schema/main/delta/52/e2e_room_keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/52/e2e_room_keys.sql rename to synapse/storage/schema/main/delta/52/e2e_room_keys.sql diff --git a/synapse/storage/databases/main/schema/delta/53/add_user_type_to_users.sql b/synapse/storage/schema/main/delta/53/add_user_type_to_users.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/add_user_type_to_users.sql rename to synapse/storage/schema/main/delta/53/add_user_type_to_users.sql diff --git a/synapse/storage/databases/main/schema/delta/53/drop_sent_transactions.sql b/synapse/storage/schema/main/delta/53/drop_sent_transactions.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/drop_sent_transactions.sql rename to synapse/storage/schema/main/delta/53/drop_sent_transactions.sql diff --git a/synapse/storage/databases/main/schema/delta/53/event_format_version.sql b/synapse/storage/schema/main/delta/53/event_format_version.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/event_format_version.sql rename to synapse/storage/schema/main/delta/53/event_format_version.sql diff --git a/synapse/storage/databases/main/schema/delta/53/user_dir_populate.sql b/synapse/storage/schema/main/delta/53/user_dir_populate.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/user_dir_populate.sql rename to synapse/storage/schema/main/delta/53/user_dir_populate.sql diff --git a/synapse/storage/databases/main/schema/delta/53/user_ips_index.sql b/synapse/storage/schema/main/delta/53/user_ips_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/user_ips_index.sql rename to synapse/storage/schema/main/delta/53/user_ips_index.sql diff --git a/synapse/storage/databases/main/schema/delta/53/user_share.sql b/synapse/storage/schema/main/delta/53/user_share.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/user_share.sql rename to synapse/storage/schema/main/delta/53/user_share.sql diff --git a/synapse/storage/databases/main/schema/delta/53/user_threepid_id.sql b/synapse/storage/schema/main/delta/53/user_threepid_id.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/user_threepid_id.sql rename to synapse/storage/schema/main/delta/53/user_threepid_id.sql diff --git a/synapse/storage/databases/main/schema/delta/53/users_in_public_rooms.sql b/synapse/storage/schema/main/delta/53/users_in_public_rooms.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/53/users_in_public_rooms.sql rename to synapse/storage/schema/main/delta/53/users_in_public_rooms.sql diff --git a/synapse/storage/databases/main/schema/delta/54/account_validity_with_renewal.sql b/synapse/storage/schema/main/delta/54/account_validity_with_renewal.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/account_validity_with_renewal.sql rename to synapse/storage/schema/main/delta/54/account_validity_with_renewal.sql diff --git a/synapse/storage/databases/main/schema/delta/54/add_validity_to_server_keys.sql b/synapse/storage/schema/main/delta/54/add_validity_to_server_keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/add_validity_to_server_keys.sql rename to synapse/storage/schema/main/delta/54/add_validity_to_server_keys.sql diff --git a/synapse/storage/databases/main/schema/delta/54/delete_forward_extremities.sql b/synapse/storage/schema/main/delta/54/delete_forward_extremities.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/delete_forward_extremities.sql rename to synapse/storage/schema/main/delta/54/delete_forward_extremities.sql diff --git a/synapse/storage/databases/main/schema/delta/54/drop_legacy_tables.sql b/synapse/storage/schema/main/delta/54/drop_legacy_tables.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/drop_legacy_tables.sql rename to synapse/storage/schema/main/delta/54/drop_legacy_tables.sql diff --git a/synapse/storage/databases/main/schema/delta/54/drop_presence_list.sql b/synapse/storage/schema/main/delta/54/drop_presence_list.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/drop_presence_list.sql rename to synapse/storage/schema/main/delta/54/drop_presence_list.sql diff --git a/synapse/storage/databases/main/schema/delta/54/relations.sql b/synapse/storage/schema/main/delta/54/relations.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/relations.sql rename to synapse/storage/schema/main/delta/54/relations.sql diff --git a/synapse/storage/databases/main/schema/delta/54/stats.sql b/synapse/storage/schema/main/delta/54/stats.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/stats.sql rename to synapse/storage/schema/main/delta/54/stats.sql diff --git a/synapse/storage/databases/main/schema/delta/54/stats2.sql b/synapse/storage/schema/main/delta/54/stats2.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/54/stats2.sql rename to synapse/storage/schema/main/delta/54/stats2.sql diff --git a/synapse/storage/databases/main/schema/delta/55/access_token_expiry.sql b/synapse/storage/schema/main/delta/55/access_token_expiry.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/55/access_token_expiry.sql rename to synapse/storage/schema/main/delta/55/access_token_expiry.sql diff --git a/synapse/storage/databases/main/schema/delta/55/track_threepid_validations.sql b/synapse/storage/schema/main/delta/55/track_threepid_validations.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/55/track_threepid_validations.sql rename to synapse/storage/schema/main/delta/55/track_threepid_validations.sql diff --git a/synapse/storage/databases/main/schema/delta/55/users_alter_deactivated.sql b/synapse/storage/schema/main/delta/55/users_alter_deactivated.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/55/users_alter_deactivated.sql rename to synapse/storage/schema/main/delta/55/users_alter_deactivated.sql diff --git a/synapse/storage/databases/main/schema/delta/56/add_spans_to_device_lists.sql b/synapse/storage/schema/main/delta/56/add_spans_to_device_lists.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/add_spans_to_device_lists.sql rename to synapse/storage/schema/main/delta/56/add_spans_to_device_lists.sql diff --git a/synapse/storage/databases/main/schema/delta/56/current_state_events_membership.sql b/synapse/storage/schema/main/delta/56/current_state_events_membership.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/current_state_events_membership.sql rename to synapse/storage/schema/main/delta/56/current_state_events_membership.sql diff --git a/synapse/storage/databases/main/schema/delta/56/current_state_events_membership_mk2.sql b/synapse/storage/schema/main/delta/56/current_state_events_membership_mk2.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/current_state_events_membership_mk2.sql rename to synapse/storage/schema/main/delta/56/current_state_events_membership_mk2.sql diff --git a/synapse/storage/databases/main/schema/delta/56/delete_keys_from_deleted_backups.sql b/synapse/storage/schema/main/delta/56/delete_keys_from_deleted_backups.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/delete_keys_from_deleted_backups.sql rename to synapse/storage/schema/main/delta/56/delete_keys_from_deleted_backups.sql diff --git a/synapse/storage/databases/main/schema/delta/56/destinations_failure_ts.sql b/synapse/storage/schema/main/delta/56/destinations_failure_ts.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/destinations_failure_ts.sql rename to synapse/storage/schema/main/delta/56/destinations_failure_ts.sql diff --git a/synapse/storage/databases/main/schema/delta/56/destinations_retry_interval_type.sql.postgres b/synapse/storage/schema/main/delta/56/destinations_retry_interval_type.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/destinations_retry_interval_type.sql.postgres rename to synapse/storage/schema/main/delta/56/destinations_retry_interval_type.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/56/device_stream_id_insert.sql b/synapse/storage/schema/main/delta/56/device_stream_id_insert.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/device_stream_id_insert.sql rename to synapse/storage/schema/main/delta/56/device_stream_id_insert.sql diff --git a/synapse/storage/databases/main/schema/delta/56/devices_last_seen.sql b/synapse/storage/schema/main/delta/56/devices_last_seen.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/devices_last_seen.sql rename to synapse/storage/schema/main/delta/56/devices_last_seen.sql diff --git a/synapse/storage/databases/main/schema/delta/56/drop_unused_event_tables.sql b/synapse/storage/schema/main/delta/56/drop_unused_event_tables.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/drop_unused_event_tables.sql rename to synapse/storage/schema/main/delta/56/drop_unused_event_tables.sql diff --git a/synapse/storage/databases/main/schema/delta/56/event_expiry.sql b/synapse/storage/schema/main/delta/56/event_expiry.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/event_expiry.sql rename to synapse/storage/schema/main/delta/56/event_expiry.sql diff --git a/synapse/storage/databases/main/schema/delta/56/event_labels.sql b/synapse/storage/schema/main/delta/56/event_labels.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/event_labels.sql rename to synapse/storage/schema/main/delta/56/event_labels.sql diff --git a/synapse/storage/databases/main/schema/delta/56/event_labels_background_update.sql b/synapse/storage/schema/main/delta/56/event_labels_background_update.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/event_labels_background_update.sql rename to synapse/storage/schema/main/delta/56/event_labels_background_update.sql diff --git a/synapse/storage/databases/main/schema/delta/56/fix_room_keys_index.sql b/synapse/storage/schema/main/delta/56/fix_room_keys_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/fix_room_keys_index.sql rename to synapse/storage/schema/main/delta/56/fix_room_keys_index.sql diff --git a/synapse/storage/databases/main/schema/delta/56/hidden_devices.sql b/synapse/storage/schema/main/delta/56/hidden_devices.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/hidden_devices.sql rename to synapse/storage/schema/main/delta/56/hidden_devices.sql diff --git a/synapse/storage/databases/main/schema/delta/56/hidden_devices_fix.sql.sqlite b/synapse/storage/schema/main/delta/56/hidden_devices_fix.sql.sqlite similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/hidden_devices_fix.sql.sqlite rename to synapse/storage/schema/main/delta/56/hidden_devices_fix.sql.sqlite diff --git a/synapse/storage/databases/main/schema/delta/56/nuke_empty_communities_from_db.sql b/synapse/storage/schema/main/delta/56/nuke_empty_communities_from_db.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/nuke_empty_communities_from_db.sql rename to synapse/storage/schema/main/delta/56/nuke_empty_communities_from_db.sql diff --git a/synapse/storage/databases/main/schema/delta/56/public_room_list_idx.sql b/synapse/storage/schema/main/delta/56/public_room_list_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/public_room_list_idx.sql rename to synapse/storage/schema/main/delta/56/public_room_list_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/56/redaction_censor.sql b/synapse/storage/schema/main/delta/56/redaction_censor.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/redaction_censor.sql rename to synapse/storage/schema/main/delta/56/redaction_censor.sql diff --git a/synapse/storage/databases/main/schema/delta/56/redaction_censor2.sql b/synapse/storage/schema/main/delta/56/redaction_censor2.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/redaction_censor2.sql rename to synapse/storage/schema/main/delta/56/redaction_censor2.sql diff --git a/synapse/storage/databases/main/schema/delta/56/redaction_censor3_fix_update.sql.postgres b/synapse/storage/schema/main/delta/56/redaction_censor3_fix_update.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/redaction_censor3_fix_update.sql.postgres rename to synapse/storage/schema/main/delta/56/redaction_censor3_fix_update.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/56/redaction_censor4.sql b/synapse/storage/schema/main/delta/56/redaction_censor4.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/redaction_censor4.sql rename to synapse/storage/schema/main/delta/56/redaction_censor4.sql diff --git a/synapse/storage/databases/main/schema/delta/56/remove_tombstoned_rooms_from_directory.sql b/synapse/storage/schema/main/delta/56/remove_tombstoned_rooms_from_directory.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/remove_tombstoned_rooms_from_directory.sql rename to synapse/storage/schema/main/delta/56/remove_tombstoned_rooms_from_directory.sql diff --git a/synapse/storage/databases/main/schema/delta/56/room_key_etag.sql b/synapse/storage/schema/main/delta/56/room_key_etag.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/room_key_etag.sql rename to synapse/storage/schema/main/delta/56/room_key_etag.sql diff --git a/synapse/storage/databases/main/schema/delta/56/room_membership_idx.sql b/synapse/storage/schema/main/delta/56/room_membership_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/room_membership_idx.sql rename to synapse/storage/schema/main/delta/56/room_membership_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/56/room_retention.sql b/synapse/storage/schema/main/delta/56/room_retention.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/room_retention.sql rename to synapse/storage/schema/main/delta/56/room_retention.sql diff --git a/synapse/storage/databases/main/schema/delta/56/signing_keys.sql b/synapse/storage/schema/main/delta/56/signing_keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/signing_keys.sql rename to synapse/storage/schema/main/delta/56/signing_keys.sql diff --git a/synapse/storage/databases/main/schema/delta/56/signing_keys_nonunique_signatures.sql b/synapse/storage/schema/main/delta/56/signing_keys_nonunique_signatures.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/signing_keys_nonunique_signatures.sql rename to synapse/storage/schema/main/delta/56/signing_keys_nonunique_signatures.sql diff --git a/synapse/storage/databases/main/schema/delta/56/stats_separated.sql b/synapse/storage/schema/main/delta/56/stats_separated.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/stats_separated.sql rename to synapse/storage/schema/main/delta/56/stats_separated.sql diff --git a/synapse/storage/databases/main/schema/delta/56/unique_user_filter_index.py b/synapse/storage/schema/main/delta/56/unique_user_filter_index.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/unique_user_filter_index.py rename to synapse/storage/schema/main/delta/56/unique_user_filter_index.py diff --git a/synapse/storage/databases/main/schema/delta/56/user_external_ids.sql b/synapse/storage/schema/main/delta/56/user_external_ids.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/user_external_ids.sql rename to synapse/storage/schema/main/delta/56/user_external_ids.sql diff --git a/synapse/storage/databases/main/schema/delta/56/users_in_public_rooms_idx.sql b/synapse/storage/schema/main/delta/56/users_in_public_rooms_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/56/users_in_public_rooms_idx.sql rename to synapse/storage/schema/main/delta/56/users_in_public_rooms_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/57/delete_old_current_state_events.sql b/synapse/storage/schema/main/delta/57/delete_old_current_state_events.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/delete_old_current_state_events.sql rename to synapse/storage/schema/main/delta/57/delete_old_current_state_events.sql diff --git a/synapse/storage/databases/main/schema/delta/57/device_list_remote_cache_stale.sql b/synapse/storage/schema/main/delta/57/device_list_remote_cache_stale.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/device_list_remote_cache_stale.sql rename to synapse/storage/schema/main/delta/57/device_list_remote_cache_stale.sql diff --git a/synapse/storage/databases/main/schema/delta/57/local_current_membership.py b/synapse/storage/schema/main/delta/57/local_current_membership.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/local_current_membership.py rename to synapse/storage/schema/main/delta/57/local_current_membership.py diff --git a/synapse/storage/databases/main/schema/delta/57/remove_sent_outbound_pokes.sql b/synapse/storage/schema/main/delta/57/remove_sent_outbound_pokes.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/remove_sent_outbound_pokes.sql rename to synapse/storage/schema/main/delta/57/remove_sent_outbound_pokes.sql diff --git a/synapse/storage/databases/main/schema/delta/57/rooms_version_column.sql b/synapse/storage/schema/main/delta/57/rooms_version_column.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/rooms_version_column.sql rename to synapse/storage/schema/main/delta/57/rooms_version_column.sql diff --git a/synapse/storage/databases/main/schema/delta/57/rooms_version_column_2.sql.postgres b/synapse/storage/schema/main/delta/57/rooms_version_column_2.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/rooms_version_column_2.sql.postgres rename to synapse/storage/schema/main/delta/57/rooms_version_column_2.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/57/rooms_version_column_2.sql.sqlite b/synapse/storage/schema/main/delta/57/rooms_version_column_2.sql.sqlite similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/rooms_version_column_2.sql.sqlite rename to synapse/storage/schema/main/delta/57/rooms_version_column_2.sql.sqlite diff --git a/synapse/storage/databases/main/schema/delta/57/rooms_version_column_3.sql.postgres b/synapse/storage/schema/main/delta/57/rooms_version_column_3.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/rooms_version_column_3.sql.postgres rename to synapse/storage/schema/main/delta/57/rooms_version_column_3.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/57/rooms_version_column_3.sql.sqlite b/synapse/storage/schema/main/delta/57/rooms_version_column_3.sql.sqlite similarity index 100% rename from synapse/storage/databases/main/schema/delta/57/rooms_version_column_3.sql.sqlite rename to synapse/storage/schema/main/delta/57/rooms_version_column_3.sql.sqlite diff --git a/synapse/storage/databases/main/schema/delta/58/02remove_dup_outbound_pokes.sql b/synapse/storage/schema/main/delta/58/02remove_dup_outbound_pokes.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/02remove_dup_outbound_pokes.sql rename to synapse/storage/schema/main/delta/58/02remove_dup_outbound_pokes.sql diff --git a/synapse/storage/databases/main/schema/delta/58/03persist_ui_auth.sql b/synapse/storage/schema/main/delta/58/03persist_ui_auth.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/03persist_ui_auth.sql rename to synapse/storage/schema/main/delta/58/03persist_ui_auth.sql diff --git a/synapse/storage/databases/main/schema/delta/58/05cache_instance.sql.postgres b/synapse/storage/schema/main/delta/58/05cache_instance.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/05cache_instance.sql.postgres rename to synapse/storage/schema/main/delta/58/05cache_instance.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/58/06dlols_unique_idx.py b/synapse/storage/schema/main/delta/58/06dlols_unique_idx.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/06dlols_unique_idx.py rename to synapse/storage/schema/main/delta/58/06dlols_unique_idx.py diff --git a/synapse/storage/databases/main/schema/delta/58/07add_method_to_thumbnail_constraint.sql.postgres b/synapse/storage/schema/main/delta/58/07add_method_to_thumbnail_constraint.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/07add_method_to_thumbnail_constraint.sql.postgres rename to synapse/storage/schema/main/delta/58/07add_method_to_thumbnail_constraint.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/58/07add_method_to_thumbnail_constraint.sql.sqlite b/synapse/storage/schema/main/delta/58/07add_method_to_thumbnail_constraint.sql.sqlite similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/07add_method_to_thumbnail_constraint.sql.sqlite rename to synapse/storage/schema/main/delta/58/07add_method_to_thumbnail_constraint.sql.sqlite diff --git a/synapse/storage/databases/main/schema/delta/58/07persist_ui_auth_ips.sql b/synapse/storage/schema/main/delta/58/07persist_ui_auth_ips.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/07persist_ui_auth_ips.sql rename to synapse/storage/schema/main/delta/58/07persist_ui_auth_ips.sql diff --git a/synapse/storage/databases/main/schema/delta/58/08_media_safe_from_quarantine.sql.postgres b/synapse/storage/schema/main/delta/58/08_media_safe_from_quarantine.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/08_media_safe_from_quarantine.sql.postgres rename to synapse/storage/schema/main/delta/58/08_media_safe_from_quarantine.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/58/08_media_safe_from_quarantine.sql.sqlite b/synapse/storage/schema/main/delta/58/08_media_safe_from_quarantine.sql.sqlite similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/08_media_safe_from_quarantine.sql.sqlite rename to synapse/storage/schema/main/delta/58/08_media_safe_from_quarantine.sql.sqlite diff --git a/synapse/storage/databases/main/schema/delta/58/09shadow_ban.sql b/synapse/storage/schema/main/delta/58/09shadow_ban.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/09shadow_ban.sql rename to synapse/storage/schema/main/delta/58/09shadow_ban.sql diff --git a/synapse/storage/databases/main/schema/delta/58/10_pushrules_enabled_delete_obsolete.sql b/synapse/storage/schema/main/delta/58/10_pushrules_enabled_delete_obsolete.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/10_pushrules_enabled_delete_obsolete.sql rename to synapse/storage/schema/main/delta/58/10_pushrules_enabled_delete_obsolete.sql diff --git a/synapse/storage/databases/main/schema/delta/58/10drop_local_rejections_stream.sql b/synapse/storage/schema/main/delta/58/10drop_local_rejections_stream.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/10drop_local_rejections_stream.sql rename to synapse/storage/schema/main/delta/58/10drop_local_rejections_stream.sql diff --git a/synapse/storage/databases/main/schema/delta/58/10federation_pos_instance_name.sql b/synapse/storage/schema/main/delta/58/10federation_pos_instance_name.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/10federation_pos_instance_name.sql rename to synapse/storage/schema/main/delta/58/10federation_pos_instance_name.sql diff --git a/synapse/storage/databases/main/schema/delta/58/11dehydration.sql b/synapse/storage/schema/main/delta/58/11dehydration.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/11dehydration.sql rename to synapse/storage/schema/main/delta/58/11dehydration.sql diff --git a/synapse/storage/databases/main/schema/delta/58/11fallback.sql b/synapse/storage/schema/main/delta/58/11fallback.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/11fallback.sql rename to synapse/storage/schema/main/delta/58/11fallback.sql diff --git a/synapse/storage/databases/main/schema/delta/58/11user_id_seq.py b/synapse/storage/schema/main/delta/58/11user_id_seq.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/11user_id_seq.py rename to synapse/storage/schema/main/delta/58/11user_id_seq.py diff --git a/synapse/storage/databases/main/schema/delta/58/12room_stats.sql b/synapse/storage/schema/main/delta/58/12room_stats.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/12room_stats.sql rename to synapse/storage/schema/main/delta/58/12room_stats.sql diff --git a/synapse/storage/databases/main/schema/delta/58/13remove_presence_allow_inbound.sql b/synapse/storage/schema/main/delta/58/13remove_presence_allow_inbound.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/13remove_presence_allow_inbound.sql rename to synapse/storage/schema/main/delta/58/13remove_presence_allow_inbound.sql diff --git a/synapse/storage/databases/main/schema/delta/58/14events_instance_name.sql b/synapse/storage/schema/main/delta/58/14events_instance_name.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/14events_instance_name.sql rename to synapse/storage/schema/main/delta/58/14events_instance_name.sql diff --git a/synapse/storage/databases/main/schema/delta/58/14events_instance_name.sql.postgres b/synapse/storage/schema/main/delta/58/14events_instance_name.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/14events_instance_name.sql.postgres rename to synapse/storage/schema/main/delta/58/14events_instance_name.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/58/15_catchup_destination_rooms.sql b/synapse/storage/schema/main/delta/58/15_catchup_destination_rooms.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/15_catchup_destination_rooms.sql rename to synapse/storage/schema/main/delta/58/15_catchup_destination_rooms.sql diff --git a/synapse/storage/databases/main/schema/delta/58/15unread_count.sql b/synapse/storage/schema/main/delta/58/15unread_count.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/15unread_count.sql rename to synapse/storage/schema/main/delta/58/15unread_count.sql diff --git a/synapse/storage/databases/main/schema/delta/58/16populate_stats_process_rooms_fix.sql b/synapse/storage/schema/main/delta/58/16populate_stats_process_rooms_fix.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/16populate_stats_process_rooms_fix.sql rename to synapse/storage/schema/main/delta/58/16populate_stats_process_rooms_fix.sql diff --git a/synapse/storage/databases/main/schema/delta/58/17_catchup_last_successful.sql b/synapse/storage/schema/main/delta/58/17_catchup_last_successful.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/17_catchup_last_successful.sql rename to synapse/storage/schema/main/delta/58/17_catchup_last_successful.sql diff --git a/synapse/storage/databases/main/schema/delta/58/18stream_positions.sql b/synapse/storage/schema/main/delta/58/18stream_positions.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/18stream_positions.sql rename to synapse/storage/schema/main/delta/58/18stream_positions.sql diff --git a/synapse/storage/databases/main/schema/delta/58/19instance_map.sql.postgres b/synapse/storage/schema/main/delta/58/19instance_map.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/19instance_map.sql.postgres rename to synapse/storage/schema/main/delta/58/19instance_map.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/58/19txn_id.sql b/synapse/storage/schema/main/delta/58/19txn_id.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/19txn_id.sql rename to synapse/storage/schema/main/delta/58/19txn_id.sql diff --git a/synapse/storage/databases/main/schema/delta/58/20instance_name_event_tables.sql b/synapse/storage/schema/main/delta/58/20instance_name_event_tables.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/20instance_name_event_tables.sql rename to synapse/storage/schema/main/delta/58/20instance_name_event_tables.sql diff --git a/synapse/storage/databases/main/schema/delta/58/20user_daily_visits.sql b/synapse/storage/schema/main/delta/58/20user_daily_visits.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/20user_daily_visits.sql rename to synapse/storage/schema/main/delta/58/20user_daily_visits.sql diff --git a/synapse/storage/databases/main/schema/delta/58/21as_device_stream.sql b/synapse/storage/schema/main/delta/58/21as_device_stream.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/21as_device_stream.sql rename to synapse/storage/schema/main/delta/58/21as_device_stream.sql diff --git a/synapse/storage/databases/main/schema/delta/58/21drop_device_max_stream_id.sql b/synapse/storage/schema/main/delta/58/21drop_device_max_stream_id.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/21drop_device_max_stream_id.sql rename to synapse/storage/schema/main/delta/58/21drop_device_max_stream_id.sql diff --git a/synapse/storage/databases/main/schema/delta/58/22puppet_token.sql b/synapse/storage/schema/main/delta/58/22puppet_token.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/22puppet_token.sql rename to synapse/storage/schema/main/delta/58/22puppet_token.sql diff --git a/synapse/storage/databases/main/schema/delta/58/22users_have_local_media.sql b/synapse/storage/schema/main/delta/58/22users_have_local_media.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/22users_have_local_media.sql rename to synapse/storage/schema/main/delta/58/22users_have_local_media.sql diff --git a/synapse/storage/databases/main/schema/delta/58/23e2e_cross_signing_keys_idx.sql b/synapse/storage/schema/main/delta/58/23e2e_cross_signing_keys_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/23e2e_cross_signing_keys_idx.sql rename to synapse/storage/schema/main/delta/58/23e2e_cross_signing_keys_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/58/24drop_event_json_index.sql b/synapse/storage/schema/main/delta/58/24drop_event_json_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/24drop_event_json_index.sql rename to synapse/storage/schema/main/delta/58/24drop_event_json_index.sql diff --git a/synapse/storage/databases/main/schema/delta/58/25user_external_ids_user_id_idx.sql b/synapse/storage/schema/main/delta/58/25user_external_ids_user_id_idx.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/25user_external_ids_user_id_idx.sql rename to synapse/storage/schema/main/delta/58/25user_external_ids_user_id_idx.sql diff --git a/synapse/storage/databases/main/schema/delta/58/26access_token_last_validated.sql b/synapse/storage/schema/main/delta/58/26access_token_last_validated.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/26access_token_last_validated.sql rename to synapse/storage/schema/main/delta/58/26access_token_last_validated.sql diff --git a/synapse/storage/databases/main/schema/delta/58/27local_invites.sql b/synapse/storage/schema/main/delta/58/27local_invites.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/27local_invites.sql rename to synapse/storage/schema/main/delta/58/27local_invites.sql diff --git a/synapse/storage/databases/main/schema/delta/58/28drop_last_used_column.sql.postgres b/synapse/storage/schema/main/delta/58/28drop_last_used_column.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/28drop_last_used_column.sql.postgres rename to synapse/storage/schema/main/delta/58/28drop_last_used_column.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/58/28drop_last_used_column.sql.sqlite b/synapse/storage/schema/main/delta/58/28drop_last_used_column.sql.sqlite similarity index 100% rename from synapse/storage/databases/main/schema/delta/58/28drop_last_used_column.sql.sqlite rename to synapse/storage/schema/main/delta/58/28drop_last_used_column.sql.sqlite diff --git a/synapse/storage/databases/main/schema/delta/59/01ignored_user.py b/synapse/storage/schema/main/delta/59/01ignored_user.py similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/01ignored_user.py rename to synapse/storage/schema/main/delta/59/01ignored_user.py diff --git a/synapse/storage/databases/main/schema/delta/59/02shard_send_to_device.sql b/synapse/storage/schema/main/delta/59/02shard_send_to_device.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/02shard_send_to_device.sql rename to synapse/storage/schema/main/delta/59/02shard_send_to_device.sql diff --git a/synapse/storage/databases/main/schema/delta/59/03shard_send_to_device_sequence.sql.postgres b/synapse/storage/schema/main/delta/59/03shard_send_to_device_sequence.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/03shard_send_to_device_sequence.sql.postgres rename to synapse/storage/schema/main/delta/59/03shard_send_to_device_sequence.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/59/04_event_auth_chains.sql b/synapse/storage/schema/main/delta/59/04_event_auth_chains.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/04_event_auth_chains.sql rename to synapse/storage/schema/main/delta/59/04_event_auth_chains.sql diff --git a/synapse/storage/databases/main/schema/delta/59/04_event_auth_chains.sql.postgres b/synapse/storage/schema/main/delta/59/04_event_auth_chains.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/04_event_auth_chains.sql.postgres rename to synapse/storage/schema/main/delta/59/04_event_auth_chains.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/59/04drop_account_data.sql b/synapse/storage/schema/main/delta/59/04drop_account_data.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/04drop_account_data.sql rename to synapse/storage/schema/main/delta/59/04drop_account_data.sql diff --git a/synapse/storage/databases/main/schema/delta/59/05cache_invalidation.sql b/synapse/storage/schema/main/delta/59/05cache_invalidation.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/05cache_invalidation.sql rename to synapse/storage/schema/main/delta/59/05cache_invalidation.sql diff --git a/synapse/storage/databases/main/schema/delta/59/06chain_cover_index.sql b/synapse/storage/schema/main/delta/59/06chain_cover_index.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/06chain_cover_index.sql rename to synapse/storage/schema/main/delta/59/06chain_cover_index.sql diff --git a/synapse/storage/databases/main/schema/delta/59/06shard_account_data.sql b/synapse/storage/schema/main/delta/59/06shard_account_data.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/06shard_account_data.sql rename to synapse/storage/schema/main/delta/59/06shard_account_data.sql diff --git a/synapse/storage/databases/main/schema/delta/59/06shard_account_data.sql.postgres b/synapse/storage/schema/main/delta/59/06shard_account_data.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/06shard_account_data.sql.postgres rename to synapse/storage/schema/main/delta/59/06shard_account_data.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/59/07shard_account_data_fix.sql b/synapse/storage/schema/main/delta/59/07shard_account_data_fix.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/07shard_account_data_fix.sql rename to synapse/storage/schema/main/delta/59/07shard_account_data_fix.sql diff --git a/synapse/storage/databases/main/schema/delta/59/08delete_pushers_for_deactivated_accounts.sql b/synapse/storage/schema/main/delta/59/08delete_pushers_for_deactivated_accounts.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/08delete_pushers_for_deactivated_accounts.sql rename to synapse/storage/schema/main/delta/59/08delete_pushers_for_deactivated_accounts.sql diff --git a/synapse/storage/databases/main/schema/delta/59/08delete_stale_pushers.sql b/synapse/storage/schema/main/delta/59/08delete_stale_pushers.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/08delete_stale_pushers.sql rename to synapse/storage/schema/main/delta/59/08delete_stale_pushers.sql diff --git a/synapse/storage/databases/main/schema/delta/59/09rejected_events_metadata.sql b/synapse/storage/schema/main/delta/59/09rejected_events_metadata.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/09rejected_events_metadata.sql rename to synapse/storage/schema/main/delta/59/09rejected_events_metadata.sql diff --git a/synapse/storage/databases/main/schema/delta/59/10delete_purged_chain_cover.sql b/synapse/storage/schema/main/delta/59/10delete_purged_chain_cover.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/10delete_purged_chain_cover.sql rename to synapse/storage/schema/main/delta/59/10delete_purged_chain_cover.sql diff --git a/synapse/storage/databases/main/schema/delta/59/11drop_thumbnail_constraint.sql.postgres b/synapse/storage/schema/main/delta/59/11drop_thumbnail_constraint.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/11drop_thumbnail_constraint.sql.postgres rename to synapse/storage/schema/main/delta/59/11drop_thumbnail_constraint.sql.postgres diff --git a/synapse/storage/databases/main/schema/delta/59/12account_validity_token_used_ts_ms.sql b/synapse/storage/schema/main/delta/59/12account_validity_token_used_ts_ms.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/12account_validity_token_used_ts_ms.sql rename to synapse/storage/schema/main/delta/59/12account_validity_token_used_ts_ms.sql diff --git a/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance.sql b/synapse/storage/schema/main/delta/59/12presence_stream_instance.sql similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/12presence_stream_instance.sql rename to synapse/storage/schema/main/delta/59/12presence_stream_instance.sql diff --git a/synapse/storage/databases/main/schema/delta/59/12presence_stream_instance_seq.sql.postgres b/synapse/storage/schema/main/delta/59/12presence_stream_instance_seq.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/delta/59/12presence_stream_instance_seq.sql.postgres rename to synapse/storage/schema/main/delta/59/12presence_stream_instance_seq.sql.postgres diff --git a/synapse/storage/databases/main/schema/full_schemas/16/application_services.sql b/synapse/storage/schema/main/full_schemas/16/application_services.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/application_services.sql rename to synapse/storage/schema/main/full_schemas/16/application_services.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/event_edges.sql b/synapse/storage/schema/main/full_schemas/16/event_edges.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/event_edges.sql rename to synapse/storage/schema/main/full_schemas/16/event_edges.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/event_signatures.sql b/synapse/storage/schema/main/full_schemas/16/event_signatures.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/event_signatures.sql rename to synapse/storage/schema/main/full_schemas/16/event_signatures.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/im.sql b/synapse/storage/schema/main/full_schemas/16/im.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/im.sql rename to synapse/storage/schema/main/full_schemas/16/im.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/keys.sql b/synapse/storage/schema/main/full_schemas/16/keys.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/keys.sql rename to synapse/storage/schema/main/full_schemas/16/keys.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/media_repository.sql b/synapse/storage/schema/main/full_schemas/16/media_repository.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/media_repository.sql rename to synapse/storage/schema/main/full_schemas/16/media_repository.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/presence.sql b/synapse/storage/schema/main/full_schemas/16/presence.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/presence.sql rename to synapse/storage/schema/main/full_schemas/16/presence.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/profiles.sql b/synapse/storage/schema/main/full_schemas/16/profiles.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/profiles.sql rename to synapse/storage/schema/main/full_schemas/16/profiles.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/push.sql b/synapse/storage/schema/main/full_schemas/16/push.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/push.sql rename to synapse/storage/schema/main/full_schemas/16/push.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/redactions.sql b/synapse/storage/schema/main/full_schemas/16/redactions.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/redactions.sql rename to synapse/storage/schema/main/full_schemas/16/redactions.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/room_aliases.sql b/synapse/storage/schema/main/full_schemas/16/room_aliases.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/room_aliases.sql rename to synapse/storage/schema/main/full_schemas/16/room_aliases.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/state.sql b/synapse/storage/schema/main/full_schemas/16/state.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/state.sql rename to synapse/storage/schema/main/full_schemas/16/state.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/transactions.sql b/synapse/storage/schema/main/full_schemas/16/transactions.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/transactions.sql rename to synapse/storage/schema/main/full_schemas/16/transactions.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/16/users.sql b/synapse/storage/schema/main/full_schemas/16/users.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/16/users.sql rename to synapse/storage/schema/main/full_schemas/16/users.sql diff --git a/synapse/storage/databases/main/schema/full_schemas/54/full.sql.postgres b/synapse/storage/schema/main/full_schemas/54/full.sql.postgres similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/54/full.sql.postgres rename to synapse/storage/schema/main/full_schemas/54/full.sql.postgres diff --git a/synapse/storage/databases/main/schema/full_schemas/54/full.sql.sqlite b/synapse/storage/schema/main/full_schemas/54/full.sql.sqlite similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/54/full.sql.sqlite rename to synapse/storage/schema/main/full_schemas/54/full.sql.sqlite diff --git a/synapse/storage/databases/main/schema/full_schemas/54/stream_positions.sql b/synapse/storage/schema/main/full_schemas/54/stream_positions.sql similarity index 100% rename from synapse/storage/databases/main/schema/full_schemas/54/stream_positions.sql rename to synapse/storage/schema/main/full_schemas/54/stream_positions.sql diff --git a/synapse/storage/databases/state/schema/delta/23/drop_state_index.sql b/synapse/storage/schema/state/delta/23/drop_state_index.sql similarity index 100% rename from synapse/storage/databases/state/schema/delta/23/drop_state_index.sql rename to synapse/storage/schema/state/delta/23/drop_state_index.sql diff --git a/synapse/storage/databases/state/schema/delta/30/state_stream.sql b/synapse/storage/schema/state/delta/30/state_stream.sql similarity index 100% rename from synapse/storage/databases/state/schema/delta/30/state_stream.sql rename to synapse/storage/schema/state/delta/30/state_stream.sql diff --git a/synapse/storage/databases/state/schema/delta/32/remove_state_indices.sql b/synapse/storage/schema/state/delta/32/remove_state_indices.sql similarity index 100% rename from synapse/storage/databases/state/schema/delta/32/remove_state_indices.sql rename to synapse/storage/schema/state/delta/32/remove_state_indices.sql diff --git a/synapse/storage/databases/state/schema/delta/35/add_state_index.sql b/synapse/storage/schema/state/delta/35/add_state_index.sql similarity index 100% rename from synapse/storage/databases/state/schema/delta/35/add_state_index.sql rename to synapse/storage/schema/state/delta/35/add_state_index.sql diff --git a/synapse/storage/databases/state/schema/delta/35/state.sql b/synapse/storage/schema/state/delta/35/state.sql similarity index 100% rename from synapse/storage/databases/state/schema/delta/35/state.sql rename to synapse/storage/schema/state/delta/35/state.sql diff --git a/synapse/storage/databases/state/schema/delta/35/state_dedupe.sql b/synapse/storage/schema/state/delta/35/state_dedupe.sql similarity index 100% rename from synapse/storage/databases/state/schema/delta/35/state_dedupe.sql rename to synapse/storage/schema/state/delta/35/state_dedupe.sql diff --git a/synapse/storage/databases/state/schema/delta/47/state_group_seq.py b/synapse/storage/schema/state/delta/47/state_group_seq.py similarity index 100% rename from synapse/storage/databases/state/schema/delta/47/state_group_seq.py rename to synapse/storage/schema/state/delta/47/state_group_seq.py diff --git a/synapse/storage/databases/state/schema/delta/56/state_group_room_idx.sql b/synapse/storage/schema/state/delta/56/state_group_room_idx.sql similarity index 100% rename from synapse/storage/databases/state/schema/delta/56/state_group_room_idx.sql rename to synapse/storage/schema/state/delta/56/state_group_room_idx.sql diff --git a/synapse/storage/databases/state/schema/full_schemas/54/full.sql b/synapse/storage/schema/state/full_schemas/54/full.sql similarity index 100% rename from synapse/storage/databases/state/schema/full_schemas/54/full.sql rename to synapse/storage/schema/state/full_schemas/54/full.sql diff --git a/synapse/storage/databases/state/schema/full_schemas/54/sequence.sql.postgres b/synapse/storage/schema/state/full_schemas/54/sequence.sql.postgres similarity index 100% rename from synapse/storage/databases/state/schema/full_schemas/54/sequence.sql.postgres rename to synapse/storage/schema/state/full_schemas/54/sequence.sql.postgres diff --git a/tests/storage/test_cleanup_extrems.py b/tests/storage/test_cleanup_extrems.py index aa20588bbe..77c4fe721c 100644 --- a/tests/storage/test_cleanup_extrems.py +++ b/tests/storage/test_cleanup_extrems.py @@ -47,10 +47,8 @@ def run_background_update(self): ) schema_path = os.path.join( - prepare_database.dir_path, - "databases", + prepare_database.schema_path, "main", - "schema", "delta", "54", "delete_forward_extremities.sql", From 4df26abf28a13f005dc4b220a5f7eb96bc4df116 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 7 May 2021 12:57:21 +0100 Subject: [PATCH 132/619] Unpin attrs dep after new version has been released (#9946) c.f. #9936 --- changelog.d/9946.misc | 1 + synapse/python_dependencies.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9946.misc diff --git a/changelog.d/9946.misc b/changelog.d/9946.misc new file mode 100644 index 0000000000..142ec5496f --- /dev/null +++ b/changelog.d/9946.misc @@ -0,0 +1 @@ +Unpin attrs dependency. diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 7709361f16..45a6b82834 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -79,7 +79,7 @@ # Fedora 31 only has 19.1, so if we want to upgrade we should wait until 33 # is out in November.) # Note: 21.1.0 broke `/sync`, see #9936 - "attrs>=19.1.0,<21.1.0", + "attrs>=19.1.0,!=21.1.0", "netaddr>=0.7.18", "Jinja2>=2.9", "bleach>=1.4.3", From 765473567cd7e9520bdb9f85491bb5fe719c360b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 7 May 2021 14:01:57 +0100 Subject: [PATCH 133/619] Fix make_full_schema to create the db with the right options and user (#9931) --- changelog.d/9931.misc | 1 + scripts-dev/make_full_schema.sh | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 changelog.d/9931.misc diff --git a/changelog.d/9931.misc b/changelog.d/9931.misc new file mode 100644 index 0000000000..326adc7f3c --- /dev/null +++ b/changelog.d/9931.misc @@ -0,0 +1 @@ +Minor fixes to the `make_full_schema.sh` script. diff --git a/scripts-dev/make_full_schema.sh b/scripts-dev/make_full_schema.sh index bc8f978660..39bf30d258 100755 --- a/scripts-dev/make_full_schema.sh +++ b/scripts-dev/make_full_schema.sh @@ -6,7 +6,7 @@ # It does so by having Synapse generate an up-to-date SQLite DB, then running # synapse_port_db to convert it to Postgres. It then dumps the contents of both. -POSTGRES_HOST="localhost" +export PGHOST="localhost" POSTGRES_DB_NAME="synapse_full_schema.$$" SQLITE_FULL_SCHEMA_OUTPUT_FILE="full.sql.sqlite" @@ -32,7 +32,7 @@ usage() { while getopts "p:co:h" opt; do case $opt in p) - POSTGRES_USERNAME=$OPTARG + export PGUSER=$OPTARG ;; c) # Print all commands that are being executed @@ -69,7 +69,7 @@ if [ ${#unsatisfied_requirements} -ne 0 ]; then exit 1 fi -if [ -z "$POSTGRES_USERNAME" ]; then +if [ -z "$PGUSER" ]; then echo "No postgres username supplied" usage exit 1 @@ -84,8 +84,9 @@ fi # Create the output directory if it doesn't exist mkdir -p "$OUTPUT_DIR" -read -rsp "Postgres password for '$POSTGRES_USERNAME': " POSTGRES_PASSWORD +read -rsp "Postgres password for '$PGUSER': " PGPASSWORD echo "" +export PGPASSWORD # Exit immediately if a command fails set -e @@ -131,9 +132,9 @@ report_stats: false database: name: "psycopg2" args: - user: "$POSTGRES_USERNAME" - host: "$POSTGRES_HOST" - password: "$POSTGRES_PASSWORD" + user: "$PGUSER" + host: "$PGHOST" + password: "$PGPASSWORD" database: "$POSTGRES_DB_NAME" # Suppress the key server warning. @@ -150,7 +151,7 @@ scripts-dev/update_database --database-config "$SQLITE_CONFIG" # Create the PostgreSQL database. echo "Creating postgres database..." -createdb $POSTGRES_DB_NAME +createdb --lc-collate=C --lc-ctype=C --template=template0 "$POSTGRES_DB_NAME" echo "Copying data from SQLite3 to Postgres with synapse_port_db..." if [ -z "$COVERAGE" ]; then @@ -181,7 +182,7 @@ DROP TABLE user_directory_search_docsize; DROP TABLE user_directory_search_stat; " sqlite3 "$SQLITE_DB" <<< "$SQL" -psql $POSTGRES_DB_NAME -U "$POSTGRES_USERNAME" -w <<< "$SQL" +psql "$POSTGRES_DB_NAME" -w <<< "$SQL" echo "Dumping SQLite3 schema to '$OUTPUT_DIR/$SQLITE_FULL_SCHEMA_OUTPUT_FILE'..." sqlite3 "$SQLITE_DB" ".dump" > "$OUTPUT_DIR/$SQLITE_FULL_SCHEMA_OUTPUT_FILE" From 6c847785494473c0a430f368a0d79c9202b8ddd0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 7 May 2021 14:54:09 +0100 Subject: [PATCH 134/619] Always cache 'event_to_prev_state_group' (#9950) Fixes regression in send PDU times introduced in #9905. --- changelog.d/9950.feature | 1 + synapse/handlers/message.py | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 changelog.d/9950.feature diff --git a/changelog.d/9950.feature b/changelog.d/9950.feature new file mode 100644 index 0000000000..96a0e7f09f --- /dev/null +++ b/changelog.d/9950.feature @@ -0,0 +1 @@ +Improve performance of sending events for worker-based deployments using Redis. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 8729332d4b..5afb7fc261 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1050,6 +1050,13 @@ async def cache_joined_hosts_for_event( ) if state_entry.state_group: + await self._external_cache.set( + "event_to_prev_state_group", + event.event_id, + state_entry.state_group, + expiry_ms=60 * 60 * 1000, + ) + if state_entry.state_group in self._external_cache_joined_hosts_updates: return @@ -1057,12 +1064,6 @@ async def cache_joined_hosts_for_event( # Note that the expiry times must be larger than the expiry time in # _external_cache_joined_hosts_updates. - await self._external_cache.set( - "event_to_prev_state_group", - event.event_id, - state_entry.state_group, - expiry_ms=60 * 60 * 1000, - ) await self._external_cache.set( "get_joined_hosts", str(state_entry.state_group), From 51065c44bb0875373fada2838e69e4bc5005a95d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 10 May 2021 13:02:55 +0100 Subject: [PATCH 135/619] Fix port_db on empty db (#9930) ... and test it. --- .buildkite/scripts/create_postgres_db.py | 36 ---------------------- .buildkite/scripts/postgres_exec.py | 31 +++++++++++++++++++ .buildkite/scripts/test_synapse_port_db.sh | 35 +++++++++++++++------ .github/workflows/tests.yml | 2 +- changelog.d/9930.bugfix | 1 + scripts/synapse_port_db | 18 ++++++----- 6 files changed, 69 insertions(+), 54 deletions(-) delete mode 100755 .buildkite/scripts/create_postgres_db.py create mode 100755 .buildkite/scripts/postgres_exec.py create mode 100644 changelog.d/9930.bugfix diff --git a/.buildkite/scripts/create_postgres_db.py b/.buildkite/scripts/create_postgres_db.py deleted file mode 100755 index cc829db216..0000000000 --- a/.buildkite/scripts/create_postgres_db.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python -# Copyright 2019 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from synapse.storage.engines import create_engine - -logger = logging.getLogger("create_postgres_db") - -if __name__ == "__main__": - # Create a PostgresEngine. - db_engine = create_engine({"name": "psycopg2", "args": {}}) - - # Connect to postgres to create the base database. - # We use "postgres" as a database because it's bound to exist and the "synapse" one - # doesn't exist yet. - db_conn = db_engine.module.connect( - user="postgres", host="postgres", password="postgres", dbname="postgres" - ) - db_conn.autocommit = True - cur = db_conn.cursor() - cur.execute("CREATE DATABASE synapse;") - cur.close() - db_conn.close() diff --git a/.buildkite/scripts/postgres_exec.py b/.buildkite/scripts/postgres_exec.py new file mode 100755 index 0000000000..086b391724 --- /dev/null +++ b/.buildkite/scripts/postgres_exec.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import psycopg2 + +# a very simple replacment for `psql`, to make up for the lack of the postgres client +# libraries in the synapse docker image. + +# We use "postgres" as a database because it's bound to exist and the "synapse" one +# doesn't exist yet. +db_conn = psycopg2.connect( + user="postgres", host="postgres", password="postgres", dbname="postgres" +) +db_conn.autocommit = True +cur = db_conn.cursor() +for c in sys.argv[1:]: + cur.execute(c) diff --git a/.buildkite/scripts/test_synapse_port_db.sh b/.buildkite/scripts/test_synapse_port_db.sh index 8914319e38..a7e2454769 100755 --- a/.buildkite/scripts/test_synapse_port_db.sh +++ b/.buildkite/scripts/test_synapse_port_db.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # -# Test script for 'synapse_port_db', which creates a virtualenv, installs Synapse along -# with additional dependencies needed for the test (such as coverage or the PostgreSQL -# driver), update the schema of the test SQLite database and run background updates on it, -# create an empty test database in PostgreSQL, then run the 'synapse_port_db' script to -# test porting the SQLite database to the PostgreSQL database (with coverage). +# Test script for 'synapse_port_db'. +# - sets up synapse and deps +# - runs the port script on a prepopulated test sqlite db +# - also runs it against an new sqlite db + set -xe cd `dirname $0`/../.. @@ -22,15 +22,32 @@ echo "--- Generate the signing key" # Generate the server's signing key. python -m synapse.app.homeserver --generate-keys -c .buildkite/sqlite-config.yaml -echo "--- Prepare the databases" +echo "--- Prepare test database" # Make sure the SQLite3 database is using the latest schema and has no pending background update. scripts-dev/update_database --database-config .buildkite/sqlite-config.yaml # Create the PostgreSQL database. -./.buildkite/scripts/create_postgres_db.py +./.buildkite/scripts/postgres_exec.py "CREATE DATABASE synapse" + +echo "+++ Run synapse_port_db against test database" +coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml + +##### + +# Now do the same again, on an empty database. + +echo "--- Prepare empty SQLite database" + +# we do this by deleting the sqlite db, and then doing the same again. +rm .buildkite/test_db.db + +scripts-dev/update_database --database-config .buildkite/sqlite-config.yaml -echo "+++ Run synapse_port_db" +# re-create the PostgreSQL database. +./.buildkite/scripts/postgres_exec.py \ + "DROP DATABASE synapse" \ + "CREATE DATABASE synapse" -# Run the script +echo "+++ Run synapse_port_db against empty database" coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 12c82ac620..e7f3be1b4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -273,7 +273,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Patch Buildkite-specific test scripts run: | - sed -i -e 's/host="postgres"/host="localhost"/' .buildkite/scripts/create_postgres_db.py + sed -i -e 's/host="postgres"/host="localhost"/' .buildkite/scripts/postgres_exec.py sed -i -e 's/host: postgres/host: localhost/' .buildkite/postgres-config.yaml sed -i -e 's|/src/||' .buildkite/{sqlite,postgres}-config.yaml sed -i -e 's/\$TOP/\$GITHUB_WORKSPACE/' .coveragerc diff --git a/changelog.d/9930.bugfix b/changelog.d/9930.bugfix new file mode 100644 index 0000000000..9b22ed4458 --- /dev/null +++ b/changelog.d/9930.bugfix @@ -0,0 +1 @@ +Fix bugs introduced in v1.23.0 which made the PostgreSQL port script fail when run with a newly-created SQLite database. diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index f0c93d5226..5fb5bb35f7 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -913,10 +913,11 @@ class Porter(object): (curr_forward_id + 1,), ) - txn.execute( - "ALTER SEQUENCE events_backfill_stream_seq RESTART WITH %s", - (curr_backward_id + 1,), - ) + if curr_backward_id: + txn.execute( + "ALTER SEQUENCE events_backfill_stream_seq RESTART WITH %s", + (curr_backward_id + 1,), + ) await self.postgres_store.db_pool.runInteraction( "_setup_events_stream_seqs", _setup_events_stream_seqs_set_pos, @@ -954,10 +955,11 @@ class Porter(object): (curr_chain_id,), ) - await self.postgres_store.db_pool.runInteraction( - "_setup_event_auth_chain_id", r, - ) - + if curr_chain_id is not None: + await self.postgres_store.db_pool.runInteraction( + "_setup_event_auth_chain_id", + r, + ) ############################################## From 2b2985b5cfc4267dfb0f6b900e1a0f69f3f2cdc5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 10 May 2021 13:29:02 +0100 Subject: [PATCH 136/619] Improve performance of backfilling in large rooms. (#9935) We were pulling the full auth chain for the room out of the DB each time we backfilled, which can be *huge* for large rooms and is totally unnecessary. --- changelog.d/9935.feature | 1 + synapse/handlers/federation.py | 123 +++++++++++++++------------------ 2 files changed, 55 insertions(+), 69 deletions(-) create mode 100644 changelog.d/9935.feature diff --git a/changelog.d/9935.feature b/changelog.d/9935.feature new file mode 100644 index 0000000000..eeda5bf50e --- /dev/null +++ b/changelog.d/9935.feature @@ -0,0 +1 @@ +Improve performance of backfilling in large rooms. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index e8330a2b50..798ed75b30 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -552,8 +552,12 @@ async def _get_state_for_room( destination: str, room_id: str, event_id: str, - ) -> Tuple[List[EventBase], List[EventBase]]: - """Requests all of the room state at a given event from a remote homeserver. + ) -> List[EventBase]: + """Requests all of the room state at a given event from a remote + homeserver. + + Will also fetch any missing events reported in the `auth_chain_ids` + section of `/state_ids`. Args: destination: The remote homeserver to query for the state. @@ -561,8 +565,7 @@ async def _get_state_for_room( event_id: The id of the event we want the state at. Returns: - A list of events in the state, not including the event itself, and - a list of events in the auth chain for the given event. + A list of events in the state, not including the event itself. """ ( state_event_ids, @@ -571,68 +574,53 @@ async def _get_state_for_room( destination, room_id, event_id=event_id ) - desired_events = set(state_event_ids + auth_event_ids) - - event_map = await self._get_events_from_store_or_dest( - destination, room_id, desired_events - ) + # Fetch the state events from the DB, and check we have the auth events. + event_map = await self.store.get_events(state_event_ids, allow_rejected=True) + auth_events_in_store = await self.store.have_seen_events(auth_event_ids) - failed_to_fetch = desired_events - event_map.keys() - if failed_to_fetch: - logger.warning( - "Failed to fetch missing state/auth events for %s %s", - event_id, - failed_to_fetch, + # Check for missing events. We handle state and auth event seperately, + # as we want to pull the state from the DB, but we don't for the auth + # events. (Note: we likely won't use the majority of the auth chain, and + # it can be *huge* for large rooms, so it's worth ensuring that we don't + # unnecessarily pull it from the DB). + missing_state_events = set(state_event_ids) - set(event_map) + missing_auth_events = set(auth_event_ids) - set(auth_events_in_store) + if missing_state_events or missing_auth_events: + await self._get_events_and_persist( + destination=destination, + room_id=room_id, + events=missing_state_events | missing_auth_events, ) - remote_state = [ - event_map[e_id] for e_id in state_event_ids if e_id in event_map - ] - - auth_chain = [event_map[e_id] for e_id in auth_event_ids if e_id in event_map] - auth_chain.sort(key=lambda e: e.depth) - - return remote_state, auth_chain - - async def _get_events_from_store_or_dest( - self, destination: str, room_id: str, event_ids: Iterable[str] - ) -> Dict[str, EventBase]: - """Fetch events from a remote destination, checking if we already have them. - - Persists any events we don't already have as outliers. - - If we fail to fetch any of the events, a warning will be logged, and the event - will be omitted from the result. Likewise, any events which turn out not to - be in the given room. + if missing_state_events: + new_events = await self.store.get_events( + missing_state_events, allow_rejected=True + ) + event_map.update(new_events) - This function *does not* automatically get missing auth events of the - newly fetched events. Callers must include the full auth chain of - of the missing events in the `event_ids` argument, to ensure that any - missing auth events are correctly fetched. + missing_state_events.difference_update(new_events) - Returns: - map from event_id to event - """ - fetched_events = await self.store.get_events(event_ids, allow_rejected=True) - - missing_events = set(event_ids) - fetched_events.keys() + if missing_state_events: + logger.warning( + "Failed to fetch missing state events for %s %s", + event_id, + missing_state_events, + ) - if missing_events: - logger.debug( - "Fetching unknown state/auth events %s for room %s", - missing_events, - room_id, - ) + if missing_auth_events: + auth_events_in_store = await self.store.have_seen_events( + missing_auth_events + ) + missing_auth_events.difference_update(auth_events_in_store) - await self._get_events_and_persist( - destination=destination, room_id=room_id, events=missing_events - ) + if missing_auth_events: + logger.warning( + "Failed to fetch missing auth events for %s %s", + event_id, + missing_auth_events, + ) - # we need to make sure we re-load from the database to get the rejected - # state correct. - fetched_events.update( - (await self.store.get_events(missing_events, allow_rejected=True)) - ) + remote_state = list(event_map.values()) # check for events which were in the wrong room. # @@ -640,8 +628,8 @@ async def _get_events_from_store_or_dest( # auth_events at an event in room A are actually events in room B bad_events = [ - (event_id, event.room_id) - for event_id, event in fetched_events.items() + (event.event_id, event.room_id) + for event in remote_state if event.room_id != room_id ] @@ -658,9 +646,10 @@ async def _get_events_from_store_or_dest( room_id, ) - del fetched_events[bad_event_id] + if bad_events: + remote_state = [e for e in remote_state if e.room_id == room_id] - return fetched_events + return remote_state async def _get_state_after_missing_prev_event( self, @@ -963,27 +952,23 @@ async def backfill( # For each edge get the current state. - auth_events = {} state_events = {} events_to_state = {} for e_id in edges: - state, auth = await self._get_state_for_room( + state = await self._get_state_for_room( destination=dest, room_id=room_id, event_id=e_id, ) - auth_events.update({a.event_id: a for a in auth}) - auth_events.update({s.event_id: s for s in state}) state_events.update({s.event_id: s for s in state}) events_to_state[e_id] = state required_auth = { a_id - for event in events - + list(state_events.values()) - + list(auth_events.values()) + for event in events + list(state_events.values()) for a_id in event.auth_event_ids() } + auth_events = await self.store.get_events(required_auth, allow_rejected=True) auth_events.update( {e_id: event_map[e_id] for e_id in required_auth if e_id in event_map} ) From 03318a766cac9f8b053db2214d9c332a977d226c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 11 May 2021 10:47:23 +0100 Subject: [PATCH 137/619] Merge pull request from GHSA-x345-32rc-8h85 * tests for push rule pattern matching * tests for acl pattern matching * factor out common `re.escape` * Factor out common re.compile * Factor out common anchoring code * add word_boundary support to `glob_to_regex` * Use `glob_to_regex` in push rule evaluator NB that this drops support for character classes. I don't think anyone ever used them. * Improve efficiency of globs with multiple wildcards The idea here is that we compress multiple `*` globs into a single `.*`. We also need to consider `?`, since `*?*` is as hard to implement efficiently as `**`. * add assertion on regex pattern * Fix mypy * Simplify glob_to_regex * Inline the glob_to_regex helper function Signed-off-by: Dan Callahan * Moar comments Signed-off-by: Dan Callahan Co-authored-by: Dan Callahan --- synapse/config/tls.py | 4 +- synapse/push/push_rule_evaluator.py | 55 +------ synapse/util/__init__.py | 61 ++++++-- tests/federation/test_federation_server.py | 19 +++ tests/push/test_push_rule_evaluator.py | 166 +++++++++++++++++++++ tests/util/test_glob_to_regex.py | 59 ++++++++ 6 files changed, 296 insertions(+), 68 deletions(-) create mode 100644 tests/util/test_glob_to_regex.py diff --git a/synapse/config/tls.py b/synapse/config/tls.py index b041869758..7df4e4c3e6 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -17,7 +17,7 @@ import warnings from datetime import datetime from hashlib import sha256 -from typing import List, Optional +from typing import List, Optional, Pattern from unpaddedbase64 import encode_base64 @@ -124,7 +124,7 @@ def read_config(self, config: dict, config_dir_path: str, **kwargs): fed_whitelist_entries = [] # Support globs (*) in whitelist values - self.federation_certificate_verification_whitelist = [] # type: List[str] + self.federation_certificate_verification_whitelist = [] # type: List[Pattern] for entry in fed_whitelist_entries: try: entry_regex = glob_to_regex(entry.encode("ascii").decode("ascii")) diff --git a/synapse/push/push_rule_evaluator.py b/synapse/push/push_rule_evaluator.py index 49ecb38522..98b90a4f51 100644 --- a/synapse/push/push_rule_evaluator.py +++ b/synapse/push/push_rule_evaluator.py @@ -19,6 +19,7 @@ from synapse.events import EventBase from synapse.types import UserID +from synapse.util import glob_to_regex, re_word_boundary from synapse.util.caches.lrucache import LruCache logger = logging.getLogger(__name__) @@ -183,7 +184,7 @@ def _contains_display_name(self, display_name: str) -> bool: r = regex_cache.get((display_name, False, True), None) if not r: r1 = re.escape(display_name) - r1 = _re_word_boundary(r1) + r1 = re_word_boundary(r1) r = re.compile(r1, flags=re.IGNORECASE) regex_cache[(display_name, False, True)] = r @@ -212,7 +213,7 @@ def _glob_matches(glob: str, value: str, word_boundary: bool = False) -> bool: try: r = regex_cache.get((glob, True, word_boundary), None) if not r: - r = _glob_to_re(glob, word_boundary) + r = glob_to_regex(glob, word_boundary) regex_cache[(glob, True, word_boundary)] = r return bool(r.search(value)) except re.error: @@ -220,56 +221,6 @@ def _glob_matches(glob: str, value: str, word_boundary: bool = False) -> bool: return False -def _glob_to_re(glob: str, word_boundary: bool) -> Pattern: - """Generates regex for a given glob. - - Args: - glob - word_boundary: Whether to match against word boundaries or entire string. - """ - if IS_GLOB.search(glob): - r = re.escape(glob) - - r = r.replace(r"\*", ".*?") - r = r.replace(r"\?", ".") - - # handle [abc], [a-z] and [!a-z] style ranges. - r = GLOB_REGEX.sub( - lambda x: ( - "[%s%s]" % (x.group(1) and "^" or "", x.group(2).replace(r"\\\-", "-")) - ), - r, - ) - if word_boundary: - r = _re_word_boundary(r) - - return re.compile(r, flags=re.IGNORECASE) - else: - r = "^" + r + "$" - - return re.compile(r, flags=re.IGNORECASE) - elif word_boundary: - r = re.escape(glob) - r = _re_word_boundary(r) - - return re.compile(r, flags=re.IGNORECASE) - else: - r = "^" + re.escape(glob) + "$" - return re.compile(r, flags=re.IGNORECASE) - - -def _re_word_boundary(r: str) -> str: - """ - Adds word boundary characters to the start and end of an - expression to require that the match occur as a whole word, - but do so respecting the fact that strings starting or ending - with non-word characters will change word boundaries. - """ - # we can't use \b as it chokes on unicode. however \W seems to be okay - # as shorthand for [^0-9A-Za-z_]. - return r"(^|\W)%s(\W|$)" % (r,) - - def _flatten_dict( d: Union[EventBase, dict], prefix: Optional[List[str]] = None, diff --git a/synapse/util/__init__.py b/synapse/util/__init__.py index 0f84fa3f4e..b69f562ca5 100644 --- a/synapse/util/__init__.py +++ b/synapse/util/__init__.py @@ -15,6 +15,7 @@ import json import logging import re +from typing import Pattern import attr from frozendict import frozendict @@ -26,6 +27,9 @@ logger = logging.getLogger(__name__) +_WILDCARD_RUN = re.compile(r"([\?\*]+)") + + def _reject_invalid_json(val): """Do not allow Infinity, -Infinity, or NaN values in JSON.""" raise ValueError("Invalid JSON value: '%s'" % val) @@ -158,25 +162,54 @@ def log_failure(failure, msg, consumeErrors=True): return failure -def glob_to_regex(glob): +def glob_to_regex(glob: str, word_boundary: bool = False) -> Pattern: """Converts a glob to a compiled regex object. - The regex is anchored at the beginning and end of the string. - Args: - glob (str) + glob: pattern to match + word_boundary: If True, the pattern will be allowed to match at word boundaries + anywhere in the string. Otherwise, the pattern is anchored at the start and + end of the string. Returns: - re.RegexObject + compiled regex pattern """ - res = "" - for c in glob: - if c == "*": - res = res + ".*" - elif c == "?": - res = res + "." + + # Patterns with wildcards must be simplified to avoid performance cliffs + # - The glob `?**?**?` is equivalent to the glob `???*` + # - The glob `???*` is equivalent to the regex `.{3,}` + chunks = [] + for chunk in _WILDCARD_RUN.split(glob): + # No wildcards? re.escape() + if not _WILDCARD_RUN.match(chunk): + chunks.append(re.escape(chunk)) + continue + + # Wildcards? Simplify. + qmarks = chunk.count("?") + if "*" in chunk: + chunks.append(".{%d,}" % qmarks) else: - res = res + re.escape(c) + chunks.append(".{%d}" % qmarks) + + res = "".join(chunks) - # \A anchors at start of string, \Z at end of string - return re.compile(r"\A" + res + r"\Z", re.IGNORECASE) + if word_boundary: + res = re_word_boundary(res) + else: + # \A anchors at start of string, \Z at end of string + res = r"\A" + res + r"\Z" + + return re.compile(res, re.IGNORECASE) + + +def re_word_boundary(r: str) -> str: + """ + Adds word boundary characters to the start and end of an + expression to require that the match occur as a whole word, + but do so respecting the fact that strings starting or ending + with non-word characters will change word boundaries. + """ + # we can't use \b as it chokes on unicode. however \W seems to be okay + # as shorthand for [^0-9A-Za-z_]. + return r"(^|\W)%s(\W|$)" % (r,) diff --git a/tests/federation/test_federation_server.py b/tests/federation/test_federation_server.py index 8508b6bd3b..1737891564 100644 --- a/tests/federation/test_federation_server.py +++ b/tests/federation/test_federation_server.py @@ -74,6 +74,25 @@ def test_block_ip_literals(self): self.assertFalse(server_matches_acl_event("[1:2::]", e)) self.assertTrue(server_matches_acl_event("1:2:3:4", e)) + def test_wildcard_matching(self): + e = _create_acl_event({"allow": ["good*.com"]}) + self.assertTrue( + server_matches_acl_event("good.com", e), + "* matches 0 characters", + ) + self.assertTrue( + server_matches_acl_event("GOOD.COM", e), + "pattern is case-insensitive", + ) + self.assertTrue( + server_matches_acl_event("good.aa.com", e), + "* matches several characters, including '.'", + ) + self.assertFalse( + server_matches_acl_event("ishgood.com", e), + "pattern does not allow prefixes", + ) + class StateQueryTests(unittest.FederatingHomeserverTestCase): diff --git a/tests/push/test_push_rule_evaluator.py b/tests/push/test_push_rule_evaluator.py index 45906ce720..a52e89e407 100644 --- a/tests/push/test_push_rule_evaluator.py +++ b/tests/push/test_push_rule_evaluator.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Dict + from synapse.api.room_versions import RoomVersions from synapse.events import FrozenEvent from synapse.push import push_rule_evaluator @@ -66,6 +68,170 @@ def test_display_name(self): # A display name with spaces should work fine. self.assertTrue(evaluator.matches(condition, "@user:test", "foo bar")) + def _assert_matches( + self, condition: Dict[str, Any], content: Dict[str, Any], msg=None + ) -> None: + evaluator = self._get_evaluator(content) + self.assertTrue(evaluator.matches(condition, "@user:test", "display_name"), msg) + + def _assert_not_matches( + self, condition: Dict[str, Any], content: Dict[str, Any], msg=None + ) -> None: + evaluator = self._get_evaluator(content) + self.assertFalse( + evaluator.matches(condition, "@user:test", "display_name"), msg + ) + + def test_event_match_body(self): + """Check that event_match conditions on content.body work as expected""" + + # if the key is `content.body`, the pattern matches substrings. + + # non-wildcards should match + condition = { + "kind": "event_match", + "key": "content.body", + "pattern": "foobaz", + } + self._assert_matches( + condition, + {"body": "aaa FoobaZ zzz"}, + "patterns should match and be case-insensitive", + ) + self._assert_not_matches( + condition, + {"body": "aa xFoobaZ yy"}, + "pattern should only match at word boundaries", + ) + self._assert_not_matches( + condition, + {"body": "aa foobazx yy"}, + "pattern should only match at word boundaries", + ) + + # wildcards should match + condition = { + "kind": "event_match", + "key": "content.body", + "pattern": "f?o*baz", + } + + self._assert_matches( + condition, + {"body": "aaa FoobarbaZ zzz"}, + "* should match string and pattern should be case-insensitive", + ) + self._assert_matches( + condition, {"body": "aa foobaz yy"}, "* should match 0 characters" + ) + self._assert_not_matches( + condition, {"body": "aa fobbaz yy"}, "? should not match 0 characters" + ) + self._assert_not_matches( + condition, {"body": "aa fiiobaz yy"}, "? should not match 2 characters" + ) + self._assert_not_matches( + condition, + {"body": "aa xfooxbaz yy"}, + "pattern should only match at word boundaries", + ) + self._assert_not_matches( + condition, + {"body": "aa fooxbazx yy"}, + "pattern should only match at word boundaries", + ) + + # test backslashes + condition = { + "kind": "event_match", + "key": "content.body", + "pattern": r"f\oobaz", + } + self._assert_matches( + condition, + {"body": r"F\oobaz"}, + "backslash should match itself", + ) + condition = { + "kind": "event_match", + "key": "content.body", + "pattern": r"f\?obaz", + } + self._assert_matches( + condition, + {"body": r"F\oobaz"}, + r"? after \ should match any character", + ) + + def test_event_match_non_body(self): + """Check that event_match conditions on other keys work as expected""" + + # if the key is anything other than 'content.body', the pattern must match the + # whole value. + + # non-wildcards should match + condition = { + "kind": "event_match", + "key": "content.value", + "pattern": "foobaz", + } + self._assert_matches( + condition, + {"value": "FoobaZ"}, + "patterns should match and be case-insensitive", + ) + self._assert_not_matches( + condition, + {"value": "xFoobaZ"}, + "pattern should only match at the start/end of the value", + ) + self._assert_not_matches( + condition, + {"value": "FoobaZz"}, + "pattern should only match at the start/end of the value", + ) + + # wildcards should match + condition = { + "kind": "event_match", + "key": "content.value", + "pattern": "f?o*baz", + } + self._assert_matches( + condition, + {"value": "FoobarbaZ"}, + "* should match string and pattern should be case-insensitive", + ) + self._assert_matches( + condition, {"value": "foobaz"}, "* should match 0 characters" + ) + self._assert_not_matches( + condition, {"value": "fobbaz"}, "? should not match 0 characters" + ) + self._assert_not_matches( + condition, {"value": "fiiobaz"}, "? should not match 2 characters" + ) + self._assert_not_matches( + condition, + {"value": "xfooxbaz"}, + "pattern should only match at the start/end of the value", + ) + self._assert_not_matches( + condition, + {"value": "fooxbazx"}, + "pattern should only match at the start/end of the value", + ) + self._assert_not_matches( + condition, + {"value": "x\nfooxbaz"}, + "pattern should not match after a newline", + ) + self._assert_not_matches( + condition, + {"value": "fooxbaz\nx"}, + "pattern should not match before a newline", + ) + def test_no_body(self): """Not having a body shouldn't break the evaluator.""" evaluator = self._get_evaluator({}) diff --git a/tests/util/test_glob_to_regex.py b/tests/util/test_glob_to_regex.py new file mode 100644 index 0000000000..220accb92b --- /dev/null +++ b/tests/util/test_glob_to_regex.py @@ -0,0 +1,59 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from synapse.util import glob_to_regex + +from tests.unittest import TestCase + + +class GlobToRegexTestCase(TestCase): + def test_literal_match(self): + """patterns without wildcards should match""" + pat = glob_to_regex("foobaz") + self.assertTrue( + pat.match("FoobaZ"), "patterns should match and be case-insensitive" + ) + self.assertFalse( + pat.match("x foobaz"), "pattern should not match at word boundaries" + ) + + def test_wildcard_match(self): + pat = glob_to_regex("f?o*baz") + + self.assertTrue( + pat.match("FoobarbaZ"), + "* should match string and pattern should be case-insensitive", + ) + self.assertTrue(pat.match("foobaz"), "* should match 0 characters") + self.assertFalse(pat.match("fooxaz"), "the character after * must match") + self.assertFalse(pat.match("fobbaz"), "? should not match 0 characters") + self.assertFalse(pat.match("fiiobaz"), "? should not match 2 characters") + + def test_multi_wildcard(self): + """patterns with multiple wildcards in a row should match""" + pat = glob_to_regex("**baz") + self.assertTrue(pat.match("agsgsbaz"), "** should match any string") + self.assertTrue(pat.match("baz"), "** should match the empty string") + self.assertEqual(pat.pattern, r"\A.{0,}baz\Z") + + pat = glob_to_regex("*?baz") + self.assertTrue(pat.match("agsgsbaz"), "*? should match any string") + self.assertTrue(pat.match("abaz"), "*? should match a single char") + self.assertFalse(pat.match("baz"), "*? should not match the empty string") + self.assertEqual(pat.pattern, r"\A.{1,}baz\Z") + + pat = glob_to_regex("a?*?*?baz") + self.assertTrue(pat.match("a g baz"), "?*?*? should match 3 chars") + self.assertFalse(pat.match("a..baz"), "?*?*? should not match 2 chars") + self.assertTrue(pat.match("a.gg.baz"), "?*?*? should match 4 chars") + self.assertEqual(pat.pattern, r"\Aa.{3,}baz\Z") From 7967b36efe6a033f46cd882d0b31a8c3eb18631c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 11 May 2021 11:02:56 +0100 Subject: [PATCH 138/619] Fix `m.room_key_request` to-device messages (#9961) fixes #9960 --- changelog.d/9961.bugfix | 1 + synapse/api/constants.py | 5 +++- synapse/federation/federation_server.py | 19 -------------- synapse/handlers/devicemessage.py | 33 ++++++++++++++++++++----- 4 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 changelog.d/9961.bugfix diff --git a/changelog.d/9961.bugfix b/changelog.d/9961.bugfix new file mode 100644 index 0000000000..e26d141a53 --- /dev/null +++ b/changelog.d/9961.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse 1.29.0 which caused `m.room_key_request` to-device messages sent from one user to another to be dropped. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index ab628b2be7..3940da5c88 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -116,9 +116,12 @@ class EventTypes: MSC1772_SPACE_PARENT = "org.matrix.msc1772.space.parent" +class ToDeviceEventTypes: + RoomKeyRequest = "m.room_key_request" + + class EduTypes: Presence = "m.presence" - RoomKeyRequest = "m.room_key_request" class RejectedReason: diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index b729a69203..ace30aa450 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -44,7 +44,6 @@ SynapseError, UnsupportedRoomVersionError, ) -from synapse.api.ratelimiting import Ratelimiter from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.events import EventBase from synapse.federation.federation_base import FederationBase, event_from_pdu_json @@ -865,14 +864,6 @@ def __init__(self, hs: "HomeServer"): # EDU received. self._edu_type_to_instance = {} # type: Dict[str, List[str]] - # A rate limiter for incoming room key requests per origin. - self._room_key_request_rate_limiter = Ratelimiter( - store=hs.get_datastore(), - clock=self.clock, - rate_hz=self.config.rc_key_requests.per_second, - burst_count=self.config.rc_key_requests.burst_count, - ) - def register_edu_handler( self, edu_type: str, handler: Callable[[str, JsonDict], Awaitable[None]] ) -> None: @@ -926,16 +917,6 @@ async def on_edu(self, edu_type: str, origin: str, content: dict) -> None: if not self.config.use_presence and edu_type == EduTypes.Presence: return - # If the incoming room key requests from a particular origin are over - # the limit, drop them. - if ( - edu_type == EduTypes.RoomKeyRequest - and not await self._room_key_request_rate_limiter.can_do_action( - None, origin - ) - ): - return - # Check if we have a handler on this instance handler = self.edu_handlers.get(edu_type) if handler: diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index c5d631de07..580b941595 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -15,7 +15,7 @@ import logging from typing import TYPE_CHECKING, Any, Dict -from synapse.api.constants import EduTypes +from synapse.api.constants import ToDeviceEventTypes from synapse.api.errors import SynapseError from synapse.api.ratelimiting import Ratelimiter from synapse.logging.context import run_in_background @@ -79,6 +79,8 @@ def __init__(self, hs: "HomeServer"): ReplicationUserDevicesResyncRestServlet.make_client(hs) ) + # a rate limiter for room key requests. The keys are + # (sending_user_id, sending_device_id). self._ratelimiter = Ratelimiter( store=self.store, clock=hs.get_clock(), @@ -100,12 +102,25 @@ async def on_direct_to_device_edu(self, origin: str, content: JsonDict) -> None: for user_id, by_device in content["messages"].items(): # we use UserID.from_string to catch invalid user ids if not self.is_mine(UserID.from_string(user_id)): - logger.warning("Request for keys for non-local user %s", user_id) + logger.warning("To-device message to non-local user %s", user_id) raise SynapseError(400, "Not a user here") if not by_device: continue + # Ratelimit key requests by the sending user. + if message_type == ToDeviceEventTypes.RoomKeyRequest: + allowed, _ = await self._ratelimiter.can_do_action( + None, (sender_user_id, None) + ) + if not allowed: + logger.info( + "Dropping room_key_request from %s to %s due to rate limit", + sender_user_id, + user_id, + ) + continue + messages_by_device = { device_id: { "content": message_content, @@ -192,13 +207,19 @@ async def send_device_message( for user_id, by_device in messages.items(): # Ratelimit local cross-user key requests by the sending device. if ( - message_type == EduTypes.RoomKeyRequest + message_type == ToDeviceEventTypes.RoomKeyRequest and user_id != sender_user_id - and await self._ratelimiter.can_do_action( + ): + allowed, _ = await self._ratelimiter.can_do_action( requester, (sender_user_id, requester.device_id) ) - ): - continue + if not allowed: + logger.info( + "Dropping room_key_request from %s to %s due to rate limit", + sender_user_id, + user_id, + ) + continue # we use UserID.from_string to catch invalid user ids if self.is_mine(UserID.from_string(user_id)): From b378d98c8f93412bc51f0d4b9c4aa2d1701cf523 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 11 May 2021 11:04:03 +0100 Subject: [PATCH 139/619] Add debug logging for issue #9533 (#9959) Hopefully this will help us track down where to-device messages are getting lost/delayed. --- changelog.d/9959.misc | 1 + .../federation/sender/per_destination_queue.py | 9 +++++++++ synapse/logging/__init__.py | 7 ++++++- synapse/notifier.py | 8 ++++++++ synapse/replication/tcp/client.py | 1 - synapse/storage/databases/main/deviceinbox.py | 18 ++++++++++++++++++ 6 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9959.misc diff --git a/changelog.d/9959.misc b/changelog.d/9959.misc new file mode 100644 index 0000000000..7231f29d79 --- /dev/null +++ b/changelog.d/9959.misc @@ -0,0 +1 @@ +Add debug logging for lost/delayed to-device messages. diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index 3b053ebcfb..3a2efd56ee 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -28,6 +28,7 @@ from synapse.events import EventBase from synapse.federation.units import Edu from synapse.handlers.presence import format_user_presence_state +from synapse.logging import issue9533_logger from synapse.logging.opentracing import SynapseTags, set_tag from synapse.metrics import sent_transactions_counter from synapse.metrics.background_process_metrics import run_as_background_process @@ -574,6 +575,14 @@ async def _get_to_device_message_edus(self, limit: int) -> Tuple[List[Edu], int] for content in contents ] + if edus: + issue9533_logger.debug( + "Sending %i to-device messages to %s, up to stream id %i", + len(edus), + self._destination, + stream_id, + ) + return (edus, stream_id) def _start_catching_up(self) -> None: diff --git a/synapse/logging/__init__.py b/synapse/logging/__init__.py index e00969f8b1..b50a4f95eb 100644 --- a/synapse/logging/__init__.py +++ b/synapse/logging/__init__.py @@ -12,8 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -# These are imported to allow for nicer logging configuration files. +import logging + from synapse.logging._remote import RemoteHandler from synapse.logging._terse_json import JsonFormatter, TerseJsonFormatter +# These are imported to allow for nicer logging configuration files. __all__ = ["RemoteHandler", "JsonFormatter", "TerseJsonFormatter"] + +# Debug logger for https://github.com/matrix-org/synapse/issues/9533 etc +issue9533_logger = logging.getLogger("synapse.9533_debug") diff --git a/synapse/notifier.py b/synapse/notifier.py index b9531007e2..24b4e6649f 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -38,6 +38,7 @@ from synapse.api.errors import AuthError from synapse.events import EventBase from synapse.handlers.presence import format_user_presence_state +from synapse.logging import issue9533_logger from synapse.logging.context import PreserveLoggingContext from synapse.logging.opentracing import log_kv, start_active_span from synapse.logging.utils import log_function @@ -426,6 +427,13 @@ def on_new_event( for room in rooms: user_streams |= self.room_to_user_streams.get(room, set()) + if stream_key == "to_device_key": + issue9533_logger.debug( + "to-device messages stream id %s, awaking streams for %s", + new_token, + users, + ) + time_now_ms = self.clock.time_msec() for user_stream in user_streams: try: diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 4f3c6a18b6..62d7809175 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -51,7 +51,6 @@ logger = logging.getLogger(__name__) - # How long we allow callers to wait for replication updates before timing out. _WAIT_FOR_REPLICATION_TIMEOUT_SECONDS = 30 diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py index 7c9d1f744e..50e7ddd735 100644 --- a/synapse/storage/databases/main/deviceinbox.py +++ b/synapse/storage/databases/main/deviceinbox.py @@ -15,6 +15,7 @@ import logging from typing import List, Optional, Tuple +from synapse.logging import issue9533_logger from synapse.logging.opentracing import log_kv, set_tag, trace from synapse.replication.tcp.streams import ToDeviceStream from synapse.storage._base import SQLBaseStore, db_to_json @@ -404,6 +405,13 @@ def add_messages_txn(txn, now_ms, stream_id): ], ) + if remote_messages_by_destination: + issue9533_logger.debug( + "Queued outgoing to-device messages with stream_id %i for %s", + stream_id, + list(remote_messages_by_destination.keys()), + ) + async with self._device_inbox_id_gen.get_next() as stream_id: now_ms = self.clock.time_msec() await self.db_pool.runInteraction( @@ -533,6 +541,16 @@ def _add_messages_to_local_device_inbox_txn( ], ) + issue9533_logger.debug( + "Stored to-device messages with stream_id %i for %s", + stream_id, + [ + (user_id, device_id) + for (user_id, messages_by_device) in local_by_user_then_device.items() + for device_id in messages_by_device.keys() + ], + ) + class DeviceInboxBackgroundUpdateStore(SQLBaseStore): DEVICE_INBOX_STREAM_ID = "device_inbox_stream_drop" From 86fb71431ca562ba536c50620d1e367a9c6d4ac9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 11 May 2021 13:53:49 +0100 Subject: [PATCH 140/619] 1.33.2 --- CHANGES.md | 16 ++++++++++++++++ changelog.d/9946.misc | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/9946.misc diff --git a/CHANGES.md b/CHANGES.md index a41abbefba..7ae0e7b3c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +Synapse 1.33.2 (2021-05-11) +=========================== + +Due to the security issue highlighted below, server administrators are encouraged to update Synapse. We are not aware of these vulnerabilities being exploited in the wild. + +Security advisory +----------------- + +This release fixes a denial of service attack ([CVE-2021-29471](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-29471)) against Synapse's push rules implementation. Server admins are encouraged to upgrade. + +Internal Changes +---------------- + +- Unpin attrs dependency. ([\#9946](https://github.com/matrix-org/synapse/issues/9946)) + + Synapse 1.33.1 (2021-05-06) =========================== diff --git a/changelog.d/9946.misc b/changelog.d/9946.misc deleted file mode 100644 index 142ec5496f..0000000000 --- a/changelog.d/9946.misc +++ /dev/null @@ -1 +0,0 @@ -Unpin attrs dependency. diff --git a/debian/changelog b/debian/changelog index de50dd14ea..76b82c172e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.33.2) stable; urgency=medium + + * New synapse release 1.33.2. + + -- Synapse Packaging team Tue, 11 May 2021 11:17:59 +0100 + matrix-synapse-py3 (1.33.1) stable; urgency=medium * New synapse release 1.33.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 441cd8b339..ce822ccb04 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.33.1" +__version__ = "1.33.2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From dc6366a9bd370a0f772f376a2053c0ce48cb6607 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 11 May 2021 08:03:23 -0500 Subject: [PATCH 141/619] Add config option to hide device names over federation (#9945) Now that cross signing exists there is much less of a need for other people to look at devices and verify them individually. This PR adds a config option to allow you to prevent device display names from being shared with other servers. Signed-off-by: Aaron Raimist --- changelog.d/9945.feature | 1 + docs/sample_config.yaml | 6 ++++++ synapse/config/federation.py | 10 ++++++++++ synapse/storage/databases/main/end_to_end_keys.py | 4 +++- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9945.feature diff --git a/changelog.d/9945.feature b/changelog.d/9945.feature new file mode 100644 index 0000000000..84308e8cce --- /dev/null +++ b/changelog.d/9945.feature @@ -0,0 +1 @@ +Add a config option to allow you to prevent device display names from being shared over federation. Contributed by @aaronraimist. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index f469d6e54f..7cf222d356 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -741,6 +741,12 @@ acme: # #allow_profile_lookup_over_federation: false +# Uncomment to disable device display name lookup over federation. By default, the +# Federation API allows other homeservers to obtain device display names of any user +# on this homeserver. Defaults to 'true'. +# +#allow_device_name_lookup_over_federation: false + ## Caching ## diff --git a/synapse/config/federation.py b/synapse/config/federation.py index 090ba047fa..cdd7a1ef05 100644 --- a/synapse/config/federation.py +++ b/synapse/config/federation.py @@ -44,6 +44,10 @@ def read_config(self, config, **kwargs): "allow_profile_lookup_over_federation", True ) + self.allow_device_name_lookup_over_federation = config.get( + "allow_device_name_lookup_over_federation", True + ) + def generate_config_section(self, config_dir_path, server_name, **kwargs): return """\ ## Federation ## @@ -75,6 +79,12 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # on this homeserver. Defaults to 'true'. # #allow_profile_lookup_over_federation: false + + # Uncomment to disable device display name lookup over federation. By default, the + # Federation API allows other homeservers to obtain device display names of any user + # on this homeserver. Defaults to 'true'. + # + #allow_device_name_lookup_over_federation: false """ diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index 88afe97c41..398d6b6acb 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -84,7 +84,9 @@ async def get_e2e_device_keys_for_federation_query( if keys: result["keys"] = keys - device_display_name = device.display_name + device_display_name = None + if self.hs.config.allow_device_name_lookup_over_federation: + device_display_name = device.display_name if device_display_name: result["device_display_name"] = device_display_name From d1473f7362e9b146dbd256076c8e3c7d163e7d94 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 11 May 2021 14:09:46 +0100 Subject: [PATCH 142/619] Use link to advisory rather than to the CVE repo --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7ae0e7b3c1..93efa3ce56 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Due to the security issue highlighted below, server administrators are encourage Security advisory ----------------- -This release fixes a denial of service attack ([CVE-2021-29471](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-29471)) against Synapse's push rules implementation. Server admins are encouraged to upgrade. +This release fixes a denial of service attack ([CVE-2021-29471](https://github.com/matrix-org/synapse/security/advisories/GHSA-x345-32rc-8h85)) against Synapse's push rules implementation. Server admins are encouraged to upgrade. Internal Changes ---------------- From 28c68411028e15858819f2a5896313ce0e71c25b Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 11 May 2021 10:58:58 -0400 Subject: [PATCH 143/619] Send the `m.room.create` stripped event with invites (support MSC1772). (#9966) MSC1772 specifies the m.room.create event should be sent as part of the invite_state. This was done optionally behind an experimental flag, but is now done by default due to MSC1772 being approved. --- UPGRADE.rst | 29 +++++++++++++++++++++++++++++ changelog.d/9915.feature | 2 +- changelog.d/9966.feature | 1 + docs/sample_config.yaml | 1 + synapse/config/api.py | 6 ++---- 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 changelog.d/9966.feature diff --git a/UPGRADE.rst b/UPGRADE.rst index e921e0c08a..606e357b6e 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -85,6 +85,35 @@ for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb +Upgrading to v1.34.0 +==================== + +`room_invite_state_types` configuration setting +----------------------------------------------- + +The ``room_invite_state_types`` configuration setting has been deprecated and +replaced with ``room_prejoin_state``. See the `sample configuration file `_. + +If you have set ``room_invite_state_types`` to the default value you should simply +remove it from your configuration file. The default value used to be: + +.. code:: yaml + + room_invite_state_types: + - "m.room.join_rules" + - "m.room.canonical_alias" + - "m.room.avatar" + - "m.room.encryption" + - "m.room.name" + +If you have customised this value by adding addition state types, you should +remove ``room_invite_state_types`` and configure ``additional_event_types`` with +your customisations. + +If you have customised this value by removing state types, you should rename +``room_invite_state_types`` to ``additional_event_types``, and set +``disable_default_event_types`` to ``true``. + Upgrading to v1.33.0 ==================== diff --git a/changelog.d/9915.feature b/changelog.d/9915.feature index 832916cb01..7b81faabea 100644 --- a/changelog.d/9915.feature +++ b/changelog.d/9915.feature @@ -1 +1 @@ -Support stable identifiers from [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772). +Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see `UPGRADE.rst` if you have customised `room_invite_state_types` in your configuration. \ No newline at end of file diff --git a/changelog.d/9966.feature b/changelog.d/9966.feature new file mode 100644 index 0000000000..7b81faabea --- /dev/null +++ b/changelog.d/9966.feature @@ -0,0 +1 @@ +Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see `UPGRADE.rst` if you have customised `room_invite_state_types` in your configuration. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 7cf222d356..67ad57b1aa 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1521,6 +1521,7 @@ room_prejoin_state: # - m.room.avatar # - m.room.encryption # - m.room.name + # - m.room.create # # Uncomment the following to disable these defaults (so that only the event # types listed in 'additional_event_types' are shared). Defaults to 'false'. diff --git a/synapse/config/api.py b/synapse/config/api.py index 55c038c0c4..b18044f982 100644 --- a/synapse/config/api.py +++ b/synapse/config/api.py @@ -88,10 +88,6 @@ def _get_prejoin_state_types(self, config: JsonDict) -> Iterable[str]: if not room_prejoin_state_config.get("disable_default_event_types"): yield from _DEFAULT_PREJOIN_STATE_TYPES - if self.spaces_enabled: - # MSC1772 suggests adding m.room.create to the prejoin state - yield EventTypes.Create - yield from room_prejoin_state_config.get("additional_event_types", []) @@ -109,6 +105,8 @@ def _get_prejoin_state_types(self, config: JsonDict) -> Iterable[str]: EventTypes.RoomAvatar, EventTypes.RoomEncryption, EventTypes.Name, + # Per MSC1772. + EventTypes.Create, ] From f4833e0c062e189fcfd8186fc197d1fc1052814a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 11 May 2021 12:21:43 -0400 Subject: [PATCH 144/619] Support fetching the spaces summary via GET over federation. (#9947) Per changes in MSC2946, the C-S and S-S APIs for spaces summary should use GET requests. Until this is stable, the POST endpoints still exist. This does not switch federation requests to use the GET version yet since it is newly added and already deployed servers might not support it. When switching to the stable endpoint we should switch to GET requests. --- changelog.d/9947.feature | 1 + synapse/federation/transport/client.py | 1 + synapse/federation/transport/server.py | 26 ++++++++++++++++++++++++++ synapse/rest/client/v1/room.py | 1 + 4 files changed, 29 insertions(+) create mode 100644 changelog.d/9947.feature diff --git a/changelog.d/9947.feature b/changelog.d/9947.feature new file mode 100644 index 0000000000..ce8874f810 --- /dev/null +++ b/changelog.d/9947.feature @@ -0,0 +1 @@ +Update support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary. diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index ada322a81e..497848a2b7 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -995,6 +995,7 @@ async def get_space_summary( returned per space exclude_rooms: a list of any rooms we can skip """ + # TODO When switching to the stable endpoint, use GET instead of POST. path = _create_path( FEDERATION_UNSTABLE_PREFIX, "/org.matrix.msc2946/spaces/%s", room_id ) diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index a3759bdda1..e1b7462474 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1376,6 +1376,32 @@ class FederationSpaceSummaryServlet(BaseFederationServlet): PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" PATH = "/spaces/(?P[^/]*)" + async def on_GET( + self, + origin: str, + content: JsonDict, + query: Mapping[bytes, Sequence[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) + max_rooms_per_space = parse_integer_from_args(query, "max_rooms_per_space") + + exclude_rooms = [] + if b"exclude_rooms" in query: + try: + exclude_rooms = [ + room_id.decode("ascii") for room_id in query[b"exclude_rooms"] + ] + except Exception: + raise SynapseError( + 400, "Bad query parameter for exclude_rooms", Codes.INVALID_PARAM + ) + + return 200, await self.handler.federation_space_summary( + room_id, suggested_only, max_rooms_per_space, exclude_rooms + ) + + # TODO When switching to the stable endpoint, remove the POST handler. async def on_POST( self, origin: str, diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 5cab4d3c7b..51813cccbe 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -1020,6 +1020,7 @@ async def on_GET( max_rooms_per_space=parse_integer(request, "max_rooms_per_space"), ) + # TODO When switching to the stable endpoint, remove the POST handler. async def on_POST( self, request: SynapseRequest, room_id: str ) -> Tuple[int, JsonDict]: From 27c375f812725bead5fa41a55aacf316f6e2376c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 11 May 2021 12:57:39 -0400 Subject: [PATCH 145/619] Sort child events according to MSC1772 for the spaces summary API. (#9954) This should help ensure that equivalent results are achieved between homeservers querying for the summary of a space. This implements modified MSC1772 rules, according to MSC2946. The different is that the origin_server_ts of the m.room.create event is not used as a tie-breaker since this might not be known if the homeserver is not part of the room. --- changelog.d/9954.feature | 1 + synapse/handlers/space_summary.py | 71 +++++++++++++++++++++++- tests/handlers/test_space_summary.py | 81 ++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9954.feature create mode 100644 tests/handlers/test_space_summary.py diff --git a/changelog.d/9954.feature b/changelog.d/9954.feature new file mode 100644 index 0000000000..ce8874f810 --- /dev/null +++ b/changelog.d/9954.feature @@ -0,0 +1 @@ +Update support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 2e997841f1..e35d91832b 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -14,6 +14,7 @@ import itertools import logging +import re from collections import deque from typing import TYPE_CHECKING, Iterable, List, Optional, Sequence, Set, Tuple, cast @@ -226,6 +227,23 @@ async def _summarize_local_room( suggested_only: bool, max_children: Optional[int], ) -> Tuple[Sequence[JsonDict], Sequence[JsonDict]]: + """ + Generate a room entry and a list of event entries for a given room. + + Args: + requester: The requesting user, or None if this is over federation. + room_id: The room ID to summarize. + suggested_only: True if only suggested children should be returned. + Otherwise, all children are returned. + max_children: The maximum number of children to return for this node. + + Returns: + A tuple of: + An iterable of a single value of the room. + + An iterable of the sorted children events. This may be limited + to a maximum size or may include all children. + """ if not await self._is_room_accessible(room_id, requester): return (), () @@ -357,6 +375,18 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: return room_entry async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: + """ + Get the child events for a given room. + + The returned results are sorted for stability. + + Args: + room_id: The room id to get the children of. + + Returns: + An iterable of sorted child events. + """ + # look for child rooms/spaces. current_state_ids = await self._store.get_current_state_ids(room_id) @@ -370,8 +400,9 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: ] ) - # filter out any events without a "via" (which implies it has been redacted) - return (e for e in events if _has_valid_via(e)) + # filter out any events without a "via" (which implies it has been redacted), + # and order to ensure we return stable results. + return sorted(filter(_has_valid_via, events), key=_child_events_comparison_key) @attr.s(frozen=True, slots=True) @@ -397,3 +428,39 @@ def _is_suggested_child_event(edge_event: EventBase) -> bool: return True logger.debug("Ignorning not-suggested child %s", edge_event.state_key) return False + + +# Order may only contain characters in the range of \x20 (space) to \x7F (~). +_INVALID_ORDER_CHARS_RE = re.compile(r"[^\x20-\x7F]") + + +def _child_events_comparison_key(child: EventBase) -> Tuple[bool, Optional[str], str]: + """ + Generate a value for comparing two child events for ordering. + + The rules for ordering are supposed to be: + + 1. The 'order' key, if it is valid. + 2. The 'origin_server_ts' of the 'm.room.create' event. + 3. The 'room_id'. + + But we skip step 2 since we may not have any state from the room. + + Args: + child: The event for generating a comparison key. + + Returns: + The comparison key as a tuple of: + False if the ordering is valid. + The ordering field. + The room ID. + """ + order = child.content.get("order") + # If order is not a string or doesn't meet the requirements, ignore it. + if not isinstance(order, str): + order = None + elif len(order) > 50 or _INVALID_ORDER_CHARS_RE.search(order): + order = None + + # Items without an order come last. + return (order is None, order, child.room_id) diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py new file mode 100644 index 0000000000..2c5e81531b --- /dev/null +++ b/tests/handlers/test_space_summary.py @@ -0,0 +1,81 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Optional +from unittest import mock + +from synapse.handlers.space_summary import _child_events_comparison_key + +from tests import unittest + + +def _create_event(room_id: str, order: Optional[Any] = None): + result = mock.Mock() + result.room_id = room_id + result.content = {} + if order is not None: + result.content["order"] = order + return result + + +def _order(*events): + return sorted(events, key=_child_events_comparison_key) + + +class TestSpaceSummarySort(unittest.TestCase): + def test_no_order_last(self): + """An event with no ordering is placed behind those with an ordering.""" + ev1 = _create_event("!abc:test") + ev2 = _create_event("!xyz:test", "xyz") + + self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + def test_order(self): + """The ordering should be used.""" + ev1 = _create_event("!abc:test", "xyz") + ev2 = _create_event("!xyz:test", "abc") + + self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + def test_order_room_id(self): + """Room ID is a tie-breaker for ordering.""" + ev1 = _create_event("!abc:test", "abc") + ev2 = _create_event("!xyz:test", "abc") + + self.assertEqual([ev1, ev2], _order(ev1, ev2)) + + def test_invalid_ordering_type(self): + """Invalid orderings are considered the same as missing.""" + ev1 = _create_event("!abc:test", 1) + ev2 = _create_event("!xyz:test", "xyz") + + self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + ev1 = _create_event("!abc:test", {}) + self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + ev1 = _create_event("!abc:test", []) + self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + ev1 = _create_event("!abc:test", True) + self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + def test_invalid_ordering_value(self): + """Invalid orderings are considered the same as missing.""" + ev1 = _create_event("!abc:test", "foo\n") + ev2 = _create_event("!xyz:test", "xyz") + + self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + ev1 = _create_event("!abc:test", "a" * 51) + self.assertEqual([ev2, ev1], _order(ev1, ev2)) From 63fb220e5f90b63acfa4bdfb61788f2c2d867c86 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 11 May 2021 18:01:11 +0100 Subject: [PATCH 146/619] Tests for to-device messages (#9965) --- changelog.d/9965.bugfix | 1 + .../rest/client/v2_alpha/test_sendtodevice.py | 201 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 changelog.d/9965.bugfix create mode 100644 tests/rest/client/v2_alpha/test_sendtodevice.py diff --git a/changelog.d/9965.bugfix b/changelog.d/9965.bugfix new file mode 100644 index 0000000000..e26d141a53 --- /dev/null +++ b/changelog.d/9965.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse 1.29.0 which caused `m.room_key_request` to-device messages sent from one user to another to be dropped. diff --git a/tests/rest/client/v2_alpha/test_sendtodevice.py b/tests/rest/client/v2_alpha/test_sendtodevice.py new file mode 100644 index 0000000000..c9c99cc5d7 --- /dev/null +++ b/tests/rest/client/v2_alpha/test_sendtodevice.py @@ -0,0 +1,201 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.rest import admin +from synapse.rest.client.v1 import login +from synapse.rest.client.v2_alpha import sendtodevice, sync + +from tests.unittest import HomeserverTestCase, override_config + + +class SendToDeviceTestCase(HomeserverTestCase): + servlets = [ + admin.register_servlets, + login.register_servlets, + sendtodevice.register_servlets, + sync.register_servlets, + ] + + def test_user_to_user(self): + """A to-device message from one user to another should get delivered""" + + user1 = self.register_user("u1", "pass") + user1_tok = self.login("u1", "pass", "d1") + + user2 = self.register_user("u2", "pass") + user2_tok = self.login("u2", "pass", "d2") + + # send the message + test_msg = {"foo": "bar"} + chan = self.make_request( + "PUT", + "/_matrix/client/r0/sendToDevice/m.test/1234", + content={"messages": {user2: {"d2": test_msg}}}, + access_token=user1_tok, + ) + self.assertEqual(chan.code, 200, chan.result) + + # check it appears + channel = self.make_request("GET", "/sync", access_token=user2_tok) + self.assertEqual(channel.code, 200, channel.result) + expected_result = { + "events": [ + { + "sender": user1, + "type": "m.test", + "content": test_msg, + } + ] + } + self.assertEqual(channel.json_body["to_device"], expected_result) + + # it should re-appear if we do another sync + channel = self.make_request("GET", "/sync", access_token=user2_tok) + self.assertEqual(channel.code, 200, channel.result) + self.assertEqual(channel.json_body["to_device"], expected_result) + + # it should *not* appear if we do an incremental sync + sync_token = channel.json_body["next_batch"] + channel = self.make_request( + "GET", f"/sync?since={sync_token}", access_token=user2_tok + ) + self.assertEqual(channel.code, 200, channel.result) + self.assertEqual(channel.json_body.get("to_device", {}).get("events", []), []) + + @override_config({"rc_key_requests": {"per_second": 10, "burst_count": 2}}) + def test_local_room_key_request(self): + """m.room_key_request has special-casing; test from local user""" + user1 = self.register_user("u1", "pass") + user1_tok = self.login("u1", "pass", "d1") + + user2 = self.register_user("u2", "pass") + user2_tok = self.login("u2", "pass", "d2") + + # send three messages + for i in range(3): + chan = self.make_request( + "PUT", + f"/_matrix/client/r0/sendToDevice/m.room_key_request/{i}", + content={"messages": {user2: {"d2": {"idx": i}}}}, + access_token=user1_tok, + ) + self.assertEqual(chan.code, 200, chan.result) + + # now sync: we should get two of the three + channel = self.make_request("GET", "/sync", access_token=user2_tok) + self.assertEqual(channel.code, 200, channel.result) + msgs = channel.json_body["to_device"]["events"] + self.assertEqual(len(msgs), 2) + for i in range(2): + self.assertEqual( + msgs[i], + {"sender": user1, "type": "m.room_key_request", "content": {"idx": i}}, + ) + sync_token = channel.json_body["next_batch"] + + # ... time passes + self.reactor.advance(1) + + # and we can send more messages + chan = self.make_request( + "PUT", + "/_matrix/client/r0/sendToDevice/m.room_key_request/3", + content={"messages": {user2: {"d2": {"idx": 3}}}}, + access_token=user1_tok, + ) + self.assertEqual(chan.code, 200, chan.result) + + # ... which should arrive + channel = self.make_request( + "GET", f"/sync?since={sync_token}", access_token=user2_tok + ) + self.assertEqual(channel.code, 200, channel.result) + msgs = channel.json_body["to_device"]["events"] + self.assertEqual(len(msgs), 1) + self.assertEqual( + msgs[0], + {"sender": user1, "type": "m.room_key_request", "content": {"idx": 3}}, + ) + + @override_config({"rc_key_requests": {"per_second": 10, "burst_count": 2}}) + def test_remote_room_key_request(self): + """m.room_key_request has special-casing; test from remote user""" + user2 = self.register_user("u2", "pass") + user2_tok = self.login("u2", "pass", "d2") + + federation_registry = self.hs.get_federation_registry() + + # send three messages + for i in range(3): + self.get_success( + federation_registry.on_edu( + "m.direct_to_device", + "remote_server", + { + "sender": "@user:remote_server", + "type": "m.room_key_request", + "messages": {user2: {"d2": {"idx": i}}}, + "message_id": f"{i}", + }, + ) + ) + + # now sync: we should get two of the three + channel = self.make_request("GET", "/sync", access_token=user2_tok) + self.assertEqual(channel.code, 200, channel.result) + msgs = channel.json_body["to_device"]["events"] + self.assertEqual(len(msgs), 2) + for i in range(2): + self.assertEqual( + msgs[i], + { + "sender": "@user:remote_server", + "type": "m.room_key_request", + "content": {"idx": i}, + }, + ) + sync_token = channel.json_body["next_batch"] + + # ... time passes + self.reactor.advance(1) + + # and we can send more messages + self.get_success( + federation_registry.on_edu( + "m.direct_to_device", + "remote_server", + { + "sender": "@user:remote_server", + "type": "m.room_key_request", + "messages": {user2: {"d2": {"idx": 3}}}, + "message_id": "3", + }, + ) + ) + + # ... which should arrive + channel = self.make_request( + "GET", f"/sync?since={sync_token}", access_token=user2_tok + ) + self.assertEqual(channel.code, 200, channel.result) + msgs = channel.json_body["to_device"]["events"] + self.assertEqual(len(msgs), 1) + self.assertEqual( + msgs[0], + { + "sender": "@user:remote_server", + "type": "m.room_key_request", + "content": {"idx": 3}, + }, + ) From affaffb0abc3993501ec024e00c286da85e121e9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 12 May 2021 13:17:11 +0100 Subject: [PATCH 147/619] Run cache_joined_hosts_for_event in background (#9951) --- changelog.d/9951.feature | 1 + synapse/handlers/message.py | 45 ++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 changelog.d/9951.feature diff --git a/changelog.d/9951.feature b/changelog.d/9951.feature new file mode 100644 index 0000000000..96a0e7f09f --- /dev/null +++ b/changelog.d/9951.feature @@ -0,0 +1 @@ +Improve performance of sending events for worker-based deployments using Redis. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 5afb7fc261..9f365eb5ad 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -19,6 +19,7 @@ from canonicaljson import encode_canonical_json +from twisted.internet import defer from twisted.internet.interfaces import IDelayedCall from synapse import event_auth @@ -43,14 +44,14 @@ from synapse.events.builder import EventBuilder from synapse.events.snapshot import EventContext from synapse.events.validator import EventValidator -from synapse.logging.context import run_in_background +from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.metrics.background_process_metrics import run_as_background_process from synapse.replication.http.send_event import ReplicationSendEventRestServlet from synapse.storage.databases.main.events_worker import EventRedactBehaviour from synapse.storage.state import StateFilter from synapse.types import Requester, RoomAlias, StreamToken, UserID, create_requester -from synapse.util import json_decoder, json_encoder -from synapse.util.async_helpers import Linearizer +from synapse.util import json_decoder, json_encoder, log_failure +from synapse.util.async_helpers import Linearizer, unwrapFirstError from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.metrics import measure_func from synapse.visibility import filter_events_for_client @@ -979,9 +980,43 @@ async def handle_new_client_event( logger.exception("Failed to encode content: %r", event.content) raise - await self.action_generator.handle_push_actions_for_event(event, context) + # We now persist the event (and update the cache in parallel, since we + # don't want to block on it). + result = await make_deferred_yieldable( + defer.gatherResults( + [ + run_in_background( + self._persist_event, + requester=requester, + event=event, + context=context, + ratelimit=ratelimit, + extra_users=extra_users, + ), + run_in_background( + self.cache_joined_hosts_for_event, event, context + ).addErrback(log_failure, "cache_joined_hosts_for_event failed"), + ], + consumeErrors=True, + ) + ).addErrback(unwrapFirstError) + + return result[0] + + async def _persist_event( + self, + requester: Requester, + event: EventBase, + context: EventContext, + ratelimit: bool = True, + extra_users: Optional[List[UserID]] = None, + ) -> EventBase: + """Actually persists the event. Should only be called by + `handle_new_client_event`, and see its docstring for documentation of + the arguments. + """ - await self.cache_joined_hosts_for_event(event, context) + await self.action_generator.handle_push_actions_for_event(event, context) try: # If we're a worker we need to hit out to the master. From 7562d887e159f404c8d752271310f4432f246656 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 12 May 2021 15:04:51 +0100 Subject: [PATCH 148/619] Change the format of access tokens away from macaroons (#5588) --- changelog.d/5588.misc | 1 + scripts-dev/dump_macaroon.py | 2 +- synapse/handlers/auth.py | 28 +++++++++++---- synapse/handlers/register.py | 4 +-- synapse/util/stringutils.py | 20 +++++++++++ tests/api/test_auth.py | 63 --------------------------------- tests/handlers/test_auth.py | 43 +++++++++++----------- tests/handlers/test_register.py | 12 +++---- tests/util/test_stringutils.py | 8 ++++- 9 files changed, 78 insertions(+), 103 deletions(-) create mode 100644 changelog.d/5588.misc diff --git a/changelog.d/5588.misc b/changelog.d/5588.misc new file mode 100644 index 0000000000..b8f52a212c --- /dev/null +++ b/changelog.d/5588.misc @@ -0,0 +1 @@ +Reduce the length of Synapse's access tokens. diff --git a/scripts-dev/dump_macaroon.py b/scripts-dev/dump_macaroon.py index 980b5e709f..0ca75d3fe1 100755 --- a/scripts-dev/dump_macaroon.py +++ b/scripts-dev/dump_macaroon.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python import sys diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 36f2450e2e..8a6666a4ad 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -17,6 +17,7 @@ import time import unicodedata import urllib.parse +from binascii import crc32 from typing import ( TYPE_CHECKING, Any, @@ -34,6 +35,7 @@ import attr import bcrypt import pymacaroons +import unpaddedbase64 from twisted.web.server import Request @@ -66,6 +68,7 @@ from synapse.util.async_helpers import maybe_awaitable from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry from synapse.util.msisdn import phone_number_to_msisdn +from synapse.util.stringutils import base62_encode from synapse.util.threepids import canonicalise_email if TYPE_CHECKING: @@ -808,10 +811,12 @@ async def get_access_token_for_user_id( logger.info( "Logging in user %s as %s%s", user_id, puppets_user_id, fmt_expiry ) + target_user_id_obj = UserID.from_string(puppets_user_id) else: logger.info( "Logging in user %s on device %s%s", user_id, device_id, fmt_expiry ) + target_user_id_obj = UserID.from_string(user_id) if ( not is_appservice_ghost @@ -819,7 +824,7 @@ async def get_access_token_for_user_id( ): await self.auth.check_auth_blocking(user_id) - access_token = self.macaroon_gen.generate_access_token(user_id) + access_token = self.generate_access_token(target_user_id_obj) await self.store.add_access_token_to_user( user_id=user_id, token=access_token, @@ -1192,6 +1197,19 @@ async def _check_local_password(self, user_id: str, password: str) -> Optional[s return None return user_id + def generate_access_token(self, for_user: UserID) -> str: + """Generates an opaque string, for use as an access token""" + + # we use the following format for access tokens: + # syt___ + + b64local = unpaddedbase64.encode_base64(for_user.localpart.encode("utf-8")) + random_string = stringutils.random_string(20) + base = f"syt_{b64local}_{random_string}" + + crc = base62_encode(crc32(base.encode("ascii")), minwidth=6) + return f"{base}_{crc}" + async def validate_short_term_login_token( self, login_token: str ) -> LoginTokenAttributes: @@ -1585,10 +1603,7 @@ class MacaroonGenerator: hs = attr.ib() - def generate_access_token( - self, user_id: str, extra_caveats: Optional[List[str]] = None - ) -> str: - extra_caveats = extra_caveats or [] + def generate_guest_access_token(self, user_id: str) -> str: macaroon = self._generate_base_macaroon(user_id) macaroon.add_first_party_caveat("type = access") # Include a nonce, to make sure that each login gets a different @@ -1596,8 +1611,7 @@ def generate_access_token( macaroon.add_first_party_caveat( "nonce = %s" % (stringutils.random_string_with_symbols(16),) ) - for caveat in extra_caveats: - macaroon.add_first_party_caveat(caveat) + macaroon.add_first_party_caveat("guest = true") return macaroon.serialize() def generate_short_term_login_token( diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 007fb12840..4ceef3fab3 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -722,9 +722,7 @@ class and RegisterDeviceReplicationServlet. ) if is_guest: assert valid_until_ms is None - access_token = self.macaroon_gen.generate_access_token( - user_id, ["guest = true"] - ) + access_token = self.macaroon_gen.generate_guest_access_token(user_id) else: access_token = await self._auth_handler.get_access_token_for_user_id( user_id, diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index cd82777f80..4f25cd1d26 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -220,3 +220,23 @@ def strtobool(val: str) -> bool: return False else: raise ValueError("invalid truth value %r" % (val,)) + + +_BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + +def base62_encode(num: int, minwidth: int = 1) -> str: + """Encode a number using base62 + + Args: + num: number to be encoded + minwidth: width to pad to, if the number is small + """ + res = "" + while num: + num, rem = divmod(num, 62) + res = _BASE62[rem] + res + + # pad to minimum width + pad = "0" * (minwidth - len(res)) + return pad + res diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index c0ed64f784..1b0a815757 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -21,13 +21,11 @@ from synapse.api.errors import ( AuthError, Codes, - InvalidClientCredentialsError, InvalidClientTokenError, MissingClientTokenError, ResourceLimitError, ) from synapse.storage.databases.main.registration import TokenLookupResult -from synapse.types import UserID from tests import unittest from tests.test_utils import simple_async_mock @@ -253,67 +251,6 @@ def test_get_guest_user_from_macaroon(self): self.assertTrue(user_info.is_guest) self.store.get_user_by_id.assert_called_with(user_id) - def test_cannot_use_regular_token_as_guest(self): - USER_ID = "@percy:matrix.org" - self.store.add_access_token_to_user = simple_async_mock(None) - self.store.get_device = simple_async_mock(None) - - token = self.get_success( - self.hs.get_auth_handler().get_access_token_for_user_id( - USER_ID, "DEVICE", valid_until_ms=None - ) - ) - self.store.add_access_token_to_user.assert_called_with( - user_id=USER_ID, - token=token, - device_id="DEVICE", - valid_until_ms=None, - puppets_user_id=None, - ) - - async def get_user(tok): - if token != tok: - return None - return TokenLookupResult( - user_id=USER_ID, - is_guest=False, - token_id=1234, - device_id="DEVICE", - ) - - self.store.get_user_by_access_token = get_user - self.store.get_user_by_id = simple_async_mock({"is_guest": False}) - - # check the token works - request = Mock(args={}) - request.args[b"access_token"] = [token.encode("ascii")] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - requester = self.get_success( - self.auth.get_user_by_req(request, allow_guest=True) - ) - self.assertEqual(UserID.from_string(USER_ID), requester.user) - self.assertFalse(requester.is_guest) - - # add an is_guest caveat - mac = pymacaroons.Macaroon.deserialize(token) - mac.add_first_party_caveat("guest = true") - guest_tok = mac.serialize() - - # the token should *not* work now - request = Mock(args={}) - request.args[b"access_token"] = [guest_tok.encode("ascii")] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - - cm = self.get_failure( - self.auth.get_user_by_req(request, allow_guest=True), - InvalidClientCredentialsError, - ) - - self.assertEqual(401, cm.value.code) - self.assertEqual("Guest access token used for regular user", cm.value.msg) - - self.store.get_user_by_id.assert_called_with(USER_ID) - def test_blocking_mau(self): self.auth_blocking._limit_usage_by_mau = False self.auth_blocking._max_mau_value = 50 diff --git a/tests/handlers/test_auth.py b/tests/handlers/test_auth.py index fe7e9484fd..5f3350e490 100644 --- a/tests/handlers/test_auth.py +++ b/tests/handlers/test_auth.py @@ -16,12 +16,17 @@ import pymacaroons from synapse.api.errors import AuthError, ResourceLimitError +from synapse.rest import admin from tests import unittest from tests.test_utils import make_awaitable class AuthTestCase(unittest.HomeserverTestCase): + servlets = [ + admin.register_servlets, + ] + def prepare(self, reactor, clock, hs): self.auth_handler = hs.get_auth_handler() self.macaroon_generator = hs.get_macaroon_generator() @@ -35,16 +40,10 @@ def prepare(self, reactor, clock, hs): self.small_number_of_users = 1 self.large_number_of_users = 100 - def test_token_is_a_macaroon(self): - token = self.macaroon_generator.generate_access_token("some_user") - # Check that we can parse the thing with pymacaroons - macaroon = pymacaroons.Macaroon.deserialize(token) - # The most basic of sanity checks - if "some_user" not in macaroon.inspect(): - self.fail("some_user was not in %s" % macaroon.inspect()) + self.user1 = self.register_user("a_user", "pass") def test_macaroon_caveats(self): - token = self.macaroon_generator.generate_access_token("a_user") + token = self.macaroon_generator.generate_guest_access_token("a_user") macaroon = pymacaroons.Macaroon.deserialize(token) def verify_gen(caveat): @@ -59,19 +58,23 @@ def verify_type(caveat): def verify_nonce(caveat): return caveat.startswith("nonce =") + def verify_guest(caveat): + return caveat == "guest = true" + v = pymacaroons.Verifier() v.satisfy_general(verify_gen) v.satisfy_general(verify_user) v.satisfy_general(verify_type) v.satisfy_general(verify_nonce) + v.satisfy_general(verify_guest) v.verify(macaroon, self.hs.config.macaroon_secret_key) def test_short_term_login_token_gives_user_id(self): token = self.macaroon_generator.generate_short_term_login_token( - "a_user", "", 5000 + self.user1, "", 5000 ) res = self.get_success(self.auth_handler.validate_short_term_login_token(token)) - self.assertEqual("a_user", res.user_id) + self.assertEqual(self.user1, res.user_id) self.assertEqual("", res.auth_provider_id) # when we advance the clock, the token should be rejected @@ -83,22 +86,22 @@ def test_short_term_login_token_gives_user_id(self): def test_short_term_login_token_gives_auth_provider(self): token = self.macaroon_generator.generate_short_term_login_token( - "a_user", auth_provider_id="my_idp" + self.user1, auth_provider_id="my_idp" ) res = self.get_success(self.auth_handler.validate_short_term_login_token(token)) - self.assertEqual("a_user", res.user_id) + self.assertEqual(self.user1, res.user_id) self.assertEqual("my_idp", res.auth_provider_id) def test_short_term_login_token_cannot_replace_user_id(self): token = self.macaroon_generator.generate_short_term_login_token( - "a_user", "", 5000 + self.user1, "", 5000 ) macaroon = pymacaroons.Macaroon.deserialize(token) res = self.get_success( self.auth_handler.validate_short_term_login_token(macaroon.serialize()) ) - self.assertEqual("a_user", res.user_id) + self.assertEqual(self.user1, res.user_id) # add another "user_id" caveat, which might allow us to override the # user_id. @@ -114,7 +117,7 @@ def test_mau_limits_disabled(self): # Ensure does not throw exception self.get_success( self.auth_handler.get_access_token_for_user_id( - "user_a", device_id=None, valid_until_ms=None + self.user1, device_id=None, valid_until_ms=None ) ) @@ -132,7 +135,7 @@ def test_mau_limits_exceeded_large(self): self.get_failure( self.auth_handler.get_access_token_for_user_id( - "user_a", device_id=None, valid_until_ms=None + self.user1, device_id=None, valid_until_ms=None ), ResourceLimitError, ) @@ -160,7 +163,7 @@ def test_mau_limits_parity(self): # If not in monthly active cohort self.get_failure( self.auth_handler.get_access_token_for_user_id( - "user_a", device_id=None, valid_until_ms=None + self.user1, device_id=None, valid_until_ms=None ), ResourceLimitError, ) @@ -177,7 +180,7 @@ def test_mau_limits_parity(self): ) self.get_success( self.auth_handler.get_access_token_for_user_id( - "user_a", device_id=None, valid_until_ms=None + self.user1, device_id=None, valid_until_ms=None ) ) self.get_success( @@ -195,7 +198,7 @@ def test_mau_limits_not_exceeded(self): # Ensure does not raise exception self.get_success( self.auth_handler.get_access_token_for_user_id( - "user_a", device_id=None, valid_until_ms=None + self.user1, device_id=None, valid_until_ms=None ) ) @@ -210,6 +213,6 @@ def test_mau_limits_not_exceeded(self): def _get_macaroon(self): token = self.macaroon_generator.generate_short_term_login_token( - "user_a", "", 5000 + self.user1, "", 5000 ) return pymacaroons.Macaroon.deserialize(token) diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index 608f8f3d33..bd43190523 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -48,10 +48,6 @@ def prepare(self, reactor, clock, hs): self.mock_distributor = Mock() self.mock_distributor.declare("registered_user") self.mock_captcha_client = Mock() - self.macaroon_generator = Mock( - generate_access_token=Mock(return_value="secret") - ) - self.hs.get_macaroon_generator = Mock(return_value=self.macaroon_generator) self.handler = self.hs.get_registration_handler() self.store = self.hs.get_datastore() self.lots_of_users = 100 @@ -67,8 +63,8 @@ def test_user_is_created_and_logged_in_if_doesnt_exist(self): self.get_or_create_user(requester, frank.localpart, "Frankie") ) self.assertEquals(result_user_id, user_id) - self.assertTrue(result_token is not None) - self.assertEquals(result_token, "secret") + self.assertIsInstance(result_token, str) + self.assertGreater(len(result_token), 20) def test_if_user_exists(self): store = self.hs.get_datastore() @@ -500,7 +496,7 @@ def check_registration_for_spam( user_id = self.get_success(self.handler.register_user(localpart="user")) # Get an access token. - token = self.macaroon_generator.generate_access_token(user_id) + token = "testtok" self.get_success( self.store.add_access_token_to_user( user_id=user_id, token=token, device_id=None, valid_until_ms=None @@ -577,7 +573,7 @@ async def get_or_create_user( user = UserID(localpart, self.hs.hostname) user_id = user.to_string() - token = self.macaroon_generator.generate_access_token(user_id) + token = self.hs.get_auth_handler().generate_access_token(user) if need_register: await self.handler.register_with_store( diff --git a/tests/util/test_stringutils.py b/tests/util/test_stringutils.py index f7fecd9cf3..ad4dd7f007 100644 --- a/tests/util/test_stringutils.py +++ b/tests/util/test_stringutils.py @@ -13,7 +13,7 @@ # limitations under the License. from synapse.api.errors import SynapseError -from synapse.util.stringutils import assert_valid_client_secret +from synapse.util.stringutils import assert_valid_client_secret, base62_encode from .. import unittest @@ -45,3 +45,9 @@ def test_client_secret_regex(self): for client_secret in bad: with self.assertRaises(SynapseError): assert_valid_client_secret(client_secret) + + def test_base62_encode(self): + self.assertEqual("0", base62_encode(0)) + self.assertEqual("10", base62_encode(62)) + self.assertEqual("1c", base62_encode(100)) + self.assertEqual("001c", base62_encode(100, minwidth=4)) From a683028d81606708f686b890c0a44f5a20b54798 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 May 2021 16:05:28 +0200 Subject: [PATCH 149/619] Correctly ratelimit invites when creating a room (#9968) * Correctly ratelimit invites when creating a room Also allow ratelimiting for more than one action at a time. --- changelog.d/9968.bugfix | 1 + synapse/api/ratelimiting.py | 22 +++++++++--- synapse/handlers/room.py | 27 ++++++++++---- synapse/handlers/room_member.py | 25 +++++++++++++ tests/api/test_ratelimiting.py | 57 ++++++++++++++++++++++++++++++ tests/rest/client/v1/test_rooms.py | 37 +++++++++++++++++++ 6 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 changelog.d/9968.bugfix diff --git a/changelog.d/9968.bugfix b/changelog.d/9968.bugfix new file mode 100644 index 0000000000..39e75f9956 --- /dev/null +++ b/changelog.d/9968.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.27.0 preventing users and appservices exempt from ratelimiting from creating rooms with many invitees. diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py index 2244b8a340..b9a10283f4 100644 --- a/synapse/api/ratelimiting.py +++ b/synapse/api/ratelimiting.py @@ -57,6 +57,7 @@ async def can_do_action( rate_hz: Optional[float] = None, burst_count: Optional[int] = None, update: bool = True, + n_actions: int = 1, _time_now_s: Optional[int] = None, ) -> Tuple[bool, float]: """Can the entity (e.g. user or IP address) perform the action? @@ -76,6 +77,9 @@ async def can_do_action( burst_count: How many actions that can be performed before being limited. Overrides the value set during instantiation if set. update: Whether to count this check as performing the action + n_actions: The number of times the user wants to do this action. If the user + cannot do all of the actions, the user's action count is not incremented + at all. _time_now_s: The current time. Optional, defaults to the current time according to self.clock. Only used by tests. @@ -124,17 +128,20 @@ async def can_do_action( time_delta = time_now_s - time_start performed_count = action_count - time_delta * rate_hz if performed_count < 0: - # Allow, reset back to count 1 - allowed = True + performed_count = 0 time_start = time_now_s - action_count = 1.0 - elif performed_count > burst_count - 1.0: + + # This check would be easier read as performed_count + n_actions > burst_count, + # but performed_count might be a very precise float (with lots of numbers + # following the point) in which case Python might round it up when adding it to + # n_actions. Writing it this way ensures it doesn't happen. + if performed_count > burst_count - n_actions: # Deny, we have exceeded our burst count allowed = False else: # We haven't reached our limit yet allowed = True - action_count += 1.0 + action_count = performed_count + n_actions if update: self.actions[key] = (action_count, time_start, rate_hz) @@ -182,6 +189,7 @@ async def ratelimit( rate_hz: Optional[float] = None, burst_count: Optional[int] = None, update: bool = True, + n_actions: int = 1, _time_now_s: Optional[int] = None, ): """Checks if an action can be performed. If not, raises a LimitExceededError @@ -201,6 +209,9 @@ async def ratelimit( burst_count: How many actions that can be performed before being limited. Overrides the value set during instantiation if set. update: Whether to count this check as performing the action + n_actions: The number of times the user wants to do this action. If the user + cannot do all of the actions, the user's action count is not incremented + at all. _time_now_s: The current time. Optional, defaults to the current time according to self.clock. Only used by tests. @@ -216,6 +227,7 @@ async def ratelimit( rate_hz=rate_hz, burst_count=burst_count, update=update, + n_actions=n_actions, _time_now_s=time_now_s, ) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index fb4823a5cc..835d874cee 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -32,7 +32,14 @@ RoomCreationPreset, RoomEncryptionAlgorithms, ) -from synapse.api.errors import AuthError, Codes, NotFoundError, StoreError, SynapseError +from synapse.api.errors import ( + AuthError, + Codes, + LimitExceededError, + NotFoundError, + StoreError, + SynapseError, +) from synapse.api.filtering import Filter from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.events import EventBase @@ -126,10 +133,6 @@ def __init__(self, hs: "HomeServer"): self.third_party_event_rules = hs.get_third_party_event_rules() - self._invite_burst_count = ( - hs.config.ratelimiting.rc_invites_per_room.burst_count - ) - async def upgrade_room( self, requester: Requester, old_room_id: str, new_version: RoomVersion ) -> str: @@ -676,8 +679,18 @@ async def create_room( invite_3pid_list = [] invite_list = [] - if len(invite_list) + len(invite_3pid_list) > self._invite_burst_count: - raise SynapseError(400, "Cannot invite so many users at once") + if invite_list or invite_3pid_list: + try: + # If there are invites in the request, see if the ratelimiting settings + # allow that number of invites to be sent from the current user. + await self.room_member_handler.ratelimit_multiple_invites( + requester, + room_id=None, + n_invites=len(invite_list) + len(invite_3pid_list), + update=False, + ) + except LimitExceededError: + raise SynapseError(400, "Cannot invite so many users at once") await self.event_creation_handler.assert_accepted_privacy_policy(requester) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 20700fc5a8..9a092da715 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -163,6 +163,31 @@ async def _user_left_room(self, target: UserID, room_id: str) -> None: async def forget(self, user: UserID, room_id: str) -> None: raise NotImplementedError() + async def ratelimit_multiple_invites( + self, + requester: Optional[Requester], + room_id: Optional[str], + n_invites: int, + update: bool = True, + ): + """Ratelimit more than one invite sent by the given requester in the given room. + + Args: + requester: The requester sending the invites. + room_id: The room the invites are being sent in. + n_invites: The amount of invites to ratelimit for. + update: Whether to update the ratelimiter's cache. + + Raises: + LimitExceededError: The requester can't send that many invites in the room. + """ + await self._invites_per_room_limiter.ratelimit( + requester, + room_id, + update=update, + n_actions=n_invites, + ) + async def ratelimit_invite( self, requester: Optional[Requester], diff --git a/tests/api/test_ratelimiting.py b/tests/api/test_ratelimiting.py index fa96ba07a5..dcf0110c16 100644 --- a/tests/api/test_ratelimiting.py +++ b/tests/api/test_ratelimiting.py @@ -230,3 +230,60 @@ def test_db_user_override(self): # Shouldn't raise for _ in range(20): self.get_success_or_raise(limiter.ratelimit(requester, _time_now_s=0)) + + def test_multiple_actions(self): + limiter = Ratelimiter( + store=self.hs.get_datastore(), clock=None, rate_hz=0.1, burst_count=3 + ) + # Test that 4 actions aren't allowed with a maximum burst of 3. + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", n_actions=4, _time_now_s=0) + ) + self.assertFalse(allowed) + + # Test that 3 actions are allowed with a maximum burst of 3. + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", n_actions=3, _time_now_s=0) + ) + self.assertTrue(allowed) + self.assertEquals(10.0, time_allowed) + + # Test that, after doing these 3 actions, we can't do any more action without + # waiting. + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", n_actions=1, _time_now_s=0) + ) + self.assertFalse(allowed) + self.assertEquals(10.0, time_allowed) + + # Test that after waiting we can do only 1 action. + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action( + None, + key="test_id", + update=False, + n_actions=1, + _time_now_s=10, + ) + ) + self.assertTrue(allowed) + # The time allowed is the current time because we could still repeat the action + # once. + self.assertEquals(10.0, time_allowed) + + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", n_actions=2, _time_now_s=10) + ) + self.assertFalse(allowed) + # The time allowed doesn't change despite allowed being False because, while we + # don't allow 2 actions, we could still do 1. + self.assertEquals(10.0, time_allowed) + + # Test that after waiting a bit more we can do 2 actions. + allowed, time_allowed = self.get_success_or_raise( + limiter.can_do_action(None, key="test_id", n_actions=2, _time_now_s=20) + ) + self.assertTrue(allowed) + # The time allowed is the current time because we could still repeat the action + # once. + self.assertEquals(20.0, time_allowed) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index a3694f3d02..7c4bdcdfdd 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -463,6 +463,43 @@ def test_post_room_invitees_invalid_mxid(self): ) self.assertEquals(400, channel.code) + @unittest.override_config({"rc_invites": {"per_room": {"burst_count": 3}}}) + def test_post_room_invitees_ratelimit(self): + """Test that invites sent when creating a room are ratelimited by a RateLimiter, + which ratelimits them correctly, including by not limiting when the requester is + exempt from ratelimiting. + """ + + # Build the request's content. We use local MXIDs because invites over federation + # are more difficult to mock. + content = json.dumps( + { + "invite": [ + "@alice1:red", + "@alice2:red", + "@alice3:red", + "@alice4:red", + ] + } + ).encode("utf8") + + # Test that the invites are correctly ratelimited. + channel = self.make_request("POST", "/createRoom", content) + self.assertEqual(400, channel.code) + self.assertEqual( + "Cannot invite so many users at once", + channel.json_body["error"], + ) + + # Add the current user to the ratelimit overrides, allowing them no ratelimiting. + self.get_success( + self.hs.get_datastore().set_ratelimit_for_user(self.user_id, 0, 0) + ) + + # Test that the invites aren't ratelimited anymore. + channel = self.make_request("POST", "/createRoom", content) + self.assertEqual(200, channel.code) + class RoomTopicTestCase(RoomBase): """ Tests /rooms/$room_id/topic REST events. """ From 47806b0869da4adf84a978e4898ec1b4f5985af5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 May 2021 16:59:46 +0100 Subject: [PATCH 150/619] 1.34.0rc1 --- CHANGES.md | 61 ++++++++++++++++++++++++++++++++++++++++ changelog.d/5588.misc | 1 - changelog.d/9881.feature | 1 - changelog.d/9882.misc | 1 - changelog.d/9885.misc | 1 - changelog.d/9886.misc | 1 - changelog.d/9889.feature | 1 - changelog.d/9889.removal | 1 - changelog.d/9895.bugfix | 1 - changelog.d/9896.bugfix | 1 - changelog.d/9896.misc | 1 - changelog.d/9902.feature | 1 - changelog.d/9904.misc | 1 - changelog.d/9905.feature | 1 - changelog.d/9910.bugfix | 1 - changelog.d/9910.feature | 1 - changelog.d/9911.doc | 1 - changelog.d/9913.docker | 1 - changelog.d/9915.feature | 1 - changelog.d/9916.feature | 1 - changelog.d/9928.bugfix | 1 - changelog.d/9930.bugfix | 1 - changelog.d/9931.misc | 1 - changelog.d/9932.misc | 1 - changelog.d/9935.feature | 1 - changelog.d/9945.feature | 1 - changelog.d/9947.feature | 1 - changelog.d/9950.feature | 1 - changelog.d/9951.feature | 1 - changelog.d/9954.feature | 1 - changelog.d/9959.misc | 1 - changelog.d/9961.bugfix | 1 - changelog.d/9965.bugfix | 1 - changelog.d/9966.feature | 1 - changelog.d/9968.bugfix | 1 - synapse/__init__.py | 2 +- 36 files changed, 62 insertions(+), 35 deletions(-) delete mode 100644 changelog.d/5588.misc delete mode 100644 changelog.d/9881.feature delete mode 100644 changelog.d/9882.misc delete mode 100644 changelog.d/9885.misc delete mode 100644 changelog.d/9886.misc delete mode 100644 changelog.d/9889.feature delete mode 100644 changelog.d/9889.removal delete mode 100644 changelog.d/9895.bugfix delete mode 100644 changelog.d/9896.bugfix delete mode 100644 changelog.d/9896.misc delete mode 100644 changelog.d/9902.feature delete mode 100644 changelog.d/9904.misc delete mode 100644 changelog.d/9905.feature delete mode 100644 changelog.d/9910.bugfix delete mode 100644 changelog.d/9910.feature delete mode 100644 changelog.d/9911.doc delete mode 100644 changelog.d/9913.docker delete mode 100644 changelog.d/9915.feature delete mode 100644 changelog.d/9916.feature delete mode 100644 changelog.d/9928.bugfix delete mode 100644 changelog.d/9930.bugfix delete mode 100644 changelog.d/9931.misc delete mode 100644 changelog.d/9932.misc delete mode 100644 changelog.d/9935.feature delete mode 100644 changelog.d/9945.feature delete mode 100644 changelog.d/9947.feature delete mode 100644 changelog.d/9950.feature delete mode 100644 changelog.d/9951.feature delete mode 100644 changelog.d/9954.feature delete mode 100644 changelog.d/9959.misc delete mode 100644 changelog.d/9961.bugfix delete mode 100644 changelog.d/9965.bugfix delete mode 100644 changelog.d/9966.feature delete mode 100644 changelog.d/9968.bugfix diff --git a/CHANGES.md b/CHANGES.md index 93efa3ce56..ddc1f13a31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,64 @@ +Synapse 1.34.0rc1 (2021-05-12) +============================== + +Features +-------- + +- Add experimental option to track memory usage of the caches. ([\#9881](https://github.com/matrix-org/synapse/issues/9881)) +- Add support for `DELETE /_synapse/admin/v1/rooms/`. ([\#9889](https://github.com/matrix-org/synapse/issues/9889)) +- Add limits to how often Synapse will GC, ensuring that large servers do not end up GC thrashing if `gc_thresholds` has not been correctly set. ([\#9902](https://github.com/matrix-org/synapse/issues/9902)) +- Improve performance of sending events for worker-based deployments using Redis. ([\#9905](https://github.com/matrix-org/synapse/issues/9905), [\#9950](https://github.com/matrix-org/synapse/issues/9950), [\#9951](https://github.com/matrix-org/synapse/issues/9951)) +- Improve performance after joining a large room when presence is enabled. ([\#9910](https://github.com/matrix-org/synapse/issues/9910), [\#9916](https://github.com/matrix-org/synapse/issues/9916)) +- Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see `UPGRADE.rst` if you have customised `room_invite_state_types` in your configuration. ([\#9915](https://github.com/matrix-org/synapse/issues/9915), [\#9966](https://github.com/matrix-org/synapse/issues/9966)) +- Improve performance of backfilling in large rooms. ([\#9935](https://github.com/matrix-org/synapse/issues/9935)) +- Add a config option to allow you to prevent device display names from being shared over federation. Contributed by @aaronraimist. ([\#9945](https://github.com/matrix-org/synapse/issues/9945)) +- Update support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary. ([\#9947](https://github.com/matrix-org/synapse/issues/9947), [\#9954](https://github.com/matrix-org/synapse/issues/9954)) + + +Bugfixes +-------- + +- Fix a bug introduced in v1.32.0 where the associated connection was improperly logged for SQL logging statements. ([\#9895](https://github.com/matrix-org/synapse/issues/9895)) +- Correct the type hint for the `user_may_create_room_alias` method of spam checkers. It is provided a `RoomAlias`, not a `str`. ([\#9896](https://github.com/matrix-org/synapse/issues/9896)) +- Fix bug where user directory could get out of sync if room visibility and membership changed in quick succession. ([\#9910](https://github.com/matrix-org/synapse/issues/9910)) +- Include the `origin_server_ts` property in the experimental [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946) support to allow clients to properly sort rooms. ([\#9928](https://github.com/matrix-org/synapse/issues/9928)) +- Fix bugs introduced in v1.23.0 which made the PostgreSQL port script fail when run with a newly-created SQLite database. ([\#9930](https://github.com/matrix-org/synapse/issues/9930)) +- Fix a bug introduced in Synapse 1.29.0 which caused `m.room_key_request` to-device messages sent from one user to another to be dropped. ([\#9961](https://github.com/matrix-org/synapse/issues/9961), [\#9965](https://github.com/matrix-org/synapse/issues/9965)) +- Fix a bug introduced in v1.27.0 preventing users and appservices exempt from ratelimiting from creating rooms with many invitees. ([\#9968](https://github.com/matrix-org/synapse/issues/9968)) + + +Updates to the Docker image +--------------------------- + +- Added startup_delay to docker healthcheck to reduce waiting time for coming online, updated readme for extra options, contributed by @Maquis196. ([\#9913](https://github.com/matrix-org/synapse/issues/9913)) + + +Improved Documentation +---------------------- + +- Add `port` argument to the Postgres database sample config section. ([\#9911](https://github.com/matrix-org/synapse/issues/9911)) + + +Deprecations and Removals +------------------------- + +- Mark as deprecated `POST /_synapse/admin/v1/rooms//delete`. ([\#9889](https://github.com/matrix-org/synapse/issues/9889)) + + +Internal Changes +---------------- + +- Reduce the length of Synapse's access tokens. ([\#5588](https://github.com/matrix-org/synapse/issues/5588)) +- Export jemalloc stats to Prometheus if it is being used. ([\#9882](https://github.com/matrix-org/synapse/issues/9882)) +- Add type hints to presence handler. ([\#9885](https://github.com/matrix-org/synapse/issues/9885)) +- Reduce memory usage of the LRU caches. ([\#9886](https://github.com/matrix-org/synapse/issues/9886)) +- Add type hints to the `synapse.handlers` module. ([\#9896](https://github.com/matrix-org/synapse/issues/9896)) +- Time response time for external cache requests. ([\#9904](https://github.com/matrix-org/synapse/issues/9904)) +- Minor fixes to the `make_full_schema.sh` script. ([\#9931](https://github.com/matrix-org/synapse/issues/9931)) +- Move database schema files into a common directory. ([\#9932](https://github.com/matrix-org/synapse/issues/9932)) +- Add debug logging for lost/delayed to-device messages. ([\#9959](https://github.com/matrix-org/synapse/issues/9959)) + + Synapse 1.33.2 (2021-05-11) =========================== diff --git a/changelog.d/5588.misc b/changelog.d/5588.misc deleted file mode 100644 index b8f52a212c..0000000000 --- a/changelog.d/5588.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce the length of Synapse's access tokens. diff --git a/changelog.d/9881.feature b/changelog.d/9881.feature deleted file mode 100644 index 088a517e02..0000000000 --- a/changelog.d/9881.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental option to track memory usage of the caches. diff --git a/changelog.d/9882.misc b/changelog.d/9882.misc deleted file mode 100644 index facfa31f38..0000000000 --- a/changelog.d/9882.misc +++ /dev/null @@ -1 +0,0 @@ -Export jemalloc stats to Prometheus if it is being used. diff --git a/changelog.d/9885.misc b/changelog.d/9885.misc deleted file mode 100644 index 492fccea46..0000000000 --- a/changelog.d/9885.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to presence handler. diff --git a/changelog.d/9886.misc b/changelog.d/9886.misc deleted file mode 100644 index 8ff869e659..0000000000 --- a/changelog.d/9886.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce memory usage of the LRU caches. diff --git a/changelog.d/9889.feature b/changelog.d/9889.feature deleted file mode 100644 index 74d46f222e..0000000000 --- a/changelog.d/9889.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for `DELETE /_synapse/admin/v1/rooms/`. \ No newline at end of file diff --git a/changelog.d/9889.removal b/changelog.d/9889.removal deleted file mode 100644 index 398b9e129b..0000000000 --- a/changelog.d/9889.removal +++ /dev/null @@ -1 +0,0 @@ -Mark as deprecated `POST /_synapse/admin/v1/rooms//delete`. \ No newline at end of file diff --git a/changelog.d/9895.bugfix b/changelog.d/9895.bugfix deleted file mode 100644 index 1053f975bf..0000000000 --- a/changelog.d/9895.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.32.0 where the associated connection was improperly logged for SQL logging statements. diff --git a/changelog.d/9896.bugfix b/changelog.d/9896.bugfix deleted file mode 100644 index 07a8e87f9f..0000000000 --- a/changelog.d/9896.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correct the type hint for the `user_may_create_room_alias` method of spam checkers. It is provided a `RoomAlias`, not a `str`. diff --git a/changelog.d/9896.misc b/changelog.d/9896.misc deleted file mode 100644 index e41c7d1f02..0000000000 --- a/changelog.d/9896.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to the `synapse.handlers` module. diff --git a/changelog.d/9902.feature b/changelog.d/9902.feature deleted file mode 100644 index 4d9f324d4e..0000000000 --- a/changelog.d/9902.feature +++ /dev/null @@ -1 +0,0 @@ -Add limits to how often Synapse will GC, ensuring that large servers do not end up GC thrashing if `gc_thresholds` has not been correctly set. diff --git a/changelog.d/9904.misc b/changelog.d/9904.misc deleted file mode 100644 index 3db1e625ae..0000000000 --- a/changelog.d/9904.misc +++ /dev/null @@ -1 +0,0 @@ -Time response time for external cache requests. diff --git a/changelog.d/9905.feature b/changelog.d/9905.feature deleted file mode 100644 index 96a0e7f09f..0000000000 --- a/changelog.d/9905.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance of sending events for worker-based deployments using Redis. diff --git a/changelog.d/9910.bugfix b/changelog.d/9910.bugfix deleted file mode 100644 index 06d523fd46..0000000000 --- a/changelog.d/9910.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where user directory could get out of sync if room visibility and membership changed in quick succession. diff --git a/changelog.d/9910.feature b/changelog.d/9910.feature deleted file mode 100644 index 54165cce18..0000000000 --- a/changelog.d/9910.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance after joining a large room when presence is enabled. diff --git a/changelog.d/9911.doc b/changelog.d/9911.doc deleted file mode 100644 index f7fd9f1ba9..0000000000 --- a/changelog.d/9911.doc +++ /dev/null @@ -1 +0,0 @@ -Add `port` argument to the Postgres database sample config section. \ No newline at end of file diff --git a/changelog.d/9913.docker b/changelog.d/9913.docker deleted file mode 100644 index 93835e14cb..0000000000 --- a/changelog.d/9913.docker +++ /dev/null @@ -1 +0,0 @@ -Added startup_delay to docker healthcheck to reduce waiting time for coming online, updated readme for extra options, contributed by @Maquis196. diff --git a/changelog.d/9915.feature b/changelog.d/9915.feature deleted file mode 100644 index 7b81faabea..0000000000 --- a/changelog.d/9915.feature +++ /dev/null @@ -1 +0,0 @@ -Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see `UPGRADE.rst` if you have customised `room_invite_state_types` in your configuration. \ No newline at end of file diff --git a/changelog.d/9916.feature b/changelog.d/9916.feature deleted file mode 100644 index 54165cce18..0000000000 --- a/changelog.d/9916.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance after joining a large room when presence is enabled. diff --git a/changelog.d/9928.bugfix b/changelog.d/9928.bugfix deleted file mode 100644 index 7b74cd9fb6..0000000000 --- a/changelog.d/9928.bugfix +++ /dev/null @@ -1 +0,0 @@ -Include the `origin_server_ts` property in the experimental [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946) support to allow clients to properly sort rooms. diff --git a/changelog.d/9930.bugfix b/changelog.d/9930.bugfix deleted file mode 100644 index 9b22ed4458..0000000000 --- a/changelog.d/9930.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bugs introduced in v1.23.0 which made the PostgreSQL port script fail when run with a newly-created SQLite database. diff --git a/changelog.d/9931.misc b/changelog.d/9931.misc deleted file mode 100644 index 326adc7f3c..0000000000 --- a/changelog.d/9931.misc +++ /dev/null @@ -1 +0,0 @@ -Minor fixes to the `make_full_schema.sh` script. diff --git a/changelog.d/9932.misc b/changelog.d/9932.misc deleted file mode 100644 index 9e16a36173..0000000000 --- a/changelog.d/9932.misc +++ /dev/null @@ -1 +0,0 @@ -Move database schema files into a common directory. diff --git a/changelog.d/9935.feature b/changelog.d/9935.feature deleted file mode 100644 index eeda5bf50e..0000000000 --- a/changelog.d/9935.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance of backfilling in large rooms. diff --git a/changelog.d/9945.feature b/changelog.d/9945.feature deleted file mode 100644 index 84308e8cce..0000000000 --- a/changelog.d/9945.feature +++ /dev/null @@ -1 +0,0 @@ -Add a config option to allow you to prevent device display names from being shared over federation. Contributed by @aaronraimist. diff --git a/changelog.d/9947.feature b/changelog.d/9947.feature deleted file mode 100644 index ce8874f810..0000000000 --- a/changelog.d/9947.feature +++ /dev/null @@ -1 +0,0 @@ -Update support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary. diff --git a/changelog.d/9950.feature b/changelog.d/9950.feature deleted file mode 100644 index 96a0e7f09f..0000000000 --- a/changelog.d/9950.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance of sending events for worker-based deployments using Redis. diff --git a/changelog.d/9951.feature b/changelog.d/9951.feature deleted file mode 100644 index 96a0e7f09f..0000000000 --- a/changelog.d/9951.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance of sending events for worker-based deployments using Redis. diff --git a/changelog.d/9954.feature b/changelog.d/9954.feature deleted file mode 100644 index ce8874f810..0000000000 --- a/changelog.d/9954.feature +++ /dev/null @@ -1 +0,0 @@ -Update support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary. diff --git a/changelog.d/9959.misc b/changelog.d/9959.misc deleted file mode 100644 index 7231f29d79..0000000000 --- a/changelog.d/9959.misc +++ /dev/null @@ -1 +0,0 @@ -Add debug logging for lost/delayed to-device messages. diff --git a/changelog.d/9961.bugfix b/changelog.d/9961.bugfix deleted file mode 100644 index e26d141a53..0000000000 --- a/changelog.d/9961.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse 1.29.0 which caused `m.room_key_request` to-device messages sent from one user to another to be dropped. diff --git a/changelog.d/9965.bugfix b/changelog.d/9965.bugfix deleted file mode 100644 index e26d141a53..0000000000 --- a/changelog.d/9965.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse 1.29.0 which caused `m.room_key_request` to-device messages sent from one user to another to be dropped. diff --git a/changelog.d/9966.feature b/changelog.d/9966.feature deleted file mode 100644 index 7b81faabea..0000000000 --- a/changelog.d/9966.feature +++ /dev/null @@ -1 +0,0 @@ -Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see `UPGRADE.rst` if you have customised `room_invite_state_types` in your configuration. \ No newline at end of file diff --git a/changelog.d/9968.bugfix b/changelog.d/9968.bugfix deleted file mode 100644 index 39e75f9956..0000000000 --- a/changelog.d/9968.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.27.0 preventing users and appservices exempt from ratelimiting from creating rooms with many invitees. diff --git a/synapse/__init__.py b/synapse/__init__.py index ce822ccb04..15d54a1ceb 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.33.2" +__version__ = "1.34.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 91143bb24ee69df71f935fc8062b11508f6c4d76 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 May 2021 17:04:00 +0100 Subject: [PATCH 151/619] Refer and link to the upgrade notes rather than to the file name --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ddc1f13a31..e6c4550339 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,7 +9,7 @@ Features - Add limits to how often Synapse will GC, ensuring that large servers do not end up GC thrashing if `gc_thresholds` has not been correctly set. ([\#9902](https://github.com/matrix-org/synapse/issues/9902)) - Improve performance of sending events for worker-based deployments using Redis. ([\#9905](https://github.com/matrix-org/synapse/issues/9905), [\#9950](https://github.com/matrix-org/synapse/issues/9950), [\#9951](https://github.com/matrix-org/synapse/issues/9951)) - Improve performance after joining a large room when presence is enabled. ([\#9910](https://github.com/matrix-org/synapse/issues/9910), [\#9916](https://github.com/matrix-org/synapse/issues/9916)) -- Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see `UPGRADE.rst` if you have customised `room_invite_state_types` in your configuration. ([\#9915](https://github.com/matrix-org/synapse/issues/9915), [\#9966](https://github.com/matrix-org/synapse/issues/9966)) +- Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see [the upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#upgrading-to-v1340) if you have customised `room_invite_state_types` in your configuration. ([\#9915](https://github.com/matrix-org/synapse/issues/9915), [\#9966](https://github.com/matrix-org/synapse/issues/9966)) - Improve performance of backfilling in large rooms. ([\#9935](https://github.com/matrix-org/synapse/issues/9935)) - Add a config option to allow you to prevent device display names from being shared over federation. Contributed by @aaronraimist. ([\#9945](https://github.com/matrix-org/synapse/issues/9945)) - Update support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary. ([\#9947](https://github.com/matrix-org/synapse/issues/9947), [\#9954](https://github.com/matrix-org/synapse/issues/9954)) From 451f25172afc0ce46e416c73fa703c5edf279d54 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 12 May 2021 17:10:42 +0100 Subject: [PATCH 152/619] Incorporate changes from review --- CHANGES.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e6c4550339..2ceae0ac8c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ Synapse 1.34.0rc1 (2021-05-12) ============================== +This release deprecates the `room_invite_state_types` configuration setting. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/release-v1.34.0/UPGRADE.rst#upgrading-to-v1340) for instructions on updating your configuration file to use the new `room_prejoin_state` setting. + +This release also deprecates the `POST /_synapse/admin/v1/rooms//delete` admin API route. Server administrators are encouraged to update their scripts to use the new `DELETE /_synapse/admin/v1/rooms/` route instead. + Features -------- @@ -9,7 +13,7 @@ Features - Add limits to how often Synapse will GC, ensuring that large servers do not end up GC thrashing if `gc_thresholds` has not been correctly set. ([\#9902](https://github.com/matrix-org/synapse/issues/9902)) - Improve performance of sending events for worker-based deployments using Redis. ([\#9905](https://github.com/matrix-org/synapse/issues/9905), [\#9950](https://github.com/matrix-org/synapse/issues/9950), [\#9951](https://github.com/matrix-org/synapse/issues/9951)) - Improve performance after joining a large room when presence is enabled. ([\#9910](https://github.com/matrix-org/synapse/issues/9910), [\#9916](https://github.com/matrix-org/synapse/issues/9916)) -- Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see [the upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#upgrading-to-v1340) if you have customised `room_invite_state_types` in your configuration. ([\#9915](https://github.com/matrix-org/synapse/issues/9915), [\#9966](https://github.com/matrix-org/synapse/issues/9966)) +- Support stable identifiers for [MSC1772](https://github.com/matrix-org/matrix-doc/pull/1772) Spaces. `m.space.child` events will now be taken into account when populating the experimental spaces summary response. Please see [the upgrade notes](https://github.com/matrix-org/synapse/blob/release-v1.34.0/UPGRADE.rst#upgrading-to-v1340) if you have customised `room_invite_state_types` in your configuration. ([\#9915](https://github.com/matrix-org/synapse/issues/9915), [\#9966](https://github.com/matrix-org/synapse/issues/9966)) - Improve performance of backfilling in large rooms. ([\#9935](https://github.com/matrix-org/synapse/issues/9935)) - Add a config option to allow you to prevent device display names from being shared over federation. Contributed by @aaronraimist. ([\#9945](https://github.com/matrix-org/synapse/issues/9945)) - Update support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946): Spaces Summary. ([\#9947](https://github.com/matrix-org/synapse/issues/9947), [\#9954](https://github.com/matrix-org/synapse/issues/9954)) @@ -30,7 +34,7 @@ Bugfixes Updates to the Docker image --------------------------- -- Added startup_delay to docker healthcheck to reduce waiting time for coming online, updated readme for extra options, contributed by @Maquis196. ([\#9913](https://github.com/matrix-org/synapse/issues/9913)) +- Add `startup_delay` to docker healthcheck to reduce waiting time for coming online and update the documentation with extra options. Contributed by @Maquis196. ([\#9913](https://github.com/matrix-org/synapse/issues/9913)) Improved Documentation From d19bccdbecfeee3e59666748db7fd971cd7978d2 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 13 May 2021 14:37:20 -0400 Subject: [PATCH 153/619] Update SSO mapping providers documentation about unique IDs. (#9980) --- changelog.d/9980.doc | 1 + docs/sso_mapping_providers.md | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 changelog.d/9980.doc diff --git a/changelog.d/9980.doc b/changelog.d/9980.doc new file mode 100644 index 0000000000..d30ed0601d --- /dev/null +++ b/changelog.d/9980.doc @@ -0,0 +1 @@ +Clarify documentation around SSO mapping providers generating unique IDs and localparts. diff --git a/docs/sso_mapping_providers.md b/docs/sso_mapping_providers.md index 50020d1a4a..6db2dc8be5 100644 --- a/docs/sso_mapping_providers.md +++ b/docs/sso_mapping_providers.md @@ -67,8 +67,8 @@ A custom mapping provider must specify the following methods: - Arguments: - `userinfo` - A `authlib.oidc.core.claims.UserInfo` object to extract user information from. - - This method must return a string, which is the unique identifier for the - user. Commonly the ``sub`` claim of the response. + - This method must return a string, which is the unique, immutable identifier + for the user. Commonly the `sub` claim of the response. * `map_user_attributes(self, userinfo, token, failures)` - This method must be async. - Arguments: @@ -87,7 +87,9 @@ A custom mapping provider must specify the following methods: `localpart` value, such as `john.doe1`. - Returns a dictionary with two keys: - `localpart`: A string, used to generate the Matrix ID. If this is - `None`, the user is prompted to pick their own username. + `None`, the user is prompted to pick their own username. This is only used + during a user's first login. Once a localpart has been associated with a + remote user ID (see `get_remote_user_id`) it cannot be updated. - `displayname`: An optional string, the display name for the user. * `get_extra_attributes(self, userinfo, token)` - This method must be async. @@ -153,8 +155,8 @@ A custom mapping provider must specify the following methods: information from. - `client_redirect_url` - A string, the URL that the client will be redirected to. - - This method must return a string, which is the unique identifier for the - user. Commonly the ``uid`` claim of the response. + - This method must return a string, which is the unique, immutable identifier + for the user. Commonly the `uid` claim of the response. * `saml_response_to_user_attributes(self, saml_response, failures, client_redirect_url)` - Arguments: - `saml_response` - A `saml2.response.AuthnResponse` object to extract user @@ -172,8 +174,10 @@ A custom mapping provider must specify the following methods: redirected to. - This method must return a dictionary, which will then be used by Synapse to build a new user. The following keys are allowed: - * `mxid_localpart` - The mxid localpart of the new user. If this is - `None`, the user is prompted to pick their own username. + * `mxid_localpart` - A string, the mxid localpart of the new user. If this is + `None`, the user is prompted to pick their own username. This is only used + during a user's first login. Once a localpart has been associated with a + remote user ID (see `get_remote_user_id`) it cannot be updated. * `displayname` - The displayname of the new user. If not provided, will default to the value of `mxid_localpart`. * `emails` - A list of emails for the new user. If not provided, will From 976216959b3a216a3403b953338f92852c45645c Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 14 May 2021 09:21:00 +0100 Subject: [PATCH 154/619] Update minimum supported version in postgres.md (#9988) --- changelog.d/9988.doc | 1 + docs/postgres.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9988.doc diff --git a/changelog.d/9988.doc b/changelog.d/9988.doc new file mode 100644 index 0000000000..87cda376a6 --- /dev/null +++ b/changelog.d/9988.doc @@ -0,0 +1 @@ +Fix outdated minimum PostgreSQL version in postgres.md. diff --git a/docs/postgres.md b/docs/postgres.md index 680685d04e..b99fad8a6e 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -1,6 +1,6 @@ # Using Postgres -Postgres version 9.5 or later is known to work. +Synapse supports PostgreSQL versions 9.6 or later. ## Install postgres client libraries From c14f99be461d8ac9a36ad548e8e463feeda6394c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 May 2021 10:51:08 +0100 Subject: [PATCH 155/619] Support enabling opentracing by user (#9978) Add a config option which allows enabling opentracing by user id, eg for debugging requests made by a test user. --- changelog.d/9978.feature | 1 + docs/opentracing.md | 10 +++++----- docs/sample_config.yaml | 20 ++++++++++++++------ synapse/api/auth.py | 5 +++++ synapse/config/tracer.py | 37 +++++++++++++++++++++++++++++++------ 5 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 changelog.d/9978.feature diff --git a/changelog.d/9978.feature b/changelog.d/9978.feature new file mode 100644 index 0000000000..851adb9f6e --- /dev/null +++ b/changelog.d/9978.feature @@ -0,0 +1 @@ +Add a configuration option which allows enabling opentracing by user id. diff --git a/docs/opentracing.md b/docs/opentracing.md index 4c7a56a5d7..f91362f112 100644 --- a/docs/opentracing.md +++ b/docs/opentracing.md @@ -42,17 +42,17 @@ To receive OpenTracing spans, start up a Jaeger server. This can be done using docker like so: ```sh -docker run -d --name jaeger +docker run -d --name jaeger \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ - jaegertracing/all-in-one:1.13 + jaegertracing/all-in-one:1 ``` Latest documentation is probably at - +https://www.jaegertracing.io/docs/latest/getting-started. ## Enable OpenTracing in Synapse @@ -62,7 +62,7 @@ as shown in the [sample config](./sample_config.yaml). For example: ```yaml opentracing: - tracer_enabled: true + enabled: true homeserver_whitelist: - "mytrustedhomeserver.org" - "*.myotherhomeservers.com" @@ -90,4 +90,4 @@ to two problems, namely: ## Configuring Jaeger Sampling strategies can be set as in this document: - +. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 67ad57b1aa..2952f2ba32 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -2845,7 +2845,8 @@ opentracing: #enabled: true # The list of homeservers we wish to send and receive span contexts and span baggage. - # See docs/opentracing.rst + # See docs/opentracing.rst. + # # This is a list of regexes which are matched against the server_name of the # homeserver. # @@ -2854,19 +2855,26 @@ opentracing: #homeserver_whitelist: # - ".*" + # A list of the matrix IDs of users whose requests will always be traced, + # even if the tracing system would otherwise drop the traces due to + # probabilistic sampling. + # + # By default, the list is empty. + # + #force_tracing_for_users: + # - "@user1:server_name" + # - "@user2:server_name" + # Jaeger can be configured to sample traces at different rates. # All configuration options provided by Jaeger can be set here. - # Jaeger's configuration mostly related to trace sampling which + # Jaeger's configuration is mostly related to trace sampling which # is documented here: - # https://www.jaegertracing.io/docs/1.13/sampling/. + # https://www.jaegertracing.io/docs/latest/sampling/. # #jaeger_config: # sampler: # type: const # param: 1 - - # Logging whether spans were started and reported - # # logging: # false diff --git a/synapse/api/auth.py b/synapse/api/auth.py index efc926d094..458306eba5 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -87,6 +87,7 @@ def __init__(self, hs: "HomeServer"): ) self._track_appservice_user_ips = hs.config.track_appservice_user_ips self._macaroon_secret_key = hs.config.macaroon_secret_key + self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users async def check_from_context( self, room_version: str, event, context, do_sig_check=True @@ -208,6 +209,8 @@ async def get_user_by_req( opentracing.set_tag("authenticated_entity", user_id) opentracing.set_tag("user_id", user_id) opentracing.set_tag("appservice_id", app_service.id) + if user_id in self._force_tracing_for_users: + opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) return requester @@ -260,6 +263,8 @@ async def get_user_by_req( opentracing.set_tag("user_id", user_info.user_id) if device_id: opentracing.set_tag("device_id", device_id) + if user_info.token_owner in self._force_tracing_for_users: + opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) return requester except KeyError: diff --git a/synapse/config/tracer.py b/synapse/config/tracer.py index db22b5b19f..d0ea17261f 100644 --- a/synapse/config/tracer.py +++ b/synapse/config/tracer.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Set + from synapse.python_dependencies import DependencyException, check_requirements from ._base import Config, ConfigError @@ -32,6 +34,8 @@ def read_config(self, config, **kwargs): {"sampler": {"type": "const", "param": 1}, "logging": False}, ) + self.force_tracing_for_users: Set[str] = set() + if not self.opentracer_enabled: return @@ -48,6 +52,19 @@ def read_config(self, config, **kwargs): if not isinstance(self.opentracer_whitelist, list): raise ConfigError("Tracer homeserver_whitelist config is malformed") + force_tracing_for_users = opentracing_config.get("force_tracing_for_users", []) + if not isinstance(force_tracing_for_users, list): + raise ConfigError( + "Expected a list", ("opentracing", "force_tracing_for_users") + ) + for i, u in enumerate(force_tracing_for_users): + if not isinstance(u, str): + raise ConfigError( + "Expected a string", + ("opentracing", "force_tracing_for_users", f"index {i}"), + ) + self.force_tracing_for_users.add(u) + def generate_config_section(cls, **kwargs): return """\ ## Opentracing ## @@ -64,7 +81,8 @@ def generate_config_section(cls, **kwargs): #enabled: true # The list of homeservers we wish to send and receive span contexts and span baggage. - # See docs/opentracing.rst + # See docs/opentracing.rst. + # # This is a list of regexes which are matched against the server_name of the # homeserver. # @@ -73,19 +91,26 @@ def generate_config_section(cls, **kwargs): #homeserver_whitelist: # - ".*" + # A list of the matrix IDs of users whose requests will always be traced, + # even if the tracing system would otherwise drop the traces due to + # probabilistic sampling. + # + # By default, the list is empty. + # + #force_tracing_for_users: + # - "@user1:server_name" + # - "@user2:server_name" + # Jaeger can be configured to sample traces at different rates. # All configuration options provided by Jaeger can be set here. - # Jaeger's configuration mostly related to trace sampling which + # Jaeger's configuration is mostly related to trace sampling which # is documented here: - # https://www.jaegertracing.io/docs/1.13/sampling/. + # https://www.jaegertracing.io/docs/latest/sampling/. # #jaeger_config: # sampler: # type: const # param: 1 - - # Logging whether spans were started and reported - # # logging: # false """ From 498084228b89d30462df0a5adfcc737fdc21d314 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Fri, 14 May 2021 10:58:46 +0100 Subject: [PATCH 156/619] Use Python's secrets module instead of random (#9984) Functionally identical, but more obviously cryptographically secure. ...Explicit is better than implicit? Avoids needing to know that SystemRandom() implies a CSPRNG, and complies with the big scary red box on the documentation for random: > Warning: > The pseudo-random generators of this module should not be used for > security purposes. For security or cryptographic uses, see the > secrets module. https://docs.python.org/3/library/random.html Signed-off-by: Dan Callahan --- changelog.d/9984.misc | 1 + synapse/util/stringutils.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 changelog.d/9984.misc diff --git a/changelog.d/9984.misc b/changelog.d/9984.misc new file mode 100644 index 0000000000..97bd747f26 --- /dev/null +++ b/changelog.d/9984.misc @@ -0,0 +1 @@ +Simplify a few helper functions. diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index 4f25cd1d26..40cd51a8ca 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. import itertools -import random import re +import secrets import string from collections.abc import Iterable from typing import Optional, Tuple @@ -35,18 +35,21 @@ # MXC_REGEX = re.compile("^mxc://([^/]+)/([^/#?]+)$") -# random_string and random_string_with_symbols are used for a range of things, -# some cryptographically important, some less so. We use SystemRandom to make sure -# we get cryptographically-secure randoms. -rand = random.SystemRandom() - def random_string(length: int) -> str: - return "".join(rand.choice(string.ascii_letters) for _ in range(length)) + """Generate a cryptographically secure string of random letters. + + Drawn from the characters: `a-z` and `A-Z` + """ + return "".join(secrets.choice(string.ascii_letters) for _ in range(length)) def random_string_with_symbols(length: int) -> str: - return "".join(rand.choice(_string_with_symbols) for _ in range(length)) + """Generate a cryptographically secure string of random letters/numbers/symbols. + + Drawn from the characters: `a-z`, `A-Z`, `0-9`, and `.,;:^&*-_+=#~@` + """ + return "".join(secrets.choice(_string_with_symbols) for _ in range(length)) def is_ascii(s: bytes) -> bool: From bd918d874f4eb459b9c2f9af6e8e994b6b19d264 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Fri, 14 May 2021 10:58:52 +0100 Subject: [PATCH 157/619] Simplify exception handling in is_ascii. (#9985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can get away with just catching UnicodeError here. ⋮ +-- ValueError | +-- UnicodeError | +-- UnicodeDecodeError | +-- UnicodeEncodeError | +-- UnicodeTranslateError ⋮ https://docs.python.org/3/library/exceptions.html#exception-hierarchy Signed-off-by: Dan Callahan --- changelog.d/9985.misc | 1 + synapse/util/stringutils.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9985.misc diff --git a/changelog.d/9985.misc b/changelog.d/9985.misc new file mode 100644 index 0000000000..97bd747f26 --- /dev/null +++ b/changelog.d/9985.misc @@ -0,0 +1 @@ +Simplify a few helper functions. diff --git a/synapse/util/stringutils.py b/synapse/util/stringutils.py index 40cd51a8ca..f029432191 100644 --- a/synapse/util/stringutils.py +++ b/synapse/util/stringutils.py @@ -55,9 +55,7 @@ def random_string_with_symbols(length: int) -> str: def is_ascii(s: bytes) -> bool: try: s.decode("ascii").encode("ascii") - except UnicodeDecodeError: - return False - except UnicodeEncodeError: + except UnicodeError: return False return True From ebdef256b36ec67a8aac9721eea8971dd5cf361f Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Fri, 14 May 2021 10:58:57 +0100 Subject: [PATCH 158/619] Remove superfluous call to bool() (#9986) Our strtobool already returns a bool, so no need to re-cast here Signed-off-by: Dan Callahan --- changelog.d/9986.misc | 1 + synapse/config/registration.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9986.misc diff --git a/changelog.d/9986.misc b/changelog.d/9986.misc new file mode 100644 index 0000000000..97bd747f26 --- /dev/null +++ b/changelog.d/9986.misc @@ -0,0 +1 @@ +Simplify a few helper functions. diff --git a/synapse/config/registration.py b/synapse/config/registration.py index e6f52b4f40..d9dc55a0c3 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -349,4 +349,4 @@ def add_arguments(parser): def read_arguments(self, args): if args.enable_registration is not None: - self.enable_registration = bool(strtobool(str(args.enable_registration))) + self.enable_registration = strtobool(str(args.enable_registration)) From 52ed9655edf8849d68a178e1c76040c79824a353 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Fri, 14 May 2021 10:59:10 +0100 Subject: [PATCH 159/619] Remove unnecessary SystemRandom from SQLBaseStore (#9987) It's not obvious that instances of SQLBaseStore each need their own instances of random.SystemRandom(); let's just use random directly. Introduced by 52839886d664576831462e033b88e5aba4c019e3 Signed-off-by: Dan Callahan --- changelog.d/9987.misc | 1 + synapse/storage/_base.py | 2 -- synapse/storage/databases/main/registration.py | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9987.misc diff --git a/changelog.d/9987.misc b/changelog.d/9987.misc new file mode 100644 index 0000000000..02c088e3e6 --- /dev/null +++ b/changelog.d/9987.misc @@ -0,0 +1 @@ +Remove unnecessary property from SQLBaseStore. diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index 3d98d3f5f8..0623da9aa1 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -import random from abc import ABCMeta from typing import TYPE_CHECKING, Any, Collection, Iterable, Optional, Union @@ -44,7 +43,6 @@ def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer" self._clock = hs.get_clock() self.database_engine = database.engine self.db_pool = database - self.rand = random.SystemRandom() def process_replication_rows( self, diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index 6e5ee557d2..e5c5cf8ff0 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +import random import re from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union @@ -997,7 +998,7 @@ def set_expiration_date_for_user_txn(self, txn, user_id, use_delta=False): expiration_ts = now_ms + self._account_validity_period if use_delta: - expiration_ts = self.rand.randrange( + expiration_ts = random.randrange( expiration_ts - self._account_validity_startup_job_max_delta, expiration_ts, ) From 5090f26b636bf4439575767a2272d033fb33b2d5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 May 2021 11:12:36 +0100 Subject: [PATCH 160/619] Minor `@cachedList` enhancements (#9975) - use a tuple rather than a list for the iterable that is passed into the wrapped function, for performance - test that we can pass an iterable and that keys are correctly deduped. --- changelog.d/9975.misc | 1 + synapse/storage/databases/main/devices.py | 2 +- .../storage/databases/main/end_to_end_keys.py | 4 ++-- .../databases/main/user_erasure_store.py | 13 +++++-------- synapse/util/caches/descriptors.py | 14 ++++++++------ tests/util/caches/test_descriptors.py | 17 ++++++++++++++--- 6 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 changelog.d/9975.misc diff --git a/changelog.d/9975.misc b/changelog.d/9975.misc new file mode 100644 index 0000000000..28b1e40c2b --- /dev/null +++ b/changelog.d/9975.misc @@ -0,0 +1 @@ +Minor enhancements to the `@cachedList` descriptor. diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index c9346de316..a1f98b7e38 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -665,7 +665,7 @@ async def get_device_list_last_stream_id_for_remote( cached_method_name="get_device_list_last_stream_id_for_remote", list_name="user_ids", ) - async def get_device_list_last_stream_id_for_remotes(self, user_ids: str): + async def get_device_list_last_stream_id_for_remotes(self, user_ids: Iterable[str]): rows = await self.db_pool.simple_select_many_batch( table="device_lists_remote_extremeties", column="user_id", diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index 398d6b6acb..9ba5778a88 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -473,7 +473,7 @@ def _get_bare_e2e_cross_signing_keys(self, user_id): num_args=1, ) async def _get_bare_e2e_cross_signing_keys_bulk( - self, user_ids: List[str] + self, user_ids: Iterable[str] ) -> Dict[str, Dict[str, dict]]: """Returns the cross-signing keys for a set of users. The output of this function should be passed to _get_e2e_cross_signing_signatures_txn if @@ -497,7 +497,7 @@ async def _get_bare_e2e_cross_signing_keys_bulk( def _get_bare_e2e_cross_signing_keys_bulk_txn( self, txn: Connection, - user_ids: List[str], + user_ids: Iterable[str], ) -> Dict[str, Dict[str, dict]]: """Returns the cross-signing keys for a set of users. The output of this function should be passed to _get_e2e_cross_signing_signatures_txn if diff --git a/synapse/storage/databases/main/user_erasure_store.py b/synapse/storage/databases/main/user_erasure_store.py index acf6b2fb64..1ecdd40c38 100644 --- a/synapse/storage/databases/main/user_erasure_store.py +++ b/synapse/storage/databases/main/user_erasure_store.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict, Iterable + from synapse.storage._base import SQLBaseStore from synapse.util.caches.descriptors import cached, cachedList @@ -37,21 +39,16 @@ async def is_user_erased(self, user_id: str) -> bool: return bool(result) @cachedList(cached_method_name="is_user_erased", list_name="user_ids") - async def are_users_erased(self, user_ids): + async def are_users_erased(self, user_ids: Iterable[str]) -> Dict[str, bool]: """ Checks which users in a list have requested erasure Args: - user_ids (iterable[str]): full user id to check + user_ids: full user ids to check Returns: - dict[str, bool]: - for each user, whether the user has requested erasure. + for each user, whether the user has requested erasure. """ - # this serves the dual purpose of (a) making sure we can do len and - # iterate it multiple times, and (b) avoiding duplicates. - user_ids = tuple(set(user_ids)) - rows = await self.db_pool.simple_select_many_batch( table="erased_users", column="user_id", diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index ac4a078b26..3a4d027095 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -322,8 +322,8 @@ def _wrapped(*args, **kwargs): class DeferredCacheListDescriptor(_CacheDescriptorBase): """Wraps an existing cache to support bulk fetching of keys. - Given a list of keys it looks in the cache to find any hits, then passes - the list of missing keys to the wrapped function. + Given an iterable of keys it looks in the cache to find any hits, then passes + the tuple of missing keys to the wrapped function. Once wrapped, the function returns a Deferred which resolves to the list of results. @@ -437,7 +437,9 @@ def errback(f): return f args_to_call = dict(arg_dict) - args_to_call[self.list_name] = list(missing) + # copy the missing set before sending it to the callee, to guard against + # modification. + args_to_call[self.list_name] = tuple(missing) cached_defers.append( defer.maybeDeferred( @@ -522,14 +524,14 @@ def cachedList( Used to do batch lookups for an already created cache. A single argument is specified as a list that is iterated through to lookup keys in the - original cache. A new list consisting of the keys that weren't in the cache - get passed to the original function, the result of which is stored in the + original cache. A new tuple consisting of the (deduplicated) keys that weren't in + the cache gets passed to the original function, the result of which is stored in the cache. Args: cached_method_name: The name of the single-item lookup method. This is only used to find the cache to use. - list_name: The name of the argument that is the list to use to + list_name: The name of the argument that is the iterable to use to do batch lookups in the cache. num_args: Number of arguments to use as the key in the cache (including list_name). Defaults to all named parameters. diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index 178ac8a68c..bbbc276697 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -666,18 +666,20 @@ async def list_fn(self, args1, arg2): with LoggingContext("c1") as c1: obj = Cls() obj.mock.return_value = {10: "fish", 20: "chips"} + + # start the lookup off d1 = obj.list_fn([10, 20], 2) self.assertEqual(current_context(), SENTINEL_CONTEXT) r = yield d1 self.assertEqual(current_context(), c1) - obj.mock.assert_called_once_with([10, 20], 2) + obj.mock.assert_called_once_with((10, 20), 2) self.assertEqual(r, {10: "fish", 20: "chips"}) obj.mock.reset_mock() # a call with different params should call the mock again obj.mock.return_value = {30: "peas"} r = yield obj.list_fn([20, 30], 2) - obj.mock.assert_called_once_with([30], 2) + obj.mock.assert_called_once_with((30,), 2) self.assertEqual(r, {20: "chips", 30: "peas"}) obj.mock.reset_mock() @@ -692,6 +694,15 @@ async def list_fn(self, args1, arg2): obj.mock.assert_not_called() self.assertEqual(r, {10: "fish", 20: "chips", 30: "peas"}) + # we should also be able to use a (single-use) iterable, and should + # deduplicate the keys + obj.mock.reset_mock() + obj.mock.return_value = {40: "gravy"} + iterable = (x for x in [10, 40, 40]) + r = yield obj.list_fn(iterable, 2) + obj.mock.assert_called_once_with((40,), 2) + self.assertEqual(r, {10: "fish", 40: "gravy"}) + @defer.inlineCallbacks def test_invalidate(self): """Make sure that invalidation callbacks are called.""" @@ -717,7 +728,7 @@ async def list_fn(self, args1, arg2): # cache miss obj.mock.return_value = {10: "fish", 20: "chips"} r1 = yield obj.list_fn([10, 20], 2, on_invalidate=invalidate0) - obj.mock.assert_called_once_with([10, 20], 2) + obj.mock.assert_called_once_with((10, 20), 2) self.assertEqual(r1, {10: "fish", 20: "chips"}) obj.mock.reset_mock() From 6482075c95957ad980d9c1323f9f982e6f7aaff4 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 May 2021 11:46:35 +0100 Subject: [PATCH 161/619] Run `black` on the scripts (#9981) Turns out these scripts weren't getting linted. --- changelog.d/9981.misc | 1 + scripts-dev/build_debian_packages | 105 +++++++++++++++++++----------- scripts-dev/lint.sh | 18 ++++- scripts/export_signing_key | 13 +++- scripts/generate_config | 18 ++--- scripts/hash_password | 6 +- scripts/synapse_port_db | 46 +++++++------ tox.ini | 10 +++ 8 files changed, 141 insertions(+), 76 deletions(-) create mode 100644 changelog.d/9981.misc diff --git a/changelog.d/9981.misc b/changelog.d/9981.misc new file mode 100644 index 0000000000..677c9b4cbd --- /dev/null +++ b/changelog.d/9981.misc @@ -0,0 +1 @@ +Run `black` on files in the `scripts` directory. diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index 07d018db99..546724f89f 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -21,18 +21,18 @@ DISTS = ( "debian:buster", "debian:bullseye", "debian:sid", - "ubuntu:bionic", # 18.04 LTS (our EOL forced by Py36 on 2021-12-23) - "ubuntu:focal", # 20.04 LTS (our EOL forced by Py38 on 2024-10-14) - "ubuntu:groovy", # 20.10 (EOL 2021-07-07) + "ubuntu:bionic", # 18.04 LTS (our EOL forced by Py36 on 2021-12-23) + "ubuntu:focal", # 20.04 LTS (our EOL forced by Py38 on 2024-10-14) + "ubuntu:groovy", # 20.10 (EOL 2021-07-07) "ubuntu:hirsute", # 21.04 (EOL 2022-01-05) ) -DESC = '''\ +DESC = """\ Builds .debs for synapse, using a Docker image for the build environment. By default, builds for all known distributions, but a list of distributions can be passed on the commandline for debugging. -''' +""" class Builder(object): @@ -46,7 +46,7 @@ class Builder(object): """Build deb for a single distribution""" if self._failed: - print("not building %s due to earlier failure" % (dist, )) + print("not building %s due to earlier failure" % (dist,)) raise Exception("failed") try: @@ -68,48 +68,65 @@ class Builder(object): # we tend to get source packages which are full of debs. (We could hack # around that with more magic in the build_debian.sh script, but that # doesn't solve the problem for natively-run dpkg-buildpakage). - debsdir = os.path.join(projdir, '../debs') + debsdir = os.path.join(projdir, "../debs") os.makedirs(debsdir, exist_ok=True) if self.redirect_stdout: - logfile = os.path.join(debsdir, "%s.buildlog" % (tag, )) + logfile = os.path.join(debsdir, "%s.buildlog" % (tag,)) print("building %s: directing output to %s" % (dist, logfile)) stdout = open(logfile, "w") else: stdout = None # first build a docker image for the build environment - subprocess.check_call([ - "docker", "build", - "--tag", "dh-venv-builder:" + tag, - "--build-arg", "distro=" + dist, - "-f", "docker/Dockerfile-dhvirtualenv", - "docker", - ], stdout=stdout, stderr=subprocess.STDOUT) + subprocess.check_call( + [ + "docker", + "build", + "--tag", + "dh-venv-builder:" + tag, + "--build-arg", + "distro=" + dist, + "-f", + "docker/Dockerfile-dhvirtualenv", + "docker", + ], + stdout=stdout, + stderr=subprocess.STDOUT, + ) container_name = "synapse_build_" + tag with self._lock: self.active_containers.add(container_name) # then run the build itself - subprocess.check_call([ - "docker", "run", - "--rm", - "--name", container_name, - "--volume=" + projdir + ":/synapse/source:ro", - "--volume=" + debsdir + ":/debs", - "-e", "TARGET_USERID=%i" % (os.getuid(), ), - "-e", "TARGET_GROUPID=%i" % (os.getgid(), ), - "-e", "DEB_BUILD_OPTIONS=%s" % ("nocheck" if skip_tests else ""), - "dh-venv-builder:" + tag, - ], stdout=stdout, stderr=subprocess.STDOUT) + subprocess.check_call( + [ + "docker", + "run", + "--rm", + "--name", + container_name, + "--volume=" + projdir + ":/synapse/source:ro", + "--volume=" + debsdir + ":/debs", + "-e", + "TARGET_USERID=%i" % (os.getuid(),), + "-e", + "TARGET_GROUPID=%i" % (os.getgid(),), + "-e", + "DEB_BUILD_OPTIONS=%s" % ("nocheck" if skip_tests else ""), + "dh-venv-builder:" + tag, + ], + stdout=stdout, + stderr=subprocess.STDOUT, + ) with self._lock: self.active_containers.remove(container_name) if stdout is not None: stdout.close() - print("Completed build of %s" % (dist, )) + print("Completed build of %s" % (dist,)) def kill_containers(self): with self._lock: @@ -117,9 +134,14 @@ class Builder(object): for c in active: print("killing container %s" % (c,)) - subprocess.run([ - "docker", "kill", c, - ], stdout=subprocess.DEVNULL) + subprocess.run( + [ + "docker", + "kill", + c, + ], + stdout=subprocess.DEVNULL, + ) with self._lock: self.active_containers.remove(c) @@ -130,31 +152,38 @@ def run_builds(dists, jobs=1, skip_tests=False): def sig(signum, _frame): print("Caught SIGINT") builder.kill_containers() + signal.signal(signal.SIGINT, sig) with ThreadPoolExecutor(max_workers=jobs) as e: res = e.map(lambda dist: builder.run_build(dist, skip_tests), dists) # make sure we consume the iterable so that exceptions are raised. - for r in res: + for _ in res: pass -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser( description=DESC, ) parser.add_argument( - '-j', '--jobs', type=int, default=1, - help='specify the number of builds to run in parallel', + "-j", + "--jobs", + type=int, + default=1, + help="specify the number of builds to run in parallel", ) parser.add_argument( - '--no-check', action='store_true', - help='skip running tests after building', + "--no-check", + action="store_true", + help="skip running tests after building", ) parser.add_argument( - 'dist', nargs='*', default=DISTS, - help='a list of distributions to build for. Default: %(default)s', + "dist", + nargs="*", + default=DISTS, + help="a list of distributions to build for. Default: %(default)s", ) args = parser.parse_args() run_builds(dists=args.dist, jobs=args.jobs, skip_tests=args.no_check) diff --git a/scripts-dev/lint.sh b/scripts-dev/lint.sh index 9761e97594..869eb2372d 100755 --- a/scripts-dev/lint.sh +++ b/scripts-dev/lint.sh @@ -80,8 +80,22 @@ else # then lint everything! if [[ -z ${files+x} ]]; then # Lint all source code files and directories - # Note: this list aims the mirror the one in tox.ini - files=("synapse" "docker" "tests" "scripts-dev" "scripts" "contrib" "synctl" "setup.py" "synmark" "stubs" ".buildkite") + # Note: this list aims to mirror the one in tox.ini + files=( + "synapse" "docker" "tests" + # annoyingly, black doesn't find these so we have to list them + "scripts/export_signing_key" + "scripts/generate_config" + "scripts/generate_log_config" + "scripts/hash_password" + "scripts/register_new_matrix_user" + "scripts/synapse_port_db" + "scripts-dev" + "scripts-dev/build_debian_packages" + "scripts-dev/sign_json" + "scripts-dev/update_database" + "contrib" "synctl" "setup.py" "synmark" "stubs" ".buildkite" + ) fi fi diff --git a/scripts/export_signing_key b/scripts/export_signing_key index 0ed167ea85..bf0139bd64 100755 --- a/scripts/export_signing_key +++ b/scripts/export_signing_key @@ -30,7 +30,11 @@ def exit(status: int = 0, message: Optional[str] = None): def format_plain(public_key: nacl.signing.VerifyKey): print( "%s:%s %s" - % (public_key.alg, public_key.version, encode_verify_key_base64(public_key),) + % ( + public_key.alg, + public_key.version, + encode_verify_key_base64(public_key), + ) ) @@ -50,7 +54,10 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( - "key_file", nargs="+", type=argparse.FileType("r"), help="The key file to read", + "key_file", + nargs="+", + type=argparse.FileType("r"), + help="The key file to read", ) parser.add_argument( @@ -63,7 +70,7 @@ if __name__ == "__main__": parser.add_argument( "--expiry-ts", type=int, - default=int(time.time() * 1000) + 6*3600000, + default=int(time.time() * 1000) + 6 * 3600000, help=( "The expiry time to use for -x, in milliseconds since 1970. The default " "is (now+6h)." diff --git a/scripts/generate_config b/scripts/generate_config index 771cbf8d95..931b40c045 100755 --- a/scripts/generate_config +++ b/scripts/generate_config @@ -11,23 +11,22 @@ if __name__ == "__main__": parser.add_argument( "--config-dir", default="CONFDIR", - help="The path where the config files are kept. Used to create filenames for " - "things like the log config and the signing key. Default: %(default)s", + "things like the log config and the signing key. Default: %(default)s", ) parser.add_argument( "--data-dir", default="DATADIR", help="The path where the data files are kept. Used to create filenames for " - "things like the database and media store. Default: %(default)s", + "things like the database and media store. Default: %(default)s", ) parser.add_argument( "--server-name", default="SERVERNAME", help="The server name. Used to initialise the server_name config param, but also " - "used in the names of some of the config files. Default: %(default)s", + "used in the names of some of the config files. Default: %(default)s", ) parser.add_argument( @@ -41,21 +40,22 @@ if __name__ == "__main__": "--generate-secrets", action="store_true", help="Enable generation of new secrets for things like the macaroon_secret_key." - "By default, these parameters will be left unset." + "By default, these parameters will be left unset.", ) parser.add_argument( - "-o", "--output-file", - type=argparse.FileType('w'), + "-o", + "--output-file", + type=argparse.FileType("w"), default=sys.stdout, help="File to write the configuration to. Default: stdout", ) parser.add_argument( "--header-file", - type=argparse.FileType('r'), + type=argparse.FileType("r"), help="File from which to read a header, which will be printed before the " - "generated config.", + "generated config.", ) args = parser.parse_args() diff --git a/scripts/hash_password b/scripts/hash_password index a30767f758..1d6fb0d700 100755 --- a/scripts/hash_password +++ b/scripts/hash_password @@ -41,7 +41,7 @@ if __name__ == "__main__": parser.add_argument( "-c", "--config", - type=argparse.FileType('r'), + type=argparse.FileType("r"), help=( "Path to server config file. " "Used to read in bcrypt_rounds and password_pepper." @@ -72,8 +72,8 @@ if __name__ == "__main__": pw = unicodedata.normalize("NFKC", password) hashed = bcrypt.hashpw( - pw.encode('utf8') + password_pepper.encode("utf8"), + pw.encode("utf8") + password_pepper.encode("utf8"), bcrypt.gensalt(bcrypt_rounds), - ).decode('ascii') + ).decode("ascii") print(hashed) diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 5fb5bb35f7..7c7645c05a 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -294,8 +294,7 @@ class Porter(object): return table, already_ported, total_to_port, forward_chunk, backward_chunk async def get_table_constraints(self) -> Dict[str, Set[str]]: - """Returns a map of tables that have foreign key constraints to tables they depend on. - """ + """Returns a map of tables that have foreign key constraints to tables they depend on.""" def _get_constraints(txn): # We can pull the information about foreign key constraints out from @@ -504,7 +503,9 @@ class Porter(object): return def build_db_store( - self, db_config: DatabaseConnectionConfig, allow_outdated_version: bool = False, + self, + db_config: DatabaseConnectionConfig, + allow_outdated_version: bool = False, ): """Builds and returns a database store using the provided configuration. @@ -740,7 +741,7 @@ class Porter(object): return col outrows = [] - for i, row in enumerate(rows): + for row in rows: try: outrows.append( tuple(conv(j, col) for j, col in enumerate(row) if j > 0) @@ -890,8 +891,7 @@ class Porter(object): await self.postgres_store.db_pool.runInteraction("setup_user_id_seq", r) async def _setup_events_stream_seqs(self) -> None: - """Set the event stream sequences to the correct values. - """ + """Set the event stream sequences to the correct values.""" # We get called before we've ported the events table, so we need to # fetch the current positions from the SQLite store. @@ -920,12 +920,14 @@ class Porter(object): ) await self.postgres_store.db_pool.runInteraction( - "_setup_events_stream_seqs", _setup_events_stream_seqs_set_pos, + "_setup_events_stream_seqs", + _setup_events_stream_seqs_set_pos, ) - async def _setup_sequence(self, sequence_name: str, stream_id_tables: Iterable[str]) -> None: - """Set a sequence to the correct value. - """ + async def _setup_sequence( + self, sequence_name: str, stream_id_tables: Iterable[str] + ) -> None: + """Set a sequence to the correct value.""" current_stream_ids = [] for stream_id_table in stream_id_tables: max_stream_id = await self.sqlite_store.db_pool.simple_select_one_onecol( @@ -939,14 +941,19 @@ class Porter(object): next_id = max(current_stream_ids) + 1 def r(txn): - sql = "ALTER SEQUENCE %s RESTART WITH" % (sequence_name, ) - txn.execute(sql + " %s", (next_id, )) + sql = "ALTER SEQUENCE %s RESTART WITH" % (sequence_name,) + txn.execute(sql + " %s", (next_id,)) - await self.postgres_store.db_pool.runInteraction("_setup_%s" % (sequence_name,), r) + await self.postgres_store.db_pool.runInteraction( + "_setup_%s" % (sequence_name,), r + ) async def _setup_auth_chain_sequence(self) -> None: curr_chain_id = await self.sqlite_store.db_pool.simple_select_one_onecol( - table="event_auth_chains", keyvalues={}, retcol="MAX(chain_id)", allow_none=True + table="event_auth_chains", + keyvalues={}, + retcol="MAX(chain_id)", + allow_none=True, ) def r(txn): @@ -968,8 +975,7 @@ class Porter(object): class Progress(object): - """Used to report progress of the port - """ + """Used to report progress of the port""" def __init__(self): self.tables = {} @@ -994,8 +1000,7 @@ class Progress(object): class CursesProgress(Progress): - """Reports progress to a curses window - """ + """Reports progress to a curses window""" def __init__(self, stdscr): self.stdscr = stdscr @@ -1020,7 +1025,7 @@ class CursesProgress(Progress): self.total_processed = 0 self.total_remaining = 0 - for table, data in self.tables.items(): + for data in self.tables.values(): self.total_processed += data["num_done"] - data["start"] self.total_remaining += data["total"] - data["num_done"] @@ -1111,8 +1116,7 @@ class CursesProgress(Progress): class TerminalProgress(Progress): - """Just prints progress to the terminal - """ + """Just prints progress to the terminal""" def update(self, table, num_done): super(TerminalProgress, self).update(table, num_done) diff --git a/tox.ini b/tox.ini index ecd609271d..da77d124fc 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,17 @@ lint_targets = synapse tests scripts + # annoyingly, black doesn't find these so we have to list them + scripts/export_signing_key + scripts/generate_config + scripts/generate_log_config + scripts/hash_password + scripts/register_new_matrix_user + scripts/synapse_port_db scripts-dev + scripts-dev/build_debian_packages + scripts-dev/sign_json + scripts-dev/update_database stubs contrib synctl From 66609122260ad151359b9c0028634094cf51b5c5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 14 May 2021 13:14:48 +0100 Subject: [PATCH 162/619] Update postgres docs (#9989) --- changelog.d/9988.doc | 2 +- changelog.d/9989.doc | 1 + docs/postgres.md | 198 +++++++++++++++++++++---------------------- 3 files changed, 98 insertions(+), 103 deletions(-) create mode 100644 changelog.d/9989.doc diff --git a/changelog.d/9988.doc b/changelog.d/9988.doc index 87cda376a6..25338c44c3 100644 --- a/changelog.d/9988.doc +++ b/changelog.d/9988.doc @@ -1 +1 @@ -Fix outdated minimum PostgreSQL version in postgres.md. +Updates to the PostgreSQL documentation (`postgres.md`). diff --git a/changelog.d/9989.doc b/changelog.d/9989.doc new file mode 100644 index 0000000000..25338c44c3 --- /dev/null +++ b/changelog.d/9989.doc @@ -0,0 +1 @@ +Updates to the PostgreSQL documentation (`postgres.md`). diff --git a/docs/postgres.md b/docs/postgres.md index b99fad8a6e..f83155e52a 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -33,28 +33,15 @@ Assuming your PostgreSQL database user is called `postgres`, first authenticate # Or, if your system uses sudo to get administrative rights sudo -u postgres bash -Then, create a user ``synapse_user`` with: +Then, create a postgres user and a database with: + # this will prompt for a password for the new user createuser --pwprompt synapse_user -Before you can authenticate with the `synapse_user`, you must create a -database that it can access. To create a database, first connect to the -database with your database user: + createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse_user synapse - su - postgres # Or: sudo -u postgres bash - psql - -and then run: - - CREATE DATABASE synapse - ENCODING 'UTF8' - LC_COLLATE='C' - LC_CTYPE='C' - template=template0 - OWNER synapse_user; - -This would create an appropriate database named `synapse` owned by the -`synapse_user` user (which must already have been created as above). +The above will create a user called `synapse_user`, and a database called +`synapse`. Note that the PostgreSQL database *must* have the correct encoding set (as shown above), otherwise it will not be able to store UTF8 strings. @@ -63,79 +50,6 @@ You may need to enable password authentication so `synapse_user` can connect to the database. See . -If you get an error along the lines of `FATAL: Ident authentication failed for -user "synapse_user"`, you may need to use an authentication method other than -`ident`: - -* If the `synapse_user` user has a password, add the password to the `database:` - section of `homeserver.yaml`. Then add the following to `pg_hba.conf`: - - ``` - host synapse synapse_user ::1/128 md5 # or `scram-sha-256` instead of `md5` if you use that - ``` - -* If the `synapse_user` user does not have a password, then a password doesn't - have to be added to `homeserver.yaml`. But the following does need to be added - to `pg_hba.conf`: - - ``` - host synapse synapse_user ::1/128 trust - ``` - -Note that line order matters in `pg_hba.conf`, so make sure that if you do add a -new line, it is inserted before: - -``` -host all all ::1/128 ident -``` - -### Fixing incorrect `COLLATE` or `CTYPE` - -Synapse will refuse to set up a new database if it has the wrong values of -`COLLATE` and `CTYPE` set, and will log warnings on existing databases. Using -different locales can cause issues if the locale library is updated from -underneath the database, or if a different version of the locale is used on any -replicas. - -The safest way to fix the issue is to take a dump and recreate the database with -the correct `COLLATE` and `CTYPE` parameters (as shown above). It is also possible to change the -parameters on a live database and run a `REINDEX` on the entire database, -however extreme care must be taken to avoid database corruption. - -Note that the above may fail with an error about duplicate rows if corruption -has already occurred, and such duplicate rows will need to be manually removed. - - -## Fixing inconsistent sequences error - -Synapse uses Postgres sequences to generate IDs for various tables. A sequence -and associated table can get out of sync if, for example, Synapse has been -downgraded and then upgraded again. - -To fix the issue shut down Synapse (including any and all workers) and run the -SQL command included in the error message. Once done Synapse should start -successfully. - - -## Tuning Postgres - -The default settings should be fine for most deployments. For larger -scale deployments tuning some of the settings is recommended, details of -which can be found at -. - -In particular, we've found tuning the following values helpful for -performance: - -- `shared_buffers` -- `effective_cache_size` -- `work_mem` -- `maintenance_work_mem` -- `autovacuum_work_mem` - -Note that the appropriate values for those fields depend on the amount -of free memory the database host has available. - ## Synapse config When you are ready to start using PostgreSQL, edit the `database` @@ -165,18 +79,42 @@ may block for an extended period while it waits for a response from the database server. Example values might be: ```yaml -# seconds of inactivity after which TCP should send a keepalive message to the server -keepalives_idle: 10 +database: + args: + # ... as above + + # seconds of inactivity after which TCP should send a keepalive message to the server + keepalives_idle: 10 -# the number of seconds after which a TCP keepalive message that is not -# acknowledged by the server should be retransmitted -keepalives_interval: 10 + # the number of seconds after which a TCP keepalive message that is not + # acknowledged by the server should be retransmitted + keepalives_interval: 10 -# the number of TCP keepalives that can be lost before the client's connection -# to the server is considered dead -keepalives_count: 3 + # the number of TCP keepalives that can be lost before the client's connection + # to the server is considered dead + keepalives_count: 3 ``` +## Tuning Postgres + +The default settings should be fine for most deployments. For larger +scale deployments tuning some of the settings is recommended, details of +which can be found at +. + +In particular, we've found tuning the following values helpful for +performance: + +- `shared_buffers` +- `effective_cache_size` +- `work_mem` +- `maintenance_work_mem` +- `autovacuum_work_mem` + +Note that the appropriate values for those fields depend on the amount +of free memory the database host has available. + + ## Porting from SQLite ### Overview @@ -185,9 +123,8 @@ The script `synapse_port_db` allows porting an existing synapse server backed by SQLite to using PostgreSQL. This is done in as a two phase process: -1. Copy the existing SQLite database to a separate location (while the - server is down) and running the port script against that offline - database. +1. Copy the existing SQLite database to a separate location and run + the port script against that offline database. 2. Shut down the server. Rerun the port script to port any data that has come in since taking the first snapshot. Restart server against the PostgreSQL database. @@ -245,3 +182,60 @@ PostgreSQL database configuration file `homeserver-postgres.yaml`: ./synctl start Synapse should now be running against PostgreSQL. + + +## Troubleshooting + +### Alternative auth methods + +If you get an error along the lines of `FATAL: Ident authentication failed for +user "synapse_user"`, you may need to use an authentication method other than +`ident`: + +* If the `synapse_user` user has a password, add the password to the `database:` + section of `homeserver.yaml`. Then add the following to `pg_hba.conf`: + + ``` + host synapse synapse_user ::1/128 md5 # or `scram-sha-256` instead of `md5` if you use that + ``` + +* If the `synapse_user` user does not have a password, then a password doesn't + have to be added to `homeserver.yaml`. But the following does need to be added + to `pg_hba.conf`: + + ``` + host synapse synapse_user ::1/128 trust + ``` + +Note that line order matters in `pg_hba.conf`, so make sure that if you do add a +new line, it is inserted before: + +``` +host all all ::1/128 ident +``` + +### Fixing incorrect `COLLATE` or `CTYPE` + +Synapse will refuse to set up a new database if it has the wrong values of +`COLLATE` and `CTYPE` set, and will log warnings on existing databases. Using +different locales can cause issues if the locale library is updated from +underneath the database, or if a different version of the locale is used on any +replicas. + +The safest way to fix the issue is to dump the database and recreate it with +the correct locale parameter (as shown above). It is also possible to change the +parameters on a live database and run a `REINDEX` on the entire database, +however extreme care must be taken to avoid database corruption. + +Note that the above may fail with an error about duplicate rows if corruption +has already occurred, and such duplicate rows will need to be manually removed. + +### Fixing inconsistent sequences error + +Synapse uses Postgres sequences to generate IDs for various tables. A sequence +and associated table can get out of sync if, for example, Synapse has been +downgraded and then upgraded again. + +To fix the issue shut down Synapse (including any and all workers) and run the +SQL command included in the error message. Once done Synapse should start +successfully. From 41ac128fd39b30fe33b6c871a8317ba833eb4ef7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 17 May 2021 12:33:38 +0200 Subject: [PATCH 163/619] Split multiplart email sending into a dedicated handler (#9977) Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/9977.misc | 1 + synapse/handlers/account_validity.py | 55 +++------------- synapse/handlers/send_email.py | 98 ++++++++++++++++++++++++++++ synapse/push/mailer.py | 53 +++------------ synapse/server.py | 5 ++ 5 files changed, 122 insertions(+), 90 deletions(-) create mode 100644 changelog.d/9977.misc create mode 100644 synapse/handlers/send_email.py diff --git a/changelog.d/9977.misc b/changelog.d/9977.misc new file mode 100644 index 0000000000..093dffc6be --- /dev/null +++ b/changelog.d/9977.misc @@ -0,0 +1 @@ +Split multipart email sending into a dedicated handler. diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index 5b927f10b3..d752cf34f0 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -15,12 +15,9 @@ import email.mime.multipart import email.utils import logging -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText from typing import TYPE_CHECKING, List, Optional, Tuple from synapse.api.errors import StoreError, SynapseError -from synapse.logging.context import make_deferred_yieldable from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.types import UserID from synapse.util import stringutils @@ -36,9 +33,11 @@ def __init__(self, hs: "HomeServer"): self.hs = hs self.config = hs.config self.store = self.hs.get_datastore() - self.sendmail = self.hs.get_sendmail() + self.send_email_handler = self.hs.get_send_email_handler() self.clock = self.hs.get_clock() + self._app_name = self.hs.config.email_app_name + self._account_validity_enabled = ( hs.config.account_validity.account_validity_enabled ) @@ -63,23 +62,10 @@ def __init__(self, hs: "HomeServer"): self._template_text = ( hs.config.account_validity.account_validity_template_text ) - account_validity_renew_email_subject = ( + self._renew_email_subject = ( hs.config.account_validity.account_validity_renew_email_subject ) - try: - app_name = hs.config.email_app_name - - self._subject = account_validity_renew_email_subject % {"app": app_name} - - self._from_string = hs.config.email_notif_from % {"app": app_name} - except Exception: - # If substitution failed, fall back to the bare strings. - self._subject = account_validity_renew_email_subject - self._from_string = hs.config.email_notif_from - - self._raw_from = email.utils.parseaddr(self._from_string)[1] - # Check the renewal emails to send and send them every 30min. if hs.config.run_background_tasks: self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000) @@ -159,38 +145,17 @@ async def _send_renewal_email(self, user_id: str, expiration_ts: int) -> None: } html_text = self._template_html.render(**template_vars) - html_part = MIMEText(html_text, "html", "utf8") - plain_text = self._template_text.render(**template_vars) - text_part = MIMEText(plain_text, "plain", "utf8") for address in addresses: raw_to = email.utils.parseaddr(address)[1] - multipart_msg = MIMEMultipart("alternative") - multipart_msg["Subject"] = self._subject - multipart_msg["From"] = self._from_string - multipart_msg["To"] = address - multipart_msg["Date"] = email.utils.formatdate() - multipart_msg["Message-ID"] = email.utils.make_msgid() - multipart_msg.attach(text_part) - multipart_msg.attach(html_part) - - logger.info("Sending renewal email to %s", address) - - await make_deferred_yieldable( - self.sendmail( - self.hs.config.email_smtp_host, - self._raw_from, - raw_to, - multipart_msg.as_string().encode("utf8"), - reactor=self.hs.get_reactor(), - port=self.hs.config.email_smtp_port, - requireAuthentication=self.hs.config.email_smtp_user is not None, - username=self.hs.config.email_smtp_user, - password=self.hs.config.email_smtp_pass, - requireTransportSecurity=self.hs.config.require_transport_security, - ) + await self.send_email_handler.send_email( + email_address=raw_to, + subject=self._renew_email_subject, + app_name=self._app_name, + html=html_text, + text=plain_text, ) await self.store.set_renewal_mail_status(user_id=user_id, email_sent=True) diff --git a/synapse/handlers/send_email.py b/synapse/handlers/send_email.py new file mode 100644 index 0000000000..e9f6aef06f --- /dev/null +++ b/synapse/handlers/send_email.py @@ -0,0 +1,98 @@ +# Copyright 2021 The Matrix.org C.I.C. Foundation +# +# 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 email.utils +import logging +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from typing import TYPE_CHECKING + +from synapse.logging.context import make_deferred_yieldable + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class SendEmailHandler: + def __init__(self, hs: "HomeServer"): + self.hs = hs + + self._sendmail = hs.get_sendmail() + self._reactor = hs.get_reactor() + + self._from = hs.config.email.email_notif_from + self._smtp_host = hs.config.email.email_smtp_host + self._smtp_port = hs.config.email.email_smtp_port + self._smtp_user = hs.config.email.email_smtp_user + self._smtp_pass = hs.config.email.email_smtp_pass + self._require_transport_security = hs.config.email.require_transport_security + + async def send_email( + self, + email_address: str, + subject: str, + app_name: str, + html: str, + text: str, + ) -> None: + """Send a multipart email with the given information. + + Args: + email_address: The address to send the email to. + subject: The email's subject. + app_name: The app name to include in the From header. + html: The HTML content to include in the email. + text: The plain text content to include in the email. + """ + try: + from_string = self._from % {"app": app_name} + except (KeyError, TypeError): + from_string = self._from + + raw_from = email.utils.parseaddr(from_string)[1] + raw_to = email.utils.parseaddr(email_address)[1] + + if raw_to == "": + raise RuntimeError("Invalid 'to' address") + + html_part = MIMEText(html, "html", "utf8") + text_part = MIMEText(text, "plain", "utf8") + + multipart_msg = MIMEMultipart("alternative") + multipart_msg["Subject"] = subject + multipart_msg["From"] = from_string + multipart_msg["To"] = email_address + multipart_msg["Date"] = email.utils.formatdate() + multipart_msg["Message-ID"] = email.utils.make_msgid() + multipart_msg.attach(text_part) + multipart_msg.attach(html_part) + + logger.info("Sending email to %s" % email_address) + + await make_deferred_yieldable( + self._sendmail( + self._smtp_host, + raw_from, + raw_to, + multipart_msg.as_string().encode("utf8"), + reactor=self._reactor, + port=self._smtp_port, + requireAuthentication=self._smtp_user is not None, + username=self._smtp_user, + password=self._smtp_pass, + requireTransportSecurity=self._require_transport_security, + ) + ) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index c4b43b0d3f..5f9ea5003a 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -12,12 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import email.mime.multipart -import email.utils import logging import urllib.parse -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, TypeVar import bleach @@ -27,7 +23,6 @@ from synapse.api.errors import StoreError from synapse.config.emailconfig import EmailSubjectConfig from synapse.events import EventBase -from synapse.logging.context import make_deferred_yieldable from synapse.push.presentable_names import ( calculate_room_name, descriptor_from_member_events, @@ -108,7 +103,7 @@ def __init__( self.template_html = template_html self.template_text = template_text - self.sendmail = self.hs.get_sendmail() + self.send_email_handler = hs.get_send_email_handler() self.store = self.hs.get_datastore() self.state_store = self.hs.get_storage().state self.macaroon_gen = self.hs.get_macaroon_generator() @@ -310,17 +305,6 @@ async def send_email( self, email_address: str, subject: str, extra_template_vars: Dict[str, Any] ) -> None: """Send an email with the given information and template text""" - try: - from_string = self.hs.config.email_notif_from % {"app": self.app_name} - except TypeError: - from_string = self.hs.config.email_notif_from - - raw_from = email.utils.parseaddr(from_string)[1] - raw_to = email.utils.parseaddr(email_address)[1] - - if raw_to == "": - raise RuntimeError("Invalid 'to' address") - template_vars = { "app_name": self.app_name, "server_name": self.hs.config.server.server_name, @@ -329,35 +313,14 @@ async def send_email( template_vars.update(extra_template_vars) html_text = self.template_html.render(**template_vars) - html_part = MIMEText(html_text, "html", "utf8") - plain_text = self.template_text.render(**template_vars) - text_part = MIMEText(plain_text, "plain", "utf8") - - multipart_msg = MIMEMultipart("alternative") - multipart_msg["Subject"] = subject - multipart_msg["From"] = from_string - multipart_msg["To"] = email_address - multipart_msg["Date"] = email.utils.formatdate() - multipart_msg["Message-ID"] = email.utils.make_msgid() - multipart_msg.attach(text_part) - multipart_msg.attach(html_part) - - logger.info("Sending email to %s" % email_address) - - await make_deferred_yieldable( - self.sendmail( - self.hs.config.email_smtp_host, - raw_from, - raw_to, - multipart_msg.as_string().encode("utf8"), - reactor=self.hs.get_reactor(), - port=self.hs.config.email_smtp_port, - requireAuthentication=self.hs.config.email_smtp_user is not None, - username=self.hs.config.email_smtp_user, - password=self.hs.config.email_smtp_pass, - requireTransportSecurity=self.hs.config.require_transport_security, - ) + + await self.send_email_handler.send_email( + email_address=email_address, + subject=subject, + app_name=self.app_name, + html=html_text, + text=plain_text, ) async def _get_room_vars( diff --git a/synapse/server.py b/synapse/server.py index 2337d2d9b4..fec0024c89 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -104,6 +104,7 @@ from synapse.handlers.room_member import RoomMemberHandler, RoomMemberMasterHandler from synapse.handlers.room_member_worker import RoomMemberWorkerHandler from synapse.handlers.search import SearchHandler +from synapse.handlers.send_email import SendEmailHandler from synapse.handlers.set_password import SetPasswordHandler from synapse.handlers.space_summary import SpaceSummaryHandler from synapse.handlers.sso import SsoHandler @@ -549,6 +550,10 @@ def get_deactivate_account_handler(self) -> DeactivateAccountHandler: def get_search_handler(self) -> SearchHandler: return SearchHandler(self) + @cache_in_self + def get_send_email_handler(self) -> SendEmailHandler: + return SendEmailHandler(self) + @cache_in_self def get_set_password_handler(self) -> SetPasswordHandler: return SetPasswordHandler(self) From afb6dcf806d5a290d8cbd2c911c6a712ae3cf391 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 17 May 2021 11:34:39 +0100 Subject: [PATCH 164/619] 1.34.0 --- CHANGES.md | 11 +++++++++-- debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2ceae0ac8c..1e3fd130fd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,17 @@ -Synapse 1.34.0rc1 (2021-05-12) -============================== +Synapse 1.34.0 (2021-05-17) +=========================== This release deprecates the `room_invite_state_types` configuration setting. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/release-v1.34.0/UPGRADE.rst#upgrading-to-v1340) for instructions on updating your configuration file to use the new `room_prejoin_state` setting. This release also deprecates the `POST /_synapse/admin/v1/rooms//delete` admin API route. Server administrators are encouraged to update their scripts to use the new `DELETE /_synapse/admin/v1/rooms/` route instead. + +No significant changes. + + +Synapse 1.34.0rc1 (2021-05-12) +============================== + Features -------- diff --git a/debian/changelog b/debian/changelog index 76b82c172e..bf99ae772c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.34.0) stable; urgency=medium + + * New synapse release 1.34.0. + + -- Synapse Packaging team Mon, 17 May 2021 11:34:18 +0100 + matrix-synapse-py3 (1.33.2) stable; urgency=medium * New synapse release 1.33.2. diff --git a/synapse/__init__.py b/synapse/__init__.py index 15d54a1ceb..7498a6016f 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.34.0rc1" +__version__ = "1.34.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 8dde0bf8b3faa75763d6b0f0fb9413f3b8691067 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 17 May 2021 11:50:08 +0100 Subject: [PATCH 165/619] Update UPGRADE.rst --- UPGRADE.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/UPGRADE.rst b/UPGRADE.rst index 606e357b6e..9f61aad412 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -88,7 +88,7 @@ for example: Upgrading to v1.34.0 ==================== -`room_invite_state_types` configuration setting +``room_invite_state_types`` configuration setting ----------------------------------------------- The ``room_invite_state_types`` configuration setting has been deprecated and @@ -106,13 +106,10 @@ remove it from your configuration file. The default value used to be: - "m.room.encryption" - "m.room.name" -If you have customised this value by adding addition state types, you should -remove ``room_invite_state_types`` and configure ``additional_event_types`` with -your customisations. +If you have customised this value, you should remove ``room_invite_state_types`` and +configure ``room_prejoin_state`` instead. + -If you have customised this value by removing state types, you should rename -``room_invite_state_types`` to ``additional_event_types``, and set -``disable_default_event_types`` to ``true``. Upgrading to v1.33.0 ==================== From 13b0673b5a0bceafbcfce1407544c2421fd69210 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 17 May 2021 12:00:28 +0100 Subject: [PATCH 166/619] Changelog --- CHANGES.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1e3fd130fd..709436da97 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ This release deprecates the `room_invite_state_types` configuration setting. See This release also deprecates the `POST /_synapse/admin/v1/rooms//delete` admin API route. Server administrators are encouraged to update their scripts to use the new `DELETE /_synapse/admin/v1/rooms/` route instead. -No significant changes. +No significant changes since v1.34.0rc1. Synapse 1.34.0rc1 (2021-05-12) @@ -181,7 +181,7 @@ Synapse 1.32.1 (2021-04-21) =========================== This release fixes [a regression](https://github.com/matrix-org/synapse/issues/9853) -in Synapse 1.32.0 that caused connected Prometheus instances to become unstable. +in Synapse 1.32.0 that caused connected Prometheus instances to become unstable. However, as this release is still subject to the `LoggingContext` change in 1.32.0, it is recommended to remain on or downgrade to 1.31.0. @@ -197,11 +197,11 @@ Synapse 1.32.0 (2021-04-20) **Note:** This release introduces [a regression](https://github.com/matrix-org/synapse/issues/9853) that can overwhelm connected Prometheus instances. This issue was not present in -1.32.0rc1. If affected, it is recommended to downgrade to 1.31.0 in the meantime, and +1.32.0rc1. If affected, it is recommended to downgrade to 1.31.0 in the meantime, and follow [these instructions](https://github.com/matrix-org/synapse/pull/9854#issuecomment-823472183) to clean up any excess writeahead logs. -**Note:** This release also mistakenly included a change that may affected Synapse +**Note:** This release also mistakenly included a change that may affected Synapse modules that import `synapse.logging.context.LoggingContext`, such as [synapse-s3-storage-provider](https://github.com/matrix-org/synapse-s3-storage-provider). This will be fixed in a later Synapse version. @@ -212,8 +212,8 @@ This release removes the deprecated `GET /_synapse/admin/v1/users/` adm This release requires Application Services to use type `m.login.application_service` when registering users via the `/_matrix/client/r0/register` endpoint to comply with the spec. Please ensure your Application Services are up to date. -If you are using the `packages.matrix.org` Debian repository for Synapse packages, -note that we have recently updated the expiry date on the gpg signing key. If you see an +If you are using the `packages.matrix.org` Debian repository for Synapse packages, +note that we have recently updated the expiry date on the gpg signing key. If you see an error similar to `The following signatures were invalid: EXPKEYSIG F473DD4473365DE1`, you will need to get a fresh copy of the keys. You can do so with: From 9752849e2b41968613ca244e86311d805bdd27df Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 17 May 2021 09:01:19 -0400 Subject: [PATCH 167/619] Clarify comments in the space summary handler. (#9974) --- changelog.d/9974.misc | 1 + synapse/handlers/space_summary.py | 51 ++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 changelog.d/9974.misc diff --git a/changelog.d/9974.misc b/changelog.d/9974.misc new file mode 100644 index 0000000000..9ddee2618e --- /dev/null +++ b/changelog.d/9974.misc @@ -0,0 +1 @@ +Update comments in the space summary handler. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index e35d91832b..953356f34d 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -32,7 +32,6 @@ logger = logging.getLogger(__name__) # number of rooms to return. We'll stop once we hit this limit. -# TODO: allow clients to reduce this with a request param. MAX_ROOMS = 50 # max number of events to return per room. @@ -231,11 +230,15 @@ async def _summarize_local_room( Generate a room entry and a list of event entries for a given room. Args: - requester: The requesting user, or None if this is over federation. + requester: + The user requesting the summary, if it is a local request. None + if this is a federation request. room_id: The room ID to summarize. suggested_only: True if only suggested children should be returned. Otherwise, all children are returned. - max_children: The maximum number of children to return for this node. + max_children: + The maximum number of children rooms to include. This is capped + to a server-set limit. Returns: A tuple of: @@ -278,6 +281,26 @@ async def _summarize_remote_room( max_children: Optional[int], exclude_rooms: Iterable[str], ) -> Tuple[Sequence[JsonDict], Sequence[JsonDict]]: + """ + Request room entries and a list of event entries for a given room by querying a remote server. + + Args: + room: The room to summarize. + suggested_only: True if only suggested children should be returned. + Otherwise, all children are returned. + max_children: + The maximum number of children rooms to include. This is capped + to a server-set limit. + exclude_rooms: + Rooms IDs which do not need to be summarized. + + Returns: + A tuple of: + An iterable of rooms. + + An iterable of the sorted children events. This may be limited + to a maximum size or may include all children. + """ room_id = room.room_id logger.info("Requesting summary for %s via %s", room_id, room.via) @@ -310,8 +333,26 @@ async def _summarize_remote_room( ) async def _is_room_accessible(self, room_id: str, requester: Optional[str]) -> bool: - # if we have an authenticated requesting user, first check if they are in the - # room + """ + Calculate whether the room should be shown in the spaces summary. + + It should be included if: + + * The requester is joined or invited to the room. + * The history visibility is set to world readable. + + Args: + room_id: The room ID to summarize. + requester: + The user requesting the summary, if it is a local request. None + if this is a federation request. + + Returns: + True if the room should be included in the spaces summary. + """ + + # if we have an authenticated requesting user, first check if they are able to view + # stripped state in the room. if requester: try: await self._auth.check_user_in_room(room_id, requester) From 206a7b5f12fd3d88ec24a1f53ce75e5b701faed8 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 17 May 2021 09:59:17 -0400 Subject: [PATCH 168/619] Fix the allowed range of valid ordering characters for spaces. (#10002) \x7F was meant to be \0x7E (~) this was originally incorrect in MSC1772. --- changelog.d/10002.bugfix | 1 + synapse/handlers/space_summary.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10002.bugfix diff --git a/changelog.d/10002.bugfix b/changelog.d/10002.bugfix new file mode 100644 index 0000000000..1fabdad22e --- /dev/null +++ b/changelog.d/10002.bugfix @@ -0,0 +1 @@ +Fix a validation bug introduced in v1.34.0 in the ordering of spaces in the space summary API. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 953356f34d..eb80a5ad67 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -471,8 +471,8 @@ def _is_suggested_child_event(edge_event: EventBase) -> bool: return False -# Order may only contain characters in the range of \x20 (space) to \x7F (~). -_INVALID_ORDER_CHARS_RE = re.compile(r"[^\x20-\x7F]") +# Order may only contain characters in the range of \x20 (space) to \x7E (~) inclusive. +_INVALID_ORDER_CHARS_RE = re.compile(r"[^\x20-\x7E]") def _child_events_comparison_key(child: EventBase) -> Tuple[bool, Optional[str], str]: From 4d6e5a5e995590efe44855d10dcd2a89b841dae8 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 18 May 2021 14:13:45 +0100 Subject: [PATCH 169/619] Use a database table to hold the users that should have full presence sent to them, instead of something in-memory (#9823) --- changelog.d/9823.misc | 1 + docs/presence_router_module.md | 6 +- synapse/handlers/presence.py | 136 ++++++-- synapse/module_api/__init__.py | 63 ++-- synapse/replication/http/presence.py | 11 +- synapse/rest/admin/server_notice_servlet.py | 8 +- synapse/storage/databases/main/presence.py | 58 +++- .../59/13users_to_send_full_presence_to.sql | 34 ++ tests/events/test_presence_router.py | 15 +- tests/module_api/test_api.py | 303 +++++++++++++----- .../test_sharded_event_persister.py | 2 +- 11 files changed, 479 insertions(+), 158 deletions(-) create mode 100644 changelog.d/9823.misc create mode 100644 synapse/storage/schema/main/delta/59/13users_to_send_full_presence_to.sql diff --git a/changelog.d/9823.misc b/changelog.d/9823.misc new file mode 100644 index 0000000000..bf924ab68c --- /dev/null +++ b/changelog.d/9823.misc @@ -0,0 +1 @@ +Allow sending full presence to users via workers other than the one that called `ModuleApi.send_local_online_presence_to`. \ No newline at end of file diff --git a/docs/presence_router_module.md b/docs/presence_router_module.md index d6566d978d..d2844915df 100644 --- a/docs/presence_router_module.md +++ b/docs/presence_router_module.md @@ -28,7 +28,11 @@ async def ModuleApi.send_local_online_presence_to(users: Iterable[str]) -> None which can be given a list of local or remote MXIDs to broadcast known, online user presence to (for those users that the receiving user is considered interested in). It does not include state for users who are currently offline, and it can only be -called on workers that support sending federation. +called on workers that support sending federation. Additionally, this method must +only be called from the process that has been configured to write to the +the [presence stream](https://github.com/matrix-org/synapse/blob/master/docs/workers.md#stream-writers). +By default, this is the main process, but another worker can be configured to do +so. ### Module structure diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 6fd1f34289..f5a049d754 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -222,9 +222,21 @@ async def current_state_for_users( @abc.abstractmethod async def set_state( - self, target_user: UserID, state: JsonDict, ignore_status_msg: bool = False + self, + target_user: UserID, + state: JsonDict, + ignore_status_msg: bool = False, + force_notify: bool = False, ) -> None: - """Set the presence state of the user. """ + """Set the presence state of the user. + + Args: + target_user: The ID of the user to set the presence state of. + state: The presence state as a JSON dictionary. + ignore_status_msg: True to ignore the "status_msg" field of the `state` dict. + If False, the user's current status will be updated. + force_notify: Whether to force notification of the update to clients. + """ @abc.abstractmethod async def bump_presence_active_time(self, user: UserID): @@ -296,6 +308,51 @@ async def maybe_send_presence_to_interested_destinations( for destinations, states in hosts_and_states: self._federation.send_presence_to_destinations(states, destinations) + async def send_full_presence_to_users(self, user_ids: Collection[str]): + """ + Adds to the list of users who should receive a full snapshot of presence + upon their next sync. Note that this only works for local users. + + Then, grabs the current presence state for a given set of users and adds it + to the top of the presence stream. + + Args: + user_ids: The IDs of the local users to send full presence to. + """ + # Retrieve one of the users from the given set + if not user_ids: + raise Exception( + "send_full_presence_to_users must be called with at least one user" + ) + user_id = next(iter(user_ids)) + + # Mark all users as receiving full presence on their next sync + await self.store.add_users_to_send_full_presence_to(user_ids) + + # Add a new entry to the presence stream. Since we use stream tokens to determine whether a + # local user should receive a full snapshot of presence when they sync, we need to bump the + # presence stream so that subsequent syncs with no presence activity in between won't result + # in the client receiving multiple full snapshots of presence. + # + # If we bump the stream ID, then the user will get a higher stream token next sync, and thus + # correctly won't receive a second snapshot. + + # Get the current presence state for one of the users (defaults to offline if not found) + current_presence_state = await self.get_state(UserID.from_string(user_id)) + + # Convert the UserPresenceState object into a serializable dict + state = { + "presence": current_presence_state.state, + "status_message": current_presence_state.status_msg, + } + + # Copy the presence state to the tip of the presence stream. + + # We set force_notify=True here so that this presence update is guaranteed to + # increment the presence stream ID (which resending the current user's presence + # otherwise would not do). + await self.set_state(UserID.from_string(user_id), state, force_notify=True) + class _NullContextManager(ContextManager[None]): """A context manager which does nothing.""" @@ -480,8 +537,17 @@ async def set_state( target_user: UserID, state: JsonDict, ignore_status_msg: bool = False, + force_notify: bool = False, ) -> None: - """Set the presence state of the user.""" + """Set the presence state of the user. + + Args: + target_user: The ID of the user to set the presence state of. + state: The presence state as a JSON dictionary. + ignore_status_msg: True to ignore the "status_msg" field of the `state` dict. + If False, the user's current status will be updated. + force_notify: Whether to force notification of the update to clients. + """ presence = state["presence"] valid_presence = ( @@ -508,6 +574,7 @@ async def set_state( user_id=user_id, state=state, ignore_status_msg=ignore_status_msg, + force_notify=force_notify, ) async def bump_presence_active_time(self, user: UserID) -> None: @@ -677,13 +744,19 @@ async def _persist_unpersisted_changes(self) -> None: [self.user_to_current_state[user_id] for user_id in unpersisted] ) - async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None: + async def _update_states( + self, new_states: Iterable[UserPresenceState], force_notify: bool = False + ) -> None: """Updates presence of users. Sets the appropriate timeouts. Pokes the notifier and federation if and only if the changed presence state should be sent to clients/servers. Args: new_states: The new user presence state updates to process. + force_notify: Whether to force notifying clients of this presence state update, + even if it doesn't change the state of a user's presence (e.g online -> online). + This is currently used to bump the max presence stream ID without changing any + user's presence (see PresenceHandler.add_users_to_send_full_presence_to). """ now = self.clock.time_msec() @@ -720,6 +793,9 @@ async def _update_states(self, new_states: Iterable[UserPresenceState]) -> None: now=now, ) + if force_notify: + should_notify = True + self.user_to_current_state[user_id] = new_state if should_notify: @@ -1058,9 +1134,21 @@ async def incoming_presence(self, origin: str, content: JsonDict) -> None: await self._update_states(updates) async def set_state( - self, target_user: UserID, state: JsonDict, ignore_status_msg: bool = False + self, + target_user: UserID, + state: JsonDict, + ignore_status_msg: bool = False, + force_notify: bool = False, ) -> None: - """Set the presence state of the user.""" + """Set the presence state of the user. + + Args: + target_user: The ID of the user to set the presence state of. + state: The presence state as a JSON dictionary. + ignore_status_msg: True to ignore the "status_msg" field of the `state` dict. + If False, the user's current status will be updated. + force_notify: Whether to force notification of the update to clients. + """ status_msg = state.get("status_msg", None) presence = state["presence"] @@ -1091,7 +1179,9 @@ async def set_state( ): new_fields["last_active_ts"] = self.clock.time_msec() - await self._update_states([prev_state.copy_and_replace(**new_fields)]) + await self._update_states( + [prev_state.copy_and_replace(**new_fields)], force_notify=force_notify + ) async def is_visible(self, observed_user: UserID, observer_user: UserID) -> bool: """Returns whether a user can see another user's presence.""" @@ -1389,11 +1479,10 @@ def __init__(self, hs: "HomeServer"): # # Presence -> Notifier -> PresenceEventSource -> Presence # - # Same with get_module_api, get_presence_router + # Same with get_presence_router: # # AuthHandler -> Notifier -> PresenceEventSource -> ModuleApi -> AuthHandler self.get_presence_handler = hs.get_presence_handler - self.get_module_api = hs.get_module_api self.get_presence_router = hs.get_presence_router self.clock = hs.get_clock() self.store = hs.get_datastore() @@ -1424,16 +1513,21 @@ async def get_new_events( stream_change_cache = self.store.presence_stream_cache with Measure(self.clock, "presence.get_new_events"): - if user_id in self.get_module_api()._send_full_presence_to_local_users: - # This user has been specified by a module to receive all current, online - # user presence. Removing from_key and setting include_offline to false - # will do effectively this. - from_key = None - include_offline = False - if from_key is not None: from_key = int(from_key) + # Check if this user should receive all current, online user presence. We only + # bother to do this if from_key is set, as otherwise the user will receive all + # user presence anyways. + if await self.store.should_user_receive_full_presence_with_token( + user_id, from_key + ): + # This user has been specified by a module to receive all current, online + # user presence. Removing from_key and setting include_offline to false + # will do effectively this. + from_key = None + include_offline = False + max_token = self.store.get_current_presence_token() if from_key == max_token: # This is necessary as due to the way stream ID generators work @@ -1467,12 +1561,6 @@ async def get_new_events( user_id, include_offline, from_key ) - # Remove the user from the list of users to receive all presence - if user_id in self.get_module_api()._send_full_presence_to_local_users: - self.get_module_api()._send_full_presence_to_local_users.remove( - user_id - ) - return presence_updates, max_token # Make mypy happy. users_interested_in should now be a set @@ -1522,10 +1610,6 @@ async def get_new_events( ) presence_updates = list(users_to_state.values()) - # Remove the user from the list of users to receive all presence - if user_id in self.get_module_api()._send_full_presence_to_local_users: - self.get_module_api()._send_full_presence_to_local_users.remove(user_id) - if not include_offline: # Filter out offline presence states presence_updates = self._filter_offline_presence_state(presence_updates) diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index a1a2b9aecc..cecdc96bf5 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -56,14 +56,6 @@ def __init__(self, hs, auth_handler): self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient self._public_room_list_manager = PublicRoomListManager(hs) - # The next time these users sync, they will receive the current presence - # state of all local users. Users are added by send_local_online_presence_to, - # and removed after a successful sync. - # - # We make this a private variable to deter modules from accessing it directly, - # though other classes in Synapse will still do so. - self._send_full_presence_to_local_users = set() - @property def http_client(self): """Allows making outbound HTTP requests to remote resources. @@ -405,39 +397,44 @@ async def send_local_online_presence_to(self, users: Iterable[str]) -> None: Updates to remote users will be sent immediately, whereas local users will receive them on their next sync attempt. - Note that this method can only be run on the main or federation_sender worker - processes. + Note that this method can only be run on the process that is configured to write to the + presence stream. By default this is the main process. """ - if not self._hs.should_send_federation(): + if self._hs._instance_name not in self._hs.config.worker.writers.presence: raise Exception( "send_local_online_presence_to can only be run " - "on processes that send federation", + "on the process that is configured to write to the " + "presence stream (by default this is the main process)", ) + local_users = set() + remote_users = set() for user in users: if self._hs.is_mine_id(user): - # Modify SyncHandler._generate_sync_entry_for_presence to call - # presence_source.get_new_events with an empty `from_key` if - # that user's ID were in a list modified by ModuleApi somewhere. - # That user would then get all presence state on next incremental sync. - - # Force a presence initial_sync for this user next time - self._send_full_presence_to_local_users.add(user) + local_users.add(user) else: - # Retrieve presence state for currently online users that this user - # is considered interested in - presence_events, _ = await self._presence_stream.get_new_events( - UserID.from_string(user), from_key=None, include_offline=False - ) - - # Send to remote destinations. - - # We pull out the presence handler here to break a cyclic - # dependency between the presence router and module API. - presence_handler = self._hs.get_presence_handler() - await presence_handler.maybe_send_presence_to_interested_destinations( - presence_events - ) + remote_users.add(user) + + # We pull out the presence handler here to break a cyclic + # dependency between the presence router and module API. + presence_handler = self._hs.get_presence_handler() + + if local_users: + # Force a presence initial_sync for these users next time they sync. + await presence_handler.send_full_presence_to_users(local_users) + + for user in remote_users: + # Retrieve presence state for currently online users that this user + # is considered interested in. + presence_events, _ = await self._presence_stream.get_new_events( + UserID.from_string(user), from_key=None, include_offline=False + ) + + # Send to remote destinations. + destination = UserID.from_string(user).domain + presence_handler.get_federation_queue().send_presence_to_destinations( + presence_events, destination + ) class PublicRoomListManager: diff --git a/synapse/replication/http/presence.py b/synapse/replication/http/presence.py index f25307620d..bb00247953 100644 --- a/synapse/replication/http/presence.py +++ b/synapse/replication/http/presence.py @@ -73,6 +73,7 @@ class ReplicationPresenceSetState(ReplicationEndpoint): { "state": { ... }, "ignore_status_msg": false, + "force_notify": false } 200 OK @@ -91,17 +92,23 @@ def __init__(self, hs: "HomeServer"): self._presence_handler = hs.get_presence_handler() @staticmethod - async def _serialize_payload(user_id, state, ignore_status_msg=False): + async def _serialize_payload( + user_id, state, ignore_status_msg=False, force_notify=False + ): return { "state": state, "ignore_status_msg": ignore_status_msg, + "force_notify": force_notify, } async def _handle_request(self, request, user_id): content = parse_json_object_from_request(request) await self._presence_handler.set_state( - UserID.from_string(user_id), content["state"], content["ignore_status_msg"] + UserID.from_string(user_id), + content["state"], + content["ignore_status_msg"], + content["force_notify"], ) return ( diff --git a/synapse/rest/admin/server_notice_servlet.py b/synapse/rest/admin/server_notice_servlet.py index cc3ab5854b..b5e4c474ef 100644 --- a/synapse/rest/admin/server_notice_servlet.py +++ b/synapse/rest/admin/server_notice_servlet.py @@ -54,7 +54,6 @@ def __init__(self, hs: "HomeServer"): self.hs = hs self.auth = hs.get_auth() self.txns = HttpTransactionCache(hs) - self.snm = hs.get_server_notices_manager() def register(self, json_resource: HttpServer): PATTERN = "/send_server_notice" @@ -77,7 +76,10 @@ async def on_POST( event_type = body.get("type", EventTypes.Message) state_key = body.get("state_key") - if not self.snm.is_enabled(): + # We grab the server notices manager here as its initialisation has a check for worker processes, + # but worker processes still need to initialise SendServerNoticeServlet (as it is part of the + # admin api). + if not self.hs.get_server_notices_manager().is_enabled(): raise SynapseError(400, "Server notices are not enabled on this server") user_id = body["user_id"] @@ -85,7 +87,7 @@ async def on_POST( if not self.hs.is_mine_id(user_id): raise SynapseError(400, "Server notices can only be sent to local users") - event = await self.snm.send_notice( + event = await self.hs.get_server_notices_manager().send_notice( user_id=body["user_id"], type=event_type, state_key=state_key, diff --git a/synapse/storage/databases/main/presence.py b/synapse/storage/databases/main/presence.py index db22fab23e..669a2af884 100644 --- a/synapse/storage/databases/main/presence.py +++ b/synapse/storage/databases/main/presence.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Dict, List, Tuple +from typing import TYPE_CHECKING, Dict, Iterable, List, Tuple from synapse.api.presence import PresenceState, UserPresenceState from synapse.replication.tcp.streams import PresenceStream @@ -57,6 +57,7 @@ def __init__( db_conn, "presence_stream", "stream_id" ) + self.hs = hs self._presence_on_startup = self._get_active_presence(db_conn) presence_cache_prefill, min_presence_val = self.db_pool.get_cache_dict( @@ -210,6 +211,61 @@ async def get_presence_for_users(self, user_ids): return {row["user_id"]: UserPresenceState(**row) for row in rows} + async def should_user_receive_full_presence_with_token( + self, + user_id: str, + from_token: int, + ) -> bool: + """Check whether the given user should receive full presence using the stream token + they're updating from. + + Args: + user_id: The ID of the user to check. + from_token: The stream token included in their /sync token. + + Returns: + True if the user should have full presence sent to them, False otherwise. + """ + + def _should_user_receive_full_presence_with_token_txn(txn): + sql = """ + SELECT 1 FROM users_to_send_full_presence_to + WHERE user_id = ? + AND presence_stream_id >= ? + """ + txn.execute(sql, (user_id, from_token)) + return bool(txn.fetchone()) + + return await self.db_pool.runInteraction( + "should_user_receive_full_presence_with_token", + _should_user_receive_full_presence_with_token_txn, + ) + + async def add_users_to_send_full_presence_to(self, user_ids: Iterable[str]): + """Adds to the list of users who should receive a full snapshot of presence + upon their next sync. + + Args: + user_ids: An iterable of user IDs. + """ + # Add user entries to the table, updating the presence_stream_id column if the user already + # exists in the table. + await self.db_pool.simple_upsert_many( + table="users_to_send_full_presence_to", + key_names=("user_id",), + key_values=[(user_id,) for user_id in user_ids], + value_names=("presence_stream_id",), + # We save the current presence stream ID token along with the user ID entry so + # that when a user /sync's, even if they syncing multiple times across separate + # devices at different times, each device will receive full presence once - when + # the presence stream ID in their sync token is less than the one in the table + # for their user ID. + value_values=( + (self._presence_id_gen.get_current_token(),) for _ in user_ids + ), + desc="add_users_to_send_full_presence_to", + ) + async def get_presence_for_all_users( self, include_offline: bool = True, diff --git a/synapse/storage/schema/main/delta/59/13users_to_send_full_presence_to.sql b/synapse/storage/schema/main/delta/59/13users_to_send_full_presence_to.sql new file mode 100644 index 0000000000..07b0f53ecf --- /dev/null +++ b/synapse/storage/schema/main/delta/59/13users_to_send_full_presence_to.sql @@ -0,0 +1,34 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Add a table that keeps track of a list of users who should, upon their next +-- sync request, receive presence for all currently online users that they are +-- "interested" in. + +-- The motivation for a DB table over an in-memory list is so that this list +-- can be added to and retrieved from by any worker. Specifically, we don't +-- want to duplicate work across multiple sync workers. + +CREATE TABLE IF NOT EXISTS users_to_send_full_presence_to( + -- The user ID to send full presence to. + user_id TEXT PRIMARY KEY, + -- A presence stream ID token - the current presence stream token when the row was last upserted. + -- If a user calls /sync and this token is part of the update they're to receive, we also include + -- full user presence in the response. + -- This allows multiple devices for a user to receive full presence whenever they next call /sync. + presence_stream_id BIGINT, + FOREIGN KEY (user_id) + REFERENCES users (name) +); \ No newline at end of file diff --git a/tests/events/test_presence_router.py b/tests/events/test_presence_router.py index 01d257307c..875b0d0a11 100644 --- a/tests/events/test_presence_router.py +++ b/tests/events/test_presence_router.py @@ -302,11 +302,18 @@ def test_send_local_online_presence_to_with_module(self): ) # Check that the expected presence updates were sent - expected_users = [ + # We explicitly compare using sets as we expect that calling + # module_api.send_local_online_presence_to will create a presence + # update that is a duplicate of the specified user's current presence. + # These are sent to clients and will be picked up below, thus we use a + # set to deduplicate. We're just interested that non-offline updates were + # sent out for each user ID. + expected_users = { self.other_user_id, self.presence_receiving_user_one_id, self.presence_receiving_user_two_id, - ] + } + found_users = set() calls = ( self.hs.get_federation_transport_client().send_transaction.call_args_list @@ -326,12 +333,12 @@ def test_send_local_online_presence_to_with_module(self): # EDUs can contain multiple presence updates for presence_update in edu["content"]["push"]: # Check for presence updates that contain the user IDs we're after - expected_users.remove(presence_update["user_id"]) + found_users.add(presence_update["user_id"]) # Ensure that no offline states are being sent out self.assertNotEqual(presence_update["presence"], "offline") - self.assertEqual(len(expected_users), 0) + self.assertEqual(found_users, expected_users) def send_presence_update( diff --git a/tests/module_api/test_api.py b/tests/module_api/test_api.py index 742ad14b8c..2c68b9a13c 100644 --- a/tests/module_api/test_api.py +++ b/tests/module_api/test_api.py @@ -13,6 +13,8 @@ # limitations under the License. from unittest.mock import Mock +from twisted.internet import defer + from synapse.api.constants import EduTypes from synapse.events import EventBase from synapse.federation.units import Transaction @@ -22,11 +24,13 @@ from synapse.types import create_requester from tests.events.test_presence_router import send_presence_update, sync_presence +from tests.replication._base import BaseMultiWorkerStreamTestCase from tests.test_utils.event_injection import inject_member_event -from tests.unittest import FederatingHomeserverTestCase, override_config +from tests.unittest import HomeserverTestCase, override_config +from tests.utils import USE_POSTGRES_FOR_TESTS -class ModuleApiTestCase(FederatingHomeserverTestCase): +class ModuleApiTestCase(HomeserverTestCase): servlets = [ admin.register_servlets, login.register_servlets, @@ -217,97 +221,16 @@ def test_public_rooms(self): ) self.assertFalse(is_in_public_rooms) - # The ability to send federation is required by send_local_online_presence_to. - @override_config({"send_federation": True}) def test_send_local_online_presence_to(self): - """Tests that send_local_presence_to_users sends local online presence to local users.""" - # Create a user who will send presence updates - self.presence_receiver_id = self.register_user("presence_receiver", "monkey") - self.presence_receiver_tok = self.login("presence_receiver", "monkey") - - # And another user that will send presence updates out - self.presence_sender_id = self.register_user("presence_sender", "monkey") - self.presence_sender_tok = self.login("presence_sender", "monkey") - - # Put them in a room together so they will receive each other's presence updates - room_id = self.helper.create_room_as( - self.presence_receiver_id, - tok=self.presence_receiver_tok, - ) - self.helper.join(room_id, self.presence_sender_id, tok=self.presence_sender_tok) - - # Presence sender comes online - send_presence_update( - self, - self.presence_sender_id, - self.presence_sender_tok, - "online", - "I'm online!", - ) - - # Presence receiver should have received it - presence_updates, sync_token = sync_presence(self, self.presence_receiver_id) - self.assertEqual(len(presence_updates), 1) - - presence_update = presence_updates[0] # type: UserPresenceState - self.assertEqual(presence_update.user_id, self.presence_sender_id) - self.assertEqual(presence_update.state, "online") - - # Syncing again should result in no presence updates - presence_updates, sync_token = sync_presence( - self, self.presence_receiver_id, sync_token - ) - self.assertEqual(len(presence_updates), 0) - - # Trigger sending local online presence - self.get_success( - self.module_api.send_local_online_presence_to( - [ - self.presence_receiver_id, - ] - ) - ) - - # Presence receiver should have received online presence again - presence_updates, sync_token = sync_presence( - self, self.presence_receiver_id, sync_token - ) - self.assertEqual(len(presence_updates), 1) - - presence_update = presence_updates[0] # type: UserPresenceState - self.assertEqual(presence_update.user_id, self.presence_sender_id) - self.assertEqual(presence_update.state, "online") - - # Presence sender goes offline - send_presence_update( - self, - self.presence_sender_id, - self.presence_sender_tok, - "offline", - "I slink back into the darkness.", - ) - - # Trigger sending local online presence - self.get_success( - self.module_api.send_local_online_presence_to( - [ - self.presence_receiver_id, - ] - ) - ) - - # Presence receiver should *not* have received offline state - presence_updates, sync_token = sync_presence( - self, self.presence_receiver_id, sync_token - ) - self.assertEqual(len(presence_updates), 0) + # Test sending local online presence to users from the main process + _test_sending_local_online_presence_to_local_user(self, test_with_workers=False) @override_config({"send_federation": True}) def test_send_local_online_presence_to_federation(self): """Tests that send_local_presence_to_users sends local online presence to remote users.""" # Create a user who will send presence updates - self.presence_sender_id = self.register_user("presence_sender", "monkey") - self.presence_sender_tok = self.login("presence_sender", "monkey") + self.presence_sender_id = self.register_user("presence_sender1", "monkey") + self.presence_sender_tok = self.login("presence_sender1", "monkey") # And a room they're a part of room_id = self.helper.create_room_as( @@ -374,3 +297,209 @@ def test_send_local_online_presence_to_federation(self): found_update = True self.assertTrue(found_update) + + +class ModuleApiWorkerTestCase(BaseMultiWorkerStreamTestCase): + """For testing ModuleApi functionality in a multi-worker setup""" + + # Testing stream ID replication from the main to worker processes requires postgres + # (due to needing `MultiWriterIdGenerator`). + if not USE_POSTGRES_FOR_TESTS: + skip = "Requires Postgres" + + servlets = [ + admin.register_servlets, + login.register_servlets, + room.register_servlets, + presence.register_servlets, + ] + + def default_config(self): + conf = super().default_config() + conf["redis"] = {"enabled": "true"} + conf["stream_writers"] = {"presence": ["presence_writer"]} + conf["instance_map"] = { + "presence_writer": {"host": "testserv", "port": 1001}, + } + return conf + + def prepare(self, reactor, clock, homeserver): + self.module_api = homeserver.get_module_api() + self.sync_handler = homeserver.get_sync_handler() + + def test_send_local_online_presence_to_workers(self): + # Test sending local online presence to users from a worker process + _test_sending_local_online_presence_to_local_user(self, test_with_workers=True) + + +def _test_sending_local_online_presence_to_local_user( + test_case: HomeserverTestCase, test_with_workers: bool = False +): + """Tests that send_local_presence_to_users sends local online presence to local users. + + This simultaneously tests two different usecases: + * Testing that this method works when either called from a worker or the main process. + - We test this by calling this method from both a TestCase that runs in monolith mode, and one that + runs with a main and generic_worker. + * Testing that multiple devices syncing simultaneously will all receive a snapshot of local, + online presence - but only once per device. + + Args: + test_with_workers: If True, this method will call ModuleApi.send_local_online_presence_to on a + worker process. The test users will still sync with the main process. The purpose of testing + with a worker is to check whether a Synapse module running on a worker can inform other workers/ + the main process that they should include additional presence when a user next syncs. + """ + if test_with_workers: + # Create a worker process to make module_api calls against + worker_hs = test_case.make_worker_hs( + "synapse.app.generic_worker", {"worker_name": "presence_writer"} + ) + + # Create a user who will send presence updates + test_case.presence_receiver_id = test_case.register_user( + "presence_receiver1", "monkey" + ) + test_case.presence_receiver_tok = test_case.login("presence_receiver1", "monkey") + + # And another user that will send presence updates out + test_case.presence_sender_id = test_case.register_user("presence_sender2", "monkey") + test_case.presence_sender_tok = test_case.login("presence_sender2", "monkey") + + # Put them in a room together so they will receive each other's presence updates + room_id = test_case.helper.create_room_as( + test_case.presence_receiver_id, + tok=test_case.presence_receiver_tok, + ) + test_case.helper.join( + room_id, test_case.presence_sender_id, tok=test_case.presence_sender_tok + ) + + # Presence sender comes online + send_presence_update( + test_case, + test_case.presence_sender_id, + test_case.presence_sender_tok, + "online", + "I'm online!", + ) + + # Presence receiver should have received it + presence_updates, sync_token = sync_presence( + test_case, test_case.presence_receiver_id + ) + test_case.assertEqual(len(presence_updates), 1) + + presence_update = presence_updates[0] # type: UserPresenceState + test_case.assertEqual(presence_update.user_id, test_case.presence_sender_id) + test_case.assertEqual(presence_update.state, "online") + + if test_with_workers: + # Replicate the current sync presence token from the main process to the worker process. + # We need to do this so that the worker process knows the current presence stream ID to + # insert into the database when we call ModuleApi.send_local_online_presence_to. + test_case.replicate() + + # Syncing again should result in no presence updates + presence_updates, sync_token = sync_presence( + test_case, test_case.presence_receiver_id, sync_token + ) + test_case.assertEqual(len(presence_updates), 0) + + # We do an (initial) sync with a second "device" now, getting a new sync token. + # We'll use this in a moment. + _, sync_token_second_device = sync_presence( + test_case, test_case.presence_receiver_id + ) + + # Determine on which process (main or worker) to call ModuleApi.send_local_online_presence_to on + if test_with_workers: + module_api_to_use = worker_hs.get_module_api() + else: + module_api_to_use = test_case.module_api + + # Trigger sending local online presence. We expect this information + # to be saved to the database where all processes can access it. + # Note that we're syncing via the master. + d = module_api_to_use.send_local_online_presence_to( + [ + test_case.presence_receiver_id, + ] + ) + d = defer.ensureDeferred(d) + + if test_with_workers: + # In order for the required presence_set_state replication request to occur between the + # worker and main process, we need to pump the reactor. Otherwise, the coordinator that + # reads the request on the main process won't do so, and the request will time out. + while not d.called: + test_case.reactor.advance(0.1) + + test_case.get_success(d) + + # The presence receiver should have received online presence again. + presence_updates, sync_token = sync_presence( + test_case, test_case.presence_receiver_id, sync_token + ) + test_case.assertEqual(len(presence_updates), 1) + + presence_update = presence_updates[0] # type: UserPresenceState + test_case.assertEqual(presence_update.user_id, test_case.presence_sender_id) + test_case.assertEqual(presence_update.state, "online") + + # We attempt to sync with the second sync token we received above - just to check that + # multiple syncing devices will each receive the necessary online presence. + presence_updates, sync_token_second_device = sync_presence( + test_case, test_case.presence_receiver_id, sync_token_second_device + ) + test_case.assertEqual(len(presence_updates), 1) + + presence_update = presence_updates[0] # type: UserPresenceState + test_case.assertEqual(presence_update.user_id, test_case.presence_sender_id) + test_case.assertEqual(presence_update.state, "online") + + # However, if we now sync with either "device", we won't receive another burst of online presence + # until the API is called again sometime in the future + presence_updates, sync_token = sync_presence( + test_case, test_case.presence_receiver_id, sync_token + ) + + # Now we check that we don't receive *offline* updates using ModuleApi.send_local_online_presence_to. + + # Presence sender goes offline + send_presence_update( + test_case, + test_case.presence_sender_id, + test_case.presence_sender_tok, + "offline", + "I slink back into the darkness.", + ) + + # Presence receiver should have received the updated, offline state + presence_updates, sync_token = sync_presence( + test_case, test_case.presence_receiver_id, sync_token + ) + test_case.assertEqual(len(presence_updates), 1) + + # Now trigger sending local online presence. + d = module_api_to_use.send_local_online_presence_to( + [ + test_case.presence_receiver_id, + ] + ) + d = defer.ensureDeferred(d) + + if test_with_workers: + # In order for the required presence_set_state replication request to occur between the + # worker and main process, we need to pump the reactor. Otherwise, the coordinator that + # reads the request on the main process won't do so, and the request will time out. + while not d.called: + test_case.reactor.advance(0.1) + + test_case.get_success(d) + + # Presence receiver should *not* have received offline state + presence_updates, sync_token = sync_presence( + test_case, test_case.presence_receiver_id, sync_token + ) + test_case.assertEqual(len(presence_updates), 0) diff --git a/tests/replication/test_sharded_event_persister.py b/tests/replication/test_sharded_event_persister.py index d739eb6b17..5eca5c165d 100644 --- a/tests/replication/test_sharded_event_persister.py +++ b/tests/replication/test_sharded_event_persister.py @@ -30,7 +30,7 @@ class EventPersisterShardTestCase(BaseMultiWorkerStreamTestCase): """Checks event persisting sharding works""" # Event persister sharding requires postgres (due to needing - # `MutliWriterIdGenerator`). + # `MultiWriterIdGenerator`). if not USE_POSTGRES_FOR_TESTS: skip = "Requires Postgres" From ac6bfcd52f03e9574324978f83a281cf35f4ea89 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 18 May 2021 12:17:04 -0400 Subject: [PATCH 170/619] Refactor checking restricted join rules (#10007) To be more consistent with similar code. The check now automatically raises an AuthError instead of passing back a boolean. It also absorbs some shared logic between callers. --- changelog.d/10007.feature | 1 + synapse/handlers/event_auth.py | 51 ++++++++++++++++++++++----------- synapse/handlers/federation.py | 29 ++++++------------- synapse/handlers/room_member.py | 20 ++++--------- 4 files changed, 50 insertions(+), 51 deletions(-) create mode 100644 changelog.d/10007.feature diff --git a/changelog.d/10007.feature b/changelog.d/10007.feature new file mode 100644 index 0000000000..2c655350c0 --- /dev/null +++ b/changelog.d/10007.feature @@ -0,0 +1 @@ +Experimental support to allow a user who could join a restricted room to view it in the spaces summary. diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index eff639f407..5b2fe103e7 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -11,10 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from synapse.api.constants import EventTypes, JoinRules +from synapse.api.constants import EventTypes, JoinRules, Membership +from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersion +from synapse.events import EventBase from synapse.types import StateMap if TYPE_CHECKING: @@ -29,44 +31,58 @@ class EventAuthHandler: def __init__(self, hs: "HomeServer"): self._store = hs.get_datastore() - async def can_join_without_invite( - self, state_ids: StateMap[str], room_version: RoomVersion, user_id: str - ) -> bool: + async def check_restricted_join_rules( + self, + state_ids: StateMap[str], + room_version: RoomVersion, + user_id: str, + prev_member_event: Optional[EventBase], + ) -> None: """ - Check whether a user can join a room without an invite. + Check whether a user can join a room without an invite due to restricted join rules. When joining a room with restricted joined rules (as defined in MSC3083), - the membership of spaces must be checked during join. + the membership of spaces must be checked during a room join. Args: state_ids: The state of the room as it currently is. room_version: The room version of the room being joined. user_id: The user joining the room. + prev_member_event: The current membership event for this user. - Returns: - True if the user can join the room, false otherwise. + Raises: + AuthError if the user cannot join the room. """ + # If the member is invited or currently joined, then nothing to do. + if prev_member_event and ( + prev_member_event.membership in (Membership.JOIN, Membership.INVITE) + ): + return + # This only applies to room versions which support the new join rule. if not room_version.msc3083_join_rules: - return True + return # If there's no join rule, then it defaults to invite (so this doesn't apply). join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) if not join_rules_event_id: - return True + return # If the join rule is not restricted, this doesn't apply. join_rules_event = await self._store.get_event(join_rules_event_id) if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: - return True + return # If allowed is of the wrong form, then only allow invited users. allowed_spaces = join_rules_event.content.get("allow", []) if not isinstance(allowed_spaces, list): - return False + allowed_spaces = () # Get the list of joined rooms and see if there's an overlap. - joined_rooms = await self._store.get_rooms_for_user(user_id) + if allowed_spaces: + joined_rooms = await self._store.get_rooms_for_user(user_id) + else: + joined_rooms = () # Pull out the other room IDs, invalid data gets filtered. for space in allowed_spaces: @@ -80,7 +96,10 @@ async def can_join_without_invite( # The user was joined to one of the spaces specified, they can join # this room! if space_id in joined_rooms: - return True + return # The user was not in any of the required spaces. - return False + raise AuthError( + 403, + "You do not belong to any of the required spaces to join this room.", + ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 798ed75b30..678f6b7707 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1668,28 +1668,17 @@ async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: # Check if the user is already in the room or invited to the room. user_id = event.state_key prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None) - newly_joined = True - user_is_invited = False + prev_member_event = None if prev_member_event_id: prev_member_event = await self.store.get_event(prev_member_event_id) - newly_joined = prev_member_event.membership != Membership.JOIN - user_is_invited = prev_member_event.membership == Membership.INVITE - - # If the member is not already in the room, and not invited, check if - # they should be allowed access via membership in a space. - if ( - newly_joined - and not user_is_invited - and not await self._event_auth_handler.can_join_without_invite( - prev_state_ids, - event.room_version, - user_id, - ) - ): - raise AuthError( - 403, - "You do not belong to any of the required spaces to join this room.", - ) + + # Check if the member should be allowed access via membership in a space. + await self._event_auth_handler.check_restricted_join_rules( + prev_state_ids, + event.room_version, + user_id, + prev_member_event, + ) # Persist the event. await self._auth_and_persist_event(origin, event, context) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 9a092da715..d6fc43e798 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -260,25 +260,15 @@ async def _local_membership_update( if event.membership == Membership.JOIN: newly_joined = True - user_is_invited = False + prev_member_event = None if prev_member_event_id: prev_member_event = await self.store.get_event(prev_member_event_id) newly_joined = prev_member_event.membership != Membership.JOIN - user_is_invited = prev_member_event.membership == Membership.INVITE - # If the member is not already in the room and is not accepting an invite, - # check if they should be allowed access via membership in a space. - if ( - newly_joined - and not user_is_invited - and not await self.event_auth_handler.can_join_without_invite( - prev_state_ids, event.room_version, user_id - ) - ): - raise AuthError( - 403, - "You do not belong to any of the required spaces to join this room.", - ) + # Check if the member should be allowed access via membership in a space. + await self.event_auth_handler.check_restricted_join_rules( + prev_state_ids, event.room_version, user_id, prev_member_event + ) # Only rate-limit if the user actually joined the room, otherwise we'll end # up blocking profile updates. From 5bba1b49058a648197f217268a3978d8acf09c51 Mon Sep 17 00:00:00 2001 From: Savyasachee Jha Date: Wed, 19 May 2021 16:14:16 +0530 Subject: [PATCH 171/619] Hardened systemd unit files (#9803) Signed-off-by: Savyasachee Jha savya.jha@hawkradius.com --- changelog.d/9803.doc | 1 + contrib/systemd/override-hardened.conf | 71 ++++++++++++++++++++++++++ docs/systemd-with-workers/README.md | 30 +++++++++++ 3 files changed, 102 insertions(+) create mode 100644 changelog.d/9803.doc create mode 100644 contrib/systemd/override-hardened.conf diff --git a/changelog.d/9803.doc b/changelog.d/9803.doc new file mode 100644 index 0000000000..16c7ba7033 --- /dev/null +++ b/changelog.d/9803.doc @@ -0,0 +1 @@ +Add hardened systemd files as proposed in [#9760](https://github.com/matrix-org/synapse/issues/9760) and added them to `contrib/`. Change the docs to reflect the presence of these files. diff --git a/contrib/systemd/override-hardened.conf b/contrib/systemd/override-hardened.conf new file mode 100644 index 0000000000..b2fa3ae7c5 --- /dev/null +++ b/contrib/systemd/override-hardened.conf @@ -0,0 +1,71 @@ +[Service] +# The following directives give the synapse service R/W access to: +# - /run/matrix-synapse +# - /var/lib/matrix-synapse +# - /var/log/matrix-synapse + +RuntimeDirectory=matrix-synapse +StateDirectory=matrix-synapse +LogsDirectory=matrix-synapse + +###################### +## Security Sandbox ## +###################### + +# Make sure that the service has its own unshared tmpfs at /tmp and that it +# cannot see or change any real devices +PrivateTmp=true +PrivateDevices=true + +# We give no capabilities to a service by default +CapabilityBoundingSet= +AmbientCapabilities= + +# Protect the following from modification: +# - The entire filesystem +# - sysctl settings and loaded kernel modules +# - No modifications allowed to Control Groups +# - Hostname +# - System Clock +ProtectSystem=strict +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectClock=true +ProtectHostname=true + +# Prevent access to the following: +# - /home directory +# - Kernel logs +ProtectHome=tmpfs +ProtectKernelLogs=true + +# Make sure that the process can only see PIDs and process details of itself, +# and the second option disables seeing details of things like system load and +# I/O etc +ProtectProc=invisible +ProcSubset=pid + +# While not needed, we set these options explicitly +# - This process has been given access to the host network +# - It can also communicate with any IP Address +PrivateNetwork=false +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +IPAddressAllow=any + +# Restrict system calls to a sane bunch +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallFilter=~@privileged @resources @obsolete + +# Misc restrictions +# - Since the process is a python process it needs to be able to write and +# execute memory regions, so we set MemoryDenyWriteExecute to false +RestrictSUIDSGID=true +RemoveIPC=true +NoNewPrivileges=true +RestrictRealtime=true +RestrictNamespaces=true +LockPersonality=true +PrivateUsers=true +MemoryDenyWriteExecute=false diff --git a/docs/systemd-with-workers/README.md b/docs/systemd-with-workers/README.md index cfa36be7b4..a1135e9ed5 100644 --- a/docs/systemd-with-workers/README.md +++ b/docs/systemd-with-workers/README.md @@ -65,3 +65,33 @@ systemctl restart matrix-synapse-worker@federation_reader.service systemctl enable matrix-synapse-worker@federation_writer.service systemctl restart matrix-synapse.target ``` + +## Hardening + +**Optional:** If further hardening is desired, the file +`override-hardened.conf` may be copied from +`contrib/systemd/override-hardened.conf` in this repository to the location +`/etc/systemd/system/matrix-synapse.service.d/override-hardened.conf` (the +directory may have to be created). It enables certain sandboxing features in +systemd to further secure the synapse service. You may read the comments to +understand what the override file is doing. The same file will need to be copied +to +`/etc/systemd/system/matrix-synapse-worker@.service.d/override-hardened-worker.conf` +(this directory may also have to be created) in order to apply the same +hardening options to any worker processes. + +Once these files have been copied to their appropriate locations, simply reload +systemd's manager config files and restart all Synapse services to apply the hardening options. They will automatically +be applied at every restart as long as the override files are present at the +specified locations. + +```sh +systemctl daemon-reload + +# Restart services +systemctl restart matrix-synapse.target +``` + +In order to see their effect, you may run `systemd-analyze security +matrix-synapse.service` before and after applying the hardening options to see +the changes being applied at a glance. From 9c76d0561bdbe6741088fe8af1d336166058bb01 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 19 May 2021 11:47:16 +0100 Subject: [PATCH 172/619] Update the contrib grafana dashboard (#10001) --- changelog.d/10001.misc | 1 + contrib/grafana/synapse.json | 5623 ++++++++++++++++++++++++++-------- 2 files changed, 4269 insertions(+), 1355 deletions(-) create mode 100644 changelog.d/10001.misc diff --git a/changelog.d/10001.misc b/changelog.d/10001.misc new file mode 100644 index 0000000000..8740cc478d --- /dev/null +++ b/changelog.d/10001.misc @@ -0,0 +1 @@ +Update the Grafana dashboard in `contrib/`. diff --git a/contrib/grafana/synapse.json b/contrib/grafana/synapse.json index 539569b5b1..0c4816b7cd 100644 --- a/contrib/grafana/synapse.json +++ b/contrib/grafana/synapse.json @@ -14,7 +14,7 @@ "type": "grafana", "id": "grafana", "name": "Grafana", - "version": "6.7.4" + "version": "7.3.7" }, { "type": "panel", @@ -38,7 +38,6 @@ "annotations": { "list": [ { - "$$hashKey": "object:76", "builtIn": 1, "datasource": "$datasource", "enable": false, @@ -55,11 +54,12 @@ "gnetId": null, "graphTooltip": 0, "id": null, - "iteration": 1594646317221, + "iteration": 1621258266004, "links": [ { - "asDropdown": true, + "asDropdown": false, "icon": "external link", + "includeVars": true, "keepTime": true, "tags": [ "matrix" @@ -83,73 +83,255 @@ "title": "Overview", "type": "row" }, + { + "cards": { + "cardPadding": -1, + "cardRound": 0 + }, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateInferno", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "tsbuckets", + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 1 + }, + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "id": 189, + "legend": { + "show": false + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\"}[$bucket_size])) by (le)", + "format": "heatmap", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "title": "Event Send Time (excluding errors, all workers)", + "tooltip": { + "show": true, + "showHistogram": true + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": null, + "format": "s", + "logBase": 2, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", - "fill": 1, + "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 0, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, - "x": 0, + "x": 12, "y": 1 }, "hiddenSeries": false, - "id": 75, + "id": 152, "legend": { "avg": false, "current": false, "max": false, "min": false, + "rightSide": false, "show": true, "total": false, "values": false }, "lines": true, - "linewidth": 1, + "linewidth": 0, "links": [], - "nullPointMode": "null", + "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [], + "seriesOverrides": [ + { + "alias": "Avg", + "fill": 0, + "linewidth": 3 + }, + { + "alias": "99%", + "color": "#C4162A", + "fillBelowTo": "90%" + }, + { + "alias": "90%", + "color": "#FF7383", + "fillBelowTo": "75%" + }, + { + "alias": "75%", + "color": "#FFEE52", + "fillBelowTo": "50%" + }, + { + "alias": "50%", + "color": "#73BF69", + "fillBelowTo": "25%" + }, + { + "alias": "25%", + "color": "#1F60C4", + "fillBelowTo": "5%" + }, + { + "alias": "5%", + "lines": false + }, + { + "alias": "Average", + "color": "rgb(255, 255, 255)", + "lines": true, + "linewidth": 3 + }, + { + "alias": "Events", + "color": "#B877D9", + "hideTooltip": true, + "points": true, + "yaxis": 2, + "zindex": -3 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { - "expr": "rate(process_cpu_seconds_total{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "histogram_quantile(0.99, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size])) by (le))", "format": "time_series", "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} ", + "legendFormat": "99%", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.9, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size])) by (le))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "90%", "refId": "A" + }, + { + "expr": "histogram_quantile(0.75, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size])) by (le))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "75%", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size])) by (le))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "50%", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.25, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size])) by (le))", + "legendFormat": "25%", + "refId": "F" + }, + { + "expr": "histogram_quantile(0.05, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size])) by (le))", + "legendFormat": "5%", + "refId": "G" + }, + { + "expr": "sum(rate(synapse_http_server_response_time_seconds_sum{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size])) / sum(rate(synapse_http_server_response_time_seconds_count{servlet='RoomSendEventRestServlet',index=~\"$index\",instance=\"$instance\",code=~\"2..\"}[$bucket_size]))", + "legendFormat": "Average", + "refId": "H" + }, + { + "expr": "sum(rate(synapse_storage_events_persisted_events{instance=\"$instance\"}[$bucket_size]))", + "hide": false, + "instant": false, + "legendFormat": "Events", + "refId": "E" } ], "thresholds": [ { - "colorMode": "critical", - "fill": true, + "$$hashKey": "object:283", + "colorMode": "warning", + "fill": false, "line": true, "op": "gt", "value": 1, "yaxis": "left" + }, + { + "$$hashKey": "object:284", + "colorMode": "critical", + "fill": false, + "line": true, + "op": "gt", + "value": 2, + "yaxis": "left" } ], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "CPU usage", + "title": "Event Send Time Quantiles (excluding errors, all workers)", "tooltip": { "shared": true, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -162,20 +344,22 @@ }, "yaxes": [ { + "$$hashKey": "object:255", "decimals": null, - "format": "percentunit", - "label": null, + "format": "s", + "label": "", "logBase": 1, - "max": "1.5", + "max": null, "min": "0", "show": true }, { - "format": "short", - "label": null, + "$$hashKey": "object:256", + "format": "hertz", + "label": "", "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true } ], @@ -190,37 +374,42 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", - "editable": true, - "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, - "grid": {}, "gridPos": { "h": 9, "w": 12, - "x": 12, - "y": 1 + "x": 0, + "y": 10 }, "hiddenSeries": false, - "id": 33, + "id": 75, "legend": { "avg": false, "current": false, "max": false, "min": false, - "show": false, + "show": true, "total": false, "values": false }, "lines": true, - "linewidth": 2, + "linewidth": 3, "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -230,24 +419,33 @@ "steppedLine": false, "targets": [ { - "expr": "sum(rate(synapse_storage_events_persisted_events{instance=\"$instance\"}[$bucket_size])) without (job,index)", + "expr": "rate(process_cpu_seconds_total{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", "format": "time_series", - "intervalFactor": 2, - "legendFormat": "", - "refId": "A", - "step": 20, - "target": "" + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} ", + "refId": "A" + } + ], + "thresholds": [ + { + "$$hashKey": "object:566", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 1, + "yaxis": "left" } ], - "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Events Persisted", + "title": "CPU usage", "tooltip": { - "shared": true, + "shared": false, "sort": 0, - "value_type": "cumulative" + "value_type": "individual" }, "type": "graph", "xaxis": { @@ -259,14 +457,19 @@ }, "yaxes": [ { - "format": "hertz", + "$$hashKey": "object:538", + "decimals": null, + "format": "percentunit", + "label": null, "logBase": 1, - "max": null, - "min": null, + "max": "1.5", + "min": "0", "show": true }, { + "$$hashKey": "object:539", "format": "short", + "label": null, "logBase": 1, "max": null, "min": null, @@ -278,76 +481,24 @@ "alignLevel": null } }, - { - "cards": { - "cardPadding": 0, - "cardRound": null - }, - "color": { - "cardColor": "#b4ff00", - "colorScale": "sqrt", - "colorScheme": "interpolateSpectral", - "exponent": 0.5, - "mode": "spectrum" - }, - "dataFormat": "tsbuckets", - "datasource": "$datasource", - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 10 - }, - "heatmap": {}, - "hideZeroBuckets": true, - "highlightCards": true, - "id": 85, - "legend": { - "show": false - }, - "links": [], - "reverseYBuckets": false, - "targets": [ - { - "expr": "sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\"}[$bucket_size])) by (le)", - "format": "heatmap", - "intervalFactor": 1, - "legendFormat": "{{le}}", - "refId": "A" - } - ], - "title": "Event Send Time", - "tooltip": { - "show": true, - "showHistogram": false - }, - "type": "heatmap", - "xAxis": { - "show": true - }, - "xBucketNumber": null, - "xBucketSize": null, - "yAxis": { - "decimals": null, - "format": "s", - "logBase": 2, - "max": null, - "min": null, - "show": true, - "splitFactor": null - }, - "yBucketBound": "auto", - "yBucketNumber": null, - "yBucketSize": null - }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", - "fill": 0, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, "fillGradient": 0, + "grid": {}, "gridPos": { "h": 9, "w": 12, @@ -355,7 +506,7 @@ "y": 10 }, "hiddenSeries": false, - "id": 107, + "id": 198, "legend": { "avg": false, "current": false, @@ -366,76 +517,52 @@ "values": false }, "lines": true, - "linewidth": 1, + "linewidth": 3, "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "repeat": null, - "repeatDirection": "h", - "seriesOverrides": [ - { - "alias": "mean", - "linewidth": 2 - } - ], + "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { - "expr": "histogram_quantile(0.99, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\"}[$bucket_size])) without (job, index, method))", + "expr": "process_resident_memory_bytes{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", "format": "time_series", "interval": "", - "intervalFactor": 1, - "legendFormat": "99%", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\"}[$bucket_size])) without (job, index, method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "95%", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.90, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\"}[$bucket_size])) without (job, index, method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "90%", - "refId": "C" - }, - { - "expr": "histogram_quantile(0.50, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\"}[$bucket_size])) without (job, index, method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "50%", - "refId": "D" + "intervalFactor": 2, + "legendFormat": "{{job}} {{index}}", + "refId": "A", + "step": 20, + "target": "" }, { - "expr": "sum(rate(synapse_http_server_response_time_seconds_sum{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\"}[$bucket_size])) without (job, index, method) / sum(rate(synapse_http_server_response_time_seconds_count{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\"}[$bucket_size])) without (job, index, method)", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "mean", - "refId": "E" + "expr": "sum(process_resident_memory_bytes{instance=\"$instance\",job=~\"$job\",index=~\"$index\"})", + "hide": true, + "interval": "", + "legendFormat": "total", + "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Event send time quantiles", + "title": "Memory", "tooltip": { - "shared": true, + "shared": false, "sort": 0, - "value_type": "individual" + "value_type": "cumulative" }, + "transformations": [], "type": "graph", "xaxis": { "buckets": null, @@ -446,16 +573,16 @@ }, "yaxes": [ { - "format": "s", - "label": null, + "$$hashKey": "object:1560", + "format": "bytes", "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { + "$$hashKey": "object:1561", "format": "short", - "label": null, "logBase": 1, "max": null, "min": null, @@ -473,16 +600,23 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", - "fill": 0, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, "fillGradient": 0, "gridPos": { - "h": 9, + "h": 7, "w": 12, - "x": 0, + "x": 12, "y": 19 }, "hiddenSeries": false, - "id": 118, + "id": 37, "legend": { "avg": false, "current": false, @@ -497,18 +631,21 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "repeatDirection": "h", "seriesOverrides": [ { - "alias": "mean", - "linewidth": 2 + "$$hashKey": "object:639", + "alias": "/max$/", + "color": "#890F02", + "fill": 0, + "legend": false } ], "spaceLength": 10, @@ -516,49 +653,33 @@ "steppedLine": false, "targets": [ { - "expr": "histogram_quantile(0.99, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", + "expr": "process_open_fds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", "format": "time_series", + "hide": false, "interval": "", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} 99%", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.95, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} 95%", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.90, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} 90%", - "refId": "C" - }, - { - "expr": "histogram_quantile(0.50, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} 50%", - "refId": "D" + "intervalFactor": 2, + "legendFormat": "{{job}}-{{index}}", + "refId": "A", + "step": 20 }, { - "expr": "sum(rate(synapse_http_server_response_time_seconds_sum{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method) / sum(rate(synapse_http_server_response_time_seconds_count{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method)", + "expr": "process_max_fds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} mean", - "refId": "E" + "hide": true, + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{job}}-{{index}} max", + "refId": "B", + "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Event send time quantiles by worker", + "title": "Open FDs", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -572,14 +693,18 @@ }, "yaxes": [ { - "format": "s", - "label": null, + "$$hashKey": "object:650", + "decimals": null, + "format": "none", + "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { + "$$hashKey": "object:651", + "decimals": null, "format": "short", "label": null, "logBase": 1, @@ -600,7 +725,7 @@ "h": 1, "w": 24, "x": 0, - "y": 28 + "y": 26 }, "id": 54, "panels": [ @@ -612,6 +737,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -619,7 +751,7 @@ "h": 7, "w": 12, "x": 0, - "y": 2 + "y": 25 }, "hiddenSeries": false, "id": 5, @@ -637,22 +769,25 @@ "values": false }, "lines": true, - "linewidth": 1, + "linewidth": 3, "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { + "$$hashKey": "object:1240", "alias": "/user/" }, { + "$$hashKey": "object:1241", "alias": "/system/" } ], @@ -682,20 +817,33 @@ ], "thresholds": [ { + "$$hashKey": "object:1278", "colorMode": "custom", "fillColor": "rgba(255, 255, 255, 1)", "line": true, "lineColor": "rgba(216, 200, 27, 0.27)", "op": "gt", - "value": 0.5 + "value": 0.5, + "yaxis": "left" }, { + "$$hashKey": "object:1279", "colorMode": "custom", "fillColor": "rgba(255, 255, 255, 1)", "line": true, - "lineColor": "rgba(234, 112, 112, 0.22)", + "lineColor": "rgb(87, 6, 16)", + "op": "gt", + "value": 0.8, + "yaxis": "left" + }, + { + "$$hashKey": "object:1498", + "colorMode": "critical", + "fill": true, + "line": true, "op": "gt", - "value": 0.8 + "value": 1, + "yaxis": "left" } ], "timeFrom": null, @@ -703,7 +851,7 @@ "timeShift": null, "title": "CPU", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -717,6 +865,7 @@ }, "yaxes": [ { + "$$hashKey": "object:1250", "decimals": null, "format": "percentunit", "label": "", @@ -726,6 +875,7 @@ "show": true }, { + "$$hashKey": "object:1251", "format": "short", "logBase": 1, "max": null, @@ -744,16 +894,25 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "description": "Shows the time in which the given percentage of reactor ticks completed, over the sampled timespan", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 2 + "y": 25 }, "hiddenSeries": false, - "id": 37, + "id": 105, + "interval": "", "legend": { "avg": false, "current": false, @@ -768,51 +927,57 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [ - { - "alias": "/max$/", - "color": "#890F02", - "fill": 0, - "legend": false - } - ], + "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { - "expr": "process_open_fds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "expr": "histogram_quantile(0.99, rate(python_twisted_reactor_tick_time_bucket{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]))", "format": "time_series", - "hide": false, + "interval": "", "intervalFactor": 2, - "legendFormat": "{{job}}-{{index}}", + "legendFormat": "{{job}}-{{index}} 99%", "refId": "A", "step": 20 }, { - "expr": "process_max_fds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "expr": "histogram_quantile(0.95, rate(python_twisted_reactor_tick_time_bucket{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]))", "format": "time_series", - "hide": true, - "intervalFactor": 2, - "legendFormat": "{{job}}-{{index}} max", - "refId": "B", - "step": 20 + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} 95%", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.90, rate(python_twisted_reactor_tick_time_bucket{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} 90%", + "refId": "C" + }, + { + "expr": "rate(python_twisted_reactor_tick_time_sum{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]) / rate(python_twisted_reactor_tick_time_count{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} mean", + "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Open FDs", + "title": "Reactor tick quantiles", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -826,7 +991,7 @@ }, "yaxes": [ { - "format": "none", + "format": "s", "label": null, "logBase": 1, "max": null, @@ -839,7 +1004,7 @@ "logBase": 1, "max": null, "min": null, - "show": true + "show": false } ], "yaxis": { @@ -855,6 +1020,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 0, "fillGradient": 0, "grid": {}, @@ -862,7 +1034,7 @@ "h": 7, "w": 12, "x": 0, - "y": 9 + "y": 32 }, "hiddenSeries": false, "id": 34, @@ -880,10 +1052,11 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -895,11 +1068,18 @@ { "expr": "process_resident_memory_bytes{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", "format": "time_series", + "interval": "", "intervalFactor": 2, "legendFormat": "{{job}} {{index}}", "refId": "A", "step": 20, "target": "" + }, + { + "expr": "sum(process_resident_memory_bytes{instance=\"$instance\",job=~\"$job\",index=~\"$index\"})", + "interval": "", + "legendFormat": "total", + "refId": "B" } ], "thresholds": [], @@ -908,10 +1088,11 @@ "timeShift": null, "title": "Memory", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "cumulative" }, + "transformations": [], "type": "graph", "xaxis": { "buckets": null, @@ -947,18 +1128,23 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", - "description": "Shows the time in which the given percentage of reactor ticks completed, over the sampled timespan", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 9 + "y": 32 }, "hiddenSeries": false, - "id": 105, - "interval": "", + "id": 49, "legend": { "avg": false, "current": false, @@ -973,54 +1159,40 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [], + "seriesOverrides": [ + { + "alias": "/^up/", + "legend": false, + "yaxis": 2 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { - "expr": "histogram_quantile(0.99, rate(python_twisted_reactor_tick_time_bucket{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]))", + "expr": "scrape_duration_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", "format": "time_series", "interval": "", "intervalFactor": 2, - "legendFormat": "{{job}}-{{index}} 99%", + "legendFormat": "{{job}}-{{index}}", "refId": "A", "step": 20 - }, - { - "expr": "histogram_quantile(0.95, rate(python_twisted_reactor_tick_time_bucket{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} 95%", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.90, rate(python_twisted_reactor_tick_time_bucket{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} 90%", - "refId": "C" - }, - { - "expr": "rate(python_twisted_reactor_tick_time_sum{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size]) / rate(python_twisted_reactor_tick_time_count{index=~\"$index\",instance=\"$instance\",job=~\"$job\"}[$bucket_size])", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} mean", - "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Reactor tick quantiles", + "title": "Prometheus scrape time", "tooltip": { "shared": false, "sort": 0, @@ -1040,15 +1212,16 @@ "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { - "format": "short", - "label": null, + "decimals": 0, + "format": "none", + "label": "", "logBase": 1, - "max": null, - "min": null, + "max": "0", + "min": "-1", "show": false } ], @@ -1063,13 +1236,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 0, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, - "y": 16 + "y": 39 }, "hiddenSeries": false, "id": 53, @@ -1087,10 +1267,11 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -1113,7 +1294,7 @@ "timeShift": null, "title": "Up", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -1154,16 +1335,23 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 16 + "y": 39 }, "hiddenSeries": false, - "id": 49, + "id": 120, "legend": { "avg": false, "current": false, @@ -1176,43 +1364,56 @@ "lines": true, "linewidth": 1, "links": [], - "nullPointMode": "null", + "nullPointMode": "null as zero", "options": { - "dataLinks": [] + "alertThreshold": true }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", - "seriesOverrides": [ - { - "alias": "/^up/", - "legend": false, - "yaxis": 2 - } - ], + "seriesOverrides": [], "spaceLength": 10, - "stack": false, + "stack": true, "steppedLine": false, "targets": [ { - "expr": "scrape_duration_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "expr": "rate(synapse_http_server_response_ru_utime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])+rate(synapse_http_server_response_ru_stime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}} {{tag}}", + "refId": "A" + }, + { + "expr": "rate(synapse_background_process_ru_utime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])+rate(synapse_background_process_ru_stime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", "format": "time_series", + "hide": false, + "instant": false, "interval": "", - "intervalFactor": 2, - "legendFormat": "{{job}}-{{index}}", - "refId": "A", - "step": 20 + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{name}}", + "refId": "B" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 1, + "yaxis": "left" } ], - "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Prometheus scrape time", + "title": "Stacked CPU usage", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -1226,21 +1427,22 @@ }, "yaxes": [ { - "format": "s", + "$$hashKey": "object:572", + "format": "percentunit", "label": null, "logBase": 1, "max": null, - "min": "0", + "min": null, "show": true }, { - "decimals": 0, - "format": "none", - "label": "", + "$$hashKey": "object:573", + "format": "short", + "label": null, "logBase": 1, - "max": "0", - "min": "-1", - "show": false + "max": null, + "min": null, + "show": true } ], "yaxis": { @@ -1254,13 +1456,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, - "y": 23 + "y": 46 }, "hiddenSeries": false, "id": 136, @@ -1278,9 +1487,10 @@ "linewidth": 1, "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -1306,7 +1516,7 @@ "timeShift": null, "title": "Outgoing HTTP request rate", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -1340,6 +1550,90 @@ "align": false, "alignLevel": null } + } + ], + "repeat": null, + "title": "Process info", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 56, + "panels": [ + { + "cards": { + "cardPadding": -1, + "cardRound": 0 + }, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateInferno", + "exponent": 0.5, + "mode": "spectrum" + }, + "dataFormat": "tsbuckets", + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 21 + }, + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "id": 85, + "legend": { + "show": false + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\"}[$bucket_size])) by (le)", + "format": "heatmap", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "title": "Event Send Time (Including errors, across all workers)", + "tooltip": { + "show": true, + "showHistogram": true + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": null, + "format": "s", + "logBase": 2, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null }, { "aliasColors": {}, @@ -1347,79 +1641,74 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, + "grid": {}, "gridPos": { - "h": 7, + "h": 9, "w": 12, "x": 12, - "y": 23 + "y": 21 }, "hiddenSeries": false, - "id": 120, + "id": 33, "legend": { "avg": false, "current": false, "max": false, "min": false, - "show": true, + "show": false, "total": false, "values": false }, "lines": true, - "linewidth": 1, + "linewidth": 2, "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, + "paceLength": 10, "percentage": false, - "pointradius": 2, + "pluginVersion": "7.3.7", + "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, - "stack": true, + "stack": false, "steppedLine": false, "targets": [ { - "expr": "rate(synapse_http_server_response_ru_utime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])+rate(synapse_http_server_response_ru_stime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", - "format": "time_series", - "hide": false, - "instant": false, - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}} {{tag}}", - "refId": "A" - }, - { - "expr": "rate(synapse_background_process_ru_utime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])+rate(synapse_background_process_ru_stime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "sum(rate(synapse_storage_events_persisted_events{instance=\"$instance\"}[$bucket_size])) without (job,index)", "format": "time_series", - "hide": false, - "instant": false, "interval": "", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} {{name}}", - "refId": "B" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "line": true, - "op": "gt", - "value": 1, - "yaxis": "left" + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 20, + "target": "" } ], + "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Stacked CPU usage", + "title": "Events Persisted (all workers)", "tooltip": { - "shared": false, + "shared": true, "sort": 0, - "value_type": "individual" + "value_type": "cumulative" }, "type": "graph", "xaxis": { @@ -1431,16 +1720,16 @@ }, "yaxes": [ { - "format": "percentunit", - "label": null, + "$$hashKey": "object:102", + "format": "hertz", "logBase": 1, "max": null, "min": null, "show": true }, { + "$$hashKey": "object:103", "format": "short", - "label": null, "logBase": 1, "max": null, "min": null, @@ -1451,23 +1740,7 @@ "align": false, "alignLevel": null } - } - ], - "repeat": null, - "title": "Process info", - "type": "row" - }, - { - "collapsed": true, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 29 - }, - "id": 56, - "panels": [ + }, { "aliasColors": {}, "bars": false, @@ -1475,13 +1748,21 @@ "dashes": false, "datasource": "$datasource", "decimals": 1, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, - "y": 58 + "y": 30 }, + "hiddenSeries": false, "id": 40, "legend": { "avg": false, @@ -1496,7 +1777,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -1561,13 +1846,21 @@ "dashes": false, "datasource": "$datasource", "decimals": 1, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 58 + "y": 30 }, + "hiddenSeries": false, "id": 46, "legend": { "avg": false, @@ -1582,7 +1875,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -1651,13 +1948,21 @@ "dashes": false, "datasource": "$datasource", "decimals": 1, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, - "y": 65 + "y": 37 }, + "hiddenSeries": false, "id": 44, "legend": { "alignAsTable": true, @@ -1675,7 +1980,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -1741,13 +2050,21 @@ "dashes": false, "datasource": "$datasource", "decimals": 1, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 65 + "y": 37 }, + "hiddenSeries": false, "id": 45, "legend": { "alignAsTable": true, @@ -1765,7 +2082,11 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -1823,52 +2144,35 @@ "align": false, "alignLevel": null } - } - ], - "repeat": null, - "title": "Event persist rates", - "type": "row" - }, - { - "collapsed": true, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 30 - }, - "id": 57, - "panels": [ + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", - "decimals": null, - "editable": true, - "error": false, - "fill": 2, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 0, "fillGradient": 0, - "grid": {}, "gridPos": { - "h": 8, + "h": 9, "w": 12, "x": 0, - "y": 31 + "y": 44 }, "hiddenSeries": false, - "id": 4, + "id": 118, "legend": { - "alignAsTable": true, "avg": false, "current": false, - "hideEmpty": false, - "hideZero": true, "max": false, "min": false, - "rightSide": false, "show": true, "total": false, "values": false @@ -1878,50 +2182,212 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, + "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [], + "repeatDirection": "h", + "seriesOverrides": [ + { + "alias": "mean", + "linewidth": 2 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { - "expr": "rate(synapse_http_server_requests_received{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "histogram_quantile(0.99, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", "format": "time_series", "interval": "", - "intervalFactor": 2, - "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}} {{tag}}", - "refId": "A", - "step": 20 - } - ], - "thresholds": [ + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} 99%", + "refId": "A" + }, { - "colorMode": "custom", - "fill": true, - "fillColor": "rgba(216, 200, 27, 0.27)", - "op": "gt", - "value": 100 + "expr": "histogram_quantile(0.95, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} 95%", + "refId": "B" }, { - "colorMode": "custom", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.22)", - "op": "gt", - "value": 250 - } - ], + "expr": "histogram_quantile(0.90, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} 90%", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.50, sum(rate(synapse_http_server_response_time_seconds_bucket{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} 50%", + "refId": "D" + }, + { + "expr": "sum(rate(synapse_http_server_response_time_seconds_sum{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method) / sum(rate(synapse_http_server_response_time_seconds_count{servlet='RoomSendEventRestServlet',instance=\"$instance\",code=~\"2..\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (method)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} mean", + "refId": "E" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Event send time quantiles by worker", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "repeat": null, + "title": "Event persistence", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 28 + }, + "id": 57, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "decimals": null, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "hideEmpty": false, + "hideZero": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(synapse_http_server_requests_received{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}} {{tag}}", + "refId": "A", + "step": 20 + } + ], + "thresholds": [ + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(216, 200, 27, 0.27)", + "op": "gt", + "value": 100, + "yaxis": "left" + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(234, 112, 112, 0.22)", + "op": "gt", + "value": 250, + "yaxis": "left" + } + ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Request Count by arrival time", "tooltip": { "shared": false, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -1961,6 +2427,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -1986,9 +2459,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2014,7 +2488,7 @@ "title": "Top 10 Request Counts", "tooltip": { "shared": false, - "sort": 0, + "sort": 2, "value_type": "cumulative" }, "type": "graph", @@ -2055,6 +2529,13 @@ "decimals": null, "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 2, "fillGradient": 0, "grid": {}, @@ -2084,9 +2565,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2129,7 +2611,7 @@ "title": "Total CPU Usage by Endpoint", "tooltip": { "shared": false, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -2170,7 +2652,14 @@ "decimals": null, "editable": true, "error": false, - "fill": 2, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 0, "fillGradient": 0, "grid": {}, "gridPos": { @@ -2199,9 +2688,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2214,7 +2704,7 @@ "expr": "(rate(synapse_http_server_in_flight_requests_ru_utime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])+rate(synapse_http_server_in_flight_requests_ru_stime_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) / rate(synapse_http_server_requests_received{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", "format": "time_series", "interval": "", - "intervalFactor": 2, + "intervalFactor": 1, "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}} {{tag}}", "refId": "A", "step": 20 @@ -2226,14 +2716,16 @@ "fill": true, "fillColor": "rgba(216, 200, 27, 0.27)", "op": "gt", - "value": 100 + "value": 100, + "yaxis": "left" }, { "colorMode": "custom", "fill": true, "fillColor": "rgba(234, 112, 112, 0.22)", "op": "gt", - "value": 250 + "value": 250, + "yaxis": "left" } ], "timeFrom": null, @@ -2242,7 +2734,7 @@ "title": "Average CPU Usage by Endpoint", "tooltip": { "shared": false, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -2282,6 +2774,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -2310,9 +2809,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2325,7 +2825,7 @@ "expr": "rate(synapse_http_server_in_flight_requests_db_txn_duration_seconds{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", "format": "time_series", "interval": "", - "intervalFactor": 2, + "intervalFactor": 1, "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}} {{tag}}", "refId": "A", "step": 20 @@ -2338,7 +2838,7 @@ "title": "DB Usage by endpoint", "tooltip": { "shared": false, - "sort": 0, + "sort": 2, "value_type": "cumulative" }, "type": "graph", @@ -2379,6 +2879,13 @@ "decimals": null, "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 2, "fillGradient": 0, "grid": {}, @@ -2408,9 +2915,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -2424,7 +2932,7 @@ "format": "time_series", "hide": false, "interval": "", - "intervalFactor": 2, + "intervalFactor": 1, "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}}", "refId": "A", "step": 20 @@ -2437,7 +2945,7 @@ "title": "Non-sync avg response time", "tooltip": { "shared": false, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -2475,6 +2983,13 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -2499,13 +3014,21 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [], + "seriesOverrides": [ + { + "alias": "Total", + "color": "rgb(255, 255, 255)", + "fill": 0, + "linewidth": 3 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, @@ -2517,6 +3040,12 @@ "intervalFactor": 1, "legendFormat": "{{job}}-{{index}} {{method}} {{servlet}}", "refId": "A" + }, + { + "expr": "sum(avg_over_time(synapse_http_server_in_flight_requests_count{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size]))", + "interval": "", + "legendFormat": "Total", + "refId": "B" } ], "thresholds": [], @@ -2526,7 +3055,7 @@ "title": "Requests in flight", "tooltip": { "shared": false, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -2572,7 +3101,7 @@ "h": 1, "w": 24, "x": 0, - "y": 31 + "y": 29 }, "id": 97, "panels": [ @@ -2582,6 +3111,13 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -2605,11 +3141,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -2674,6 +3208,13 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -2697,11 +3238,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -2717,12 +3256,6 @@ "intervalFactor": 1, "legendFormat": "{{job}}-{{index}} {{name}}", "refId": "A" - }, - { - "expr": "", - "format": "time_series", - "intervalFactor": 1, - "refId": "B" } ], "thresholds": [], @@ -2731,7 +3264,7 @@ "timeShift": null, "title": "DB usage by background jobs (including scheduling time)", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -2772,6 +3305,13 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -2794,10 +3334,8 @@ "lines": true, "linewidth": 1, "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 2, "points": false, "renderer": "flot", @@ -2864,7 +3402,7 @@ "h": 1, "w": 24, "x": 0, - "y": 32 + "y": 30 }, "id": 81, "panels": [ @@ -2874,13 +3412,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 6 + "y": 33 }, "hiddenSeries": false, "id": 79, @@ -2897,11 +3442,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -2970,13 +3513,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 6 + "y": 33 }, "hiddenSeries": false, "id": 83, @@ -2993,11 +3543,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -3068,13 +3616,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 15 + "y": 42 }, "hiddenSeries": false, "id": 109, @@ -3091,11 +3646,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -3167,13 +3720,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 15 + "y": 42 }, "hiddenSeries": false, "id": 111, @@ -3190,11 +3750,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -3258,18 +3816,25 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "description": "Number of events queued up on the master process for processing by the federation sender", + "datasource": "${DS_PROMETHEUS}", + "description": "The number of events in the in-memory queues ", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 9, + "h": 8, "w": 12, "x": 0, - "y": 24 + "y": 51 }, "hiddenSeries": false, - "id": 140, + "id": 142, "legend": { "avg": false, "current": false, @@ -3281,14 +3846,112 @@ }, "lines": true, "linewidth": 1, - "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.1.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "synapse_federation_transaction_queue_pending_pdus{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "interval": "", + "legendFormat": "pending PDUs {{job}}-{{index}}", + "refId": "A" + }, + { + "expr": "synapse_federation_transaction_queue_pending_edus{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "interval": "", + "legendFormat": "pending EDUs {{job}}-{{index}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "In-memory federation transmission queues", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "events", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Number of events queued up on the master process for processing by the federation sender", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 51 + }, + "hiddenSeries": false, + "id": 140, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.1.3", + "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -3391,68 +4054,243 @@ "alignLevel": null } }, + { + "cards": { + "cardPadding": -1, + "cardRound": null + }, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateInferno", + "exponent": 0.5, + "min": 0, + "mode": "spectrum" + }, + "dataFormat": "tsbuckets", + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 59 + }, + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "id": 166, + "legend": { + "show": false + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "sum(rate(synapse_event_processing_lag_by_event_bucket{instance=\"$instance\",name=\"federation_sender\"}[$bucket_size])) by (le)", + "format": "heatmap", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ le }}", + "refId": "A" + } + ], + "title": "Federation send PDU lag", + "tooltip": { + "show": true, + "showHistogram": true + }, + "tooltipDecimals": 2, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": 0, + "format": "s", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "description": "The number of events in the in-memory queues ", - "fill": 1, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 0, "fillGradient": 0, "gridPos": { - "h": 8, + "h": 9, "w": 12, "x": 12, - "y": 24 + "y": 60 }, "hiddenSeries": false, - "id": 142, + "id": 162, "legend": { "avg": false, "current": false, "max": false, "min": false, + "rightSide": false, "show": true, "total": false, "values": false }, "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, + "linewidth": 0, + "links": [], + "nullPointMode": "connected", + "paceLength": 10, "percentage": false, - "pointradius": 2, + "pluginVersion": "7.1.3", + "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [], + "seriesOverrides": [ + { + "alias": "Avg", + "fill": 0, + "linewidth": 3 + }, + { + "alias": "99%", + "color": "#C4162A", + "fillBelowTo": "90%" + }, + { + "alias": "90%", + "color": "#FF7383", + "fillBelowTo": "75%" + }, + { + "alias": "75%", + "color": "#FFEE52", + "fillBelowTo": "50%" + }, + { + "alias": "50%", + "color": "#73BF69", + "fillBelowTo": "25%" + }, + { + "alias": "25%", + "color": "#1F60C4", + "fillBelowTo": "5%" + }, + { + "alias": "5%", + "lines": false + }, + { + "alias": "Average", + "color": "rgb(255, 255, 255)", + "lines": true, + "linewidth": 3 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { - "expr": "synapse_federation_transaction_queue_pending_pdus{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "expr": "histogram_quantile(0.99, sum(rate(synapse_event_processing_lag_by_event_bucket{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size])) by (le))", + "format": "time_series", "interval": "", - "legendFormat": "pending PDUs {{job}}-{{index}}", + "intervalFactor": 1, + "legendFormat": "99%", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.9, sum(rate(synapse_event_processing_lag_by_event_bucket{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size])) by (le))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "90%", "refId": "A" }, { - "expr": "synapse_federation_transaction_queue_pending_edus{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "expr": "histogram_quantile(0.75, sum(rate(synapse_event_processing_lag_by_event_bucket{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size])) by (le))", + "format": "time_series", "interval": "", - "legendFormat": "pending EDUs {{job}}-{{index}}", + "intervalFactor": 1, + "legendFormat": "75%", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(synapse_event_processing_lag_by_event_bucket{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size])) by (le))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "50%", "refId": "B" + }, + { + "expr": "histogram_quantile(0.25, sum(rate(synapse_event_processing_lag_by_event_bucket{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size])) by (le))", + "interval": "", + "legendFormat": "25%", + "refId": "F" + }, + { + "expr": "histogram_quantile(0.05, sum(rate(synapse_event_processing_lag_by_event_bucket{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size])) by (le))", + "interval": "", + "legendFormat": "5%", + "refId": "G" + }, + { + "expr": "sum(rate(synapse_event_processing_lag_by_event_sum{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size])) / sum(rate(synapse_event_processing_lag_by_event_count{name='federation_sender',index=~\"$index\",instance=\"$instance\"}[$bucket_size]))", + "interval": "", + "legendFormat": "Average", + "refId": "H" + } + ], + "thresholds": [ + { + "colorMode": "warning", + "fill": false, + "line": true, + "op": "gt", + "value": 0.25, + "yaxis": "left" + }, + { + "colorMode": "critical", + "fill": false, + "line": true, + "op": "gt", + "value": 1, + "yaxis": "left" } ], - "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "In-memory federation transmission queues", + "title": "Federation send PDU lag quantiles", "tooltip": { "shared": true, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -3465,21 +4303,20 @@ }, "yaxes": [ { - "$$hashKey": "object:317", - "format": "short", - "label": "events", + "decimals": null, + "format": "s", + "label": "", "logBase": 1, "max": null, "min": "0", "show": true }, { - "$$hashKey": "object:318", - "format": "short", + "format": "hertz", "label": "", "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true } ], @@ -3487,7 +4324,79 @@ "align": false, "alignLevel": null } - } + }, + { + "cards": { + "cardPadding": -1, + "cardRound": null + }, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateInferno", + "exponent": 0.5, + "min": 0, + "mode": "spectrum" + }, + "dataFormat": "tsbuckets", + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 68 + }, + "heatmap": {}, + "hideZeroBuckets": false, + "highlightCards": true, + "id": 164, + "legend": { + "show": false + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "sum(rate(synapse_federation_server_pdu_process_time_bucket{instance=\"$instance\"}[$bucket_size])) by (le)", + "format": "heatmap", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ le }}", + "refId": "A" + } + ], + "title": "Handle inbound PDU time", + "tooltip": { + "show": true, + "showHistogram": true + }, + "tooltipDecimals": 2, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": 0, + "format": "s", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + } ], "title": "Federation", "type": "row" @@ -3499,7 +4408,7 @@ "h": 1, "w": 24, "x": 0, - "y": 33 + "y": 31 }, "id": 60, "panels": [ @@ -3509,6 +4418,13 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -3532,11 +4448,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -3611,6 +4525,13 @@ "dashes": false, "datasource": "$datasource", "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -3634,10 +4555,8 @@ "lines": true, "linewidth": 1, "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 2, "points": false, "renderer": "flot", @@ -3705,7 +4624,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 32 }, "id": 58, "panels": [ @@ -3715,13 +4634,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, - "y": 79 + "y": 8 }, "hiddenSeries": false, "id": 48, @@ -3739,10 +4665,11 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -3809,13 +4736,20 @@ "dashes": false, "datasource": "$datasource", "description": "Shows the time in which the given percentage of database queries were scheduled, over the sampled timespan", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, - "y": 79 + "y": 8 }, "hiddenSeries": false, "id": 104, @@ -3834,10 +4768,11 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -3928,6 +4863,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 0, "fillGradient": 0, "grid": {}, @@ -3935,7 +4877,7 @@ "h": 7, "w": 12, "x": 0, - "y": 86 + "y": 15 }, "hiddenSeries": false, "id": 10, @@ -3955,10 +4897,11 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -4024,6 +4967,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4031,7 +4981,7 @@ "h": 7, "w": 12, "x": 12, - "y": 86 + "y": 15 }, "hiddenSeries": false, "id": 11, @@ -4051,10 +5001,11 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -4078,7 +5029,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Top DB transactions by total txn time", + "title": "DB transactions by total txn time", "tooltip": { "shared": false, "sort": 0, @@ -4112,6 +5063,111 @@ "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 22 + }, + "hiddenSeries": false, + "id": 180, + "legend": { + "avg": false, + "current": false, + "hideEmpty": true, + "hideZero": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": false + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(synapse_storage_transaction_time_sum{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])/rate(synapse_storage_transaction_time_count{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{desc}}", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average DB txn time", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "repeat": null, @@ -4125,7 +5181,7 @@ "h": 1, "w": 24, "x": 0, - "y": 35 + "y": 33 }, "id": 59, "panels": [ @@ -4137,6 +5193,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4144,7 +5207,7 @@ "h": 13, "w": 12, "x": 0, - "y": 80 + "y": 9 }, "hiddenSeries": false, "id": 12, @@ -4162,11 +5225,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -4191,8 +5252,8 @@ "timeShift": null, "title": "Total CPU Usage by Block", "tooltip": { - "shared": false, - "sort": 0, + "shared": true, + "sort": 2, "value_type": "cumulative" }, "type": "graph", @@ -4232,6 +5293,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4239,7 +5307,7 @@ "h": 13, "w": 12, "x": 12, - "y": 80 + "y": 9 }, "hiddenSeries": false, "id": 26, @@ -4257,11 +5325,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -4286,8 +5352,8 @@ "timeShift": null, "title": "Average CPU Time per Block", "tooltip": { - "shared": false, - "sort": 0, + "shared": true, + "sort": 2, "value_type": "cumulative" }, "type": "graph", @@ -4327,6 +5393,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4334,7 +5407,7 @@ "h": 13, "w": 12, "x": 0, - "y": 93 + "y": 22 }, "hiddenSeries": false, "id": 13, @@ -4352,11 +5425,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -4381,8 +5452,8 @@ "timeShift": null, "title": "Total DB Usage by Block", "tooltip": { - "shared": false, - "sort": 0, + "shared": true, + "sort": 2, "value_type": "cumulative" }, "type": "graph", @@ -4423,6 +5494,13 @@ "description": "The time each database transaction takes to execute, on average, broken down by metrics block.", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4430,7 +5508,7 @@ "h": 13, "w": 12, "x": 12, - "y": 93 + "y": 22 }, "hiddenSeries": false, "id": 27, @@ -4448,11 +5526,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -4477,8 +5553,8 @@ "timeShift": null, "title": "Average Database Transaction time, by Block", "tooltip": { - "shared": false, - "sort": 0, + "shared": true, + "sort": 2, "value_type": "cumulative" }, "type": "graph", @@ -4518,6 +5594,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4525,7 +5608,7 @@ "h": 13, "w": 12, "x": 0, - "y": 106 + "y": 35 }, "hiddenSeries": false, "id": 28, @@ -4542,11 +5625,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -4612,6 +5693,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4619,7 +5707,7 @@ "h": 13, "w": 12, "x": 12, - "y": 106 + "y": 35 }, "hiddenSeries": false, "id": 25, @@ -4636,11 +5724,9 @@ "linewidth": 2, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -4697,49 +5783,33 @@ "align": false, "alignLevel": null } - } - ], - "repeat": null, - "title": "Per-block metrics", - "type": "row" - }, - { - "collapsed": true, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 36 - }, - "id": 61, - "panels": [ + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", - "decimals": 2, - "editable": true, - "error": false, - "fill": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, "fillGradient": 0, - "grid": {}, "gridPos": { - "h": 10, + "h": 15, "w": 12, "x": 0, - "y": 37 + "y": 48 }, "hiddenSeries": false, - "id": 1, + "id": 154, "legend": { "alignAsTable": true, "avg": false, "current": false, - "hideEmpty": true, - "hideZero": false, "max": false, "min": false, "show": true, @@ -4747,13 +5817,130 @@ "values": false }, "lines": true, - "linewidth": 2, - "links": [], + "linewidth": 1, "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "percentage": false, + "pluginVersion": "7.1.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(synapse_util_metrics_block_count{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "interval": "", + "legendFormat": "{{job}}-{{index}} {{block_name}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Block count", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "hertz", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "repeat": null, + "title": "Per-block metrics", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 34 + }, + "id": 61, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "decimals": 2, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 84 + }, + "hiddenSeries": false, + "id": 1, + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "hideEmpty": true, + "hideZero": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -4821,6 +6008,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4828,7 +6022,7 @@ "h": 10, "w": 12, "x": 12, - "y": 37 + "y": 84 }, "hiddenSeries": false, "id": 8, @@ -4848,9 +6042,10 @@ "links": [], "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -4917,6 +6112,13 @@ "datasource": "$datasource", "editable": true, "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "grid": {}, @@ -4924,7 +6126,7 @@ "h": 10, "w": 12, "x": 0, - "y": 47 + "y": 94 }, "hiddenSeries": false, "id": 38, @@ -4944,9 +6146,10 @@ "links": [], "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -5010,13 +6213,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 10, "w": 12, "x": 12, - "y": 47 + "y": 94 }, "hiddenSeries": false, "id": 39, @@ -5035,9 +6245,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -5102,13 +6313,20 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 57 + "y": 104 }, "hiddenSeries": false, "id": 65, @@ -5127,9 +6345,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -5200,9 +6419,9 @@ "h": 1, "w": 24, "x": 0, - "y": 37 + "y": 35 }, - "id": 62, + "id": 148, "panels": [ { "aliasColors": {}, @@ -5210,16 +6429,23 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 9, + "h": 8, "w": 12, "x": 0, - "y": 121 + "y": 29 }, "hiddenSeries": false, - "id": 91, + "id": 146, "legend": { "avg": false, "current": false, @@ -5231,26 +6457,24 @@ }, "lines": true, "linewidth": 1, - "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, - "stack": true, + "stack": false, "steppedLine": false, "targets": [ { - "expr": "rate(python_gc_time_sum{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[10m])", - "format": "time_series", - "instant": false, - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} gen {{gen}}", + "expr": "synapse_util_caches_response_cache:size{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "interval": "", + "legendFormat": "{{name}} {{job}}-{{index}}", "refId": "A" } ], @@ -5258,9 +6482,9 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Total GC time by bucket (10m smoothing)", + "title": "Response cache size", "tooltip": { - "shared": true, + "shared": false, "sort": 0, "value_type": "individual" }, @@ -5274,12 +6498,11 @@ }, "yaxes": [ { - "decimals": null, - "format": "percentunit", + "format": "short", "label": null, "logBase": 1, "max": null, - "min": "0", + "min": null, "show": true }, { @@ -5302,22 +6525,24 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", - "decimals": 3, - "editable": true, - "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, - "grid": {}, "gridPos": { - "h": 9, + "h": 8, "w": 12, "x": 12, - "y": 121 + "y": 29 }, "hiddenSeries": false, - "id": 21, + "id": 150, "legend": { - "alignAsTable": true, "avg": false, "current": false, "max": false, @@ -5327,14 +6552,14 @@ "values": false }, "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "null as zero", + "linewidth": 1, + "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -5343,24 +6568,27 @@ "steppedLine": false, "targets": [ { - "expr": "rate(python_gc_time_sum{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])/rate(python_gc_time_count[$bucket_size])", - "format": "time_series", - "intervalFactor": 2, - "legendFormat": "{{job}} {{index}} gen {{gen}} ", - "refId": "A", - "step": 20, - "target": "" + "expr": "rate(synapse_util_caches_response_cache:hits{instance=\"$instance\", job=~\"$job\", index=~\"$index\"}[$bucket_size])/rate(synapse_util_caches_response_cache:total{instance=\"$instance\", job=~\"$job\", index=~\"$index\"}[$bucket_size])", + "interval": "", + "legendFormat": "{{name}} {{job}}-{{index}}", + "refId": "A" + }, + { + "expr": "", + "interval": "", + "legendFormat": "", + "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Average GC Time Per Collection", + "title": "Response cache hit rate", "tooltip": { "shared": false, "sort": 0, - "value_type": "cumulative" + "value_type": "individual" }, "type": "graph", "xaxis": { @@ -5372,14 +6600,17 @@ }, "yaxes": [ { - "format": "s", + "decimals": null, + "format": "percentunit", + "label": null, "logBase": 1, - "max": null, - "min": null, + "max": "1", + "min": "0", "show": true }, { "format": "short", + "label": null, "logBase": 1, "max": null, "min": null, @@ -5390,29 +6621,48 @@ "align": false, "alignLevel": null } - }, + } + ], + "title": "Response caches", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 36 + }, + "id": 62, + "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", - "description": "'gen 0' shows the number of objects allocated since the last gen0 GC.\n'gen 1' / 'gen 2' show the number of gen0/gen1 GCs since the last gen1/gen2 GC.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 130 + "y": 30 }, "hiddenSeries": false, - "id": 89, + "id": 91, "legend": { "avg": false, "current": false, - "hideEmpty": true, - "hideZero": false, "max": false, "min": false, "show": true, @@ -5424,25 +6674,22 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [ - { - "alias": "/gen 0$/", - "yaxis": 2 - } - ], + "seriesOverrides": [], "spaceLength": 10, - "stack": false, + "stack": true, "steppedLine": false, "targets": [ { - "expr": "python_gc_counts{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}", + "expr": "rate(python_gc_time_sum{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[10m])", "format": "time_series", + "instant": false, "intervalFactor": 1, "legendFormat": "{{job}}-{{index}} gen {{gen}}", "refId": "A" @@ -5452,9 +6699,9 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Allocation counts", + "title": "Total GC time by bucket (10m smoothing)", "tooltip": { - "shared": false, + "shared": true, "sort": 0, "value_type": "individual" }, @@ -5468,17 +6715,17 @@ }, "yaxes": [ { - "format": "short", - "label": "Gen N-1 GCs since last Gen N GC", + "decimals": null, + "format": "percentunit", + "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { - "decimals": null, "format": "short", - "label": "Objects since last Gen 0 GC", + "label": null, "logBase": 1, "max": null, "min": null, @@ -5496,17 +6743,29 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "decimals": 3, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, + "grid": {}, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 130 + "y": 30 }, "hiddenSeries": false, - "id": 93, + "id": 21, "legend": { + "alignAsTable": true, "avg": false, "current": false, "max": false, @@ -5516,13 +6775,219 @@ "values": false }, "lines": true, - "linewidth": 1, + "linewidth": 2, "links": [], - "nullPointMode": "connected", + "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(python_gc_time_sum{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])/rate(python_gc_time_count[$bucket_size])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{job}} {{index}} gen {{gen}} ", + "refId": "A", + "step": 20, + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Average GC Time Per Collection", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "'gen 0' shows the number of objects allocated since the last gen0 GC.\n'gen 1' / 'gen 2' show the number of gen0/gen1 GCs since the last gen1/gen2 GC.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 39 + }, + "hiddenSeries": false, + "id": 89, + "legend": { + "avg": false, + "current": false, + "hideEmpty": true, + "hideZero": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "/gen 0$/", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "python_gc_counts{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} gen {{gen}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Allocation counts", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "Gen N-1 GCs since last Gen N GC", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "decimals": null, + "format": "short", + "label": "Objects since last Gen 0 GC", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 39 + }, + "hiddenSeries": false, + "id": 93, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -5586,16 +7051,1579 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 48 + }, + "hiddenSeries": false, + "id": 95, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(python_gc_time_count{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} gen {{gen}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "GC frequency", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "hertz", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cards": { + "cardPadding": 0, + "cardRound": null + }, + "color": { + "cardColor": "#b4ff00", + "colorScale": "sqrt", + "colorScheme": "interpolateSpectral", + "exponent": 0.5, + "max": null, + "min": 0, + "mode": "spectrum" + }, + "dataFormat": "tsbuckets", + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 48 + }, + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "id": 87, + "legend": { + "show": true + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "sum(rate(python_gc_time_bucket[$bucket_size])) by (le)", + "format": "heatmap", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "title": "GC durations", + "tooltip": { + "show": true, + "showHistogram": false + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": null, + "format": "s", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + } + ], + "repeat": null, + "title": "GC", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 37 + }, + "id": 63, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 13 + }, + "hiddenSeries": false, + "id": 42, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum (rate(synapse_replication_tcp_protocol_inbound_commands{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (name, conn_id)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{job}}-{{index}} {{command}}", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Rate of incoming commands", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "hertz", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "description": "", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 13 + }, + "hiddenSeries": false, + "id": 144, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "synapse_replication_tcp_command_queue{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "interval": "", + "legendFormat": "{{stream_name}} {{job}}-{{index}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Queued incoming RDATA commands, by stream", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 20 + }, + "hiddenSeries": false, + "id": 43, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum (rate(synapse_replication_tcp_protocol_outbound_commands{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (name, conn_id)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{job}}-{{index}} {{command}}", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Rate of outgoing commands", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "hertz", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 20 + }, + "hiddenSeries": false, + "id": 41, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(synapse_replication_tcp_resource_stream_updates{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{stream_name}}", + "refId": "A", + "step": 20 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Outgoing stream updates", + "tooltip": { + "shared": false, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "hertz", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 27 + }, + "hiddenSeries": false, + "id": 113, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "synapse_replication_tcp_resource_connections_per_stream{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{stream_name}}", + "refId": "A" + }, + { + "expr": "synapse_replication_tcp_resource_total_connections{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Replication connections", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 27 + }, + "hiddenSeries": false, + "id": 115, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(synapse_replication_tcp_protocol_close_reason{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{reason_type}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Replication connection close reasons", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "hertz", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "repeat": null, + "title": "Replication", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 69, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 41 + }, + "hiddenSeries": false, + "id": 67, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "max(synapse_event_persisted_position{instance=\"$instance\"}) - on() group_right() synapse_event_processing_positions{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{name}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Event processing lag", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "events", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 41 + }, + "hiddenSeries": false, + "id": 71, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "time()*1000-synapse_event_processing_last_ts{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{name}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Age of last processed event", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 50 + }, + "hiddenSeries": false, + "id": 121, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "deriv(synapse_event_processing_last_ts{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])/1000 - 1", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{job}}-{{index}} {{name}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Event processing catchup rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "none", + "label": "fallbehind(-) / catchup(+): s/sec", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Event processing loop positions", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 126, + "panels": [ + { + "cards": { + "cardPadding": 0, + "cardRound": null + }, + "color": { + "cardColor": "#B877D9", + "colorScale": "sqrt", + "colorScheme": "interpolateInferno", + "exponent": 0.5, + "max": null, + "min": 0, + "mode": "opacity" + }, + "dataFormat": "tsbuckets", + "datasource": "$datasource", + "description": "Colour reflects the number of rooms with the given number of forward extremities, or fewer.\n\nThis is only updated once an hour.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 42 + }, + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "id": 122, + "legend": { + "show": true + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "synapse_forward_extremities_bucket{instance=\"$instance\"} and on (index, instance, job) (synapse_storage_events_persisted_events > 0)", + "format": "heatmap", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Number of rooms, by number of forward extremities in room", + "tooltip": { + "show": true, + "showHistogram": true + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": 0, + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Number of rooms with the given number of forward extremities or fewer.\n\nThis is only updated once an hour.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 42 + }, + "hiddenSeries": false, + "id": 124, + "interval": "", + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pluginVersion": "7.1.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "synapse_forward_extremities_bucket{instance=\"$instance\"} > 0", + "format": "heatmap", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Room counts, by number of extremities", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "none", + "label": "Number of rooms", + "logBase": 10, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cards": { + "cardPadding": 0, + "cardRound": null + }, + "color": { + "cardColor": "#5794F2", + "colorScale": "sqrt", + "colorScheme": "interpolateInferno", + "exponent": 0.5, + "min": 0, + "mode": "opacity" + }, + "dataFormat": "tsbuckets", + "datasource": "$datasource", + "description": "Colour reflects the number of events persisted to rooms with the given number of forward extremities, or fewer.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 50 + }, + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "id": 127, + "legend": { + "show": true + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0)", + "format": "heatmap", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Events persisted, by number of forward extremities in room (heatmap)", + "tooltip": { + "show": true, + "showHistogram": true + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": 0, + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "For a given percentage P, the number X where P% of events were persisted to rooms with X forward extremities or fewer.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 50 + }, + "hiddenSeries": false, + "id": 128, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "histogram_quantile(0.5, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "50%", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.75, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "75%", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.90, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "90%", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.99, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "99%", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Events persisted, by number of forward extremities in room (quantiles)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "Number of extremities in room", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cards": { + "cardPadding": 0, + "cardRound": null + }, + "color": { + "cardColor": "#FF9830", + "colorScale": "sqrt", + "colorScheme": "interpolateInferno", + "exponent": 0.5, + "min": 0, + "mode": "opacity" + }, + "dataFormat": "tsbuckets", + "datasource": "$datasource", + "description": "Colour reflects the number of events persisted to rooms with the given number of stale forward extremities, or fewer.\n\nStale forward extremities are those that were in the previous set of extremities as well as the new.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 58 + }, + "heatmap": {}, + "hideZeroBuckets": true, + "highlightCards": true, + "id": 129, + "legend": { + "show": true + }, + "links": [], + "reverseYBuckets": false, + "targets": [ + { + "expr": "rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0)", + "format": "heatmap", + "intervalFactor": 1, + "legendFormat": "{{le}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Events persisted, by number of stale forward extremities in room (heatmap)", + "tooltip": { + "show": true, + "showHistogram": true + }, + "type": "heatmap", + "xAxis": { + "show": true + }, + "xBucketNumber": null, + "xBucketSize": null, + "yAxis": { + "decimals": 0, + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true, + "splitFactor": null + }, + "yBucketBound": "auto", + "yBucketNumber": null, + "yBucketSize": null + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "For given percentage P, the number X where P% of events were persisted to rooms with X stale forward extremities or fewer.\n\nStale forward extremities are those that were in the previous set of extremities as well as the new.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 9, + "h": 8, "w": 12, - "x": 0, - "y": 139 + "x": 12, + "y": 58 }, "hiddenSeries": false, - "id": 95, + "id": 130, "legend": { "avg": false, "current": false, @@ -5609,11 +8637,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.1.3", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -5622,18 +8648,39 @@ "steppedLine": false, "targets": [ { - "expr": "rate(python_gc_time_count{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "histogram_quantile(0.5, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", "format": "time_series", "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} gen {{gen}}", + "legendFormat": "50%", "refId": "A" + }, + { + "expr": "histogram_quantile(0.75, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "75%", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.90, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "90%", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.99, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "99%", + "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "GC frequency", + "title": "Events persisted, by number of stale forward extremities in room (quantiles)", "tooltip": { "shared": true, "sort": 0, @@ -5649,11 +8696,11 @@ }, "yaxes": [ { - "format": "hertz", - "label": null, + "format": "short", + "label": "Number of stale forward extremities in room", "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { @@ -5676,26 +8723,32 @@ "cardRound": null }, "color": { - "cardColor": "#b4ff00", + "cardColor": "#73BF69", "colorScale": "sqrt", - "colorScheme": "interpolateSpectral", + "colorScheme": "interpolateInferno", "exponent": 0.5, - "max": null, "min": 0, - "mode": "spectrum" + "mode": "opacity" }, "dataFormat": "tsbuckets", - "datasource": "${DS_PROMETHEUS}", + "datasource": "$datasource", + "description": "Colour reflects the number of state resolution operations performed over the given number of state groups, or fewer.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "gridPos": { - "h": 9, + "h": 8, "w": 12, - "x": 12, - "y": 139 + "x": 0, + "y": 66 }, "heatmap": {}, "hideZeroBuckets": true, "highlightCards": true, - "id": 87, + "id": 131, "legend": { "show": true }, @@ -5703,17 +8756,20 @@ "reverseYBuckets": false, "targets": [ { - "expr": "sum(rate(python_gc_time_bucket[$bucket_size])) by (le)", + "expr": "rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", "format": "heatmap", + "interval": "", "intervalFactor": 1, "legendFormat": "{{le}}", "refId": "A" } ], - "title": "GC durations", + "timeFrom": null, + "timeShift": null, + "title": "Number of state resolution performed, by number of state groups involved (heatmap)", "tooltip": { "show": true, - "showHistogram": false + "showHistogram": true }, "type": "heatmap", "xAxis": { @@ -5722,8 +8778,8 @@ "xBucketNumber": null, "xBucketSize": null, "yAxis": { - "decimals": null, - "format": "s", + "decimals": 0, + "format": "short", "logBase": 1, "max": null, "min": null, @@ -5733,39 +8789,32 @@ "yBucketBound": "auto", "yBucketNumber": null, "yBucketSize": null - } - ], - "repeat": null, - "title": "GC", - "type": "row" - }, - { - "collapsed": true, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 38 - }, - "id": 63, - "panels": [ + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", + "description": "For a given percentage P, the number X where P% of state resolution operations took place over X state groups or fewer.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 7, + "h": 8, "w": 12, - "x": 0, + "x": 12, "y": 66 }, "hiddenSeries": false, - "id": 2, + "id": 132, + "interval": "", "legend": { "avg": false, "current": false, @@ -5779,12 +8828,9 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.1.3", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -5793,53 +8839,150 @@ "steppedLine": false, "targets": [ { - "expr": "rate(synapse_replication_tcp_resource_user_sync{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "histogram_quantile(0.5, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", "format": "time_series", - "intervalFactor": 2, - "legendFormat": "user started/stopped syncing", - "refId": "A", - "step": 20 + "interval": "", + "intervalFactor": 1, + "legendFormat": "50%", + "refId": "A" }, { - "expr": "rate(synapse_replication_tcp_resource_federation_ack{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "histogram_quantile(0.75, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", "format": "time_series", - "intervalFactor": 2, - "legendFormat": "federation ack", - "refId": "B", - "step": 20 + "interval": "", + "intervalFactor": 1, + "legendFormat": "75%", + "refId": "B" }, { - "expr": "rate(synapse_replication_tcp_resource_remove_pusher{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "histogram_quantile(0.90, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", "format": "time_series", - "intervalFactor": 2, - "legendFormat": "remove pusher", - "refId": "C", - "step": 20 + "interval": "", + "intervalFactor": 1, + "legendFormat": "90%", + "refId": "C" }, { - "expr": "rate(synapse_replication_tcp_resource_invalidate_cache{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "histogram_quantile(0.99, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", "format": "time_series", - "intervalFactor": 2, - "legendFormat": "invalidate cache", - "refId": "D", - "step": 20 + "interval": "", + "intervalFactor": 1, + "legendFormat": "99%", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Number of state resolutions performed, by number of state groups involved (quantiles)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "Number of state groups", + "logBase": 1, + "max": null, + "min": "0", + "show": true }, { - "expr": "rate(synapse_replication_tcp_resource_user_ip_cache{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", - "format": "time_series", - "intervalFactor": 2, - "legendFormat": "user ip cache", - "refId": "E", - "step": 20 + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "When we do a state res while persisting events we try and see if we can prune any stale extremities.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 74 + }, + "hiddenSeries": false, + "id": 179, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.3", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(synapse_storage_events_state_resolutions_during_persistence{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", + "interval": "", + "legendFormat": "State res ", + "refId": "A" + }, + { + "expr": "sum(rate(synapse_storage_events_potential_times_prune_extremities{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", + "interval": "", + "legendFormat": "Potential to prune", + "refId": "B" + }, + { + "expr": "sum(rate(synapse_storage_events_times_pruned_extremities{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", + "interval": "", + "legendFormat": "Pruned", + "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Rate of events on replication master", + "title": "Stale extremity dropping", "tooltip": { - "shared": false, + "shared": true, "sort": 0, "value_type": "individual" }, @@ -5873,23 +9016,45 @@ "align": false, "alignLevel": null } - }, + } + ], + "title": "Extremities", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 158, + "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 7, + "h": 8, "w": 12, - "x": 12, - "y": 66 + "x": 0, + "y": 119 }, "hiddenSeries": false, - "id": 41, + "id": 156, "legend": { "avg": false, "current": false, @@ -5904,35 +9069,49 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "paceLength": 10, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", - "seriesOverrides": [], + "seriesOverrides": [ + { + "alias": "Max", + "color": "#bf1b00", + "fill": 0, + "linewidth": 2 + } + ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { - "expr": "rate(synapse_replication_tcp_resource_stream_updates{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "expr": "synapse_admin_mau:current{instance=\"$instance\"}", "format": "time_series", "interval": "", - "intervalFactor": 2, - "legendFormat": "{{stream_name}}", - "refId": "A", - "step": 20 + "intervalFactor": 1, + "legendFormat": "Current", + "refId": "A" + }, + { + "expr": "synapse_admin_mau:max{instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Max", + "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Outgoing stream updates", + "title": "MAU Limits", "tooltip": { - "shared": false, + "shared": true, "sort": 0, "value_type": "individual" }, @@ -5946,11 +9125,11 @@ }, "yaxes": [ { - "format": "hertz", + "format": "short", "label": null, "logBase": 1, "max": null, - "min": null, + "min": "0", "show": true }, { @@ -5973,16 +9152,22 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 7, + "h": 8, "w": 12, - "x": 0, - "y": 73 + "x": 12, + "y": 119 }, "hiddenSeries": false, - "id": 42, + "id": 160, "legend": { "avg": false, "current": false, @@ -5994,14 +9179,13 @@ }, "lines": true, "linewidth": 1, - "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -6010,21 +9194,19 @@ "steppedLine": false, "targets": [ { - "expr": "sum (rate(synapse_replication_tcp_protocol_inbound_commands{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (name, conn_id)", - "format": "time_series", - "intervalFactor": 2, - "legendFormat": "{{job}}-{{index}} {{command}}", - "refId": "A", - "step": 20 + "expr": "synapse_admin_mau_current_mau_by_service{instance=\"$instance\"}", + "interval": "", + "legendFormat": "{{ app_service }}", + "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Rate of incoming commands", + "title": "MAU by Appservice", "tooltip": { - "shared": false, + "shared": true, "sort": 0, "value_type": "individual" }, @@ -6038,7 +9220,7 @@ }, "yaxes": [ { - "format": "hertz", + "format": "short", "label": null, "logBase": 1, "max": null, @@ -6058,23 +9240,45 @@ "align": false, "alignLevel": null } - }, + } + ], + "title": "MAU", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 41 + }, + "id": 177, + "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, - "x": 12, - "y": 73 + "x": 0, + "y": 1 }, "hiddenSeries": false, - "id": 43, + "id": 173, "legend": { "avg": false, "current": false, @@ -6088,11 +9292,8 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -6102,22 +9303,24 @@ "steppedLine": false, "targets": [ { - "expr": "sum (rate(synapse_replication_tcp_protocol_outbound_commands{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])) without (name, conn_id)", + "expr": "rate(synapse_notifier_users_woken_by_stream{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", "format": "time_series", + "hide": false, "intervalFactor": 2, - "legendFormat": "{{job}}-{{index}} {{command}}", + "legendFormat": "{{stream}} {{index}}", + "metric": "synapse_notifier", "refId": "A", - "step": 20 + "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Rate of outgoing commands", + "title": "Notifier Streams Woken", "tooltip": { - "shared": false, - "sort": 0, + "shared": true, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -6157,16 +9360,23 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, - "x": 0, - "y": 80 + "x": 12, + "y": 1 }, "hiddenSeries": false, - "id": 113, + "id": 175, "legend": { "avg": false, "current": false, @@ -6180,11 +9390,8 @@ "linewidth": 1, "links": [], "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "paceLength": 10, "percentage": false, + "pluginVersion": "7.1.3", "pointradius": 5, "points": false, "renderer": "flot", @@ -6194,28 +9401,23 @@ "steppedLine": false, "targets": [ { - "expr": "synapse_replication_tcp_resource_connections_per_stream{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} {{stream_name}}", - "refId": "A" - }, - { - "expr": "synapse_replication_tcp_resource_total_connections{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}", + "expr": "rate(synapse_handler_presence_get_updates{job=~\"$job\",instance=\"$instance\"}[$bucket_size])", "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}}", - "refId": "B" + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{type}} {{index}}", + "refId": "A", + "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Replication connections", + "title": "Presence Stream Fetch Type Rates", "tooltip": { "shared": true, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -6228,7 +9430,7 @@ }, "yaxes": [ { - "format": "short", + "format": "hertz", "label": null, "logBase": 1, "max": null, @@ -6248,23 +9450,44 @@ "align": false, "alignLevel": null } - }, + } + ], + "title": "Notifier", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 170, + "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 7, + "h": 8, "w": 12, - "x": 12, - "y": 80 + "x": 0, + "y": 73 }, "hiddenSeries": false, - "id": 115, + "id": 168, "legend": { "avg": false, "current": false, @@ -6276,14 +9499,13 @@ }, "lines": true, "linewidth": 1, - "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -6292,10 +9514,9 @@ "steppedLine": false, "targets": [ { - "expr": "rate(synapse_replication_tcp_protocol_close_reason{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} {{reason_type}}", + "expr": "rate(synapse_appservice_api_sent_events{instance=\"$instance\"}[$bucket_size])", + "interval": "", + "legendFormat": "{{exported_service}}", "refId": "A" } ], @@ -6303,7 +9524,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Replication connection close reasons", + "title": "Sent Events rate", "tooltip": { "shared": true, "sort": 0, @@ -6339,39 +9560,29 @@ "align": false, "alignLevel": null } - } - ], - "repeat": null, - "title": "Replication", - "type": "row" - }, - { - "collapsed": true, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 39 - }, - "id": 69, - "panels": [ + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 9, + "h": 8, "w": 12, - "x": 0, - "y": 40 + "x": 12, + "y": 73 }, "hiddenSeries": false, - "id": 67, + "id": 171, "legend": { "avg": false, "current": false, @@ -6383,14 +9594,13 @@ }, "lines": true, "linewidth": 1, - "links": [], - "nullPointMode": "connected", + "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -6399,11 +9609,9 @@ "steppedLine": false, "targets": [ { - "expr": "max(synapse_event_persisted_position{instance=\"$instance\"}) - ignoring(instance,index, job, name) group_right() synapse_event_processing_positions{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", - "format": "time_series", + "expr": "rate(synapse_appservice_api_sent_transactions{instance=\"$instance\"}[$bucket_size])", "interval": "", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} {{name}}", + "legendFormat": "{{exported_service}}", "refId": "A" } ], @@ -6411,7 +9619,7 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Event processing lag", + "title": "Transactions rate", "tooltip": { "shared": true, "sort": 0, @@ -6427,8 +9635,8 @@ }, "yaxes": [ { - "format": "short", - "label": "events", + "format": "hertz", + "label": null, "logBase": 1, "max": null, "min": null, @@ -6447,23 +9655,44 @@ "align": false, "alignLevel": null } - }, + } + ], + "title": "Appservices", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 188, + "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 9, + "h": 8, "w": 12, - "x": 12, - "y": 40 + "x": 0, + "y": 44 }, "hiddenSeries": false, - "id": 71, + "id": 182, "legend": { "avg": false, "current": false, @@ -6475,14 +9704,13 @@ }, "lines": true, "linewidth": 1, - "links": [], - "nullPointMode": "connected", + "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -6491,23 +9719,44 @@ "steppedLine": false, "targets": [ { - "expr": "time()*1000-synapse_event_processing_last_ts{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}", - "format": "time_series", - "hide": false, + "expr": "rate(synapse_handler_presence_notified_presence{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", "interval": "", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} {{name}}", + "legendFormat": "Notified", + "refId": "A" + }, + { + "expr": "rate(synapse_handler_presence_federation_presence_out{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "interval": "", + "legendFormat": "Remote ping", "refId": "B" + }, + { + "expr": "rate(synapse_handler_presence_presence_updates{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "interval": "", + "legendFormat": "Total updates", + "refId": "C" + }, + { + "expr": "rate(synapse_handler_presence_federation_presence{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "interval": "", + "legendFormat": "Remote updates", + "refId": "D" + }, + { + "expr": "rate(synapse_handler_presence_bump_active_time{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", + "interval": "", + "legendFormat": "Bump active time", + "refId": "E" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Age of last processed event", + "title": "Presence", "tooltip": { "shared": true, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -6520,7 +9769,7 @@ }, "yaxes": [ { - "format": "ms", + "format": "hertz", "label": null, "logBase": 1, "max": null, @@ -6547,17 +9796,22 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { - "h": 9, + "h": 8, "w": 12, - "x": 0, - "y": 49 + "x": 12, + "y": 44 }, "hiddenSeries": false, - "id": 121, - "interval": "", + "id": 184, "legend": { "avg": false, "current": false, @@ -6569,14 +9823,13 @@ }, "lines": true, "linewidth": 1, - "links": [], - "nullPointMode": "connected", + "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, - "paceLength": 10, "percentage": false, - "pointradius": 5, + "pluginVersion": "7.3.7", + "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -6585,23 +9838,20 @@ "steppedLine": false, "targets": [ { - "expr": "deriv(synapse_event_processing_last_ts{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])/1000 - 1", - "format": "time_series", - "hide": false, + "expr": "rate(synapse_handler_presence_state_transition{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", "interval": "", - "intervalFactor": 1, - "legendFormat": "{{job}}-{{index}} {{name}}", - "refId": "B" + "legendFormat": "{{from}} -> {{to}}", + "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Event processing catchup rate", + "title": "Presence state transitions", "tooltip": { "shared": true, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -6614,9 +9864,8 @@ }, "yaxes": [ { - "decimals": null, - "format": "none", - "label": "fallbehind(-) / catchup(+): s/sec", + "format": "hertz", + "label": null, "logBase": 1, "max": null, "min": null, @@ -6635,88 +9884,6 @@ "align": false, "alignLevel": null } - } - ], - "title": "Event processing loop positions", - "type": "row" - }, - { - "collapsed": true, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 40 - }, - "id": 126, - "panels": [ - { - "cards": { - "cardPadding": 0, - "cardRound": null - }, - "color": { - "cardColor": "#B877D9", - "colorScale": "sqrt", - "colorScheme": "interpolateInferno", - "exponent": 0.5, - "max": null, - "min": 0, - "mode": "opacity" - }, - "dataFormat": "tsbuckets", - "datasource": "$datasource", - "description": "Colour reflects the number of rooms with the given number of forward extremities, or fewer.\n\nThis is only updated once an hour.", - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 86 - }, - "heatmap": {}, - "hideZeroBuckets": true, - "highlightCards": true, - "id": 122, - "legend": { - "show": true - }, - "links": [], - "reverseYBuckets": false, - "targets": [ - { - "expr": "synapse_forward_extremities_bucket{instance=\"$instance\"} and on (index, instance, job) (synapse_storage_events_persisted_events > 0)", - "format": "heatmap", - "intervalFactor": 1, - "legendFormat": "{{le}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Number of rooms, by number of forward extremities in room", - "tooltip": { - "show": true, - "showHistogram": true - }, - "type": "heatmap", - "xAxis": { - "show": true - }, - "xBucketNumber": null, - "xBucketSize": null, - "yAxis": { - "decimals": 0, - "format": "short", - "logBase": 1, - "max": null, - "min": null, - "show": true, - "splitFactor": null - }, - "yBucketBound": "auto", - "yBucketNumber": null, - "yBucketSize": null }, { "aliasColors": {}, @@ -6724,18 +9891,22 @@ "dashLength": 10, "dashes": false, "datasource": "$datasource", - "description": "Number of rooms with the given number of forward extremities or fewer.\n\nThis is only updated once an hour.", - "fill": 0, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, - "x": 12, - "y": 86 + "x": 0, + "y": 52 }, "hiddenSeries": false, - "id": 124, - "interval": "", + "id": 186, "legend": { "avg": false, "current": false, @@ -6747,12 +9918,12 @@ }, "lines": true, "linewidth": 1, - "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -6762,11 +9933,9 @@ "steppedLine": false, "targets": [ { - "expr": "synapse_forward_extremities_bucket{instance=\"$instance\"} > 0", - "format": "time_series", + "expr": "rate(synapse_handler_presence_notify_reason{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size])", "interval": "", - "intervalFactor": 1, - "legendFormat": "{{le}}", + "legendFormat": "{{reason}}", "refId": "A" } ], @@ -6774,10 +9943,10 @@ "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Room counts, by number of extremities", + "title": "Presence notify reason", "tooltip": { - "shared": false, - "sort": 1, + "shared": true, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -6790,111 +9959,66 @@ }, "yaxes": [ { - "decimals": null, - "format": "none", - "label": "Number of rooms", + "$$hashKey": "object:165", + "format": "hertz", + "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { + "$$hashKey": "object:166", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, - "show": false + "show": true } ], "yaxis": { "align": false, "alignLevel": null } - }, - { - "cards": { - "cardPadding": 0, - "cardRound": null - }, - "color": { - "cardColor": "#5794F2", - "colorScale": "sqrt", - "colorScheme": "interpolateInferno", - "exponent": 0.5, - "min": 0, - "mode": "opacity" - }, - "dataFormat": "tsbuckets", - "datasource": "$datasource", - "description": "Colour reflects the number of events persisted to rooms with the given number of forward extremities, or fewer.", - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 94 - }, - "heatmap": {}, - "hideZeroBuckets": true, - "highlightCards": true, - "id": 127, - "legend": { - "show": true - }, - "links": [], - "reverseYBuckets": false, - "targets": [ - { - "expr": "rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0)", - "format": "heatmap", - "intervalFactor": 1, - "legendFormat": "{{le}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Events persisted, by number of forward extremities in room (heatmap)", - "tooltip": { - "show": true, - "showHistogram": true - }, - "type": "heatmap", - "xAxis": { - "show": true - }, - "xBucketNumber": null, - "xBucketSize": null, - "yAxis": { - "decimals": 0, - "format": "short", - "logBase": 1, - "max": null, - "min": null, - "show": true, - "splitFactor": null - }, - "yBucketBound": "auto", - "yBucketNumber": null, - "yBucketSize": null - }, + } + ], + "title": "Presence", + "type": "row" + }, + { + "collapsed": true, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 197, + "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", - "description": "For a given percentage P, the number X where P% of events were persisted to rooms with X forward extremities or fewer.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, - "x": 12, - "y": 94 + "x": 0, + "y": 1 }, "hiddenSeries": false, - "id": 128, + "id": 191, "legend": { "avg": false, "current": false, @@ -6906,12 +10030,12 @@ }, "lines": true, "linewidth": 1, - "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -6921,42 +10045,20 @@ "steppedLine": false, "targets": [ { - "expr": "histogram_quantile(0.5, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "50%", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.75, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "75%", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.90, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "90%", - "refId": "C" - }, - { - "expr": "histogram_quantile(0.99, rate(synapse_storage_events_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "99%", - "refId": "D" + "expr": "rate(synapse_external_cache_set{job=\"$job\", instance=\"$instance\", index=~\"$index\"}[$bucket_size])", + "interval": "", + "legendFormat": "{{ cache_name }} {{ index }}", + "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Events persisted, by number of forward extremities in room (quantiles)", + "title": "External Cache Set Rate", "tooltip": { "shared": true, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -6969,14 +10071,16 @@ }, "yaxes": [ { - "format": "short", - "label": "Number of extremities in room", + "$$hashKey": "object:390", + "format": "hertz", + "label": null, "logBase": 1, "max": null, - "min": "0", + "min": null, "show": true }, { + "$$hashKey": "object:391", "format": "short", "label": null, "logBase": 1, @@ -6990,89 +10094,29 @@ "alignLevel": null } }, - { - "cards": { - "cardPadding": 0, - "cardRound": null - }, - "color": { - "cardColor": "#FF9830", - "colorScale": "sqrt", - "colorScheme": "interpolateInferno", - "exponent": 0.5, - "min": 0, - "mode": "opacity" - }, - "dataFormat": "tsbuckets", - "datasource": "$datasource", - "description": "Colour reflects the number of events persisted to rooms with the given number of stale forward extremities, or fewer.\n\nStale forward extremities are those that were in the previous set of extremities as well as the new.", - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 102 - }, - "heatmap": {}, - "hideZeroBuckets": true, - "highlightCards": true, - "id": 129, - "legend": { - "show": true - }, - "links": [], - "reverseYBuckets": false, - "targets": [ - { - "expr": "rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0)", - "format": "heatmap", - "intervalFactor": 1, - "legendFormat": "{{le}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Events persisted, by number of stale forward extremities in room (heatmap)", - "tooltip": { - "show": true, - "showHistogram": true - }, - "type": "heatmap", - "xAxis": { - "show": true - }, - "xBucketNumber": null, - "xBucketSize": null, - "yAxis": { - "decimals": 0, - "format": "short", - "logBase": 1, - "max": null, - "min": null, - "show": true, - "splitFactor": null - }, - "yBucketBound": "auto", - "yBucketNumber": null, - "yBucketSize": null - }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$datasource", - "description": "For given percentage P, the number X where P% of events were persisted to rooms with X stale forward extremities or fewer.\n\nStale forward extremities are those that were in the previous set of extremities as well as the new.", + "description": "", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 102 + "y": 1 }, "hiddenSeries": false, - "id": 130, + "id": 193, "legend": { "avg": false, "current": false, @@ -7084,12 +10128,12 @@ }, "lines": true, "linewidth": 1, - "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "7.3.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -7099,42 +10143,20 @@ "steppedLine": false, "targets": [ { - "expr": "histogram_quantile(0.5, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "50%", + "expr": "rate(synapse_external_cache_get{job=\"$job\", instance=\"$instance\", index=~\"$index\"}[$bucket_size])", + "interval": "", + "legendFormat": "{{ cache_name }} {{ index }}", "refId": "A" - }, - { - "expr": "histogram_quantile(0.75, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "75%", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.90, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "90%", - "refId": "C" - }, - { - "expr": "histogram_quantile(0.99, rate(synapse_storage_events_stale_forward_extremities_persisted_bucket{instance=\"$instance\"}[$bucket_size]) and on (index, instance, job) (synapse_storage_events_persisted_events > 0))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "99%", - "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, - "title": "Events persisted, by number of stale forward extremities in room (quantiles)", + "title": "External Cache Get Rate", "tooltip": { "shared": true, - "sort": 0, + "sort": 2, "value_type": "individual" }, "type": "graph", @@ -7147,14 +10169,16 @@ }, "yaxes": [ { - "format": "short", - "label": "Number of stale forward extremities in room", + "$$hashKey": "object:390", + "format": "hertz", + "label": null, "logBase": 1, "max": null, - "min": "0", + "min": null, "show": true }, { + "$$hashKey": "object:391", "format": "short", "label": null, "logBase": 1, @@ -7170,52 +10194,57 @@ }, { "cards": { - "cardPadding": 0, + "cardPadding": -1, "cardRound": null }, "color": { - "cardColor": "#73BF69", + "cardColor": "#b4ff00", "colorScale": "sqrt", "colorScheme": "interpolateInferno", "exponent": 0.5, "min": 0, - "mode": "opacity" + "mode": "spectrum" }, "dataFormat": "tsbuckets", "datasource": "$datasource", - "description": "Colour reflects the number of state resolution operations performed over the given number of state groups, or fewer.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, "gridPos": { - "h": 8, + "h": 9, "w": 12, "x": 0, - "y": 110 + "y": 9 }, "heatmap": {}, - "hideZeroBuckets": true, + "hideZeroBuckets": false, "highlightCards": true, - "id": 131, + "id": 195, "legend": { - "show": true + "show": false }, "links": [], "reverseYBuckets": false, "targets": [ { - "expr": "rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size])", + "expr": "sum(rate(synapse_external_cache_response_time_seconds_bucket{index=~\"$index\",instance=\"$instance\",job=\"$job\"}[$bucket_size])) by (le)", "format": "heatmap", + "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{le}}", "refId": "A" } ], - "timeFrom": null, - "timeShift": null, - "title": "Number of state resolution performed, by number of state groups involved (heatmap)", + "title": "External Cache Response Time", "tooltip": { "show": true, "showHistogram": true }, + "tooltipDecimals": 2, "type": "heatmap", "xAxis": { "show": true @@ -7224,7 +10253,7 @@ "xBucketSize": null, "yAxis": { "decimals": 0, - "format": "short", + "format": "s", "logBase": 1, "max": null, "min": null, @@ -7234,131 +10263,14 @@ "yBucketBound": "auto", "yBucketNumber": null, "yBucketSize": null - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "description": "For a given percentage P, the number X where P% of state resolution operations took place over X state groups or fewer.", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 110 - }, - "hiddenSeries": false, - "id": 132, - "interval": "", - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "options": { - "dataLinks": [] - }, - "percentage": false, - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.5, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", - "format": "time_series", - "interval": "", - "intervalFactor": 1, - "legendFormat": "50%", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.75, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", - "format": "time_series", - "interval": "", - "intervalFactor": 1, - "legendFormat": "75%", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.90, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", - "format": "time_series", - "interval": "", - "intervalFactor": 1, - "legendFormat": "90%", - "refId": "C" - }, - { - "expr": "histogram_quantile(0.99, rate(synapse_state_number_state_groups_in_resolution_bucket{instance=\"$instance\",job=~\"$job\",index=~\"$index\"}[$bucket_size]))", - "format": "time_series", - "interval": "", - "intervalFactor": 1, - "legendFormat": "99%", - "refId": "D" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Number of state resolutions performed, by number of state groups involved (quantiles)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "Number of state groups", - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } } ], - "title": "Extremities", + "title": "External Cache", "type": "row" } ], - "refresh": "5m", - "schemaVersion": 22, + "refresh": false, + "schemaVersion": 26, "style": "dark", "tags": [ "matrix" @@ -7368,9 +10280,10 @@ { "current": { "selected": false, - "text": "Prometheus", - "value": "Prometheus" + "text": "default", + "value": "default" }, + "error": null, "hide": 0, "includeAll": false, "label": null, @@ -7378,6 +10291,7 @@ "name": "datasource", "options": [], "query": "prometheus", + "queryValue": "", "refresh": 1, "regex": "", "skipUrlSync": false, @@ -7387,13 +10301,14 @@ "allFormat": "glob", "auto": true, "auto_count": 100, - "auto_min": "30s", + "auto_min": "60s", "current": { "selected": false, "text": "auto", "value": "$__auto_interval_bucket_size" }, "datasource": null, + "error": null, "hide": 0, "includeAll": false, "label": "Bucket Size", @@ -7438,6 +10353,7 @@ } ], "query": "30s,1m,2m,5m,10m,15m", + "queryValue": "", "refresh": 2, "skipUrlSync": false, "type": "interval" @@ -7447,9 +10363,9 @@ "current": {}, "datasource": "$datasource", "definition": "", + "error": null, "hide": 0, "includeAll": false, - "index": -1, "label": null, "multi": false, "name": "instance", @@ -7458,7 +10374,7 @@ "refresh": 2, "regex": "", "skipUrlSync": false, - "sort": 0, + "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", @@ -7471,10 +10387,10 @@ "current": {}, "datasource": "$datasource", "definition": "", + "error": null, "hide": 0, "hideLabel": false, "includeAll": true, - "index": -1, "label": "Job", "multi": true, "multiFormat": "regex values", @@ -7498,10 +10414,10 @@ "current": {}, "datasource": "$datasource", "definition": "", + "error": null, "hide": 0, "hideLabel": false, "includeAll": true, - "index": -1, "label": "", "multi": true, "multiFormat": "regex values", @@ -7522,7 +10438,7 @@ ] }, "time": { - "from": "now-1h", + "from": "now-3h", "to": "now" }, "timepicker": { @@ -7554,8 +10470,5 @@ "timezone": "", "title": "Synapse", "uid": "000000012", - "variables": { - "list": [] - }, - "version": 32 + "version": 90 } \ No newline at end of file From 141b073c7bf649ac12b7e12b118770677a64f51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Junquera=20S=C3=A1nchez?= Date: Thu, 20 May 2021 15:24:19 +0200 Subject: [PATCH 173/619] Update user_directory.md (#10016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Javier Junquera Sánchez --- changelog.d/10016.doc | 1 + docs/user_directory.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10016.doc diff --git a/changelog.d/10016.doc b/changelog.d/10016.doc new file mode 100644 index 0000000000..f9b615d7d7 --- /dev/null +++ b/changelog.d/10016.doc @@ -0,0 +1 @@ +Fix broken link in user directory documentation. Contributed by @junquera. diff --git a/docs/user_directory.md b/docs/user_directory.md index 872fc21979..d4f38d2cf1 100644 --- a/docs/user_directory.md +++ b/docs/user_directory.md @@ -7,6 +7,6 @@ who are present in a publicly viewable room present on the server. The directory info is stored in various tables, which can (typically after DB corruption) get stale or out of sync. If this happens, for now the -solution to fix it is to execute the SQL [here](../synapse/storage/databases/main/schema/delta/53/user_dir_populate.sql) +solution to fix it is to execute the SQL [here](https://github.com/matrix-org/synapse/blob/master/synapse/storage/schema/main/delta/53/user_dir_populate.sql) and then restart synapse. This should then start a background task to flush the current tables and regenerate the directory. From 551d2c3f4b492d59b3c670c1a8e82869b16a594d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 20 May 2021 11:10:36 -0400 Subject: [PATCH 174/619] Allow a user who could join a restricted room to see it in spaces summary. (#9922) This finishes up the experimental implementation of MSC3083 by showing the restricted rooms in the spaces summary (from MSC2946). --- changelog.d/9922.feature | 1 + synapse/federation/transport/server.py | 2 +- synapse/handlers/event_auth.py | 104 ++++++++++--- synapse/handlers/space_summary.py | 201 +++++++++++++++++++++---- 4 files changed, 254 insertions(+), 54 deletions(-) create mode 100644 changelog.d/9922.feature diff --git a/changelog.d/9922.feature b/changelog.d/9922.feature new file mode 100644 index 0000000000..2c655350c0 --- /dev/null +++ b/changelog.d/9922.feature @@ -0,0 +1 @@ +Experimental support to allow a user who could join a restricted room to view it in the spaces summary. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index e1b7462474..c17a085a4f 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1428,7 +1428,7 @@ async def on_POST( ) return 200, await self.handler.federation_space_summary( - room_id, suggested_only, max_rooms_per_space, exclude_rooms + origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms ) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 5b2fe103e7..a0df16a32f 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Collection, Optional from synapse.api.constants import EventTypes, JoinRules, Membership from synapse.api.errors import AuthError @@ -59,32 +59,76 @@ async def check_restricted_join_rules( ): return + # This is not a room with a restricted join rule, so we don't need to do the + # restricted room specific checks. + # + # Note: We'll be applying the standard join rule checks later, which will + # catch the cases of e.g. trying to join private rooms without an invite. + if not await self.has_restricted_join_rules(state_ids, room_version): + return + + # Get the spaces which allow access to this room and check if the user is + # in any of them. + allowed_spaces = await self.get_spaces_that_allow_join(state_ids) + if not await self.is_user_in_rooms(allowed_spaces, user_id): + raise AuthError( + 403, + "You do not belong to any of the required spaces to join this room.", + ) + + async def has_restricted_join_rules( + self, state_ids: StateMap[str], room_version: RoomVersion + ) -> bool: + """ + Return if the room has the proper join rules set for access via spaces. + + Args: + state_ids: The state of the room as it currently is. + room_version: The room version of the room to query. + + Returns: + True if the proper room version and join rules are set for restricted access. + """ # This only applies to room versions which support the new join rule. if not room_version.msc3083_join_rules: - return + return False # If there's no join rule, then it defaults to invite (so this doesn't apply). join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) if not join_rules_event_id: - return + return False + + # If the join rule is not restricted, this doesn't apply. + join_rules_event = await self._store.get_event(join_rules_event_id) + return join_rules_event.content.get("join_rule") == JoinRules.MSC3083_RESTRICTED + + async def get_spaces_that_allow_join( + self, state_ids: StateMap[str] + ) -> Collection[str]: + """ + Generate a list of spaces which allow access to a room. + + Args: + state_ids: The state of the room as it currently is. + + Returns: + A collection of spaces which provide membership to the room. + """ + # If there's no join rule, then it defaults to invite (so this doesn't apply). + join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) + if not join_rules_event_id: + return () # If the join rule is not restricted, this doesn't apply. join_rules_event = await self._store.get_event(join_rules_event_id) - if join_rules_event.content.get("join_rule") != JoinRules.MSC3083_RESTRICTED: - return # If allowed is of the wrong form, then only allow invited users. allowed_spaces = join_rules_event.content.get("allow", []) if not isinstance(allowed_spaces, list): - allowed_spaces = () - - # Get the list of joined rooms and see if there's an overlap. - if allowed_spaces: - joined_rooms = await self._store.get_rooms_for_user(user_id) - else: - joined_rooms = () + return () # Pull out the other room IDs, invalid data gets filtered. + result = [] for space in allowed_spaces: if not isinstance(space, dict): continue @@ -93,13 +137,31 @@ async def check_restricted_join_rules( if not isinstance(space_id, str): continue - # The user was joined to one of the spaces specified, they can join - # this room! - if space_id in joined_rooms: - return + result.append(space_id) + + return result + + async def is_user_in_rooms(self, room_ids: Collection[str], user_id: str) -> bool: + """ + Check whether a user is a member of any of the provided rooms. + + Args: + room_ids: The rooms to check for membership. + user_id: The user to check. + + Returns: + True if the user is in any of the rooms, false otherwise. + """ + if not room_ids: + return False + + # Get the list of joined rooms and see if there's an overlap. + joined_rooms = await self._store.get_rooms_for_user(user_id) + + # Check each room and see if the user is in it. + for room_id in room_ids: + if room_id in joined_rooms: + return True - # The user was not in any of the required spaces. - raise AuthError( - 403, - "You do not belong to any of the required spaces to join this room.", - ) + # The user was not in any of the rooms. + return False diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index eb80a5ad67..8d49ba8164 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -16,11 +16,16 @@ import logging import re from collections import deque -from typing import TYPE_CHECKING, Iterable, List, Optional, Sequence, Set, Tuple, cast +from typing import TYPE_CHECKING, Iterable, List, Optional, Sequence, Set, Tuple import attr -from synapse.api.constants import EventContentFields, EventTypes, HistoryVisibility +from synapse.api.constants import ( + EventContentFields, + EventTypes, + HistoryVisibility, + Membership, +) from synapse.api.errors import AuthError from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 @@ -47,6 +52,7 @@ def __init__(self, hs: "HomeServer"): self._auth = hs.get_auth() self._room_list_handler = hs.get_room_list_handler() self._state_handler = hs.get_state_handler() + self._event_auth_handler = hs.get_event_auth_handler() self._store = hs.get_datastore() self._event_serializer = hs.get_event_client_serializer() self._server_name = hs.hostname @@ -111,28 +117,88 @@ async def get_space_summary( max_children = max_rooms_per_space if processed_rooms else None if is_in_room: - rooms, events = await self._summarize_local_room( - requester, room_id, suggested_only, max_children + room, events = await self._summarize_local_room( + requester, None, room_id, suggested_only, max_children ) + + logger.debug( + "Query of local room %s returned events %s", + room_id, + ["%s->%s" % (ev["room_id"], ev["state_key"]) for ev in events], + ) + + if room: + rooms_result.append(room) else: - rooms, events = await self._summarize_remote_room( + fed_rooms, fed_events = await self._summarize_remote_room( queue_entry, suggested_only, max_children, exclude_rooms=processed_rooms, ) - logger.debug( - "Query of %s returned rooms %s, events %s", - queue_entry.room_id, - [room.get("room_id") for room in rooms], - ["%s->%s" % (ev["room_id"], ev["state_key"]) for ev in events], - ) - - rooms_result.extend(rooms) - - # any rooms returned don't need visiting again - processed_rooms.update(cast(str, room.get("room_id")) for room in rooms) + # The results over federation might include rooms that the we, + # as the requesting server, are allowed to see, but the requesting + # user is not permitted see. + # + # Filter the returned results to only what is accessible to the user. + room_ids = set() + events = [] + for room in fed_rooms: + fed_room_id = room.get("room_id") + if not fed_room_id or not isinstance(fed_room_id, str): + continue + + # The room should only be included in the summary if: + # a. the user is in the room; + # b. the room is world readable; or + # c. the user is in a space that has been granted access to + # the room. + # + # Note that we know the user is not in the root room (which is + # why the remote call was made in the first place), but the user + # could be in one of the children rooms and we just didn't know + # about the link. + include_room = room.get("world_readable") is True + + # Check if the user is a member of any of the allowed spaces + # from the response. + allowed_spaces = room.get("allowed_spaces") + if ( + not include_room + and allowed_spaces + and isinstance(allowed_spaces, list) + ): + include_room = await self._event_auth_handler.is_user_in_rooms( + allowed_spaces, requester + ) + + # Finally, if this isn't the requested room, check ourselves + # if we can access the room. + if not include_room and fed_room_id != queue_entry.room_id: + include_room = await self._is_room_accessible( + fed_room_id, requester, None + ) + + # The user can see the room, include it! + if include_room: + rooms_result.append(room) + room_ids.add(fed_room_id) + + # All rooms returned don't need visiting again (even if the user + # didn't have access to them). + processed_rooms.add(fed_room_id) + + for event in fed_events: + if event.get("room_id") in room_ids: + events.append(event) + + logger.debug( + "Query of %s returned rooms %s, events %s", + room_id, + [room.get("room_id") for room in fed_rooms], + ["%s->%s" % (ev["room_id"], ev["state_key"]) for ev in fed_events], + ) # the room we queried may or may not have been returned, but don't process # it again, anyway. @@ -158,10 +224,16 @@ async def get_space_summary( ) processed_events.add(ev_key) + # Before returning to the client, remove the allowed_spaces key for any + # rooms. + for room in rooms_result: + room.pop("allowed_spaces", None) + return {"rooms": rooms_result, "events": events_result} async def federation_space_summary( self, + origin: str, room_id: str, suggested_only: bool, max_rooms_per_space: Optional[int], @@ -171,6 +243,8 @@ async def federation_space_summary( Implementation of the space summary Federation API Args: + origin: The server requesting the spaces summary. + room_id: room id to start the summary at suggested_only: whether we should only return children with the "suggested" @@ -205,14 +279,15 @@ async def federation_space_summary( logger.debug("Processing room %s", room_id) - rooms, events = await self._summarize_local_room( - None, room_id, suggested_only, max_rooms_per_space + room, events = await self._summarize_local_room( + None, origin, room_id, suggested_only, max_rooms_per_space ) processed_rooms.add(room_id) - rooms_result.extend(rooms) - events_result.extend(events) + if room: + rooms_result.append(room) + events_result.extend(events) # add any children to the queue room_queue.extend(edge_event["state_key"] for edge_event in events) @@ -222,10 +297,11 @@ async def federation_space_summary( async def _summarize_local_room( self, requester: Optional[str], + origin: Optional[str], room_id: str, suggested_only: bool, max_children: Optional[int], - ) -> Tuple[Sequence[JsonDict], Sequence[JsonDict]]: + ) -> Tuple[Optional[JsonDict], Sequence[JsonDict]]: """ Generate a room entry and a list of event entries for a given room. @@ -233,6 +309,9 @@ async def _summarize_local_room( requester: The user requesting the summary, if it is a local request. None if this is a federation request. + origin: + The server requesting the summary, if it is a federation request. + None if this is a local request. room_id: The room ID to summarize. suggested_only: True if only suggested children should be returned. Otherwise, all children are returned. @@ -247,8 +326,8 @@ async def _summarize_local_room( An iterable of the sorted children events. This may be limited to a maximum size or may include all children. """ - if not await self._is_room_accessible(room_id, requester): - return (), () + if not await self._is_room_accessible(room_id, requester, origin): + return None, () room_entry = await self._build_room_entry(room_id) @@ -272,7 +351,7 @@ async def _summarize_local_room( event_format=format_event_for_client_v2, ) ) - return (room_entry,), events_result + return room_entry, events_result async def _summarize_remote_room( self, @@ -332,13 +411,17 @@ async def _summarize_remote_room( or ev.event_type == EventTypes.SpaceChild ) - async def _is_room_accessible(self, room_id: str, requester: Optional[str]) -> bool: + async def _is_room_accessible( + self, room_id: str, requester: Optional[str], origin: Optional[str] + ) -> bool: """ Calculate whether the room should be shown in the spaces summary. It should be included if: * The requester is joined or invited to the room. + * The requester can join without an invite (per MSC3083). + * The origin server has any user that is joined or invited to the room. * The history visibility is set to world readable. Args: @@ -346,31 +429,75 @@ async def _is_room_accessible(self, room_id: str, requester: Optional[str]) -> b requester: The user requesting the summary, if it is a local request. None if this is a federation request. + origin: + The server requesting the summary, if it is a federation request. + None if this is a local request. Returns: True if the room should be included in the spaces summary. """ + state_ids = await self._store.get_current_state_ids(room_id) + + # If there's no state for the room, it isn't known. + if not state_ids: + logger.info("room %s is unknown, omitting from summary", room_id) + return False + + room_version = await self._store.get_room_version(room_id) # if we have an authenticated requesting user, first check if they are able to view # stripped state in the room. if requester: + member_event_id = state_ids.get((EventTypes.Member, requester), None) + + # If they're in the room they can see info on it. + member_event = None + if member_event_id: + member_event = await self._store.get_event(member_event_id) + if member_event.membership in (Membership.JOIN, Membership.INVITE): + return True + + # Otherwise, check if they should be allowed access via membership in a space. try: - await self._auth.check_user_in_room(room_id, requester) - return True + await self._event_auth_handler.check_restricted_join_rules( + state_ids, room_version, requester, member_event + ) except AuthError: + # The user doesn't have access due to spaces, but might have access + # another way. Keep trying. pass + else: + return True + + # If this is a request over federation, check if the host is in the room or + # is in one of the spaces specified via the join rules. + elif origin: + if await self._auth.check_host_in_room(room_id, origin): + return True + + # Alternately, if the host has a user in any of the spaces specified + # for access, then the host can see this room (and should do filtering + # if the requester cannot see it). + if await self._event_auth_handler.has_restricted_join_rules( + state_ids, room_version + ): + allowed_spaces = ( + await self._event_auth_handler.get_spaces_that_allow_join(state_ids) + ) + for space_id in allowed_spaces: + if await self._auth.check_host_in_room(space_id, origin): + return True # otherwise, check if the room is peekable - hist_vis_ev = await self._state_handler.get_current_state( - room_id, EventTypes.RoomHistoryVisibility, "" - ) - if hist_vis_ev: + hist_vis_event_id = state_ids.get((EventTypes.RoomHistoryVisibility, ""), None) + if hist_vis_event_id: + hist_vis_ev = await self._store.get_event(hist_vis_event_id) hist_vis = hist_vis_ev.content.get("history_visibility") if hist_vis == HistoryVisibility.WORLD_READABLE: return True logger.info( - "room %s is unpeekable and user %s is not a member, omitting from summary", + "room %s is unpeekable and user %s is not a member / not allowed to join, omitting from summary", room_id, requester, ) @@ -395,6 +522,15 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: if not room_type: room_type = create_event.content.get(EventContentFields.MSC1772_ROOM_TYPE) + room_version = await self._store.get_room_version(room_id) + allowed_spaces = None + if await self._event_auth_handler.has_restricted_join_rules( + current_state_ids, room_version + ): + allowed_spaces = await self._event_auth_handler.get_spaces_that_allow_join( + current_state_ids + ) + entry = { "room_id": stats["room_id"], "name": stats["name"], @@ -408,6 +544,7 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: "guest_can_join": stats["guest_access"] == "can_join", "creation_ts": create_event.origin_server_ts, "room_type": room_type, + "allowed_spaces": allowed_spaces, } # Filter out Nones – rather omit the field altogether From 64887f06fcac63e069364d625d984b4951bf1ffc Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 20 May 2021 16:11:48 +0100 Subject: [PATCH 175/619] Use ijson to parse the response to `/send_join`, reducing memory usage. (#9958) Instead of parsing the full response to `/send_join` into Python objects (which can be huge for large rooms) and *then* parsing that into events, we instead use ijson to stream parse the response directly into `EventBase` objects. --- changelog.d/9958.feature | 1 + mypy.ini | 3 + synapse/federation/federation_client.py | 28 ++--- synapse/federation/transport/client.py | 85 ++++++++++++- synapse/http/client.py | 7 +- synapse/http/matrixfederationclient.py | 160 ++++++++++++++++++------ synapse/python_dependencies.py | 1 + 7 files changed, 227 insertions(+), 58 deletions(-) create mode 100644 changelog.d/9958.feature diff --git a/changelog.d/9958.feature b/changelog.d/9958.feature new file mode 100644 index 0000000000..d86ba36519 --- /dev/null +++ b/changelog.d/9958.feature @@ -0,0 +1 @@ +Reduce memory usage when joining very large rooms over federation. diff --git a/mypy.ini b/mypy.ini index ea655a0d4d..1d1d1ea0f2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -174,3 +174,6 @@ ignore_missing_imports = True [mypy-pympler.*] ignore_missing_imports = True + +[mypy-ijson.*] +ignore_missing_imports = True diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index a5b6a61195..e0e9f5d0be 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -55,6 +55,7 @@ ) from synapse.events import EventBase, builder from synapse.federation.federation_base import FederationBase, event_from_pdu_json +from synapse.federation.transport.client import SendJoinResponse from synapse.logging.context import make_deferred_yieldable, preserve_fn from synapse.logging.utils import log_function from synapse.types import JsonDict, get_domain_from_id @@ -665,19 +666,10 @@ async def send_join( """ async def send_request(destination) -> Dict[str, Any]: - content = await self._do_send_join(destination, pdu) + response = await self._do_send_join(room_version, destination, pdu) - logger.debug("Got content: %s", content) - - state = [ - event_from_pdu_json(p, room_version, outlier=True) - for p in content.get("state", []) - ] - - auth_chain = [ - event_from_pdu_json(p, room_version, outlier=True) - for p in content.get("auth_chain", []) - ] + state = response.state + auth_chain = response.auth_events pdus = {p.event_id: p for p in itertools.chain(state, auth_chain)} @@ -752,11 +744,14 @@ async def send_request(destination) -> Dict[str, Any]: return await self._try_destination_list("send_join", destinations, send_request) - async def _do_send_join(self, destination: str, pdu: EventBase) -> JsonDict: + async def _do_send_join( + self, room_version: RoomVersion, destination: str, pdu: EventBase + ) -> SendJoinResponse: time_now = self._clock.time_msec() try: return await self.transport_layer.send_join_v2( + room_version=room_version, destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, @@ -771,17 +766,14 @@ async def _do_send_join(self, destination: str, pdu: EventBase) -> JsonDict: logger.debug("Couldn't send_join with the v2 API, falling back to the v1 API") - resp = await self.transport_layer.send_join_v1( + return await self.transport_layer.send_join_v1( + room_version=room_version, destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) - # We expect the v1 API to respond with [200, content], so we only return the - # content. - return resp[1] - async def send_invite( self, destination: str, diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 497848a2b7..e93ab83f7f 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -17,13 +17,19 @@ import urllib from typing import Any, Dict, List, Optional +import attr +import ijson + from synapse.api.constants import Membership from synapse.api.errors import Codes, HttpResponseException, SynapseError +from synapse.api.room_versions import RoomVersion from synapse.api.urls import ( FEDERATION_UNSTABLE_PREFIX, FEDERATION_V1_PREFIX, FEDERATION_V2_PREFIX, ) +from synapse.events import EventBase, make_event_from_dict +from synapse.http.matrixfederationclient import ByteParser from synapse.logging.utils import log_function from synapse.types import JsonDict @@ -240,21 +246,36 @@ async def make_membership_event( return content @log_function - async def send_join_v1(self, destination, room_id, event_id, content): + async def send_join_v1( + self, + room_version, + destination, + room_id, + event_id, + content, + ) -> "SendJoinResponse": path = _create_v1_path("/send_join/%s/%s", room_id, event_id) response = await self.client.put_json( - destination=destination, path=path, data=content + destination=destination, + path=path, + data=content, + parser=SendJoinParser(room_version, v1_api=True), ) return response @log_function - async def send_join_v2(self, destination, room_id, event_id, content): + async def send_join_v2( + self, room_version, destination, room_id, event_id, content + ) -> "SendJoinResponse": path = _create_v2_path("/send_join/%s/%s", room_id, event_id) response = await self.client.put_json( - destination=destination, path=path, data=content + destination=destination, + path=path, + data=content, + parser=SendJoinParser(room_version, v1_api=False), ) return response @@ -1053,3 +1074,59 @@ def _create_v2_path(path, *args): str """ return _create_path(FEDERATION_V2_PREFIX, path, *args) + + +@attr.s(slots=True, auto_attribs=True) +class SendJoinResponse: + """The parsed response of a `/send_join` request.""" + + auth_events: List[EventBase] + state: List[EventBase] + + +@ijson.coroutine +def _event_list_parser(room_version: RoomVersion, events: List[EventBase]): + """Helper function for use with `ijson.items_coro` to parse an array of + events and add them to the given list. + """ + + while True: + obj = yield + event = make_event_from_dict(obj, room_version) + events.append(event) + + +class SendJoinParser(ByteParser[SendJoinResponse]): + """A parser for the response to `/send_join` requests. + + Args: + room_version: The version of the room. + v1_api: Whether the response is in the v1 format. + """ + + CONTENT_TYPE = "application/json" + + def __init__(self, room_version: RoomVersion, v1_api: bool): + self._response = SendJoinResponse([], []) + + # The V1 API has the shape of `[200, {...}]`, which we handle by + # prefixing with `item.*`. + prefix = "item." if v1_api else "" + + self._coro_state = ijson.items_coro( + _event_list_parser(room_version, self._response.state), + prefix + "state.item", + ) + self._coro_auth = ijson.items_coro( + _event_list_parser(room_version, self._response.auth_events), + prefix + "auth_chain.item", + ) + + def write(self, data: bytes) -> int: + self._coro_state.send(data) + self._coro_auth.send(data) + + return len(data) + + def finish(self) -> SendJoinResponse: + return self._response diff --git a/synapse/http/client.py b/synapse/http/client.py index 5f40f16e24..1ca6624fd5 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -813,7 +813,12 @@ def dataReceived(self, data: bytes) -> None: if self.deferred.called: return - self.stream.write(data) + try: + self.stream.write(data) + except Exception: + self.deferred.errback() + return + self.length += len(data) # The first time the maximum size is exceeded, error and cancel the # connection. dataReceived might be called again if data was received diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index bb837b7b19..f5503b394b 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import abc import cgi import codecs import logging @@ -19,13 +20,24 @@ import typing import urllib.parse from io import BytesIO, StringIO -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import ( + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + TypeVar, + Union, + overload, +) import attr import treq from canonicaljson import encode_canonical_json from prometheus_client import Counter from signedjson.sign import sign_json +from typing_extensions import Literal from twisted.internet import defer from twisted.internet.error import DNSLookupError @@ -48,6 +60,7 @@ BlacklistingAgentWrapper, BlacklistingReactorWrapper, BodyExceededMaxSize, + ByteWriteable, encode_query_args, read_body_with_max_size, ) @@ -88,6 +101,27 @@ QueryArgs = Dict[str, Union[str, List[str]]] +T = TypeVar("T") + + +class ByteParser(ByteWriteable, Generic[T], abc.ABC): + """A `ByteWriteable` that has an additional `finish` function that returns + the parsed data. + """ + + CONTENT_TYPE = abc.abstractproperty() # type: str # type: ignore + """The expected content type of the response, e.g. `application/json`. If + the content type doesn't match we fail the request. + """ + + @abc.abstractmethod + def finish(self) -> T: + """Called when response has finished streaming and the parser should + return the final result (or error). + """ + pass + + @attr.s(slots=True, frozen=True) class MatrixFederationRequest: method = attr.ib(type=str) @@ -148,15 +182,32 @@ def get_json(self) -> Optional[JsonDict]: return self.json -async def _handle_json_response( +class JsonParser(ByteParser[Union[JsonDict, list]]): + """A parser that buffers the response and tries to parse it as JSON.""" + + CONTENT_TYPE = "application/json" + + def __init__(self): + self._buffer = StringIO() + self._binary_wrapper = BinaryIOWrapper(self._buffer) + + def write(self, data: bytes) -> int: + return self._binary_wrapper.write(data) + + def finish(self) -> Union[JsonDict, list]: + return json_decoder.decode(self._buffer.getvalue()) + + +async def _handle_response( reactor: IReactorTime, timeout_sec: float, request: MatrixFederationRequest, response: IResponse, start_ms: int, -) -> JsonDict: + parser: ByteParser[T], +) -> T: """ - Reads the JSON body of a response, with a timeout + Reads the body of a response with a timeout and sends it to a parser Args: reactor: twisted reactor, for the timeout @@ -164,23 +215,21 @@ async def _handle_json_response( request: the request that triggered the response response: response to the request start_ms: Timestamp when request was made + parser: The parser for the response Returns: - The parsed JSON response + The parsed response """ + try: - check_content_type_is_json(response.headers) + check_content_type_is(response.headers, parser.CONTENT_TYPE) - buf = StringIO() - d = read_body_with_max_size(response, BinaryIOWrapper(buf), MAX_RESPONSE_SIZE) + d = read_body_with_max_size(response, parser, MAX_RESPONSE_SIZE) d = timeout_deferred(d, timeout=timeout_sec, reactor=reactor) - def parse(_len: int): - return json_decoder.decode(buf.getvalue()) - - d.addCallback(parse) + length = await make_deferred_yieldable(d) - body = await make_deferred_yieldable(d) + value = parser.finish() except BodyExceededMaxSize as e: # The response was too big. logger.warning( @@ -193,9 +242,9 @@ def parse(_len: int): ) raise RequestSendFailed(e, can_retry=False) from e except ValueError as e: - # The JSON content was invalid. + # The content was invalid. logger.warning( - "{%s} [%s] Failed to parse JSON response - %s %s", + "{%s} [%s] Failed to parse response - %s %s", request.txn_id, request.destination, request.method, @@ -225,16 +274,17 @@ def parse(_len: int): time_taken_secs = reactor.seconds() - start_ms / 1000 logger.info( - "{%s} [%s] Completed request: %d %s in %.2f secs - %s %s", + "{%s} [%s] Completed request: %d %s in %.2f secs, got %d bytes - %s %s", request.txn_id, request.destination, response.code, response.phrase.decode("ascii", errors="replace"), time_taken_secs, + length, request.method, request.uri.decode("ascii"), ) - return body + return value class BinaryIOWrapper: @@ -671,6 +721,7 @@ def build_auth_headers( ) return auth_headers + @overload async def put_json( self, destination: str, @@ -683,7 +734,41 @@ async def put_json( ignore_backoff: bool = False, backoff_on_404: bool = False, try_trailing_slash_on_400: bool = False, + parser: Literal[None] = None, ) -> Union[JsonDict, list]: + ... + + @overload + async def put_json( + self, + destination: str, + path: str, + args: Optional[QueryArgs] = None, + data: Optional[JsonDict] = None, + json_data_callback: Optional[Callable[[], JsonDict]] = None, + long_retries: bool = False, + timeout: Optional[int] = None, + ignore_backoff: bool = False, + backoff_on_404: bool = False, + try_trailing_slash_on_400: bool = False, + parser: Optional[ByteParser[T]] = None, + ) -> T: + ... + + async def put_json( + self, + destination: str, + path: str, + args: Optional[QueryArgs] = None, + data: Optional[JsonDict] = None, + json_data_callback: Optional[Callable[[], JsonDict]] = None, + long_retries: bool = False, + timeout: Optional[int] = None, + ignore_backoff: bool = False, + backoff_on_404: bool = False, + try_trailing_slash_on_400: bool = False, + parser: Optional[ByteParser] = None, + ): """Sends the specified json data using PUT Args: @@ -716,6 +801,8 @@ async def put_json( of the request. Workaround for #3622 in Synapse <= v0.99.3. This will be attempted before backing off if backing off has been enabled. + parser: The parser to use to decode the response. Defaults to + parsing as JSON. Returns: Succeeds when we get a 2xx HTTP response. The @@ -756,8 +843,16 @@ async def put_json( else: _sec_timeout = self.default_timeout - body = await _handle_json_response( - self.reactor, _sec_timeout, request, response, start_ms + if parser is None: + parser = JsonParser() + + body = await _handle_response( + self.reactor, + _sec_timeout, + request, + response, + start_ms, + parser=parser, ) return body @@ -830,12 +925,8 @@ async def post_json( else: _sec_timeout = self.default_timeout - body = await _handle_json_response( - self.reactor, - _sec_timeout, - request, - response, - start_ms, + body = await _handle_response( + self.reactor, _sec_timeout, request, response, start_ms, parser=JsonParser() ) return body @@ -907,8 +998,8 @@ async def get_json( else: _sec_timeout = self.default_timeout - body = await _handle_json_response( - self.reactor, _sec_timeout, request, response, start_ms + body = await _handle_response( + self.reactor, _sec_timeout, request, response, start_ms, parser=JsonParser() ) return body @@ -975,8 +1066,8 @@ async def delete_json( else: _sec_timeout = self.default_timeout - body = await _handle_json_response( - self.reactor, _sec_timeout, request, response, start_ms + body = await _handle_response( + self.reactor, _sec_timeout, request, response, start_ms, parser=JsonParser() ) return body @@ -1068,16 +1159,16 @@ def _flatten_response_never_received(e): return repr(e) -def check_content_type_is_json(headers: Headers) -> None: +def check_content_type_is(headers: Headers, expected_content_type: str) -> None: """ Check that a set of HTTP headers have a Content-Type header, and that it - is application/json. + is the expected value.. Args: headers: headers to check Raises: - RequestSendFailed: if the Content-Type header is missing or isn't JSON + RequestSendFailed: if the Content-Type header is missing or doesn't match """ content_type_headers = headers.getRawHeaders(b"Content-Type") @@ -1089,11 +1180,10 @@ def check_content_type_is_json(headers: Headers) -> None: c_type = content_type_headers[0].decode("ascii") # only the first header val, options = cgi.parse_header(c_type) - if val != "application/json": + if val != expected_content_type: raise RequestSendFailed( RuntimeError( - "Remote server sent Content-Type header of '%s', not 'application/json'" - % c_type, + f"Remote server sent Content-Type header of '{c_type}', not '{expected_content_type}'", ), can_retry=False, ) diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 989523c823..546231bec0 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -87,6 +87,7 @@ # We enforce that we have a `cryptography` version that bundles an `openssl` # with the latest security patches. "cryptography>=3.4.7", + "ijson>=3.0", ] CONDITIONAL_REQUIREMENTS = { From 1c6a19002cb56cf93bc920854c81ae88bd7308ac Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 20 May 2021 16:25:11 +0100 Subject: [PATCH 176/619] Add `Keyring.verify_events_for_server` and reduce memory usage (#10018) Also add support for giving a callback to generate the JSON object to verify. This should reduce memory usage, as we no longer have the event in memory in dict form (which has a large memory footprint) for extend periods of time. --- changelog.d/10018.misc | 1 + synapse/crypto/keyring.py | 98 ++++++++++++++++++++++++--- synapse/federation/federation_base.py | 17 ++--- 3 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 changelog.d/10018.misc diff --git a/changelog.d/10018.misc b/changelog.d/10018.misc new file mode 100644 index 0000000000..eaf9f64867 --- /dev/null +++ b/changelog.d/10018.misc @@ -0,0 +1 @@ +Reduce memory usage when verifying signatures on large numbers of events at once. diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 5f18ef7748..6fc0712978 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -17,7 +17,7 @@ import logging import urllib from collections import defaultdict -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Set, Tuple import attr from signedjson.key import ( @@ -42,6 +42,8 @@ SynapseError, ) from synapse.config.key import TrustedKeyServer +from synapse.events import EventBase +from synapse.events.utils import prune_event_dict from synapse.logging.context import ( PreserveLoggingContext, make_deferred_yieldable, @@ -69,7 +71,11 @@ class VerifyJsonRequest: Attributes: server_name: The name of the server to verify against. - json_object: The JSON object to verify. + get_json_object: A callback to fetch the JSON object to verify. + A callback is used to allow deferring the creation of the JSON + object to verify until needed, e.g. for events we can defer + creating the redacted copy. This reduces the memory usage when + there are large numbers of in flight requests. minimum_valid_until_ts: time at which we require the signing key to be valid. (0 implies we don't care) @@ -88,14 +94,50 @@ class VerifyJsonRequest: """ server_name = attr.ib(type=str) - json_object = attr.ib(type=JsonDict) + get_json_object = attr.ib(type=Callable[[], JsonDict]) minimum_valid_until_ts = attr.ib(type=int) request_name = attr.ib(type=str) - key_ids = attr.ib(init=False, type=List[str]) + key_ids = attr.ib(type=List[str]) key_ready = attr.ib(default=attr.Factory(defer.Deferred), type=defer.Deferred) - def __attrs_post_init__(self): - self.key_ids = signature_ids(self.json_object, self.server_name) + @staticmethod + def from_json_object( + server_name: str, + json_object: JsonDict, + minimum_valid_until_ms: int, + request_name: str, + ): + """Create a VerifyJsonRequest to verify all signatures on a signed JSON + object for the given server. + """ + key_ids = signature_ids(json_object, server_name) + return VerifyJsonRequest( + server_name, + lambda: json_object, + minimum_valid_until_ms, + request_name=request_name, + key_ids=key_ids, + ) + + @staticmethod + def from_event( + server_name: str, + event: EventBase, + minimum_valid_until_ms: int, + ): + """Create a VerifyJsonRequest to verify all signatures on an event + object for the given server. + """ + key_ids = list(event.signatures.get(server_name, [])) + return VerifyJsonRequest( + server_name, + # We defer creating the redacted json object, as it uses a lot more + # memory than the Event object itself. + lambda: prune_event_dict(event.room_version, event.get_pdu_json()), + minimum_valid_until_ms, + request_name=event.event_id, + key_ids=key_ids, + ) class KeyLookupError(ValueError): @@ -147,8 +189,13 @@ def verify_json_for_server( Deferred[None]: completes if the the object was correctly signed, otherwise errbacks with an error """ - req = VerifyJsonRequest(server_name, json_object, validity_time, request_name) - requests = (req,) + request = VerifyJsonRequest.from_json_object( + server_name, + json_object, + validity_time, + request_name, + ) + requests = (request,) return make_deferred_yieldable(self._verify_objects(requests)[0]) def verify_json_objects_for_server( @@ -175,10 +222,41 @@ def verify_json_objects_for_server( logcontext. """ return self._verify_objects( - VerifyJsonRequest(server_name, json_object, validity_time, request_name) + VerifyJsonRequest.from_json_object( + server_name, json_object, validity_time, request_name + ) for server_name, json_object, validity_time, request_name in server_and_json ) + def verify_events_for_server( + self, server_and_events: Iterable[Tuple[str, EventBase, int]] + ) -> List[defer.Deferred]: + """Bulk verification of signatures on events. + + Args: + server_and_events: + Iterable of `(server_name, event, validity_time)` tuples. + + `server_name` is which server we are verifying the signature for + on the event. + + `event` is the event that we'll verify the signatures of for + the given `server_name`. + + `validity_time` is a timestamp at which the signing key must be + valid. + + Returns: + List: for each input triplet, a deferred indicating success + or failure to verify each event's signature for the given + server_name. The deferreds run their callbacks in the sentinel + logcontext. + """ + return self._verify_objects( + VerifyJsonRequest.from_event(server_name, event, validity_time) + for server_name, event, validity_time in server_and_events + ) + def _verify_objects( self, verify_requests: Iterable[VerifyJsonRequest] ) -> List[defer.Deferred]: @@ -892,7 +970,7 @@ async def _handle_key_deferred(verify_request: VerifyJsonRequest) -> None: with PreserveLoggingContext(): _, key_id, verify_key = await verify_request.key_ready - json_object = verify_request.json_object + json_object = verify_request.get_json_object() try: verify_signed_json(json_object, server_name, verify_key) diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 949dcd4614..3fe496dcd3 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -137,11 +137,7 @@ def errback(failure: Failure, pdu: EventBase): return deferreds -class PduToCheckSig( - namedtuple( - "PduToCheckSig", ["pdu", "redacted_pdu_json", "sender_domain", "deferreds"] - ) -): +class PduToCheckSig(namedtuple("PduToCheckSig", ["pdu", "sender_domain", "deferreds"])): pass @@ -184,7 +180,6 @@ def _check_sigs_on_pdus( pdus_to_check = [ PduToCheckSig( pdu=p, - redacted_pdu_json=prune_event(p).get_pdu_json(), sender_domain=get_domain_from_id(p.sender), deferreds=[], ) @@ -195,13 +190,12 @@ def _check_sigs_on_pdus( # (except if its a 3pid invite, in which case it may be sent by any server) pdus_to_check_sender = [p for p in pdus_to_check if not _is_invite_via_3pid(p.pdu)] - more_deferreds = keyring.verify_json_objects_for_server( + more_deferreds = keyring.verify_events_for_server( [ ( p.sender_domain, - p.redacted_pdu_json, + p.pdu, p.pdu.origin_server_ts if room_version.enforce_key_validity else 0, - p.pdu.event_id, ) for p in pdus_to_check_sender ] @@ -230,13 +224,12 @@ def sender_err(e, pdu_to_check): if p.sender_domain != get_domain_from_id(p.pdu.event_id) ] - more_deferreds = keyring.verify_json_objects_for_server( + more_deferreds = keyring.verify_events_for_server( [ ( get_domain_from_id(p.pdu.event_id), - p.redacted_pdu_json, + p.pdu, p.pdu.origin_server_ts if room_version.enforce_key_validity else 0, - p.pdu.event_id, ) for p in pdus_to_check_event_id ] From 7958eadcd16087e6aaf7cce240c1f82856e0bcc7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 21 May 2021 11:20:51 +0100 Subject: [PATCH 177/619] Add a batching queue implementation. (#10017) --- changelog.d/10017.misc | 1 + synapse/util/batching_queue.py | 153 +++++++++++++++++++++++++++ tests/util/test_batching_queue.py | 169 ++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 changelog.d/10017.misc create mode 100644 synapse/util/batching_queue.py create mode 100644 tests/util/test_batching_queue.py diff --git a/changelog.d/10017.misc b/changelog.d/10017.misc new file mode 100644 index 0000000000..4777b7fb57 --- /dev/null +++ b/changelog.d/10017.misc @@ -0,0 +1 @@ +Add a batching queue implementation. diff --git a/synapse/util/batching_queue.py b/synapse/util/batching_queue.py new file mode 100644 index 0000000000..44bbb7b1a8 --- /dev/null +++ b/synapse/util/batching_queue.py @@ -0,0 +1,153 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import ( + Awaitable, + Callable, + Dict, + Generic, + Hashable, + List, + Set, + Tuple, + TypeVar, +) + +from twisted.internet import defer + +from synapse.logging.context import PreserveLoggingContext, make_deferred_yieldable +from synapse.metrics import LaterGauge +from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.util import Clock + +logger = logging.getLogger(__name__) + + +V = TypeVar("V") +R = TypeVar("R") + + +class BatchingQueue(Generic[V, R]): + """A queue that batches up work, calling the provided processing function + with all pending work (for a given key). + + The provided processing function will only be called once at a time for each + key. It will be called the next reactor tick after `add_to_queue` has been + called, and will keep being called until the queue has been drained (for the + given key). + + Note that the return value of `add_to_queue` will be the return value of the + processing function that processed the given item. This means that the + returned value will likely include data for other items that were in the + batch. + """ + + def __init__( + self, + name: str, + clock: Clock, + process_batch_callback: Callable[[List[V]], Awaitable[R]], + ): + self._name = name + self._clock = clock + + # The set of keys currently being processed. + self._processing_keys = set() # type: Set[Hashable] + + # The currently pending batch of values by key, with a Deferred to call + # with the result of the corresponding `_process_batch_callback` call. + self._next_values = {} # type: Dict[Hashable, List[Tuple[V, defer.Deferred]]] + + # The function to call with batches of values. + self._process_batch_callback = process_batch_callback + + LaterGauge( + "synapse_util_batching_queue_number_queued", + "The number of items waiting in the queue across all keys", + labels=("name",), + caller=lambda: sum(len(v) for v in self._next_values.values()), + ) + + LaterGauge( + "synapse_util_batching_queue_number_of_keys", + "The number of distinct keys that have items queued", + labels=("name",), + caller=lambda: len(self._next_values), + ) + + async def add_to_queue(self, value: V, key: Hashable = ()) -> R: + """Adds the value to the queue with the given key, returning the result + of the processing function for the batch that included the given value. + + The optional `key` argument allows sharding the queue by some key. The + queues will then be processed in parallel, i.e. the process batch + function will be called in parallel with batched values from a single + key. + """ + + # First we create a defer and add it and the value to the list of + # pending items. + d = defer.Deferred() + self._next_values.setdefault(key, []).append((value, d)) + + # If we're not currently processing the key fire off a background + # process to start processing. + if key not in self._processing_keys: + run_as_background_process(self._name, self._process_queue, key) + + return await make_deferred_yieldable(d) + + async def _process_queue(self, key: Hashable) -> None: + """A background task to repeatedly pull things off the queue for the + given key and call the `self._process_batch_callback` with the values. + """ + + try: + if key in self._processing_keys: + return + + self._processing_keys.add(key) + + while True: + # We purposefully wait a reactor tick to allow us to batch + # together requests that we're about to receive. A common + # pattern is to call `add_to_queue` multiple times at once, and + # deferring to the next reactor tick allows us to batch all of + # those up. + await self._clock.sleep(0) + + next_values = self._next_values.pop(key, []) + if not next_values: + # We've exhausted the queue. + break + + try: + values = [value for value, _ in next_values] + results = await self._process_batch_callback(values) + + for _, deferred in next_values: + with PreserveLoggingContext(): + deferred.callback(results) + + except Exception as e: + for _, deferred in next_values: + if deferred.called: + continue + + with PreserveLoggingContext(): + deferred.errback(e) + + finally: + self._processing_keys.discard(key) diff --git a/tests/util/test_batching_queue.py b/tests/util/test_batching_queue.py new file mode 100644 index 0000000000..5def1e56c9 --- /dev/null +++ b/tests/util/test_batching_queue.py @@ -0,0 +1,169 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from twisted.internet import defer + +from synapse.logging.context import make_deferred_yieldable +from synapse.util.batching_queue import BatchingQueue + +from tests.server import get_clock +from tests.unittest import TestCase + + +class BatchingQueueTestCase(TestCase): + def setUp(self): + self.clock, hs_clock = get_clock() + + self._pending_calls = [] + self.queue = BatchingQueue("test_queue", hs_clock, self._process_queue) + + async def _process_queue(self, values): + d = defer.Deferred() + self._pending_calls.append((values, d)) + return await make_deferred_yieldable(d) + + def test_simple(self): + """Tests the basic case of calling `add_to_queue` once and having + `_process_queue` return. + """ + + self.assertFalse(self._pending_calls) + + queue_d = defer.ensureDeferred(self.queue.add_to_queue("foo")) + + # The queue should wait a reactor tick before calling the processing + # function. + self.assertFalse(self._pending_calls) + self.assertFalse(queue_d.called) + + # We should see a call to `_process_queue` after a reactor tick. + self.clock.pump([0]) + + self.assertEqual(len(self._pending_calls), 1) + self.assertEqual(self._pending_calls[0][0], ["foo"]) + self.assertFalse(queue_d.called) + + # Return value of the `_process_queue` should be propagated back. + self._pending_calls.pop()[1].callback("bar") + + self.assertEqual(self.successResultOf(queue_d), "bar") + + def test_batching(self): + """Test that multiple calls at the same time get batched up into one + call to `_process_queue`. + """ + + self.assertFalse(self._pending_calls) + + queue_d1 = defer.ensureDeferred(self.queue.add_to_queue("foo1")) + queue_d2 = defer.ensureDeferred(self.queue.add_to_queue("foo2")) + + self.clock.pump([0]) + + # We should see only *one* call to `_process_queue` + self.assertEqual(len(self._pending_calls), 1) + self.assertEqual(self._pending_calls[0][0], ["foo1", "foo2"]) + self.assertFalse(queue_d1.called) + self.assertFalse(queue_d2.called) + + # Return value of the `_process_queue` should be propagated back to both. + self._pending_calls.pop()[1].callback("bar") + + self.assertEqual(self.successResultOf(queue_d1), "bar") + self.assertEqual(self.successResultOf(queue_d2), "bar") + + def test_queuing(self): + """Test that we queue up requests while a `_process_queue` is being + called. + """ + + self.assertFalse(self._pending_calls) + + queue_d1 = defer.ensureDeferred(self.queue.add_to_queue("foo1")) + self.clock.pump([0]) + + queue_d2 = defer.ensureDeferred(self.queue.add_to_queue("foo2")) + + # We should see only *one* call to `_process_queue` + self.assertEqual(len(self._pending_calls), 1) + self.assertEqual(self._pending_calls[0][0], ["foo1"]) + self.assertFalse(queue_d1.called) + self.assertFalse(queue_d2.called) + + # Return value of the `_process_queue` should be propagated back to the + # first. + self._pending_calls.pop()[1].callback("bar1") + + self.assertEqual(self.successResultOf(queue_d1), "bar1") + self.assertFalse(queue_d2.called) + + # We should now see a second call to `_process_queue` + self.clock.pump([0]) + self.assertEqual(len(self._pending_calls), 1) + self.assertEqual(self._pending_calls[0][0], ["foo2"]) + self.assertFalse(queue_d2.called) + + # Return value of the `_process_queue` should be propagated back to the + # second. + self._pending_calls.pop()[1].callback("bar2") + + self.assertEqual(self.successResultOf(queue_d2), "bar2") + + def test_different_keys(self): + """Test that calls to different keys get processed in parallel.""" + + self.assertFalse(self._pending_calls) + + queue_d1 = defer.ensureDeferred(self.queue.add_to_queue("foo1", key=1)) + self.clock.pump([0]) + queue_d2 = defer.ensureDeferred(self.queue.add_to_queue("foo2", key=2)) + self.clock.pump([0]) + + # We queue up another item with key=2 to check that we will keep taking + # things off the queue. + queue_d3 = defer.ensureDeferred(self.queue.add_to_queue("foo3", key=2)) + + # We should see two calls to `_process_queue` + self.assertEqual(len(self._pending_calls), 2) + self.assertEqual(self._pending_calls[0][0], ["foo1"]) + self.assertEqual(self._pending_calls[1][0], ["foo2"]) + self.assertFalse(queue_d1.called) + self.assertFalse(queue_d2.called) + self.assertFalse(queue_d3.called) + + # Return value of the `_process_queue` should be propagated back to the + # first. + self._pending_calls.pop(0)[1].callback("bar1") + + self.assertEqual(self.successResultOf(queue_d1), "bar1") + self.assertFalse(queue_d2.called) + self.assertFalse(queue_d3.called) + + # Return value of the `_process_queue` should be propagated back to the + # second. + self._pending_calls.pop()[1].callback("bar2") + + self.assertEqual(self.successResultOf(queue_d2), "bar2") + self.assertFalse(queue_d3.called) + + # We should now see a call `_pending_calls` for `foo3` + self.clock.pump([0]) + self.assertEqual(len(self._pending_calls), 1) + self.assertEqual(self._pending_calls[0][0], ["foo3"]) + self.assertFalse(queue_d3.called) + + # Return value of the `_process_queue` should be propagated back to the + # third deferred. + self._pending_calls.pop()[1].callback("bar4") + + self.assertEqual(self.successResultOf(queue_d3), "bar4") From 6a8643ff3da905568e3f2ec047182753352e39d1 Mon Sep 17 00:00:00 2001 From: Marek Matys <57749215+thermaq@users.noreply.github.com> Date: Fri, 21 May 2021 13:02:06 +0200 Subject: [PATCH 178/619] Fixed removal of new presence stream states (#10014) Fixes: https://github.com/matrix-org/synapse/issues/9962 This is a fix for above problem. I fixed it by swaping the order of insertion of new records and deletion of old ones. This ensures that we don't delete fresh database records as we do deletes before inserts. Signed-off-by: Marek Matys --- changelog.d/10014.bugfix | 1 + synapse/storage/databases/main/presence.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10014.bugfix diff --git a/changelog.d/10014.bugfix b/changelog.d/10014.bugfix new file mode 100644 index 0000000000..7cf3603f94 --- /dev/null +++ b/changelog.d/10014.bugfix @@ -0,0 +1 @@ +Fixed deletion of new presence stream states from database. diff --git a/synapse/storage/databases/main/presence.py b/synapse/storage/databases/main/presence.py index 669a2af884..6a2baa7841 100644 --- a/synapse/storage/databases/main/presence.py +++ b/synapse/storage/databases/main/presence.py @@ -97,6 +97,15 @@ def _update_presence_txn(self, txn, stream_orderings, presence_states): ) txn.call_after(self._get_presence_for_user.invalidate, (state.user_id,)) + # Delete old rows to stop database from getting really big + sql = "DELETE FROM presence_stream WHERE stream_id < ? AND " + + for states in batch_iter(presence_states, 50): + clause, args = make_in_list_sql_clause( + self.database_engine, "user_id", [s.user_id for s in states] + ) + txn.execute(sql + clause, [stream_id] + list(args)) + # Actually insert new rows self.db_pool.simple_insert_many_txn( txn, @@ -117,15 +126,6 @@ def _update_presence_txn(self, txn, stream_orderings, presence_states): ], ) - # Delete old rows to stop database from getting really big - sql = "DELETE FROM presence_stream WHERE stream_id < ? AND " - - for states in batch_iter(presence_states, 50): - clause, args = make_in_list_sql_clause( - self.database_engine, "user_id", [s.user_id for s in states] - ) - txn.execute(sql + clause, [stream_id] + list(args)) - async def get_all_presence_updates( self, instance_name: str, last_id: int, current_id: int, limit: int ) -> Tuple[List[Tuple[int, list]], int, bool]: From c5413d0e9ef6afa9cb5140774acfb4954fe0bf37 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 21 May 2021 12:02:01 -0400 Subject: [PATCH 179/619] Remove unused properties from the SpaceSummaryHandler. (#10038) --- changelog.d/10038.feature | 1 + synapse/handlers/space_summary.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog.d/10038.feature diff --git a/changelog.d/10038.feature b/changelog.d/10038.feature new file mode 100644 index 0000000000..2c655350c0 --- /dev/null +++ b/changelog.d/10038.feature @@ -0,0 +1 @@ +Experimental support to allow a user who could join a restricted room to view it in the spaces summary. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 8d49ba8164..abd9ddecca 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -50,8 +50,6 @@ class SpaceSummaryHandler: def __init__(self, hs: "HomeServer"): self._clock = hs.get_clock() self._auth = hs.get_auth() - self._room_list_handler = hs.get_room_list_handler() - self._state_handler = hs.get_state_handler() self._event_auth_handler = hs.get_event_auth_handler() self._store = hs.get_datastore() self._event_serializer = hs.get_event_client_serializer() From 21bd230831022a69ce90f334e869edd151155067 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 21 May 2021 17:29:14 +0100 Subject: [PATCH 180/619] Add a test for update_presence (#10033) https://github.com/matrix-org/synapse/issues/9962 uncovered that we accidentally removed all but one of the presence updates that we store in the database when persisting multiple updates. This could cause users' presence state to be stale. The bug was fixed in #10014, and this PR just adds a test that failed on the old code, and was used to initially verify the bug. The test attempts to insert some presence into the database in a batch using `PresenceStore.update_presence`, and then simply pulls it out again. --- changelog.d/10033.bugfix | 1 + tests/handlers/test_presence.py | 47 ++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10033.bugfix diff --git a/changelog.d/10033.bugfix b/changelog.d/10033.bugfix new file mode 100644 index 0000000000..587d839b8c --- /dev/null +++ b/changelog.d/10033.bugfix @@ -0,0 +1 @@ +Fixed deletion of new presence stream states from database. \ No newline at end of file diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 1ffab709fc..d90a9fec91 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -32,13 +32,19 @@ handle_timeout, handle_update, ) +from synapse.rest import admin from synapse.rest.client.v1 import room from synapse.types import UserID, get_domain_from_id from tests import unittest -class PresenceUpdateTestCase(unittest.TestCase): +class PresenceUpdateTestCase(unittest.HomeserverTestCase): + servlets = [admin.register_servlets] + + def prepare(self, reactor, clock, homeserver): + self.store = homeserver.get_datastore() + def test_offline_to_online(self): wheel_timer = Mock() user_id = "@foo:bar" @@ -292,6 +298,45 @@ def test_online_to_idle(self): any_order=True, ) + def test_persisting_presence_updates(self): + """Tests that the latest presence state for each user is persisted correctly""" + # Create some test users and presence states for them + presence_states = [] + for i in range(5): + user_id = self.register_user(f"user_{i}", "password") + + presence_state = UserPresenceState( + user_id=user_id, + state="online", + last_active_ts=1, + last_federation_update_ts=1, + last_user_sync_ts=1, + status_msg="I'm online!", + currently_active=True, + ) + presence_states.append(presence_state) + + # Persist these presence updates to the database + self.get_success(self.store.update_presence(presence_states)) + + # Check that each update is present in the database + db_presence_states = self.get_success( + self.store.get_all_presence_updates( + instance_name="master", + last_id=0, + current_id=len(presence_states) + 1, + limit=len(presence_states), + ) + ) + + # Extract presence update user ID and state information into lists of tuples + db_presence_states = [(ps[0], ps[1]) for _, ps in db_presence_states[0]] + presence_states = [(ps.user_id, ps.state) for ps in presence_states] + + # Compare what we put into the storage with what we got out. + # They should be identical. + self.assertEqual(presence_states, db_presence_states) + class PresenceTimeoutTestCase(unittest.TestCase): def test_idle_timer(self): From e8ac9ac8ca18fe3456bfeba7a5883be1c991b2a6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 May 2021 17:31:59 +0100 Subject: [PATCH 181/619] Fix /upload 500'ing when presented a very large image (#10029) * Fix /upload 500'ing when presented a very large image Catch DecompressionBombError and re-raise as ThumbnailErrors * Set PIL's MAX_IMAGE_PIXELS to match homeserver.yaml to get it to bomb out quicker, to load less into memory in the case of super large images * Add changelog entry for 10029 --- changelog.d/10029.bugfix | 1 + synapse/rest/media/v1/media_repository.py | 2 ++ synapse/rest/media/v1/thumbnailer.py | 9 +++++++++ 3 files changed, 12 insertions(+) create mode 100644 changelog.d/10029.bugfix diff --git a/changelog.d/10029.bugfix b/changelog.d/10029.bugfix new file mode 100644 index 0000000000..c214cbdaec --- /dev/null +++ b/changelog.d/10029.bugfix @@ -0,0 +1 @@ +Fixed a bug with very high resolution image uploads throwing internal server errors. \ No newline at end of file diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index e8a875b900..21c43c340c 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -76,6 +76,8 @@ def __init__(self, hs: "HomeServer"): self.max_upload_size = hs.config.max_upload_size self.max_image_pixels = hs.config.max_image_pixels + Thumbnailer.set_limits(self.max_image_pixels) + self.primary_base_path = hs.config.media_store_path # type: str self.filepaths = MediaFilePaths(self.primary_base_path) # type: MediaFilePaths diff --git a/synapse/rest/media/v1/thumbnailer.py b/synapse/rest/media/v1/thumbnailer.py index 37fe582390..a65e9e1802 100644 --- a/synapse/rest/media/v1/thumbnailer.py +++ b/synapse/rest/media/v1/thumbnailer.py @@ -40,6 +40,10 @@ class Thumbnailer: FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG"} + @staticmethod + def set_limits(max_image_pixels: int): + Image.MAX_IMAGE_PIXELS = max_image_pixels + def __init__(self, input_path: str): try: self.image = Image.open(input_path) @@ -47,6 +51,11 @@ def __init__(self, input_path: str): # If an error occurs opening the image, a thumbnail won't be able to # be generated. raise ThumbnailError from e + except Image.DecompressionBombError as e: + # If an image decompression bomb error occurs opening the image, + # then the image exceeds the pixel limit and a thumbnail won't + # be able to be generated. + raise ThumbnailError from e self.width, self.height = self.image.size self.transpose_method = None From 3e831f24ffc887e174f67ff7b1cfe3a429b7b5c1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 21 May 2021 17:57:08 +0100 Subject: [PATCH 182/619] Don't hammer the database for destination retry timings every ~5mins (#10036) --- changelog.d/10036.misc | 1 + synapse/app/generic_worker.py | 2 - synapse/federation/transport/server.py | 2 +- .../replication/slave/storage/transactions.py | 21 ------ synapse/storage/databases/main/__init__.py | 4 +- .../storage/databases/main/transactions.py | 66 +++++++++++-------- synapse/util/retryutils.py | 8 +-- tests/handlers/test_typing.py | 8 +-- tests/storage/test_transactions.py | 8 ++- tests/util/test_retryutils.py | 18 +++-- 10 files changed, 62 insertions(+), 76 deletions(-) create mode 100644 changelog.d/10036.misc delete mode 100644 synapse/replication/slave/storage/transactions.py diff --git a/changelog.d/10036.misc b/changelog.d/10036.misc new file mode 100644 index 0000000000..d2cf1e5473 --- /dev/null +++ b/changelog.d/10036.misc @@ -0,0 +1 @@ +Properly invalidate caches for destination retry timings every (instead of expiring entries every 5 minutes). diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index f730cdbd78..91ad326f19 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -61,7 +61,6 @@ from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.slave.storage.room import RoomStore -from synapse.replication.slave.storage.transactions import SlavedTransactionStore from synapse.rest.admin import register_servlets_for_media_repo from synapse.rest.client.v1 import events, login, presence, room from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet @@ -237,7 +236,6 @@ class GenericWorkerSlavedStore( DirectoryStore, SlavedApplicationServiceStore, SlavedRegistrationStore, - SlavedTransactionStore, SlavedProfileStore, SlavedClientIpStore, SlavedFilteringStore, diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index c17a085a4f..9d50b05d01 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -160,7 +160,7 @@ async def authenticate_request(self, request, content): # If we get a valid signed request from the other side, its probably # alive retry_timings = await self.store.get_destination_retry_timings(origin) - if retry_timings and retry_timings["retry_last_ts"]: + if retry_timings and retry_timings.retry_last_ts: run_in_background(self._reset_retry_timings, origin) return origin diff --git a/synapse/replication/slave/storage/transactions.py b/synapse/replication/slave/storage/transactions.py deleted file mode 100644 index a59e543924..0000000000 --- a/synapse/replication/slave/storage/transactions.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2015, 2016 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. - -from synapse.storage.databases.main.transactions import TransactionStore - -from ._base import BaseSlavedStore - - -class SlavedTransactionStore(TransactionStore, BaseSlavedStore): - pass diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index 49c7606d51..9cce62ae6c 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -67,7 +67,7 @@ from .stats import StatsStore from .stream import StreamStore from .tags import TagsStore -from .transactions import TransactionStore +from .transactions import TransactionWorkerStore from .ui_auth import UIAuthStore from .user_directory import UserDirectoryStore from .user_erasure_store import UserErasureStore @@ -83,7 +83,7 @@ class DataStore( StreamStore, ProfileStore, PresenceStore, - TransactionStore, + TransactionWorkerStore, DirectoryStore, KeyStore, StateStore, diff --git a/synapse/storage/databases/main/transactions.py b/synapse/storage/databases/main/transactions.py index 82335e7a9d..d211c423b2 100644 --- a/synapse/storage/databases/main/transactions.py +++ b/synapse/storage/databases/main/transactions.py @@ -16,13 +16,15 @@ from collections import namedtuple from typing import Iterable, List, Optional, Tuple +import attr from canonicaljson import encode_canonical_json from synapse.metrics.background_process_metrics import wrap_as_background_process -from synapse.storage._base import SQLBaseStore, db_to_json +from synapse.storage._base import db_to_json from synapse.storage.database import DatabasePool, LoggingTransaction +from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore from synapse.types import JsonDict -from synapse.util.caches.expiringcache import ExpiringCache +from synapse.util.caches.descriptors import cached db_binary_type = memoryview @@ -38,10 +40,23 @@ "_TransactionRow", ("response_code", "response_json") ) -SENTINEL = object() +@attr.s(slots=True, frozen=True, auto_attribs=True) +class DestinationRetryTimings: + """The current destination retry timing info for a remote server.""" -class TransactionWorkerStore(SQLBaseStore): + # The first time we tried and failed to reach the remote server, in ms. + failure_ts: int + + # The last time we tried and failed to reach the remote server, in ms. + retry_last_ts: int + + # How long since the last time we tried to reach the remote server before + # trying again, in ms. + retry_interval: int + + +class TransactionWorkerStore(CacheInvalidationWorkerStore): def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) @@ -60,19 +75,6 @@ def _cleanup_transactions_txn(txn): "_cleanup_transactions", _cleanup_transactions_txn ) - -class TransactionStore(TransactionWorkerStore): - """A collection of queries for handling PDUs.""" - - def __init__(self, database: DatabasePool, db_conn, hs): - super().__init__(database, db_conn, hs) - - self._destination_retry_cache = ExpiringCache( - cache_name="get_destination_retry_timings", - clock=self._clock, - expiry_ms=5 * 60 * 1000, - ) - async def get_received_txn_response( self, transaction_id: str, origin: str ) -> Optional[Tuple[int, JsonDict]]: @@ -145,7 +147,11 @@ async def set_received_txn_response( desc="set_received_txn_response", ) - async def get_destination_retry_timings(self, destination): + @cached(max_entries=10000) + async def get_destination_retry_timings( + self, + destination: str, + ) -> Optional[DestinationRetryTimings]: """Gets the current retry timings (if any) for a given destination. Args: @@ -156,34 +162,29 @@ async def get_destination_retry_timings(self, destination): Otherwise a dict for the retry scheme """ - result = self._destination_retry_cache.get(destination, SENTINEL) - if result is not SENTINEL: - return result - result = await self.db_pool.runInteraction( "get_destination_retry_timings", self._get_destination_retry_timings, destination, ) - # We don't hugely care about race conditions between getting and - # invalidating the cache, since we time out fairly quickly anyway. - self._destination_retry_cache[destination] = result return result - def _get_destination_retry_timings(self, txn, destination): + def _get_destination_retry_timings( + self, txn, destination: str + ) -> Optional[DestinationRetryTimings]: result = self.db_pool.simple_select_one_txn( txn, table="destinations", keyvalues={"destination": destination}, - retcols=("destination", "failure_ts", "retry_last_ts", "retry_interval"), + retcols=("failure_ts", "retry_last_ts", "retry_interval"), allow_none=True, ) # check we have a row and retry_last_ts is not null or zero # (retry_last_ts can't be negative) if result and result["retry_last_ts"]: - return result + return DestinationRetryTimings(**result) else: return None @@ -204,7 +205,6 @@ async def set_destination_retry_timings( retry_interval: how long until next retry in ms """ - self._destination_retry_cache.pop(destination, None) if self.database_engine.can_native_upsert: return await self.db_pool.runInteraction( "set_destination_retry_timings", @@ -252,6 +252,10 @@ def _set_destination_retry_timings_native( txn.execute(sql, (destination, failure_ts, retry_last_ts, retry_interval)) + self._invalidate_cache_and_stream( + txn, self.get_destination_retry_timings, (destination,) + ) + def _set_destination_retry_timings_emulated( self, txn, destination, failure_ts, retry_last_ts, retry_interval ): @@ -295,6 +299,10 @@ def _set_destination_retry_timings_emulated( }, ) + self._invalidate_cache_and_stream( + txn, self.get_destination_retry_timings, (destination,) + ) + async def store_destination_rooms_entries( self, destinations: Iterable[str], diff --git a/synapse/util/retryutils.py b/synapse/util/retryutils.py index f9c370a814..129b47cd49 100644 --- a/synapse/util/retryutils.py +++ b/synapse/util/retryutils.py @@ -82,11 +82,9 @@ async def get_retry_limiter(destination, clock, store, ignore_backoff=False, **k retry_timings = await store.get_destination_retry_timings(destination) if retry_timings: - failure_ts = retry_timings["failure_ts"] - retry_last_ts, retry_interval = ( - retry_timings["retry_last_ts"], - retry_timings["retry_interval"], - ) + failure_ts = retry_timings.failure_ts + retry_last_ts = retry_timings.retry_last_ts + retry_interval = retry_timings.retry_interval now = int(clock.time_msec()) diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 0c89487eaf..f58afbc244 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -89,14 +89,8 @@ def prepare(self, reactor, clock, hs): self.event_source = hs.get_event_sources().sources["typing"] self.datastore = hs.get_datastore() - retry_timings_res = { - "destination": "", - "retry_last_ts": 0, - "retry_interval": 0, - "failure_ts": None, - } self.datastore.get_destination_retry_timings = Mock( - return_value=defer.succeed(retry_timings_res) + return_value=defer.succeed(None) ) self.datastore.get_device_updates_by_remote = Mock( diff --git a/tests/storage/test_transactions.py b/tests/storage/test_transactions.py index b7f7eae8d0..bea9091d30 100644 --- a/tests/storage/test_transactions.py +++ b/tests/storage/test_transactions.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from synapse.storage.databases.main.transactions import DestinationRetryTimings from synapse.util.retryutils import MAX_RETRY_INTERVAL from tests.unittest import HomeserverTestCase @@ -36,8 +37,11 @@ def test_get_set_transactions(self): d = self.store.get_destination_retry_timings("example.com") r = self.get_success(d) - self.assert_dict( - {"retry_last_ts": 50, "retry_interval": 100, "failure_ts": 1000}, r + self.assertEqual( + DestinationRetryTimings( + retry_last_ts=50, retry_interval=100, failure_ts=1000 + ), + r, ) def test_initial_set_transactions(self): diff --git a/tests/util/test_retryutils.py b/tests/util/test_retryutils.py index 9b2be83a43..9e1bebdc83 100644 --- a/tests/util/test_retryutils.py +++ b/tests/util/test_retryutils.py @@ -51,10 +51,12 @@ def test_limiter(self): except AssertionError: pass + self.pump() + new_timings = self.get_success(store.get_destination_retry_timings("test_dest")) - self.assertEqual(new_timings["failure_ts"], failure_ts) - self.assertEqual(new_timings["retry_last_ts"], failure_ts) - self.assertEqual(new_timings["retry_interval"], MIN_RETRY_INTERVAL) + self.assertEqual(new_timings.failure_ts, failure_ts) + self.assertEqual(new_timings.retry_last_ts, failure_ts) + self.assertEqual(new_timings.retry_interval, MIN_RETRY_INTERVAL) # now if we try again we should get a failure self.get_failure( @@ -77,14 +79,16 @@ def test_limiter(self): except AssertionError: pass + self.pump() + new_timings = self.get_success(store.get_destination_retry_timings("test_dest")) - self.assertEqual(new_timings["failure_ts"], failure_ts) - self.assertEqual(new_timings["retry_last_ts"], retry_ts) + self.assertEqual(new_timings.failure_ts, failure_ts) + self.assertEqual(new_timings.retry_last_ts, retry_ts) self.assertGreaterEqual( - new_timings["retry_interval"], MIN_RETRY_INTERVAL * RETRY_MULTIPLIER * 0.5 + new_timings.retry_interval, MIN_RETRY_INTERVAL * RETRY_MULTIPLIER * 0.5 ) self.assertLessEqual( - new_timings["retry_interval"], MIN_RETRY_INTERVAL * RETRY_MULTIPLIER * 2.0 + new_timings.retry_interval, MIN_RETRY_INTERVAL * RETRY_MULTIPLIER * 2.0 ) # From 5f1198a67ebe182db14b5f98bb4c9a19d4889918 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 24 May 2021 04:43:33 -0500 Subject: [PATCH 183/619] Fix `get_state_ids_for_event` return type typo to match what the function actually does (#10050) It looks like a typo copy/paste from `get_state_for_event` above. --- changelog.d/10050.misc | 1 + synapse/storage/state.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10050.misc diff --git a/changelog.d/10050.misc b/changelog.d/10050.misc new file mode 100644 index 0000000000..2cac953cca --- /dev/null +++ b/changelog.d/10050.misc @@ -0,0 +1 @@ +Fix typo in `get_state_ids_for_event` docstring where the return type was incorrect. diff --git a/synapse/storage/state.py b/synapse/storage/state.py index cfafba22c5..c9dce726cb 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -540,7 +540,7 @@ async def get_state_ids_for_event( state_filter: The state filter used to fetch state from the database. Returns: - A dict from (type, state_key) -> state_event + A dict from (type, state_key) -> state_event_id """ state_map = await self.get_state_ids_for_events( [event_id], state_filter or StateFilter.all() From 387c297489b90bd80212ac1391666eecd01ff701 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 24 May 2021 13:37:30 +0200 Subject: [PATCH 184/619] Add missing entry to the table of contents of room admin API (#10043) --- changelog.d/10043.doc | 1 + docs/admin_api/rooms.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/10043.doc diff --git a/changelog.d/10043.doc b/changelog.d/10043.doc new file mode 100644 index 0000000000..a574ec0bf0 --- /dev/null +++ b/changelog.d/10043.doc @@ -0,0 +1 @@ +Add missing room state entry to the table of contents of room admin API. \ No newline at end of file diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 01d3882426..5721210fee 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -4,6 +4,7 @@ * [Usage](#usage) - [Room Details API](#room-details-api) - [Room Members API](#room-members-api) +- [Room State API](#room-state-api) - [Delete Room API](#delete-room-api) * [Parameters](#parameters-1) * [Response](#response) From 316f89e87f462608a5e63ba567ad549330f1456a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 24 May 2021 08:57:14 -0400 Subject: [PATCH 185/619] Enable experimental spaces by default. (#10011) The previous spaces_enabled flag now defaults to true and is exposed in the sample config. --- changelog.d/10011.feature | 1 + docs/sample_config.yaml | 15 +++++++++++++++ synapse/config/experimental.py | 19 ++++++++++++++++++- synapse/config/homeserver.py | 2 +- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10011.feature diff --git a/changelog.d/10011.feature b/changelog.d/10011.feature new file mode 100644 index 0000000000..409140fb13 --- /dev/null +++ b/changelog.d/10011.feature @@ -0,0 +1 @@ +Enable experimental support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946) (spaces summary API) and [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) (restricted join rules) by default. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 2952f2ba32..f0f9f06a6e 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -2943,3 +2943,18 @@ redis: # Optional password if configured on the Redis instance # #password: + + +# Enable experimental features in Synapse. +# +# Experimental features might break or be removed without a deprecation +# period. +# +experimental_features: + # Support for Spaces (MSC1772), it enables the following: + # + # * The Spaces Summary API (MSC2946). + # * Restricting room membership based on space membership (MSC3083). + # + # Uncomment to disable support for Spaces. + #spaces_enabled: false diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index a693fba877..cc67377f0f 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -29,9 +29,26 @@ def read_config(self, config: JsonDict, **kwargs): self.msc2858_enabled = experimental.get("msc2858_enabled", False) # type: bool # Spaces (MSC1772, MSC2946, MSC3083, etc) - self.spaces_enabled = experimental.get("spaces_enabled", False) # type: bool + self.spaces_enabled = experimental.get("spaces_enabled", True) # type: bool if self.spaces_enabled: KNOWN_ROOM_VERSIONS[RoomVersions.MSC3083.identifier] = RoomVersions.MSC3083 # MSC3026 (busy presence state) self.msc3026_enabled = experimental.get("msc3026_enabled", False) # type: bool + + def generate_config_section(self, **kwargs): + return """\ + # Enable experimental features in Synapse. + # + # Experimental features might break or be removed without a deprecation + # period. + # + experimental_features: + # Support for Spaces (MSC1772), it enables the following: + # + # * The Spaces Summary API (MSC2946). + # * Restricting room membership based on space membership (MSC3083). + # + # Uncomment to disable support for Spaces. + #spaces_enabled: false + """ diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index c23b66c88c..5ae0f55bcc 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -57,7 +57,6 @@ class HomeServerConfig(RootConfig): config_classes = [ ServerConfig, - ExperimentalConfig, TlsConfig, FederationConfig, CacheConfig, @@ -94,4 +93,5 @@ class HomeServerConfig(RootConfig): TracerConfig, WorkerConfig, RedisConfig, + ExperimentalConfig, ] From c0df6bae066fe818bb80d41af65503be7a07275d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 24 May 2021 14:02:01 +0100 Subject: [PATCH 186/619] Remove `keylen` from `LruCache`. (#9993) `keylen` seems to be a thing that is frequently incorrectly set, and we don't really need it. The only time it was used was to figure out if we had removed a subtree in `del_multi`, which we can do better by changing `TreeCache.pop` to return a different type (`TreeCacheNode`). Commits should be independently reviewable. --- changelog.d/9993.misc | 1 + .../replication/slave/storage/client_ips.py | 2 +- synapse/storage/databases/main/client_ips.py | 2 +- synapse/storage/databases/main/devices.py | 2 +- .../storage/databases/main/events_worker.py | 1 - synapse/util/caches/deferred_cache.py | 2 - synapse/util/caches/descriptors.py | 1 - synapse/util/caches/lrucache.py | 10 +- synapse/util/caches/treecache.py | 104 +++++++++++------- tests/util/test_lrucache.py | 4 +- tests/util/test_treecache.py | 6 +- 11 files changed, 80 insertions(+), 55 deletions(-) create mode 100644 changelog.d/9993.misc diff --git a/changelog.d/9993.misc b/changelog.d/9993.misc new file mode 100644 index 0000000000..0dd9244071 --- /dev/null +++ b/changelog.d/9993.misc @@ -0,0 +1 @@ +Remove `keylen` param on `LruCache`. diff --git a/synapse/replication/slave/storage/client_ips.py b/synapse/replication/slave/storage/client_ips.py index 8730966380..13ed87adc4 100644 --- a/synapse/replication/slave/storage/client_ips.py +++ b/synapse/replication/slave/storage/client_ips.py @@ -24,7 +24,7 @@ def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) self.client_ip_last_seen = LruCache( - cache_name="client_ip_last_seen", keylen=4, max_size=50000 + cache_name="client_ip_last_seen", max_size=50000 ) # type: LruCache[tuple, int] async def insert_client_ip(self, user_id, access_token, ip, user_agent, device_id): diff --git a/synapse/storage/databases/main/client_ips.py b/synapse/storage/databases/main/client_ips.py index d60010e942..074b077bef 100644 --- a/synapse/storage/databases/main/client_ips.py +++ b/synapse/storage/databases/main/client_ips.py @@ -436,7 +436,7 @@ class ClientIpStore(ClientIpWorkerStore): def __init__(self, database: DatabasePool, db_conn, hs): self.client_ip_last_seen = LruCache( - cache_name="client_ip_last_seen", keylen=4, max_size=50000 + cache_name="client_ip_last_seen", max_size=50000 ) super().__init__(database, db_conn, hs) diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index a1f98b7e38..fd87ba71ab 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -1053,7 +1053,7 @@ def __init__(self, database: DatabasePool, db_conn, hs): # Map of (user_id, device_id) -> bool. If there is an entry that implies # the device exists. self.device_id_exists_cache = LruCache( - cache_name="device_id_exists", keylen=2, max_size=10000 + cache_name="device_id_exists", max_size=10000 ) async def store_device( diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index 2c823e09cf..6963bbf7f4 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -157,7 +157,6 @@ def __init__(self, database: DatabasePool, db_conn, hs): self._get_event_cache = LruCache( cache_name="*getEvent*", - keylen=3, max_size=hs.config.caches.event_cache_size, ) diff --git a/synapse/util/caches/deferred_cache.py b/synapse/util/caches/deferred_cache.py index 484097a48a..371e7e4dd0 100644 --- a/synapse/util/caches/deferred_cache.py +++ b/synapse/util/caches/deferred_cache.py @@ -70,7 +70,6 @@ def __init__( self, name: str, max_entries: int = 1000, - keylen: int = 1, tree: bool = False, iterable: bool = False, apply_cache_factor_from_config: bool = True, @@ -101,7 +100,6 @@ def metrics_cb(): # a Deferred. self.cache = LruCache( max_size=max_entries, - keylen=keylen, cache_name=name, cache_type=cache_type, size_callback=(lambda d: len(d) or 1) if iterable else None, diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index 3a4d027095..2ac24a2f25 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -270,7 +270,6 @@ def __get__(self, obj, owner): cache = DeferredCache( name=self.orig.__name__, max_entries=self.max_entries, - keylen=self.num_args, tree=self.tree, iterable=self.iterable, ) # type: DeferredCache[CacheKey, Any] diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index 1be675e014..54df407ff7 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -34,7 +34,7 @@ from synapse.config import cache as cache_config from synapse.util import caches from synapse.util.caches import CacheMetric, register_cache -from synapse.util.caches.treecache import TreeCache +from synapse.util.caches.treecache import TreeCache, iterate_tree_cache_entry try: from pympler.asizeof import Asizer @@ -160,7 +160,6 @@ def __init__( self, max_size: int, cache_name: Optional[str] = None, - keylen: int = 1, cache_type: Type[Union[dict, TreeCache]] = dict, size_callback: Optional[Callable] = None, metrics_collection_callback: Optional[Callable[[], None]] = None, @@ -173,9 +172,6 @@ def __init__( cache_name: The name of this cache, for the prometheus metrics. If unset, no metrics will be reported on this cache. - keylen: The length of the tuple used as the cache key. Ignored unless - cache_type is `TreeCache`. - cache_type (type): type of underlying cache to be used. Typically one of dict or TreeCache. @@ -403,7 +399,9 @@ def cache_del_multi(key: KT) -> None: popped = cache.pop(key) if popped is None: return - for leaf in enumerate_leaves(popped, keylen - len(cast(tuple, key))): + # for each deleted node, we now need to remove it from the linked list + # and run its callbacks. + for leaf in iterate_tree_cache_entry(popped): delete_node(leaf) @synchronized diff --git a/synapse/util/caches/treecache.py b/synapse/util/caches/treecache.py index eb4d98f683..73502a8b06 100644 --- a/synapse/util/caches/treecache.py +++ b/synapse/util/caches/treecache.py @@ -1,18 +1,43 @@ -from typing import Dict +# Copyright 2016-2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. SENTINEL = object() +class TreeCacheNode(dict): + """The type of nodes in our tree. + + Has its own type so we can distinguish it from real dicts that are stored at the + leaves. + """ + + pass + + class TreeCache: """ Tree-based backing store for LruCache. Allows subtrees of data to be deleted efficiently. Keys must be tuples. + + The data structure is a chain of TreeCacheNodes: + root = {key_1: {key_2: _value}} """ def __init__(self): self.size = 0 - self.root = {} # type: Dict + self.root = TreeCacheNode() def __setitem__(self, key, value): return self.set(key, value) @@ -21,10 +46,23 @@ def __contains__(self, key): return self.get(key, SENTINEL) is not SENTINEL def set(self, key, value): + if isinstance(value, TreeCacheNode): + # this would mean we couldn't tell where our tree ended and the value + # started. + raise ValueError("Cannot store TreeCacheNodes in a TreeCache") + node = self.root for k in key[:-1]: - node = node.setdefault(k, {}) - node[key[-1]] = _Entry(value) + next_node = node.get(k, SENTINEL) + if next_node is SENTINEL: + next_node = node[k] = TreeCacheNode() + elif not isinstance(next_node, TreeCacheNode): + # this suggests that the caller is not being consistent with its key + # length. + raise ValueError("value conflicts with an existing subtree") + node = next_node + + node[key[-1]] = value self.size += 1 def get(self, key, default=None): @@ -33,25 +71,41 @@ def get(self, key, default=None): node = node.get(k, None) if node is None: return default - return node.get(key[-1], _Entry(default)).value + return node.get(key[-1], default) def clear(self): self.size = 0 - self.root = {} + self.root = TreeCacheNode() def pop(self, key, default=None): + """Remove the given key, or subkey, from the cache + + Args: + key: key or subkey to remove. + default: value to return if key is not found + + Returns: + If the key is not found, 'default'. If the key is complete, the removed + value. If the key is partial, the TreeCacheNode corresponding to the part + of the tree that was removed. + """ + # a list of the nodes we have touched on the way down the tree nodes = [] node = self.root for k in key[:-1]: node = node.get(k, None) - nodes.append(node) # don't add the root node if node is None: return default + if not isinstance(node, TreeCacheNode): + # we've gone off the end of the tree + raise ValueError("pop() key too long") + nodes.append(node) # don't add the root node popped = node.pop(key[-1], SENTINEL) if popped is SENTINEL: return default + # working back up the tree, clear out any nodes that are now empty node_and_keys = list(zip(nodes, key)) node_and_keys.reverse() node_and_keys.append((self.root, None)) @@ -61,14 +115,15 @@ def pop(self, key, default=None): if n: break + # found an empty node: remove it from its parent, and loop. node_and_keys[i + 1][0].pop(k) - popped, cnt = _strip_and_count_entires(popped) + cnt = sum(1 for _ in iterate_tree_cache_entry(popped)) self.size -= cnt return popped def values(self): - return list(iterate_tree_cache_entry(self.root)) + return iterate_tree_cache_entry(self.root) def __len__(self): return self.size @@ -78,36 +133,9 @@ def iterate_tree_cache_entry(d): """Helper function to iterate over the leaves of a tree, i.e. a dict of that can contain dicts. """ - if isinstance(d, dict): + if isinstance(d, TreeCacheNode): for value_d in d.values(): for value in iterate_tree_cache_entry(value_d): yield value else: - if isinstance(d, _Entry): - yield d.value - else: - yield d - - -class _Entry: - __slots__ = ["value"] - - def __init__(self, value): - self.value = value - - -def _strip_and_count_entires(d): - """Takes an _Entry or dict with leaves of _Entry's, and either returns the - value or a dictionary with _Entry's replaced by their values. - - Also returns the count of _Entry's - """ - if isinstance(d, dict): - cnt = 0 - for key, value in d.items(): - v, n = _strip_and_count_entires(value) - d[key] = v - cnt += n - return d, cnt - else: - return d.value, 1 + yield d diff --git a/tests/util/test_lrucache.py b/tests/util/test_lrucache.py index df3e27779f..377904e72e 100644 --- a/tests/util/test_lrucache.py +++ b/tests/util/test_lrucache.py @@ -59,7 +59,7 @@ def test_pop(self): self.assertEquals(cache.pop("key"), None) def test_del_multi(self): - cache = LruCache(4, keylen=2, cache_type=TreeCache) + cache = LruCache(4, cache_type=TreeCache) cache[("animal", "cat")] = "mew" cache[("animal", "dog")] = "woof" cache[("vehicles", "car")] = "vroom" @@ -165,7 +165,7 @@ def test_del_multi(self): m2 = Mock() m3 = Mock() m4 = Mock() - cache = LruCache(4, keylen=2, cache_type=TreeCache) + cache = LruCache(4, cache_type=TreeCache) cache.set(("a", "1"), "value", callbacks=[m1]) cache.set(("a", "2"), "value", callbacks=[m2]) diff --git a/tests/util/test_treecache.py b/tests/util/test_treecache.py index 3b077af27e..6066372053 100644 --- a/tests/util/test_treecache.py +++ b/tests/util/test_treecache.py @@ -13,7 +13,7 @@ # limitations under the License. -from synapse.util.caches.treecache import TreeCache +from synapse.util.caches.treecache import TreeCache, iterate_tree_cache_entry from .. import unittest @@ -64,12 +64,14 @@ def test_pop_mixedlevel(self): cache[("a", "b")] = "AB" cache[("b", "a")] = "BA" self.assertEquals(cache.get(("a", "a")), "AA") - cache.pop(("a",)) + popped = cache.pop(("a",)) self.assertEquals(cache.get(("a", "a")), None) self.assertEquals(cache.get(("a", "b")), None) self.assertEquals(cache.get(("b", "a")), "BA") self.assertEquals(len(cache), 1) + self.assertEquals({"AA", "AB"}, set(iterate_tree_cache_entry(popped))) + def test_clear(self): cache = TreeCache() cache[("a",)] = "A" From daca7b2794fb86514dc01de551bb0e5db77cf914 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 24 May 2021 14:03:00 +0100 Subject: [PATCH 187/619] Fix off-by-one-error in synapse_port_db (#9991) fixes #9979 --- .buildkite/postgres-config.yaml | 6 ++---- .buildkite/scripts/test_synapse_port_db.sh | 4 ++++ .buildkite/sqlite-config.yaml | 6 ++---- changelog.d/9991.bugfix | 1 + scripts/synapse_port_db | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 changelog.d/9991.bugfix diff --git a/.buildkite/postgres-config.yaml b/.buildkite/postgres-config.yaml index 2acbe66f4c..67e17fa9d1 100644 --- a/.buildkite/postgres-config.yaml +++ b/.buildkite/postgres-config.yaml @@ -3,7 +3,7 @@ # CI's Docker setup at the point where this file is considered. server_name: "localhost:8800" -signing_key_path: "/src/.buildkite/test.signing.key" +signing_key_path: ".buildkite/test.signing.key" report_stats: false @@ -16,6 +16,4 @@ database: database: synapse # Suppress the key server warning. -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true +trusted_key_servers: [] diff --git a/.buildkite/scripts/test_synapse_port_db.sh b/.buildkite/scripts/test_synapse_port_db.sh index a7e2454769..82d7d56d4e 100755 --- a/.buildkite/scripts/test_synapse_port_db.sh +++ b/.buildkite/scripts/test_synapse_port_db.sh @@ -33,6 +33,10 @@ scripts-dev/update_database --database-config .buildkite/sqlite-config.yaml echo "+++ Run synapse_port_db against test database" coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml +# We should be able to run twice against the same database. +echo "+++ Run synapse_port_db a second time" +coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml + ##### # Now do the same again, on an empty database. diff --git a/.buildkite/sqlite-config.yaml b/.buildkite/sqlite-config.yaml index 6d9bf80d84..d16459cfd9 100644 --- a/.buildkite/sqlite-config.yaml +++ b/.buildkite/sqlite-config.yaml @@ -3,7 +3,7 @@ # schema and run background updates on it. server_name: "localhost:8800" -signing_key_path: "/src/.buildkite/test.signing.key" +signing_key_path: ".buildkite/test.signing.key" report_stats: false @@ -13,6 +13,4 @@ database: database: ".buildkite/test_db.db" # Suppress the key server warning. -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true +trusted_key_servers: [] diff --git a/changelog.d/9991.bugfix b/changelog.d/9991.bugfix new file mode 100644 index 0000000000..665ff04dea --- /dev/null +++ b/changelog.d/9991.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.26.0 which meant that `synapse_port_db` would not correctly initialise some postgres sequences, requiring manual updates afterwards. diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 7c7645c05a..86eb76cbca 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -959,7 +959,7 @@ class Porter(object): def r(txn): txn.execute( "ALTER SEQUENCE event_auth_chain_id RESTART WITH %s", - (curr_chain_id,), + (curr_chain_id + 1,), ) if curr_chain_id is not None: From 82eacb0e071657e796952638fe8da90cdb94f2a1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 24 May 2021 14:03:30 +0100 Subject: [PATCH 188/619] Fix --no-daemonize for synctl with workers (#9995) --- changelog.d/9995.bugfix | 1 + synctl | 102 +++++++++++++--------------------------- 2 files changed, 33 insertions(+), 70 deletions(-) create mode 100644 changelog.d/9995.bugfix diff --git a/changelog.d/9995.bugfix b/changelog.d/9995.bugfix new file mode 100644 index 0000000000..3b63e7c42a --- /dev/null +++ b/changelog.d/9995.bugfix @@ -0,0 +1 @@ +Fix `synctl`'s `--no-daemonize` parameter to work correctly with worker processes. diff --git a/synctl b/synctl index ccf404accb..6ce19918d2 100755 --- a/synctl +++ b/synctl @@ -24,12 +24,13 @@ import signal import subprocess import sys import time +from typing import Iterable import yaml from synapse.config import find_config_files -SYNAPSE = [sys.executable, "-m", "synapse.app.homeserver"] +MAIN_PROCESS = "synapse.app.homeserver" GREEN = "\x1b[1;32m" YELLOW = "\x1b[1;33m" @@ -68,71 +69,37 @@ def abort(message, colour=RED, stream=sys.stderr): sys.exit(1) -def start(configfile: str, daemonize: bool = True) -> bool: - """Attempts to start synapse. +def start(pidfile: str, app: str, config_files: Iterable[str], daemonize: bool) -> bool: + """Attempts to start a synapse main or worker process. Args: - configfile: path to a yaml synapse config file - daemonize: whether to daemonize synapse or keep it attached to the current - session + pidfile: the pidfile we expect the process to create + app: the python module to run + config_files: config files to pass to synapse + daemonize: if True, will include a --daemonize argument to synapse Returns: - True if the process started successfully + True if the process started successfully or was already running False if there was an error starting the process - - If deamonize is False it will only return once synapse exits. """ - write("Starting ...") - args = SYNAPSE - - if daemonize: - args.extend(["--daemonize", "-c", configfile]) - else: - args.extend(["-c", configfile]) - - try: - subprocess.check_call(args) - write("started synapse.app.homeserver(%r)" % (configfile,), colour=GREEN) + if os.path.exists(pidfile) and pid_running(int(open(pidfile).read())): + print(app + " already running") return True - except subprocess.CalledProcessError as e: - write( - "error starting (exit code: %d); see above for logs" % e.returncode, - colour=RED, - ) - return False - -def start_worker(app: str, configfile: str, worker_configfile: str) -> bool: - """Attempts to start a synapse worker. - Args: - app: name of the worker's appservice - configfile: path to a yaml synapse config file - worker_configfile: path to worker specific yaml synapse file - - Returns: - True if the process started successfully - False if there was an error starting the process - """ - - args = [ - sys.executable, - "-m", - app, - "-c", - configfile, - "-c", - worker_configfile, - "--daemonize", - ] + args = [sys.executable, "-m", app] + for c in config_files: + args += ["-c", c] + if daemonize: + args.append("--daemonize") try: subprocess.check_call(args) - write("started %s(%r)" % (app, worker_configfile), colour=GREEN) + write("started %s(%s)" % (app, ",".join(config_files)), colour=GREEN) return True except subprocess.CalledProcessError as e: write( - "error starting %s(%r) (exit code: %d); see above for logs" - % (app, worker_configfile, e.returncode), + "error starting %s(%s) (exit code: %d); see above for logs" + % (app, ",".join(config_files), e.returncode), colour=RED, ) return False @@ -224,10 +191,11 @@ def main(): if not os.path.exists(configfile): write( - "No config file found\n" - "To generate a config file, run '%s -c %s --generate-config" - " --server-name= --report-stats='\n" - % (" ".join(SYNAPSE), options.configfile), + f"Config file {configfile} does not exist.\n" + f"To generate a config file, run:\n" + f" {sys.executable} -m {MAIN_PROCESS}" + f" -c {configfile} --generate-config" + f" --server-name= --report-stats=\n", stream=sys.stderr, ) sys.exit(1) @@ -323,7 +291,7 @@ def main(): has_stopped = False if start_stop_synapse: - if not stop(pidfile, "synapse.app.homeserver"): + if not stop(pidfile, MAIN_PROCESS): has_stopped = False if not has_stopped and action == "stop": sys.exit(1) @@ -346,30 +314,24 @@ def main(): if action == "start" or action == "restart": error = False if start_stop_synapse: - # Check if synapse is already running - if os.path.exists(pidfile) and pid_running(int(open(pidfile).read())): - abort("synapse.app.homeserver already running") - - if not start(configfile, bool(options.daemonize)): + if not start(pidfile, MAIN_PROCESS, (configfile,), options.daemonize): error = True for worker in workers: env = os.environ.copy() - # Skip starting a worker if its already running - if os.path.exists(worker.pidfile) and pid_running( - int(open(worker.pidfile).read()) - ): - print(worker.app + " already running") - continue - if worker.cache_factor: os.environ["SYNAPSE_CACHE_FACTOR"] = str(worker.cache_factor) for cache_name, factor in worker.cache_factors.items(): os.environ["SYNAPSE_CACHE_FACTOR_" + cache_name.upper()] = str(factor) - if not start_worker(worker.app, configfile, worker.configfile): + if not start( + worker.pidfile, + worker.app, + (configfile, worker.configfile), + options.daemonize, + ): error = True # Reset env back to the original From 057ce7b75406dc97be8ff2c890c47fd9357b0773 Mon Sep 17 00:00:00 2001 From: Jerin J Titus <72017981+jerinjtitus@users.noreply.github.com> Date: Mon, 24 May 2021 22:13:30 +0530 Subject: [PATCH 189/619] Remove tls_fingerprints option (#9280) Signed-off-by: Jerin J Titus <72017981+jerinjtitus@users.noreply.github.com> --- changelog.d/9280.removal | 1 + docs/sample_config.yaml | 27 ------------ scripts-dev/convert_server_keys.py | 7 --- synapse/config/tls.py | 50 ---------------------- synapse/rest/key/v2/local_key_resource.py | 8 ---- synapse/rest/key/v2/remote_key_resource.py | 3 -- 6 files changed, 1 insertion(+), 95 deletions(-) create mode 100644 changelog.d/9280.removal diff --git a/changelog.d/9280.removal b/changelog.d/9280.removal new file mode 100644 index 0000000000..c2ed3d308d --- /dev/null +++ b/changelog.d/9280.removal @@ -0,0 +1 @@ +Removed support for the deprecated `tls_fingerprints` configuration setting. Contributed by Jerin J Titus. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index f0f9f06a6e..6576b153d0 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -683,33 +683,6 @@ acme: # account_key_file: DATADIR/acme_account.key -# List of allowed TLS fingerprints for this server to publish along -# with the signing keys for this server. Other matrix servers that -# make HTTPS requests to this server will check that the TLS -# certificates returned by this server match one of the fingerprints. -# -# Synapse automatically adds the fingerprint of its own certificate -# to the list. So if federation traffic is handled directly by synapse -# then no modification to the list is required. -# -# If synapse is run behind a load balancer that handles the TLS then it -# will be necessary to add the fingerprints of the certificates used by -# the loadbalancers to this list if they are different to the one -# synapse is using. -# -# Homeservers are permitted to cache the list of TLS fingerprints -# returned in the key responses up to the "valid_until_ts" returned in -# key. It may be necessary to publish the fingerprints of a new -# certificate and wait until the "valid_until_ts" of the previous key -# responses have passed before deploying it. -# -# You can calculate a fingerprint from a given TLS listener via: -# openssl s_client -connect $host:$port < /dev/null 2> /dev/null | -# openssl x509 -outform DER | openssl sha256 -binary | base64 | tr -d '=' -# or by checking matrix.org/federationtester/api/report?server_name=$host -# -#tls_fingerprints: [{"sha256": ""}] - ## Federation ## diff --git a/scripts-dev/convert_server_keys.py b/scripts-dev/convert_server_keys.py index 961dc59f11..d4314a054c 100644 --- a/scripts-dev/convert_server_keys.py +++ b/scripts-dev/convert_server_keys.py @@ -1,4 +1,3 @@ -import hashlib import json import sys import time @@ -54,15 +53,9 @@ def convert_v1_to_v2(server_name, valid_until, keys, certificate): "server_name": server_name, "verify_keys": {key_id: {"key": key} for key_id, key in keys.items()}, "valid_until_ts": valid_until, - "tls_fingerprints": [fingerprint(certificate)], } -def fingerprint(certificate): - finger = hashlib.sha256(certificate) - return {"sha256": encode_base64(finger.digest())} - - def rows_v2(server, json): valid_until = json["valid_until_ts"] key_json = encode_canonical_json(json) diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 7df4e4c3e6..26f1150ca5 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -16,11 +16,8 @@ import os import warnings from datetime import datetime -from hashlib import sha256 from typing import List, Optional, Pattern -from unpaddedbase64 import encode_base64 - from OpenSSL import SSL, crypto from twisted.internet._sslverify import Certificate, trustRootFromCertificates @@ -83,13 +80,6 @@ def read_config(self, config: dict, config_dir_path: str, **kwargs): "configured." ) - self._original_tls_fingerprints = config.get("tls_fingerprints", []) - - if self._original_tls_fingerprints is None: - self._original_tls_fingerprints = [] - - self.tls_fingerprints = list(self._original_tls_fingerprints) - # Whether to verify certificates on outbound federation traffic self.federation_verify_certificates = config.get( "federation_verify_certificates", True @@ -248,19 +238,6 @@ def read_certificate_from_disk(self, require_cert_and_key: bool): e, ) - self.tls_fingerprints = list(self._original_tls_fingerprints) - - if self.tls_certificate: - # Check that our own certificate is included in the list of fingerprints - # and include it if it is not. - x509_certificate_bytes = crypto.dump_certificate( - crypto.FILETYPE_ASN1, self.tls_certificate - ) - sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest()) - sha256_fingerprints = {f["sha256"] for f in self.tls_fingerprints} - if sha256_fingerprint not in sha256_fingerprints: - self.tls_fingerprints.append({"sha256": sha256_fingerprint}) - def generate_config_section( self, config_dir_path, @@ -443,33 +420,6 @@ def generate_config_section( # If unspecified, we will use CONFDIR/client.key. # account_key_file: %(default_acme_account_file)s - - # List of allowed TLS fingerprints for this server to publish along - # with the signing keys for this server. Other matrix servers that - # make HTTPS requests to this server will check that the TLS - # certificates returned by this server match one of the fingerprints. - # - # Synapse automatically adds the fingerprint of its own certificate - # to the list. So if federation traffic is handled directly by synapse - # then no modification to the list is required. - # - # If synapse is run behind a load balancer that handles the TLS then it - # will be necessary to add the fingerprints of the certificates used by - # the loadbalancers to this list if they are different to the one - # synapse is using. - # - # Homeservers are permitted to cache the list of TLS fingerprints - # returned in the key responses up to the "valid_until_ts" returned in - # key. It may be necessary to publish the fingerprints of a new - # certificate and wait until the "valid_until_ts" of the previous key - # responses have passed before deploying it. - # - # You can calculate a fingerprint from a given TLS listener via: - # openssl s_client -connect $host:$port < /dev/null 2> /dev/null | - # openssl x509 -outform DER | openssl sha256 -binary | base64 | tr -d '=' - # or by checking matrix.org/federationtester/api/report?server_name=$host - # - #tls_fingerprints: [{"sha256": ""}] """ # Lowercase the string representation of boolean values % { diff --git a/synapse/rest/key/v2/local_key_resource.py b/synapse/rest/key/v2/local_key_resource.py index e8dbe240d8..a5fcd15e3a 100644 --- a/synapse/rest/key/v2/local_key_resource.py +++ b/synapse/rest/key/v2/local_key_resource.py @@ -48,11 +48,6 @@ class LocalKey(Resource): "key": # base64 encoded NACL verification key. } }, - "tls_fingerprints": [ # Fingerprints of the TLS certs this server uses. - { - "sha256": # base64 encoded sha256 fingerprint of the X509 cert - }, - ], "signatures": { "this.server.example.com": { "algorithm:version": # NACL signature for this server @@ -89,14 +84,11 @@ def response_json_object(self): "expired_ts": key.expired_ts, } - tls_fingerprints = self.config.tls_fingerprints - json_object = { "valid_until_ts": self.valid_until_ts, "server_name": self.config.server_name, "verify_keys": verify_keys, "old_verify_keys": old_verify_keys, - "tls_fingerprints": tls_fingerprints, } for key in self.config.signing_key: json_object = sign_json(json_object, self.config.server_name, key) diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index f648678b09..aba1734a55 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -73,9 +73,6 @@ class RemoteKey(DirectServeJsonResource): "expired_ts": 0, # when the key stop being used. } } - "tls_fingerprints": [ - { "sha256": # fingerprint } - ] "signatures": { "remote.server.example.com": {...} "this.server.example.com": {...} From 22a8838f626834c4ffc09761f4c5d65215cfc885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Migu=C3=A9ns?= Date: Mon, 24 May 2021 21:23:54 +0200 Subject: [PATCH 190/619] Fix docker image to not log at `/homeserver.log` (#10045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #9970 Signed-off-by: Sergio Miguéns Iglesias lonyelon@lony.xyz --- changelog.d/10045.docker | 1 + docker/conf/log.config | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10045.docker diff --git a/changelog.d/10045.docker b/changelog.d/10045.docker new file mode 100644 index 0000000000..70b65b0a01 --- /dev/null +++ b/changelog.d/10045.docker @@ -0,0 +1 @@ +Fix bug introduced in Synapse 1.33.0 which caused a `Permission denied: '/homeserver.log'` error when starting Synapse with the generated log configuration. Contributed by Sergio Miguéns Iglesias. diff --git a/docker/conf/log.config b/docker/conf/log.config index 34572bc0f3..a994626926 100644 --- a/docker/conf/log.config +++ b/docker/conf/log.config @@ -9,10 +9,11 @@ formatters: {% endif %} handlers: +{% if LOG_FILE_PATH %} file: class: logging.handlers.TimedRotatingFileHandler formatter: precise - filename: {{ LOG_FILE_PATH or "homeserver.log" }} + filename: {{ LOG_FILE_PATH }} when: "midnight" backupCount: 6 # Does not include the current log file. encoding: utf8 @@ -29,6 +30,7 @@ handlers: # be written to disk. capacity: 10 flushLevel: 30 # Flush for WARNING logs as well +{% endif %} console: class: logging.StreamHandler From 7adcb20fc02d614b4a2b03b128b279f25633e2bd Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 24 May 2021 15:32:01 -0400 Subject: [PATCH 191/619] Add missing type hints to synapse.util (#9982) --- changelog.d/9982.misc | 1 + mypy.ini | 9 +++++++++ synapse/config/saml2.py | 8 +++++++- synapse/storage/databases/main/keys.py | 2 +- synapse/util/hash.py | 10 +++++----- synapse/util/iterutils.py | 11 ++++------- synapse/util/module_loader.py | 9 +++++---- synapse/util/msisdn.py | 10 +++++----- tests/util/test_itertools.py | 4 ++-- 9 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 changelog.d/9982.misc diff --git a/changelog.d/9982.misc b/changelog.d/9982.misc new file mode 100644 index 0000000000..f3821f61a3 --- /dev/null +++ b/changelog.d/9982.misc @@ -0,0 +1 @@ +Add missing type hints to `synapse.util` module. diff --git a/mypy.ini b/mypy.ini index 1d1d1ea0f2..062872020e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -71,8 +71,13 @@ files = synapse/types.py, synapse/util/async_helpers.py, synapse/util/caches, + synapse/util/daemonize.py, + synapse/util/hash.py, + synapse/util/iterutils.py, synapse/util/metrics.py, synapse/util/macaroons.py, + synapse/util/module_loader.py, + synapse/util/msisdn.py, synapse/util/stringutils.py, synapse/visibility.py, tests/replication, @@ -80,6 +85,7 @@ files = tests/handlers/test_password_providers.py, tests/rest/client/v1/test_login.py, tests/rest/client/v2_alpha/test_auth.py, + tests/util/test_itertools.py, tests/util/test_stream_change_cache.py [mypy-pymacaroons.*] @@ -175,5 +181,8 @@ ignore_missing_imports = True [mypy-pympler.*] ignore_missing_imports = True +[mypy-phonenumbers.*] +ignore_missing_imports = True + [mypy-ijson.*] ignore_missing_imports = True diff --git a/synapse/config/saml2.py b/synapse/config/saml2.py index 3d1218c8d1..05e983625d 100644 --- a/synapse/config/saml2.py +++ b/synapse/config/saml2.py @@ -164,7 +164,13 @@ def read_config(self, config, **kwargs): config_path = saml2_config.get("config_path", None) if config_path is not None: mod = load_python_module(config_path) - _dict_merge(merge_dict=mod.CONFIG, into_dict=saml2_config_dict) + config = getattr(mod, "CONFIG", None) + if config is None: + raise ConfigError( + "Config path specified by saml2_config.config_path does not " + "have a CONFIG property." + ) + _dict_merge(merge_dict=config, into_dict=saml2_config_dict) import saml2.config diff --git a/synapse/storage/databases/main/keys.py b/synapse/storage/databases/main/keys.py index 0e86807834..6990f3ed1d 100644 --- a/synapse/storage/databases/main/keys.py +++ b/synapse/storage/databases/main/keys.py @@ -55,7 +55,7 @@ async def get_server_verify_keys( """ keys = {} - def _get_keys(txn: Cursor, batch: Tuple[Tuple[str, str]]) -> None: + def _get_keys(txn: Cursor, batch: Tuple[Tuple[str, str], ...]) -> None: """Processes a batch of keys to fetch, and adds the result to `keys`.""" # batch_iter always returns tuples so it's safe to do len(batch) diff --git a/synapse/util/hash.py b/synapse/util/hash.py index ba676e1762..7625ca8c2c 100644 --- a/synapse/util/hash.py +++ b/synapse/util/hash.py @@ -17,15 +17,15 @@ import unpaddedbase64 -def sha256_and_url_safe_base64(input_text): +def sha256_and_url_safe_base64(input_text: str) -> str: """SHA256 hash an input string, encode the digest as url-safe base64, and return - :param input_text: string to hash - :type input_text: str + Args: + input_text: string to hash - :returns a sha256 hashed and url-safe base64 encoded digest - :rtype: str + returns: + A sha256 hashed and url-safe base64 encoded digest """ digest = hashlib.sha256(input_text.encode()).digest() return unpaddedbase64.encode_base64(digest, urlsafe=True) diff --git a/synapse/util/iterutils.py b/synapse/util/iterutils.py index abfdc29832..886afa9d19 100644 --- a/synapse/util/iterutils.py +++ b/synapse/util/iterutils.py @@ -30,12 +30,12 @@ T = TypeVar("T") -def batch_iter(iterable: Iterable[T], size: int) -> Iterator[Tuple[T]]: +def batch_iter(iterable: Iterable[T], size: int) -> Iterator[Tuple[T, ...]]: """batch an iterable up into tuples with a maximum size Args: - iterable (iterable): the iterable to slice - size (int): the maximum batch size + iterable: the iterable to slice + size: the maximum batch size Returns: an iterator over the chunks @@ -46,10 +46,7 @@ def batch_iter(iterable: Iterable[T], size: int) -> Iterator[Tuple[T]]: return iter(lambda: tuple(islice(sourceiter, size)), ()) -ISeq = TypeVar("ISeq", bound=Sequence, covariant=True) - - -def chunk_seq(iseq: ISeq, maxlen: int) -> Iterable[ISeq]: +def chunk_seq(iseq: Sequence[T], maxlen: int) -> Iterable[Sequence[T]]: """Split the given sequence into chunks of the given size The last chunk may be shorter than the given size. diff --git a/synapse/util/module_loader.py b/synapse/util/module_loader.py index 8acbe276e4..cbfbd097f9 100644 --- a/synapse/util/module_loader.py +++ b/synapse/util/module_loader.py @@ -15,6 +15,7 @@ import importlib import importlib.util import itertools +from types import ModuleType from typing import Any, Iterable, Tuple, Type import jsonschema @@ -44,8 +45,8 @@ def load_module(provider: dict, config_path: Iterable[str]) -> Tuple[Type, Any]: # We need to import the module, and then pick the class out of # that, so we split based on the last dot. - module, clz = modulename.rsplit(".", 1) - module = importlib.import_module(module) + module_name, clz = modulename.rsplit(".", 1) + module = importlib.import_module(module_name) provider_class = getattr(module, clz) # Load the module config. If None, pass an empty dictionary instead @@ -69,11 +70,11 @@ def load_module(provider: dict, config_path: Iterable[str]) -> Tuple[Type, Any]: return provider_class, provider_config -def load_python_module(location: str): +def load_python_module(location: str) -> ModuleType: """Load a python module, and return a reference to its global namespace Args: - location (str): path to the module + location: path to the module Returns: python module object diff --git a/synapse/util/msisdn.py b/synapse/util/msisdn.py index bbbdebf264..1046224f15 100644 --- a/synapse/util/msisdn.py +++ b/synapse/util/msisdn.py @@ -17,19 +17,19 @@ from synapse.api.errors import SynapseError -def phone_number_to_msisdn(country, number): +def phone_number_to_msisdn(country: str, number: str) -> str: """ Takes an ISO-3166-1 2 letter country code and phone number and returns an msisdn representing the canonical version of that phone number. Args: - country (str): ISO-3166-1 2 letter country code - number (str): Phone number in a national or international format + country: ISO-3166-1 2 letter country code + number: Phone number in a national or international format Returns: - (str) The canonical form of the phone number, as an msisdn + The canonical form of the phone number, as an msisdn Raises: - SynapseError if the number could not be parsed. + SynapseError if the number could not be parsed. """ try: phoneNumber = phonenumbers.parse(number, country) diff --git a/tests/util/test_itertools.py b/tests/util/test_itertools.py index 1bd0b45d94..e712eb42ea 100644 --- a/tests/util/test_itertools.py +++ b/tests/util/test_itertools.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List +from typing import Dict, Iterable, List, Sequence from synapse.util.iterutils import chunk_seq, sorted_topologically @@ -44,7 +44,7 @@ def test_uneven_parts(self): ) def test_empty_input(self): - parts = chunk_seq([], 5) + parts = chunk_seq([], 5) # type: Iterable[Sequence] self.assertEqual( list(parts), From 7d90d6ce9b6733bdebd4c5aca0c785e28e265e13 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 24 May 2021 15:32:45 -0400 Subject: [PATCH 192/619] Run complement with Synapse workers manually. (#10039) Adds an option to complement.sh to run Synapse in worker mode (instead of the default monolith mode). --- changelog.d/10039.misc | 1 + docker/configure_workers_and_start.py | 8 ++++---- scripts-dev/complement.sh | 25 ++++++++++++++++++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 changelog.d/10039.misc diff --git a/changelog.d/10039.misc b/changelog.d/10039.misc new file mode 100644 index 0000000000..8855f141d9 --- /dev/null +++ b/changelog.d/10039.misc @@ -0,0 +1 @@ +Fix running complement tests with Synapse workers. diff --git a/docker/configure_workers_and_start.py b/docker/configure_workers_and_start.py index 4be6afc65d..1d22a4d571 100755 --- a/docker/configure_workers_and_start.py +++ b/docker/configure_workers_and_start.py @@ -184,18 +184,18 @@ """ NGINX_LOCATION_CONFIG_BLOCK = """ - location ~* {endpoint} { + location ~* {endpoint} {{ proxy_pass {upstream}; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $host; - } + }} """ NGINX_UPSTREAM_CONFIG_BLOCK = """ -upstream {upstream_worker_type} { +upstream {upstream_worker_type} {{ {body} -} +}} """ diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index 1612ab522c..0043964673 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -10,6 +10,9 @@ # checkout by setting the COMPLEMENT_DIR environment variable to the # filepath of a local Complement checkout. # +# By default Synapse is run in monolith mode. This can be overridden by +# setting the WORKERS environment variable. +# # A regular expression of test method names can be supplied as the first # argument to the script. Complement will then only run those tests. If # no regex is supplied, all tests are run. For example; @@ -32,10 +35,26 @@ if [[ -z "$COMPLEMENT_DIR" ]]; then echo "Checkout available at 'complement-master'" fi +# If we're using workers, modify the docker files slightly. +if [[ -n "$WORKERS" ]]; then + BASE_IMAGE=matrixdotorg/synapse-workers + BASE_DOCKERFILE=docker/Dockerfile-workers + export COMPLEMENT_BASE_IMAGE=complement-synapse-workers + COMPLEMENT_DOCKERFILE=SynapseWorkers.Dockerfile + # And provide some more configuration to complement. + export COMPLEMENT_CA=true + export COMPLEMENT_VERSION_CHECK_ITERATIONS=500 +else + BASE_IMAGE=matrixdotorg/synapse + BASE_DOCKERFILE=docker/Dockerfile + export COMPLEMENT_BASE_IMAGE=complement-synapse + COMPLEMENT_DOCKERFILE=Synapse.Dockerfile +fi + # Build the base Synapse image from the local checkout -docker build -t matrixdotorg/synapse -f docker/Dockerfile . +docker build -t $BASE_IMAGE -f "$BASE_DOCKERFILE" . # Build the Synapse monolith image from Complement, based on the above image we just built -docker build -t complement-synapse -f "$COMPLEMENT_DIR/dockerfiles/Synapse.Dockerfile" "$COMPLEMENT_DIR/dockerfiles" +docker build -t $COMPLEMENT_BASE_IMAGE -f "$COMPLEMENT_DIR/dockerfiles/$COMPLEMENT_DOCKERFILE" "$COMPLEMENT_DIR/dockerfiles" cd "$COMPLEMENT_DIR" @@ -46,4 +65,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -COMPLEMENT_BASE_IMAGE=complement-synapse go test -v -tags synapse_blacklist,msc2946,msc3083 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests +go test -v -tags synapse_blacklist,msc2946,msc3083 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests From 557635f69ab734142ae5889e215ea512f7678f21 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 25 May 2021 11:00:13 +0100 Subject: [PATCH 193/619] 1.35.0rc1 --- CHANGES.md | 64 +++++++++++++++++++++++++++++++++++++++ changelog.d/10001.misc | 1 - changelog.d/10002.bugfix | 1 - changelog.d/10007.feature | 1 - changelog.d/10011.feature | 1 - changelog.d/10014.bugfix | 1 - changelog.d/10016.doc | 1 - changelog.d/10017.misc | 1 - changelog.d/10018.misc | 1 - changelog.d/10029.bugfix | 1 - changelog.d/10033.bugfix | 1 - changelog.d/10036.misc | 1 - changelog.d/10038.feature | 1 - changelog.d/10039.misc | 1 - changelog.d/10043.doc | 1 - changelog.d/10045.docker | 1 - changelog.d/10050.misc | 1 - changelog.d/9280.removal | 1 - changelog.d/9803.doc | 1 - changelog.d/9823.misc | 1 - changelog.d/9922.feature | 1 - changelog.d/9958.feature | 1 - changelog.d/9974.misc | 1 - changelog.d/9975.misc | 1 - changelog.d/9977.misc | 1 - changelog.d/9978.feature | 1 - changelog.d/9980.doc | 1 - changelog.d/9981.misc | 1 - changelog.d/9982.misc | 1 - changelog.d/9984.misc | 1 - changelog.d/9985.misc | 1 - changelog.d/9986.misc | 1 - changelog.d/9987.misc | 1 - changelog.d/9988.doc | 1 - changelog.d/9989.doc | 1 - changelog.d/9991.bugfix | 1 - changelog.d/9993.misc | 1 - changelog.d/9995.bugfix | 1 - synapse/__init__.py | 2 +- 39 files changed, 65 insertions(+), 38 deletions(-) delete mode 100644 changelog.d/10001.misc delete mode 100644 changelog.d/10002.bugfix delete mode 100644 changelog.d/10007.feature delete mode 100644 changelog.d/10011.feature delete mode 100644 changelog.d/10014.bugfix delete mode 100644 changelog.d/10016.doc delete mode 100644 changelog.d/10017.misc delete mode 100644 changelog.d/10018.misc delete mode 100644 changelog.d/10029.bugfix delete mode 100644 changelog.d/10033.bugfix delete mode 100644 changelog.d/10036.misc delete mode 100644 changelog.d/10038.feature delete mode 100644 changelog.d/10039.misc delete mode 100644 changelog.d/10043.doc delete mode 100644 changelog.d/10045.docker delete mode 100644 changelog.d/10050.misc delete mode 100644 changelog.d/9280.removal delete mode 100644 changelog.d/9803.doc delete mode 100644 changelog.d/9823.misc delete mode 100644 changelog.d/9922.feature delete mode 100644 changelog.d/9958.feature delete mode 100644 changelog.d/9974.misc delete mode 100644 changelog.d/9975.misc delete mode 100644 changelog.d/9977.misc delete mode 100644 changelog.d/9978.feature delete mode 100644 changelog.d/9980.doc delete mode 100644 changelog.d/9981.misc delete mode 100644 changelog.d/9982.misc delete mode 100644 changelog.d/9984.misc delete mode 100644 changelog.d/9985.misc delete mode 100644 changelog.d/9986.misc delete mode 100644 changelog.d/9987.misc delete mode 100644 changelog.d/9988.doc delete mode 100644 changelog.d/9989.doc delete mode 100644 changelog.d/9991.bugfix delete mode 100644 changelog.d/9993.misc delete mode 100644 changelog.d/9995.bugfix diff --git a/CHANGES.md b/CHANGES.md index 709436da97..0e451f983c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,67 @@ +Synapse 1.35.0rc1 (2021-05-25) +============================== + +Features +-------- + +- Add experimental support to allow a user who could join a restricted room to view it in the spaces summary. ([\#9922](https://github.com/matrix-org/synapse/issues/9922), [\#10007](https://github.com/matrix-org/synapse/issues/10007), [\#10038](https://github.com/matrix-org/synapse/issues/10038)) +- Reduce memory usage when joining very large rooms over federation. ([\#9958](https://github.com/matrix-org/synapse/issues/9958)) +- Add a configuration option which allows enabling opentracing by user id. ([\#9978](https://github.com/matrix-org/synapse/issues/9978)) +- Enable experimental support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946) (spaces summary API) and [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) (restricted join rules) by default. ([\#10011](https://github.com/matrix-org/synapse/issues/10011)) + + +Bugfixes +-------- + +- Fix a bug introduced in v1.26.0 which meant that `synapse_port_db` would not correctly initialise some postgres sequences, requiring manual updates afterwards. ([\#9991](https://github.com/matrix-org/synapse/issues/9991)) +- Fix `synctl`'s `--no-daemonize` parameter to work correctly with worker processes. ([\#9995](https://github.com/matrix-org/synapse/issues/9995)) +- Fix a validation bug introduced in v1.34.0 in the ordering of spaces in the space summary API. ([\#10002](https://github.com/matrix-org/synapse/issues/10002)) +- Fixed deletion of new presence stream states from database. ([\#10014](https://github.com/matrix-org/synapse/issues/10014), [\#10033](https://github.com/matrix-org/synapse/issues/10033)) +- Fixed a bug with very high resolution image uploads throwing internal server errors. ([\#10029](https://github.com/matrix-org/synapse/issues/10029)) + + +Updates to the Docker image +--------------------------- + +- Fix bug introduced in Synapse 1.33.0 which caused a `Permission denied: '/homeserver.log'` error when starting Synapse with the generated log configuration. Contributed by Sergio Miguéns Iglesias. ([\#10045](https://github.com/matrix-org/synapse/issues/10045)) + + +Improved Documentation +---------------------- + +- Add hardened systemd files as proposed in [#9760](https://github.com/matrix-org/synapse/issues/9760) and added them to `contrib/`. Change the docs to reflect the presence of these files. ([\#9803](https://github.com/matrix-org/synapse/issues/9803)) +- Clarify documentation around SSO mapping providers generating unique IDs and localparts. ([\#9980](https://github.com/matrix-org/synapse/issues/9980)) +- Updates to the PostgreSQL documentation (`postgres.md`). ([\#9988](https://github.com/matrix-org/synapse/issues/9988), [\#9989](https://github.com/matrix-org/synapse/issues/9989)) +- Fix broken link in user directory documentation. Contributed by @junquera. ([\#10016](https://github.com/matrix-org/synapse/issues/10016)) +- Add missing room state entry to the table of contents of room admin API. ([\#10043](https://github.com/matrix-org/synapse/issues/10043)) + + +Deprecations and Removals +------------------------- + +- Removed support for the deprecated `tls_fingerprints` configuration setting. Contributed by Jerin J Titus. ([\#9280](https://github.com/matrix-org/synapse/issues/9280)) + + +Internal Changes +---------------- + +- Allow sending full presence to users via workers other than the one that called `ModuleApi.send_local_online_presence_to`. ([\#9823](https://github.com/matrix-org/synapse/issues/9823)) +- Update comments in the space summary handler. ([\#9974](https://github.com/matrix-org/synapse/issues/9974)) +- Minor enhancements to the `@cachedList` descriptor. ([\#9975](https://github.com/matrix-org/synapse/issues/9975)) +- Split multipart email sending into a dedicated handler. ([\#9977](https://github.com/matrix-org/synapse/issues/9977)) +- Run `black` on files in the `scripts` directory. ([\#9981](https://github.com/matrix-org/synapse/issues/9981)) +- Add missing type hints to `synapse.util` module. ([\#9982](https://github.com/matrix-org/synapse/issues/9982)) +- Simplify a few helper functions. ([\#9984](https://github.com/matrix-org/synapse/issues/9984), [\#9985](https://github.com/matrix-org/synapse/issues/9985), [\#9986](https://github.com/matrix-org/synapse/issues/9986)) +- Remove unnecessary property from SQLBaseStore. ([\#9987](https://github.com/matrix-org/synapse/issues/9987)) +- Remove `keylen` param on `LruCache`. ([\#9993](https://github.com/matrix-org/synapse/issues/9993)) +- Update the Grafana dashboard in `contrib/`. ([\#10001](https://github.com/matrix-org/synapse/issues/10001)) +- Add a batching queue implementation. ([\#10017](https://github.com/matrix-org/synapse/issues/10017)) +- Reduce memory usage when verifying signatures on large numbers of events at once. ([\#10018](https://github.com/matrix-org/synapse/issues/10018)) +- Properly invalidate caches for destination retry timings every (instead of expiring entries every 5 minutes). ([\#10036](https://github.com/matrix-org/synapse/issues/10036)) +- Fix running complement tests with Synapse workers. ([\#10039](https://github.com/matrix-org/synapse/issues/10039)) +- Fix typo in `get_state_ids_for_event` docstring where the return type was incorrect. ([\#10050](https://github.com/matrix-org/synapse/issues/10050)) + + Synapse 1.34.0 (2021-05-17) =========================== diff --git a/changelog.d/10001.misc b/changelog.d/10001.misc deleted file mode 100644 index 8740cc478d..0000000000 --- a/changelog.d/10001.misc +++ /dev/null @@ -1 +0,0 @@ -Update the Grafana dashboard in `contrib/`. diff --git a/changelog.d/10002.bugfix b/changelog.d/10002.bugfix deleted file mode 100644 index 1fabdad22e..0000000000 --- a/changelog.d/10002.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a validation bug introduced in v1.34.0 in the ordering of spaces in the space summary API. diff --git a/changelog.d/10007.feature b/changelog.d/10007.feature deleted file mode 100644 index 2c655350c0..0000000000 --- a/changelog.d/10007.feature +++ /dev/null @@ -1 +0,0 @@ -Experimental support to allow a user who could join a restricted room to view it in the spaces summary. diff --git a/changelog.d/10011.feature b/changelog.d/10011.feature deleted file mode 100644 index 409140fb13..0000000000 --- a/changelog.d/10011.feature +++ /dev/null @@ -1 +0,0 @@ -Enable experimental support for [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946) (spaces summary API) and [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) (restricted join rules) by default. diff --git a/changelog.d/10014.bugfix b/changelog.d/10014.bugfix deleted file mode 100644 index 7cf3603f94..0000000000 --- a/changelog.d/10014.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed deletion of new presence stream states from database. diff --git a/changelog.d/10016.doc b/changelog.d/10016.doc deleted file mode 100644 index f9b615d7d7..0000000000 --- a/changelog.d/10016.doc +++ /dev/null @@ -1 +0,0 @@ -Fix broken link in user directory documentation. Contributed by @junquera. diff --git a/changelog.d/10017.misc b/changelog.d/10017.misc deleted file mode 100644 index 4777b7fb57..0000000000 --- a/changelog.d/10017.misc +++ /dev/null @@ -1 +0,0 @@ -Add a batching queue implementation. diff --git a/changelog.d/10018.misc b/changelog.d/10018.misc deleted file mode 100644 index eaf9f64867..0000000000 --- a/changelog.d/10018.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce memory usage when verifying signatures on large numbers of events at once. diff --git a/changelog.d/10029.bugfix b/changelog.d/10029.bugfix deleted file mode 100644 index c214cbdaec..0000000000 --- a/changelog.d/10029.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug with very high resolution image uploads throwing internal server errors. \ No newline at end of file diff --git a/changelog.d/10033.bugfix b/changelog.d/10033.bugfix deleted file mode 100644 index 587d839b8c..0000000000 --- a/changelog.d/10033.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed deletion of new presence stream states from database. \ No newline at end of file diff --git a/changelog.d/10036.misc b/changelog.d/10036.misc deleted file mode 100644 index d2cf1e5473..0000000000 --- a/changelog.d/10036.misc +++ /dev/null @@ -1 +0,0 @@ -Properly invalidate caches for destination retry timings every (instead of expiring entries every 5 minutes). diff --git a/changelog.d/10038.feature b/changelog.d/10038.feature deleted file mode 100644 index 2c655350c0..0000000000 --- a/changelog.d/10038.feature +++ /dev/null @@ -1 +0,0 @@ -Experimental support to allow a user who could join a restricted room to view it in the spaces summary. diff --git a/changelog.d/10039.misc b/changelog.d/10039.misc deleted file mode 100644 index 8855f141d9..0000000000 --- a/changelog.d/10039.misc +++ /dev/null @@ -1 +0,0 @@ -Fix running complement tests with Synapse workers. diff --git a/changelog.d/10043.doc b/changelog.d/10043.doc deleted file mode 100644 index a574ec0bf0..0000000000 --- a/changelog.d/10043.doc +++ /dev/null @@ -1 +0,0 @@ -Add missing room state entry to the table of contents of room admin API. \ No newline at end of file diff --git a/changelog.d/10045.docker b/changelog.d/10045.docker deleted file mode 100644 index 70b65b0a01..0000000000 --- a/changelog.d/10045.docker +++ /dev/null @@ -1 +0,0 @@ -Fix bug introduced in Synapse 1.33.0 which caused a `Permission denied: '/homeserver.log'` error when starting Synapse with the generated log configuration. Contributed by Sergio Miguéns Iglesias. diff --git a/changelog.d/10050.misc b/changelog.d/10050.misc deleted file mode 100644 index 2cac953cca..0000000000 --- a/changelog.d/10050.misc +++ /dev/null @@ -1 +0,0 @@ -Fix typo in `get_state_ids_for_event` docstring where the return type was incorrect. diff --git a/changelog.d/9280.removal b/changelog.d/9280.removal deleted file mode 100644 index c2ed3d308d..0000000000 --- a/changelog.d/9280.removal +++ /dev/null @@ -1 +0,0 @@ -Removed support for the deprecated `tls_fingerprints` configuration setting. Contributed by Jerin J Titus. \ No newline at end of file diff --git a/changelog.d/9803.doc b/changelog.d/9803.doc deleted file mode 100644 index 16c7ba7033..0000000000 --- a/changelog.d/9803.doc +++ /dev/null @@ -1 +0,0 @@ -Add hardened systemd files as proposed in [#9760](https://github.com/matrix-org/synapse/issues/9760) and added them to `contrib/`. Change the docs to reflect the presence of these files. diff --git a/changelog.d/9823.misc b/changelog.d/9823.misc deleted file mode 100644 index bf924ab68c..0000000000 --- a/changelog.d/9823.misc +++ /dev/null @@ -1 +0,0 @@ -Allow sending full presence to users via workers other than the one that called `ModuleApi.send_local_online_presence_to`. \ No newline at end of file diff --git a/changelog.d/9922.feature b/changelog.d/9922.feature deleted file mode 100644 index 2c655350c0..0000000000 --- a/changelog.d/9922.feature +++ /dev/null @@ -1 +0,0 @@ -Experimental support to allow a user who could join a restricted room to view it in the spaces summary. diff --git a/changelog.d/9958.feature b/changelog.d/9958.feature deleted file mode 100644 index d86ba36519..0000000000 --- a/changelog.d/9958.feature +++ /dev/null @@ -1 +0,0 @@ -Reduce memory usage when joining very large rooms over federation. diff --git a/changelog.d/9974.misc b/changelog.d/9974.misc deleted file mode 100644 index 9ddee2618e..0000000000 --- a/changelog.d/9974.misc +++ /dev/null @@ -1 +0,0 @@ -Update comments in the space summary handler. diff --git a/changelog.d/9975.misc b/changelog.d/9975.misc deleted file mode 100644 index 28b1e40c2b..0000000000 --- a/changelog.d/9975.misc +++ /dev/null @@ -1 +0,0 @@ -Minor enhancements to the `@cachedList` descriptor. diff --git a/changelog.d/9977.misc b/changelog.d/9977.misc deleted file mode 100644 index 093dffc6be..0000000000 --- a/changelog.d/9977.misc +++ /dev/null @@ -1 +0,0 @@ -Split multipart email sending into a dedicated handler. diff --git a/changelog.d/9978.feature b/changelog.d/9978.feature deleted file mode 100644 index 851adb9f6e..0000000000 --- a/changelog.d/9978.feature +++ /dev/null @@ -1 +0,0 @@ -Add a configuration option which allows enabling opentracing by user id. diff --git a/changelog.d/9980.doc b/changelog.d/9980.doc deleted file mode 100644 index d30ed0601d..0000000000 --- a/changelog.d/9980.doc +++ /dev/null @@ -1 +0,0 @@ -Clarify documentation around SSO mapping providers generating unique IDs and localparts. diff --git a/changelog.d/9981.misc b/changelog.d/9981.misc deleted file mode 100644 index 677c9b4cbd..0000000000 --- a/changelog.d/9981.misc +++ /dev/null @@ -1 +0,0 @@ -Run `black` on files in the `scripts` directory. diff --git a/changelog.d/9982.misc b/changelog.d/9982.misc deleted file mode 100644 index f3821f61a3..0000000000 --- a/changelog.d/9982.misc +++ /dev/null @@ -1 +0,0 @@ -Add missing type hints to `synapse.util` module. diff --git a/changelog.d/9984.misc b/changelog.d/9984.misc deleted file mode 100644 index 97bd747f26..0000000000 --- a/changelog.d/9984.misc +++ /dev/null @@ -1 +0,0 @@ -Simplify a few helper functions. diff --git a/changelog.d/9985.misc b/changelog.d/9985.misc deleted file mode 100644 index 97bd747f26..0000000000 --- a/changelog.d/9985.misc +++ /dev/null @@ -1 +0,0 @@ -Simplify a few helper functions. diff --git a/changelog.d/9986.misc b/changelog.d/9986.misc deleted file mode 100644 index 97bd747f26..0000000000 --- a/changelog.d/9986.misc +++ /dev/null @@ -1 +0,0 @@ -Simplify a few helper functions. diff --git a/changelog.d/9987.misc b/changelog.d/9987.misc deleted file mode 100644 index 02c088e3e6..0000000000 --- a/changelog.d/9987.misc +++ /dev/null @@ -1 +0,0 @@ -Remove unnecessary property from SQLBaseStore. diff --git a/changelog.d/9988.doc b/changelog.d/9988.doc deleted file mode 100644 index 25338c44c3..0000000000 --- a/changelog.d/9988.doc +++ /dev/null @@ -1 +0,0 @@ -Updates to the PostgreSQL documentation (`postgres.md`). diff --git a/changelog.d/9989.doc b/changelog.d/9989.doc deleted file mode 100644 index 25338c44c3..0000000000 --- a/changelog.d/9989.doc +++ /dev/null @@ -1 +0,0 @@ -Updates to the PostgreSQL documentation (`postgres.md`). diff --git a/changelog.d/9991.bugfix b/changelog.d/9991.bugfix deleted file mode 100644 index 665ff04dea..0000000000 --- a/changelog.d/9991.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.26.0 which meant that `synapse_port_db` would not correctly initialise some postgres sequences, requiring manual updates afterwards. diff --git a/changelog.d/9993.misc b/changelog.d/9993.misc deleted file mode 100644 index 0dd9244071..0000000000 --- a/changelog.d/9993.misc +++ /dev/null @@ -1 +0,0 @@ -Remove `keylen` param on `LruCache`. diff --git a/changelog.d/9995.bugfix b/changelog.d/9995.bugfix deleted file mode 100644 index 3b63e7c42a..0000000000 --- a/changelog.d/9995.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `synctl`'s `--no-daemonize` parameter to work correctly with worker processes. diff --git a/synapse/__init__.py b/synapse/__init__.py index 7498a6016f..e60e9db71e 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.34.0" +__version__ = "1.35.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 3e1beb75e65f48acb778a64da66a97b01f48bdd3 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Wed, 26 May 2021 04:55:30 -0500 Subject: [PATCH 194/619] Update CAPTCHA documentation to mention turning off verify origin feature (#10046) * Update CAPTCHA documentation to mention turning off verify origin Signed-off-by: Aaron Raimist --- changelog.d/10046.doc | 1 + docs/CAPTCHA_SETUP.md | 50 ++++++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 changelog.d/10046.doc diff --git a/changelog.d/10046.doc b/changelog.d/10046.doc new file mode 100644 index 0000000000..995960163b --- /dev/null +++ b/changelog.d/10046.doc @@ -0,0 +1 @@ +Update CAPTCHA documentation to mention turning off the verify origin feature. Contributed by @aaronraimist. diff --git a/docs/CAPTCHA_SETUP.md b/docs/CAPTCHA_SETUP.md index 331e5d059a..fabdd7b726 100644 --- a/docs/CAPTCHA_SETUP.md +++ b/docs/CAPTCHA_SETUP.md @@ -1,31 +1,37 @@ # Overview -Captcha can be enabled for this home server. This file explains how to do that. -The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google. - -## Getting keys - -Requires a site/secret key pair from: - - - -Must be a reCAPTCHA v2 key using the "I'm not a robot" Checkbox option - -## Setting ReCaptcha Keys - -The keys are a config option on the home server config. If they are not -visible, you can generate them via `--generate-config`. Set the following value: - +A captcha can be enabled on your homeserver to help prevent bots from registering +accounts. Synapse currently uses Google's reCAPTCHA service which requires API keys +from Google. + +## Getting API keys + +1. Create a new site at +1. Set the label to anything you want +1. Set the type to reCAPTCHA v2 using the "I'm not a robot" Checkbox option. +This is the only type of captcha that works with Synapse. +1. Add the public hostname for your server, as set in `public_baseurl` +in `homeserver.yaml`, to the list of authorized domains. If you have not set +`public_baseurl`, use `server_name`. +1. Agree to the terms of service and submit. +1. Copy your site key and secret key and add them to your `homeserver.yaml` +configuration file + ``` recaptcha_public_key: YOUR_SITE_KEY recaptcha_private_key: YOUR_SECRET_KEY - -In addition, you MUST enable captchas via: - + ``` +1. Enable the CAPTCHA for new registrations + ``` enable_registration_captcha: true + ``` +1. Go to the settings page for the CAPTCHA you just created +1. Uncheck the "Verify the origin of reCAPTCHA solutions" checkbox so that the +captcha can be displayed in any client. If you do not disable this option then you +must specify the domains of every client that is allowed to display the CAPTCHA. ## Configuring IP used for auth -The ReCaptcha API requires that the IP address of the user who solved the -captcha is sent. If the client is connecting through a proxy or load balancer, +The reCAPTCHA API requires that the IP address of the user who solved the +CAPTCHA is sent. If the client is connecting through a proxy or load balancer, it may be required to use the `X-Forwarded-For` (XFF) header instead of the origin IP address. This can be configured using the `x_forwarded` directive in the -listeners section of the homeserver.yaml configuration file. +listeners section of the `homeserver.yaml` configuration file. From 65e6c64d8317d3a10527a7e422753281f3e9ec81 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Wed, 26 May 2021 12:19:47 +0200 Subject: [PATCH 195/619] Add an admin API for unprotecting local media from quarantine (#10040) Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10040.feature | 1 + docs/admin_api/media_admin_api.md | 21 ++++ synapse/rest/admin/media.py | 28 +++++- .../databases/main/media_repository.py | 7 +- tests/rest/admin/test_media.py | 99 +++++++++++++++++++ 5 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10040.feature diff --git a/changelog.d/10040.feature b/changelog.d/10040.feature new file mode 100644 index 0000000000..ec78a30f00 --- /dev/null +++ b/changelog.d/10040.feature @@ -0,0 +1 @@ +Add an admin API for unprotecting local media from quarantine. Contributed by @dklimpel. diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index 9dbec68c19..d1b7e390d5 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -7,6 +7,7 @@ * [Quarantining media in a room](#quarantining-media-in-a-room) * [Quarantining all media of a user](#quarantining-all-media-of-a-user) * [Protecting media from being quarantined](#protecting-media-from-being-quarantined) + * [Unprotecting media from being quarantined](#unprotecting-media-from-being-quarantined) - [Delete local media](#delete-local-media) * [Delete a specific local media](#delete-a-specific-local-media) * [Delete local media by date or size](#delete-local-media-by-date-or-size) @@ -159,6 +160,26 @@ Response: {} ``` +## Unprotecting media from being quarantined + +This API reverts the protection of a media. + +Request: + +``` +POST /_synapse/admin/v1/media/unprotect/ + +{} +``` + +Where `media_id` is in the form of `abcdefg12345...`. + +Response: + +```json +{} +``` + # Delete local media This API deletes the *local* media from the disk of your own server. This includes any local thumbnails and copies of media downloaded from diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index 24dd46113a..2c71af4279 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -137,8 +137,31 @@ async def on_POST( logging.info("Protecting local media by ID: %s", media_id) - # Quarantine this media id - await self.store.mark_local_media_as_safe(media_id) + # Protect this media id + await self.store.mark_local_media_as_safe(media_id, safe=True) + + return 200, {} + + +class UnprotectMediaByID(RestServlet): + """Unprotect local media from being quarantined.""" + + PATTERNS = admin_patterns("/media/unprotect/(?P[^/]+)") + + def __init__(self, hs: "HomeServer"): + self.store = hs.get_datastore() + self.auth = hs.get_auth() + + async def on_POST( + self, request: SynapseRequest, media_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + logging.info("Unprotecting local media by ID: %s", media_id) + + # Unprotect this media id + await self.store.mark_local_media_as_safe(media_id, safe=False) return 200, {} @@ -269,6 +292,7 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server): QuarantineMediaByID(hs).register(http_server) QuarantineMediaByUser(hs).register(http_server) ProtectMediaByID(hs).register(http_server) + UnprotectMediaByID(hs).register(http_server) ListMediaInRoom(hs).register(http_server) DeleteMediaByID(hs).register(http_server) DeleteMediaByDateSize(hs).register(http_server) diff --git a/synapse/storage/databases/main/media_repository.py b/synapse/storage/databases/main/media_repository.py index c584868188..2fa945d171 100644 --- a/synapse/storage/databases/main/media_repository.py +++ b/synapse/storage/databases/main/media_repository.py @@ -143,6 +143,7 @@ async def get_local_media(self, media_id: str) -> Optional[Dict[str, Any]]: "created_ts", "quarantined_by", "url_cache", + "safe_from_quarantine", ), allow_none=True, desc="get_local_media", @@ -296,12 +297,12 @@ async def store_local_media( desc="store_local_media", ) - async def mark_local_media_as_safe(self, media_id: str) -> None: - """Mark a local media as safe from quarantining.""" + async def mark_local_media_as_safe(self, media_id: str, safe: bool = True) -> None: + """Mark a local media as safe or unsafe from quarantining.""" await self.db_pool.simple_update_one( table="local_media_repository", keyvalues={"media_id": media_id}, - updatevalues={"safe_from_quarantine": True}, + updatevalues={"safe_from_quarantine": safe}, desc="mark_local_media_as_safe", ) diff --git a/tests/rest/admin/test_media.py b/tests/rest/admin/test_media.py index ac7b219700..f741121ea2 100644 --- a/tests/rest/admin/test_media.py +++ b/tests/rest/admin/test_media.py @@ -16,6 +16,8 @@ import os from binascii import unhexlify +from parameterized import parameterized + import synapse.rest.admin from synapse.api.errors import Codes from synapse.rest.client.v1 import login, profile, room @@ -562,3 +564,100 @@ def _access_media(self, server_and_media_id, expect_success=True): ) # Test that the file is deleted self.assertFalse(os.path.exists(local_path)) + + +class ProtectMediaByIDTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + synapse.rest.admin.register_servlets_for_media_repo, + login.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + media_repo = hs.get_media_repository_resource() + self.store = hs.get_datastore() + + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + # Create media + upload_resource = media_repo.children[b"upload"] + # file size is 67 Byte + image_data = unhexlify( + b"89504e470d0a1a0a0000000d4948445200000001000000010806" + b"0000001f15c4890000000a49444154789c63000100000500010d" + b"0a2db40000000049454e44ae426082" + ) + + # Upload some media into the room + response = self.helper.upload_media( + upload_resource, image_data, tok=self.admin_user_tok, expect_code=200 + ) + # Extract media ID from the response + server_and_media_id = response["content_uri"][6:] # Cut off 'mxc://' + self.media_id = server_and_media_id.split("/")[1] + + self.url = "/_synapse/admin/v1/media/%s/%s" + + @parameterized.expand(["protect", "unprotect"]) + def test_no_auth(self, action: str): + """ + Try to protect media without authentication. + """ + + channel = self.make_request("POST", self.url % (action, self.media_id), b"{}") + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) + + @parameterized.expand(["protect", "unprotect"]) + def test_requester_is_no_admin(self, action: str): + """ + If the user is not a server admin, an error is returned. + """ + self.other_user = self.register_user("user", "pass") + self.other_user_token = self.login("user", "pass") + + channel = self.make_request( + "POST", + self.url % (action, self.media_id), + access_token=self.other_user_token, + ) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_protect_media(self): + """ + Tests that protect and unprotect a media is successfully + """ + + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertFalse(media_info["safe_from_quarantine"]) + + # protect + channel = self.make_request( + "POST", + self.url % ("protect", self.media_id), + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertFalse(channel.json_body) + + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertTrue(media_info["safe_from_quarantine"]) + + # unprotect + channel = self.make_request( + "POST", + self.url % ("unprotect", self.media_id), + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertFalse(channel.json_body) + + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertFalse(media_info["safe_from_quarantine"]) From 913a761a53640b725245408ff8f49bf54493707c Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Wed, 26 May 2021 13:16:06 +0100 Subject: [PATCH 196/619] Tell CircleCI to build Docker images from `main` (#9906) The `only` field takes a string or list of strings per the Circle docs: https://circleci.com/docs/2.0/configuration-reference/#branches Signed-off-by: Dan Callahan --- .circleci/config.yml | 2 +- changelog.d/9906.misc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/9906.misc diff --git a/.circleci/config.yml b/.circleci/config.yml index 1ac48a71ba..cf1989eff9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,7 +41,7 @@ workflows: - dockerhubuploadlatest: filters: branches: - only: master + only: [ master, main ] commands: docker_prepare: diff --git a/changelog.d/9906.misc b/changelog.d/9906.misc new file mode 100644 index 0000000000..667d51a4c0 --- /dev/null +++ b/changelog.d/9906.misc @@ -0,0 +1 @@ +Tell CircleCI to build Docker images from `main` branch. From f95e7a03fa66e00b581d11f17244bf1701559f6c Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Wed, 26 May 2021 07:29:02 -0500 Subject: [PATCH 197/619] Tweak wording of database recommendation in INSTALL.md (#10057) * Tweak wording of database recommendation in INSTALL.md Signed-off-by: Aaron Raimist --- INSTALL.md | 12 +++++++----- changelog.d/10057.doc | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10057.doc diff --git a/INSTALL.md b/INSTALL.md index 7b40689234..3c498edd29 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -399,11 +399,9 @@ Once you have installed synapse as above, you will need to configure it. ### Using PostgreSQL -By default Synapse uses [SQLite](https://sqlite.org/) and in doing so trades performance for convenience. -SQLite is only recommended in Synapse for testing purposes or for servers with -very light workloads. - -Almost all installations should opt to use [PostgreSQL](https://www.postgresql.org). Advantages include: +By default Synapse uses an [SQLite](https://sqlite.org/) database and in doing so trades +performance for convenience. Almost all installations should opt to use [PostgreSQL](https://www.postgresql.org) +instead. Advantages include: - significant performance improvements due to the superior threading and caching model, smarter query optimiser @@ -412,6 +410,10 @@ Almost all installations should opt to use [PostgreSQL](https://www.postgresql.o For information on how to install and use PostgreSQL in Synapse, please see [docs/postgres.md](docs/postgres.md) +SQLite is only acceptable for testing purposes. SQLite should not be used in +a production server. Synapse will perform poorly when using +SQLite, especially when participating in large rooms. + ### TLS certificates The default configuration exposes a single HTTP port on the local diff --git a/changelog.d/10057.doc b/changelog.d/10057.doc new file mode 100644 index 0000000000..35437cb017 --- /dev/null +++ b/changelog.d/10057.doc @@ -0,0 +1 @@ +Tweak wording of database recommendation in `INSTALL.md`. Contributed by @aaronraimist. \ No newline at end of file From 49df2c28e3f2f11ddb29536468990d0cd3ff68d0 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Wed, 26 May 2021 14:14:43 +0100 Subject: [PATCH 198/619] Fix GitHub Actions lint for newsfragments (#10069) * Fix GitHub Actions lint for newsfragments Signed-off-by: Dan Callahan --- .github/workflows/tests.yml | 6 ++++++ changelog.d/10069.misc | 1 + 2 files changed, 7 insertions(+) create mode 100644 changelog.d/10069.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e7f3be1b4e..2ae81b5fcf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,7 +34,13 @@ jobs: if: ${{ github.base_ref == 'develop' || contains(github.base_ref, 'release-') }} runs-on: ubuntu-latest steps: + # Note: This and the script can be simplified once we drop Buildkite. See: + # https://github.com/actions/checkout/issues/266#issuecomment-638346893 + # https://github.com/actions/checkout/issues/416 - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - uses: actions/setup-python@v2 - run: pip install tox - name: Patch Buildkite-specific test script diff --git a/changelog.d/10069.misc b/changelog.d/10069.misc new file mode 100644 index 0000000000..a8d2629e9b --- /dev/null +++ b/changelog.d/10069.misc @@ -0,0 +1 @@ +Fix GitHub Actions lint for newsfragments. From f42e4c4eb9b5b84bd1da80a4f3938c1c06305364 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 26 May 2021 14:35:16 -0400 Subject: [PATCH 199/619] Remove the experimental spaces enabled flag. (#10063) In lieu of just always enabling the unstable spaces endpoint and unstable room version. --- changelog.d/10063.removal | 1 + docs/sample_config.yaml | 15 --------------- synapse/api/room_versions.py | 2 +- synapse/config/experimental.py | 23 ----------------------- synapse/federation/transport/server.py | 13 ++++++------- synapse/rest/client/v1/room.py | 4 +--- 6 files changed, 9 insertions(+), 49 deletions(-) create mode 100644 changelog.d/10063.removal diff --git a/changelog.d/10063.removal b/changelog.d/10063.removal new file mode 100644 index 0000000000..0f8889b6b4 --- /dev/null +++ b/changelog.d/10063.removal @@ -0,0 +1 @@ +Remove the experimental `spaces_enabled` flag. The spaces features are always available now. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 6576b153d0..7b97f73a29 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -2916,18 +2916,3 @@ redis: # Optional password if configured on the Redis instance # #password: - - -# Enable experimental features in Synapse. -# -# Experimental features might break or be removed without a deprecation -# period. -# -experimental_features: - # Support for Spaces (MSC1772), it enables the following: - # - # * The Spaces Summary API (MSC2946). - # * Restricting room membership based on space membership (MSC3083). - # - # Uncomment to disable support for Spaces. - #spaces_enabled: false diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index c9f9596ada..373a4669d0 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -181,6 +181,6 @@ class RoomVersions: RoomVersions.V5, RoomVersions.V6, RoomVersions.MSC2176, + RoomVersions.MSC3083, ) - # Note that we do not include MSC3083 here unless it is enabled in the config. } # type: Dict[str, RoomVersion] diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index cc67377f0f..6ebce4b2f7 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.config._base import Config from synapse.types import JsonDict @@ -28,27 +27,5 @@ def read_config(self, config: JsonDict, **kwargs): # MSC2858 (multiple SSO identity providers) self.msc2858_enabled = experimental.get("msc2858_enabled", False) # type: bool - # Spaces (MSC1772, MSC2946, MSC3083, etc) - self.spaces_enabled = experimental.get("spaces_enabled", True) # type: bool - if self.spaces_enabled: - KNOWN_ROOM_VERSIONS[RoomVersions.MSC3083.identifier] = RoomVersions.MSC3083 - # MSC3026 (busy presence state) self.msc3026_enabled = experimental.get("msc3026_enabled", False) # type: bool - - def generate_config_section(self, **kwargs): - return """\ - # Enable experimental features in Synapse. - # - # Experimental features might break or be removed without a deprecation - # period. - # - experimental_features: - # Support for Spaces (MSC1772), it enables the following: - # - # * The Spaces Summary API (MSC2946). - # * Restricting room membership based on space membership (MSC3083). - # - # Uncomment to disable support for Spaces. - #spaces_enabled: false - """ diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 9d50b05d01..00ff02c7cb 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1562,13 +1562,12 @@ def register_servlets( server_name=hs.hostname, ).register(resource) - if hs.config.experimental.spaces_enabled: - FederationSpaceSummaryServlet( - handler=hs.get_space_summary_handler(), - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) + FederationSpaceSummaryServlet( + handler=hs.get_space_summary_handler(), + authenticator=authenticator, + ratelimiter=ratelimiter, + server_name=hs.hostname, + ).register(resource) if "openid" in servlet_groups: for servletclass in OPENID_SERVLET_CLASSES: diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 51813cccbe..d6d55893af 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -1060,9 +1060,7 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False): RoomRedactEventRestServlet(hs).register(http_server) RoomTypingRestServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) - - if hs.config.experimental.spaces_enabled: - RoomSpaceSummaryRestServlet(hs).register(http_server) + RoomSpaceSummaryRestServlet(hs).register(http_server) # Some servlets only get registered for the main process. if not is_worker: From 224f2f949b1661094a64d1105efb64159ddf4aa0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 27 May 2021 10:33:56 +0100 Subject: [PATCH 200/619] Combine `LruCache.invalidate` and `invalidate_many` (#9973) * Make `invalidate` and `invalidate_many` do the same thing ... so that we can do either over the invalidation replication stream, and also because they always confused me a bit. * Kill off `invalidate_many` * changelog --- changelog.d/9973.misc | 1 + synapse/replication/slave/storage/devices.py | 2 +- synapse/storage/databases/main/cache.py | 6 +-- synapse/storage/databases/main/devices.py | 2 +- .../databases/main/event_push_actions.py | 2 +- synapse/storage/databases/main/events.py | 8 ++-- synapse/storage/databases/main/receipts.py | 6 +-- synapse/util/caches/deferred_cache.py | 42 +++++++------------ synapse/util/caches/descriptors.py | 8 +++- synapse/util/caches/lrucache.py | 18 ++++---- synapse/util/caches/treecache.py | 3 ++ tests/util/caches/test_descriptors.py | 6 +-- 12 files changed, 52 insertions(+), 52 deletions(-) create mode 100644 changelog.d/9973.misc diff --git a/changelog.d/9973.misc b/changelog.d/9973.misc new file mode 100644 index 0000000000..7f22d42291 --- /dev/null +++ b/changelog.d/9973.misc @@ -0,0 +1 @@ +Make `LruCache.invalidate` support tree invalidation, and remove `invalidate_many`. diff --git a/synapse/replication/slave/storage/devices.py b/synapse/replication/slave/storage/devices.py index 70207420a6..26bdead565 100644 --- a/synapse/replication/slave/storage/devices.py +++ b/synapse/replication/slave/storage/devices.py @@ -68,7 +68,7 @@ def _invalidate_caches_for_devices(self, token, rows): if row.entity.startswith("@"): self._device_list_stream_cache.entity_has_changed(row.entity, token) self.get_cached_devices_for_user.invalidate((row.entity,)) - self._get_cached_user_device.invalidate_many((row.entity,)) + self._get_cached_user_device.invalidate((row.entity,)) self.get_device_list_last_stream_id_for_remote.invalidate((row.entity,)) else: diff --git a/synapse/storage/databases/main/cache.py b/synapse/storage/databases/main/cache.py index ecc1f935e2..f7872501a0 100644 --- a/synapse/storage/databases/main/cache.py +++ b/synapse/storage/databases/main/cache.py @@ -171,7 +171,7 @@ def _invalidate_caches_for_event( self.get_latest_event_ids_in_room.invalidate((room_id,)) - self.get_unread_event_push_actions_by_room_for_user.invalidate_many((room_id,)) + self.get_unread_event_push_actions_by_room_for_user.invalidate((room_id,)) if not backfilled: self._events_stream_cache.entity_has_changed(room_id, stream_ordering) @@ -184,8 +184,8 @@ def _invalidate_caches_for_event( self.get_invited_rooms_for_local_user.invalidate((state_key,)) if relates_to: - self.get_relations_for_event.invalidate_many((relates_to,)) - self.get_aggregation_groups_for_event.invalidate_many((relates_to,)) + self.get_relations_for_event.invalidate((relates_to,)) + self.get_aggregation_groups_for_event.invalidate((relates_to,)) self.get_applicable_edit.invalidate((relates_to,)) async def invalidate_cache_and_stream(self, cache_name: str, keys: Tuple[Any, ...]): diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index fd87ba71ab..18f07d96dc 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -1282,7 +1282,7 @@ def _update_remote_device_list_cache_txn( ) txn.call_after(self.get_cached_devices_for_user.invalidate, (user_id,)) - txn.call_after(self._get_cached_user_device.invalidate_many, (user_id,)) + txn.call_after(self._get_cached_user_device.invalidate, (user_id,)) txn.call_after( self.get_device_list_last_stream_id_for_remote.invalidate, (user_id,) ) diff --git a/synapse/storage/databases/main/event_push_actions.py b/synapse/storage/databases/main/event_push_actions.py index 5845322118..d1237c65cc 100644 --- a/synapse/storage/databases/main/event_push_actions.py +++ b/synapse/storage/databases/main/event_push_actions.py @@ -860,7 +860,7 @@ def _remove_old_push_actions_before_txn( not be deleted. """ txn.call_after( - self.get_unread_event_push_actions_by_room_for_user.invalidate_many, + self.get_unread_event_push_actions_by_room_for_user.invalidate, (room_id, user_id), ) diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index fd25c8112d..897fa06639 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -1748,9 +1748,9 @@ def _handle_event_relations(self, txn, event): }, ) - txn.call_after(self.store.get_relations_for_event.invalidate_many, (parent_id,)) + txn.call_after(self.store.get_relations_for_event.invalidate, (parent_id,)) txn.call_after( - self.store.get_aggregation_groups_for_event.invalidate_many, (parent_id,) + self.store.get_aggregation_groups_for_event.invalidate, (parent_id,) ) if rel_type == RelationTypes.REPLACE: @@ -1903,7 +1903,7 @@ def _set_push_actions_for_event_and_users_txn( for user_id in user_ids: txn.call_after( - self.store.get_unread_event_push_actions_by_room_for_user.invalidate_many, + self.store.get_unread_event_push_actions_by_room_for_user.invalidate, (room_id, user_id), ) @@ -1917,7 +1917,7 @@ def _set_push_actions_for_event_and_users_txn( def _remove_push_actions_for_event_id_txn(self, txn, room_id, event_id): # Sad that we have to blow away the cache for the whole room here txn.call_after( - self.store.get_unread_event_push_actions_by_room_for_user.invalidate_many, + self.store.get_unread_event_push_actions_by_room_for_user.invalidate, (room_id,), ) txn.execute( diff --git a/synapse/storage/databases/main/receipts.py b/synapse/storage/databases/main/receipts.py index 3647276acb..edeaacd7a6 100644 --- a/synapse/storage/databases/main/receipts.py +++ b/synapse/storage/databases/main/receipts.py @@ -460,7 +460,7 @@ def _invalidate_get_users_with_receipts_in_room( def invalidate_caches_for_receipt(self, room_id, receipt_type, user_id): self.get_receipts_for_user.invalidate((user_id, receipt_type)) - self._get_linearized_receipts_for_room.invalidate_many((room_id,)) + self._get_linearized_receipts_for_room.invalidate((room_id,)) self.get_last_receipt_event_id_for_user.invalidate( (user_id, room_id, receipt_type) ) @@ -659,9 +659,7 @@ def insert_graph_receipt_txn( ) txn.call_after(self.get_receipts_for_user.invalidate, (user_id, receipt_type)) # FIXME: This shouldn't invalidate the whole cache - txn.call_after( - self._get_linearized_receipts_for_room.invalidate_many, (room_id,) - ) + txn.call_after(self._get_linearized_receipts_for_room.invalidate, (room_id,)) self.db_pool.simple_delete_txn( txn, diff --git a/synapse/util/caches/deferred_cache.py b/synapse/util/caches/deferred_cache.py index 371e7e4dd0..1044139119 100644 --- a/synapse/util/caches/deferred_cache.py +++ b/synapse/util/caches/deferred_cache.py @@ -16,16 +16,7 @@ import enum import threading -from typing import ( - Callable, - Generic, - Iterable, - MutableMapping, - Optional, - TypeVar, - Union, - cast, -) +from typing import Callable, Generic, Iterable, MutableMapping, Optional, TypeVar, Union from prometheus_client import Gauge @@ -91,7 +82,7 @@ def __init__( # _pending_deferred_cache maps from the key value to a `CacheEntry` object. self._pending_deferred_cache = ( cache_type() - ) # type: MutableMapping[KT, CacheEntry] + ) # type: Union[TreeCache, MutableMapping[KT, CacheEntry]] def metrics_cb(): cache_pending_metric.labels(name).set(len(self._pending_deferred_cache)) @@ -287,8 +278,17 @@ def prefill( self.cache.set(key, value, callbacks=callbacks) def invalidate(self, key): + """Delete a key, or tree of entries + + If the cache is backed by a regular dict, then "key" must be of + the right type for this cache + + If the cache is backed by a TreeCache, then "key" must be a tuple, but + may be of lower cardinality than the TreeCache - in which case the whole + subtree is deleted. + """ self.check_thread() - self.cache.pop(key, None) + self.cache.del_multi(key) # if we have a pending lookup for this key, remove it from the # _pending_deferred_cache, which will (a) stop it being returned @@ -299,20 +299,10 @@ def invalidate(self, key): # run the invalidation callbacks now, rather than waiting for the # deferred to resolve. if entry: - entry.invalidate() - - def invalidate_many(self, key: KT): - self.check_thread() - if not isinstance(key, tuple): - raise TypeError("The cache key must be a tuple not %r" % (type(key),)) - key = cast(KT, key) - self.cache.del_multi(key) - - # if we have a pending lookup for this key, remove it from the - # _pending_deferred_cache, as above - entry_dict = self._pending_deferred_cache.pop(key, None) - if entry_dict is not None: - for entry in iterate_tree_cache_entry(entry_dict): + # _pending_deferred_cache.pop should either return a CacheEntry, or, in the + # case of a TreeCache, a dict of keys to cache entries. Either way calling + # iterate_tree_cache_entry on it will do the right thing. + for entry in iterate_tree_cache_entry(entry): entry.invalidate() def invalidate_all(self): diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index 2ac24a2f25..d77e8edeea 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -48,7 +48,6 @@ class _CachedFunction(Generic[F]): invalidate = None # type: Any invalidate_all = None # type: Any - invalidate_many = None # type: Any prefill = None # type: Any cache = None # type: Any num_args = None # type: Any @@ -262,6 +261,11 @@ def __init__( ): super().__init__(orig, num_args=num_args, cache_context=cache_context) + if tree and self.num_args < 2: + raise RuntimeError( + "tree=True is nonsensical for cached functions with a single parameter" + ) + self.max_entries = max_entries self.tree = tree self.iterable = iterable @@ -302,11 +306,11 @@ def _wrapped(*args, **kwargs): wrapped = cast(_CachedFunction, _wrapped) if self.num_args == 1: + assert not self.tree wrapped.invalidate = lambda key: cache.invalidate(key[0]) wrapped.prefill = lambda key, val: cache.prefill(key[0], val) else: wrapped.invalidate = cache.invalidate - wrapped.invalidate_many = cache.invalidate_many wrapped.prefill = cache.prefill wrapped.invalidate_all = cache.invalidate_all diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index 54df407ff7..d89e9d9b1d 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -152,7 +152,6 @@ class LruCache(Generic[KT, VT]): """ Least-recently-used cache, supporting prometheus metrics and invalidation callbacks. - Supports del_multi only if cache_type=TreeCache If cache_type=TreeCache, all keys must be tuples. """ @@ -393,10 +392,16 @@ def cache_pop(key: KT, default: Optional[T] = None): @synchronized def cache_del_multi(key: KT) -> None: + """Delete an entry, or tree of entries + + If the LruCache is backed by a regular dict, then "key" must be of + the right type for this cache + + If the LruCache is backed by a TreeCache, then "key" must be a tuple, but + may be of lower cardinality than the TreeCache - in which case the whole + subtree is deleted. """ - This will only work if constructed with cache_type=TreeCache - """ - popped = cache.pop(key) + popped = cache.pop(key, None) if popped is None: return # for each deleted node, we now need to remove it from the linked list @@ -430,11 +435,10 @@ def cache_contains(key: KT) -> bool: self.set = cache_set self.setdefault = cache_set_default self.pop = cache_pop + self.del_multi = cache_del_multi # `invalidate` is exposed for consistency with DeferredCache, so that it can be # invalidated by the cache invalidation replication stream. - self.invalidate = cache_pop - if cache_type is TreeCache: - self.del_multi = cache_del_multi + self.invalidate = cache_del_multi self.len = synchronized(cache_len) self.contains = cache_contains self.clear = cache_clear diff --git a/synapse/util/caches/treecache.py b/synapse/util/caches/treecache.py index 73502a8b06..a6df81ebff 100644 --- a/synapse/util/caches/treecache.py +++ b/synapse/util/caches/treecache.py @@ -89,6 +89,9 @@ def pop(self, key, default=None): value. If the key is partial, the TreeCacheNode corresponding to the part of the tree that was removed. """ + if not isinstance(key, tuple): + raise TypeError("The cache key must be a tuple not %r" % (type(key),)) + # a list of the nodes we have touched on the way down the tree nodes = [] diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index bbbc276697..0277998cbe 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -622,17 +622,17 @@ def func2(self, key, cache_context): self.assertEquals(callcount2[0], 1) a.func2.invalidate(("foo",)) - self.assertEquals(a.func2.cache.cache.pop.call_count, 1) + self.assertEquals(a.func2.cache.cache.del_multi.call_count, 1) yield a.func2("foo") a.func2.invalidate(("foo",)) - self.assertEquals(a.func2.cache.cache.pop.call_count, 2) + self.assertEquals(a.func2.cache.cache.del_multi.call_count, 2) self.assertEquals(callcount[0], 1) self.assertEquals(callcount2[0], 2) a.func.invalidate(("foo",)) - self.assertEquals(a.func2.cache.cache.pop.call_count, 3) + self.assertEquals(a.func2.cache.cache.del_multi.call_count, 3) yield a.func("foo") self.assertEquals(callcount[0], 2) From fe5dad46b0da00e9757ed54eb23304ed3c6ceadf Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 27 May 2021 10:34:24 +0100 Subject: [PATCH 201/619] Remove redundant code to reload tls cert (#10054) we don't need to reload the tls cert if we don't have any tls listeners. Follow-up to #9280. --- changelog.d/10054.misc | 1 + synapse/app/_base.py | 5 +---- synapse/config/tls.py | 22 +++------------------- tests/config/test_tls.py | 3 +-- 4 files changed, 6 insertions(+), 25 deletions(-) create mode 100644 changelog.d/10054.misc diff --git a/changelog.d/10054.misc b/changelog.d/10054.misc new file mode 100644 index 0000000000..cebe39ce54 --- /dev/null +++ b/changelog.d/10054.misc @@ -0,0 +1 @@ +Remove some dead code regarding TLS certificate handling. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 59918d789e..1329af2e2b 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -261,13 +261,10 @@ def refresh_certificate(hs): Refresh the TLS certificates that Synapse is using by re-reading them from disk and updating the TLS context factories to use them. """ - if not hs.config.has_tls_listener(): - # attempt to reload the certs for the good of the tls_fingerprints - hs.config.read_certificate_from_disk(require_cert_and_key=False) return - hs.config.read_certificate_from_disk(require_cert_and_key=True) + hs.config.read_certificate_from_disk() hs.tls_server_context_factory = context_factory.ServerContextFactory(hs.config) if hs._listening_services: diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 26f1150ca5..0e9bba53c9 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -215,28 +215,12 @@ def is_disk_cert_valid(self, allow_self_signed=True): days_remaining = (expires_on - now).days return days_remaining - def read_certificate_from_disk(self, require_cert_and_key: bool): + def read_certificate_from_disk(self): """ Read the certificates and private key from disk. - - Args: - require_cert_and_key: set to True to throw an error if the certificate - and key file are not given """ - if require_cert_and_key: - self.tls_private_key = self.read_tls_private_key() - self.tls_certificate = self.read_tls_certificate() - elif self.tls_certificate_file: - # we only need the certificate for the tls_fingerprints. Reload it if we - # can, but it's not a fatal error if we can't. - try: - self.tls_certificate = self.read_tls_certificate() - except Exception as e: - logger.info( - "Unable to read TLS certificate (%s). Ignoring as no " - "tls listeners enabled.", - e, - ) + self.tls_private_key = self.read_tls_private_key() + self.tls_certificate = self.read_tls_certificate() def generate_config_section( self, diff --git a/tests/config/test_tls.py b/tests/config/test_tls.py index 183034f7d4..dcf336416c 100644 --- a/tests/config/test_tls.py +++ b/tests/config/test_tls.py @@ -74,12 +74,11 @@ def test_warn_self_signed(self): config = { "tls_certificate_path": os.path.join(config_dir, "cert.pem"), - "tls_fingerprints": [], } t = TestConfig() t.read_config(config, config_dir_path="", data_dir_path="") - t.read_certificate_from_disk(require_cert_and_key=False) + t.read_tls_certificate() warnings = self.flushWarnings() self.assertEqual(len(warnings), 1) From 5447a763327c37f07cd4135418e991a3b4346896 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 27 May 2021 10:34:55 +0100 Subject: [PATCH 202/619] Remove redundant, unmaintained `convert_server_keys` script. (#10055) --- changelog.d/10055.misc | 1 + scripts-dev/convert_server_keys.py | 108 ----------------------------- 2 files changed, 1 insertion(+), 108 deletions(-) create mode 100644 changelog.d/10055.misc delete mode 100644 scripts-dev/convert_server_keys.py diff --git a/changelog.d/10055.misc b/changelog.d/10055.misc new file mode 100644 index 0000000000..da84a2dde8 --- /dev/null +++ b/changelog.d/10055.misc @@ -0,0 +1 @@ +Remove redundant, unmaintained `convert_server_keys` script. diff --git a/scripts-dev/convert_server_keys.py b/scripts-dev/convert_server_keys.py deleted file mode 100644 index d4314a054c..0000000000 --- a/scripts-dev/convert_server_keys.py +++ /dev/null @@ -1,108 +0,0 @@ -import json -import sys -import time - -import psycopg2 -import yaml -from canonicaljson import encode_canonical_json -from signedjson.key import read_signing_keys -from signedjson.sign import sign_json -from unpaddedbase64 import encode_base64 - -db_binary_type = memoryview - - -def select_v1_keys(connection): - cursor = connection.cursor() - cursor.execute("SELECT server_name, key_id, verify_key FROM server_signature_keys") - rows = cursor.fetchall() - cursor.close() - results = {} - for server_name, key_id, verify_key in rows: - results.setdefault(server_name, {})[key_id] = encode_base64(verify_key) - return results - - -def select_v1_certs(connection): - cursor = connection.cursor() - cursor.execute("SELECT server_name, tls_certificate FROM server_tls_certificates") - rows = cursor.fetchall() - cursor.close() - results = {} - for server_name, tls_certificate in rows: - results[server_name] = tls_certificate - return results - - -def select_v2_json(connection): - cursor = connection.cursor() - cursor.execute("SELECT server_name, key_id, key_json FROM server_keys_json") - rows = cursor.fetchall() - cursor.close() - results = {} - for server_name, key_id, key_json in rows: - results.setdefault(server_name, {})[key_id] = json.loads( - str(key_json).decode("utf-8") - ) - return results - - -def convert_v1_to_v2(server_name, valid_until, keys, certificate): - return { - "old_verify_keys": {}, - "server_name": server_name, - "verify_keys": {key_id: {"key": key} for key_id, key in keys.items()}, - "valid_until_ts": valid_until, - } - - -def rows_v2(server, json): - valid_until = json["valid_until_ts"] - key_json = encode_canonical_json(json) - for key_id in json["verify_keys"]: - yield (server, key_id, "-", valid_until, valid_until, db_binary_type(key_json)) - - -def main(): - config = yaml.safe_load(open(sys.argv[1])) - valid_until = int(time.time() / (3600 * 24)) * 1000 * 3600 * 24 - - server_name = config["server_name"] - signing_key = read_signing_keys(open(config["signing_key_path"]))[0] - - database = config["database"] - assert database["name"] == "psycopg2", "Can only convert for postgresql" - args = database["args"] - args.pop("cp_max") - args.pop("cp_min") - connection = psycopg2.connect(**args) - keys = select_v1_keys(connection) - certificates = select_v1_certs(connection) - json = select_v2_json(connection) - - result = {} - for server in keys: - if server not in json: - v2_json = convert_v1_to_v2( - server, valid_until, keys[server], certificates[server] - ) - v2_json = sign_json(v2_json, server_name, signing_key) - result[server] = v2_json - - yaml.safe_dump(result, sys.stdout, default_flow_style=False) - - rows = [row for server, json in result.items() for row in rows_v2(server, json)] - - cursor = connection.cursor() - cursor.executemany( - "INSERT INTO server_keys_json (" - " server_name, key_id, from_server," - " ts_added_ms, ts_valid_until_ms, key_json" - ") VALUES (%s, %s, %s, %s, %s, %s)", - rows, - ) - connection.commit() - - -if __name__ == "__main__": - main() From dcbfec919ba27da970849cae73c69cacd78432d5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 27 May 2021 10:35:06 +0100 Subject: [PATCH 203/619] Improve the error message printed by synctl when synapse fails to start. (#10059) --- changelog.d/10059.misc | 1 + synctl | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 changelog.d/10059.misc diff --git a/changelog.d/10059.misc b/changelog.d/10059.misc new file mode 100644 index 0000000000..ca6e0e8a5a --- /dev/null +++ b/changelog.d/10059.misc @@ -0,0 +1 @@ +Improve the error message printed by synctl when synapse fails to start. diff --git a/synctl b/synctl index 6ce19918d2..90559ded62 100755 --- a/synctl +++ b/synctl @@ -97,11 +97,15 @@ def start(pidfile: str, app: str, config_files: Iterable[str], daemonize: bool) write("started %s(%s)" % (app, ",".join(config_files)), colour=GREEN) return True except subprocess.CalledProcessError as e: - write( - "error starting %s(%s) (exit code: %d); see above for logs" - % (app, ",".join(config_files), e.returncode), - colour=RED, + err = "%s(%s) failed to start (exit code: %d). Check the Synapse logfile" % ( + app, + ",".join(config_files), + e.returncode, ) + if daemonize: + err += ", or run synctl with --no-daemonize" + err += "." + write(err, colour=RED, stream=sys.stderr) return False From d9f44fd0b9214e09caa9c8dd46e651bbf0fffad8 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Thu, 27 May 2021 11:41:16 +0000 Subject: [PATCH 204/619] Clarify security note regarding the domain Synapse is hosted on. (#9221) --- README.rst | 46 +++++++++++++++++++++++++++++++++----------- changelog.d/9221.doc | 1 + 2 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 changelog.d/9221.doc diff --git a/README.rst b/README.rst index 1a5503572e..a14a687fd1 100644 --- a/README.rst +++ b/README.rst @@ -149,21 +149,45 @@ For details on having Synapse manage your federation TLS certificates automatically, please see ``_. -Security Note +Security note ============= -Matrix serves raw user generated data in some APIs - specifically the `content -repository endpoints `_. +Matrix serves raw, user-supplied data in some APIs -- specifically the `content +repository endpoints`_. -Whilst we have tried to mitigate against possible XSS attacks (e.g. -https://github.com/matrix-org/synapse/pull/1021) we recommend running -matrix homeservers on a dedicated domain name, to limit any malicious user generated -content served to web browsers a matrix API from being able to attack webapps hosted -on the same domain. This is particularly true of sharing a matrix webclient and -server on the same domain. +.. _content repository endpoints: https://matrix.org/docs/spec/client_server/latest.html#get-matrix-media-r0-download-servername-mediaid -See https://github.com/vector-im/riot-web/issues/1977 and -https://developer.github.com/changes/2014-04-25-user-content-security for more details. +Whilst we make a reasonable effort to mitigate against XSS attacks (for +instance, by using `CSP`_), a Matrix homeserver should not be hosted on a +domain hosting other web applications. This especially applies to sharing +the domain with Matrix web clients and other sensitive applications like +webmail. See +https://developer.github.com/changes/2014-04-25-user-content-security for more +information. + +.. _CSP: https://github.com/matrix-org/synapse/pull/1021 + +Ideally, the homeserver should not simply be on a different subdomain, but on +a completely different `registered domain`_ (also known as top-level site or +eTLD+1). This is because `some attacks`_ are still possible as long as the two +applications share the same registered domain. + +.. _registered domain: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-2.3 + +.. _some attacks: https://en.wikipedia.org/wiki/Session_fixation#Attacks_using_cross-subdomain_cookie + +To illustrate this with an example, if your Element Web or other sensitive web +application is hosted on ``A.example1.com``, you should ideally host Synapse on +``example2.com``. Some amount of protection is offered by hosting on +``B.example1.com`` instead, so this is also acceptable in some scenarios. +However, you should *not* host your Synapse on ``A.example1.com``. + +Note that all of the above refers exclusively to the domain used in Synapse's +``public_baseurl`` setting. In particular, it has no bearing on the domain +mentioned in MXIDs hosted on that server. + +Following this advice ensures that even if an XSS is found in Synapse, the +impact to other applications will be minimal. Upgrading an existing Synapse diff --git a/changelog.d/9221.doc b/changelog.d/9221.doc new file mode 100644 index 0000000000..9b3476064b --- /dev/null +++ b/changelog.d/9221.doc @@ -0,0 +1 @@ +Clarify security note regarding hosting Synapse on the same domain as other web applications. From 8e15c92c2f9e581e59ff68495fa99e998849bb4d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 27 May 2021 08:52:28 -0400 Subject: [PATCH 205/619] Pass the origin when calculating the spaces summary over GET. (#10079) Fixes a bug due to conflicting PRs which were merged. (One added a new caller to a method, the other added a new parameter to the same method.) --- changelog.d/10079.bugfix | 1 + synapse/federation/transport/server.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10079.bugfix diff --git a/changelog.d/10079.bugfix b/changelog.d/10079.bugfix new file mode 100644 index 0000000000..2b93c4534a --- /dev/null +++ b/changelog.d/10079.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.35.0rc1 when calling the spaces summary API via a GET request. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 9d50b05d01..40eab45549 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1398,7 +1398,7 @@ async def on_GET( ) return 200, await self.handler.federation_space_summary( - room_id, suggested_only, max_rooms_per_space, exclude_rooms + origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms ) # TODO When switching to the stable endpoint, remove the POST handler. From 78b5102ae71f828deb851eca8e677381710bf716 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 27 May 2021 14:32:31 +0100 Subject: [PATCH 206/619] Fix up `BatchingQueue` (#10078) Fixes #10068 --- changelog.d/10078.misc | 1 + synapse/util/batching_queue.py | 70 ++++++++++++++++++--------- tests/util/test_batching_queue.py | 78 ++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 changelog.d/10078.misc diff --git a/changelog.d/10078.misc b/changelog.d/10078.misc new file mode 100644 index 0000000000..a4b089d0fd --- /dev/null +++ b/changelog.d/10078.misc @@ -0,0 +1 @@ +Fix up `BatchingQueue` implementation. diff --git a/synapse/util/batching_queue.py b/synapse/util/batching_queue.py index 44bbb7b1a8..8fd5bfb69b 100644 --- a/synapse/util/batching_queue.py +++ b/synapse/util/batching_queue.py @@ -25,10 +25,11 @@ TypeVar, ) +from prometheus_client import Gauge + from twisted.internet import defer from synapse.logging.context import PreserveLoggingContext, make_deferred_yieldable -from synapse.metrics import LaterGauge from synapse.metrics.background_process_metrics import run_as_background_process from synapse.util import Clock @@ -38,6 +39,24 @@ V = TypeVar("V") R = TypeVar("R") +number_queued = Gauge( + "synapse_util_batching_queue_number_queued", + "The number of items waiting in the queue across all keys", + labelnames=("name",), +) + +number_in_flight = Gauge( + "synapse_util_batching_queue_number_pending", + "The number of items across all keys either being processed or waiting in a queue", + labelnames=("name",), +) + +number_of_keys = Gauge( + "synapse_util_batching_queue_number_of_keys", + "The number of distinct keys that have items queued", + labelnames=("name",), +) + class BatchingQueue(Generic[V, R]): """A queue that batches up work, calling the provided processing function @@ -48,10 +67,20 @@ class BatchingQueue(Generic[V, R]): called, and will keep being called until the queue has been drained (for the given key). + If the processing function raises an exception then the exception is proxied + through to the callers waiting on that batch of work. + Note that the return value of `add_to_queue` will be the return value of the processing function that processed the given item. This means that the returned value will likely include data for other items that were in the batch. + + Args: + name: A name for the queue, used for logging contexts and metrics. + This must be unique, otherwise the metrics will be wrong. + clock: The clock to use to schedule work. + process_batch_callback: The callback to to be run to process a batch of + work. """ def __init__( @@ -73,19 +102,15 @@ def __init__( # The function to call with batches of values. self._process_batch_callback = process_batch_callback - LaterGauge( - "synapse_util_batching_queue_number_queued", - "The number of items waiting in the queue across all keys", - labels=("name",), - caller=lambda: sum(len(v) for v in self._next_values.values()), + number_queued.labels(self._name).set_function( + lambda: sum(len(q) for q in self._next_values.values()) ) - LaterGauge( - "synapse_util_batching_queue_number_of_keys", - "The number of distinct keys that have items queued", - labels=("name",), - caller=lambda: len(self._next_values), - ) + number_of_keys.labels(self._name).set_function(lambda: len(self._next_values)) + + self._number_in_flight_metric = number_in_flight.labels( + self._name + ) # type: Gauge async def add_to_queue(self, value: V, key: Hashable = ()) -> R: """Adds the value to the queue with the given key, returning the result @@ -107,17 +132,18 @@ async def add_to_queue(self, value: V, key: Hashable = ()) -> R: if key not in self._processing_keys: run_as_background_process(self._name, self._process_queue, key) - return await make_deferred_yieldable(d) + with self._number_in_flight_metric.track_inprogress(): + return await make_deferred_yieldable(d) async def _process_queue(self, key: Hashable) -> None: """A background task to repeatedly pull things off the queue for the given key and call the `self._process_batch_callback` with the values. """ - try: - if key in self._processing_keys: - return + if key in self._processing_keys: + return + try: self._processing_keys.add(key) while True: @@ -137,16 +163,16 @@ async def _process_queue(self, key: Hashable) -> None: values = [value for value, _ in next_values] results = await self._process_batch_callback(values) - for _, deferred in next_values: - with PreserveLoggingContext(): + with PreserveLoggingContext(): + for _, deferred in next_values: deferred.callback(results) except Exception as e: - for _, deferred in next_values: - if deferred.called: - continue + with PreserveLoggingContext(): + for _, deferred in next_values: + if deferred.called: + continue - with PreserveLoggingContext(): deferred.errback(e) finally: diff --git a/tests/util/test_batching_queue.py b/tests/util/test_batching_queue.py index 5def1e56c9..edf29e5b96 100644 --- a/tests/util/test_batching_queue.py +++ b/tests/util/test_batching_queue.py @@ -14,7 +14,12 @@ from twisted.internet import defer from synapse.logging.context import make_deferred_yieldable -from synapse.util.batching_queue import BatchingQueue +from synapse.util.batching_queue import ( + BatchingQueue, + number_in_flight, + number_of_keys, + number_queued, +) from tests.server import get_clock from tests.unittest import TestCase @@ -24,6 +29,14 @@ class BatchingQueueTestCase(TestCase): def setUp(self): self.clock, hs_clock = get_clock() + # We ensure that we remove any existing metrics for "test_queue". + try: + number_queued.remove("test_queue") + number_of_keys.remove("test_queue") + number_in_flight.remove("test_queue") + except KeyError: + pass + self._pending_calls = [] self.queue = BatchingQueue("test_queue", hs_clock, self._process_queue) @@ -32,6 +45,41 @@ async def _process_queue(self, values): self._pending_calls.append((values, d)) return await make_deferred_yieldable(d) + def _assert_metrics(self, queued, keys, in_flight): + """Assert that the metrics are correct""" + + self.assertEqual(len(number_queued.collect()), 1) + self.assertEqual(len(number_queued.collect()[0].samples), 1) + self.assertEqual( + number_queued.collect()[0].samples[0].labels, + {"name": self.queue._name}, + ) + self.assertEqual( + number_queued.collect()[0].samples[0].value, + queued, + "number_queued", + ) + + self.assertEqual(len(number_of_keys.collect()), 1) + self.assertEqual(len(number_of_keys.collect()[0].samples), 1) + self.assertEqual( + number_queued.collect()[0].samples[0].labels, {"name": self.queue._name} + ) + self.assertEqual( + number_of_keys.collect()[0].samples[0].value, keys, "number_of_keys" + ) + + self.assertEqual(len(number_in_flight.collect()), 1) + self.assertEqual(len(number_in_flight.collect()[0].samples), 1) + self.assertEqual( + number_queued.collect()[0].samples[0].labels, {"name": self.queue._name} + ) + self.assertEqual( + number_in_flight.collect()[0].samples[0].value, + in_flight, + "number_in_flight", + ) + def test_simple(self): """Tests the basic case of calling `add_to_queue` once and having `_process_queue` return. @@ -41,6 +89,8 @@ def test_simple(self): queue_d = defer.ensureDeferred(self.queue.add_to_queue("foo")) + self._assert_metrics(queued=1, keys=1, in_flight=1) + # The queue should wait a reactor tick before calling the processing # function. self.assertFalse(self._pending_calls) @@ -52,12 +102,15 @@ def test_simple(self): self.assertEqual(len(self._pending_calls), 1) self.assertEqual(self._pending_calls[0][0], ["foo"]) self.assertFalse(queue_d.called) + self._assert_metrics(queued=0, keys=0, in_flight=1) # Return value of the `_process_queue` should be propagated back. self._pending_calls.pop()[1].callback("bar") self.assertEqual(self.successResultOf(queue_d), "bar") + self._assert_metrics(queued=0, keys=0, in_flight=0) + def test_batching(self): """Test that multiple calls at the same time get batched up into one call to `_process_queue`. @@ -68,6 +121,8 @@ def test_batching(self): queue_d1 = defer.ensureDeferred(self.queue.add_to_queue("foo1")) queue_d2 = defer.ensureDeferred(self.queue.add_to_queue("foo2")) + self._assert_metrics(queued=2, keys=1, in_flight=2) + self.clock.pump([0]) # We should see only *one* call to `_process_queue` @@ -75,12 +130,14 @@ def test_batching(self): self.assertEqual(self._pending_calls[0][0], ["foo1", "foo2"]) self.assertFalse(queue_d1.called) self.assertFalse(queue_d2.called) + self._assert_metrics(queued=0, keys=0, in_flight=2) # Return value of the `_process_queue` should be propagated back to both. self._pending_calls.pop()[1].callback("bar") self.assertEqual(self.successResultOf(queue_d1), "bar") self.assertEqual(self.successResultOf(queue_d2), "bar") + self._assert_metrics(queued=0, keys=0, in_flight=0) def test_queuing(self): """Test that we queue up requests while a `_process_queue` is being @@ -92,13 +149,20 @@ def test_queuing(self): queue_d1 = defer.ensureDeferred(self.queue.add_to_queue("foo1")) self.clock.pump([0]) + self.assertEqual(len(self._pending_calls), 1) + + # We queue up work after the process function has been called, testing + # that they get correctly queued up. queue_d2 = defer.ensureDeferred(self.queue.add_to_queue("foo2")) + queue_d3 = defer.ensureDeferred(self.queue.add_to_queue("foo3")) # We should see only *one* call to `_process_queue` self.assertEqual(len(self._pending_calls), 1) self.assertEqual(self._pending_calls[0][0], ["foo1"]) self.assertFalse(queue_d1.called) self.assertFalse(queue_d2.called) + self.assertFalse(queue_d3.called) + self._assert_metrics(queued=2, keys=1, in_flight=3) # Return value of the `_process_queue` should be propagated back to the # first. @@ -106,18 +170,24 @@ def test_queuing(self): self.assertEqual(self.successResultOf(queue_d1), "bar1") self.assertFalse(queue_d2.called) + self.assertFalse(queue_d3.called) + self._assert_metrics(queued=2, keys=1, in_flight=2) # We should now see a second call to `_process_queue` self.clock.pump([0]) self.assertEqual(len(self._pending_calls), 1) - self.assertEqual(self._pending_calls[0][0], ["foo2"]) + self.assertEqual(self._pending_calls[0][0], ["foo2", "foo3"]) self.assertFalse(queue_d2.called) + self.assertFalse(queue_d3.called) + self._assert_metrics(queued=0, keys=0, in_flight=2) # Return value of the `_process_queue` should be propagated back to the # second. self._pending_calls.pop()[1].callback("bar2") self.assertEqual(self.successResultOf(queue_d2), "bar2") + self.assertEqual(self.successResultOf(queue_d3), "bar2") + self._assert_metrics(queued=0, keys=0, in_flight=0) def test_different_keys(self): """Test that calls to different keys get processed in parallel.""" @@ -140,6 +210,7 @@ def test_different_keys(self): self.assertFalse(queue_d1.called) self.assertFalse(queue_d2.called) self.assertFalse(queue_d3.called) + self._assert_metrics(queued=1, keys=1, in_flight=3) # Return value of the `_process_queue` should be propagated back to the # first. @@ -148,6 +219,7 @@ def test_different_keys(self): self.assertEqual(self.successResultOf(queue_d1), "bar1") self.assertFalse(queue_d2.called) self.assertFalse(queue_d3.called) + self._assert_metrics(queued=1, keys=1, in_flight=2) # Return value of the `_process_queue` should be propagated back to the # second. @@ -161,9 +233,11 @@ def test_different_keys(self): self.assertEqual(len(self._pending_calls), 1) self.assertEqual(self._pending_calls[0][0], ["foo3"]) self.assertFalse(queue_d3.called) + self._assert_metrics(queued=0, keys=0, in_flight=1) # Return value of the `_process_queue` should be propagated back to the # third deferred. self._pending_calls.pop()[1].callback("bar4") self.assertEqual(self.successResultOf(queue_d3), "bar4") + self._assert_metrics(queued=0, keys=0, in_flight=0) From b1bc26a909f4f9af137e72ab27952a8d2dfc1cb3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 27 May 2021 14:46:24 +0100 Subject: [PATCH 207/619] 1.35.0rc2 --- CHANGES.md | 9 +++++++++ changelog.d/10079.bugfix | 1 - synapse/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/10079.bugfix diff --git a/CHANGES.md b/CHANGES.md index 0e451f983c..1fac16580d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.35.0rc2 (2021-05-27) +============================== + +Bugfixes +-------- + +- Fix a bug introduced in v1.35.0rc1 when calling the spaces summary API via a GET request. ([\#10079](https://github.com/matrix-org/synapse/issues/10079)) + + Synapse 1.35.0rc1 (2021-05-25) ============================== diff --git a/changelog.d/10079.bugfix b/changelog.d/10079.bugfix deleted file mode 100644 index 2b93c4534a..0000000000 --- a/changelog.d/10079.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.35.0rc1 when calling the spaces summary API via a GET request. diff --git a/synapse/__init__.py b/synapse/__init__.py index e60e9db71e..6faf31dbbc 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.35.0rc1" +__version__ = "1.35.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From f828a70be331105c98ebfbe3738ef57d9d54df5b Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 27 May 2021 18:10:58 +0200 Subject: [PATCH 208/619] Limit the number of events sent over replication when persisting events. (#10082) --- changelog.d/10082.bugfix | 1 + synapse/handlers/federation.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changelog.d/10082.bugfix diff --git a/changelog.d/10082.bugfix b/changelog.d/10082.bugfix new file mode 100644 index 0000000000..b4f8bcc4fa --- /dev/null +++ b/changelog.d/10082.bugfix @@ -0,0 +1 @@ +Fixed a bug causing replication requests to fail when receiving a lot of events via federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 678f6b7707..bf11315251 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -91,6 +91,7 @@ get_domain_from_id, ) from synapse.util.async_helpers import Linearizer, concurrently_execute +from synapse.util.iterutils import batch_iter from synapse.util.retryutils import NotRetryingDestination from synapse.util.stringutils import shortstr from synapse.visibility import filter_events_for_server @@ -3053,13 +3054,15 @@ async def persist_events_and_notify( """ instance = self.config.worker.events_shard_config.get_instance(room_id) if instance != self._instance_name: - result = await self._send_events( - instance_name=instance, - store=self.store, - room_id=room_id, - event_and_contexts=event_and_contexts, - backfilled=backfilled, - ) + # Limit the number of events sent over federation. + for batch in batch_iter(event_and_contexts, 1000): + result = await self._send_events( + instance_name=instance, + store=self.store, + room_id=room_id, + event_and_contexts=batch, + backfilled=backfilled, + ) return result["max_stream_id"] else: assert self.storage.persistence From 8fb9af570f942d2057e8acb4a047d61ed7048f58 Mon Sep 17 00:00:00 2001 From: Callum Brown Date: Thu, 27 May 2021 18:42:23 +0100 Subject: [PATCH 209/619] Make reason and score optional for report_event (#10077) Implements MSC2414: https://github.com/matrix-org/matrix-doc/pull/2414 See #8551 Signed-off-by: Callum Brown --- changelog.d/10077.feature | 1 + docs/admin_api/event_reports.md | 4 +- synapse/rest/client/v2_alpha/report_event.py | 13 +-- synapse/storage/databases/main/room.py | 2 +- tests/rest/admin/test_event_reports.py | 15 +++- .../rest/client/v2_alpha/test_report_event.py | 83 +++++++++++++++++++ 6 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 changelog.d/10077.feature create mode 100644 tests/rest/client/v2_alpha/test_report_event.py diff --git a/changelog.d/10077.feature b/changelog.d/10077.feature new file mode 100644 index 0000000000..808feb2215 --- /dev/null +++ b/changelog.d/10077.feature @@ -0,0 +1 @@ +Make reason and score parameters optional for reporting content. Implements [MSC2414](https://github.com/matrix-org/matrix-doc/pull/2414). Contributed by Callum Brown. diff --git a/docs/admin_api/event_reports.md b/docs/admin_api/event_reports.md index 0159098138..bfec06f755 100644 --- a/docs/admin_api/event_reports.md +++ b/docs/admin_api/event_reports.md @@ -75,9 +75,9 @@ The following fields are returned in the JSON response body: * `name`: string - The name of the room. * `event_id`: string - The ID of the reported event. * `user_id`: string - This is the user who reported the event and wrote the reason. -* `reason`: string - Comment made by the `user_id` in this report. May be blank. +* `reason`: string - Comment made by the `user_id` in this report. May be blank or `null`. * `score`: integer - Content is reported based upon a negative score, where -100 is - "most offensive" and 0 is "inoffensive". + "most offensive" and 0 is "inoffensive". May be `null`. * `sender`: string - This is the ID of the user who sent the original message/event that was reported. * `canonical_alias`: string - The canonical alias of the room. `null` if the room does not diff --git a/synapse/rest/client/v2_alpha/report_event.py b/synapse/rest/client/v2_alpha/report_event.py index 2c169abbf3..07ea39a8a3 100644 --- a/synapse/rest/client/v2_alpha/report_event.py +++ b/synapse/rest/client/v2_alpha/report_event.py @@ -16,11 +16,7 @@ from http import HTTPStatus from synapse.api.errors import Codes, SynapseError -from synapse.http.servlet import ( - RestServlet, - assert_params_in_dict, - parse_json_object_from_request, -) +from synapse.http.servlet import RestServlet, parse_json_object_from_request from ._base import client_patterns @@ -42,15 +38,14 @@ async def on_POST(self, request, room_id, event_id): user_id = requester.user.to_string() body = parse_json_object_from_request(request) - assert_params_in_dict(body, ("reason", "score")) - if not isinstance(body["reason"], str): + if not isinstance(body.get("reason", ""), str): raise SynapseError( HTTPStatus.BAD_REQUEST, "Param 'reason' must be a string", Codes.BAD_JSON, ) - if not isinstance(body["score"], int): + if not isinstance(body.get("score", 0), int): raise SynapseError( HTTPStatus.BAD_REQUEST, "Param 'score' must be an integer", @@ -61,7 +56,7 @@ async def on_POST(self, request, room_id, event_id): room_id=room_id, event_id=event_id, user_id=user_id, - reason=body["reason"], + reason=body.get("reason"), content=body, received_ts=self.clock.time_msec(), ) diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 5f38634f48..0cf450f81d 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -1498,7 +1498,7 @@ async def add_event_report( room_id: str, event_id: str, user_id: str, - reason: str, + reason: Optional[str], content: JsonDict, received_ts: int, ) -> None: diff --git a/tests/rest/admin/test_event_reports.py b/tests/rest/admin/test_event_reports.py index 29341bc6e9..f15d1cf6f7 100644 --- a/tests/rest/admin/test_event_reports.py +++ b/tests/rest/admin/test_event_reports.py @@ -64,7 +64,7 @@ def prepare(self, reactor, clock, hs): user_tok=self.admin_user_tok, ) for _ in range(5): - self._create_event_and_report( + self._create_event_and_report_without_parameters( room_id=self.room_id2, user_tok=self.admin_user_tok, ) @@ -378,6 +378,19 @@ def _create_event_and_report(self, room_id, user_tok): ) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + def _create_event_and_report_without_parameters(self, room_id, user_tok): + """Create and report an event, but omit reason and score""" + resp = self.helper.send(room_id, tok=user_tok) + event_id = resp["event_id"] + + channel = self.make_request( + "POST", + "rooms/%s/report/%s" % (room_id, event_id), + json.dumps({}), + access_token=user_tok, + ) + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + def _check_fields(self, content): """Checks that all attributes are present in an event report""" for c in content: diff --git a/tests/rest/client/v2_alpha/test_report_event.py b/tests/rest/client/v2_alpha/test_report_event.py new file mode 100644 index 0000000000..1ec6b05e5b --- /dev/null +++ b/tests/rest/client/v2_alpha/test_report_event.py @@ -0,0 +1,83 @@ +# Copyright 2021 Callum Brown +# +# 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 json + +import synapse.rest.admin +from synapse.rest.client.v1 import login, room +from synapse.rest.client.v2_alpha import report_event + +from tests import unittest + + +class ReportEventTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + report_event.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + self.other_user = self.register_user("user", "pass") + self.other_user_tok = self.login("user", "pass") + + self.room_id = self.helper.create_room_as( + self.other_user, tok=self.other_user_tok, is_public=True + ) + self.helper.join(self.room_id, user=self.admin_user, tok=self.admin_user_tok) + resp = self.helper.send(self.room_id, tok=self.admin_user_tok) + self.event_id = resp["event_id"] + self.report_path = "rooms/{}/report/{}".format(self.room_id, self.event_id) + + def test_reason_str_and_score_int(self): + data = {"reason": "this makes me sad", "score": -100} + self._assert_status(200, data) + + def test_no_reason(self): + data = {"score": 0} + self._assert_status(200, data) + + def test_no_score(self): + data = {"reason": "this makes me sad"} + self._assert_status(200, data) + + def test_no_reason_and_no_score(self): + data = {} + self._assert_status(200, data) + + def test_reason_int_and_score_str(self): + data = {"reason": 10, "score": "string"} + self._assert_status(400, data) + + def test_reason_zero_and_score_blank(self): + data = {"reason": 0, "score": ""} + self._assert_status(400, data) + + def test_reason_and_score_null(self): + data = {"reason": None, "score": None} + self._assert_status(400, data) + + def _assert_status(self, response_status, data): + channel = self.make_request( + "POST", + self.report_path, + json.dumps(data), + access_token=self.other_user_tok, + ) + self.assertEqual( + response_status, int(channel.result["code"]), msg=channel.result["body"] + ) From 5eed6348ce747b26883ffe812d69ec4d0fbde5fd Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 27 May 2021 22:45:43 +0100 Subject: [PATCH 210/619] Move some more endpoints off master (#10084) --- changelog.d/10084.feature | 1 + docs/workers.md | 3 +++ synapse/app/generic_worker.py | 4 ++-- synapse/rest/client/v1/room.py | 8 ++++---- 4 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10084.feature diff --git a/changelog.d/10084.feature b/changelog.d/10084.feature new file mode 100644 index 0000000000..602cb6ff51 --- /dev/null +++ b/changelog.d/10084.feature @@ -0,0 +1 @@ +Add support for routing more requests to workers. diff --git a/docs/workers.md b/docs/workers.md index c6282165b0..46b5e4b737 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -228,6 +228,9 @@ expressions: ^/_matrix/client/(api/v1|r0|unstable)/joined_groups$ ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups$ ^/_matrix/client/(api/v1|r0|unstable)/publicised_groups/ + ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/event/ + ^/_matrix/client/(api/v1|r0|unstable)/joined_rooms$ + ^/_matrix/client/(api/v1|r0|unstable)/search$ # Registration/login requests ^/_matrix/client/(api/v1|r0|unstable)/login$ diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 91ad326f19..57c2fc2e88 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -109,7 +109,7 @@ MonthlyActiveUsersWorkerStore, ) from synapse.storage.databases.main.presence import PresenceStore -from synapse.storage.databases.main.search import SearchWorkerStore +from synapse.storage.databases.main.search import SearchStore from synapse.storage.databases.main.stats import StatsStore from synapse.storage.databases.main.transactions import TransactionWorkerStore from synapse.storage.databases.main.ui_auth import UIAuthWorkerStore @@ -242,7 +242,7 @@ class GenericWorkerSlavedStore( MonthlyActiveUsersWorkerStore, MediaRepositoryStore, ServerMetricsStore, - SearchWorkerStore, + SearchStore, TransactionWorkerStore, BaseSlavedStore, ): diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index d6d55893af..70286b0ff7 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -1061,15 +1061,15 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False): RoomTypingRestServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) RoomSpaceSummaryRestServlet(hs).register(http_server) + RoomEventServlet(hs).register(http_server) + JoinedRoomsRestServlet(hs).register(http_server) + RoomAliasListServlet(hs).register(http_server) + SearchRestServlet(hs).register(http_server) # Some servlets only get registered for the main process. if not is_worker: RoomCreateRestServlet(hs).register(http_server) RoomForgetRestServlet(hs).register(http_server) - SearchRestServlet(hs).register(http_server) - JoinedRoomsRestServlet(hs).register(http_server) - RoomEventServlet(hs).register(http_server) - RoomAliasListServlet(hs).register(http_server) def register_deprecated_servlets(hs, http_server): From ac3e02d0892c267aea02c637df9761c5820478e6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 28 May 2021 08:19:06 -0500 Subject: [PATCH 211/619] Add `parse_strings_from_args` to get `prev_events` array (#10048) Split out from https://github.com/matrix-org/synapse/pull/9247 Strings: - `parse_string` - `parse_string_from_args` - `parse_strings_from_args` For comparison with ints: - `parse_integer` - `parse_integer_from_args` Previous discussions: - https://github.com/matrix-org/synapse/pull/9247#discussion_r573195687 - https://github.com/matrix-org/synapse/pull/9247#discussion_r574214156 - https://github.com/matrix-org/synapse/pull/9247#discussion_r573264791 Signed-off-by: Eric Eastwood --- changelog.d/10048.misc | 1 + synapse/http/servlet.py | 196 +++++++++++++++++++++++++++++++--------- 2 files changed, 154 insertions(+), 43 deletions(-) create mode 100644 changelog.d/10048.misc diff --git a/changelog.d/10048.misc b/changelog.d/10048.misc new file mode 100644 index 0000000000..a901f8431e --- /dev/null +++ b/changelog.d/10048.misc @@ -0,0 +1 @@ +Add `parse_strings_from_args` for parsing an array from query parameters. diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 31897546a9..3f4f2411fc 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -15,6 +15,9 @@ """ This module contains base REST classes for constructing REST servlets. """ import logging +from typing import Iterable, List, Optional, Union, overload + +from typing_extensions import Literal from synapse.api.errors import Codes, SynapseError from synapse.util import json_decoder @@ -107,12 +110,11 @@ def parse_boolean_from_args(args, name, default=None, required=False): def parse_string( request, - name, - default=None, - required=False, - allowed_values=None, - param_type="string", - encoding="ascii", + name: Union[bytes, str], + default: Optional[str] = None, + required: bool = False, + allowed_values: Optional[Iterable[str]] = None, + encoding: Optional[str] = "ascii", ): """ Parse a string parameter from the request query string. @@ -122,18 +124,17 @@ def parse_string( Args: request: the twisted HTTP request. - name (bytes|unicode): the name of the query parameter. - default (bytes|unicode|None): value to use if the parameter is absent, + name: the name of the query parameter. + default: value to use if the parameter is absent, defaults to None. Must be bytes if encoding is None. - required (bool): whether to raise a 400 SynapseError if the + required: whether to raise a 400 SynapseError if the parameter is absent, defaults to False. - allowed_values (list[bytes|unicode]): List of allowed values for the + allowed_values: List of allowed values for the string, or None if any value is allowed, defaults to None. Must be the same type as name, if given. - encoding (str|None): The encoding to decode the string content with. - + encoding : The encoding to decode the string content with. Returns: - bytes/unicode|None: A string value or the default. Unicode if encoding + A string value or the default. Unicode if encoding was given, bytes otherwise. Raises: @@ -142,45 +143,105 @@ def parse_string( is not one of those allowed values. """ return parse_string_from_args( - request.args, name, default, required, allowed_values, param_type, encoding + request.args, name, default, required, allowed_values, encoding ) -def parse_string_from_args( - args, - name, - default=None, - required=False, - allowed_values=None, - param_type="string", - encoding="ascii", -): +def _parse_string_value( + value: Union[str, bytes], + allowed_values: Optional[Iterable[str]], + name: str, + encoding: Optional[str], +) -> Union[str, bytes]: + if encoding: + try: + value = value.decode(encoding) + except ValueError: + raise SynapseError(400, "Query parameter %r must be %s" % (name, encoding)) + + if allowed_values is not None and value not in allowed_values: + message = "Query parameter %r must be one of [%s]" % ( + name, + ", ".join(repr(v) for v in allowed_values), + ) + raise SynapseError(400, message) + else: + return value + + +@overload +def parse_strings_from_args( + args: List[str], + name: Union[bytes, str], + default: Optional[List[str]] = None, + required: bool = False, + allowed_values: Optional[Iterable[str]] = None, + encoding: Literal[None] = None, +) -> Optional[List[bytes]]: + ... + + +@overload +def parse_strings_from_args( + args: List[str], + name: Union[bytes, str], + default: Optional[List[str]] = None, + required: bool = False, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> Optional[List[str]]: + ... + + +def parse_strings_from_args( + args: List[str], + name: Union[bytes, str], + default: Optional[List[str]] = None, + required: bool = False, + allowed_values: Optional[Iterable[str]] = None, + encoding: Optional[str] = "ascii", +) -> Optional[List[Union[bytes, str]]]: + """ + Parse a string parameter from the request query string list. + + If encoding is not None, the content of the query param will be + decoded to Unicode using the encoding, otherwise it will be encoded + + Args: + args: the twisted HTTP request.args list. + name: the name of the query parameter. + default: value to use if the parameter is absent, + defaults to None. Must be bytes if encoding is None. + required : whether to raise a 400 SynapseError if the + parameter is absent, defaults to False. + allowed_values (list[bytes|unicode]): List of allowed values for the + string, or None if any value is allowed, defaults to None. Must be + the same type as name, if given. + encoding: The encoding to decode the string content with. + + Returns: + A string value or the default. Unicode if encoding + was given, bytes otherwise. + + Raises: + SynapseError if the parameter is absent and required, or if the + parameter is present, must be one of a list of allowed values and + is not one of those allowed values. + """ if not isinstance(name, bytes): name = name.encode("ascii") if name in args: - value = args[name][0] - - if encoding: - try: - value = value.decode(encoding) - except ValueError: - raise SynapseError( - 400, "Query parameter %r must be %s" % (name, encoding) - ) - - if allowed_values is not None and value not in allowed_values: - message = "Query parameter %r must be one of [%s]" % ( - name, - ", ".join(repr(v) for v in allowed_values), - ) - raise SynapseError(400, message) - else: - return value + values = args[name] + + return [ + _parse_string_value(value, allowed_values, name=name, encoding=encoding) + for value in values + ] else: if required: - message = "Missing %s query parameter %r" % (param_type, name) + message = "Missing string query parameter %r" % (name) raise SynapseError(400, message, errcode=Codes.MISSING_PARAM) else: @@ -190,6 +251,55 @@ def parse_string_from_args( return default +def parse_string_from_args( + args: List[str], + name: Union[bytes, str], + default: Optional[str] = None, + required: bool = False, + allowed_values: Optional[Iterable[str]] = None, + encoding: Optional[str] = "ascii", +) -> Optional[Union[bytes, str]]: + """ + Parse the string parameter from the request query string list + and return the first result. + + If encoding is not None, the content of the query param will be + decoded to Unicode using the encoding, otherwise it will be encoded + + Args: + args: the twisted HTTP request.args list. + name: the name of the query parameter. + default: value to use if the parameter is absent, + defaults to None. Must be bytes if encoding is None. + required: whether to raise a 400 SynapseError if the + parameter is absent, defaults to False. + allowed_values: List of allowed values for the + string, or None if any value is allowed, defaults to None. Must be + the same type as name, if given. + encoding: The encoding to decode the string content with. + + Returns: + A string value or the default. Unicode if encoding + was given, bytes otherwise. + + Raises: + SynapseError if the parameter is absent and required, or if the + parameter is present, must be one of a list of allowed values and + is not one of those allowed values. + """ + + strings = parse_strings_from_args( + args, + name, + default=[default], + required=required, + allowed_values=allowed_values, + encoding=encoding, + ) + + return strings[0] + + def parse_json_value_from_request(request, allow_empty_body=False): """Parse a JSON value from the body of a twisted HTTP request. @@ -215,7 +325,7 @@ def parse_json_value_from_request(request, allow_empty_body=False): try: content = json_decoder.decode(content_bytes.decode("utf-8")) except Exception as e: - logger.warning("Unable to parse JSON: %s", e) + logger.warning("Unable to parse JSON: %s (%s)", e, content_bytes) raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) return content From 3f96dbbda7696bf1e2d6ec93ce66bbfece8fdc91 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 May 2021 15:57:53 +0100 Subject: [PATCH 212/619] Log method and path when dropping request due to size limit (#10091) --- changelog.d/10091.misc | 1 + synapse/http/site.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10091.misc diff --git a/changelog.d/10091.misc b/changelog.d/10091.misc new file mode 100644 index 0000000000..dbe310fd17 --- /dev/null +++ b/changelog.d/10091.misc @@ -0,0 +1 @@ +Log method and path when dropping request due to size limit. diff --git a/synapse/http/site.py b/synapse/http/site.py index 671fd3fbcc..40754b7bea 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -105,8 +105,10 @@ def handleContentChunk(self, data): assert self.content, "handleContentChunk() called before gotLength()" if self.content.tell() + len(data) > self._max_request_body_size: logger.warning( - "Aborting connection from %s because the request exceeds maximum size", + "Aborting connection from %s because the request exceeds maximum size: %s %s", self.client, + self.get_method(), + self.get_redacted_uri(), ) self.transport.abortConnection() return From ed53bf314fee25d79d349beae409caf81a2d677f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 28 May 2021 16:14:08 +0100 Subject: [PATCH 213/619] Set opentracing priority before setting other tags (#10092) ... because tags on spans which aren't being sampled get thrown away. --- changelog.d/10092.bugfix | 1 + synapse/api/auth.py | 8 +++---- synapse/federation/transport/server.py | 3 ++- synapse/logging/opentracing.py | 21 +++++++++++++++---- synapse/metrics/background_process_metrics.py | 10 +++++++-- 5 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 changelog.d/10092.bugfix diff --git a/changelog.d/10092.bugfix b/changelog.d/10092.bugfix new file mode 100644 index 0000000000..09b2aba7ff --- /dev/null +++ b/changelog.d/10092.bugfix @@ -0,0 +1 @@ +Fix a bug in the `force_tracing_for_users` option introduced in Synapse v1.35 which meant that the OpenTracing spans produced were missing most tags. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 458306eba5..26a3b38918 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -206,11 +206,11 @@ async def get_user_by_req( requester = create_requester(user_id, app_service=app_service) request.requester = user_id + if user_id in self._force_tracing_for_users: + opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) opentracing.set_tag("authenticated_entity", user_id) opentracing.set_tag("user_id", user_id) opentracing.set_tag("appservice_id", app_service.id) - if user_id in self._force_tracing_for_users: - opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) return requester @@ -259,12 +259,12 @@ async def get_user_by_req( ) request.requester = requester + if user_info.token_owner in self._force_tracing_for_users: + opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) opentracing.set_tag("authenticated_entity", user_info.token_owner) opentracing.set_tag("user_id", user_info.user_id) if device_id: opentracing.set_tag("device_id", device_id) - if user_info.token_owner in self._force_tracing_for_users: - opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) return requester except KeyError: diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 59e0a434dc..fdeaa0f37c 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -37,6 +37,7 @@ ) from synapse.logging.context import run_in_background from synapse.logging.opentracing import ( + SynapseTags, start_active_span, start_active_span_from_request, tags, @@ -314,7 +315,7 @@ async def new_func(request, *args, **kwargs): raise request_tags = { - "request_id": request.get_request_id(), + SynapseTags.REQUEST_ID: request.get_request_id(), tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER, tags.HTTP_METHOD: request.get_method(), tags.HTTP_URL: request.get_redacted_uri(), diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index fba2fa3904..428831dad6 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -265,6 +265,12 @@ class SynapseTags: # Whether the sync response has new data to be returned to the client. SYNC_RESULT = "sync.new_data" + # incoming HTTP request ID (as written in the logs) + REQUEST_ID = "request_id" + + # HTTP request tag (used to distinguish full vs incremental syncs, etc) + REQUEST_TAG = "request_tag" + # Block everything by default # A regex which matches the server_names to expose traces for. @@ -824,7 +830,7 @@ def trace_servlet(request: "SynapseRequest", extract_context: bool = False): return request_tags = { - "request_id": request.get_request_id(), + SynapseTags.REQUEST_ID: request.get_request_id(), tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER, tags.HTTP_METHOD: request.get_method(), tags.HTTP_URL: request.get_redacted_uri(), @@ -833,9 +839,9 @@ def trace_servlet(request: "SynapseRequest", extract_context: bool = False): request_name = request.request_metrics.name if extract_context: - scope = start_active_span_from_request(request, request_name, tags=request_tags) + scope = start_active_span_from_request(request, request_name) else: - scope = start_active_span(request_name, tags=request_tags) + scope = start_active_span(request_name) with scope: try: @@ -845,4 +851,11 @@ def trace_servlet(request: "SynapseRequest", extract_context: bool = False): # with JsonResource). scope.span.set_operation_name(request.request_metrics.name) - scope.span.set_tag("request_tag", request.request_metrics.start_context.tag) + # set the tags *after* the servlet completes, in case it decided to + # prioritise the span (tags will get dropped on unprioritised spans) + request_tags[ + SynapseTags.REQUEST_TAG + ] = request.request_metrics.start_context.tag + + for k, v in request_tags.items(): + scope.span.set_tag(k, v) diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index 714caf84c3..0d6d643d35 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -22,7 +22,11 @@ from twisted.internet import defer from synapse.logging.context import LoggingContext, PreserveLoggingContext -from synapse.logging.opentracing import noop_context_manager, start_active_span +from synapse.logging.opentracing import ( + SynapseTags, + noop_context_manager, + start_active_span, +) from synapse.util.async_helpers import maybe_awaitable if TYPE_CHECKING: @@ -202,7 +206,9 @@ async def run(): try: ctx = noop_context_manager() if bg_start_span: - ctx = start_active_span(desc, tags={"request_id": str(context)}) + ctx = start_active_span( + desc, tags={SynapseTags.REQUEST_ID: str(context)} + ) with ctx: return await maybe_awaitable(func(*args, **kwargs)) except Exception: From 84cf3e47a0318aba51d9f830d5e724182c5d93c4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 May 2021 16:28:01 +0100 Subject: [PATCH 214/619] Allow response of `/send_join` to be larger. (#10093) Fixes #10087. --- changelog.d/10093.bugfix | 1 + synapse/federation/transport/client.py | 7 +++++++ synapse/http/matrixfederationclient.py | 14 +++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10093.bugfix diff --git a/changelog.d/10093.bugfix b/changelog.d/10093.bugfix new file mode 100644 index 0000000000..e50de4b2ea --- /dev/null +++ b/changelog.d/10093.bugfix @@ -0,0 +1 @@ +Fix HTTP response size limit to allow joining very large rooms over federation. diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index e93ab83f7f..5b4f5d17f7 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -35,6 +35,11 @@ logger = logging.getLogger(__name__) +# Send join responses can be huge, so we set a separate limit here. The response +# is parsed in a streaming manner, which helps alleviate the issue of memory +# usage a bit. +MAX_RESPONSE_SIZE_SEND_JOIN = 500 * 1024 * 1024 + class TransportLayerClient: """Sends federation HTTP requests to other servers""" @@ -261,6 +266,7 @@ async def send_join_v1( path=path, data=content, parser=SendJoinParser(room_version, v1_api=True), + max_response_size=MAX_RESPONSE_SIZE_SEND_JOIN, ) return response @@ -276,6 +282,7 @@ async def send_join_v2( path=path, data=content, parser=SendJoinParser(room_version, v1_api=False), + max_response_size=MAX_RESPONSE_SIZE_SEND_JOIN, ) return response diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index f5503b394b..1998990a14 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -205,6 +205,7 @@ async def _handle_response( response: IResponse, start_ms: int, parser: ByteParser[T], + max_response_size: Optional[int] = None, ) -> T: """ Reads the body of a response with a timeout and sends it to a parser @@ -216,15 +217,20 @@ async def _handle_response( response: response to the request start_ms: Timestamp when request was made parser: The parser for the response + max_response_size: The maximum size to read from the response, if None + uses the default. Returns: The parsed response """ + if max_response_size is None: + max_response_size = MAX_RESPONSE_SIZE + try: check_content_type_is(response.headers, parser.CONTENT_TYPE) - d = read_body_with_max_size(response, parser, MAX_RESPONSE_SIZE) + d = read_body_with_max_size(response, parser, max_response_size) d = timeout_deferred(d, timeout=timeout_sec, reactor=reactor) length = await make_deferred_yieldable(d) @@ -735,6 +741,7 @@ async def put_json( backoff_on_404: bool = False, try_trailing_slash_on_400: bool = False, parser: Literal[None] = None, + max_response_size: Optional[int] = None, ) -> Union[JsonDict, list]: ... @@ -752,6 +759,7 @@ async def put_json( backoff_on_404: bool = False, try_trailing_slash_on_400: bool = False, parser: Optional[ByteParser[T]] = None, + max_response_size: Optional[int] = None, ) -> T: ... @@ -768,6 +776,7 @@ async def put_json( backoff_on_404: bool = False, try_trailing_slash_on_400: bool = False, parser: Optional[ByteParser] = None, + max_response_size: Optional[int] = None, ): """Sends the specified json data using PUT @@ -803,6 +812,8 @@ async def put_json( enabled. parser: The parser to use to decode the response. Defaults to parsing as JSON. + max_response_size: The maximum size to read from the response, if None + uses the default. Returns: Succeeds when we get a 2xx HTTP response. The @@ -853,6 +864,7 @@ async def put_json( response, start_ms, parser=parser, + max_response_size=max_response_size, ) return body From 1641c5c707fe9cac5f68589863082409c8979da6 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 May 2021 15:57:53 +0100 Subject: [PATCH 215/619] Log method and path when dropping request due to size limit (#10091) --- changelog.d/10091.misc | 1 + synapse/http/site.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10091.misc diff --git a/changelog.d/10091.misc b/changelog.d/10091.misc new file mode 100644 index 0000000000..dbe310fd17 --- /dev/null +++ b/changelog.d/10091.misc @@ -0,0 +1 @@ +Log method and path when dropping request due to size limit. diff --git a/synapse/http/site.py b/synapse/http/site.py index 671fd3fbcc..40754b7bea 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -105,8 +105,10 @@ def handleContentChunk(self, data): assert self.content, "handleContentChunk() called before gotLength()" if self.content.tell() + len(data) > self._max_request_body_size: logger.warning( - "Aborting connection from %s because the request exceeds maximum size", + "Aborting connection from %s because the request exceeds maximum size: %s %s", self.client, + self.get_method(), + self.get_redacted_uri(), ) self.transport.abortConnection() return From 9408b86f5c3616e8cfaa2c183e787780a3a64f95 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 27 May 2021 18:10:58 +0200 Subject: [PATCH 216/619] Limit the number of events sent over replication when persisting events. (#10082) --- changelog.d/10082.bugfix | 1 + synapse/handlers/federation.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changelog.d/10082.bugfix diff --git a/changelog.d/10082.bugfix b/changelog.d/10082.bugfix new file mode 100644 index 0000000000..b4f8bcc4fa --- /dev/null +++ b/changelog.d/10082.bugfix @@ -0,0 +1 @@ +Fixed a bug causing replication requests to fail when receiving a lot of events via federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 678f6b7707..bf11315251 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -91,6 +91,7 @@ get_domain_from_id, ) from synapse.util.async_helpers import Linearizer, concurrently_execute +from synapse.util.iterutils import batch_iter from synapse.util.retryutils import NotRetryingDestination from synapse.util.stringutils import shortstr from synapse.visibility import filter_events_for_server @@ -3053,13 +3054,15 @@ async def persist_events_and_notify( """ instance = self.config.worker.events_shard_config.get_instance(room_id) if instance != self._instance_name: - result = await self._send_events( - instance_name=instance, - store=self.store, - room_id=room_id, - event_and_contexts=event_and_contexts, - backfilled=backfilled, - ) + # Limit the number of events sent over federation. + for batch in batch_iter(event_and_contexts, 1000): + result = await self._send_events( + instance_name=instance, + store=self.store, + room_id=room_id, + event_and_contexts=batch, + backfilled=backfilled, + ) return result["max_stream_id"] else: assert self.storage.persistence From 258a9a9e8bea851493fc2275ac6b81639c997afb Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 May 2021 17:06:05 +0100 Subject: [PATCH 217/619] 1.35.0rc3 --- CHANGES.md | 16 ++++++++++++++++ changelog.d/10082.bugfix | 1 - changelog.d/10091.misc | 1 - changelog.d/10093.bugfix | 1 - synapse/__init__.py | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/10082.bugfix delete mode 100644 changelog.d/10091.misc delete mode 100644 changelog.d/10093.bugfix diff --git a/CHANGES.md b/CHANGES.md index 1fac16580d..8bd05c318d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +Synapse 1.35.0rc3 (2021-05-28) +============================== + +Bugfixes +-------- + +- Fixed a bug causing replication requests to fail when receiving a lot of events via federation. ([\#10082](https://github.com/matrix-org/synapse/issues/10082)) +- Fix HTTP response size limit to allow joining very large rooms over federation. ([\#10093](https://github.com/matrix-org/synapse/issues/10093)) + + +Internal Changes +---------------- + +- Log method and path when dropping request due to size limit. ([\#10091](https://github.com/matrix-org/synapse/issues/10091)) + + Synapse 1.35.0rc2 (2021-05-27) ============================== diff --git a/changelog.d/10082.bugfix b/changelog.d/10082.bugfix deleted file mode 100644 index b4f8bcc4fa..0000000000 --- a/changelog.d/10082.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug causing replication requests to fail when receiving a lot of events via federation. diff --git a/changelog.d/10091.misc b/changelog.d/10091.misc deleted file mode 100644 index dbe310fd17..0000000000 --- a/changelog.d/10091.misc +++ /dev/null @@ -1 +0,0 @@ -Log method and path when dropping request due to size limit. diff --git a/changelog.d/10093.bugfix b/changelog.d/10093.bugfix deleted file mode 100644 index e50de4b2ea..0000000000 --- a/changelog.d/10093.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix HTTP response size limit to allow joining very large rooms over federation. diff --git a/synapse/__init__.py b/synapse/__init__.py index 6faf31dbbc..4591246bd1 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.35.0rc2" +__version__ = "1.35.0rc3" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 4f41b711d8b37da3403ce67c88d62133f732a459 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 28 May 2021 17:13:57 +0100 Subject: [PATCH 218/619] CHANGELOG --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8bd05c318d..7e6f478d42 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,8 +4,8 @@ Synapse 1.35.0rc3 (2021-05-28) Bugfixes -------- -- Fixed a bug causing replication requests to fail when receiving a lot of events via federation. ([\#10082](https://github.com/matrix-org/synapse/issues/10082)) -- Fix HTTP response size limit to allow joining very large rooms over federation. ([\#10093](https://github.com/matrix-org/synapse/issues/10093)) +- Fixed a bug causing replication requests to fail when receiving a lot of events via federation. Introduced in v1.33.0. ([\#10082](https://github.com/matrix-org/synapse/issues/10082)) +- Fix HTTP response size limit to allow joining very large rooms over federation. Introduced in v1.33.0. ([\#10093](https://github.com/matrix-org/synapse/issues/10093)) Internal Changes From 10e6d2abce644d5b6d6b59516061562f54382b94 Mon Sep 17 00:00:00 2001 From: Brad Murray Date: Tue, 1 Jun 2021 03:40:26 -0400 Subject: [PATCH 219/619] Fix opentracing inject to use the SpanContext, not the Span (#10074) Signed-off-by: Brad Murray brad@beeper.com --- changelog.d/10074.misc | 1 + synapse/logging/opentracing.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10074.misc diff --git a/changelog.d/10074.misc b/changelog.d/10074.misc new file mode 100644 index 0000000000..8dbe2cd2bc --- /dev/null +++ b/changelog.d/10074.misc @@ -0,0 +1 @@ +Update opentracing to inject the right context into the carrier. diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index 428831dad6..f64845b80c 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -594,7 +594,7 @@ def inject_active_span_twisted_headers(headers, destination, check_destination=T span = opentracing.tracer.active_span carrier = {} # type: Dict[str, str] - opentracing.tracer.inject(span, opentracing.Format.HTTP_HEADERS, carrier) + opentracing.tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, carrier) for key, value in carrier.items(): headers.addRawHeaders(key, value) @@ -631,7 +631,7 @@ def inject_active_span_byte_dict(headers, destination, check_destination=True): span = opentracing.tracer.active_span carrier = {} # type: Dict[str, str] - opentracing.tracer.inject(span, opentracing.Format.HTTP_HEADERS, carrier) + opentracing.tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, carrier) for key, value in carrier.items(): headers[key.encode()] = [value.encode()] @@ -665,7 +665,7 @@ def inject_active_span_text_map(carrier, destination, check_destination=True): return opentracing.tracer.inject( - opentracing.tracer.active_span, opentracing.Format.TEXT_MAP, carrier + opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier ) @@ -687,7 +687,7 @@ def get_active_span_text_map(destination=None): carrier = {} # type: Dict[str, str] opentracing.tracer.inject( - opentracing.tracer.active_span, opentracing.Format.TEXT_MAP, carrier + opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier ) return carrier @@ -702,7 +702,7 @@ def active_span_context_as_string(): carrier = {} # type: Dict[str, str] if opentracing: opentracing.tracer.inject( - opentracing.tracer.active_span, opentracing.Format.TEXT_MAP, carrier + opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier ) return json_encoder.encode(carrier) From b4b2fd2ecee26214fa6b322bcb62bec1ea324c1a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:04:47 +0100 Subject: [PATCH 220/619] add a cache to have_seen_event (#9953) Empirically, this helped my server considerably when handling gaps in Matrix HQ. The problem was that we would repeatedly call have_seen_events for the same set of (50K or so) auth_events, each of which would take many minutes to complete, even though it's only an index scan. --- changelog.d/9953.feature | 1 + changelog.d/9973.feature | 1 + changelog.d/9973.misc | 1 - synapse/handlers/federation.py | 12 ++- synapse/storage/databases/main/cache.py | 1 + .../storage/databases/main/events_worker.py | 61 ++++++++++-- .../storage/databases/main/purge_events.py | 26 ++++- tests/storage/databases/__init__.py | 13 +++ tests/storage/databases/main/__init__.py | 13 +++ .../databases/main/test_events_worker.py | 96 +++++++++++++++++++ 10 files changed, 205 insertions(+), 20 deletions(-) create mode 100644 changelog.d/9953.feature create mode 100644 changelog.d/9973.feature delete mode 100644 changelog.d/9973.misc create mode 100644 tests/storage/databases/__init__.py create mode 100644 tests/storage/databases/main/__init__.py create mode 100644 tests/storage/databases/main/test_events_worker.py diff --git a/changelog.d/9953.feature b/changelog.d/9953.feature new file mode 100644 index 0000000000..6b3d1adc70 --- /dev/null +++ b/changelog.d/9953.feature @@ -0,0 +1 @@ +Improve performance of incoming federation transactions in large rooms. diff --git a/changelog.d/9973.feature b/changelog.d/9973.feature new file mode 100644 index 0000000000..6b3d1adc70 --- /dev/null +++ b/changelog.d/9973.feature @@ -0,0 +1 @@ +Improve performance of incoming federation transactions in large rooms. diff --git a/changelog.d/9973.misc b/changelog.d/9973.misc deleted file mode 100644 index 7f22d42291..0000000000 --- a/changelog.d/9973.misc +++ /dev/null @@ -1 +0,0 @@ -Make `LruCache.invalidate` support tree invalidation, and remove `invalidate_many`. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index bf11315251..49ed7cabcc 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -577,7 +577,9 @@ async def _get_state_for_room( # Fetch the state events from the DB, and check we have the auth events. event_map = await self.store.get_events(state_event_ids, allow_rejected=True) - auth_events_in_store = await self.store.have_seen_events(auth_event_ids) + auth_events_in_store = await self.store.have_seen_events( + room_id, auth_event_ids + ) # Check for missing events. We handle state and auth event seperately, # as we want to pull the state from the DB, but we don't for the auth @@ -610,7 +612,7 @@ async def _get_state_for_room( if missing_auth_events: auth_events_in_store = await self.store.have_seen_events( - missing_auth_events + room_id, missing_auth_events ) missing_auth_events.difference_update(auth_events_in_store) @@ -710,7 +712,7 @@ async def _get_state_after_missing_prev_event( missing_auth_events = set(auth_event_ids) - fetched_events.keys() missing_auth_events.difference_update( - await self.store.have_seen_events(missing_auth_events) + await self.store.have_seen_events(room_id, missing_auth_events) ) logger.debug("We are also missing %i auth events", len(missing_auth_events)) @@ -2475,7 +2477,7 @@ async def _update_auth_events_and_context_for_auth( # # we start by checking if they are in the store, and then try calling /event_auth/. if missing_auth: - have_events = await self.store.have_seen_events(missing_auth) + have_events = await self.store.have_seen_events(event.room_id, missing_auth) logger.debug("Events %s are in the store", have_events) missing_auth.difference_update(have_events) @@ -2494,7 +2496,7 @@ async def _update_auth_events_and_context_for_auth( return context seen_remotes = await self.store.have_seen_events( - [e.event_id for e in remote_auth_chain] + event.room_id, [e.event_id for e in remote_auth_chain] ) for e in remote_auth_chain: diff --git a/synapse/storage/databases/main/cache.py b/synapse/storage/databases/main/cache.py index f7872501a0..c57ae5ef15 100644 --- a/synapse/storage/databases/main/cache.py +++ b/synapse/storage/databases/main/cache.py @@ -168,6 +168,7 @@ def _invalidate_caches_for_event( backfilled, ): self._invalidate_get_event_cache(event_id) + self.have_seen_event.invalidate((room_id, event_id)) self.get_latest_event_ids_in_room.invalidate((room_id,)) diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index 6963bbf7f4..403a5ddaba 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -22,6 +22,7 @@ Iterable, List, Optional, + Set, Tuple, overload, ) @@ -55,7 +56,7 @@ from synapse.storage.util.id_generators import MultiWriterIdGenerator, StreamIdGenerator from synapse.storage.util.sequence import build_sequence_generator from synapse.types import JsonDict, get_domain_from_id -from synapse.util.caches.descriptors import cached +from synapse.util.caches.descriptors import cached, cachedList from synapse.util.caches.lrucache import LruCache from synapse.util.iterutils import batch_iter from synapse.util.metrics import Measure @@ -1045,32 +1046,74 @@ async def have_events_in_timeline(self, event_ids): return {r["event_id"] for r in rows} - async def have_seen_events(self, event_ids): + async def have_seen_events( + self, room_id: str, event_ids: Iterable[str] + ) -> Set[str]: """Given a list of event ids, check if we have already processed them. + The room_id is only used to structure the cache (so that it can later be + invalidated by room_id) - there is no guarantee that the events are actually + in the room in question. + Args: - event_ids (iterable[str]): + room_id: Room we are polling + event_ids: events we are looking for Returns: set[str]: The events we have already seen. """ + res = await self._have_seen_events_dict( + (room_id, event_id) for event_id in event_ids + ) + return {eid for ((_rid, eid), have_event) in res.items() if have_event} + + @cachedList("have_seen_event", "keys") + async def _have_seen_events_dict( + self, keys: Iterable[Tuple[str, str]] + ) -> Dict[Tuple[str, str], bool]: + """Helper for have_seen_events + + Returns: + a dict {(room_id, event_id)-> bool} + """ # if the event cache contains the event, obviously we've seen it. - results = {x for x in event_ids if self._get_event_cache.contains(x)} - def have_seen_events_txn(txn, chunk): - sql = "SELECT event_id FROM events as e WHERE " + cache_results = { + (rid, eid) for (rid, eid) in keys if self._get_event_cache.contains((eid,)) + } + results = {x: True for x in cache_results} + + def have_seen_events_txn(txn, chunk: Tuple[Tuple[str, str], ...]): + # we deliberately do *not* query the database for room_id, to make the + # query an index-only lookup on `events_event_id_key`. + # + # We therefore pull the events from the database into a set... + + sql = "SELECT event_id FROM events AS e WHERE " clause, args = make_in_list_sql_clause( - txn.database_engine, "e.event_id", chunk + txn.database_engine, "e.event_id", [eid for (_rid, eid) in chunk] ) txn.execute(sql + clause, args) - results.update(row[0] for row in txn) + found_events = {eid for eid, in txn} - for chunk in batch_iter((x for x in event_ids if x not in results), 100): + # ... and then we can update the results for each row in the batch + results.update({(rid, eid): (eid in found_events) for (rid, eid) in chunk}) + + # each batch requires its own index scan, so we make the batches as big as + # possible. + for chunk in batch_iter((k for k in keys if k not in cache_results), 500): await self.db_pool.runInteraction( "have_seen_events", have_seen_events_txn, chunk ) + return results + @cached(max_entries=100000, tree=True) + async def have_seen_event(self, room_id: str, event_id: str): + # this only exists for the benefit of the @cachedList descriptor on + # _have_seen_events_dict + raise NotImplementedError() + def _get_current_state_event_counts_txn(self, txn, room_id): """ See get_current_state_event_counts. diff --git a/synapse/storage/databases/main/purge_events.py b/synapse/storage/databases/main/purge_events.py index 8f83748b5e..7fb7780d0f 100644 --- a/synapse/storage/databases/main/purge_events.py +++ b/synapse/storage/databases/main/purge_events.py @@ -16,14 +16,14 @@ from typing import Any, List, Set, Tuple from synapse.api.errors import SynapseError -from synapse.storage._base import SQLBaseStore +from synapse.storage.databases.main import CacheInvalidationWorkerStore from synapse.storage.databases.main.state import StateGroupWorkerStore from synapse.types import RoomStreamToken logger = logging.getLogger(__name__) -class PurgeEventsStore(StateGroupWorkerStore, SQLBaseStore): +class PurgeEventsStore(StateGroupWorkerStore, CacheInvalidationWorkerStore): async def purge_history( self, room_id: str, token: str, delete_local_events: bool ) -> Set[int]: @@ -203,8 +203,6 @@ def _purge_history_txn( "DELETE FROM event_to_state_groups " "WHERE event_id IN (SELECT event_id from events_to_purge)" ) - for event_id, _ in event_rows: - txn.call_after(self._get_state_group_for_event.invalidate, (event_id,)) # Delete all remote non-state events for table in ( @@ -283,6 +281,20 @@ def _purge_history_txn( # so make sure to keep this actually last. txn.execute("DROP TABLE events_to_purge") + for event_id, should_delete in event_rows: + self._invalidate_cache_and_stream( + txn, self._get_state_group_for_event, (event_id,) + ) + + # XXX: This is racy, since have_seen_events could be called between the + # transaction completing and the invalidation running. On the other hand, + # that's no different to calling `have_seen_events` just before the + # event is deleted from the database. + if should_delete: + self._invalidate_cache_and_stream( + txn, self.have_seen_event, (room_id, event_id) + ) + logger.info("[purge] done") return referenced_state_groups @@ -422,7 +434,11 @@ def _purge_room_txn(self, txn, room_id: str) -> List[int]: # index on them. In any case we should be clearing out 'stream' tables # periodically anyway (#5888) - # TODO: we could probably usefully do a bunch of cache invalidation here + # TODO: we could probably usefully do a bunch more cache invalidation here + + # XXX: as with purge_history, this is racy, but no worse than other races + # that already exist. + self._invalidate_cache_and_stream(txn, self.have_seen_event, (room_id,)) logger.info("[purge] done") diff --git a/tests/storage/databases/__init__.py b/tests/storage/databases/__init__.py new file mode 100644 index 0000000000..c24c7ecd92 --- /dev/null +++ b/tests/storage/databases/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/storage/databases/main/__init__.py b/tests/storage/databases/main/__init__.py new file mode 100644 index 0000000000..c24c7ecd92 --- /dev/null +++ b/tests/storage/databases/main/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/storage/databases/main/test_events_worker.py b/tests/storage/databases/main/test_events_worker.py new file mode 100644 index 0000000000..932970fd9a --- /dev/null +++ b/tests/storage/databases/main/test_events_worker.py @@ -0,0 +1,96 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json + +from synapse.logging.context import LoggingContext +from synapse.storage.databases.main.events_worker import EventsWorkerStore + +from tests import unittest + + +class HaveSeenEventsTestCase(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, hs): + self.store: EventsWorkerStore = hs.get_datastore() + + # insert some test data + for rid in ("room1", "room2"): + self.get_success( + self.store.db_pool.simple_insert( + "rooms", + {"room_id": rid, "room_version": 4}, + ) + ) + + for idx, (rid, eid) in enumerate( + ( + ("room1", "event10"), + ("room1", "event11"), + ("room1", "event12"), + ("room2", "event20"), + ) + ): + self.get_success( + self.store.db_pool.simple_insert( + "events", + { + "event_id": eid, + "room_id": rid, + "topological_ordering": idx, + "stream_ordering": idx, + "type": "test", + "processed": True, + "outlier": False, + }, + ) + ) + self.get_success( + self.store.db_pool.simple_insert( + "event_json", + { + "event_id": eid, + "room_id": rid, + "json": json.dumps({"type": "test", "room_id": rid}), + "internal_metadata": "{}", + "format_version": 3, + }, + ) + ) + + def test_simple(self): + with LoggingContext(name="test") as ctx: + res = self.get_success( + self.store.have_seen_events("room1", ["event10", "event19"]) + ) + self.assertEquals(res, {"event10"}) + + # that should result in a single db query + self.assertEquals(ctx.get_resource_usage().db_txn_count, 1) + + # a second lookup of the same events should cause no queries + with LoggingContext(name="test") as ctx: + res = self.get_success( + self.store.have_seen_events("room1", ["event10", "event19"]) + ) + self.assertEquals(res, {"event10"}) + self.assertEquals(ctx.get_resource_usage().db_txn_count, 0) + + def test_query_via_event_cache(self): + # fetch an event into the event cache + self.get_success(self.store.get_event("event10")) + + # looking it up should now cause no db hits + with LoggingContext(name="test") as ctx: + res = self.get_success(self.store.have_seen_events("room1", ["event10"])) + self.assertEquals(res, {"event10"}) + self.assertEquals(ctx.get_resource_usage().db_txn_count, 0) From 408ecf8ece397bcf08564031379b461d5c9b0de5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 1 Jun 2021 13:19:50 +0100 Subject: [PATCH 221/619] Announce deprecation of experimental `msc2858_enabled` option. (#10101) c.f. https://github.com/matrix-org/synapse/pull/9617 and https://github.com/matrix-org/matrix-doc/blob/master/proposals/2858-Multiple-SSO-Identity-Providers.md Fixes #9627. --- changelog.d/10101.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10101.removal diff --git a/changelog.d/10101.removal b/changelog.d/10101.removal new file mode 100644 index 0000000000..f2020e9ddf --- /dev/null +++ b/changelog.d/10101.removal @@ -0,0 +1 @@ +The core Synapse development team plan to drop support for the [unstable API of MSC2858](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2858-Multiple-SSO-Identity-Providers.md#unstable-prefix), including the undocumented `experimental.msc2858_enabled` config option, in August 2021. Client authors should ensure that their clients are updated to use the stable API (which has been supported since Synapse 1.30) well before that time, to give their users time to upgrade. From a8372ad591e07fa76e194a22732a5301d9e55b6f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 1 Jun 2021 13:23:55 +0100 Subject: [PATCH 222/619] 1.35.0 --- CHANGES.md | 9 +++++++++ changelog.d/10101.removal | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/10101.removal diff --git a/CHANGES.md b/CHANGES.md index 7e6f478d42..09f0be8e17 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.35.0 (2021-06-01) +=========================== + +Deprecations and Removals +------------------------- + +- The core Synapse development team plan to drop support for the [unstable API of MSC2858](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2858-Multiple-SSO-Identity-Providers.md#unstable-prefix), including the undocumented `experimental.msc2858_enabled` config option, in August 2021. Client authors should ensure that their clients are updated to use the stable API (which has been supported since Synapse 1.30) well before that time, to give their users time to upgrade. ([\#10101](https://github.com/matrix-org/synapse/issues/10101)) + + Synapse 1.35.0rc3 (2021-05-28) ============================== diff --git a/changelog.d/10101.removal b/changelog.d/10101.removal deleted file mode 100644 index f2020e9ddf..0000000000 --- a/changelog.d/10101.removal +++ /dev/null @@ -1 +0,0 @@ -The core Synapse development team plan to drop support for the [unstable API of MSC2858](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2858-Multiple-SSO-Identity-Providers.md#unstable-prefix), including the undocumented `experimental.msc2858_enabled` config option, in August 2021. Client authors should ensure that their clients are updated to use the stable API (which has been supported since Synapse 1.30) well before that time, to give their users time to upgrade. diff --git a/debian/changelog b/debian/changelog index bf99ae772c..d5efb8ccba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.35.0) stable; urgency=medium + + * New synapse release 1.35.0. + + -- Synapse Packaging team Tue, 01 Jun 2021 13:23:35 +0100 + matrix-synapse-py3 (1.34.0) stable; urgency=medium * New synapse release 1.34.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 4591246bd1..d9843a1708 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.35.0rc3" +__version__ = "1.35.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 08e54345b1332889cd9e88a778bc13caca7b556f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 1 Jun 2021 13:25:18 +0100 Subject: [PATCH 223/619] Indicate that there were no functional changes since v1.35.0rc3 --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 09f0be8e17..c969fe1ebd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ Synapse 1.35.0 (2021-06-01) =========================== +No changes since v1.35.0rc3. + Deprecations and Removals ------------------------- From 3fdaf4df55f52ccf283cf6b0ca73a3f98cd5e8f0 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 1 Jun 2021 13:40:46 +0100 Subject: [PATCH 224/619] Merge v1.35.0rc3 into v1.35.0 due to incorrect tagging --- CHANGES.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c969fe1ebd..f03a53affc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,17 +1,13 @@ Synapse 1.35.0 (2021-06-01) =========================== -No changes since v1.35.0rc3. +Note that [the tag](https://github.com/matrix-org/synapse/releases/tag/v1.35.0rc3) and [docker images](https://hub.docker.com/layers/matrixdotorg/synapse/v1.35.0rc3/images/sha256-34ccc87bd99a17e2cbc0902e678b5937d16bdc1991ead097eee6096481ecf2c4?context=explore) for `v1.35.0rc3` were incorrectly built. If you are experiencing issues with either, it is recommended to upgrade to the equivalent tag or docker image for the `v1.35.0` release. Deprecations and Removals ------------------------- - The core Synapse development team plan to drop support for the [unstable API of MSC2858](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2858-Multiple-SSO-Identity-Providers.md#unstable-prefix), including the undocumented `experimental.msc2858_enabled` config option, in August 2021. Client authors should ensure that their clients are updated to use the stable API (which has been supported since Synapse 1.30) well before that time, to give their users time to upgrade. ([\#10101](https://github.com/matrix-org/synapse/issues/10101)) - -Synapse 1.35.0rc3 (2021-05-28) -============================== - Bugfixes -------- From 4deaebfe00e5416b408f5822b521fc9c55f09494 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 2 Jun 2021 15:48:17 +0100 Subject: [PATCH 225/619] Make /sync do less state res (#10102) --- changelog.d/10102.misc | 1 + synapse/handlers/sync.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10102.misc diff --git a/changelog.d/10102.misc b/changelog.d/10102.misc new file mode 100644 index 0000000000..87672ee295 --- /dev/null +++ b/changelog.d/10102.misc @@ -0,0 +1 @@ +Make `/sync` do fewer state resolutions. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 0fcc1532da..069ffc76f7 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -463,7 +463,7 @@ async def _load_filtered_recents( # ensure that we always include current state in the timeline current_state_ids = frozenset() # type: FrozenSet[str] if any(e.is_state() for e in recents): - current_state_ids_map = await self.state.get_current_state_ids( + current_state_ids_map = await self.store.get_current_state_ids( room_id ) current_state_ids = frozenset(current_state_ids_map.values()) @@ -523,7 +523,7 @@ async def _load_filtered_recents( # ensure that we always include current state in the timeline current_state_ids = frozenset() if any(e.is_state() for e in loaded_recents): - current_state_ids_map = await self.state.get_current_state_ids( + current_state_ids_map = await self.store.get_current_state_ids( room_id ) current_state_ids = frozenset(current_state_ids_map.values()) From 3cf6b34b4e203dcda803ab3ac88c9dadc591e4a1 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 2 Jun 2021 11:31:41 -0400 Subject: [PATCH 226/619] Do not show invite-only rooms in spaces summary (unless joined/invited). (#10109) --- changelog.d/10109.bugfix | 1 + synapse/handlers/space_summary.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10109.bugfix diff --git a/changelog.d/10109.bugfix b/changelog.d/10109.bugfix new file mode 100644 index 0000000000..bc41bf9e5e --- /dev/null +++ b/changelog.d/10109.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.35.0 where invite-only rooms would be shown to users in a space who were not invited. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index abd9ddecca..046dba6fd8 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -26,7 +26,6 @@ HistoryVisibility, Membership, ) -from synapse.api.errors import AuthError from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 from synapse.types import JsonDict @@ -456,16 +455,16 @@ async def _is_room_accessible( return True # Otherwise, check if they should be allowed access via membership in a space. - try: - await self._event_auth_handler.check_restricted_join_rules( - state_ids, room_version, requester, member_event + if self._event_auth_handler.has_restricted_join_rules( + state_ids, room_version + ): + allowed_spaces = ( + await self._event_auth_handler.get_spaces_that_allow_join(state_ids) ) - except AuthError: - # The user doesn't have access due to spaces, but might have access - # another way. Keep trying. - pass - else: - return True + if await self._event_auth_handler.is_user_in_rooms( + allowed_spaces, requester + ): + return True # If this is a request over federation, check if the host is in the room or # is in one of the spaces specified via the join rules. From fc3d2dc269a79e0404d0a9867e5042354d59147f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 2 Jun 2021 16:37:59 +0100 Subject: [PATCH 227/619] Rewrite the KeyRing (#10035) --- changelog.d/10035.feature | 1 + synapse/crypto/keyring.py | 642 ++++++++---------- synapse/federation/transport/server.py | 4 +- synapse/groups/attestations.py | 4 +- synapse/rest/key/v2/remote_key_resource.py | 9 +- tests/crypto/test_keyring.py | 170 ++--- tests/rest/key/v2/test_remote_key_resource.py | 18 +- tests/util/test_batching_queue.py | 37 +- 8 files changed, 393 insertions(+), 492 deletions(-) create mode 100644 changelog.d/10035.feature diff --git a/changelog.d/10035.feature b/changelog.d/10035.feature new file mode 100644 index 0000000000..68052b5a7e --- /dev/null +++ b/changelog.d/10035.feature @@ -0,0 +1 @@ +Rewrite logic around verifying JSON object and fetching server keys to be more performant and use less memory. diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index 6fc0712978..c840ffca71 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -16,8 +16,7 @@ import abc import logging import urllib -from collections import defaultdict -from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional, Tuple import attr from signedjson.key import ( @@ -44,17 +43,12 @@ from synapse.config.key import TrustedKeyServer from synapse.events import EventBase from synapse.events.utils import prune_event_dict -from synapse.logging.context import ( - PreserveLoggingContext, - make_deferred_yieldable, - preserve_fn, - run_in_background, -) +from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.storage.keys import FetchKeyResult from synapse.types import JsonDict from synapse.util import unwrapFirstError from synapse.util.async_helpers import yieldable_gather_results -from synapse.util.metrics import Measure +from synapse.util.batching_queue import BatchingQueue from synapse.util.retryutils import NotRetryingDestination if TYPE_CHECKING: @@ -80,32 +74,19 @@ class VerifyJsonRequest: minimum_valid_until_ts: time at which we require the signing key to be valid. (0 implies we don't care) - request_name: The name of the request. - key_ids: The set of key_ids to that could be used to verify the JSON object - - key_ready (Deferred[str, str, nacl.signing.VerifyKey]): - A deferred (server_name, key_id, verify_key) tuple that resolves when - a verify key has been fetched. The deferreds' callbacks are run with no - logcontext. - - If we are unable to find a key which satisfies the request, the deferred - errbacks with an M_UNAUTHORIZED SynapseError. """ server_name = attr.ib(type=str) get_json_object = attr.ib(type=Callable[[], JsonDict]) minimum_valid_until_ts = attr.ib(type=int) - request_name = attr.ib(type=str) key_ids = attr.ib(type=List[str]) - key_ready = attr.ib(default=attr.Factory(defer.Deferred), type=defer.Deferred) @staticmethod def from_json_object( server_name: str, json_object: JsonDict, minimum_valid_until_ms: int, - request_name: str, ): """Create a VerifyJsonRequest to verify all signatures on a signed JSON object for the given server. @@ -115,7 +96,6 @@ def from_json_object( server_name, lambda: json_object, minimum_valid_until_ms, - request_name=request_name, key_ids=key_ids, ) @@ -135,16 +115,48 @@ def from_event( # memory than the Event object itself. lambda: prune_event_dict(event.room_version, event.get_pdu_json()), minimum_valid_until_ms, - request_name=event.event_id, key_ids=key_ids, ) + def to_fetch_key_request(self) -> "_FetchKeyRequest": + """Create a key fetch request for all keys needed to satisfy the + verification request. + """ + return _FetchKeyRequest( + server_name=self.server_name, + minimum_valid_until_ts=self.minimum_valid_until_ts, + key_ids=self.key_ids, + ) + class KeyLookupError(ValueError): pass +@attr.s(slots=True) +class _FetchKeyRequest: + """A request for keys for a given server. + + We will continue to try and fetch until we have all the keys listed under + `key_ids` (with an appropriate `valid_until_ts` property) or we run out of + places to fetch keys from. + + Attributes: + server_name: The name of the server that owns the keys. + minimum_valid_until_ts: The timestamp which the keys must be valid until. + key_ids: The IDs of the keys to attempt to fetch + """ + + server_name = attr.ib(type=str) + minimum_valid_until_ts = attr.ib(type=int) + key_ids = attr.ib(type=List[str]) + + class Keyring: + """Handles verifying signed JSON objects and fetching the keys needed to do + so. + """ + def __init__( self, hs: "HomeServer", key_fetchers: "Optional[Iterable[KeyFetcher]]" = None ): @@ -158,22 +170,22 @@ def __init__( ) self._key_fetchers = key_fetchers - # map from server name to Deferred. Has an entry for each server with - # an ongoing key download; the Deferred completes once the download - # completes. - # - # These are regular, logcontext-agnostic Deferreds. - self.key_downloads = {} # type: Dict[str, defer.Deferred] + self._server_queue = BatchingQueue( + "keyring_server", + clock=hs.get_clock(), + process_batch_callback=self._inner_fetch_key_requests, + ) # type: BatchingQueue[_FetchKeyRequest, Dict[str, Dict[str, FetchKeyResult]]] - def verify_json_for_server( + async def verify_json_for_server( self, server_name: str, json_object: JsonDict, validity_time: int, - request_name: str, - ) -> defer.Deferred: + ) -> None: """Verify that a JSON object has been signed by a given server + Completes if the the object was correctly signed, otherwise raises. + Args: server_name: name of the server which must have signed this object @@ -181,52 +193,45 @@ def verify_json_for_server( validity_time: timestamp at which we require the signing key to be valid. (0 implies we don't care) - - request_name: an identifier for this json object (eg, an event id) - for logging. - - Returns: - Deferred[None]: completes if the the object was correctly signed, otherwise - errbacks with an error """ request = VerifyJsonRequest.from_json_object( server_name, json_object, validity_time, - request_name, ) - requests = (request,) - return make_deferred_yieldable(self._verify_objects(requests)[0]) + return await self.process_request(request) def verify_json_objects_for_server( - self, server_and_json: Iterable[Tuple[str, dict, int, str]] + self, server_and_json: Iterable[Tuple[str, dict, int]] ) -> List[defer.Deferred]: """Bulk verifies signatures of json objects, bulk fetching keys as necessary. Args: server_and_json: - Iterable of (server_name, json_object, validity_time, request_name) + Iterable of (server_name, json_object, validity_time) tuples. validity_time is a timestamp at which the signing key must be valid. - request_name is an identifier for this json object (eg, an event id) - for logging. - Returns: List: for each input triplet, a deferred indicating success or failure to verify each json object's signature for the given server_name. The deferreds run their callbacks in the sentinel logcontext. """ - return self._verify_objects( - VerifyJsonRequest.from_json_object( - server_name, json_object, validity_time, request_name + return [ + run_in_background( + self.process_request, + VerifyJsonRequest.from_json_object( + server_name, + json_object, + validity_time, + ), ) - for server_name, json_object, validity_time, request_name in server_and_json - ) + for server_name, json_object, validity_time in server_and_json + ] def verify_events_for_server( self, server_and_events: Iterable[Tuple[str, EventBase, int]] @@ -252,321 +257,223 @@ def verify_events_for_server( server_name. The deferreds run their callbacks in the sentinel logcontext. """ - return self._verify_objects( - VerifyJsonRequest.from_event(server_name, event, validity_time) + return [ + run_in_background( + self.process_request, + VerifyJsonRequest.from_event( + server_name, + event, + validity_time, + ), + ) for server_name, event, validity_time in server_and_events - ) - - def _verify_objects( - self, verify_requests: Iterable[VerifyJsonRequest] - ) -> List[defer.Deferred]: - """Does the work of verify_json_[objects_]for_server - - - Args: - verify_requests: Iterable of verification requests. + ] - Returns: - List: for each input item, a deferred indicating success - or failure to verify each json object's signature for the given - server_name. The deferreds run their callbacks in the sentinel - logcontext. + async def process_request(self, verify_request: VerifyJsonRequest) -> None: + """Processes the `VerifyJsonRequest`. Raises if the object is not signed + by the server, the signatures don't match or we failed to fetch the + necessary keys. """ - # a list of VerifyJsonRequests which are awaiting a key lookup - key_lookups = [] - handle = preserve_fn(_handle_key_deferred) - - def process(verify_request: VerifyJsonRequest) -> defer.Deferred: - """Process an entry in the request list - - Adds a key request to key_lookups, and returns a deferred which - will complete or fail (in the sentinel context) when verification completes. - """ - if not verify_request.key_ids: - return defer.fail( - SynapseError( - 400, - "Not signed by %s" % (verify_request.server_name,), - Codes.UNAUTHORIZED, - ) - ) - logger.debug( - "Verifying %s for %s with key_ids %s, min_validity %i", - verify_request.request_name, - verify_request.server_name, - verify_request.key_ids, - verify_request.minimum_valid_until_ts, + if not verify_request.key_ids: + raise SynapseError( + 400, + f"Not signed by {verify_request.server_name}", + Codes.UNAUTHORIZED, ) - # add the key request to the queue, but don't start it off yet. - key_lookups.append(verify_request) - - # now run _handle_key_deferred, which will wait for the key request - # to complete and then do the verification. - # - # We want _handle_key_request to log to the right context, so we - # wrap it with preserve_fn (aka run_in_background) - return handle(verify_request) - - results = [process(r) for r in verify_requests] - - if key_lookups: - run_in_background(self._start_key_lookups, key_lookups) - - return results - - async def _start_key_lookups( - self, verify_requests: List[VerifyJsonRequest] - ) -> None: - """Sets off the key fetches for each verify request - - Once each fetch completes, verify_request.key_ready will be resolved. - - Args: - verify_requests: - """ - - try: - # map from server name to a set of outstanding request ids - server_to_request_ids = {} # type: Dict[str, Set[int]] - - for verify_request in verify_requests: - server_name = verify_request.server_name - request_id = id(verify_request) - server_to_request_ids.setdefault(server_name, set()).add(request_id) - - # Wait for any previous lookups to complete before proceeding. - await self.wait_for_previous_lookups(server_to_request_ids.keys()) - - # take out a lock on each of the servers by sticking a Deferred in - # key_downloads - for server_name in server_to_request_ids.keys(): - self.key_downloads[server_name] = defer.Deferred() - logger.debug("Got key lookup lock on %s", server_name) - - # When we've finished fetching all the keys for a given server_name, - # drop the lock by resolving the deferred in key_downloads. - def drop_server_lock(server_name): - d = self.key_downloads.pop(server_name) - d.callback(None) - - def lookup_done(res, verify_request): - server_name = verify_request.server_name - server_requests = server_to_request_ids[server_name] - server_requests.remove(id(verify_request)) - - # if there are no more requests for this server, we can drop the lock. - if not server_requests: - logger.debug("Releasing key lookup lock on %s", server_name) - drop_server_lock(server_name) - - return res + # Add the keys we need to verify to the queue for retrieval. We queue + # up requests for the same server so we don't end up with many in flight + # requests for the same keys. + key_request = verify_request.to_fetch_key_request() + found_keys_by_server = await self._server_queue.add_to_queue( + key_request, key=verify_request.server_name + ) - for verify_request in verify_requests: - verify_request.key_ready.addBoth(lookup_done, verify_request) + # Since we batch up requests the returned set of keys may contain keys + # from other servers, so we pull out only the ones we care about.s + found_keys = found_keys_by_server.get(verify_request.server_name, {}) - # Actually start fetching keys. - self._get_server_verify_keys(verify_requests) - except Exception: - logger.exception("Error starting key lookups") + # Verify each signature we got valid keys for, raising if we can't + # verify any of them. + verified = False + for key_id in verify_request.key_ids: + key_result = found_keys.get(key_id) + if not key_result: + continue - async def wait_for_previous_lookups(self, server_names: Iterable[str]) -> None: - """Waits for any previous key lookups for the given servers to finish. + if key_result.valid_until_ts < verify_request.minimum_valid_until_ts: + continue - Args: - server_names: list of servers which we want to look up + verify_key = key_result.verify_key + json_object = verify_request.get_json_object() + try: + verify_signed_json( + json_object, + verify_request.server_name, + verify_key, + ) + verified = True + except SignatureVerifyException as e: + logger.debug( + "Error verifying signature for %s:%s:%s with key %s: %s", + verify_request.server_name, + verify_key.alg, + verify_key.version, + encode_verify_key_base64(verify_key), + str(e), + ) + raise SynapseError( + 401, + "Invalid signature for server %s with key %s:%s: %s" + % ( + verify_request.server_name, + verify_key.alg, + verify_key.version, + str(e), + ), + Codes.UNAUTHORIZED, + ) - Returns: - Resolves once all key lookups for the given servers have - completed. Follows the synapse rules of logcontext preservation. - """ - loop_count = 1 - while True: - wait_on = [ - (server_name, self.key_downloads[server_name]) - for server_name in server_names - if server_name in self.key_downloads - ] - if not wait_on: - break - logger.info( - "Waiting for existing lookups for %s to complete [loop %i]", - [w[0] for w in wait_on], - loop_count, + if not verified: + raise SynapseError( + 401, + f"Failed to find any key to satisfy: {key_request}", + Codes.UNAUTHORIZED, ) - with PreserveLoggingContext(): - await defer.DeferredList((w[1] for w in wait_on)) - loop_count += 1 + async def _inner_fetch_key_requests( + self, requests: List[_FetchKeyRequest] + ) -> Dict[str, Dict[str, FetchKeyResult]]: + """Processing function for the queue of `_FetchKeyRequest`.""" + + logger.debug("Starting fetch for %s", requests) + + # First we need to deduplicate requests for the same key. We do this by + # taking the *maximum* requested `minimum_valid_until_ts` for each pair + # of server name/key ID. + server_to_key_to_ts = {} # type: Dict[str, Dict[str, int]] + for request in requests: + by_server = server_to_key_to_ts.setdefault(request.server_name, {}) + for key_id in request.key_ids: + existing_ts = by_server.get(key_id, 0) + by_server[key_id] = max(request.minimum_valid_until_ts, existing_ts) + + deduped_requests = [ + _FetchKeyRequest(server_name, minimum_valid_ts, [key_id]) + for server_name, by_server in server_to_key_to_ts.items() + for key_id, minimum_valid_ts in by_server.items() + ] + + logger.debug("Deduplicated key requests to %s", deduped_requests) + + # For each key we call `_inner_verify_request` which will handle + # fetching each key. Note these shouldn't throw if we fail to contact + # other servers etc. + results_per_request = await yieldable_gather_results( + self._inner_fetch_key_request, + deduped_requests, + ) - def _get_server_verify_keys(self, verify_requests: List[VerifyJsonRequest]) -> None: - """Tries to find at least one key for each verify request + # We now convert the returned list of results into a map from server + # name to key ID to FetchKeyResult, to return. + to_return = {} # type: Dict[str, Dict[str, FetchKeyResult]] + for (request, results) in zip(deduped_requests, results_per_request): + to_return_by_server = to_return.setdefault(request.server_name, {}) + for key_id, key_result in results.items(): + existing = to_return_by_server.get(key_id) + if not existing or existing.valid_until_ts < key_result.valid_until_ts: + to_return_by_server[key_id] = key_result - For each verify_request, verify_request.key_ready is called back with - params (server_name, key_id, VerifyKey) if a key is found, or errbacked - with a SynapseError if none of the keys are found. + return to_return - Args: - verify_requests: list of verify requests + async def _inner_fetch_key_request( + self, verify_request: _FetchKeyRequest + ) -> Dict[str, FetchKeyResult]: + """Attempt to fetch the given key by calling each key fetcher one by + one. """ + logger.debug("Starting fetch for %s", verify_request) - remaining_requests = {rq for rq in verify_requests if not rq.key_ready.called} + found_keys: Dict[str, FetchKeyResult] = {} + missing_key_ids = set(verify_request.key_ids) - async def do_iterations(): - try: - with Measure(self.clock, "get_server_verify_keys"): - for f in self._key_fetchers: - if not remaining_requests: - return - await self._attempt_key_fetches_with_fetcher( - f, remaining_requests - ) - - # look for any requests which weren't satisfied - while remaining_requests: - verify_request = remaining_requests.pop() - rq_str = ( - "VerifyJsonRequest(server=%s, key_ids=%s, min_valid=%i)" - % ( - verify_request.server_name, - verify_request.key_ids, - verify_request.minimum_valid_until_ts, - ) - ) - - # If we run the errback immediately, it may cancel our - # loggingcontext while we are still in it, so instead we - # schedule it for the next time round the reactor. - # - # (this also ensures that we don't get a stack overflow if we - # has a massive queue of lookups waiting for this server). - self.clock.call_later( - 0, - verify_request.key_ready.errback, - SynapseError( - 401, - "Failed to find any key to satisfy %s" % (rq_str,), - Codes.UNAUTHORIZED, - ), - ) - except Exception as err: - # we don't really expect to get here, because any errors should already - # have been caught and logged. But if we do, let's log the error and make - # sure that all of the deferreds are resolved. - logger.error("Unexpected error in _get_server_verify_keys: %s", err) - with PreserveLoggingContext(): - for verify_request in remaining_requests: - if not verify_request.key_ready.called: - verify_request.key_ready.errback(err) - - run_in_background(do_iterations) - - async def _attempt_key_fetches_with_fetcher( - self, fetcher: "KeyFetcher", remaining_requests: Set[VerifyJsonRequest] - ): - """Use a key fetcher to attempt to satisfy some key requests + for fetcher in self._key_fetchers: + if not missing_key_ids: + break - Args: - fetcher: fetcher to use to fetch the keys - remaining_requests: outstanding key requests. - Any successfully-completed requests will be removed from the list. - """ - # The keys to fetch. - # server_name -> key_id -> min_valid_ts - missing_keys = defaultdict(dict) # type: Dict[str, Dict[str, int]] - - for verify_request in remaining_requests: - # any completed requests should already have been removed - assert not verify_request.key_ready.called - keys_for_server = missing_keys[verify_request.server_name] - - for key_id in verify_request.key_ids: - # If we have several requests for the same key, then we only need to - # request that key once, but we should do so with the greatest - # min_valid_until_ts of the requests, so that we can satisfy all of - # the requests. - keys_for_server[key_id] = max( - keys_for_server.get(key_id, -1), - verify_request.minimum_valid_until_ts, - ) + logger.debug("Getting keys from %s for %s", fetcher, verify_request) + keys = await fetcher.get_keys( + verify_request.server_name, + list(missing_key_ids), + verify_request.minimum_valid_until_ts, + ) - results = await fetcher.get_keys(missing_keys) + for key_id, key in keys.items(): + if not key: + continue - completed = [] - for verify_request in remaining_requests: - server_name = verify_request.server_name + # If we already have a result for the given key ID we keep the + # one with the highest `valid_until_ts`. + existing_key = found_keys.get(key_id) + if existing_key: + if key.valid_until_ts <= existing_key.valid_until_ts: + continue - # see if any of the keys we got this time are sufficient to - # complete this VerifyJsonRequest. - result_keys = results.get(server_name, {}) - for key_id in verify_request.key_ids: - fetch_key_result = result_keys.get(key_id) - if not fetch_key_result: - # we didn't get a result for this key - continue + # We always store the returned key even if it doesn't the + # `minimum_valid_until_ts` requirement, as some verification + # requests may still be able to be satisfied by it. + # + # We still keep looking for the key from other fetchers in that + # case though. + found_keys[key_id] = key - if ( - fetch_key_result.valid_until_ts - < verify_request.minimum_valid_until_ts - ): - # key was not valid at this point + if key.valid_until_ts < verify_request.minimum_valid_until_ts: continue - # we have a valid key for this request. If we run the callback - # immediately, it may cancel our loggingcontext while we are still in - # it, so instead we schedule it for the next time round the reactor. - # - # (this also ensures that we don't get a stack overflow if we had - # a massive queue of lookups waiting for this server). - logger.debug( - "Found key %s:%s for %s", - server_name, - key_id, - verify_request.request_name, - ) - self.clock.call_later( - 0, - verify_request.key_ready.callback, - (server_name, key_id, fetch_key_result.verify_key), - ) - completed.append(verify_request) - break + missing_key_ids.discard(key_id) - remaining_requests.difference_update(completed) + return found_keys class KeyFetcher(metaclass=abc.ABCMeta): - @abc.abstractmethod + def __init__(self, hs: "HomeServer"): + self._queue = BatchingQueue( + self.__class__.__name__, hs.get_clock(), self._fetch_keys + ) + async def get_keys( - self, keys_to_fetch: Dict[str, Dict[str, int]] - ) -> Dict[str, Dict[str, FetchKeyResult]]: - """ - Args: - keys_to_fetch: - the keys to be fetched. server_name -> key_id -> min_valid_ts + self, server_name: str, key_ids: List[str], minimum_valid_until_ts: int + ) -> Dict[str, FetchKeyResult]: + results = await self._queue.add_to_queue( + _FetchKeyRequest( + server_name=server_name, + key_ids=key_ids, + minimum_valid_until_ts=minimum_valid_until_ts, + ) + ) + return results.get(server_name, {}) - Returns: - Map from server_name -> key_id -> FetchKeyResult - """ - raise NotImplementedError + @abc.abstractmethod + async def _fetch_keys( + self, keys_to_fetch: List[_FetchKeyRequest] + ) -> Dict[str, Dict[str, FetchKeyResult]]: + pass class StoreKeyFetcher(KeyFetcher): """KeyFetcher impl which fetches keys from our data store""" def __init__(self, hs: "HomeServer"): - self.store = hs.get_datastore() + super().__init__(hs) - async def get_keys( - self, keys_to_fetch: Dict[str, Dict[str, int]] - ) -> Dict[str, Dict[str, FetchKeyResult]]: - """see KeyFetcher.get_keys""" + self.store = hs.get_datastore() + async def _fetch_keys(self, keys_to_fetch: List[_FetchKeyRequest]): key_ids_to_fetch = ( - (server_name, key_id) - for server_name, keys_for_server in keys_to_fetch.items() - for key_id in keys_for_server.keys() + (queue_value.server_name, key_id) + for queue_value in keys_to_fetch + for key_id in queue_value.key_ids ) res = await self.store.get_server_verify_keys(key_ids_to_fetch) @@ -578,6 +485,8 @@ async def get_keys( class BaseV2KeyFetcher(KeyFetcher): def __init__(self, hs: "HomeServer"): + super().__init__(hs) + self.store = hs.get_datastore() self.config = hs.config @@ -685,10 +594,10 @@ def __init__(self, hs: "HomeServer"): self.client = hs.get_federation_http_client() self.key_servers = self.config.key_servers - async def get_keys( - self, keys_to_fetch: Dict[str, Dict[str, int]] + async def _fetch_keys( + self, keys_to_fetch: List[_FetchKeyRequest] ) -> Dict[str, Dict[str, FetchKeyResult]]: - """see KeyFetcher.get_keys""" + """see KeyFetcher._fetch_keys""" async def get_key(key_server: TrustedKeyServer) -> Dict: try: @@ -724,12 +633,12 @@ async def get_key(key_server: TrustedKeyServer) -> Dict: return union_of_keys async def get_server_verify_key_v2_indirect( - self, keys_to_fetch: Dict[str, Dict[str, int]], key_server: TrustedKeyServer + self, keys_to_fetch: List[_FetchKeyRequest], key_server: TrustedKeyServer ) -> Dict[str, Dict[str, FetchKeyResult]]: """ Args: keys_to_fetch: - the keys to be fetched. server_name -> key_id -> min_valid_ts + the keys to be fetched. key_server: notary server to query for the keys @@ -743,7 +652,7 @@ async def get_server_verify_key_v2_indirect( perspective_name = key_server.server_name logger.info( "Requesting keys %s from notary server %s", - keys_to_fetch.items(), + keys_to_fetch, perspective_name, ) @@ -753,11 +662,13 @@ async def get_server_verify_key_v2_indirect( path="/_matrix/key/v2/query", data={ "server_keys": { - server_name: { - key_id: {"minimum_valid_until_ts": min_valid_ts} - for key_id, min_valid_ts in server_keys.items() + queue_value.server_name: { + key_id: { + "minimum_valid_until_ts": queue_value.minimum_valid_until_ts, + } + for key_id in queue_value.key_ids } - for server_name, server_keys in keys_to_fetch.items() + for queue_value in keys_to_fetch } }, ) @@ -858,7 +769,20 @@ def __init__(self, hs: "HomeServer"): self.client = hs.get_federation_http_client() async def get_keys( - self, keys_to_fetch: Dict[str, Dict[str, int]] + self, server_name: str, key_ids: List[str], minimum_valid_until_ts: int + ) -> Dict[str, FetchKeyResult]: + results = await self._queue.add_to_queue( + _FetchKeyRequest( + server_name=server_name, + key_ids=key_ids, + minimum_valid_until_ts=minimum_valid_until_ts, + ), + key=server_name, + ) + return results.get(server_name, {}) + + async def _fetch_keys( + self, keys_to_fetch: List[_FetchKeyRequest] ) -> Dict[str, Dict[str, FetchKeyResult]]: """ Args: @@ -871,8 +795,10 @@ async def get_keys( results = {} - async def get_key(key_to_fetch_item: Tuple[str, Dict[str, int]]) -> None: - server_name, key_ids = key_to_fetch_item + async def get_key(key_to_fetch_item: _FetchKeyRequest) -> None: + server_name = key_to_fetch_item.server_name + key_ids = key_to_fetch_item.key_ids + try: keys = await self.get_server_verify_key_v2_direct(server_name, key_ids) results[server_name] = keys @@ -883,7 +809,7 @@ async def get_key(key_to_fetch_item: Tuple[str, Dict[str, int]]) -> None: except Exception: logger.exception("Error getting keys %s from %s", key_ids, server_name) - await yieldable_gather_results(get_key, keys_to_fetch.items()) + await yieldable_gather_results(get_key, keys_to_fetch) return results async def get_server_verify_key_v2_direct( @@ -955,37 +881,3 @@ async def get_server_verify_key_v2_direct( keys.update(response_keys) return keys - - -async def _handle_key_deferred(verify_request: VerifyJsonRequest) -> None: - """Waits for the key to become available, and then performs a verification - - Args: - verify_request: - - Raises: - SynapseError if there was a problem performing the verification - """ - server_name = verify_request.server_name - with PreserveLoggingContext(): - _, key_id, verify_key = await verify_request.key_ready - - json_object = verify_request.get_json_object() - - try: - verify_signed_json(json_object, server_name, verify_key) - except SignatureVerifyException as e: - logger.debug( - "Error verifying signature for %s:%s:%s with key %s: %s", - server_name, - verify_key.alg, - verify_key.version, - encode_verify_key_base64(verify_key), - str(e), - ) - raise SynapseError( - 401, - "Invalid signature for server %s with key %s:%s: %s" - % (server_name, verify_key.alg, verify_key.version, str(e)), - Codes.UNAUTHORIZED, - ) diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index fdeaa0f37c..5756fcb551 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -152,7 +152,9 @@ async def authenticate_request(self, request, content): ) await self.keyring.verify_json_for_server( - origin, json_request, now, "Incoming request" + origin, + json_request, + now, ) logger.debug("Request from %s", origin) diff --git a/synapse/groups/attestations.py b/synapse/groups/attestations.py index d2fc8be5f5..ff8372c4e9 100644 --- a/synapse/groups/attestations.py +++ b/synapse/groups/attestations.py @@ -108,7 +108,9 @@ async def verify_attestation( assert server_name is not None await self.keyring.verify_json_for_server( - server_name, attestation, now, "Group attestation" + server_name, + attestation, + now, ) def create_attestation(self, group_id: str, user_id: str) -> JsonDict: diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index aba1734a55..d56a1ae482 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -22,6 +22,7 @@ from synapse.http.server import DirectServeJsonResource, respond_with_json from synapse.http.servlet import parse_integer, parse_json_object_from_request from synapse.util import json_decoder +from synapse.util.async_helpers import yieldable_gather_results logger = logging.getLogger(__name__) @@ -210,7 +211,13 @@ async def query_keys(self, request, query, query_remote_on_cache_miss=False): # If there is a cache miss, request the missing keys, then recurse (and # ensure the result is sent). if cache_misses and query_remote_on_cache_miss: - await self.fetcher.get_keys(cache_misses) + await yieldable_gather_results( + lambda t: self.fetcher.get_keys(*t), + ( + (server_name, list(keys), 0) + for server_name, keys in cache_misses.items() + ), + ) await self.query_keys(request, query, query_remote_on_cache_miss=False) else: signed_keys = [] diff --git a/tests/crypto/test_keyring.py b/tests/crypto/test_keyring.py index 2775dfd880..745c295d3b 100644 --- a/tests/crypto/test_keyring.py +++ b/tests/crypto/test_keyring.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import time +from typing import Dict, List from unittest.mock import Mock import attr @@ -21,7 +22,6 @@ from nacl.signing import SigningKey from signedjson.key import encode_verify_key_base64, get_verify_key -from twisted.internet import defer from twisted.internet.defer import Deferred, ensureDeferred from synapse.api.errors import SynapseError @@ -92,23 +92,23 @@ def test_verify_json_objects_for_server_awaits_previous_requests(self): # deferred completes. first_lookup_deferred = Deferred() - async def first_lookup_fetch(keys_to_fetch): - self.assertEquals(current_context().request.id, "context_11") - self.assertEqual(keys_to_fetch, {"server10": {get_key_id(key1): 0}}) + async def first_lookup_fetch( + server_name: str, key_ids: List[str], minimum_valid_until_ts: int + ) -> Dict[str, FetchKeyResult]: + # self.assertEquals(current_context().request.id, "context_11") + self.assertEqual(server_name, "server10") + self.assertEqual(key_ids, [get_key_id(key1)]) + self.assertEqual(minimum_valid_until_ts, 0) await make_deferred_yieldable(first_lookup_deferred) - return { - "server10": { - get_key_id(key1): FetchKeyResult(get_verify_key(key1), 100) - } - } + return {get_key_id(key1): FetchKeyResult(get_verify_key(key1), 100)} mock_fetcher.get_keys.side_effect = first_lookup_fetch async def first_lookup(): with LoggingContext("context_11", request=FakeRequest("context_11")): res_deferreds = kr.verify_json_objects_for_server( - [("server10", json1, 0, "test10"), ("server11", {}, 0, "test11")] + [("server10", json1, 0), ("server11", {}, 0)] ) # the unsigned json should be rejected pretty quickly @@ -126,18 +126,18 @@ async def first_lookup(): d0 = ensureDeferred(first_lookup()) + self.pump() + mock_fetcher.get_keys.assert_called_once() # a second request for a server with outstanding requests # should block rather than start a second call - async def second_lookup_fetch(keys_to_fetch): - self.assertEquals(current_context().request.id, "context_12") - return { - "server10": { - get_key_id(key1): FetchKeyResult(get_verify_key(key1), 100) - } - } + async def second_lookup_fetch( + server_name: str, key_ids: List[str], minimum_valid_until_ts: int + ) -> Dict[str, FetchKeyResult]: + # self.assertEquals(current_context().request.id, "context_12") + return {get_key_id(key1): FetchKeyResult(get_verify_key(key1), 100)} mock_fetcher.get_keys.reset_mock() mock_fetcher.get_keys.side_effect = second_lookup_fetch @@ -146,7 +146,13 @@ async def second_lookup_fetch(keys_to_fetch): async def second_lookup(): with LoggingContext("context_12", request=FakeRequest("context_12")): res_deferreds_2 = kr.verify_json_objects_for_server( - [("server10", json1, 0, "test")] + [ + ( + "server10", + json1, + 0, + ) + ] ) res_deferreds_2[0].addBoth(self.check_context, None) second_lookup_state[0] = 1 @@ -183,11 +189,11 @@ def test_verify_json_for_server(self): signedjson.sign.sign_json(json1, "server9", key1) # should fail immediately on an unsigned object - d = _verify_json_for_server(kr, "server9", {}, 0, "test unsigned") + d = kr.verify_json_for_server("server9", {}, 0) self.get_failure(d, SynapseError) # should succeed on a signed object - d = _verify_json_for_server(kr, "server9", json1, 500, "test signed") + d = kr.verify_json_for_server("server9", json1, 500) # self.assertFalse(d.called) self.get_success(d) @@ -214,24 +220,24 @@ def test_verify_json_for_server_with_null_valid_until_ms(self): signedjson.sign.sign_json(json1, "server9", key1) # should fail immediately on an unsigned object - d = _verify_json_for_server(kr, "server9", {}, 0, "test unsigned") + d = kr.verify_json_for_server("server9", {}, 0) self.get_failure(d, SynapseError) # should fail on a signed object with a non-zero minimum_valid_until_ms, # as it tries to refetch the keys and fails. - d = _verify_json_for_server( - kr, "server9", json1, 500, "test signed non-zero min" - ) + d = kr.verify_json_for_server("server9", json1, 500) self.get_failure(d, SynapseError) # We expect the keyring tried to refetch the key once. mock_fetcher.get_keys.assert_called_once_with( - {"server9": {get_key_id(key1): 500}} + "server9", [get_key_id(key1)], 500 ) # should succeed on a signed object with a 0 minimum_valid_until_ms - d = _verify_json_for_server( - kr, "server9", json1, 0, "test signed with zero min" + d = kr.verify_json_for_server( + "server9", + json1, + 0, ) self.get_success(d) @@ -239,15 +245,15 @@ def test_verify_json_dedupes_key_requests(self): """Two requests for the same key should be deduped.""" key1 = signedjson.key.generate_signing_key(1) - async def get_keys(keys_to_fetch): + async def get_keys( + server_name: str, key_ids: List[str], minimum_valid_until_ts: int + ) -> Dict[str, FetchKeyResult]: # there should only be one request object (with the max validity) - self.assertEqual(keys_to_fetch, {"server1": {get_key_id(key1): 1500}}) + self.assertEqual(server_name, "server1") + self.assertEqual(key_ids, [get_key_id(key1)]) + self.assertEqual(minimum_valid_until_ts, 1500) - return { - "server1": { - get_key_id(key1): FetchKeyResult(get_verify_key(key1), 1200) - } - } + return {get_key_id(key1): FetchKeyResult(get_verify_key(key1), 1200)} mock_fetcher = Mock() mock_fetcher.get_keys = Mock(side_effect=get_keys) @@ -259,7 +265,14 @@ async def get_keys(keys_to_fetch): # the first request should succeed; the second should fail because the key # has expired results = kr.verify_json_objects_for_server( - [("server1", json1, 500, "test1"), ("server1", json1, 1500, "test2")] + [ + ( + "server1", + json1, + 500, + ), + ("server1", json1, 1500), + ] ) self.assertEqual(len(results), 2) self.get_success(results[0]) @@ -274,19 +287,21 @@ def test_verify_json_falls_back_to_other_fetchers(self): """If the first fetcher cannot provide a recent enough key, we fall back""" key1 = signedjson.key.generate_signing_key(1) - async def get_keys1(keys_to_fetch): - self.assertEqual(keys_to_fetch, {"server1": {get_key_id(key1): 1500}}) - return { - "server1": {get_key_id(key1): FetchKeyResult(get_verify_key(key1), 800)} - } - - async def get_keys2(keys_to_fetch): - self.assertEqual(keys_to_fetch, {"server1": {get_key_id(key1): 1500}}) - return { - "server1": { - get_key_id(key1): FetchKeyResult(get_verify_key(key1), 1200) - } - } + async def get_keys1( + server_name: str, key_ids: List[str], minimum_valid_until_ts: int + ) -> Dict[str, FetchKeyResult]: + self.assertEqual(server_name, "server1") + self.assertEqual(key_ids, [get_key_id(key1)]) + self.assertEqual(minimum_valid_until_ts, 1500) + return {get_key_id(key1): FetchKeyResult(get_verify_key(key1), 800)} + + async def get_keys2( + server_name: str, key_ids: List[str], minimum_valid_until_ts: int + ) -> Dict[str, FetchKeyResult]: + self.assertEqual(server_name, "server1") + self.assertEqual(key_ids, [get_key_id(key1)]) + self.assertEqual(minimum_valid_until_ts, 1500) + return {get_key_id(key1): FetchKeyResult(get_verify_key(key1), 1200)} mock_fetcher1 = Mock() mock_fetcher1.get_keys = Mock(side_effect=get_keys1) @@ -298,7 +313,18 @@ async def get_keys2(keys_to_fetch): signedjson.sign.sign_json(json1, "server1", key1) results = kr.verify_json_objects_for_server( - [("server1", json1, 1200, "test1"), ("server1", json1, 1500, "test2")] + [ + ( + "server1", + json1, + 1200, + ), + ( + "server1", + json1, + 1500, + ), + ] ) self.assertEqual(len(results), 2) self.get_success(results[0]) @@ -349,9 +375,8 @@ async def get_json(destination, path, **kwargs): self.http_client.get_json.side_effect = get_json - keys_to_fetch = {SERVER_NAME: {"key1": 0}} - keys = self.get_success(fetcher.get_keys(keys_to_fetch)) - k = keys[SERVER_NAME][testverifykey_id] + keys = self.get_success(fetcher.get_keys(SERVER_NAME, ["key1"], 0)) + k = keys[testverifykey_id] self.assertEqual(k.valid_until_ts, VALID_UNTIL_TS) self.assertEqual(k.verify_key, testverifykey) self.assertEqual(k.verify_key.alg, "ed25519") @@ -378,7 +403,7 @@ async def get_json(destination, path, **kwargs): # change the server name: the result should be ignored response["server_name"] = "OTHER_SERVER" - keys = self.get_success(fetcher.get_keys(keys_to_fetch)) + keys = self.get_success(fetcher.get_keys(SERVER_NAME, ["key1"], 0)) self.assertEqual(keys, {}) @@ -465,10 +490,9 @@ def test_get_keys_from_perspectives(self): self.expect_outgoing_key_query(SERVER_NAME, "key1", response) - keys_to_fetch = {SERVER_NAME: {"key1": 0}} - keys = self.get_success(fetcher.get_keys(keys_to_fetch)) - self.assertIn(SERVER_NAME, keys) - k = keys[SERVER_NAME][testverifykey_id] + keys = self.get_success(fetcher.get_keys(SERVER_NAME, ["key1"], 0)) + self.assertIn(testverifykey_id, keys) + k = keys[testverifykey_id] self.assertEqual(k.valid_until_ts, VALID_UNTIL_TS) self.assertEqual(k.verify_key, testverifykey) self.assertEqual(k.verify_key.alg, "ed25519") @@ -515,10 +539,9 @@ def test_get_perspectives_own_key(self): self.expect_outgoing_key_query(SERVER_NAME, "key1", response) - keys_to_fetch = {SERVER_NAME: {"key1": 0}} - keys = self.get_success(fetcher.get_keys(keys_to_fetch)) - self.assertIn(SERVER_NAME, keys) - k = keys[SERVER_NAME][testverifykey_id] + keys = self.get_success(fetcher.get_keys(SERVER_NAME, ["key1"], 0)) + self.assertIn(testverifykey_id, keys) + k = keys[testverifykey_id] self.assertEqual(k.valid_until_ts, VALID_UNTIL_TS) self.assertEqual(k.verify_key, testverifykey) self.assertEqual(k.verify_key.alg, "ed25519") @@ -559,14 +582,13 @@ def build_response(): def get_key_from_perspectives(response): fetcher = PerspectivesKeyFetcher(self.hs) - keys_to_fetch = {SERVER_NAME: {"key1": 0}} self.expect_outgoing_key_query(SERVER_NAME, "key1", response) - return self.get_success(fetcher.get_keys(keys_to_fetch)) + return self.get_success(fetcher.get_keys(SERVER_NAME, ["key1"], 0)) # start with a valid response so we can check we are testing the right thing response = build_response() keys = get_key_from_perspectives(response) - k = keys[SERVER_NAME][testverifykey_id] + k = keys[testverifykey_id] self.assertEqual(k.verify_key, testverifykey) # remove the perspectives server's signature @@ -585,23 +607,3 @@ def get_key_from_perspectives(response): def get_key_id(key): """Get the matrix ID tag for a given SigningKey or VerifyKey""" return "%s:%s" % (key.alg, key.version) - - -@defer.inlineCallbacks -def run_in_context(f, *args, **kwargs): - with LoggingContext("testctx"): - rv = yield f(*args, **kwargs) - return rv - - -def _verify_json_for_server(kr, *args): - """thin wrapper around verify_json_for_server which makes sure it is wrapped - with the patched defer.inlineCallbacks. - """ - - @defer.inlineCallbacks - def v(): - rv1 = yield kr.verify_json_for_server(*args) - return rv1 - - return run_in_context(v) diff --git a/tests/rest/key/v2/test_remote_key_resource.py b/tests/rest/key/v2/test_remote_key_resource.py index 3b275bc23b..a75c0ea3f0 100644 --- a/tests/rest/key/v2/test_remote_key_resource.py +++ b/tests/rest/key/v2/test_remote_key_resource.py @@ -208,10 +208,10 @@ def test_get_key(self): keyid = "ed25519:%s" % (testkey.version,) fetcher = PerspectivesKeyFetcher(self.hs2) - d = fetcher.get_keys({"targetserver": {keyid: 1000}}) + d = fetcher.get_keys("targetserver", [keyid], 1000) res = self.get_success(d) - self.assertIn("targetserver", res) - keyres = res["targetserver"][keyid] + self.assertIn(keyid, res) + keyres = res[keyid] assert isinstance(keyres, FetchKeyResult) self.assertEqual( signedjson.key.encode_verify_key_base64(keyres.verify_key), @@ -230,10 +230,10 @@ def test_get_notary_key(self): keyid = "ed25519:%s" % (testkey.version,) fetcher = PerspectivesKeyFetcher(self.hs2) - d = fetcher.get_keys({self.hs.hostname: {keyid: 1000}}) + d = fetcher.get_keys(self.hs.hostname, [keyid], 1000) res = self.get_success(d) - self.assertIn(self.hs.hostname, res) - keyres = res[self.hs.hostname][keyid] + self.assertIn(keyid, res) + keyres = res[keyid] assert isinstance(keyres, FetchKeyResult) self.assertEqual( signedjson.key.encode_verify_key_base64(keyres.verify_key), @@ -247,10 +247,10 @@ def test_get_notary_keyserver_key(self): keyid = "ed25519:%s" % (self.hs_signing_key.version,) fetcher = PerspectivesKeyFetcher(self.hs2) - d = fetcher.get_keys({self.hs.hostname: {keyid: 1000}}) + d = fetcher.get_keys(self.hs.hostname, [keyid], 1000) res = self.get_success(d) - self.assertIn(self.hs.hostname, res) - keyres = res[self.hs.hostname][keyid] + self.assertIn(keyid, res) + keyres = res[keyid] assert isinstance(keyres, FetchKeyResult) self.assertEqual( signedjson.key.encode_verify_key_base64(keyres.verify_key), diff --git a/tests/util/test_batching_queue.py b/tests/util/test_batching_queue.py index edf29e5b96..07be57d72c 100644 --- a/tests/util/test_batching_queue.py +++ b/tests/util/test_batching_queue.py @@ -45,37 +45,32 @@ async def _process_queue(self, values): self._pending_calls.append((values, d)) return await make_deferred_yieldable(d) + def _get_sample_with_name(self, metric, name) -> int: + """For a prometheus metric get the value of the sample that has a + matching "name" label. + """ + for sample in metric.collect()[0].samples: + if sample.labels.get("name") == name: + return sample.value + + self.fail("Found no matching sample") + def _assert_metrics(self, queued, keys, in_flight): """Assert that the metrics are correct""" - self.assertEqual(len(number_queued.collect()), 1) - self.assertEqual(len(number_queued.collect()[0].samples), 1) + sample = self._get_sample_with_name(number_queued, self.queue._name) self.assertEqual( - number_queued.collect()[0].samples[0].labels, - {"name": self.queue._name}, - ) - self.assertEqual( - number_queued.collect()[0].samples[0].value, + sample, queued, "number_queued", ) - self.assertEqual(len(number_of_keys.collect()), 1) - self.assertEqual(len(number_of_keys.collect()[0].samples), 1) - self.assertEqual( - number_queued.collect()[0].samples[0].labels, {"name": self.queue._name} - ) - self.assertEqual( - number_of_keys.collect()[0].samples[0].value, keys, "number_of_keys" - ) + sample = self._get_sample_with_name(number_of_keys, self.queue._name) + self.assertEqual(sample, keys, "number_of_keys") - self.assertEqual(len(number_in_flight.collect()), 1) - self.assertEqual(len(number_in_flight.collect()[0].samples), 1) - self.assertEqual( - number_queued.collect()[0].samples[0].labels, {"name": self.queue._name} - ) + sample = self._get_sample_with_name(number_in_flight, self.queue._name) self.assertEqual( - number_in_flight.collect()[0].samples[0].value, + sample, in_flight, "number_in_flight", ) From bf6fd9f4fdf60aab29d5bfac2dfbf7ec3cd7e459 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 2 Jun 2021 17:10:37 +0100 Subject: [PATCH 228/619] github actions: summarize Sytest results in an easy-to-read format (#10094) ... using the script from matrix-org/sytest#1052 --- .github/workflows/tests.yml | 4 ++-- changelog.d/10094.misc | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10094.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2ae81b5fcf..955beb4aa0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -232,9 +232,9 @@ jobs: - name: Run SyTest run: /bootstrap.sh synapse working-directory: /src - - name: Dump results.tap + - name: Summarise results.tap if: ${{ always() }} - run: cat /logs/results.tap + run: /sytest/scripts/tap_to_gha.pl /logs/results.tap - name: Upload SyTest logs uses: actions/upload-artifact@v2 if: ${{ always() }} diff --git a/changelog.d/10094.misc b/changelog.d/10094.misc new file mode 100644 index 0000000000..01efe14f74 --- /dev/null +++ b/changelog.d/10094.misc @@ -0,0 +1 @@ +In Github Actions workflows, summarize the Sytest results in an easy-to-read format. From 0284d2a2976e3d58e9970fdb7590f98a2556326d Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Wed, 2 Jun 2021 19:50:35 +0200 Subject: [PATCH 229/619] Add new admin APIs to remove media by media ID from quarantine. (#10044) Related to: #6681, #5956, #10040 Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10044.feature | 1 + docs/admin_api/media_admin_api.md | 22 +++++ synapse/rest/admin/media.py | 30 ++++++ synapse/storage/databases/main/room.py | 30 ++++-- tests/rest/admin/test_media.py | 128 +++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10044.feature diff --git a/changelog.d/10044.feature b/changelog.d/10044.feature new file mode 100644 index 0000000000..70c0a3851e --- /dev/null +++ b/changelog.d/10044.feature @@ -0,0 +1 @@ +Add new admin APIs to remove media by media ID from quarantine. Contributed by @dkimpel. diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index d1b7e390d5..7709f3d8c7 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -4,6 +4,7 @@ * [List all media uploaded by a user](#list-all-media-uploaded-by-a-user) - [Quarantine media](#quarantine-media) * [Quarantining media by ID](#quarantining-media-by-id) + * [Remove media from quarantine by ID](#remove-media-from-quarantine-by-id) * [Quarantining media in a room](#quarantining-media-in-a-room) * [Quarantining all media of a user](#quarantining-all-media-of-a-user) * [Protecting media from being quarantined](#protecting-media-from-being-quarantined) @@ -77,6 +78,27 @@ Response: {} ``` +## Remove media from quarantine by ID + +This API removes a single piece of local or remote media from quarantine. + +Request: + +``` +POST /_synapse/admin/v1/media/unquarantine// + +{} +``` + +Where `server_name` is in the form of `example.org`, and `media_id` is in the +form of `abcdefg12345...`. + +Response: + +```json +{} +``` + ## Quarantining media in a room This API quarantines all local and remote media in a room. diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index 2c71af4279..b68db2c57c 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -120,6 +120,35 @@ async def on_POST( return 200, {} +class UnquarantineMediaByID(RestServlet): + """Quarantines local or remote media by a given ID so that no one can download + it via this server. + """ + + PATTERNS = admin_patterns( + "/media/unquarantine/(?P[^/]+)/(?P[^/]+)" + ) + + def __init__(self, hs: "HomeServer"): + self.store = hs.get_datastore() + self.auth = hs.get_auth() + + async def on_POST( + self, request: SynapseRequest, server_name: str, media_id: str + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + await assert_user_is_admin(self.auth, requester.user) + + logging.info( + "Remove from quarantine local media by ID: %s/%s", server_name, media_id + ) + + # Remove from quarantine this media id + await self.store.quarantine_media_by_id(server_name, media_id, None) + + return 200, {} + + class ProtectMediaByID(RestServlet): """Protect local media from being quarantined.""" @@ -290,6 +319,7 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server): PurgeMediaCacheRestServlet(hs).register(http_server) QuarantineMediaInRoom(hs).register(http_server) QuarantineMediaByID(hs).register(http_server) + UnquarantineMediaByID(hs).register(http_server) QuarantineMediaByUser(hs).register(http_server) ProtectMediaByID(hs).register(http_server) UnprotectMediaByID(hs).register(http_server) diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 0cf450f81d..2a96bcd314 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -764,14 +764,15 @@ async def quarantine_media_by_id( self, server_name: str, media_id: str, - quarantined_by: str, + quarantined_by: Optional[str], ) -> int: - """quarantines a single local or remote media id + """quarantines or unquarantines a single local or remote media id Args: server_name: The name of the server that holds this media media_id: The ID of the media to be quarantined quarantined_by: The user ID that initiated the quarantine request + If it is `None` media will be removed from quarantine """ logger.info("Quarantining media: %s/%s", server_name, media_id) is_local = server_name == self.config.server_name @@ -838,9 +839,9 @@ def _quarantine_media_txn( txn, local_mxcs: List[str], remote_mxcs: List[Tuple[str, str]], - quarantined_by: str, + quarantined_by: Optional[str], ) -> int: - """Quarantine local and remote media items + """Quarantine and unquarantine local and remote media items Args: txn (cursor) @@ -848,18 +849,27 @@ def _quarantine_media_txn( remote_mxcs: A list of (remote server, media id) tuples representing remote mxc URLs quarantined_by: The ID of the user who initiated the quarantine request + If it is `None` media will be removed from quarantine Returns: The total number of media items quarantined """ + # Update all the tables to set the quarantined_by flag - txn.executemany( - """ + sql = """ UPDATE local_media_repository SET quarantined_by = ? - WHERE media_id = ? AND safe_from_quarantine = ? - """, - ((quarantined_by, media_id, False) for media_id in local_mxcs), - ) + WHERE media_id = ? + """ + + # set quarantine + if quarantined_by is not None: + sql += "AND safe_from_quarantine = ?" + rows = [(quarantined_by, media_id, False) for media_id in local_mxcs] + # remove from quarantine + else: + rows = [(quarantined_by, media_id) for media_id in local_mxcs] + + txn.executemany(sql, rows) # Note that a rowcount of -1 can be used to indicate no rows were affected. total_media_quarantined = txn.rowcount if txn.rowcount > 0 else 0 diff --git a/tests/rest/admin/test_media.py b/tests/rest/admin/test_media.py index f741121ea2..6fee0f95b6 100644 --- a/tests/rest/admin/test_media.py +++ b/tests/rest/admin/test_media.py @@ -566,6 +566,134 @@ def _access_media(self, server_and_media_id, expect_success=True): self.assertFalse(os.path.exists(local_path)) +class QuarantineMediaByIDTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets, + synapse.rest.admin.register_servlets_for_media_repo, + login.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + media_repo = hs.get_media_repository_resource() + self.store = hs.get_datastore() + self.server_name = hs.hostname + + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + # Create media + upload_resource = media_repo.children[b"upload"] + # file size is 67 Byte + image_data = unhexlify( + b"89504e470d0a1a0a0000000d4948445200000001000000010806" + b"0000001f15c4890000000a49444154789c63000100000500010d" + b"0a2db40000000049454e44ae426082" + ) + + # Upload some media into the room + response = self.helper.upload_media( + upload_resource, image_data, tok=self.admin_user_tok, expect_code=200 + ) + # Extract media ID from the response + server_and_media_id = response["content_uri"][6:] # Cut off 'mxc://' + self.media_id = server_and_media_id.split("/")[1] + + self.url = "/_synapse/admin/v1/media/%s/%s/%s" + + @parameterized.expand(["quarantine", "unquarantine"]) + def test_no_auth(self, action: str): + """ + Try to protect media without authentication. + """ + + channel = self.make_request( + "POST", + self.url % (action, self.server_name, self.media_id), + b"{}", + ) + + self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) + + @parameterized.expand(["quarantine", "unquarantine"]) + def test_requester_is_no_admin(self, action: str): + """ + If the user is not a server admin, an error is returned. + """ + self.other_user = self.register_user("user", "pass") + self.other_user_token = self.login("user", "pass") + + channel = self.make_request( + "POST", + self.url % (action, self.server_name, self.media_id), + access_token=self.other_user_token, + ) + + self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_quarantine_media(self): + """ + Tests that quarantining and remove from quarantine a media is successfully + """ + + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertFalse(media_info["quarantined_by"]) + + # quarantining + channel = self.make_request( + "POST", + self.url % ("quarantine", self.server_name, self.media_id), + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertFalse(channel.json_body) + + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertTrue(media_info["quarantined_by"]) + + # remove from quarantine + channel = self.make_request( + "POST", + self.url % ("unquarantine", self.server_name, self.media_id), + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertFalse(channel.json_body) + + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertFalse(media_info["quarantined_by"]) + + def test_quarantine_protected_media(self): + """ + Tests that quarantining from protected media fails + """ + + # protect + self.get_success(self.store.mark_local_media_as_safe(self.media_id, safe=True)) + + # verify protection + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertTrue(media_info["safe_from_quarantine"]) + + # quarantining + channel = self.make_request( + "POST", + self.url % ("quarantine", self.server_name, self.media_id), + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertFalse(channel.json_body) + + # verify that is not in quarantine + media_info = self.get_success(self.store.get_local_media(self.media_id)) + self.assertFalse(media_info["quarantined_by"]) + + class ProtectMediaByIDTestCase(unittest.HomeserverTestCase): servlets = [ From 36a7ff0c867e6df969517c58d3eb2520b2ab39d9 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 2 Jun 2021 11:31:41 -0400 Subject: [PATCH 230/619] Do not show invite-only rooms in spaces summary (unless joined/invited). (#10109) --- changelog.d/10109.bugfix | 1 + synapse/handlers/space_summary.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10109.bugfix diff --git a/changelog.d/10109.bugfix b/changelog.d/10109.bugfix new file mode 100644 index 0000000000..bc41bf9e5e --- /dev/null +++ b/changelog.d/10109.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.35.0 where invite-only rooms would be shown to users in a space who were not invited. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index abd9ddecca..046dba6fd8 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -26,7 +26,6 @@ HistoryVisibility, Membership, ) -from synapse.api.errors import AuthError from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 from synapse.types import JsonDict @@ -456,16 +455,16 @@ async def _is_room_accessible( return True # Otherwise, check if they should be allowed access via membership in a space. - try: - await self._event_auth_handler.check_restricted_join_rules( - state_ids, room_version, requester, member_event + if self._event_auth_handler.has_restricted_join_rules( + state_ids, room_version + ): + allowed_spaces = ( + await self._event_auth_handler.get_spaces_that_allow_join(state_ids) ) - except AuthError: - # The user doesn't have access due to spaces, but might have access - # another way. Keep trying. - pass - else: - return True + if await self._event_auth_handler.is_user_in_rooms( + allowed_spaces, requester + ): + return True # If this is a request over federation, check if the host is in the room or # is in one of the spaces specified via the join rules. From 57c01dca297b7e14eb7be2b40d80f7577002754f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 3 Jun 2021 08:18:22 -0400 Subject: [PATCH 231/619] 1.35.1 --- CHANGES.md | 9 +++++++++ changelog.d/10109.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/10109.bugfix diff --git a/CHANGES.md b/CHANGES.md index f03a53affc..5794f2bffd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.35.1 (2021-06-03) +=========================== + +Bugfixes +-------- + +- Fix a bug introduced in v1.35.0 where invite-only rooms would be shown to users in a space who were not invited. ([\#10109](https://github.com/matrix-org/synapse/issues/10109)) + + Synapse 1.35.0 (2021-06-01) =========================== diff --git a/changelog.d/10109.bugfix b/changelog.d/10109.bugfix deleted file mode 100644 index bc41bf9e5e..0000000000 --- a/changelog.d/10109.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.35.0 where invite-only rooms would be shown to users in a space who were not invited. diff --git a/debian/changelog b/debian/changelog index d5efb8ccba..084e878def 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.35.1) stable; urgency=medium + + * New synapse release 1.35.1. + + -- Synapse Packaging team Thu, 03 Jun 2021 08:11:29 -0400 + matrix-synapse-py3 (1.35.0) stable; urgency=medium * New synapse release 1.35.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index d9843a1708..445e8a5cad 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.35.0" +__version__ = "1.35.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 56667733419ebf070f1a7f7c9a04070f1b944572 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 3 Jun 2021 08:19:38 -0400 Subject: [PATCH 232/619] Clarify changelog. --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5794f2bffd..04d260f8e5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Synapse 1.35.1 (2021-06-03) Bugfixes -------- -- Fix a bug introduced in v1.35.0 where invite-only rooms would be shown to users in a space who were not invited. ([\#10109](https://github.com/matrix-org/synapse/issues/10109)) +- Fix a bug introduced in v1.35.0 where invite-only rooms would be shown to all users in a space, regardless of if the user had access to it. ([\#10109](https://github.com/matrix-org/synapse/issues/10109)) Synapse 1.35.0 (2021-06-01) From 5325f0308c5937d2e4447d2c64c8819b3c148d9c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 3 Jun 2021 06:50:49 -0600 Subject: [PATCH 233/619] r0.6.1 support: /rooms/:roomId/aliases endpoint (#9224) [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432) added this endpoint originally but it has since been included in the spec for nearly a year. This is progress towards https://github.com/matrix-org/synapse/issues/8334 --- changelog.d/9224.feature | 1 + synapse/rest/client/v1/room.py | 2 +- tests/rest/client/v1/test_rooms.py | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9224.feature diff --git a/changelog.d/9224.feature b/changelog.d/9224.feature new file mode 100644 index 0000000000..76519c23e2 --- /dev/null +++ b/changelog.d/9224.feature @@ -0,0 +1 @@ +Add new endpoint `/_matrix/client/r0/rooms/{roomId}/aliases` from Client-Server API r0.6.1 (previously [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432)). diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 70286b0ff7..5a9c27f75f 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -910,7 +910,7 @@ class RoomAliasListServlet(RestServlet): r"^/_matrix/client/unstable/org\.matrix\.msc2432" r"/rooms/(?P[^/]*)/aliases" ), - ] + ] + list(client_patterns("/rooms/(?P[^/]*)/aliases$", unstable=False)) def __init__(self, hs: "HomeServer"): super().__init__() diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 7c4bdcdfdd..5b1096d091 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1880,8 +1880,7 @@ def _get_aliases(self, access_token: str, expected_code: int = 200) -> JsonDict: """Calls the endpoint under test. returns the json response object.""" channel = self.make_request( "GET", - "/_matrix/client/unstable/org.matrix.msc2432/rooms/%s/aliases" - % (self.room_id,), + "/_matrix/client/r0/rooms/%s/aliases" % (self.room_id,), access_token=access_token, ) self.assertEqual(channel.code, expected_code, channel.result) From 73636cab69c32746ef6b7708deeeb0c718b7b3b9 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 3 Jun 2021 14:06:03 +0100 Subject: [PATCH 234/619] Convert admin api docs to markdown (#10089) So that they render nicely in mdbook (see #10086), and so that we no longer have a mix of structured text languages in our documentation (excluding files outside of `docs/`). --- changelog.d/10089.doc | 1 + docs/admin_api/account_validity.md | 42 + docs/admin_api/account_validity.rst | 42 - ...e_history_api.rst => purge_history_api.md} | 63 +- docs/admin_api/register_api.md | 73 ++ docs/admin_api/register_api.rst | 68 -- docs/admin_api/user_admin_api.md | 1001 +++++++++++++++++ docs/admin_api/user_admin_api.rst | 981 ---------------- .../{version_api.rst => version_api.md} | 21 +- 9 files changed, 1160 insertions(+), 1132 deletions(-) create mode 100644 changelog.d/10089.doc create mode 100644 docs/admin_api/account_validity.md delete mode 100644 docs/admin_api/account_validity.rst rename docs/admin_api/{purge_history_api.rst => purge_history_api.md} (56%) create mode 100644 docs/admin_api/register_api.md delete mode 100644 docs/admin_api/register_api.rst create mode 100644 docs/admin_api/user_admin_api.md delete mode 100644 docs/admin_api/user_admin_api.rst rename docs/admin_api/{version_api.rst => version_api.md} (59%) diff --git a/changelog.d/10089.doc b/changelog.d/10089.doc new file mode 100644 index 0000000000..d9e93773ab --- /dev/null +++ b/changelog.d/10089.doc @@ -0,0 +1 @@ +Convert the remaining Admin API documentation files to markdown. diff --git a/docs/admin_api/account_validity.md b/docs/admin_api/account_validity.md new file mode 100644 index 0000000000..b74b5d0c1a --- /dev/null +++ b/docs/admin_api/account_validity.md @@ -0,0 +1,42 @@ +# Account validity API + +This API allows a server administrator to manage the validity of an account. To +use it, you must enable the account validity feature (under +`account_validity`) in Synapse's configuration. + +## Renew account + +This API extends the validity of an account by as much time as configured in the +`period` parameter from the `account_validity` configuration. + +The API is: + +``` +POST /_synapse/admin/v1/account_validity/validity +``` + +with the following body: + +```json +{ + "user_id": "", + "expiration_ts": 0, + "enable_renewal_emails": true +} +``` + + +`expiration_ts` is an optional parameter and overrides the expiration date, +which otherwise defaults to now + validity period. + +`enable_renewal_emails` is also an optional parameter and enables/disables +sending renewal emails to the user. Defaults to true. + +The API returns with the new expiration date for this account, as a timestamp in +milliseconds since epoch: + +```json +{ + "expiration_ts": 0 +} +``` diff --git a/docs/admin_api/account_validity.rst b/docs/admin_api/account_validity.rst deleted file mode 100644 index 7559de4c57..0000000000 --- a/docs/admin_api/account_validity.rst +++ /dev/null @@ -1,42 +0,0 @@ -Account validity API -==================== - -This API allows a server administrator to manage the validity of an account. To -use it, you must enable the account validity feature (under -``account_validity``) in Synapse's configuration. - -Renew account -------------- - -This API extends the validity of an account by as much time as configured in the -``period`` parameter from the ``account_validity`` configuration. - -The API is:: - - POST /_synapse/admin/v1/account_validity/validity - -with the following body: - -.. code:: json - - { - "user_id": "", - "expiration_ts": 0, - "enable_renewal_emails": true - } - - -``expiration_ts`` is an optional parameter and overrides the expiration date, -which otherwise defaults to now + validity period. - -``enable_renewal_emails`` is also an optional parameter and enables/disables -sending renewal emails to the user. Defaults to true. - -The API returns with the new expiration date for this account, as a timestamp in -milliseconds since epoch: - -.. code:: json - - { - "expiration_ts": 0 - } diff --git a/docs/admin_api/purge_history_api.rst b/docs/admin_api/purge_history_api.md similarity index 56% rename from docs/admin_api/purge_history_api.rst rename to docs/admin_api/purge_history_api.md index 92cd05f2a0..44971acd91 100644 --- a/docs/admin_api/purge_history_api.rst +++ b/docs/admin_api/purge_history_api.md @@ -1,5 +1,4 @@ -Purge History API -================= +# Purge History API The purge history API allows server admins to purge historic events from their database, reclaiming disk space. @@ -13,10 +12,12 @@ delete the last message in a room. The API is: -``POST /_synapse/admin/v1/purge_history/[/]`` +``` +POST /_synapse/admin/v1/purge_history/[/] +``` -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) By default, events sent by local users are not deleted, as they may represent the only copies of this content in existence. (Events sent by remote users are @@ -24,54 +25,54 @@ deleted.) Room state data (such as joins, leaves, topic) is always preserved. -To delete local message events as well, set ``delete_local_events`` in the body: +To delete local message events as well, set `delete_local_events` in the body: -.. code:: json - - { - "delete_local_events": true - } +``` +{ + "delete_local_events": true +} +``` The caller must specify the point in the room to purge up to. This can be specified by including an event_id in the URI, or by setting a -``purge_up_to_event_id`` or ``purge_up_to_ts`` in the request body. If an event +`purge_up_to_event_id` or `purge_up_to_ts` in the request body. If an event id is given, that event (and others at the same graph depth) will be retained. -If ``purge_up_to_ts`` is given, it should be a timestamp since the unix epoch, +If `purge_up_to_ts` is given, it should be a timestamp since the unix epoch, in milliseconds. The API starts the purge running, and returns immediately with a JSON body with a purge id: -.. code:: json - - { - "purge_id": "" - } +```json +{ + "purge_id": "" +} +``` -Purge status query ------------------- +## Purge status query It is possible to poll for updates on recent purges with a second API; -``GET /_synapse/admin/v1/purge_history_status/`` +``` +GET /_synapse/admin/v1/purge_history_status/ +``` -Again, you will need to authenticate by providing an ``access_token`` for a +Again, you will need to authenticate by providing an `access_token` for a server admin. This API returns a JSON body like the following: -.. code:: json - - { - "status": "active" - } +```json +{ + "status": "active" +} +``` -The status will be one of ``active``, ``complete``, or ``failed``. +The status will be one of `active`, `complete`, or `failed`. -Reclaim disk space (Postgres) ------------------------------ +## Reclaim disk space (Postgres) To reclaim the disk space and return it to the operating system, you need to run `VACUUM FULL;` on the database. -https://www.postgresql.org/docs/current/sql-vacuum.html + diff --git a/docs/admin_api/register_api.md b/docs/admin_api/register_api.md new file mode 100644 index 0000000000..c346090bb1 --- /dev/null +++ b/docs/admin_api/register_api.md @@ -0,0 +1,73 @@ +# Shared-Secret Registration + +This API allows for the creation of users in an administrative and +non-interactive way. This is generally used for bootstrapping a Synapse +instance with administrator accounts. + +To authenticate yourself to the server, you will need both the shared secret +(`registration_shared_secret` in the homeserver configuration), and a +one-time nonce. If the registration shared secret is not configured, this API +is not enabled. + +To fetch the nonce, you need to request one from the API: + +``` +> GET /_synapse/admin/v1/register + +< {"nonce": "thisisanonce"} +``` + +Once you have the nonce, you can make a `POST` to the same URL with a JSON +body containing the nonce, username, password, whether they are an admin +(optional, False by default), and a HMAC digest of the content. Also you can +set the displayname (optional, `username` by default). + +As an example: + +``` +> POST /_synapse/admin/v1/register +> { + "nonce": "thisisanonce", + "username": "pepper_roni", + "displayname": "Pepper Roni", + "password": "pizza", + "admin": true, + "mac": "mac_digest_here" + } + +< { + "access_token": "token_here", + "user_id": "@pepper_roni:localhost", + "home_server": "test", + "device_id": "device_id_here" + } +``` + +The MAC is the hex digest output of the HMAC-SHA1 algorithm, with the key being +the shared secret and the content being the nonce, user, password, either the +string "admin" or "notadmin", and optionally the user_type +each separated by NULs. For an example of generation in Python: + +```python +import hmac, hashlib + +def generate_mac(nonce, user, password, admin=False, user_type=None): + + mac = hmac.new( + key=shared_secret, + digestmod=hashlib.sha1, + ) + + mac.update(nonce.encode('utf8')) + mac.update(b"\x00") + mac.update(user.encode('utf8')) + mac.update(b"\x00") + mac.update(password.encode('utf8')) + mac.update(b"\x00") + mac.update(b"admin" if admin else b"notadmin") + if user_type: + mac.update(b"\x00") + mac.update(user_type.encode('utf8')) + + return mac.hexdigest() +``` \ No newline at end of file diff --git a/docs/admin_api/register_api.rst b/docs/admin_api/register_api.rst deleted file mode 100644 index c3057b204b..0000000000 --- a/docs/admin_api/register_api.rst +++ /dev/null @@ -1,68 +0,0 @@ -Shared-Secret Registration -========================== - -This API allows for the creation of users in an administrative and -non-interactive way. This is generally used for bootstrapping a Synapse -instance with administrator accounts. - -To authenticate yourself to the server, you will need both the shared secret -(``registration_shared_secret`` in the homeserver configuration), and a -one-time nonce. If the registration shared secret is not configured, this API -is not enabled. - -To fetch the nonce, you need to request one from the API:: - - > GET /_synapse/admin/v1/register - - < {"nonce": "thisisanonce"} - -Once you have the nonce, you can make a ``POST`` to the same URL with a JSON -body containing the nonce, username, password, whether they are an admin -(optional, False by default), and a HMAC digest of the content. Also you can -set the displayname (optional, ``username`` by default). - -As an example:: - - > POST /_synapse/admin/v1/register - > { - "nonce": "thisisanonce", - "username": "pepper_roni", - "displayname": "Pepper Roni", - "password": "pizza", - "admin": true, - "mac": "mac_digest_here" - } - - < { - "access_token": "token_here", - "user_id": "@pepper_roni:localhost", - "home_server": "test", - "device_id": "device_id_here" - } - -The MAC is the hex digest output of the HMAC-SHA1 algorithm, with the key being -the shared secret and the content being the nonce, user, password, either the -string "admin" or "notadmin", and optionally the user_type -each separated by NULs. For an example of generation in Python:: - - import hmac, hashlib - - def generate_mac(nonce, user, password, admin=False, user_type=None): - - mac = hmac.new( - key=shared_secret, - digestmod=hashlib.sha1, - ) - - mac.update(nonce.encode('utf8')) - mac.update(b"\x00") - mac.update(user.encode('utf8')) - mac.update(b"\x00") - mac.update(password.encode('utf8')) - mac.update(b"\x00") - mac.update(b"admin" if admin else b"notadmin") - if user_type: - mac.update(b"\x00") - mac.update(user_type.encode('utf8')) - - return mac.hexdigest() diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md new file mode 100644 index 0000000000..0c843316c9 --- /dev/null +++ b/docs/admin_api/user_admin_api.md @@ -0,0 +1,1001 @@ +# User Admin API + +## Query User Account + +This API returns information about a specific user account. + +The api is: + +``` +GET /_synapse/admin/v2/users/ +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +It returns a JSON body like the following: + +```json +{ + "displayname": "User", + "threepids": [ + { + "medium": "email", + "address": "" + }, + { + "medium": "email", + "address": "" + } + ], + "avatar_url": "", + "admin": 0, + "deactivated": 0, + "shadow_banned": 0, + "password_hash": "$2b$12$p9B4GkqYdRTPGD", + "creation_ts": 1560432506, + "appservice_id": null, + "consent_server_notice_sent": null, + "consent_version": null +} +``` + +URL parameters: + +- `user_id`: fully-qualified user id: for example, `@user:server.com`. + +## Create or modify Account + +This API allows an administrator to create or modify a user account with a +specific `user_id`. + +This api is: + +``` +PUT /_synapse/admin/v2/users/ +``` + +with a body of: + +```json +{ + "password": "user_password", + "displayname": "User", + "threepids": [ + { + "medium": "email", + "address": "" + }, + { + "medium": "email", + "address": "" + } + ], + "avatar_url": "", + "admin": false, + "deactivated": false +} +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +URL parameters: + +- `user_id`: fully-qualified user id: for example, `@user:server.com`. + +Body parameters: + +- `password`, optional. If provided, the user's password is updated and all + devices are logged out. + +- `displayname`, optional, defaults to the value of `user_id`. + +- `threepids`, optional, allows setting the third-party IDs (email, msisdn) + belonging to a user. + +- `avatar_url`, optional, must be a + [MXC URI](https://matrix.org/docs/spec/client_server/r0.6.0#matrix-content-mxc-uris). + +- `admin`, optional, defaults to `false`. + +- `deactivated`, optional. If unspecified, deactivation state will be left + unchanged on existing accounts and set to `false` for new accounts. + A user cannot be erased by deactivating with this API. For details on + deactivating users see [Deactivate Account](#deactivate-account). + +If the user already exists then optional parameters default to the current value. + +In order to re-activate an account `deactivated` must be set to `false`. If +users do not login via single-sign-on, a new `password` must be provided. + +## List Accounts + +This API returns all local user accounts. +By default, the response is ordered by ascending user ID. + +``` +GET /_synapse/admin/v2/users?from=0&limit=10&guests=false +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "users": [ + { + "name": "", + "is_guest": 0, + "admin": 0, + "user_type": null, + "deactivated": 0, + "shadow_banned": 0, + "displayname": "", + "avatar_url": null + }, { + "name": "", + "is_guest": 0, + "admin": 1, + "user_type": null, + "deactivated": 0, + "shadow_banned": 0, + "displayname": "", + "avatar_url": "" + } + ], + "next_token": "100", + "total": 200 +} +``` + +To paginate, check for `next_token` and if present, call the endpoint again +with `from` set to the value of `next_token`. This will return a new page. + +If the endpoint does not return a `next_token` then there are no more users +to paginate through. + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - Is optional and filters to only return users with user IDs + that contain this value. This parameter is ignored when using the `name` parameter. +- `name` - Is optional and filters to only return users with user ID localparts + **or** displaynames that contain this value. +- `guests` - string representing a bool - Is optional and if `false` will **exclude** guest users. + Defaults to `true` to include guest users. +- `deactivated` - string representing a bool - Is optional and if `true` will **include** deactivated users. + Defaults to `false` to exclude deactivated users. +- `limit` - string representing a positive integer - Is optional but is used for pagination, + denoting the maximum number of items to return in this call. Defaults to `100`. +- `from` - string representing a positive integer - Is optional but used for pagination, + denoting the offset in the returned results. This should be treated as an opaque value and + not explicitly set to anything other than the return value of `next_token` from a previous call. + Defaults to `0`. +- `order_by` - The method by which to sort the returned list of users. + If the ordered field has duplicates, the second order is always by ascending `name`, + which guarantees a stable ordering. Valid values are: + + - `name` - Users are ordered alphabetically by `name`. This is the default. + - `is_guest` - Users are ordered by `is_guest` status. + - `admin` - Users are ordered by `admin` status. + - `user_type` - Users are ordered alphabetically by `user_type`. + - `deactivated` - Users are ordered by `deactivated` status. + - `shadow_banned` - Users are ordered by `shadow_banned` status. + - `displayname` - Users are ordered alphabetically by `displayname`. + - `avatar_url` - Users are ordered alphabetically by avatar URL. + +- `dir` - Direction of media order. Either `f` for forwards or `b` for backwards. + Setting this value to `b` will reverse the above sort order. Defaults to `f`. + +Caution. The database only has indexes on the columns `name` and `created_ts`. +This means that if a different sort order is used (`is_guest`, `admin`, +`user_type`, `deactivated`, `shadow_banned`, `avatar_url` or `displayname`), +this can cause a large load on the database, especially for large environments. + +**Response** + +The following fields are returned in the JSON response body: + +- `users` - An array of objects, each containing information about an user. + User objects contain the following fields: + + - `name` - string - Fully-qualified user ID (ex. `@user:server.com`). + - `is_guest` - bool - Status if that user is a guest account. + - `admin` - bool - Status if that user is a server administrator. + - `user_type` - string - Type of the user. Normal users are type `None`. + This allows user type specific behaviour. There are also types `support` and `bot`. + - `deactivated` - bool - Status if that user has been marked as deactivated. + - `shadow_banned` - bool - Status if that user has been marked as shadow banned. + - `displayname` - string - The user's display name if they have set one. + - `avatar_url` - string - The user's avatar URL if they have set one. + +- `next_token`: string representing a positive integer - Indication for pagination. See above. +- `total` - integer - Total number of media. + + +## Query current sessions for a user + +This API returns information about the active sessions for a specific user. + +The endpoints are: + +``` +GET /_synapse/admin/v1/whois/ +``` + +and: + +``` +GET /_matrix/client/r0/admin/whois/ +``` + +See also: [Client Server +API Whois](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-admin-whois-userid). + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +It returns a JSON body like the following: + +```json +{ + "user_id": "", + "devices": { + "": { + "sessions": [ + { + "connections": [ + { + "ip": "1.2.3.4", + "last_seen": 1417222374433, + "user_agent": "Mozilla/5.0 ..." + }, + { + "ip": "1.2.3.10", + "last_seen": 1417222374500, + "user_agent": "Dalvik/2.1.0 ..." + } + ] + } + ] + } + } +} +``` + +`last_seen` is measured in milliseconds since the Unix epoch. + +## Deactivate Account + +This API deactivates an account. It removes active access tokens, resets the +password, and deletes third-party IDs (to prevent the user requesting a +password reset). + +It can also mark the user as GDPR-erased. This means messages sent by the +user will still be visible by anyone that was in the room when these messages +were sent, but hidden from users joining the room afterwards. + +The api is: + +``` +POST /_synapse/admin/v1/deactivate/ +``` + +with a body of: + +```json +{ + "erase": true +} +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +The erase parameter is optional and defaults to `false`. +An empty body may be passed for backwards compatibility. + +The following actions are performed when deactivating an user: + +- Try to unpind 3PIDs from the identity server +- Remove all 3PIDs from the homeserver +- Delete all devices and E2EE keys +- Delete all access tokens +- Delete the password hash +- Removal from all rooms the user is a member of +- Remove the user from the user directory +- Reject all pending invites +- Remove all account validity information related to the user + +The following additional actions are performed during deactivation if `erase` +is set to `true`: + +- Remove the user's display name +- Remove the user's avatar URL +- Mark the user as erased + + +## Reset password + +Changes the password of another user. This will automatically log the user out of all their devices. + +The api is: + +``` +POST /_synapse/admin/v1/reset_password/ +``` + +with a body of: + +```json +{ + "new_password": "", + "logout_devices": true +} +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +The parameter `new_password` is required. +The parameter `logout_devices` is optional and defaults to `true`. + + +## Get whether a user is a server administrator or not + +The api is: + +``` +GET /_synapse/admin/v1/users//admin +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "admin": true +} +``` + + +## Change whether a user is a server administrator or not + +Note that you cannot demote yourself. + +The api is: + +``` +PUT /_synapse/admin/v1/users//admin +``` + +with a body of: + +```json +{ + "admin": true +} +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + + +## List room memberships of a user + +Gets a list of all `room_id` that a specific `user_id` is member. + +The API is: + +``` +GET /_synapse/admin/v1/users//joined_rooms +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json + { + "joined_rooms": [ + "!DuGcnbhHGaSZQoNQR:matrix.org", + "!ZtSaPCawyWtxfWiIy:matrix.org" + ], + "total": 2 + } +``` + +The server returns the list of rooms of which the user and the server +are member. If the user is local, all the rooms of which the user is +member are returned. + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - fully qualified: for example, `@user:server.com`. + +**Response** + +The following fields are returned in the JSON response body: + +- `joined_rooms` - An array of `room_id`. +- `total` - Number of rooms. + + +## List media of a user +Gets a list of all local media that a specific `user_id` has created. +By default, the response is ordered by descending creation date and ascending media ID. +The newest media is on top. You can change the order with parameters +`order_by` and `dir`. + +The API is: + +``` +GET /_synapse/admin/v1/users//media +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "media": [ + { + "created_ts": 100400, + "last_access_ts": null, + "media_id": "qXhyRzulkwLsNHTbpHreuEgo", + "media_length": 67, + "media_type": "image/png", + "quarantined_by": null, + "safe_from_quarantine": false, + "upload_name": "test1.png" + }, + { + "created_ts": 200400, + "last_access_ts": null, + "media_id": "FHfiSnzoINDatrXHQIXBtahw", + "media_length": 67, + "media_type": "image/png", + "quarantined_by": null, + "safe_from_quarantine": false, + "upload_name": "test2.png" + } + ], + "next_token": 3, + "total": 2 +} +``` + +To paginate, check for `next_token` and if present, call the endpoint again +with `from` set to the value of `next_token`. This will return a new page. + +If the endpoint does not return a `next_token` then there are no more +reports to paginate through. + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - string - fully qualified: for example, `@user:server.com`. +- `limit`: string representing a positive integer - Is optional but is used for pagination, + denoting the maximum number of items to return in this call. Defaults to `100`. +- `from`: string representing a positive integer - Is optional but used for pagination, + denoting the offset in the returned results. This should be treated as an opaque value and + not explicitly set to anything other than the return value of `next_token` from a previous call. + Defaults to `0`. +- `order_by` - The method by which to sort the returned list of media. + If the ordered field has duplicates, the second order is always by ascending `media_id`, + which guarantees a stable ordering. Valid values are: + + - `media_id` - Media are ordered alphabetically by `media_id`. + - `upload_name` - Media are ordered alphabetically by name the media was uploaded with. + - `created_ts` - Media are ordered by when the content was uploaded in ms. + Smallest to largest. This is the default. + - `last_access_ts` - Media are ordered by when the content was last accessed in ms. + Smallest to largest. + - `media_length` - Media are ordered by length of the media in bytes. + Smallest to largest. + - `media_type` - Media are ordered alphabetically by MIME-type. + - `quarantined_by` - Media are ordered alphabetically by the user ID that + initiated the quarantine request for this media. + - `safe_from_quarantine` - Media are ordered by the status if this media is safe + from quarantining. + +- `dir` - Direction of media order. Either `f` for forwards or `b` for backwards. + Setting this value to `b` will reverse the above sort order. Defaults to `f`. + +If neither `order_by` nor `dir` is set, the default order is newest media on top +(corresponds to `order_by` = `created_ts` and `dir` = `b`). + +Caution. The database only has indexes on the columns `media_id`, +`user_id` and `created_ts`. This means that if a different sort order is used +(`upload_name`, `last_access_ts`, `media_length`, `media_type`, +`quarantined_by` or `safe_from_quarantine`), this can cause a large load on the +database, especially for large environments. + +**Response** + +The following fields are returned in the JSON response body: + +- `media` - An array of objects, each containing information about a media. + Media objects contain the following fields: + + - `created_ts` - integer - Timestamp when the content was uploaded in ms. + - `last_access_ts` - integer - Timestamp when the content was last accessed in ms. + - `media_id` - string - The id used to refer to the media. + - `media_length` - integer - Length of the media in bytes. + - `media_type` - string - The MIME-type of the media. + - `quarantined_by` - string - The user ID that initiated the quarantine request + for this media. + + - `safe_from_quarantine` - bool - Status if this media is safe from quarantining. + - `upload_name` - string - The name the media was uploaded with. + +- `next_token`: integer - Indication for pagination. See above. +- `total` - integer - Total number of media. + +## Login as a user + +Get an access token that can be used to authenticate as that user. Useful for +when admins wish to do actions on behalf of a user. + +The API is: + +``` +POST /_synapse/admin/v1/users//login +{} +``` + +An optional `valid_until_ms` field can be specified in the request body as an +integer timestamp that specifies when the token should expire. By default tokens +do not expire. + +A response body like the following is returned: + +```json +{ + "access_token": "" +} +``` + +This API does *not* generate a new device for the user, and so will not appear +their `/devices` list, and in general the target user should not be able to +tell they have been logged in as. + +To expire the token call the standard `/logout` API with the token. + +Note: The token will expire if the *admin* user calls `/logout/all` from any +of their devices, but the token will *not* expire if the target user does the +same. + + +## User devices + +### List all devices +Gets information about all devices for a specific `user_id`. + +The API is: + +``` +GET /_synapse/admin/v2/users//devices +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "devices": [ + { + "device_id": "QBUAZIFURK", + "display_name": "android", + "last_seen_ip": "1.2.3.4", + "last_seen_ts": 1474491775024, + "user_id": "" + }, + { + "device_id": "AUIECTSRND", + "display_name": "ios", + "last_seen_ip": "1.2.3.5", + "last_seen_ts": 1474491775025, + "user_id": "" + } + ], + "total": 2 +} +``` + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - fully qualified: for example, `@user:server.com`. + +**Response** + +The following fields are returned in the JSON response body: + +- `devices` - An array of objects, each containing information about a device. + Device objects contain the following fields: + + - `device_id` - Identifier of device. + - `display_name` - Display name set by the user for this device. + Absent if no name has been set. + - `last_seen_ip` - The IP address where this device was last seen. + (May be a few minutes out of date, for efficiency reasons). + - `last_seen_ts` - The timestamp (in milliseconds since the unix epoch) when this + devices was last seen. (May be a few minutes out of date, for efficiency reasons). + - `user_id` - Owner of device. + +- `total` - Total number of user's devices. + +### Delete multiple devices +Deletes the given devices for a specific `user_id`, and invalidates +any access token associated with them. + +The API is: + +``` +POST /_synapse/admin/v2/users//delete_devices + +{ + "devices": [ + "QBUAZIFURK", + "AUIECTSRND" + ], +} +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +An empty JSON dict is returned. + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - fully qualified: for example, `@user:server.com`. + +The following fields are required in the JSON request body: + +- `devices` - The list of device IDs to delete. + +### Show a device +Gets information on a single device, by `device_id` for a specific `user_id`. + +The API is: + +``` +GET /_synapse/admin/v2/users//devices/ +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "device_id": "", + "display_name": "android", + "last_seen_ip": "1.2.3.4", + "last_seen_ts": 1474491775024, + "user_id": "" +} +``` + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - fully qualified: for example, `@user:server.com`. +- `device_id` - The device to retrieve. + +**Response** + +The following fields are returned in the JSON response body: + +- `device_id` - Identifier of device. +- `display_name` - Display name set by the user for this device. + Absent if no name has been set. +- `last_seen_ip` - The IP address where this device was last seen. + (May be a few minutes out of date, for efficiency reasons). +- `last_seen_ts` - The timestamp (in milliseconds since the unix epoch) when this + devices was last seen. (May be a few minutes out of date, for efficiency reasons). +- `user_id` - Owner of device. + +### Update a device +Updates the metadata on the given `device_id` for a specific `user_id`. + +The API is: + +``` +PUT /_synapse/admin/v2/users//devices/ + +{ + "display_name": "My other phone" +} +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +An empty JSON dict is returned. + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - fully qualified: for example, `@user:server.com`. +- `device_id` - The device to update. + +The following fields are required in the JSON request body: + +- `display_name` - The new display name for this device. If not given, + the display name is unchanged. + +### Delete a device +Deletes the given `device_id` for a specific `user_id`, +and invalidates any access token associated with it. + +The API is: + +``` +DELETE /_synapse/admin/v2/users//devices/ + +{} +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +An empty JSON dict is returned. + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - fully qualified: for example, `@user:server.com`. +- `device_id` - The device to delete. + +## List all pushers +Gets information about all pushers for a specific `user_id`. + +The API is: + +``` +GET /_synapse/admin/v1/users//pushers +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "pushers": [ + { + "app_display_name":"HTTP Push Notifications", + "app_id":"m.http", + "data": { + "url":"example.com" + }, + "device_display_name":"pushy push", + "kind":"http", + "lang":"None", + "profile_tag":"", + "pushkey":"a@example.com" + } + ], + "total": 1 +} +``` + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - fully qualified: for example, `@user:server.com`. + +**Response** + +The following fields are returned in the JSON response body: + +- `pushers` - An array containing the current pushers for the user + + - `app_display_name` - string - A string that will allow the user to identify + what application owns this pusher. + + - `app_id` - string - This is a reverse-DNS style identifier for the application. + Max length, 64 chars. + + - `data` - A dictionary of information for the pusher implementation itself. + + - `url` - string - Required if `kind` is `http`. The URL to use to send + notifications to. + + - `format` - string - The format to use when sending notifications to the + Push Gateway. + + - `device_display_name` - string - A string that will allow the user to identify + what device owns this pusher. + + - `profile_tag` - string - This string determines which set of device specific rules + this pusher executes. + + - `kind` - string - The kind of pusher. "http" is a pusher that sends HTTP pokes. + - `lang` - string - The preferred language for receiving notifications + (e.g. 'en' or 'en-US') + + - `profile_tag` - string - This string determines which set of device specific rules + this pusher executes. + + - `pushkey` - string - This is a unique identifier for this pusher. + Max length, 512 bytes. + +- `total` - integer - Number of pushers. + +See also the +[Client-Server API Spec on pushers](https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushers). + +## Shadow-banning users + +Shadow-banning is a useful tool for moderating malicious or egregiously abusive users. +A shadow-banned users receives successful responses to their client-server API requests, +but the events are not propagated into rooms. This can be an effective tool as it +(hopefully) takes longer for the user to realise they are being moderated before +pivoting to another account. + +Shadow-banning a user should be used as a tool of last resort and may lead to confusing +or broken behaviour for the client. A shadow-banned user will not receive any +notification and it is generally more appropriate to ban or kick abusive users. +A shadow-banned user will be unable to contact anyone on the server. + +The API is: + +``` +POST /_synapse/admin/v1/users//shadow_ban +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +An empty JSON dict is returned. + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - The fully qualified MXID: for example, `@user:server.com`. The user must + be local. + +## Override ratelimiting for users + +This API allows to override or disable ratelimiting for a specific user. +There are specific APIs to set, get and delete a ratelimit. + +### Get status of ratelimit + +The API is: + +``` +GET /_synapse/admin/v1/users//override_ratelimit +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "messages_per_second": 0, + "burst_count": 0 +} +``` + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - The fully qualified MXID: for example, `@user:server.com`. The user must + be local. + +**Response** + +The following fields are returned in the JSON response body: + +- `messages_per_second` - integer - The number of actions that can + be performed in a second. `0` mean that ratelimiting is disabled for this user. +- `burst_count` - integer - How many actions that can be performed before + being limited. + +If **no** custom ratelimit is set, an empty JSON dict is returned. + +```json +{} +``` + +### Set ratelimit + +The API is: + +``` +POST /_synapse/admin/v1/users//override_ratelimit +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +A response body like the following is returned: + +```json +{ + "messages_per_second": 0, + "burst_count": 0 +} +``` + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - The fully qualified MXID: for example, `@user:server.com`. The user must + be local. + +Body parameters: + +- `messages_per_second` - positive integer, optional. The number of actions that can + be performed in a second. Defaults to `0`. +- `burst_count` - positive integer, optional. How many actions that can be performed + before being limited. Defaults to `0`. + +To disable users' ratelimit set both values to `0`. + +**Response** + +The following fields are returned in the JSON response body: + +- `messages_per_second` - integer - The number of actions that can + be performed in a second. +- `burst_count` - integer - How many actions that can be performed before + being limited. + +### Delete ratelimit + +The API is: + +``` +DELETE /_synapse/admin/v1/users//override_ratelimit +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](README.rst) + +An empty JSON dict is returned. + +```json +{} +``` + +**Parameters** + +The following parameters should be set in the URL: + +- `user_id` - The fully qualified MXID: for example, `@user:server.com`. The user must + be local. + diff --git a/docs/admin_api/user_admin_api.rst b/docs/admin_api/user_admin_api.rst deleted file mode 100644 index dbce9c90b6..0000000000 --- a/docs/admin_api/user_admin_api.rst +++ /dev/null @@ -1,981 +0,0 @@ -.. contents:: - -Query User Account -================== - -This API returns information about a specific user account. - -The api is:: - - GET /_synapse/admin/v2/users/ - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -It returns a JSON body like the following: - -.. code:: json - - { - "displayname": "User", - "threepids": [ - { - "medium": "email", - "address": "" - }, - { - "medium": "email", - "address": "" - } - ], - "avatar_url": "", - "admin": 0, - "deactivated": 0, - "shadow_banned": 0, - "password_hash": "$2b$12$p9B4GkqYdRTPGD", - "creation_ts": 1560432506, - "appservice_id": null, - "consent_server_notice_sent": null, - "consent_version": null - } - -URL parameters: - -- ``user_id``: fully-qualified user id: for example, ``@user:server.com``. - -Create or modify Account -======================== - -This API allows an administrator to create or modify a user account with a -specific ``user_id``. - -This api is:: - - PUT /_synapse/admin/v2/users/ - -with a body of: - -.. code:: json - - { - "password": "user_password", - "displayname": "User", - "threepids": [ - { - "medium": "email", - "address": "" - }, - { - "medium": "email", - "address": "" - } - ], - "avatar_url": "", - "admin": false, - "deactivated": false - } - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -URL parameters: - -- ``user_id``: fully-qualified user id: for example, ``@user:server.com``. - -Body parameters: - -- ``password``, optional. If provided, the user's password is updated and all - devices are logged out. - -- ``displayname``, optional, defaults to the value of ``user_id``. - -- ``threepids``, optional, allows setting the third-party IDs (email, msisdn) - belonging to a user. - -- ``avatar_url``, optional, must be a - `MXC URI `_. - -- ``admin``, optional, defaults to ``false``. - -- ``deactivated``, optional. If unspecified, deactivation state will be left - unchanged on existing accounts and set to ``false`` for new accounts. - A user cannot be erased by deactivating with this API. For details on deactivating users see - `Deactivate Account <#deactivate-account>`_. - -If the user already exists then optional parameters default to the current value. - -In order to re-activate an account ``deactivated`` must be set to ``false``. If -users do not login via single-sign-on, a new ``password`` must be provided. - -List Accounts -============= - -This API returns all local user accounts. -By default, the response is ordered by ascending user ID. - -The API is:: - - GET /_synapse/admin/v2/users?from=0&limit=10&guests=false - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "users": [ - { - "name": "", - "is_guest": 0, - "admin": 0, - "user_type": null, - "deactivated": 0, - "shadow_banned": 0, - "displayname": "", - "avatar_url": null - }, { - "name": "", - "is_guest": 0, - "admin": 1, - "user_type": null, - "deactivated": 0, - "shadow_banned": 0, - "displayname": "", - "avatar_url": "" - } - ], - "next_token": "100", - "total": 200 - } - -To paginate, check for ``next_token`` and if present, call the endpoint again -with ``from`` set to the value of ``next_token``. This will return a new page. - -If the endpoint does not return a ``next_token`` then there are no more users -to paginate through. - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - Is optional and filters to only return users with user IDs - that contain this value. This parameter is ignored when using the ``name`` parameter. -- ``name`` - Is optional and filters to only return users with user ID localparts - **or** displaynames that contain this value. -- ``guests`` - string representing a bool - Is optional and if ``false`` will **exclude** guest users. - Defaults to ``true`` to include guest users. -- ``deactivated`` - string representing a bool - Is optional and if ``true`` will **include** deactivated users. - Defaults to ``false`` to exclude deactivated users. -- ``limit`` - string representing a positive integer - Is optional but is used for pagination, - denoting the maximum number of items to return in this call. Defaults to ``100``. -- ``from`` - string representing a positive integer - Is optional but used for pagination, - denoting the offset in the returned results. This should be treated as an opaque value and - not explicitly set to anything other than the return value of ``next_token`` from a previous call. - Defaults to ``0``. -- ``order_by`` - The method by which to sort the returned list of users. - If the ordered field has duplicates, the second order is always by ascending ``name``, - which guarantees a stable ordering. Valid values are: - - - ``name`` - Users are ordered alphabetically by ``name``. This is the default. - - ``is_guest`` - Users are ordered by ``is_guest`` status. - - ``admin`` - Users are ordered by ``admin`` status. - - ``user_type`` - Users are ordered alphabetically by ``user_type``. - - ``deactivated`` - Users are ordered by ``deactivated`` status. - - ``shadow_banned`` - Users are ordered by ``shadow_banned`` status. - - ``displayname`` - Users are ordered alphabetically by ``displayname``. - - ``avatar_url`` - Users are ordered alphabetically by avatar URL. - -- ``dir`` - Direction of media order. Either ``f`` for forwards or ``b`` for backwards. - Setting this value to ``b`` will reverse the above sort order. Defaults to ``f``. - -Caution. The database only has indexes on the columns ``name`` and ``created_ts``. -This means that if a different sort order is used (``is_guest``, ``admin``, -``user_type``, ``deactivated``, ``shadow_banned``, ``avatar_url`` or ``displayname``), -this can cause a large load on the database, especially for large environments. - -**Response** - -The following fields are returned in the JSON response body: - -- ``users`` - An array of objects, each containing information about an user. - User objects contain the following fields: - - - ``name`` - string - Fully-qualified user ID (ex. ``@user:server.com``). - - ``is_guest`` - bool - Status if that user is a guest account. - - ``admin`` - bool - Status if that user is a server administrator. - - ``user_type`` - string - Type of the user. Normal users are type ``None``. - This allows user type specific behaviour. There are also types ``support`` and ``bot``. - - ``deactivated`` - bool - Status if that user has been marked as deactivated. - - ``shadow_banned`` - bool - Status if that user has been marked as shadow banned. - - ``displayname`` - string - The user's display name if they have set one. - - ``avatar_url`` - string - The user's avatar URL if they have set one. - -- ``next_token``: string representing a positive integer - Indication for pagination. See above. -- ``total`` - integer - Total number of media. - - -Query current sessions for a user -================================= - -This API returns information about the active sessions for a specific user. - -The api is:: - - GET /_synapse/admin/v1/whois/ - -and:: - - GET /_matrix/client/r0/admin/whois/ - -See also: `Client Server API Whois -`_ - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -It returns a JSON body like the following: - -.. code:: json - - { - "user_id": "", - "devices": { - "": { - "sessions": [ - { - "connections": [ - { - "ip": "1.2.3.4", - "last_seen": 1417222374433, - "user_agent": "Mozilla/5.0 ..." - }, - { - "ip": "1.2.3.10", - "last_seen": 1417222374500, - "user_agent": "Dalvik/2.1.0 ..." - } - ] - } - ] - } - } - } - -``last_seen`` is measured in milliseconds since the Unix epoch. - -Deactivate Account -================== - -This API deactivates an account. It removes active access tokens, resets the -password, and deletes third-party IDs (to prevent the user requesting a -password reset). - -It can also mark the user as GDPR-erased. This means messages sent by the -user will still be visible by anyone that was in the room when these messages -were sent, but hidden from users joining the room afterwards. - -The api is:: - - POST /_synapse/admin/v1/deactivate/ - -with a body of: - -.. code:: json - - { - "erase": true - } - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -The erase parameter is optional and defaults to ``false``. -An empty body may be passed for backwards compatibility. - -The following actions are performed when deactivating an user: - -- Try to unpind 3PIDs from the identity server -- Remove all 3PIDs from the homeserver -- Delete all devices and E2EE keys -- Delete all access tokens -- Delete the password hash -- Removal from all rooms the user is a member of -- Remove the user from the user directory -- Reject all pending invites -- Remove all account validity information related to the user - -The following additional actions are performed during deactivation if ``erase`` -is set to ``true``: - -- Remove the user's display name -- Remove the user's avatar URL -- Mark the user as erased - - -Reset password -============== - -Changes the password of another user. This will automatically log the user out of all their devices. - -The api is:: - - POST /_synapse/admin/v1/reset_password/ - -with a body of: - -.. code:: json - - { - "new_password": "", - "logout_devices": true - } - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -The parameter ``new_password`` is required. -The parameter ``logout_devices`` is optional and defaults to ``true``. - -Get whether a user is a server administrator or not -=================================================== - - -The api is:: - - GET /_synapse/admin/v1/users//admin - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "admin": true - } - - -Change whether a user is a server administrator or not -====================================================== - -Note that you cannot demote yourself. - -The api is:: - - PUT /_synapse/admin/v1/users//admin - -with a body of: - -.. code:: json - - { - "admin": true - } - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - - -List room memberships of an user -================================ -Gets a list of all ``room_id`` that a specific ``user_id`` is member. - -The API is:: - - GET /_synapse/admin/v1/users//joined_rooms - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "joined_rooms": [ - "!DuGcnbhHGaSZQoNQR:matrix.org", - "!ZtSaPCawyWtxfWiIy:matrix.org" - ], - "total": 2 - } - -The server returns the list of rooms of which the user and the server -are member. If the user is local, all the rooms of which the user is -member are returned. - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - fully qualified: for example, ``@user:server.com``. - -**Response** - -The following fields are returned in the JSON response body: - -- ``joined_rooms`` - An array of ``room_id``. -- ``total`` - Number of rooms. - - -List media of a user -==================== -Gets a list of all local media that a specific ``user_id`` has created. -By default, the response is ordered by descending creation date and ascending media ID. -The newest media is on top. You can change the order with parameters -``order_by`` and ``dir``. - -The API is:: - - GET /_synapse/admin/v1/users//media - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "media": [ - { - "created_ts": 100400, - "last_access_ts": null, - "media_id": "qXhyRzulkwLsNHTbpHreuEgo", - "media_length": 67, - "media_type": "image/png", - "quarantined_by": null, - "safe_from_quarantine": false, - "upload_name": "test1.png" - }, - { - "created_ts": 200400, - "last_access_ts": null, - "media_id": "FHfiSnzoINDatrXHQIXBtahw", - "media_length": 67, - "media_type": "image/png", - "quarantined_by": null, - "safe_from_quarantine": false, - "upload_name": "test2.png" - } - ], - "next_token": 3, - "total": 2 - } - -To paginate, check for ``next_token`` and if present, call the endpoint again -with ``from`` set to the value of ``next_token``. This will return a new page. - -If the endpoint does not return a ``next_token`` then there are no more -reports to paginate through. - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - string - fully qualified: for example, ``@user:server.com``. -- ``limit``: string representing a positive integer - Is optional but is used for pagination, - denoting the maximum number of items to return in this call. Defaults to ``100``. -- ``from``: string representing a positive integer - Is optional but used for pagination, - denoting the offset in the returned results. This should be treated as an opaque value and - not explicitly set to anything other than the return value of ``next_token`` from a previous call. - Defaults to ``0``. -- ``order_by`` - The method by which to sort the returned list of media. - If the ordered field has duplicates, the second order is always by ascending ``media_id``, - which guarantees a stable ordering. Valid values are: - - - ``media_id`` - Media are ordered alphabetically by ``media_id``. - - ``upload_name`` - Media are ordered alphabetically by name the media was uploaded with. - - ``created_ts`` - Media are ordered by when the content was uploaded in ms. - Smallest to largest. This is the default. - - ``last_access_ts`` - Media are ordered by when the content was last accessed in ms. - Smallest to largest. - - ``media_length`` - Media are ordered by length of the media in bytes. - Smallest to largest. - - ``media_type`` - Media are ordered alphabetically by MIME-type. - - ``quarantined_by`` - Media are ordered alphabetically by the user ID that - initiated the quarantine request for this media. - - ``safe_from_quarantine`` - Media are ordered by the status if this media is safe - from quarantining. - -- ``dir`` - Direction of media order. Either ``f`` for forwards or ``b`` for backwards. - Setting this value to ``b`` will reverse the above sort order. Defaults to ``f``. - -If neither ``order_by`` nor ``dir`` is set, the default order is newest media on top -(corresponds to ``order_by`` = ``created_ts`` and ``dir`` = ``b``). - -Caution. The database only has indexes on the columns ``media_id``, -``user_id`` and ``created_ts``. This means that if a different sort order is used -(``upload_name``, ``last_access_ts``, ``media_length``, ``media_type``, -``quarantined_by`` or ``safe_from_quarantine``), this can cause a large load on the -database, especially for large environments. - -**Response** - -The following fields are returned in the JSON response body: - -- ``media`` - An array of objects, each containing information about a media. - Media objects contain the following fields: - - - ``created_ts`` - integer - Timestamp when the content was uploaded in ms. - - ``last_access_ts`` - integer - Timestamp when the content was last accessed in ms. - - ``media_id`` - string - The id used to refer to the media. - - ``media_length`` - integer - Length of the media in bytes. - - ``media_type`` - string - The MIME-type of the media. - - ``quarantined_by`` - string - The user ID that initiated the quarantine request - for this media. - - - ``safe_from_quarantine`` - bool - Status if this media is safe from quarantining. - - ``upload_name`` - string - The name the media was uploaded with. - -- ``next_token``: integer - Indication for pagination. See above. -- ``total`` - integer - Total number of media. - -Login as a user -=============== - -Get an access token that can be used to authenticate as that user. Useful for -when admins wish to do actions on behalf of a user. - -The API is:: - - POST /_synapse/admin/v1/users//login - {} - -An optional ``valid_until_ms`` field can be specified in the request body as an -integer timestamp that specifies when the token should expire. By default tokens -do not expire. - -A response body like the following is returned: - -.. code:: json - - { - "access_token": "" - } - - -This API does *not* generate a new device for the user, and so will not appear -their ``/devices`` list, and in general the target user should not be able to -tell they have been logged in as. - -To expire the token call the standard ``/logout`` API with the token. - -Note: The token will expire if the *admin* user calls ``/logout/all`` from any -of their devices, but the token will *not* expire if the target user does the -same. - - -User devices -============ - -List all devices ----------------- -Gets information about all devices for a specific ``user_id``. - -The API is:: - - GET /_synapse/admin/v2/users//devices - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "devices": [ - { - "device_id": "QBUAZIFURK", - "display_name": "android", - "last_seen_ip": "1.2.3.4", - "last_seen_ts": 1474491775024, - "user_id": "" - }, - { - "device_id": "AUIECTSRND", - "display_name": "ios", - "last_seen_ip": "1.2.3.5", - "last_seen_ts": 1474491775025, - "user_id": "" - } - ], - "total": 2 - } - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - fully qualified: for example, ``@user:server.com``. - -**Response** - -The following fields are returned in the JSON response body: - -- ``devices`` - An array of objects, each containing information about a device. - Device objects contain the following fields: - - - ``device_id`` - Identifier of device. - - ``display_name`` - Display name set by the user for this device. - Absent if no name has been set. - - ``last_seen_ip`` - The IP address where this device was last seen. - (May be a few minutes out of date, for efficiency reasons). - - ``last_seen_ts`` - The timestamp (in milliseconds since the unix epoch) when this - devices was last seen. (May be a few minutes out of date, for efficiency reasons). - - ``user_id`` - Owner of device. - -- ``total`` - Total number of user's devices. - -Delete multiple devices ------------------- -Deletes the given devices for a specific ``user_id``, and invalidates -any access token associated with them. - -The API is:: - - POST /_synapse/admin/v2/users//delete_devices - - { - "devices": [ - "QBUAZIFURK", - "AUIECTSRND" - ], - } - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -An empty JSON dict is returned. - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - fully qualified: for example, ``@user:server.com``. - -The following fields are required in the JSON request body: - -- ``devices`` - The list of device IDs to delete. - -Show a device ---------------- -Gets information on a single device, by ``device_id`` for a specific ``user_id``. - -The API is:: - - GET /_synapse/admin/v2/users//devices/ - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "device_id": "", - "display_name": "android", - "last_seen_ip": "1.2.3.4", - "last_seen_ts": 1474491775024, - "user_id": "" - } - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - fully qualified: for example, ``@user:server.com``. -- ``device_id`` - The device to retrieve. - -**Response** - -The following fields are returned in the JSON response body: - -- ``device_id`` - Identifier of device. -- ``display_name`` - Display name set by the user for this device. - Absent if no name has been set. -- ``last_seen_ip`` - The IP address where this device was last seen. - (May be a few minutes out of date, for efficiency reasons). -- ``last_seen_ts`` - The timestamp (in milliseconds since the unix epoch) when this - devices was last seen. (May be a few minutes out of date, for efficiency reasons). -- ``user_id`` - Owner of device. - -Update a device ---------------- -Updates the metadata on the given ``device_id`` for a specific ``user_id``. - -The API is:: - - PUT /_synapse/admin/v2/users//devices/ - - { - "display_name": "My other phone" - } - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -An empty JSON dict is returned. - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - fully qualified: for example, ``@user:server.com``. -- ``device_id`` - The device to update. - -The following fields are required in the JSON request body: - -- ``display_name`` - The new display name for this device. If not given, - the display name is unchanged. - -Delete a device ---------------- -Deletes the given ``device_id`` for a specific ``user_id``, -and invalidates any access token associated with it. - -The API is:: - - DELETE /_synapse/admin/v2/users//devices/ - - {} - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -An empty JSON dict is returned. - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - fully qualified: for example, ``@user:server.com``. -- ``device_id`` - The device to delete. - -List all pushers -================ -Gets information about all pushers for a specific ``user_id``. - -The API is:: - - GET /_synapse/admin/v1/users//pushers - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "pushers": [ - { - "app_display_name":"HTTP Push Notifications", - "app_id":"m.http", - "data": { - "url":"example.com" - }, - "device_display_name":"pushy push", - "kind":"http", - "lang":"None", - "profile_tag":"", - "pushkey":"a@example.com" - } - ], - "total": 1 - } - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - fully qualified: for example, ``@user:server.com``. - -**Response** - -The following fields are returned in the JSON response body: - -- ``pushers`` - An array containing the current pushers for the user - - - ``app_display_name`` - string - A string that will allow the user to identify - what application owns this pusher. - - - ``app_id`` - string - This is a reverse-DNS style identifier for the application. - Max length, 64 chars. - - - ``data`` - A dictionary of information for the pusher implementation itself. - - - ``url`` - string - Required if ``kind`` is ``http``. The URL to use to send - notifications to. - - - ``format`` - string - The format to use when sending notifications to the - Push Gateway. - - - ``device_display_name`` - string - A string that will allow the user to identify - what device owns this pusher. - - - ``profile_tag`` - string - This string determines which set of device specific rules - this pusher executes. - - - ``kind`` - string - The kind of pusher. "http" is a pusher that sends HTTP pokes. - - ``lang`` - string - The preferred language for receiving notifications - (e.g. 'en' or 'en-US') - - - ``profile_tag`` - string - This string determines which set of device specific rules - this pusher executes. - - - ``pushkey`` - string - This is a unique identifier for this pusher. - Max length, 512 bytes. - -- ``total`` - integer - Number of pushers. - -See also `Client-Server API Spec `_ - -Shadow-banning users -==================== - -Shadow-banning is a useful tool for moderating malicious or egregiously abusive users. -A shadow-banned users receives successful responses to their client-server API requests, -but the events are not propagated into rooms. This can be an effective tool as it -(hopefully) takes longer for the user to realise they are being moderated before -pivoting to another account. - -Shadow-banning a user should be used as a tool of last resort and may lead to confusing -or broken behaviour for the client. A shadow-banned user will not receive any -notification and it is generally more appropriate to ban or kick abusive users. -A shadow-banned user will be unable to contact anyone on the server. - -The API is:: - - POST /_synapse/admin/v1/users//shadow_ban - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -An empty JSON dict is returned. - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must - be local. - -Override ratelimiting for users -=============================== - -This API allows to override or disable ratelimiting for a specific user. -There are specific APIs to set, get and delete a ratelimit. - -Get status of ratelimit ------------------------ - -The API is:: - - GET /_synapse/admin/v1/users//override_ratelimit - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "messages_per_second": 0, - "burst_count": 0 - } - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must - be local. - -**Response** - -The following fields are returned in the JSON response body: - -- ``messages_per_second`` - integer - The number of actions that can - be performed in a second. `0` mean that ratelimiting is disabled for this user. -- ``burst_count`` - integer - How many actions that can be performed before - being limited. - -If **no** custom ratelimit is set, an empty JSON dict is returned. - -.. code:: json - - {} - -Set ratelimit -------------- - -The API is:: - - POST /_synapse/admin/v1/users//override_ratelimit - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -A response body like the following is returned: - -.. code:: json - - { - "messages_per_second": 0, - "burst_count": 0 - } - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must - be local. - -Body parameters: - -- ``messages_per_second`` - positive integer, optional. The number of actions that can - be performed in a second. Defaults to ``0``. -- ``burst_count`` - positive integer, optional. How many actions that can be performed - before being limited. Defaults to ``0``. - -To disable users' ratelimit set both values to ``0``. - -**Response** - -The following fields are returned in the JSON response body: - -- ``messages_per_second`` - integer - The number of actions that can - be performed in a second. -- ``burst_count`` - integer - How many actions that can be performed before - being limited. - -Delete ratelimit ----------------- - -The API is:: - - DELETE /_synapse/admin/v1/users//override_ratelimit - -To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see `README.rst `_. - -An empty JSON dict is returned. - -.. code:: json - - {} - -**Parameters** - -The following parameters should be set in the URL: - -- ``user_id`` - The fully qualified MXID: for example, ``@user:server.com``. The user must - be local. - diff --git a/docs/admin_api/version_api.rst b/docs/admin_api/version_api.md similarity index 59% rename from docs/admin_api/version_api.rst rename to docs/admin_api/version_api.md index 833d9028be..efb4a0c0f7 100644 --- a/docs/admin_api/version_api.rst +++ b/docs/admin_api/version_api.md @@ -1,20 +1,21 @@ -Version API -=========== +# Version API This API returns the running Synapse version and the Python version on which Synapse is being run. This is useful when a Synapse instance is behind a proxy that does not forward the 'Server' header (which also contains Synapse version information). -The api is:: +The api is: - GET /_synapse/admin/v1/server_version +``` +GET /_synapse/admin/v1/server_version +``` It returns a JSON body like the following: -.. code:: json - - { - "server_version": "0.99.2rc1 (b=develop, abcdef123)", - "python_version": "3.6.8" - } +```json +{ + "server_version": "0.99.2rc1 (b=develop, abcdef123)", + "python_version": "3.6.8" +} +``` From 1d143074c5534912cf40d28a4c31deabab2b1710 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 3 Jun 2021 16:01:30 +0100 Subject: [PATCH 235/619] Improve opentracing annotations for Notifier (#10111) The existing tracing reports an error each time there is a timeout, which isn't really representative. Additionally, we log things about the way `wait_for_events` works (eg, the result of the callback) to the *parent* span, which is confusing. --- changelog.d/10111.misc | 1 + synapse/notifier.py | 66 +++++++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 33 deletions(-) create mode 100644 changelog.d/10111.misc diff --git a/changelog.d/10111.misc b/changelog.d/10111.misc new file mode 100644 index 0000000000..42e42b69ab --- /dev/null +++ b/changelog.d/10111.misc @@ -0,0 +1 @@ +Improve opentracing annotations for `Notifier`. diff --git a/synapse/notifier.py b/synapse/notifier.py index 24b4e6649f..3c3cc47631 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -485,21 +485,21 @@ async def wait_for_events( end_time = self.clock.time_msec() + timeout while not result: - try: - now = self.clock.time_msec() - if end_time <= now: - break - - # Now we wait for the _NotifierUserStream to be told there - # is a new token. - listener = user_stream.new_listener(prev_token) - listener.deferred = timeout_deferred( - listener.deferred, - (end_time - now) / 1000.0, - self.hs.get_reactor(), - ) + with start_active_span("wait_for_events"): + try: + now = self.clock.time_msec() + if end_time <= now: + break + + # Now we wait for the _NotifierUserStream to be told there + # is a new token. + listener = user_stream.new_listener(prev_token) + listener.deferred = timeout_deferred( + listener.deferred, + (end_time - now) / 1000.0, + self.hs.get_reactor(), + ) - with start_active_span("wait_for_events.deferred"): log_kv( { "wait_for_events": "sleep", @@ -517,27 +517,27 @@ async def wait_for_events( } ) - current_token = user_stream.current_token + current_token = user_stream.current_token - result = await callback(prev_token, current_token) - log_kv( - { - "wait_for_events": "result", - "result": bool(result), - } - ) - if result: + result = await callback(prev_token, current_token) + log_kv( + { + "wait_for_events": "result", + "result": bool(result), + } + ) + if result: + break + + # Update the prev_token to the current_token since nothing + # has happened between the old prev_token and the current_token + prev_token = current_token + except defer.TimeoutError: + log_kv({"wait_for_events": "timeout"}) + break + except defer.CancelledError: + log_kv({"wait_for_events": "cancelled"}) break - - # Update the prev_token to the current_token since nothing - # has happened between the old prev_token and the current_token - prev_token = current_token - except defer.TimeoutError: - log_kv({"wait_for_events": "timeout"}) - break - except defer.CancelledError: - log_kv({"wait_for_events": "cancelled"}) - break if result is None: # This happened if there was no timeout or if the timeout had From 9eea4646be5eef1e2b24e3b0bb0fc94999c2250c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 3 Jun 2021 16:31:56 +0100 Subject: [PATCH 236/619] Add OpenTracing for database activity. (#10113) This adds quite a lot of OpenTracing decoration for database activity. Specifically it adds tracing at four different levels: * emit a span for each "interaction" - ie, the top level database function that we tend to call "transaction", but isn't really, because it can end up as multiple transactions. * emit a span while we hold a database connection open * emit a span for each database transaction - actual actual transaction. * emit a span for each database query. I'm aware this might be quite a lot of overhead, but even just running it on a local Synapse it looks really interesting, and I hope the overhead can be offset just by turning down the sampling frequency and finding other ways of tracing requests of interest (eg, the `force_tracing_for_users` setting). --- changelog.d/10113.feature | 1 + synapse/logging/opentracing.py | 6 +++ synapse/storage/database.py | 86 +++++++++++++++++++++------------- 3 files changed, 60 insertions(+), 33 deletions(-) create mode 100644 changelog.d/10113.feature diff --git a/changelog.d/10113.feature b/changelog.d/10113.feature new file mode 100644 index 0000000000..2658ab8918 --- /dev/null +++ b/changelog.d/10113.feature @@ -0,0 +1 @@ +Report OpenTracing spans for database activity. diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index f64845b80c..68f0c00151 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -271,6 +271,12 @@ class SynapseTags: # HTTP request tag (used to distinguish full vs incremental syncs, etc) REQUEST_TAG = "request_tag" + # Text description of a database transaction + DB_TXN_DESC = "db.txn_desc" + + # Uniqueish ID of a database transaction + DB_TXN_ID = "db.txn_id" + # Block everything by default # A regex which matches the server_names to expose traces for. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index a761ad603b..974703d13a 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -40,6 +40,7 @@ from synapse.api.errors import StoreError from synapse.config.database import DatabaseConnectionConfig +from synapse.logging import opentracing from synapse.logging.context import ( LoggingContext, current_context, @@ -313,7 +314,14 @@ def _do_execute(self, func: Callable[..., R], sql: str, *args: Any) -> R: start = time.time() try: - return func(sql, *args) + with opentracing.start_active_span( + "db.query", + tags={ + opentracing.tags.DATABASE_TYPE: "sql", + opentracing.tags.DATABASE_STATEMENT: sql, + }, + ): + return func(sql, *args) except Exception as e: sql_logger.debug("[SQL FAIL] {%s} %s", self.name, e) raise @@ -525,9 +533,16 @@ def new_transaction( exception_callbacks=exception_callbacks, ) try: - r = func(cursor, *args, **kwargs) - conn.commit() - return r + with opentracing.start_active_span( + "db.txn", + tags={ + opentracing.SynapseTags.DB_TXN_DESC: desc, + opentracing.SynapseTags.DB_TXN_ID: name, + }, + ): + r = func(cursor, *args, **kwargs) + conn.commit() + return r except self.engine.module.OperationalError as e: # This can happen if the database disappears mid # transaction. @@ -653,16 +668,17 @@ async def runInteraction( logger.warning("Starting db txn '%s' from sentinel context", desc) try: - result = await self.runWithConnection( - self.new_transaction, - desc, - after_callbacks, - exception_callbacks, - func, - *args, - db_autocommit=db_autocommit, - **kwargs, - ) + with opentracing.start_active_span(f"db.{desc}"): + result = await self.runWithConnection( + self.new_transaction, + desc, + after_callbacks, + exception_callbacks, + func, + *args, + db_autocommit=db_autocommit, + **kwargs, + ) for after_callback, after_args, after_kwargs in after_callbacks: after_callback(*after_args, **after_kwargs) @@ -718,25 +734,29 @@ def inner_func(conn, *args, **kwargs): with LoggingContext( str(curr_context), parent_context=parent_context ) as context: - sched_duration_sec = monotonic_time() - start_time - sql_scheduling_timer.observe(sched_duration_sec) - context.add_database_scheduled(sched_duration_sec) - - if self.engine.is_connection_closed(conn): - logger.debug("Reconnecting closed database connection") - conn.reconnect() - - try: - if db_autocommit: - self.engine.attempt_to_set_autocommit(conn, True) - - db_conn = LoggingDatabaseConnection( - conn, self.engine, "runWithConnection" - ) - return func(db_conn, *args, **kwargs) - finally: - if db_autocommit: - self.engine.attempt_to_set_autocommit(conn, False) + with opentracing.start_active_span( + operation_name="db.connection", + ): + sched_duration_sec = monotonic_time() - start_time + sql_scheduling_timer.observe(sched_duration_sec) + context.add_database_scheduled(sched_duration_sec) + + if self.engine.is_connection_closed(conn): + logger.debug("Reconnecting closed database connection") + conn.reconnect() + opentracing.log_kv({"message": "reconnected"}) + + try: + if db_autocommit: + self.engine.attempt_to_set_autocommit(conn, True) + + db_conn = LoggingDatabaseConnection( + conn, self.engine, "runWithConnection" + ) + return func(db_conn, *args, **kwargs) + finally: + if db_autocommit: + self.engine.attempt_to_set_autocommit(conn, False) return await make_deferred_yieldable( self._db_pool.runWithConnection(inner_func, *args, **kwargs) From fd9856e4a98fb3fa9c139317b0a3b79f22aff1c7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 3 Jun 2021 17:20:40 +0100 Subject: [PATCH 237/619] Compile and render Synapse's docs into a browsable, mobile-friendly and searchable website (#10086) --- .github/workflows/docs.yaml | 31 ++ .gitignore | 3 + MANIFEST.in | 1 + book.toml | 39 +++ changelog.d/10086.doc | 1 + docs/README.md | 71 +++- docs/SUMMARY.md | 87 +++++ docs/admin_api/README.rst | 30 +- docs/admin_api/delete_group.md | 2 +- docs/admin_api/event_reports.md | 4 +- docs/admin_api/media_admin_api.md | 4 +- docs/admin_api/purge_history_api.md | 2 +- docs/admin_api/room_membership.md | 2 +- docs/admin_api/rooms.md | 2 +- docs/admin_api/statistics.md | 2 +- docs/admin_api/user_admin_api.md | 40 +-- docs/development/contributing_guide.md | 7 + .../internal_documentation/README.md | 12 + docs/favicon.png | Bin 0 -> 7908 bytes docs/favicon.svg | 58 ++++ docs/setup/installation.md | 7 + docs/upgrading/README.md | 7 + docs/usage/administration/README.md | 7 + docs/usage/administration/admin_api/README.md | 29 ++ docs/usage/configuration/README.md | 4 + .../configuration/homeserver_sample_config.md | 14 + .../configuration/logging_sample_config.md | 14 + .../user_authentication/README.md | 15 + docs/website_files/README.md | 30 ++ docs/website_files/indent-section-headers.css | 7 + docs/website_files/remove-nav-buttons.css | 8 + docs/website_files/table-of-contents.css | 42 +++ docs/website_files/table-of-contents.js | 134 ++++++++ docs/website_files/theme/index.hbs | 312 ++++++++++++++++++ docs/welcome_and_overview.md | 4 + 35 files changed, 978 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/docs.yaml create mode 100644 book.toml create mode 100644 changelog.d/10086.doc create mode 100644 docs/SUMMARY.md create mode 100644 docs/development/contributing_guide.md create mode 100644 docs/development/internal_documentation/README.md create mode 100644 docs/favicon.png create mode 100644 docs/favicon.svg create mode 100644 docs/setup/installation.md create mode 100644 docs/upgrading/README.md create mode 100644 docs/usage/administration/README.md create mode 100644 docs/usage/administration/admin_api/README.md create mode 100644 docs/usage/configuration/README.md create mode 100644 docs/usage/configuration/homeserver_sample_config.md create mode 100644 docs/usage/configuration/logging_sample_config.md create mode 100644 docs/usage/configuration/user_authentication/README.md create mode 100644 docs/website_files/README.md create mode 100644 docs/website_files/indent-section-headers.css create mode 100644 docs/website_files/remove-nav-buttons.css create mode 100644 docs/website_files/table-of-contents.css create mode 100644 docs/website_files/table-of-contents.js create mode 100644 docs/website_files/theme/index.hbs create mode 100644 docs/welcome_and_overview.md diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000000..a746ae6de3 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,31 @@ +name: Deploy the documentation + +on: + push: + branches: + - develop + + workflow_dispatch: + +jobs: + pages: + name: GitHub Pages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Setup mdbook + uses: peaceiris/actions-mdbook@4b5ef36b314c2599664ca107bb8c02412548d79d # v1.1.14 + with: + mdbook-version: '0.4.9' + + - name: Build the documentation + run: mdbook build + + - name: Deploy latest documentation + uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + keep_files: true + publish_dir: ./book + destination_dir: ./develop diff --git a/.gitignore b/.gitignore index 295a18b539..6b9257b5c9 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ __pycache__/ /docs/build/ /htmlcov /pip-wheel-metadata/ + +# docs +book/ diff --git a/MANIFEST.in b/MANIFEST.in index 25d1cb758e..0522319c40 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -40,6 +40,7 @@ exclude mypy.ini exclude sytest-blacklist exclude test_postgresql.sh +include book.toml include pyproject.toml recursive-include changelog.d * diff --git a/book.toml b/book.toml new file mode 100644 index 0000000000..fa83d86ffc --- /dev/null +++ b/book.toml @@ -0,0 +1,39 @@ +# Documentation for possible options in this file is at +# https://rust-lang.github.io/mdBook/format/config.html +[book] +title = "Synapse" +authors = ["The Matrix.org Foundation C.I.C."] +language = "en" +multilingual = false + +# The directory that documentation files are stored in +src = "docs" + +[build] +# Prevent markdown pages from being automatically generated when they're +# linked to in SUMMARY.md +create-missing = false + +[output.html] +# The URL visitors will be directed to when they try to edit a page +edit-url-template = "https://github.com/matrix-org/synapse/edit/develop/{path}" + +# Remove the numbers that appear before each item in the sidebar, as they can +# get quite messy as we nest deeper +no-section-label = true + +# The source code URL of the repository +git-repository-url = "https://github.com/matrix-org/synapse" + +# The path that the docs are hosted on +site-url = "/synapse/" + +# Additional HTML, JS, CSS that's injected into each page of the book. +# More information available in docs/website_files/README.md +additional-css = [ + "docs/website_files/table-of-contents.css", + "docs/website_files/remove-nav-buttons.css", + "docs/website_files/indent-section-headers.css", +] +additional-js = ["docs/website_files/table-of-contents.js"] +theme = "docs/website_files/theme" \ No newline at end of file diff --git a/changelog.d/10086.doc b/changelog.d/10086.doc new file mode 100644 index 0000000000..2200579012 --- /dev/null +++ b/changelog.d/10086.doc @@ -0,0 +1 @@ +Add initial infrastructure for rendering Synapse documentation with mdbook. diff --git a/docs/README.md b/docs/README.md index 3c6ea48c66..e113f55d2a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,72 @@ # Synapse Documentation -This directory contains documentation specific to the `synapse` homeserver. +**The documentation is currently hosted [here](https://matrix-org.github.io/synapse).** +Please update any links to point to the new website instead. -All matrix-generic documentation now lives in its own project, located at [matrix-org/matrix-doc](https://github.com/matrix-org/matrix-doc) +## About -(Note: some items here may be moved to [matrix-org/matrix-doc](https://github.com/matrix-org/matrix-doc) at some point in the future.) +This directory currently holds a series of markdown files documenting how to install, use +and develop Synapse, the reference Matrix homeserver. The documentation is readable directly +from this repository, but it is recommended to instead browse through the +[website](https://matrix-org.github.io/synapse) for easier discoverability. + +## Adding to the documentation + +Most of the documentation currently exists as top-level files, as when organising them into +a structured website, these files were kept in place so that existing links would not break. +The rest of the documentation is stored in folders, such as `setup`, `usage`, and `development` +etc. **All new documentation files should be placed in structured folders.** For example: + +To create a new user-facing documentation page about a new Single Sign-On protocol named +"MyCoolProtocol", one should create a new file with a relevant name, such as "my_cool_protocol.md". +This file might fit into the documentation structure at: + +- Usage + - Configuration + - User Authentication + - Single Sign-On + - **My Cool Protocol** + +Given that, one would place the new file under +`usage/configuration/user_authentication/single_sign_on/my_cool_protocol.md`. + +Note that the structure of the documentation (and thus the left sidebar on the website) is determined +by the list in [SUMMARY.md](SUMMARY.md). The final thing to do when adding a new page is to add a new +line linking to the new documentation file: + +```markdown +- [My Cool Protocol](usage/configuration/user_authentication/single_sign_on/my_cool_protocol.md) +``` + +## Building the documentation + +The documentation is built with [mdbook](https://rust-lang.github.io/mdBook/), and the outline of the +documentation is determined by the structure of [SUMMARY.md](SUMMARY.md). + +First, [get mdbook](https://github.com/rust-lang/mdBook#installation). Then, **from the root of the repository**, +build the documentation with: + +```sh +mdbook build +``` + +The rendered contents will be outputted to a new `book/` directory at the root of the repository. You can +browse the book by opening `book/index.html` in a web browser. + +You can also have mdbook host the docs on a local webserver with hot-reload functionality via: + +```sh +mdbook serve +``` + +The URL at which the docs can be viewed at will be logged. + +## Configuration and theming + +The look and behaviour of the website is configured by the [book.toml](../book.toml) file +at the root of the repository. See +[mdbook's documentation on configuration](https://rust-lang.github.io/mdBook/format/config.html) +for available options. + +The site can be themed and additionally extended with extra UI and features. See +[website_files/README.md](website_files/README.md) for details. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000000..8f39ae0270 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,87 @@ +# Summary + +# Introduction +- [Welcome and Overview](welcome_and_overview.md) + +# Setup + - [Installation](setup/installation.md) + - [Using Postgres](postgres.md) + - [Configuring a Reverse Proxy](reverse_proxy.md) + - [Configuring a Turn Server](turn-howto.md) + - [Delegation](delegate.md) + +# Upgrading + - [Upgrading between Synapse Versions](upgrading/README.md) + - [Upgrading from pre-Synapse 1.0](MSC1711_certificates_FAQ.md) + +# Usage + - [Federation](federate.md) + - [Configuration](usage/configuration/README.md) + - [Homeserver Sample Config File](usage/configuration/homeserver_sample_config.md) + - [Logging Sample Config File](usage/configuration/logging_sample_config.md) + - [Structured Logging](structured_logging.md) + - [User Authentication](usage/configuration/user_authentication/README.md) + - [Single-Sign On]() + - [OpenID Connect](openid.md) + - [SAML]() + - [CAS]() + - [SSO Mapping Providers](sso_mapping_providers.md) + - [Password Auth Providers](password_auth_providers.md) + - [JSON Web Tokens](jwt.md) + - [Registration Captcha](CAPTCHA_SETUP.md) + - [Application Services](application_services.md) + - [Server Notices](server_notices.md) + - [Consent Tracking](consent_tracking.md) + - [URL Previews](url_previews.md) + - [User Directory](user_directory.md) + - [Message Retention Policies](message_retention_policies.md) + - [Pluggable Modules]() + - [Third Party Rules]() + - [Spam Checker](spam_checker.md) + - [Presence Router](presence_router_module.md) + - [Media Storage Providers]() + - [Workers](workers.md) + - [Using `synctl` with Workers](synctl_workers.md) + - [Systemd](systemd-with-workers/README.md) + - [Administration](usage/administration/README.md) + - [Admin API](usage/administration/admin_api/README.md) + - [Account Validity](admin_api/account_validity.md) + - [Delete Group](admin_api/delete_group.md) + - [Event Reports](admin_api/event_reports.md) + - [Media](admin_api/media_admin_api.md) + - [Purge History](admin_api/purge_history_api.md) + - [Purge Rooms](admin_api/purge_room.md) + - [Register Users](admin_api/register_api.md) + - [Manipulate Room Membership](admin_api/room_membership.md) + - [Rooms](admin_api/rooms.md) + - [Server Notices](admin_api/server_notices.md) + - [Shutdown Room](admin_api/shutdown_room.md) + - [Statistics](admin_api/statistics.md) + - [Users](admin_api/user_admin_api.md) + - [Server Version](admin_api/version_api.md) + - [Manhole](manhole.md) + - [Monitoring](metrics-howto.md) + - [Scripts]() + +# Development + - [Contributing Guide](development/contributing_guide.md) + - [Code Style](code_style.md) + - [Git Usage](dev/git.md) + - [Testing]() + - [OpenTracing](opentracing.md) + - [Synapse Architecture]() + - [Log Contexts](log_contexts.md) + - [Replication](replication.md) + - [TCP Replication](tcp_replication.md) + - [Internal Documentation](development/internal_documentation/README.md) + - [Single Sign-On]() + - [SAML](dev/saml.md) + - [CAS](dev/cas.md) + - [State Resolution]() + - [The Auth Chain Difference Algorithm](auth_chain_difference_algorithm.md) + - [Media Repository](media_repository.md) + - [Room and User Statistics](room_and_user_statistics.md) + - [Scripts]() + +# Other + - [Dependency Deprecation Policy](deprecation_policy.md) \ No newline at end of file diff --git a/docs/admin_api/README.rst b/docs/admin_api/README.rst index 9587bee0ce..37cee87d32 100644 --- a/docs/admin_api/README.rst +++ b/docs/admin_api/README.rst @@ -1,28 +1,14 @@ Admin APIs ========== -This directory includes documentation for the various synapse specific admin -APIs available. - -Authenticating as a server admin --------------------------------- - -Many of the API calls in the admin api will require an `access_token` for a -server admin. (Note that a server admin is distinct from a room admin.) - -A user can be marked as a server admin by updating the database directly, e.g.: - -.. code-block:: sql +**Note**: The latest documentation can be viewed `here `_. +See `docs/README.md <../docs/README.md>`_ for more information. - UPDATE users SET admin = 1 WHERE name = '@foo:bar.com'; +**Please update links to point to the website instead.** Existing files in this directory +are preserved to maintain historical links, but may be moved in the future. -A new server admin user can also be created using the -``register_new_matrix_user`` script. - -Finding your user's `access_token` is client-dependent, but will usually be shown in the client's settings. - -Once you have your `access_token`, to include it in a request, the best option is to add the token to a request header: - -``curl --header "Authorization: Bearer " `` +This directory includes documentation for the various synapse specific admin +APIs available. Updates to the existing Admin API documentation should still +be made to these files, but any new documentation files should instead be placed under +`docs/usage/administration/admin_api <../docs/usage/administration/admin_api>`_. -Fore more details, please refer to the complete `matrix spec documentation `_. diff --git a/docs/admin_api/delete_group.md b/docs/admin_api/delete_group.md index c061678e75..9c335ff759 100644 --- a/docs/admin_api/delete_group.md +++ b/docs/admin_api/delete_group.md @@ -11,4 +11,4 @@ POST /_synapse/admin/v1/delete_group/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [README.rst](README.rst). +server admin: see [Admin API](../../usage/administration/admin_api). diff --git a/docs/admin_api/event_reports.md b/docs/admin_api/event_reports.md index bfec06f755..186139185e 100644 --- a/docs/admin_api/event_reports.md +++ b/docs/admin_api/event_reports.md @@ -7,7 +7,7 @@ The api is: GET /_synapse/admin/v1/event_reports?from=0&limit=10 ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [README.rst](README.rst). +server admin: see [Admin API](../../usage/administration/admin_api). It returns a JSON body like the following: @@ -95,7 +95,7 @@ The api is: GET /_synapse/admin/v1/event_reports/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [README.rst](README.rst). +server admin: see [Admin API](../../usage/administration/admin_api). It returns a JSON body like the following: diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index 7709f3d8c7..9ab5269881 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -28,7 +28,7 @@ The API is: GET /_synapse/admin/v1/room//media ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [README.rst](README.rst). +server admin: see [Admin API](../../usage/administration/admin_api). The API returns a JSON body like the following: ```json @@ -311,7 +311,7 @@ The following fields are returned in the JSON response body: * `deleted`: integer - The number of media items successfully deleted To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [README.rst](README.rst). +server admin: see [Admin API](../../usage/administration/admin_api). If the user re-requests purged remote media, synapse will re-request the media from the originating server. diff --git a/docs/admin_api/purge_history_api.md b/docs/admin_api/purge_history_api.md index 44971acd91..25decc3e61 100644 --- a/docs/admin_api/purge_history_api.md +++ b/docs/admin_api/purge_history_api.md @@ -17,7 +17,7 @@ POST /_synapse/admin/v1/purge_history/[/] ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) By default, events sent by local users are not deleted, as they may represent the only copies of this content in existence. (Events sent by remote users are diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md index b6746ff5e4..ed40366099 100644 --- a/docs/admin_api/room_membership.md +++ b/docs/admin_api/room_membership.md @@ -24,7 +24,7 @@ POST /_synapse/admin/v1/join/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [README.rst](README.rst). +server admin: see [Admin API](../../usage/administration/admin_api). Response: diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 5721210fee..dc007fa00e 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -443,7 +443,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see [README.rst](README.rst). +server admin: see [Admin API](../../usage/administration/admin_api). A response body like the following is returned: diff --git a/docs/admin_api/statistics.md b/docs/admin_api/statistics.md index d398a120fb..d93d52a3ac 100644 --- a/docs/admin_api/statistics.md +++ b/docs/admin_api/statistics.md @@ -10,7 +10,7 @@ GET /_synapse/admin/v1/statistics/users/media ``` To use it, you will need to authenticate by providing an `access_token` -for a server admin: see [README.rst](README.rst). +for a server admin: see [Admin API](../../usage/administration/admin_api). A response body like the following is returned: diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 0c843316c9..c835e4a0cd 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -11,7 +11,7 @@ GET /_synapse/admin/v2/users/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) It returns a JSON body like the following: @@ -78,7 +78,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) URL parameters: @@ -119,7 +119,7 @@ GET /_synapse/admin/v2/users?from=0&limit=10&guests=false ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -237,7 +237,7 @@ See also: [Client Server API Whois](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-admin-whois-userid). To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) It returns a JSON body like the following: @@ -294,7 +294,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) The erase parameter is optional and defaults to `false`. An empty body may be passed for backwards compatibility. @@ -339,7 +339,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) The parameter `new_password` is required. The parameter `logout_devices` is optional and defaults to `true`. @@ -354,7 +354,7 @@ GET /_synapse/admin/v1/users//admin ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -384,7 +384,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) ## List room memberships of a user @@ -398,7 +398,7 @@ GET /_synapse/admin/v1/users//joined_rooms ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -443,7 +443,7 @@ GET /_synapse/admin/v1/users//media ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -591,7 +591,7 @@ GET /_synapse/admin/v2/users//devices ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -659,7 +659,7 @@ POST /_synapse/admin/v2/users//delete_devices ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) An empty JSON dict is returned. @@ -683,7 +683,7 @@ GET /_synapse/admin/v2/users//devices/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -731,7 +731,7 @@ PUT /_synapse/admin/v2/users//devices/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) An empty JSON dict is returned. @@ -760,7 +760,7 @@ DELETE /_synapse/admin/v2/users//devices/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) An empty JSON dict is returned. @@ -781,7 +781,7 @@ GET /_synapse/admin/v1/users//pushers ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -872,7 +872,7 @@ POST /_synapse/admin/v1/users//shadow_ban ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) An empty JSON dict is returned. @@ -897,7 +897,7 @@ GET /_synapse/admin/v1/users//override_ratelimit ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -939,7 +939,7 @@ POST /_synapse/admin/v1/users//override_ratelimit ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) A response body like the following is returned: @@ -984,7 +984,7 @@ DELETE /_synapse/admin/v1/users//override_ratelimit ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](README.rst) +server admin: [Admin API](../../usage/administration/admin_api) An empty JSON dict is returned. diff --git a/docs/development/contributing_guide.md b/docs/development/contributing_guide.md new file mode 100644 index 0000000000..ddf0887123 --- /dev/null +++ b/docs/development/contributing_guide.md @@ -0,0 +1,7 @@ + +# Contributing + +{{#include ../../CONTRIBUTING.md}} diff --git a/docs/development/internal_documentation/README.md b/docs/development/internal_documentation/README.md new file mode 100644 index 0000000000..51c5fb94d5 --- /dev/null +++ b/docs/development/internal_documentation/README.md @@ -0,0 +1,12 @@ +# Internal Documentation + +This section covers implementation documentation for various parts of Synapse. + +If a developer is planning to make a change to a feature of Synapse, it can be useful for +general documentation of how that feature is implemented to be available. This saves the +developer time in place of needing to understand how the feature works by reading the +code. + +Documentation that would be more useful for the perspective of a system administrator, +rather than a developer who's intending to change to code, should instead be placed +under the Usage section of the documentation. \ No newline at end of file diff --git a/docs/favicon.png b/docs/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..5f18bf641fae401bcfd808e1faa2ecd6dba1d7b5 GIT binary patch literal 7908 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 + + + + + image/svg+xml + + + + + + + + + diff --git a/docs/setup/installation.md b/docs/setup/installation.md new file mode 100644 index 0000000000..8bb1cffd3d --- /dev/null +++ b/docs/setup/installation.md @@ -0,0 +1,7 @@ + +{{#include ../../INSTALL.md}} \ No newline at end of file diff --git a/docs/upgrading/README.md b/docs/upgrading/README.md new file mode 100644 index 0000000000..258e58cf15 --- /dev/null +++ b/docs/upgrading/README.md @@ -0,0 +1,7 @@ + +{{#include ../../UPGRADE.rst}} \ No newline at end of file diff --git a/docs/usage/administration/README.md b/docs/usage/administration/README.md new file mode 100644 index 0000000000..e1e57546ab --- /dev/null +++ b/docs/usage/administration/README.md @@ -0,0 +1,7 @@ +# Administration + +This section contains information on managing your Synapse homeserver. This includes: + +* Managing users, rooms and media via the Admin API. +* Setting up metrics and monitoring to give you insight into your homeserver's health. +* Configuring structured logging. \ No newline at end of file diff --git a/docs/usage/administration/admin_api/README.md b/docs/usage/administration/admin_api/README.md new file mode 100644 index 0000000000..2fca96f8be --- /dev/null +++ b/docs/usage/administration/admin_api/README.md @@ -0,0 +1,29 @@ +# The Admin API + +## Authenticate as a server admin + +Many of the API calls in the admin api will require an `access_token` for a +server admin. (Note that a server admin is distinct from a room admin.) + +A user can be marked as a server admin by updating the database directly, e.g.: + +```sql +UPDATE users SET admin = 1 WHERE name = '@foo:bar.com'; +``` + +A new server admin user can also be created using the `register_new_matrix_user` +command. This is a script that is located in the `scripts/` directory, or possibly +already on your `$PATH` depending on how Synapse was installed. + +Finding your user's `access_token` is client-dependent, but will usually be shown in the client's settings. + +## Making an Admin API request +Once you have your `access_token`, you will need to authenticate each request to an Admin API endpoint by +providing the token as either a query parameter or a request header. To add it as a request header in cURL: + +```sh +curl --header "Authorization: Bearer " +``` + +For more details on access tokens in Matrix, please refer to the complete +[matrix spec documentation](https://matrix.org/docs/spec/client_server/r0.6.1#using-access-tokens). diff --git a/docs/usage/configuration/README.md b/docs/usage/configuration/README.md new file mode 100644 index 0000000000..41d41167c6 --- /dev/null +++ b/docs/usage/configuration/README.md @@ -0,0 +1,4 @@ +# Configuration + +This section contains information on tweaking Synapse via the various options in the configuration file. A configuration +file should have been generated when you [installed Synapse](../../setup/installation.html). diff --git a/docs/usage/configuration/homeserver_sample_config.md b/docs/usage/configuration/homeserver_sample_config.md new file mode 100644 index 0000000000..11e806998d --- /dev/null +++ b/docs/usage/configuration/homeserver_sample_config.md @@ -0,0 +1,14 @@ +# Homeserver Sample Configuration File + +Below is a sample homeserver configuration file. The homeserver configuration file +can be tweaked to change the behaviour of your homeserver. A restart of the server is +generally required to apply any changes made to this file. + +Note that the contents below are *not* intended to be copied and used as the basis for +a real homeserver.yaml. Instead, if you are starting from scratch, please generate +a fresh config using Synapse by following the instructions in +[Installation](../../setup/installation.md). + +```yaml +{{#include ../../sample_config.yaml}} +``` diff --git a/docs/usage/configuration/logging_sample_config.md b/docs/usage/configuration/logging_sample_config.md new file mode 100644 index 0000000000..4c4bc6fc16 --- /dev/null +++ b/docs/usage/configuration/logging_sample_config.md @@ -0,0 +1,14 @@ +# Logging Sample Configuration File + +Below is a sample logging configuration file. This file can be tweaked to control how your +homeserver will output logs. A restart of the server is generally required to apply any +changes made to this file. + +Note that the contents below are *not* intended to be copied and used as the basis for +a real homeserver.yaml. Instead, if you are starting from scratch, please generate +a fresh config using Synapse by following the instructions in +[Installation](../../setup/installation.md). + +```yaml +{{#include ../../sample_log_config.yaml}} +``__` \ No newline at end of file diff --git a/docs/usage/configuration/user_authentication/README.md b/docs/usage/configuration/user_authentication/README.md new file mode 100644 index 0000000000..087ae053cf --- /dev/null +++ b/docs/usage/configuration/user_authentication/README.md @@ -0,0 +1,15 @@ +# User Authentication + +Synapse supports multiple methods of authenticating users, either out-of-the-box or through custom pluggable +authentication modules. + +Included in Synapse is support for authenticating users via: + +* A username and password. +* An email address and password. +* Single Sign-On through the SAML, Open ID Connect or CAS protocols. +* JSON Web Tokens. +* An administrator's shared secret. + +Synapse can additionally be extended to support custom authentication schemes through optional "password auth provider" +modules. \ No newline at end of file diff --git a/docs/website_files/README.md b/docs/website_files/README.md new file mode 100644 index 0000000000..04d191479b --- /dev/null +++ b/docs/website_files/README.md @@ -0,0 +1,30 @@ +# Documentation Website Files and Assets + +This directory contains extra files for modifying the look and functionality of +[mdbook](https://github.com/rust-lang/mdBook), the documentation software that's +used to generate Synapse's documentation website. + +The configuration options in the `output.html` section of [book.toml](../../book.toml) +point to additional JS/CSS in this directory that are added on each page load. In +addition, the `theme` directory contains files that overwrite their counterparts in +each of the default themes included with mdbook. + +Currently we use these files to generate a floating Table of Contents panel. The code for +which was partially taken from +[JorelAli/mdBook-pagetoc](https://github.com/JorelAli/mdBook-pagetoc/) +before being modified such that it scrolls with the content of the page. This is handled +by the `table-of-contents.js/css` files. The table of contents panel only appears on pages +that have more than one header, as well as only appearing on desktop-sized monitors. + +We remove the navigation arrows which typically appear on the left and right side of the +screen on desktop as they interfere with the table of contents. This is handled by +the `remove-nav-buttons.css` file. + +Finally, we also stylise the chapter titles in the left sidebar by indenting them +slightly so that they are more visually distinguishable from the section headers +(the bold titles). This is done through the `indent-section-headers.css` file. + +More information can be found in mdbook's official documentation for +[injecting page JS/CSS](https://rust-lang.github.io/mdBook/format/config.html) +and +[customising the default themes](https://rust-lang.github.io/mdBook/format/theme/index.html). \ No newline at end of file diff --git a/docs/website_files/indent-section-headers.css b/docs/website_files/indent-section-headers.css new file mode 100644 index 0000000000..f9b3c82ca6 --- /dev/null +++ b/docs/website_files/indent-section-headers.css @@ -0,0 +1,7 @@ +/* + * Indents each chapter title in the left sidebar so that they aren't + * at the same level as the section headers. + */ +.chapter-item { + margin-left: 1em; +} \ No newline at end of file diff --git a/docs/website_files/remove-nav-buttons.css b/docs/website_files/remove-nav-buttons.css new file mode 100644 index 0000000000..4b280794ea --- /dev/null +++ b/docs/website_files/remove-nav-buttons.css @@ -0,0 +1,8 @@ +/* Remove the prev, next chapter buttons as they interfere with the + * table of contents. + * Note that the table of contents only appears on desktop, thus we + * only remove the desktop (wide) chapter buttons. + */ +.nav-wide-wrapper { + display: none +} \ No newline at end of file diff --git a/docs/website_files/table-of-contents.css b/docs/website_files/table-of-contents.css new file mode 100644 index 0000000000..d16bb3b988 --- /dev/null +++ b/docs/website_files/table-of-contents.css @@ -0,0 +1,42 @@ +@media only screen and (max-width:1439px) { + .sidetoc { + display: none; + } +} + +@media only screen and (min-width:1440px) { + main { + position: relative; + margin-left: 100px !important; + } + .sidetoc { + margin-left: auto; + margin-right: auto; + left: calc(100% + (var(--content-max-width))/4 - 140px); + position: absolute; + text-align: right; + } + .pagetoc { + position: fixed; + width: 250px; + overflow: auto; + right: 20px; + height: calc(100% - var(--menu-bar-height)); + } + .pagetoc a { + color: var(--fg) !important; + display: block; + padding: 5px 15px 5px 10px; + text-align: left; + text-decoration: none; + } + .pagetoc a:hover, + .pagetoc a.active { + background: var(--sidebar-bg) !important; + color: var(--sidebar-fg) !important; + } + .pagetoc .active { + background: var(--sidebar-bg); + color: var(--sidebar-fg); + } +} diff --git a/docs/website_files/table-of-contents.js b/docs/website_files/table-of-contents.js new file mode 100644 index 0000000000..0de5960b22 --- /dev/null +++ b/docs/website_files/table-of-contents.js @@ -0,0 +1,134 @@ +const getPageToc = () => document.getElementsByClassName('pagetoc')[0]; + +const pageToc = getPageToc(); +const pageTocChildren = [...pageToc.children]; +const headers = [...document.getElementsByClassName('header')]; + + +// Select highlighted item in ToC when clicking an item +pageTocChildren.forEach(child => { + child.addEventHandler('click', () => { + pageTocChildren.forEach(child => { + child.classList.remove('active'); + }); + child.classList.add('active'); + }); +}); + + +/** + * Test whether a node is in the viewport + */ +function isInViewport(node) { + const rect = node.getBoundingClientRect(); + return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth); +} + + +/** + * Set a new ToC entry. + * Clear any previously highlighted ToC items, set the new one, + * and adjust the ToC scroll position. + */ +function setTocEntry() { + let activeEntry; + const pageTocChildren = [...getPageToc().children]; + + // Calculate which header is the current one at the top of screen + headers.forEach(header => { + if (window.pageYOffset >= header.offsetTop) { + activeEntry = header; + } + }); + + // Update selected item in ToC when scrolling + pageTocChildren.forEach(child => { + if (activeEntry.href.localeCompare(child.href) === 0) { + child.classList.add('active'); + } else { + child.classList.remove('active'); + } + }); + + let tocEntryForLocation = document.querySelector(`nav a[href="${activeEntry.href}"]`); + if (tocEntryForLocation) { + const headingForLocation = document.querySelector(activeEntry.hash); + if (headingForLocation && isInViewport(headingForLocation)) { + // Update ToC scroll + const nav = getPageToc(); + const content = document.querySelector('html'); + if (content.scrollTop !== 0) { + nav.scrollTo({ + top: tocEntryForLocation.offsetTop - 100, + left: 0, + behavior: 'smooth', + }); + } else { + nav.scrollTop = 0; + } + } + } +} + + +/** + * Populate sidebar on load + */ +window.addEventListener('load', () => { + // Only create table of contents if there is more than one header on the page + if (headers.length <= 1) { + return; + } + + // Create an entry in the page table of contents for each header in the document + headers.forEach((header, index) => { + const link = document.createElement('a'); + + // Indent shows hierarchy + let indent = '0px'; + switch (header.parentElement.tagName) { + case 'H1': + indent = '5px'; + break; + case 'H2': + indent = '20px'; + break; + case 'H3': + indent = '30px'; + break; + case 'H4': + indent = '40px'; + break; + case 'H5': + indent = '50px'; + break; + case 'H6': + indent = '60px'; + break; + default: + break; + } + + let tocEntry; + if (index == 0) { + // Create a bolded title for the first element + tocEntry = document.createElement("strong"); + tocEntry.innerHTML = header.text; + } else { + // All other elements are non-bold + tocEntry = document.createTextNode(header.text); + } + link.appendChild(tocEntry); + + link.style.paddingLeft = indent; + link.href = header.href; + pageToc.appendChild(link); + }); + setTocEntry.call(); +}); + + +// Handle active headers on scroll, if there is more than one header on the page +if (headers.length > 1) { + window.addEventListener('scroll', setTocEntry); +} diff --git a/docs/website_files/theme/index.hbs b/docs/website_files/theme/index.hbs new file mode 100644 index 0000000000..3b7a5b6163 --- /dev/null +++ b/docs/website_files/theme/index.hbs @@ -0,0 +1,312 @@ + + + + + + {{ title }} + {{#if is_print }} + + {{/if}} + {{#if base_url}} + + {{/if}} + + + + {{> head}} + + + + + + + {{#if favicon_svg}} + + {{/if}} + {{#if favicon_png}} + + {{/if}} + + + + {{#if print_enable}} + + {{/if}} + + + + {{#if copy_fonts}} + + {{/if}} + + + + + + + + {{#each additional_css}} + + {{/each}} + + {{#if mathjax_support}} + + + {{/if}} + + + + + + + + + + + + + + + + +
+ +
+ {{> header}} + + + + {{#if search_enabled}} + + {{/if}} + + + + +
+
+ +
+ +
+ + {{{ content }}} +
+ + +
+
+ + + +
+ + {{#if livereload}} + + + {{/if}} + + {{#if google_analytics}} + + + {{/if}} + + {{#if playground_line_numbers}} + + {{/if}} + + {{#if playground_copyable}} + + {{/if}} + + {{#if playground_js}} + + + + + + {{/if}} + + {{#if search_js}} + + + + {{/if}} + + + + + + + {{#each additional_js}} + + {{/each}} + + {{#if is_print}} + {{#if mathjax_support}} + + {{else}} + + {{/if}} + {{/if}} + + + \ No newline at end of file diff --git a/docs/welcome_and_overview.md b/docs/welcome_and_overview.md new file mode 100644 index 0000000000..30e75984d1 --- /dev/null +++ b/docs/welcome_and_overview.md @@ -0,0 +1,4 @@ +# Introduction + +Welcome to the documentation repository for Synapse, the reference +[Matrix](https://matrix.org) homeserver implementation. \ No newline at end of file From d8be7d493d7a91a55ee37a7931157d4557a508fb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 4 Jun 2021 09:25:33 +0100 Subject: [PATCH 238/619] Enable Prometheus metrics for the jaeger client library (#10112) --- changelog.d/10112.misc | 1 + mypy.ini | 2 +- synapse/logging/opentracing.py | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10112.misc diff --git a/changelog.d/10112.misc b/changelog.d/10112.misc new file mode 100644 index 0000000000..40af09760c --- /dev/null +++ b/changelog.d/10112.misc @@ -0,0 +1 @@ +Enable Prometheus metrics for the jaeger client library. diff --git a/mypy.ini b/mypy.ini index 062872020e..8ba1b96311 100644 --- a/mypy.ini +++ b/mypy.ini @@ -130,7 +130,7 @@ ignore_missing_imports = True [mypy-canonicaljson] ignore_missing_imports = True -[mypy-jaeger_client] +[mypy-jaeger_client.*] ignore_missing_imports = True [mypy-jsonschema] diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index 68f0c00151..26c8ffe780 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -362,10 +362,13 @@ def init_tracer(hs: "HomeServer"): set_homeserver_whitelist(hs.config.opentracer_whitelist) + from jaeger_client.metrics.prometheus import PrometheusMetricsFactory + config = JaegerConfig( config=hs.config.jaeger_config, service_name="{} {}".format(hs.config.server_name, hs.get_instance_name()), scope_manager=LogContextScopeManager(hs.config), + metrics_factory=PrometheusMetricsFactory(), ) # If we have the rust jaeger reporter available let's use that. From c96ab31dff4abe9e8a09fb2cd3967e799f770b63 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 4 Jun 2021 10:35:47 +0100 Subject: [PATCH 239/619] Limit number of events in a replication request (#10118) Fixes #9956. --- changelog.d/10118.bugfix | 1 + synapse/handlers/federation.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10118.bugfix diff --git a/changelog.d/10118.bugfix b/changelog.d/10118.bugfix new file mode 100644 index 0000000000..db62b50e0b --- /dev/null +++ b/changelog.d/10118.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse 1.33.0 which caused replication requests to fail when receiving a lot of very large events via federation. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 49ed7cabcc..f3f97db2fa 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -3056,8 +3056,9 @@ async def persist_events_and_notify( """ instance = self.config.worker.events_shard_config.get_instance(room_id) if instance != self._instance_name: - # Limit the number of events sent over federation. - for batch in batch_iter(event_and_contexts, 1000): + # Limit the number of events sent over replication. We choose 200 + # here as that is what we default to in `max_request_body_size(..)` + for batch in batch_iter(event_and_contexts, 200): result = await self._send_events( instance_name=instance, store=self.store, From a0cd8ae8cbe14d2821cbe8fd6b011c4ddc729344 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 4 Jun 2021 10:47:58 +0100 Subject: [PATCH 240/619] Don't try and backfill the same room in parallel. (#10116) If backfilling is slow then the client may time out and retry, causing Synapse to start a new `/backfill` before the existing backfill has finished, duplicating work. --- changelog.d/10116.bugfix | 1 + synapse/handlers/federation.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 changelog.d/10116.bugfix diff --git a/changelog.d/10116.bugfix b/changelog.d/10116.bugfix new file mode 100644 index 0000000000..90ef707559 --- /dev/null +++ b/changelog.d/10116.bugfix @@ -0,0 +1 @@ +Fix bug where the server would attempt to fetch the same history in the room from a remote server multiple times in parallel. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index f3f97db2fa..b802822baa 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -178,6 +178,8 @@ def __init__(self, hs: "HomeServer"): self.room_queues = {} # type: Dict[str, List[Tuple[EventBase, str]]] self._room_pdu_linearizer = Linearizer("fed_room_pdu") + self._room_backfill = Linearizer("room_backfill") + self.third_party_event_rules = hs.get_third_party_event_rules() self._ephemeral_messages_enabled = hs.config.enable_ephemeral_messages @@ -1041,6 +1043,12 @@ async def maybe_backfill( return. This is used as part of the heuristic to decide if we should back paginate. """ + with (await self._room_backfill.queue(room_id)): + return await self._maybe_backfill_inner(room_id, current_depth, limit) + + async def _maybe_backfill_inner( + self, room_id: str, current_depth: int, limit: int + ) -> bool: extremities = await self.store.get_oldest_events_with_depth_in_room(room_id) if not extremities: From fa1db8f1567471e6cb29c0d6c0b740fcb79ea202 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 7 Jun 2021 09:19:06 +0100 Subject: [PATCH 241/619] Delete completes to-device messages earlier in /sync (#10124) I hope this will improve https://github.com/matrix-org/synapse/issues/9564. --- changelog.d/10124.misc | 1 + synapse/handlers/sync.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10124.misc diff --git a/changelog.d/10124.misc b/changelog.d/10124.misc new file mode 100644 index 0000000000..c06593238d --- /dev/null +++ b/changelog.d/10124.misc @@ -0,0 +1 @@ +Work to improve the responsiveness of `/sync` requests. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 069ffc76f7..b1c58ffdc8 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -315,6 +315,17 @@ async def _wait_for_sync_for_user( if context: context.tag = sync_type + # if we have a since token, delete any to-device messages before that token + # (since we now know that the device has received them) + if since_token is not None: + since_stream_id = since_token.to_device_key + deleted = await self.store.delete_messages_for_device( + sync_config.user.to_string(), sync_config.device_id, since_stream_id + ) + logger.debug( + "Deleted %d to-device messages up to %d", deleted, since_stream_id + ) + if timeout == 0 or since_token is None or full_state: # we are going to return immediately, so don't bother calling # notifier.wait_for_events. @@ -1230,16 +1241,6 @@ async def _generate_sync_entry_for_to_device( since_stream_id = int(sync_result_builder.since_token.to_device_key) if since_stream_id != int(now_token.to_device_key): - # We only delete messages when a new message comes in, but that's - # fine so long as we delete them at some point. - - deleted = await self.store.delete_messages_for_device( - user_id, device_id, since_stream_id - ) - logger.debug( - "Deleted %d to-device messages up to %d", deleted, since_stream_id - ) - messages, stream_id = await self.store.get_new_messages_for_device( user_id, device_id, since_stream_id, now_token.to_device_key ) From d558292548178dde785462bbca7f84c06c1e9eda Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 7 Jun 2021 16:12:34 +0200 Subject: [PATCH 242/619] Add missing type hints to the admin API servlets (#10105) --- changelog.d/10105.misc | 1 + synapse/rest/admin/__init__.py | 45 ++++++++++++++++++---------------- synapse/rest/admin/_base.py | 3 ++- synapse/rest/admin/groups.py | 12 +++++++-- synapse/rest/admin/media.py | 12 ++++----- synapse/rest/admin/users.py | 15 ++++-------- 6 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 changelog.d/10105.misc diff --git a/changelog.d/10105.misc b/changelog.d/10105.misc new file mode 100644 index 0000000000..244a893d3e --- /dev/null +++ b/changelog.d/10105.misc @@ -0,0 +1 @@ +Add missing type hints to the admin API servlets. \ No newline at end of file diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 9cb9a9f6aa..abf749b001 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -17,11 +17,13 @@ import logging import platform +from typing import TYPE_CHECKING, Optional, Tuple import synapse from synapse.api.errors import Codes, NotFoundError, SynapseError -from synapse.http.server import JsonResource +from synapse.http.server import HttpServer, JsonResource from synapse.http.servlet import RestServlet, parse_json_object_from_request +from synapse.http.site import SynapseRequest from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin from synapse.rest.admin.devices import ( DeleteDevicesRestServlet, @@ -66,22 +68,25 @@ UserTokenRestServlet, WhoisRestServlet, ) -from synapse.types import RoomStreamToken +from synapse.types import JsonDict, RoomStreamToken from synapse.util.versionstring import get_version_string +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) class VersionServlet(RestServlet): PATTERNS = admin_patterns("/server_version$") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.res = { "server_version": get_version_string(synapse), "python_version": platform.python_version(), } - def on_GET(self, request): + def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: return 200, self.res @@ -90,17 +95,14 @@ class PurgeHistoryRestServlet(RestServlet): "/purge_history/(?P[^/]*)(/(?P[^/]+))?" ) - def __init__(self, hs): - """ - - Args: - hs (synapse.server.HomeServer) - """ + def __init__(self, hs: "HomeServer"): self.pagination_handler = hs.get_pagination_handler() self.store = hs.get_datastore() self.auth = hs.get_auth() - async def on_POST(self, request, room_id, event_id): + async def on_POST( + self, request: SynapseRequest, room_id: str, event_id: Optional[str] + ) -> Tuple[int, JsonDict]: await assert_requester_is_admin(self.auth, request) body = parse_json_object_from_request(request, allow_empty_body=True) @@ -119,6 +121,8 @@ async def on_POST(self, request, room_id, event_id): if event.room_id != room_id: raise SynapseError(400, "Event is for wrong room.") + # RoomStreamToken expects [int] not Optional[int] + assert event.internal_metadata.stream_ordering is not None room_token = RoomStreamToken( event.depth, event.internal_metadata.stream_ordering ) @@ -173,16 +177,13 @@ async def on_POST(self, request, room_id, event_id): class PurgeHistoryStatusRestServlet(RestServlet): PATTERNS = admin_patterns("/purge_history_status/(?P[^/]+)") - def __init__(self, hs): - """ - - Args: - hs (synapse.server.HomeServer) - """ + def __init__(self, hs: "HomeServer"): self.pagination_handler = hs.get_pagination_handler() self.auth = hs.get_auth() - async def on_GET(self, request, purge_id): + async def on_GET( + self, request: SynapseRequest, purge_id: str + ) -> Tuple[int, JsonDict]: await assert_requester_is_admin(self.auth, request) purge_status = self.pagination_handler.get_purge_status(purge_id) @@ -203,12 +204,12 @@ async def on_GET(self, request, purge_id): class AdminRestResource(JsonResource): """The REST resource which gets mounted at /_synapse/admin""" - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): JsonResource.__init__(self, hs, canonical_json=False) register_servlets(hs, self) -def register_servlets(hs, http_server): +def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: """ Register all the admin servlets. """ @@ -242,7 +243,9 @@ def register_servlets(hs, http_server): RateLimitRestServlet(hs).register(http_server) -def register_servlets_for_client_rest_resource(hs, http_server): +def register_servlets_for_client_rest_resource( + hs: "HomeServer", http_server: HttpServer +) -> None: """Register only the servlets which need to be exposed on /_matrix/client/xxx""" WhoisRestServlet(hs).register(http_server) PurgeHistoryStatusRestServlet(hs).register(http_server) diff --git a/synapse/rest/admin/_base.py b/synapse/rest/admin/_base.py index f203f6fdc6..d9a2f6ca15 100644 --- a/synapse/rest/admin/_base.py +++ b/synapse/rest/admin/_base.py @@ -13,6 +13,7 @@ # limitations under the License. import re +from typing import Iterable, Pattern from synapse.api.auth import Auth from synapse.api.errors import AuthError @@ -20,7 +21,7 @@ from synapse.types import UserID -def admin_patterns(path_regex: str, version: str = "v1"): +def admin_patterns(path_regex: str, version: str = "v1") -> Iterable[Pattern]: """Returns the list of patterns for an admin endpoint Args: diff --git a/synapse/rest/admin/groups.py b/synapse/rest/admin/groups.py index 3b3ffde0b6..68a3ba3cb7 100644 --- a/synapse/rest/admin/groups.py +++ b/synapse/rest/admin/groups.py @@ -12,10 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import TYPE_CHECKING, Tuple from synapse.api.errors import SynapseError from synapse.http.servlet import RestServlet +from synapse.http.site import SynapseRequest from synapse.rest.admin._base import admin_patterns, assert_user_is_admin +from synapse.types import JsonDict + +if TYPE_CHECKING: + from synapse.server import HomeServer logger = logging.getLogger(__name__) @@ -25,12 +31,14 @@ class DeleteGroupAdminRestServlet(RestServlet): PATTERNS = admin_patterns("/delete_group/(?P[^/]*)") - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.group_server = hs.get_groups_server_handler() self.is_mine_id = hs.is_mine_id self.auth = hs.get_auth() - async def on_POST(self, request, group_id): + async def on_POST( + self, request: SynapseRequest, group_id: str + ) -> Tuple[int, JsonDict]: requester = await self.auth.get_user_by_req(request) await assert_user_is_admin(self.auth, requester.user) diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index b68db2c57c..0a19a333d7 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Tuple from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError +from synapse.http.server import HttpServer from synapse.http.servlet import RestServlet, parse_boolean, parse_integer from synapse.http.site import SynapseRequest from synapse.rest.admin._base import ( @@ -37,12 +38,11 @@ class QuarantineMediaInRoom(RestServlet): this server. """ - PATTERNS = ( - admin_patterns("/room/(?P[^/]+)/media/quarantine") - + + PATTERNS = [ + *admin_patterns("/room/(?P[^/]+)/media/quarantine"), # This path kept around for legacy reasons - admin_patterns("/quarantine_media/(?P[^/]+)") - ) + *admin_patterns("/quarantine_media/(?P[^/]+)"), + ] def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() @@ -312,7 +312,7 @@ async def on_POST( return 200, {"deleted_media": deleted_media, "total": total} -def register_servlets_for_media_repo(hs: "HomeServer", http_server): +def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer) -> None: """ Media repo specific APIs. """ diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 8c9d21d3ea..7d75564758 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -478,13 +478,12 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: class WhoisRestServlet(RestServlet): path_regex = "/whois/(?P[^/]*)$" - PATTERNS = ( - admin_patterns(path_regex) - + + PATTERNS = [ + *admin_patterns(path_regex), # URL for spec reason # https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-admin-whois-userid - client_patterns("/admin" + path_regex, v1=True) - ) + *client_patterns("/admin" + path_regex, v1=True), + ] def __init__(self, hs: "HomeServer"): self.hs = hs @@ -553,11 +552,7 @@ async def on_POST( class AccountValidityRenewServlet(RestServlet): PATTERNS = admin_patterns("/account_validity/validity$") - def __init__(self, hs): - """ - Args: - hs (synapse.server.HomeServer): server - """ + def __init__(self, hs: "HomeServer"): self.hs = hs self.account_activity_handler = hs.get_account_validity_handler() self.auth = hs.get_auth() From 8942e23a6941dc740f6f703f7e353f273874f104 Mon Sep 17 00:00:00 2001 From: 14mRh4X0r <14mRh4X0r@gmail.com> Date: Mon, 7 Jun 2021 16:42:05 +0200 Subject: [PATCH 243/619] Always update AS last_pos, even on no events (#10107) Fixes #1834. `get_new_events_for_appservice` internally calls `get_events_as_list`, which will filter out any rejected events. If all returned events are filtered out, `_notify_interested_services` will return without updating the last handled stream position. If there are 100 consecutive such events, processing will halt altogether. Breaking the loop is now done by checking whether we're up-to-date with `current_max` in the loop condition, instead of relying on an empty `events` list. Signed-off-by: Willem Mulder <14mRh4X0r@gmail.com> --- changelog.d/10107.bugfix | 1 + synapse/handlers/appservice.py | 25 ++++++++++++------------- tests/handlers/test_appservice.py | 6 ++---- 3 files changed, 15 insertions(+), 17 deletions(-) create mode 100644 changelog.d/10107.bugfix diff --git a/changelog.d/10107.bugfix b/changelog.d/10107.bugfix new file mode 100644 index 0000000000..80030efab2 --- /dev/null +++ b/changelog.d/10107.bugfix @@ -0,0 +1 @@ +Fixed a bug that could cause Synapse to stop notifying application services. Contributed by Willem Mulder. diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py index 177310f0be..862638cc4f 100644 --- a/synapse/handlers/appservice.py +++ b/synapse/handlers/appservice.py @@ -87,7 +87,8 @@ async def _notify_interested_services(self, max_token: RoomStreamToken): self.is_processing = True try: limit = 100 - while True: + upper_bound = -1 + while upper_bound < self.current_max: ( upper_bound, events, @@ -95,9 +96,6 @@ async def _notify_interested_services(self, max_token: RoomStreamToken): self.current_max, limit ) - if not events: - break - events_by_room = {} # type: Dict[str, List[EventBase]] for event in events: events_by_room.setdefault(event.room_id, []).append(event) @@ -153,9 +151,6 @@ async def handle_room_events(events): await self.store.set_appservice_last_pos(upper_bound) - now = self.clock.time_msec() - ts = await self.store.get_received_ts(events[-1].event_id) - synapse.metrics.event_processing_positions.labels( "appservice_sender" ).set(upper_bound) @@ -168,12 +163,16 @@ async def handle_room_events(events): event_processing_loop_counter.labels("appservice_sender").inc() - synapse.metrics.event_processing_lag.labels( - "appservice_sender" - ).set(now - ts) - synapse.metrics.event_processing_last_ts.labels( - "appservice_sender" - ).set(ts) + if events: + now = self.clock.time_msec() + ts = await self.store.get_received_ts(events[-1].event_id) + + synapse.metrics.event_processing_lag.labels( + "appservice_sender" + ).set(now - ts) + synapse.metrics.event_processing_last_ts.labels( + "appservice_sender" + ).set(ts) finally: self.is_processing = False diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py index b037b12a0f..5d6cc2885f 100644 --- a/tests/handlers/test_appservice.py +++ b/tests/handlers/test_appservice.py @@ -57,10 +57,10 @@ def test_notify_interested_services(self): sender="@someone:anywhere", type="m.room.message", room_id="!foo:bar" ) self.mock_store.get_new_events_for_appservice.side_effect = [ - make_awaitable((0, [event])), make_awaitable((0, [])), + make_awaitable((1, [event])), ] - self.handler.notify_interested_services(RoomStreamToken(None, 0)) + self.handler.notify_interested_services(RoomStreamToken(None, 1)) self.mock_scheduler.submit_event_for_as.assert_called_once_with( interested_service, event @@ -77,7 +77,6 @@ def test_query_user_exists_unknown_user(self): self.mock_as_api.query_user.return_value = make_awaitable(True) self.mock_store.get_new_events_for_appservice.side_effect = [ make_awaitable((0, [event])), - make_awaitable((0, [])), ] self.handler.notify_interested_services(RoomStreamToken(None, 0)) @@ -95,7 +94,6 @@ def test_query_user_exists_known_user(self): self.mock_as_api.query_user.return_value = make_awaitable(True) self.mock_store.get_new_events_for_appservice.side_effect = [ make_awaitable((0, [event])), - make_awaitable((0, [])), ] self.handler.notify_interested_services(RoomStreamToken(None, 0)) From 543e423fce64c14dd136d4021b27a99a3e9fd08b Mon Sep 17 00:00:00 2001 From: Chris Castle Date: Mon, 7 Jun 2021 08:31:39 -0700 Subject: [PATCH 244/619] Fix broken link to README at root of repo (#10132) Signed-off-by: Chris Castle chris@crc.io --- changelog.d/10132.doc | 1 + docker/README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10132.doc diff --git a/changelog.d/10132.doc b/changelog.d/10132.doc new file mode 100644 index 0000000000..70f538f077 --- /dev/null +++ b/changelog.d/10132.doc @@ -0,0 +1 @@ +Fix broken link in Docker docs. diff --git a/docker/README.md b/docker/README.md index c8d3c4b3da..3f28cdada3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -226,4 +226,4 @@ healthcheck: ## Using jemalloc Jemalloc is embedded in the image and will be used instead of the default allocator. -You can read about jemalloc by reading the Synapse [README](../README.md). +You can read about jemalloc by reading the Synapse [README](../README.rst). From beb251e3eed3f5b93fafea4650ba7146bb19bcf9 Mon Sep 17 00:00:00 2001 From: Rohan Sharma Date: Mon, 7 Jun 2021 21:05:02 +0530 Subject: [PATCH 245/619] Make link in docs use HTTPS (#10130) Fixes #10121 Signed-off-by: Rohan Sharma --- changelog.d/10130.doc | 1 + docs/turn-howto.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10130.doc diff --git a/changelog.d/10130.doc b/changelog.d/10130.doc new file mode 100644 index 0000000000..42ed1f3eac --- /dev/null +++ b/changelog.d/10130.doc @@ -0,0 +1 @@ +Make a link in docs use HTTPS. Contributed by @RhnSharma. diff --git a/docs/turn-howto.md b/docs/turn-howto.md index 41738bbe69..6433446c2a 100644 --- a/docs/turn-howto.md +++ b/docs/turn-howto.md @@ -4,7 +4,7 @@ This document explains how to enable VoIP relaying on your Home Server with TURN. The synapse Matrix Home Server supports integration with TURN server via the -[TURN server REST API](). This +[TURN server REST API](). This allows the Home Server to generate credentials that are valid for use on the TURN server through the use of a secret shared between the Home Server and the TURN server. From b2557cbf42d39fbd8a497a2f859cb45f84539da9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 7 Jun 2021 17:57:49 +0100 Subject: [PATCH 246/619] opentracing: use a consistent name for background processes (#10135) ... otherwise we tend to get a namespace clash between the bg process and the functions that it calls. --- changelog.d/10135.misc | 1 + synapse/logging/opentracing.py | 1 + synapse/metrics/background_process_metrics.py | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10135.misc diff --git a/changelog.d/10135.misc b/changelog.d/10135.misc new file mode 100644 index 0000000000..17819cbbcc --- /dev/null +++ b/changelog.d/10135.misc @@ -0,0 +1 @@ +OpenTracing: use a consistent name for background processes. diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index 26c8ffe780..dd9377340e 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -337,6 +337,7 @@ def ensure_active_span_inner_2(*args, **kwargs): @contextlib.contextmanager def noop_context_manager(*args, **kwargs): """Does exactly what it says on the tin""" + # TODO: replace with contextlib.nullcontext once we drop support for Python 3.6 yield diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index 0d6d643d35..de96ca0821 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -204,11 +204,12 @@ async def run(): with BackgroundProcessLoggingContext(desc, count) as context: try: - ctx = noop_context_manager() if bg_start_span: ctx = start_active_span( - desc, tags={SynapseTags.REQUEST_ID: str(context)} + f"bgproc.{desc}", tags={SynapseTags.REQUEST_ID: str(context)} ) + else: + ctx = noop_context_manager() with ctx: return await maybe_awaitable(func(*args, **kwargs)) except Exception: From 0acb5010eca4a31aad9b3e1537b26c1bb5237c98 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 7 Jun 2021 18:01:32 +0100 Subject: [PATCH 247/619] More database opentracing (#10136) Add a couple of extra logs/spans, to give a bit of a better idea. --- changelog.d/10136.feature | 1 + synapse/storage/database.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10136.feature diff --git a/changelog.d/10136.feature b/changelog.d/10136.feature new file mode 100644 index 0000000000..2658ab8918 --- /dev/null +++ b/changelog.d/10136.feature @@ -0,0 +1 @@ +Report OpenTracing spans for database activity. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 974703d13a..b77368a460 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -541,6 +541,7 @@ def new_transaction( }, ): r = func(cursor, *args, **kwargs) + opentracing.log_kv({"message": "commit"}) conn.commit() return r except self.engine.module.OperationalError as e: @@ -556,7 +557,8 @@ def new_transaction( if i < N: i += 1 try: - conn.rollback() + with opentracing.start_active_span("db.rollback"): + conn.rollback() except self.engine.module.Error as e1: transaction_logger.warning("[TXN EROLL] {%s} %s", name, e1) continue @@ -569,7 +571,8 @@ def new_transaction( if i < N: i += 1 try: - conn.rollback() + with opentracing.start_active_span("db.rollback"): + conn.rollback() except self.engine.module.Error as e1: transaction_logger.warning( "[TXN EROLL] {%s} %s", From a0101fc02148d7e1d603a1e95e6c5b990fd2ff58 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jun 2021 10:37:01 +0100 Subject: [PATCH 248/619] Handle /backfill returning no events (#10133) Fixes #10123 --- changelog.d/10133.bugfix | 1 + synapse/handlers/federation.py | 38 ++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 changelog.d/10133.bugfix diff --git a/changelog.d/10133.bugfix b/changelog.d/10133.bugfix new file mode 100644 index 0000000000..a62c15b260 --- /dev/null +++ b/changelog.d/10133.bugfix @@ -0,0 +1 @@ +Fix bug when using workers where pagination requests failed if a remote server returned zero events from `/backfill`. Introduced in 1.35.0. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index b802822baa..abbb71424d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -22,6 +22,7 @@ from http import HTTPStatus from typing import ( TYPE_CHECKING, + Collection, Dict, Iterable, List, @@ -1364,11 +1365,12 @@ async def get_event(event_id: str): event_infos.append(_NewEventInfo(event, None, auth)) - await self._auth_and_persist_events( - destination, - room_id, - event_infos, - ) + if event_infos: + await self._auth_and_persist_events( + destination, + room_id, + event_infos, + ) def _sanity_check_event(self, ev: EventBase) -> None: """ @@ -2077,7 +2079,7 @@ async def _auth_and_persist_events( self, origin: str, room_id: str, - event_infos: Iterable[_NewEventInfo], + event_infos: Collection[_NewEventInfo], backfilled: bool = False, ) -> None: """Creates the appropriate contexts and persists events. The events @@ -2088,6 +2090,9 @@ async def _auth_and_persist_events( Notifies about the events where appropriate. """ + if not event_infos: + return + async def prep(ev_info: _NewEventInfo): event = ev_info.event with nested_logging_context(suffix=event.event_id): @@ -2216,13 +2221,14 @@ async def _persist_auth_tree( raise events_to_context[e.event_id].rejected = RejectedReason.AUTH_ERROR - await self.persist_events_and_notify( - room_id, - [ - (e, events_to_context[e.event_id]) - for e in itertools.chain(auth_events, state) - ], - ) + if auth_events or state: + await self.persist_events_and_notify( + room_id, + [ + (e, events_to_context[e.event_id]) + for e in itertools.chain(auth_events, state) + ], + ) new_event_context = await self.state_handler.compute_event_context( event, old_state=state @@ -3061,7 +3067,13 @@ async def persist_events_and_notify( the same room. backfilled: Whether these events are a result of backfilling or not + + Returns: + The stream ID after which all events have been persisted. """ + if not event_and_contexts: + return self.store.get_current_events_token() + instance = self.config.worker.events_shard_config.get_instance(room_id) if instance != self._instance_name: # Limit the number of events sent over replication. We choose 200 From c842c581ed3d33cf0ca1972507508758f7aad1c8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jun 2021 11:07:46 +0100 Subject: [PATCH 249/619] When joining a remote room limit the number of events we concurrently check signatures/hashes for (#10117) If we do hundreds of thousands at once the memory overhead can easily reach 500+ MB. --- changelog.d/10117.feature | 1 + synapse/crypto/keyring.py | 46 ++--- synapse/federation/federation_base.py | 243 ++++++++---------------- synapse/federation/federation_client.py | 147 ++++++++------ synapse/util/async_helpers.py | 21 +- 5 files changed, 202 insertions(+), 256 deletions(-) create mode 100644 changelog.d/10117.feature diff --git a/changelog.d/10117.feature b/changelog.d/10117.feature new file mode 100644 index 0000000000..e137e142c6 --- /dev/null +++ b/changelog.d/10117.feature @@ -0,0 +1 @@ +Significantly reduce memory usage of joining large remote rooms. diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index c840ffca71..e5a4685ed4 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -233,41 +233,19 @@ def verify_json_objects_for_server( for server_name, json_object, validity_time in server_and_json ] - def verify_events_for_server( - self, server_and_events: Iterable[Tuple[str, EventBase, int]] - ) -> List[defer.Deferred]: - """Bulk verification of signatures on events. - - Args: - server_and_events: - Iterable of `(server_name, event, validity_time)` tuples. - - `server_name` is which server we are verifying the signature for - on the event. - - `event` is the event that we'll verify the signatures of for - the given `server_name`. - - `validity_time` is a timestamp at which the signing key must be - valid. - - Returns: - List: for each input triplet, a deferred indicating success - or failure to verify each event's signature for the given - server_name. The deferreds run their callbacks in the sentinel - logcontext. - """ - return [ - run_in_background( - self.process_request, - VerifyJsonRequest.from_event( - server_name, - event, - validity_time, - ), + async def verify_event_for_server( + self, + server_name: str, + event: EventBase, + validity_time: int, + ) -> None: + await self.process_request( + VerifyJsonRequest.from_event( + server_name, + event, + validity_time, ) - for server_name, event, validity_time in server_and_events - ] + ) async def process_request(self, verify_request: VerifyJsonRequest) -> None: """Processes the `VerifyJsonRequest`. Raises if the object is not signed diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 3fe496dcd3..c066617b92 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -14,11 +14,6 @@ # limitations under the License. import logging from collections import namedtuple -from typing import Iterable, List - -from twisted.internet import defer -from twisted.internet.defer import Deferred, DeferredList -from twisted.python.failure import Failure from synapse.api.constants import MAX_DEPTH, EventTypes, Membership from synapse.api.errors import Codes, SynapseError @@ -28,11 +23,6 @@ from synapse.events import EventBase, make_event_from_dict from synapse.events.utils import prune_event, validate_canonicaljson from synapse.http.servlet import assert_params_in_dict -from synapse.logging.context import ( - PreserveLoggingContext, - current_context, - make_deferred_yieldable, -) from synapse.types import JsonDict, get_domain_from_id logger = logging.getLogger(__name__) @@ -48,112 +38,82 @@ def __init__(self, hs): self.store = hs.get_datastore() self._clock = hs.get_clock() - def _check_sigs_and_hash( + async def _check_sigs_and_hash( self, room_version: RoomVersion, pdu: EventBase - ) -> Deferred: - return make_deferred_yieldable( - self._check_sigs_and_hashes(room_version, [pdu])[0] - ) - - def _check_sigs_and_hashes( - self, room_version: RoomVersion, pdus: List[EventBase] - ) -> List[Deferred]: - """Checks that each of the received events is correctly signed by the - sending server. + ) -> EventBase: + """Checks that event is correctly signed by the sending server. Args: - room_version: The room version of the PDUs - pdus: the events to be checked + room_version: The room version of the PDU + pdu: the event to be checked Returns: - For each input event, a deferred which: - * returns the original event if the checks pass - * returns a redacted version of the event (if the signature + * the original event if the checks pass + * a redacted version of the event (if the signature matched but the hash did not) - * throws a SynapseError if the signature check failed. - The deferreds run their callbacks in the sentinel - """ - deferreds = _check_sigs_on_pdus(self.keyring, room_version, pdus) - - ctx = current_context() - - @defer.inlineCallbacks - def callback(_, pdu: EventBase): - with PreserveLoggingContext(ctx): - if not check_event_content_hash(pdu): - # let's try to distinguish between failures because the event was - # redacted (which are somewhat expected) vs actual ball-tampering - # incidents. - # - # This is just a heuristic, so we just assume that if the keys are - # about the same between the redacted and received events, then the - # received event was probably a redacted copy (but we then use our - # *actual* redacted copy to be on the safe side.) - redacted_event = prune_event(pdu) - if set(redacted_event.keys()) == set(pdu.keys()) and set( - redacted_event.content.keys() - ) == set(pdu.content.keys()): - logger.info( - "Event %s seems to have been redacted; using our redacted " - "copy", - pdu.event_id, - ) - else: - logger.warning( - "Event %s content has been tampered, redacting", - pdu.event_id, - ) - return redacted_event - - result = yield defer.ensureDeferred( - self.spam_checker.check_event_for_spam(pdu) + * throws a SynapseError if the signature check failed.""" + try: + await _check_sigs_on_pdu(self.keyring, room_version, pdu) + except SynapseError as e: + logger.warning( + "Signature check failed for %s: %s", + pdu.event_id, + e, + ) + raise + + if not check_event_content_hash(pdu): + # let's try to distinguish between failures because the event was + # redacted (which are somewhat expected) vs actual ball-tampering + # incidents. + # + # This is just a heuristic, so we just assume that if the keys are + # about the same between the redacted and received events, then the + # received event was probably a redacted copy (but we then use our + # *actual* redacted copy to be on the safe side.) + redacted_event = prune_event(pdu) + if set(redacted_event.keys()) == set(pdu.keys()) and set( + redacted_event.content.keys() + ) == set(pdu.content.keys()): + logger.info( + "Event %s seems to have been redacted; using our redacted copy", + pdu.event_id, ) - - if result: - logger.warning( - "Event contains spam, redacting %s: %s", - pdu.event_id, - pdu.get_pdu_json(), - ) - return prune_event(pdu) - - return pdu - - def errback(failure: Failure, pdu: EventBase): - failure.trap(SynapseError) - with PreserveLoggingContext(ctx): + else: logger.warning( - "Signature check failed for %s: %s", + "Event %s content has been tampered, redacting", pdu.event_id, - failure.getErrorMessage(), ) - return failure + return redacted_event - for deferred, pdu in zip(deferreds, pdus): - deferred.addCallbacks( - callback, errback, callbackArgs=[pdu], errbackArgs=[pdu] + result = await self.spam_checker.check_event_for_spam(pdu) + + if result: + logger.warning( + "Event contains spam, redacting %s: %s", + pdu.event_id, + pdu.get_pdu_json(), ) + return prune_event(pdu) - return deferreds + return pdu class PduToCheckSig(namedtuple("PduToCheckSig", ["pdu", "sender_domain", "deferreds"])): pass -def _check_sigs_on_pdus( - keyring: Keyring, room_version: RoomVersion, pdus: Iterable[EventBase] -) -> List[Deferred]: +async def _check_sigs_on_pdu( + keyring: Keyring, room_version: RoomVersion, pdu: EventBase +) -> None: """Check that the given events are correctly signed + Raise a SynapseError if the event wasn't correctly signed. + Args: keyring: keyring object to do the checks room_version: the room version of the PDUs pdus: the events to be checked - - Returns: - A Deferred for each event in pdus, which will either succeed if - the signatures are valid, or fail (with a SynapseError) if not. """ # we want to check that the event is signed by: @@ -177,90 +137,47 @@ def _check_sigs_on_pdus( # let's start by getting the domain for each pdu, and flattening the event back # to JSON. - pdus_to_check = [ - PduToCheckSig( - pdu=p, - sender_domain=get_domain_from_id(p.sender), - deferreds=[], - ) - for p in pdus - ] - # First we check that the sender event is signed by the sender's domain # (except if its a 3pid invite, in which case it may be sent by any server) - pdus_to_check_sender = [p for p in pdus_to_check if not _is_invite_via_3pid(p.pdu)] - - more_deferreds = keyring.verify_events_for_server( - [ - ( - p.sender_domain, - p.pdu, - p.pdu.origin_server_ts if room_version.enforce_key_validity else 0, + if not _is_invite_via_3pid(pdu): + try: + await keyring.verify_event_for_server( + get_domain_from_id(pdu.sender), + pdu, + pdu.origin_server_ts if room_version.enforce_key_validity else 0, ) - for p in pdus_to_check_sender - ] - ) - - def sender_err(e, pdu_to_check): - errmsg = "event id %s: unable to verify signature for sender %s: %s" % ( - pdu_to_check.pdu.event_id, - pdu_to_check.sender_domain, - e.getErrorMessage(), - ) - raise SynapseError(403, errmsg, Codes.FORBIDDEN) - - for p, d in zip(pdus_to_check_sender, more_deferreds): - d.addErrback(sender_err, p) - p.deferreds.append(d) + except Exception as e: + errmsg = "event id %s: unable to verify signature for sender %s: %s" % ( + pdu.event_id, + get_domain_from_id(pdu.sender), + e, + ) + raise SynapseError(403, errmsg, Codes.FORBIDDEN) # now let's look for events where the sender's domain is different to the # event id's domain (normally only the case for joins/leaves), and add additional # checks. Only do this if the room version has a concept of event ID domain # (ie, the room version uses old-style non-hash event IDs). - if room_version.event_format == EventFormatVersions.V1: - pdus_to_check_event_id = [ - p - for p in pdus_to_check - if p.sender_domain != get_domain_from_id(p.pdu.event_id) - ] - - more_deferreds = keyring.verify_events_for_server( - [ - ( - get_domain_from_id(p.pdu.event_id), - p.pdu, - p.pdu.origin_server_ts if room_version.enforce_key_validity else 0, - ) - for p in pdus_to_check_event_id - ] - ) - - def event_err(e, pdu_to_check): + if room_version.event_format == EventFormatVersions.V1 and get_domain_from_id( + pdu.event_id + ) != get_domain_from_id(pdu.sender): + try: + await keyring.verify_event_for_server( + get_domain_from_id(pdu.event_id), + pdu, + pdu.origin_server_ts if room_version.enforce_key_validity else 0, + ) + except Exception as e: errmsg = ( - "event id %s: unable to verify signature for event id domain: %s" - % (pdu_to_check.pdu.event_id, e.getErrorMessage()) + "event id %s: unable to verify signature for event id domain %s: %s" + % ( + pdu.event_id, + get_domain_from_id(pdu.event_id), + e, + ) ) raise SynapseError(403, errmsg, Codes.FORBIDDEN) - for p, d in zip(pdus_to_check_event_id, more_deferreds): - d.addErrback(event_err, p) - p.deferreds.append(d) - - # replace lists of deferreds with single Deferreds - return [_flatten_deferred_list(p.deferreds) for p in pdus_to_check] - - -def _flatten_deferred_list(deferreds: List[Deferred]) -> Deferred: - """Given a list of deferreds, either return the single deferred, - combine into a DeferredList, or return an already resolved deferred. - """ - if len(deferreds) > 1: - return DeferredList(deferreds, fireOnOneErrback=True, consumeErrors=True) - elif len(deferreds) == 1: - return deferreds[0] - else: - return defer.succeed(None) - def _is_invite_via_3pid(event: EventBase) -> bool: return ( diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index e0e9f5d0be..1076ebc036 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -21,6 +21,7 @@ Any, Awaitable, Callable, + Collection, Dict, Iterable, List, @@ -35,9 +36,6 @@ import attr from prometheus_client import Counter -from twisted.internet import defer -from twisted.internet.defer import Deferred - from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( CodeMessageException, @@ -56,10 +54,9 @@ from synapse.events import EventBase, builder from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.federation.transport.client import SendJoinResponse -from synapse.logging.context import make_deferred_yieldable, preserve_fn from synapse.logging.utils import log_function from synapse.types import JsonDict, get_domain_from_id -from synapse.util import unwrapFirstError +from synapse.util.async_helpers import concurrently_execute from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.retryutils import NotRetryingDestination @@ -360,10 +357,9 @@ async def get_room_state_ids( async def _check_sigs_and_hash_and_fetch( self, origin: str, - pdus: List[EventBase], + pdus: Collection[EventBase], room_version: RoomVersion, outlier: bool = False, - include_none: bool = False, ) -> List[EventBase]: """Takes a list of PDUs and checks the signatures and hashes of each one. If a PDU fails its signature check then we check if we have it in @@ -380,57 +376,87 @@ async def _check_sigs_and_hash_and_fetch( pdu room_version outlier: Whether the events are outliers or not - include_none: Whether to include None in the returned list - for events that have failed their checks Returns: A list of PDUs that have valid signatures and hashes. """ - deferreds = self._check_sigs_and_hashes(room_version, pdus) - async def handle_check_result(pdu: EventBase, deferred: Deferred): - try: - res = await make_deferred_yieldable(deferred) - except SynapseError: - res = None + # We limit how many PDUs we check at once, as if we try to do hundreds + # of thousands of PDUs at once we see large memory spikes. - if not res: - # Check local db. - res = await self.store.get_event( - pdu.event_id, allow_rejected=True, allow_none=True - ) + valid_pdus = [] - pdu_origin = get_domain_from_id(pdu.sender) - if not res and pdu_origin != origin: - try: - res = await self.get_pdu( - destinations=[pdu_origin], - event_id=pdu.event_id, - room_version=room_version, - outlier=outlier, - timeout=10000, - ) - except SynapseError: - pass + async def _execute(pdu: EventBase) -> None: + valid_pdu = await self._check_sigs_and_hash_and_fetch_one( + pdu=pdu, + origin=origin, + outlier=outlier, + room_version=room_version, + ) - if not res: - logger.warning( - "Failed to find copy of %s with valid signature", pdu.event_id - ) + if valid_pdu: + valid_pdus.append(valid_pdu) - return res + await concurrently_execute(_execute, pdus, 10000) - handle = preserve_fn(handle_check_result) - deferreds2 = [handle(pdu, deferred) for pdu, deferred in zip(pdus, deferreds)] + return valid_pdus - valid_pdus = await make_deferred_yieldable( - defer.gatherResults(deferreds2, consumeErrors=True) - ).addErrback(unwrapFirstError) + async def _check_sigs_and_hash_and_fetch_one( + self, + pdu: EventBase, + origin: str, + room_version: RoomVersion, + outlier: bool = False, + ) -> Optional[EventBase]: + """Takes a PDU and checks its signatures and hashes. If the PDU fails + its signature check then we check if we have it in the database and if + not then request if from the originating server of that PDU. - if include_none: - return valid_pdus - else: - return [p for p in valid_pdus if p] + If then PDU fails its content hash check then it is redacted. + + Args: + origin + pdu + room_version + outlier: Whether the events are outliers or not + include_none: Whether to include None in the returned list + for events that have failed their checks + + Returns: + The PDU (possibly redacted) if it has valid signatures and hashes. + """ + + res = None + try: + res = await self._check_sigs_and_hash(room_version, pdu) + except SynapseError: + pass + + if not res: + # Check local db. + res = await self.store.get_event( + pdu.event_id, allow_rejected=True, allow_none=True + ) + + pdu_origin = get_domain_from_id(pdu.sender) + if not res and pdu_origin != origin: + try: + res = await self.get_pdu( + destinations=[pdu_origin], + event_id=pdu.event_id, + room_version=room_version, + outlier=outlier, + timeout=10000, + ) + except SynapseError: + pass + + if not res: + logger.warning( + "Failed to find copy of %s with valid signature", pdu.event_id + ) + + return res async def get_event_auth( self, destination: str, room_id: str, event_id: str @@ -671,8 +697,6 @@ async def send_request(destination) -> Dict[str, Any]: state = response.state auth_chain = response.auth_events - pdus = {p.event_id: p for p in itertools.chain(state, auth_chain)} - create_event = None for e in state: if (e.type, e.state_key) == (EventTypes.Create, ""): @@ -696,14 +720,29 @@ async def send_request(destination) -> Dict[str, Any]: % (create_room_version,) ) - valid_pdus = await self._check_sigs_and_hash_and_fetch( - destination, - list(pdus.values()), - outlier=True, - room_version=room_version, + logger.info( + "Processing from send_join %d events", len(state) + len(auth_chain) ) - valid_pdus_map = {p.event_id: p for p in valid_pdus} + # We now go and check the signatures and hashes for the event. Note + # that we limit how many events we process at a time to keep the + # memory overhead from exploding. + valid_pdus_map: Dict[str, EventBase] = {} + + async def _execute(pdu: EventBase) -> None: + valid_pdu = await self._check_sigs_and_hash_and_fetch_one( + pdu=pdu, + origin=destination, + outlier=True, + room_version=room_version, + ) + + if valid_pdu: + valid_pdus_map[valid_pdu.event_id] = valid_pdu + + await concurrently_execute( + _execute, itertools.chain(state, auth_chain), 10000 + ) # NB: We *need* to copy to ensure that we don't have multiple # references being passed on, as that causes... issues. diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 5c55bb0125..061102c3c8 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -15,6 +15,7 @@ import collections import inspect +import itertools import logging from contextlib import contextmanager from typing import ( @@ -160,8 +161,11 @@ def __repr__(self) -> str: ) +T = TypeVar("T") + + def concurrently_execute( - func: Callable, args: Iterable[Any], limit: int + func: Callable[[T], Any], args: Iterable[T], limit: int ) -> defer.Deferred: """Executes the function with each argument concurrently while limiting the number of concurrent executions. @@ -173,20 +177,27 @@ def concurrently_execute( limit: Maximum number of conccurent executions. Returns: - Deferred[list]: Resolved when all function invocations have finished. + Deferred: Resolved when all function invocations have finished. """ it = iter(args) - async def _concurrently_execute_inner(): + async def _concurrently_execute_inner(value: T) -> None: try: while True: - await maybe_awaitable(func(next(it))) + await maybe_awaitable(func(value)) + value = next(it) except StopIteration: pass + # We use `itertools.islice` to handle the case where the number of args is + # less than the limit, avoiding needlessly spawning unnecessary background + # tasks. return make_deferred_yieldable( defer.gatherResults( - [run_in_background(_concurrently_execute_inner) for _ in range(limit)], + [ + run_in_background(_concurrently_execute_inner, value) + for value in itertools.islice(it, limit) + ], consumeErrors=True, ) ).addErrback(unwrapFirstError) From 7dc14730d925a39a885a14ce309d99054f9617d5 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Tue, 8 Jun 2021 11:44:50 +0100 Subject: [PATCH 250/619] Name release branches just after major.minor (#10013) With the prior format, 1.33.0 / 1.33.1 / 1.33.2 got separate branches: release-v1.33.0 release-v1.33.1 release-v1.33.2 Under the new model, all three would share a common branch: release-v1.33 As before, RCs and actual releases exist as tags on these branches. This better reflects our support model, e.g., that the "1.33" series had a formal release followed by two patches / updates. Signed-off-by: Dan Callahan --- changelog.d/10013.misc | 1 + docs/dev/git.md | 8 ++++---- scripts-dev/release.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10013.misc diff --git a/changelog.d/10013.misc b/changelog.d/10013.misc new file mode 100644 index 0000000000..9d164d9ce2 --- /dev/null +++ b/changelog.d/10013.misc @@ -0,0 +1 @@ +Simplify naming convention for release branches to only include the major and minor version numbers. diff --git a/docs/dev/git.md b/docs/dev/git.md index b747ff20c9..87950f07b2 100644 --- a/docs/dev/git.md +++ b/docs/dev/git.md @@ -122,15 +122,15 @@ So, what counts as a more- or less-stable branch? A little reflection will show that our active branches are ordered thus, from more-stable to less-stable: * `master` (tracks our last release). - * `release-vX.Y.Z` (the branch where we prepare the next release)[3](#f3). * PR branches which are targeting the release. * `develop` (our "mainline" branch containing our bleeding-edge). * regular PR branches. The corollary is: if you have a bugfix that needs to land in both -`release-vX.Y.Z` *and* `develop`, then you should base your PR on -`release-vX.Y.Z`, get it merged there, and then merge from `release-vX.Y.Z` to +`release-vX.Y` *and* `develop`, then you should base your PR on +`release-vX.Y`, get it merged there, and then merge from `release-vX.Y` to `develop`. (If a fix lands in `develop` and we later need it in a release-branch, we can of course cherry-pick it, but landing it in the release branch first helps reduce the chance of annoying conflicts.) @@ -145,4 +145,4 @@ most intuitive name. [^](#a1) [3]: Very, very occasionally (I think this has happened once in the history of Synapse), we've had two releases in flight at once. Obviously, -`release-v1.2.3` is more-stable than `release-v1.3.0`. [^](#a3) +`release-v1.2` is more-stable than `release-v1.3`. [^](#a3) diff --git a/scripts-dev/release.py b/scripts-dev/release.py index 1042fa48bc..fc3df9071c 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -139,7 +139,7 @@ def run(): click.get_current_context().abort() # Switch to the release branch. - release_branch_name = f"release-v{base_version}" + release_branch_name = f"release-v{current_version.major}.{current_version.minor}" release_branch = find_ref(repo, release_branch_name) if release_branch: if release_branch.is_remote(): From 9e4610cc272fc8e5db39608de83ce48360889e42 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 8 Jun 2021 08:30:48 -0400 Subject: [PATCH 251/619] Correct type hints for parse_string(s)_from_args. (#10137) --- changelog.d/10137.misc | 1 + mypy.ini | 1 + synapse/http/servlet.py | 179 ++++++++++++++--------- synapse/rest/admin/rooms.py | 2 +- synapse/rest/client/v1/login.py | 8 +- synapse/rest/client/v1/room.py | 4 +- synapse/rest/consent/consent_resource.py | 9 +- synapse/rest/media/v1/upload_resource.py | 11 +- 8 files changed, 132 insertions(+), 83 deletions(-) create mode 100644 changelog.d/10137.misc diff --git a/changelog.d/10137.misc b/changelog.d/10137.misc new file mode 100644 index 0000000000..a901f8431e --- /dev/null +++ b/changelog.d/10137.misc @@ -0,0 +1 @@ +Add `parse_strings_from_args` for parsing an array from query parameters. diff --git a/mypy.ini b/mypy.ini index 8ba1b96311..1ab9001831 100644 --- a/mypy.ini +++ b/mypy.ini @@ -32,6 +32,7 @@ files = synapse/http/federation/matrix_federation_agent.py, synapse/http/federation/well_known_resolver.py, synapse/http/matrixfederationclient.py, + synapse/http/servlet.py, synapse/http/server.py, synapse/http/site.py, synapse/logging, diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 3f4f2411fc..d61563d39b 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -15,10 +15,12 @@ """ This module contains base REST classes for constructing REST servlets. """ import logging -from typing import Iterable, List, Optional, Union, overload +from typing import Dict, Iterable, List, Optional, overload from typing_extensions import Literal +from twisted.web.server import Request + from synapse.api.errors import Codes, SynapseError from synapse.util import json_decoder @@ -108,13 +110,66 @@ def parse_boolean_from_args(args, name, default=None, required=False): return default +@overload +def parse_bytes_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Literal[None] = None, + required: Literal[True] = True, +) -> bytes: + ... + + +@overload +def parse_bytes_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Optional[bytes] = None, + required: bool = False, +) -> Optional[bytes]: + ... + + +def parse_bytes_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Optional[bytes] = None, + required: bool = False, +) -> Optional[bytes]: + """ + Parse a string parameter as bytes from the request query string. + + Args: + args: A mapping of request args as bytes to a list of bytes (e.g. request.args). + name: the name of the query parameter. + default: value to use if the parameter is absent, + defaults to None. Must be bytes if encoding is None. + required: whether to raise a 400 SynapseError if the + parameter is absent, defaults to False. + Returns: + Bytes or the default value. + + Raises: + SynapseError if the parameter is absent and required. + """ + name_bytes = name.encode("ascii") + + if name_bytes in args: + return args[name_bytes][0] + elif required: + message = "Missing string query parameter %s" % (name,) + raise SynapseError(400, message, errcode=Codes.MISSING_PARAM) + + return default + + def parse_string( - request, - name: Union[bytes, str], + request: Request, + name: str, default: Optional[str] = None, required: bool = False, allowed_values: Optional[Iterable[str]] = None, - encoding: Optional[str] = "ascii", + encoding: str = "ascii", ): """ Parse a string parameter from the request query string. @@ -125,66 +180,65 @@ def parse_string( Args: request: the twisted HTTP request. name: the name of the query parameter. - default: value to use if the parameter is absent, - defaults to None. Must be bytes if encoding is None. + default: value to use if the parameter is absent, defaults to None. required: whether to raise a 400 SynapseError if the parameter is absent, defaults to False. allowed_values: List of allowed values for the string, or None if any value is allowed, defaults to None. Must be the same type as name, if given. - encoding : The encoding to decode the string content with. + encoding: The encoding to decode the string content with. + Returns: - A string value or the default. Unicode if encoding - was given, bytes otherwise. + A string value or the default. Raises: SynapseError if the parameter is absent and required, or if the parameter is present, must be one of a list of allowed values and is not one of those allowed values. """ + args = request.args # type: Dict[bytes, List[bytes]] # type: ignore return parse_string_from_args( - request.args, name, default, required, allowed_values, encoding + args, name, default, required, allowed_values, encoding ) def _parse_string_value( - value: Union[str, bytes], + value: bytes, allowed_values: Optional[Iterable[str]], name: str, - encoding: Optional[str], -) -> Union[str, bytes]: - if encoding: - try: - value = value.decode(encoding) - except ValueError: - raise SynapseError(400, "Query parameter %r must be %s" % (name, encoding)) + encoding: str, +) -> str: + try: + value_str = value.decode(encoding) + except ValueError: + raise SynapseError(400, "Query parameter %r must be %s" % (name, encoding)) - if allowed_values is not None and value not in allowed_values: + if allowed_values is not None and value_str not in allowed_values: message = "Query parameter %r must be one of [%s]" % ( name, ", ".join(repr(v) for v in allowed_values), ) raise SynapseError(400, message) else: - return value + return value_str @overload def parse_strings_from_args( - args: List[str], - name: Union[bytes, str], + args: Dict[bytes, List[bytes]], + name: str, default: Optional[List[str]] = None, - required: bool = False, + required: Literal[True] = True, allowed_values: Optional[Iterable[str]] = None, - encoding: Literal[None] = None, -) -> Optional[List[bytes]]: + encoding: str = "ascii", +) -> List[str]: ... @overload def parse_strings_from_args( - args: List[str], - name: Union[bytes, str], + args: Dict[bytes, List[bytes]], + name: str, default: Optional[List[str]] = None, required: bool = False, allowed_values: Optional[Iterable[str]] = None, @@ -194,46 +248,40 @@ def parse_strings_from_args( def parse_strings_from_args( - args: List[str], - name: Union[bytes, str], + args: Dict[bytes, List[bytes]], + name: str, default: Optional[List[str]] = None, required: bool = False, allowed_values: Optional[Iterable[str]] = None, - encoding: Optional[str] = "ascii", -) -> Optional[List[Union[bytes, str]]]: + encoding: str = "ascii", +) -> Optional[List[str]]: """ Parse a string parameter from the request query string list. - If encoding is not None, the content of the query param will be - decoded to Unicode using the encoding, otherwise it will be encoded + The content of the query param will be decoded to Unicode using the encoding. Args: - args: the twisted HTTP request.args list. + args: A mapping of request args as bytes to a list of bytes (e.g. request.args). name: the name of the query parameter. - default: value to use if the parameter is absent, - defaults to None. Must be bytes if encoding is None. - required : whether to raise a 400 SynapseError if the + default: value to use if the parameter is absent, defaults to None. + required: whether to raise a 400 SynapseError if the parameter is absent, defaults to False. - allowed_values (list[bytes|unicode]): List of allowed values for the - string, or None if any value is allowed, defaults to None. Must be - the same type as name, if given. + allowed_values: List of allowed values for the + string, or None if any value is allowed, defaults to None. encoding: The encoding to decode the string content with. Returns: - A string value or the default. Unicode if encoding - was given, bytes otherwise. + A string value or the default. Raises: SynapseError if the parameter is absent and required, or if the parameter is present, must be one of a list of allowed values and is not one of those allowed values. """ + name_bytes = name.encode("ascii") - if not isinstance(name, bytes): - name = name.encode("ascii") - - if name in args: - values = args[name] + if name_bytes in args: + values = args[name_bytes] return [ _parse_string_value(value, allowed_values, name=name, encoding=encoding) @@ -241,36 +289,30 @@ def parse_strings_from_args( ] else: if required: - message = "Missing string query parameter %r" % (name) + message = "Missing string query parameter %r" % (name,) raise SynapseError(400, message, errcode=Codes.MISSING_PARAM) - else: - - if encoding and isinstance(default, bytes): - return default.decode(encoding) - return default + return default def parse_string_from_args( - args: List[str], - name: Union[bytes, str], + args: Dict[bytes, List[bytes]], + name: str, default: Optional[str] = None, required: bool = False, allowed_values: Optional[Iterable[str]] = None, - encoding: Optional[str] = "ascii", -) -> Optional[Union[bytes, str]]: + encoding: str = "ascii", +) -> Optional[str]: """ Parse the string parameter from the request query string list and return the first result. - If encoding is not None, the content of the query param will be - decoded to Unicode using the encoding, otherwise it will be encoded + The content of the query param will be decoded to Unicode using the encoding. Args: - args: the twisted HTTP request.args list. + args: A mapping of request args as bytes to a list of bytes (e.g. request.args). name: the name of the query parameter. - default: value to use if the parameter is absent, - defaults to None. Must be bytes if encoding is None. + default: value to use if the parameter is absent, defaults to None. required: whether to raise a 400 SynapseError if the parameter is absent, defaults to False. allowed_values: List of allowed values for the @@ -279,8 +321,7 @@ def parse_string_from_args( encoding: The encoding to decode the string content with. Returns: - A string value or the default. Unicode if encoding - was given, bytes otherwise. + A string value or the default. Raises: SynapseError if the parameter is absent and required, or if the @@ -291,12 +332,15 @@ def parse_string_from_args( strings = parse_strings_from_args( args, name, - default=[default], + default=[default] if default is not None else None, required=required, allowed_values=allowed_values, encoding=encoding, ) + if strings is None: + return None + return strings[0] @@ -388,9 +432,8 @@ class attribute containing a pre-compiled regular expression. The automatic def register(self, http_server): """ Register this servlet with the given HTTP server. """ - if hasattr(self, "PATTERNS"): - patterns = self.PATTERNS - + patterns = getattr(self, "PATTERNS", None) + if patterns: for method in ("GET", "PUT", "POST", "DELETE"): if hasattr(self, "on_%s" % (method,)): servlet_classname = self.__class__.__name__ diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index f289ffe3d0..f0cddd2d2c 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -649,7 +649,7 @@ async def on_GET( limit = parse_integer(request, "limit", default=10) # picking the API shape for symmetry with /messages - filter_str = parse_string(request, b"filter", encoding="utf-8") + filter_str = parse_string(request, "filter", encoding="utf-8") if filter_str: filter_json = urlparse.unquote(filter_str) event_filter = Filter( diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 42e709ec14..f6be5f1020 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -14,7 +14,7 @@ import logging import re -from typing import TYPE_CHECKING, Awaitable, Callable, Dict, Optional +from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional from synapse.api.errors import Codes, LoginError, SynapseError from synapse.api.ratelimiting import Ratelimiter @@ -25,6 +25,7 @@ from synapse.http.server import HttpServer, finish_request from synapse.http.servlet import ( RestServlet, + parse_bytes_from_args, parse_json_object_from_request, parse_string, ) @@ -437,9 +438,8 @@ async def on_GET( finish_request(request) return - client_redirect_url = parse_string( - request, "redirectUrl", required=True, encoding=None - ) + args = request.args # type: Dict[bytes, List[bytes]] # type: ignore + client_redirect_url = parse_bytes_from_args(args, "redirectUrl", required=True) sso_url = await self._sso_handler.handle_redirect_request( request, client_redirect_url, diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 5a9c27f75f..122105854a 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -537,7 +537,7 @@ async def on_GET(self, request, room_id): self.store, request, default_limit=10 ) as_client_event = b"raw" not in request.args - filter_str = parse_string(request, b"filter", encoding="utf-8") + filter_str = parse_string(request, "filter", encoding="utf-8") if filter_str: filter_json = urlparse.unquote(filter_str) event_filter = Filter( @@ -652,7 +652,7 @@ async def on_GET(self, request, room_id, event_id): limit = parse_integer(request, "limit", default=10) # picking the API shape for symmetry with /messages - filter_str = parse_string(request, b"filter", encoding="utf-8") + filter_str = parse_string(request, "filter", encoding="utf-8") if filter_str: filter_json = urlparse.unquote(filter_str) event_filter = Filter( diff --git a/synapse/rest/consent/consent_resource.py b/synapse/rest/consent/consent_resource.py index b19cd8afc5..e52570cd8e 100644 --- a/synapse/rest/consent/consent_resource.py +++ b/synapse/rest/consent/consent_resource.py @@ -17,6 +17,7 @@ from hashlib import sha256 from http import HTTPStatus from os import path +from typing import Dict, List import jinja2 from jinja2 import TemplateNotFound @@ -24,7 +25,7 @@ from synapse.api.errors import NotFoundError, StoreError, SynapseError from synapse.config import ConfigError from synapse.http.server import DirectServeHtmlResource, respond_with_html -from synapse.http.servlet import parse_string +from synapse.http.servlet import parse_bytes_from_args, parse_string from synapse.types import UserID # language to use for the templates. TODO: figure this out from Accept-Language @@ -116,7 +117,8 @@ async def _async_render_GET(self, request): has_consented = False public_version = username == "" if not public_version: - userhmac_bytes = parse_string(request, "h", required=True, encoding=None) + args = request.args # type: Dict[bytes, List[bytes]] + userhmac_bytes = parse_bytes_from_args(args, "h", required=True) self._check_hash(username, userhmac_bytes) @@ -152,7 +154,8 @@ async def _async_render_POST(self, request): """ version = parse_string(request, "v", required=True) username = parse_string(request, "u", required=True) - userhmac = parse_string(request, "h", required=True, encoding=None) + args = request.args # type: Dict[bytes, List[bytes]] + userhmac = parse_bytes_from_args(args, "h", required=True) self._check_hash(username, userhmac) diff --git a/synapse/rest/media/v1/upload_resource.py b/synapse/rest/media/v1/upload_resource.py index 024a105bf2..62dc4aae2d 100644 --- a/synapse/rest/media/v1/upload_resource.py +++ b/synapse/rest/media/v1/upload_resource.py @@ -14,13 +14,13 @@ # limitations under the License. import logging -from typing import IO, TYPE_CHECKING +from typing import IO, TYPE_CHECKING, Dict, List, Optional from twisted.web.server import Request from synapse.api.errors import Codes, SynapseError from synapse.http.server import DirectServeJsonResource, respond_with_json -from synapse.http.servlet import parse_string +from synapse.http.servlet import parse_bytes_from_args from synapse.http.site import SynapseRequest from synapse.rest.media.v1.media_storage import SpamMediaException @@ -61,10 +61,11 @@ async def _async_render_POST(self, request: SynapseRequest) -> None: errcode=Codes.TOO_LARGE, ) - upload_name = parse_string(request, b"filename", encoding=None) - if upload_name: + args = request.args # type: Dict[bytes, List[bytes]] # type: ignore + upload_name_bytes = parse_bytes_from_args(args, "filename") + if upload_name_bytes: try: - upload_name = upload_name.decode("utf8") + upload_name = upload_name_bytes.decode("utf8") # type: Optional[str] except UnicodeDecodeError: raise SynapseError( msg="Invalid UTF-8 filename parameter: %r" % (upload_name), code=400 From 1092718cac3800080bb766b251ae472282aef751 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 8 Jun 2021 13:49:29 +0100 Subject: [PATCH 252/619] Fix logging context when opening new DB connection (#10141) Fixes #10140 --- changelog.d/10141.feature | 1 + synapse/storage/database.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10141.feature diff --git a/changelog.d/10141.feature b/changelog.d/10141.feature new file mode 100644 index 0000000000..2658ab8918 --- /dev/null +++ b/changelog.d/10141.feature @@ -0,0 +1 @@ +Report OpenTracing spans for database activity. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index b77368a460..d470cdacde 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -91,12 +91,18 @@ def make_pool( db_args = dict(db_config.config.get("args", {})) db_args.setdefault("cp_reconnect", True) + def _on_new_connection(conn): + # Ensure we have a logging context so we can correctly track queries, + # etc. + with LoggingContext("db.on_new_connection"): + engine.on_new_connection( + LoggingDatabaseConnection(conn, engine, "on_new_connection") + ) + return adbapi.ConnectionPool( db_config.config["name"], cp_reactor=reactor, - cp_openfun=lambda conn: engine.on_new_connection( - LoggingDatabaseConnection(conn, engine, "on_new_connection") - ), + cp_openfun=_on_new_connection, **db_args, ) From 8df9941cc2462bc8e99ebd02953c5090f4942463 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 8 Jun 2021 14:09:00 +0100 Subject: [PATCH 253/619] 1.36.0rc1 --- CHANGES.md | 68 +++++++++++++++++++++++++++++++++++++++ changelog.d/10013.misc | 1 - changelog.d/10035.feature | 1 - changelog.d/10040.feature | 1 - changelog.d/10044.feature | 1 - changelog.d/10046.doc | 1 - changelog.d/10048.misc | 1 - changelog.d/10054.misc | 1 - changelog.d/10055.misc | 1 - changelog.d/10057.doc | 1 - changelog.d/10059.misc | 1 - changelog.d/10063.removal | 1 - changelog.d/10069.misc | 1 - changelog.d/10074.misc | 1 - changelog.d/10077.feature | 1 - changelog.d/10078.misc | 1 - changelog.d/10082.bugfix | 1 - changelog.d/10084.feature | 1 - changelog.d/10086.doc | 1 - changelog.d/10089.doc | 1 - changelog.d/10091.misc | 1 - changelog.d/10092.bugfix | 1 - changelog.d/10094.misc | 1 - changelog.d/10102.misc | 1 - changelog.d/10105.misc | 1 - changelog.d/10107.bugfix | 1 - changelog.d/10111.misc | 1 - changelog.d/10112.misc | 1 - changelog.d/10113.feature | 1 - changelog.d/10116.bugfix | 1 - changelog.d/10117.feature | 1 - changelog.d/10118.bugfix | 1 - changelog.d/10124.misc | 1 - changelog.d/10130.doc | 1 - changelog.d/10132.doc | 1 - changelog.d/10133.bugfix | 1 - changelog.d/10135.misc | 1 - changelog.d/10136.feature | 1 - changelog.d/10137.misc | 1 - changelog.d/10141.feature | 1 - changelog.d/9221.doc | 1 - changelog.d/9224.feature | 1 - changelog.d/9906.misc | 1 - changelog.d/9953.feature | 1 - changelog.d/9973.feature | 1 - synapse/__init__.py | 2 +- 46 files changed, 69 insertions(+), 45 deletions(-) delete mode 100644 changelog.d/10013.misc delete mode 100644 changelog.d/10035.feature delete mode 100644 changelog.d/10040.feature delete mode 100644 changelog.d/10044.feature delete mode 100644 changelog.d/10046.doc delete mode 100644 changelog.d/10048.misc delete mode 100644 changelog.d/10054.misc delete mode 100644 changelog.d/10055.misc delete mode 100644 changelog.d/10057.doc delete mode 100644 changelog.d/10059.misc delete mode 100644 changelog.d/10063.removal delete mode 100644 changelog.d/10069.misc delete mode 100644 changelog.d/10074.misc delete mode 100644 changelog.d/10077.feature delete mode 100644 changelog.d/10078.misc delete mode 100644 changelog.d/10082.bugfix delete mode 100644 changelog.d/10084.feature delete mode 100644 changelog.d/10086.doc delete mode 100644 changelog.d/10089.doc delete mode 100644 changelog.d/10091.misc delete mode 100644 changelog.d/10092.bugfix delete mode 100644 changelog.d/10094.misc delete mode 100644 changelog.d/10102.misc delete mode 100644 changelog.d/10105.misc delete mode 100644 changelog.d/10107.bugfix delete mode 100644 changelog.d/10111.misc delete mode 100644 changelog.d/10112.misc delete mode 100644 changelog.d/10113.feature delete mode 100644 changelog.d/10116.bugfix delete mode 100644 changelog.d/10117.feature delete mode 100644 changelog.d/10118.bugfix delete mode 100644 changelog.d/10124.misc delete mode 100644 changelog.d/10130.doc delete mode 100644 changelog.d/10132.doc delete mode 100644 changelog.d/10133.bugfix delete mode 100644 changelog.d/10135.misc delete mode 100644 changelog.d/10136.feature delete mode 100644 changelog.d/10137.misc delete mode 100644 changelog.d/10141.feature delete mode 100644 changelog.d/9221.doc delete mode 100644 changelog.d/9224.feature delete mode 100644 changelog.d/9906.misc delete mode 100644 changelog.d/9953.feature delete mode 100644 changelog.d/9973.feature diff --git a/CHANGES.md b/CHANGES.md index 04d260f8e5..69c876e38e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,71 @@ +Synapse 1.36.0rc1 (2021-06-08) +============================== + +Features +-------- + +- Add new endpoint `/_matrix/client/r0/rooms/{roomId}/aliases` from Client-Server API r0.6.1 (previously [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432)). ([\#9224](https://github.com/matrix-org/synapse/issues/9224)) +- Improve performance of incoming federation transactions in large rooms. ([\#9953](https://github.com/matrix-org/synapse/issues/9953), [\#9973](https://github.com/matrix-org/synapse/issues/9973)) +- Rewrite logic around verifying JSON object and fetching server keys to be more performant and use less memory. ([\#10035](https://github.com/matrix-org/synapse/issues/10035)) +- Add an admin API for unprotecting local media from quarantine. Contributed by @dklimpel. ([\#10040](https://github.com/matrix-org/synapse/issues/10040)) +- Add new admin APIs to remove media by media ID from quarantine. Contributed by @dkimpel. ([\#10044](https://github.com/matrix-org/synapse/issues/10044)) +- Make reason and score parameters optional for reporting content. Implements [MSC2414](https://github.com/matrix-org/matrix-doc/pull/2414). Contributed by Callum Brown. ([\#10077](https://github.com/matrix-org/synapse/issues/10077)) +- Add support for routing more requests to workers. ([\#10084](https://github.com/matrix-org/synapse/issues/10084)) +- Report OpenTracing spans for database activity. ([\#10113](https://github.com/matrix-org/synapse/issues/10113), [\#10136](https://github.com/matrix-org/synapse/issues/10136), [\#10141](https://github.com/matrix-org/synapse/issues/10141)) +- Significantly reduce memory usage of joining large remote rooms. ([\#10117](https://github.com/matrix-org/synapse/issues/10117)) + + +Bugfixes +-------- + +- Fixed a bug causing replication requests to fail when receiving a lot of events via federation. ([\#10082](https://github.com/matrix-org/synapse/issues/10082)) +- Fix a bug in the `force_tracing_for_users` option introduced in Synapse v1.35 which meant that the OpenTracing spans produced were missing most tags. ([\#10092](https://github.com/matrix-org/synapse/issues/10092)) +- Fixed a bug that could cause Synapse to stop notifying application services. Contributed by Willem Mulder. ([\#10107](https://github.com/matrix-org/synapse/issues/10107)) +- Fix bug where the server would attempt to fetch the same history in the room from a remote server multiple times in parallel. ([\#10116](https://github.com/matrix-org/synapse/issues/10116)) +- Fix a bug introduced in Synapse 1.33.0 which caused replication requests to fail when receiving a lot of very large events via federation. ([\#10118](https://github.com/matrix-org/synapse/issues/10118)) +- Fix bug when using workers where pagination requests failed if a remote server returned zero events from `/backfill`. Introduced in 1.35.0. ([\#10133](https://github.com/matrix-org/synapse/issues/10133)) + + +Improved Documentation +---------------------- + +- Clarify security note regarding hosting Synapse on the same domain as other web applications. ([\#9221](https://github.com/matrix-org/synapse/issues/9221)) +- Update CAPTCHA documentation to mention turning off the verify origin feature. Contributed by @aaronraimist. ([\#10046](https://github.com/matrix-org/synapse/issues/10046)) +- Tweak wording of database recommendation in `INSTALL.md`. Contributed by @aaronraimist. ([\#10057](https://github.com/matrix-org/synapse/issues/10057)) +- Add initial infrastructure for rendering Synapse documentation with mdbook. ([\#10086](https://github.com/matrix-org/synapse/issues/10086)) +- Convert the remaining Admin API documentation files to markdown. ([\#10089](https://github.com/matrix-org/synapse/issues/10089)) +- Make a link in docs use HTTPS. Contributed by @RhnSharma. ([\#10130](https://github.com/matrix-org/synapse/issues/10130)) +- Fix broken link in Docker docs. ([\#10132](https://github.com/matrix-org/synapse/issues/10132)) + + +Deprecations and Removals +------------------------- + +- Remove the experimental `spaces_enabled` flag. The spaces features are always available now. ([\#10063](https://github.com/matrix-org/synapse/issues/10063)) + + +Internal Changes +---------------- + +- Tell CircleCI to build Docker images from `main` branch. ([\#9906](https://github.com/matrix-org/synapse/issues/9906)) +- Simplify naming convention for release branches to only include the major and minor version numbers. ([\#10013](https://github.com/matrix-org/synapse/issues/10013)) +- Add `parse_strings_from_args` for parsing an array from query parameters. ([\#10048](https://github.com/matrix-org/synapse/issues/10048), [\#10137](https://github.com/matrix-org/synapse/issues/10137)) +- Remove some dead code regarding TLS certificate handling. ([\#10054](https://github.com/matrix-org/synapse/issues/10054)) +- Remove redundant, unmaintained `convert_server_keys` script. ([\#10055](https://github.com/matrix-org/synapse/issues/10055)) +- Improve the error message printed by synctl when synapse fails to start. ([\#10059](https://github.com/matrix-org/synapse/issues/10059)) +- Fix GitHub Actions lint for newsfragments. ([\#10069](https://github.com/matrix-org/synapse/issues/10069)) +- Update opentracing to inject the right context into the carrier. ([\#10074](https://github.com/matrix-org/synapse/issues/10074)) +- Fix up `BatchingQueue` implementation. ([\#10078](https://github.com/matrix-org/synapse/issues/10078)) +- Log method and path when dropping request due to size limit. ([\#10091](https://github.com/matrix-org/synapse/issues/10091)) +- In Github Actions workflows, summarize the Sytest results in an easy-to-read format. ([\#10094](https://github.com/matrix-org/synapse/issues/10094)) +- Make `/sync` do fewer state resolutions. ([\#10102](https://github.com/matrix-org/synapse/issues/10102)) +- Add missing type hints to the admin API servlets. ([\#10105](https://github.com/matrix-org/synapse/issues/10105)) +- Improve opentracing annotations for `Notifier`. ([\#10111](https://github.com/matrix-org/synapse/issues/10111)) +- Enable Prometheus metrics for the jaeger client library. ([\#10112](https://github.com/matrix-org/synapse/issues/10112)) +- Work to improve the responsiveness of `/sync` requests. ([\#10124](https://github.com/matrix-org/synapse/issues/10124)) +- OpenTracing: use a consistent name for background processes. ([\#10135](https://github.com/matrix-org/synapse/issues/10135)) + + Synapse 1.35.1 (2021-06-03) =========================== diff --git a/changelog.d/10013.misc b/changelog.d/10013.misc deleted file mode 100644 index 9d164d9ce2..0000000000 --- a/changelog.d/10013.misc +++ /dev/null @@ -1 +0,0 @@ -Simplify naming convention for release branches to only include the major and minor version numbers. diff --git a/changelog.d/10035.feature b/changelog.d/10035.feature deleted file mode 100644 index 68052b5a7e..0000000000 --- a/changelog.d/10035.feature +++ /dev/null @@ -1 +0,0 @@ -Rewrite logic around verifying JSON object and fetching server keys to be more performant and use less memory. diff --git a/changelog.d/10040.feature b/changelog.d/10040.feature deleted file mode 100644 index ec78a30f00..0000000000 --- a/changelog.d/10040.feature +++ /dev/null @@ -1 +0,0 @@ -Add an admin API for unprotecting local media from quarantine. Contributed by @dklimpel. diff --git a/changelog.d/10044.feature b/changelog.d/10044.feature deleted file mode 100644 index 70c0a3851e..0000000000 --- a/changelog.d/10044.feature +++ /dev/null @@ -1 +0,0 @@ -Add new admin APIs to remove media by media ID from quarantine. Contributed by @dkimpel. diff --git a/changelog.d/10046.doc b/changelog.d/10046.doc deleted file mode 100644 index 995960163b..0000000000 --- a/changelog.d/10046.doc +++ /dev/null @@ -1 +0,0 @@ -Update CAPTCHA documentation to mention turning off the verify origin feature. Contributed by @aaronraimist. diff --git a/changelog.d/10048.misc b/changelog.d/10048.misc deleted file mode 100644 index a901f8431e..0000000000 --- a/changelog.d/10048.misc +++ /dev/null @@ -1 +0,0 @@ -Add `parse_strings_from_args` for parsing an array from query parameters. diff --git a/changelog.d/10054.misc b/changelog.d/10054.misc deleted file mode 100644 index cebe39ce54..0000000000 --- a/changelog.d/10054.misc +++ /dev/null @@ -1 +0,0 @@ -Remove some dead code regarding TLS certificate handling. diff --git a/changelog.d/10055.misc b/changelog.d/10055.misc deleted file mode 100644 index da84a2dde8..0000000000 --- a/changelog.d/10055.misc +++ /dev/null @@ -1 +0,0 @@ -Remove redundant, unmaintained `convert_server_keys` script. diff --git a/changelog.d/10057.doc b/changelog.d/10057.doc deleted file mode 100644 index 35437cb017..0000000000 --- a/changelog.d/10057.doc +++ /dev/null @@ -1 +0,0 @@ -Tweak wording of database recommendation in `INSTALL.md`. Contributed by @aaronraimist. \ No newline at end of file diff --git a/changelog.d/10059.misc b/changelog.d/10059.misc deleted file mode 100644 index ca6e0e8a5a..0000000000 --- a/changelog.d/10059.misc +++ /dev/null @@ -1 +0,0 @@ -Improve the error message printed by synctl when synapse fails to start. diff --git a/changelog.d/10063.removal b/changelog.d/10063.removal deleted file mode 100644 index 0f8889b6b4..0000000000 --- a/changelog.d/10063.removal +++ /dev/null @@ -1 +0,0 @@ -Remove the experimental `spaces_enabled` flag. The spaces features are always available now. diff --git a/changelog.d/10069.misc b/changelog.d/10069.misc deleted file mode 100644 index a8d2629e9b..0000000000 --- a/changelog.d/10069.misc +++ /dev/null @@ -1 +0,0 @@ -Fix GitHub Actions lint for newsfragments. diff --git a/changelog.d/10074.misc b/changelog.d/10074.misc deleted file mode 100644 index 8dbe2cd2bc..0000000000 --- a/changelog.d/10074.misc +++ /dev/null @@ -1 +0,0 @@ -Update opentracing to inject the right context into the carrier. diff --git a/changelog.d/10077.feature b/changelog.d/10077.feature deleted file mode 100644 index 808feb2215..0000000000 --- a/changelog.d/10077.feature +++ /dev/null @@ -1 +0,0 @@ -Make reason and score parameters optional for reporting content. Implements [MSC2414](https://github.com/matrix-org/matrix-doc/pull/2414). Contributed by Callum Brown. diff --git a/changelog.d/10078.misc b/changelog.d/10078.misc deleted file mode 100644 index a4b089d0fd..0000000000 --- a/changelog.d/10078.misc +++ /dev/null @@ -1 +0,0 @@ -Fix up `BatchingQueue` implementation. diff --git a/changelog.d/10082.bugfix b/changelog.d/10082.bugfix deleted file mode 100644 index b4f8bcc4fa..0000000000 --- a/changelog.d/10082.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug causing replication requests to fail when receiving a lot of events via federation. diff --git a/changelog.d/10084.feature b/changelog.d/10084.feature deleted file mode 100644 index 602cb6ff51..0000000000 --- a/changelog.d/10084.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for routing more requests to workers. diff --git a/changelog.d/10086.doc b/changelog.d/10086.doc deleted file mode 100644 index 2200579012..0000000000 --- a/changelog.d/10086.doc +++ /dev/null @@ -1 +0,0 @@ -Add initial infrastructure for rendering Synapse documentation with mdbook. diff --git a/changelog.d/10089.doc b/changelog.d/10089.doc deleted file mode 100644 index d9e93773ab..0000000000 --- a/changelog.d/10089.doc +++ /dev/null @@ -1 +0,0 @@ -Convert the remaining Admin API documentation files to markdown. diff --git a/changelog.d/10091.misc b/changelog.d/10091.misc deleted file mode 100644 index dbe310fd17..0000000000 --- a/changelog.d/10091.misc +++ /dev/null @@ -1 +0,0 @@ -Log method and path when dropping request due to size limit. diff --git a/changelog.d/10092.bugfix b/changelog.d/10092.bugfix deleted file mode 100644 index 09b2aba7ff..0000000000 --- a/changelog.d/10092.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug in the `force_tracing_for_users` option introduced in Synapse v1.35 which meant that the OpenTracing spans produced were missing most tags. diff --git a/changelog.d/10094.misc b/changelog.d/10094.misc deleted file mode 100644 index 01efe14f74..0000000000 --- a/changelog.d/10094.misc +++ /dev/null @@ -1 +0,0 @@ -In Github Actions workflows, summarize the Sytest results in an easy-to-read format. diff --git a/changelog.d/10102.misc b/changelog.d/10102.misc deleted file mode 100644 index 87672ee295..0000000000 --- a/changelog.d/10102.misc +++ /dev/null @@ -1 +0,0 @@ -Make `/sync` do fewer state resolutions. diff --git a/changelog.d/10105.misc b/changelog.d/10105.misc deleted file mode 100644 index 244a893d3e..0000000000 --- a/changelog.d/10105.misc +++ /dev/null @@ -1 +0,0 @@ -Add missing type hints to the admin API servlets. \ No newline at end of file diff --git a/changelog.d/10107.bugfix b/changelog.d/10107.bugfix deleted file mode 100644 index 80030efab2..0000000000 --- a/changelog.d/10107.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug that could cause Synapse to stop notifying application services. Contributed by Willem Mulder. diff --git a/changelog.d/10111.misc b/changelog.d/10111.misc deleted file mode 100644 index 42e42b69ab..0000000000 --- a/changelog.d/10111.misc +++ /dev/null @@ -1 +0,0 @@ -Improve opentracing annotations for `Notifier`. diff --git a/changelog.d/10112.misc b/changelog.d/10112.misc deleted file mode 100644 index 40af09760c..0000000000 --- a/changelog.d/10112.misc +++ /dev/null @@ -1 +0,0 @@ -Enable Prometheus metrics for the jaeger client library. diff --git a/changelog.d/10113.feature b/changelog.d/10113.feature deleted file mode 100644 index 2658ab8918..0000000000 --- a/changelog.d/10113.feature +++ /dev/null @@ -1 +0,0 @@ -Report OpenTracing spans for database activity. diff --git a/changelog.d/10116.bugfix b/changelog.d/10116.bugfix deleted file mode 100644 index 90ef707559..0000000000 --- a/changelog.d/10116.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where the server would attempt to fetch the same history in the room from a remote server multiple times in parallel. diff --git a/changelog.d/10117.feature b/changelog.d/10117.feature deleted file mode 100644 index e137e142c6..0000000000 --- a/changelog.d/10117.feature +++ /dev/null @@ -1 +0,0 @@ -Significantly reduce memory usage of joining large remote rooms. diff --git a/changelog.d/10118.bugfix b/changelog.d/10118.bugfix deleted file mode 100644 index db62b50e0b..0000000000 --- a/changelog.d/10118.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse 1.33.0 which caused replication requests to fail when receiving a lot of very large events via federation. diff --git a/changelog.d/10124.misc b/changelog.d/10124.misc deleted file mode 100644 index c06593238d..0000000000 --- a/changelog.d/10124.misc +++ /dev/null @@ -1 +0,0 @@ -Work to improve the responsiveness of `/sync` requests. diff --git a/changelog.d/10130.doc b/changelog.d/10130.doc deleted file mode 100644 index 42ed1f3eac..0000000000 --- a/changelog.d/10130.doc +++ /dev/null @@ -1 +0,0 @@ -Make a link in docs use HTTPS. Contributed by @RhnSharma. diff --git a/changelog.d/10132.doc b/changelog.d/10132.doc deleted file mode 100644 index 70f538f077..0000000000 --- a/changelog.d/10132.doc +++ /dev/null @@ -1 +0,0 @@ -Fix broken link in Docker docs. diff --git a/changelog.d/10133.bugfix b/changelog.d/10133.bugfix deleted file mode 100644 index a62c15b260..0000000000 --- a/changelog.d/10133.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug when using workers where pagination requests failed if a remote server returned zero events from `/backfill`. Introduced in 1.35.0. diff --git a/changelog.d/10135.misc b/changelog.d/10135.misc deleted file mode 100644 index 17819cbbcc..0000000000 --- a/changelog.d/10135.misc +++ /dev/null @@ -1 +0,0 @@ -OpenTracing: use a consistent name for background processes. diff --git a/changelog.d/10136.feature b/changelog.d/10136.feature deleted file mode 100644 index 2658ab8918..0000000000 --- a/changelog.d/10136.feature +++ /dev/null @@ -1 +0,0 @@ -Report OpenTracing spans for database activity. diff --git a/changelog.d/10137.misc b/changelog.d/10137.misc deleted file mode 100644 index a901f8431e..0000000000 --- a/changelog.d/10137.misc +++ /dev/null @@ -1 +0,0 @@ -Add `parse_strings_from_args` for parsing an array from query parameters. diff --git a/changelog.d/10141.feature b/changelog.d/10141.feature deleted file mode 100644 index 2658ab8918..0000000000 --- a/changelog.d/10141.feature +++ /dev/null @@ -1 +0,0 @@ -Report OpenTracing spans for database activity. diff --git a/changelog.d/9221.doc b/changelog.d/9221.doc deleted file mode 100644 index 9b3476064b..0000000000 --- a/changelog.d/9221.doc +++ /dev/null @@ -1 +0,0 @@ -Clarify security note regarding hosting Synapse on the same domain as other web applications. diff --git a/changelog.d/9224.feature b/changelog.d/9224.feature deleted file mode 100644 index 76519c23e2..0000000000 --- a/changelog.d/9224.feature +++ /dev/null @@ -1 +0,0 @@ -Add new endpoint `/_matrix/client/r0/rooms/{roomId}/aliases` from Client-Server API r0.6.1 (previously [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432)). diff --git a/changelog.d/9906.misc b/changelog.d/9906.misc deleted file mode 100644 index 667d51a4c0..0000000000 --- a/changelog.d/9906.misc +++ /dev/null @@ -1 +0,0 @@ -Tell CircleCI to build Docker images from `main` branch. diff --git a/changelog.d/9953.feature b/changelog.d/9953.feature deleted file mode 100644 index 6b3d1adc70..0000000000 --- a/changelog.d/9953.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance of incoming federation transactions in large rooms. diff --git a/changelog.d/9973.feature b/changelog.d/9973.feature deleted file mode 100644 index 6b3d1adc70..0000000000 --- a/changelog.d/9973.feature +++ /dev/null @@ -1 +0,0 @@ -Improve performance of incoming federation transactions in large rooms. diff --git a/synapse/__init__.py b/synapse/__init__.py index 445e8a5cad..58261d04ef 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.35.1" +__version__ = "1.36.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 684df9b21d3e7d66c919970c705f28e45275f88f Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 8 Jun 2021 14:11:16 +0100 Subject: [PATCH 254/619] fix typo in changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 69c876e38e..f9aaecc6bd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Features - Improve performance of incoming federation transactions in large rooms. ([\#9953](https://github.com/matrix-org/synapse/issues/9953), [\#9973](https://github.com/matrix-org/synapse/issues/9973)) - Rewrite logic around verifying JSON object and fetching server keys to be more performant and use less memory. ([\#10035](https://github.com/matrix-org/synapse/issues/10035)) - Add an admin API for unprotecting local media from quarantine. Contributed by @dklimpel. ([\#10040](https://github.com/matrix-org/synapse/issues/10040)) -- Add new admin APIs to remove media by media ID from quarantine. Contributed by @dkimpel. ([\#10044](https://github.com/matrix-org/synapse/issues/10044)) +- Add new admin APIs to remove media by media ID from quarantine. Contributed by @dklimpel. ([\#10044](https://github.com/matrix-org/synapse/issues/10044)) - Make reason and score parameters optional for reporting content. Implements [MSC2414](https://github.com/matrix-org/matrix-doc/pull/2414). Contributed by Callum Brown. ([\#10077](https://github.com/matrix-org/synapse/issues/10077)) - Add support for routing more requests to workers. ([\#10084](https://github.com/matrix-org/synapse/issues/10084)) - Report OpenTracing spans for database activity. ([\#10113](https://github.com/matrix-org/synapse/issues/10113), [\#10136](https://github.com/matrix-org/synapse/issues/10136), [\#10141](https://github.com/matrix-org/synapse/issues/10141)) From e0ddd82f2ce58f9dd0038cb95047c316295b1b0d Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 8 Jun 2021 14:21:22 +0100 Subject: [PATCH 255/619] Make changelog lines consistent --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f9aaecc6bd..48e9b55c8a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,7 @@ Features - Add new endpoint `/_matrix/client/r0/rooms/{roomId}/aliases` from Client-Server API r0.6.1 (previously [MSC2432](https://github.com/matrix-org/matrix-doc/pull/2432)). ([\#9224](https://github.com/matrix-org/synapse/issues/9224)) - Improve performance of incoming federation transactions in large rooms. ([\#9953](https://github.com/matrix-org/synapse/issues/9953), [\#9973](https://github.com/matrix-org/synapse/issues/9973)) - Rewrite logic around verifying JSON object and fetching server keys to be more performant and use less memory. ([\#10035](https://github.com/matrix-org/synapse/issues/10035)) -- Add an admin API for unprotecting local media from quarantine. Contributed by @dklimpel. ([\#10040](https://github.com/matrix-org/synapse/issues/10040)) +- Add new admin APIs for unprotecting local media from quarantine. Contributed by @dklimpel. ([\#10040](https://github.com/matrix-org/synapse/issues/10040)) - Add new admin APIs to remove media by media ID from quarantine. Contributed by @dklimpel. ([\#10044](https://github.com/matrix-org/synapse/issues/10044)) - Make reason and score parameters optional for reporting content. Implements [MSC2414](https://github.com/matrix-org/matrix-doc/pull/2414). Contributed by Callum Brown. ([\#10077](https://github.com/matrix-org/synapse/issues/10077)) - Add support for routing more requests to workers. ([\#10084](https://github.com/matrix-org/synapse/issues/10084)) From c7f3fb27451038c0f4b80a557f27eae849d55de2 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 8 Jun 2021 11:19:25 -0400 Subject: [PATCH 256/619] Add type hints to the federation server transport. (#10080) --- changelog.d/10080.misc | 1 + synapse/federation/federation_server.py | 6 +- synapse/federation/transport/server.py | 232 +++++++++++++++++------- synapse/handlers/room_list.py | 6 +- synapse/http/servlet.py | 24 +++ 5 files changed, 194 insertions(+), 75 deletions(-) create mode 100644 changelog.d/10080.misc diff --git a/changelog.d/10080.misc b/changelog.d/10080.misc new file mode 100644 index 0000000000..9adb0fbd02 --- /dev/null +++ b/changelog.d/10080.misc @@ -0,0 +1 @@ +Add type hints to the federation servlets. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index ace30aa450..86562cd04f 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -129,7 +129,7 @@ def __init__(self, hs: "HomeServer"): # come in waves. self._state_resp_cache = ResponseCache( hs.get_clock(), "state_resp", timeout_ms=30000 - ) # type: ResponseCache[Tuple[str, str]] + ) # type: ResponseCache[Tuple[str, Optional[str]]] self._state_ids_resp_cache = ResponseCache( hs.get_clock(), "state_ids_resp", timeout_ms=30000 ) # type: ResponseCache[Tuple[str, str]] @@ -406,7 +406,7 @@ async def _process_edu(edu_dict): ) async def on_room_state_request( - self, origin: str, room_id: str, event_id: str + self, origin: str, room_id: str, event_id: Optional[str] ) -> Tuple[int, Dict[str, Any]]: origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) @@ -463,7 +463,7 @@ async def _on_state_ids_request_compute(self, room_id, event_id): return {"pdu_ids": state_ids, "auth_chain_ids": auth_chain_ids} async def _on_context_state_request_compute( - self, room_id: str, event_id: str + self, room_id: str, event_id: Optional[str] ) -> Dict[str, list]: if event_id: pdus = await self.handler.get_state_for_pdu( diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 5756fcb551..4bc7d2015b 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -28,6 +28,7 @@ FEDERATION_V1_PREFIX, FEDERATION_V2_PREFIX, ) +from synapse.handlers.groups_local import GroupsLocalHandler from synapse.http.server import HttpServer, JsonResource from synapse.http.servlet import ( parse_boolean_from_args, @@ -275,10 +276,17 @@ class BaseFederationServlet: RATELIMIT = True # Whether to rate limit requests or not - def __init__(self, handler, authenticator, ratelimiter, server_name): - self.handler = handler + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + self.hs = hs self.authenticator = authenticator self.ratelimiter = ratelimiter + self.server_name = server_name def _wrap(self, func): authenticator = self.authenticator @@ -375,17 +383,30 @@ def register(self, server): ) -class FederationSendServlet(BaseFederationServlet): +class BaseFederationServerServlet(BaseFederationServlet): + """Abstract base class for federation servlet classes which provides a federation server handler. + + See BaseFederationServlet for more information. + """ + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_federation_server() + + +class FederationSendServlet(BaseFederationServerServlet): PATH = "/send/(?P[^/]*)/?" # We ratelimit manually in the handler as we queue up the requests and we # don't want to fill up the ratelimiter with blocked requests. RATELIMIT = False - def __init__(self, handler, server_name, **kwargs): - super().__init__(handler, server_name=server_name, **kwargs) - self.server_name = server_name - # This is when someone is trying to send us a bunch of data. async def on_PUT(self, origin, content, query, transaction_id): """Called on PUT /send// @@ -434,7 +455,7 @@ async def on_PUT(self, origin, content, query, transaction_id): return code, response -class FederationEventServlet(BaseFederationServlet): +class FederationEventServlet(BaseFederationServerServlet): PATH = "/event/(?P[^/]*)/?" # This is when someone asks for a data item for a given server data_id pair. @@ -442,7 +463,7 @@ async def on_GET(self, origin, content, query, event_id): return await self.handler.on_pdu_request(origin, event_id) -class FederationStateV1Servlet(BaseFederationServlet): +class FederationStateV1Servlet(BaseFederationServerServlet): PATH = "/state/(?P[^/]*)/?" # This is when someone asks for all data for a given room. @@ -454,7 +475,7 @@ async def on_GET(self, origin, content, query, room_id): ) -class FederationStateIdsServlet(BaseFederationServlet): +class FederationStateIdsServlet(BaseFederationServerServlet): PATH = "/state_ids/(?P[^/]*)/?" async def on_GET(self, origin, content, query, room_id): @@ -465,7 +486,7 @@ async def on_GET(self, origin, content, query, room_id): ) -class FederationBackfillServlet(BaseFederationServlet): +class FederationBackfillServlet(BaseFederationServerServlet): PATH = "/backfill/(?P[^/]*)/?" async def on_GET(self, origin, content, query, room_id): @@ -478,7 +499,7 @@ async def on_GET(self, origin, content, query, room_id): return await self.handler.on_backfill_request(origin, room_id, versions, limit) -class FederationQueryServlet(BaseFederationServlet): +class FederationQueryServlet(BaseFederationServerServlet): PATH = "/query/(?P[^/]*)" # This is when we receive a server-server Query @@ -488,7 +509,7 @@ async def on_GET(self, origin, content, query, query_type): return await self.handler.on_query_request(query_type, args) -class FederationMakeJoinServlet(BaseFederationServlet): +class FederationMakeJoinServlet(BaseFederationServerServlet): PATH = "/make_join/(?P[^/]*)/(?P[^/]*)" async def on_GET(self, origin, _content, query, room_id, user_id): @@ -518,7 +539,7 @@ async def on_GET(self, origin, _content, query, room_id, user_id): return 200, content -class FederationMakeLeaveServlet(BaseFederationServlet): +class FederationMakeLeaveServlet(BaseFederationServerServlet): PATH = "/make_leave/(?P[^/]*)/(?P[^/]*)" async def on_GET(self, origin, content, query, room_id, user_id): @@ -526,7 +547,7 @@ async def on_GET(self, origin, content, query, room_id, user_id): return 200, content -class FederationV1SendLeaveServlet(BaseFederationServlet): +class FederationV1SendLeaveServlet(BaseFederationServerServlet): PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id, event_id): @@ -534,7 +555,7 @@ async def on_PUT(self, origin, content, query, room_id, event_id): return 200, (200, content) -class FederationV2SendLeaveServlet(BaseFederationServlet): +class FederationV2SendLeaveServlet(BaseFederationServerServlet): PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" PREFIX = FEDERATION_V2_PREFIX @@ -544,14 +565,14 @@ async def on_PUT(self, origin, content, query, room_id, event_id): return 200, content -class FederationEventAuthServlet(BaseFederationServlet): +class FederationEventAuthServlet(BaseFederationServerServlet): PATH = "/event_auth/(?P[^/]*)/(?P[^/]*)" async def on_GET(self, origin, content, query, room_id, event_id): return await self.handler.on_event_auth(origin, room_id, event_id) -class FederationV1SendJoinServlet(BaseFederationServlet): +class FederationV1SendJoinServlet(BaseFederationServerServlet): PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id, event_id): @@ -561,7 +582,7 @@ async def on_PUT(self, origin, content, query, room_id, event_id): return 200, (200, content) -class FederationV2SendJoinServlet(BaseFederationServlet): +class FederationV2SendJoinServlet(BaseFederationServerServlet): PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" PREFIX = FEDERATION_V2_PREFIX @@ -573,7 +594,7 @@ async def on_PUT(self, origin, content, query, room_id, event_id): return 200, content -class FederationV1InviteServlet(BaseFederationServlet): +class FederationV1InviteServlet(BaseFederationServerServlet): PATH = "/invite/(?P[^/]*)/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id, event_id): @@ -590,7 +611,7 @@ async def on_PUT(self, origin, content, query, room_id, event_id): return 200, (200, content) -class FederationV2InviteServlet(BaseFederationServlet): +class FederationV2InviteServlet(BaseFederationServerServlet): PATH = "/invite/(?P[^/]*)/(?P[^/]*)" PREFIX = FEDERATION_V2_PREFIX @@ -614,7 +635,7 @@ async def on_PUT(self, origin, content, query, room_id, event_id): return 200, content -class FederationThirdPartyInviteExchangeServlet(BaseFederationServlet): +class FederationThirdPartyInviteExchangeServlet(BaseFederationServerServlet): PATH = "/exchange_third_party_invite/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id): @@ -622,21 +643,21 @@ async def on_PUT(self, origin, content, query, room_id): return 200, {} -class FederationClientKeysQueryServlet(BaseFederationServlet): +class FederationClientKeysQueryServlet(BaseFederationServerServlet): PATH = "/user/keys/query" async def on_POST(self, origin, content, query): return await self.handler.on_query_client_keys(origin, content) -class FederationUserDevicesQueryServlet(BaseFederationServlet): +class FederationUserDevicesQueryServlet(BaseFederationServerServlet): PATH = "/user/devices/(?P[^/]*)" async def on_GET(self, origin, content, query, user_id): return await self.handler.on_query_user_devices(origin, user_id) -class FederationClientKeysClaimServlet(BaseFederationServlet): +class FederationClientKeysClaimServlet(BaseFederationServerServlet): PATH = "/user/keys/claim" async def on_POST(self, origin, content, query): @@ -644,7 +665,7 @@ async def on_POST(self, origin, content, query): return 200, response -class FederationGetMissingEventsServlet(BaseFederationServlet): +class FederationGetMissingEventsServlet(BaseFederationServerServlet): # TODO(paul): Why does this path alone end with "/?" optional? PATH = "/get_missing_events/(?P[^/]*)/?" @@ -664,7 +685,7 @@ async def on_POST(self, origin, content, query, room_id): return 200, content -class On3pidBindServlet(BaseFederationServlet): +class On3pidBindServlet(BaseFederationServerServlet): PATH = "/3pid/onbind" REQUIRE_AUTH = False @@ -694,7 +715,7 @@ async def on_POST(self, origin, content, query): return 200, {} -class OpenIdUserInfo(BaseFederationServlet): +class OpenIdUserInfo(BaseFederationServerServlet): """ Exchange a bearer token for information about a user. @@ -770,8 +791,16 @@ class PublicRoomList(BaseFederationServlet): PATH = "/publicRooms" - def __init__(self, handler, authenticator, ratelimiter, server_name, allow_access): - super().__init__(handler, authenticator, ratelimiter, server_name) + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + allow_access: bool, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_room_list_handler() self.allow_access = allow_access async def on_GET(self, origin, content, query): @@ -856,7 +885,24 @@ async def on_GET(self, origin, content, query): ) -class FederationGroupsProfileServlet(BaseFederationServlet): +class BaseGroupsServerServlet(BaseFederationServlet): + """Abstract base class for federation servlet classes which provides a groups server handler. + + See BaseFederationServlet for more information. + """ + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_groups_server_handler() + + +class FederationGroupsProfileServlet(BaseGroupsServerServlet): """Get/set the basic profile of a group on behalf of a user""" PATH = "/groups/(?P[^/]*)/profile" @@ -882,7 +928,7 @@ async def on_POST(self, origin, content, query, group_id): return 200, new_content -class FederationGroupsSummaryServlet(BaseFederationServlet): +class FederationGroupsSummaryServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/summary" async def on_GET(self, origin, content, query, group_id): @@ -895,7 +941,7 @@ async def on_GET(self, origin, content, query, group_id): return 200, new_content -class FederationGroupsRoomsServlet(BaseFederationServlet): +class FederationGroupsRoomsServlet(BaseGroupsServerServlet): """Get the rooms in a group on behalf of a user""" PATH = "/groups/(?P[^/]*)/rooms" @@ -910,7 +956,7 @@ async def on_GET(self, origin, content, query, group_id): return 200, new_content -class FederationGroupsAddRoomsServlet(BaseFederationServlet): +class FederationGroupsAddRoomsServlet(BaseGroupsServerServlet): """Add/remove room from group""" PATH = "/groups/(?P[^/]*)/room/(?P[^/]*)" @@ -938,7 +984,7 @@ async def on_DELETE(self, origin, content, query, group_id, room_id): return 200, new_content -class FederationGroupsAddRoomsConfigServlet(BaseFederationServlet): +class FederationGroupsAddRoomsConfigServlet(BaseGroupsServerServlet): """Update room config in group""" PATH = ( @@ -958,7 +1004,7 @@ async def on_POST(self, origin, content, query, group_id, room_id, config_key): return 200, result -class FederationGroupsUsersServlet(BaseFederationServlet): +class FederationGroupsUsersServlet(BaseGroupsServerServlet): """Get the users in a group on behalf of a user""" PATH = "/groups/(?P[^/]*)/users" @@ -973,7 +1019,7 @@ async def on_GET(self, origin, content, query, group_id): return 200, new_content -class FederationGroupsInvitedUsersServlet(BaseFederationServlet): +class FederationGroupsInvitedUsersServlet(BaseGroupsServerServlet): """Get the users that have been invited to a group""" PATH = "/groups/(?P[^/]*)/invited_users" @@ -990,7 +1036,7 @@ async def on_GET(self, origin, content, query, group_id): return 200, new_content -class FederationGroupsInviteServlet(BaseFederationServlet): +class FederationGroupsInviteServlet(BaseGroupsServerServlet): """Ask a group server to invite someone to the group""" PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/invite" @@ -1007,7 +1053,7 @@ async def on_POST(self, origin, content, query, group_id, user_id): return 200, new_content -class FederationGroupsAcceptInviteServlet(BaseFederationServlet): +class FederationGroupsAcceptInviteServlet(BaseGroupsServerServlet): """Accept an invitation from the group server""" PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/accept_invite" @@ -1021,7 +1067,7 @@ async def on_POST(self, origin, content, query, group_id, user_id): return 200, new_content -class FederationGroupsJoinServlet(BaseFederationServlet): +class FederationGroupsJoinServlet(BaseGroupsServerServlet): """Attempt to join a group""" PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/join" @@ -1035,7 +1081,7 @@ async def on_POST(self, origin, content, query, group_id, user_id): return 200, new_content -class FederationGroupsRemoveUserServlet(BaseFederationServlet): +class FederationGroupsRemoveUserServlet(BaseGroupsServerServlet): """Leave or kick a user from the group""" PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/remove" @@ -1052,7 +1098,24 @@ async def on_POST(self, origin, content, query, group_id, user_id): return 200, new_content -class FederationGroupsLocalInviteServlet(BaseFederationServlet): +class BaseGroupsLocalServlet(BaseFederationServlet): + """Abstract base class for federation servlet classes which provides a groups local handler. + + See BaseFederationServlet for more information. + """ + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_groups_local_handler() + + +class FederationGroupsLocalInviteServlet(BaseGroupsLocalServlet): """A group server has invited a local user""" PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/invite" @@ -1061,12 +1124,16 @@ async def on_POST(self, origin, content, query, group_id, user_id): if get_domain_from_id(group_id) != origin: raise SynapseError(403, "group_id doesn't match origin") + assert isinstance( + self.handler, GroupsLocalHandler + ), "Workers cannot handle group invites." + new_content = await self.handler.on_invite(group_id, user_id, content) return 200, new_content -class FederationGroupsRemoveLocalUserServlet(BaseFederationServlet): +class FederationGroupsRemoveLocalUserServlet(BaseGroupsLocalServlet): """A group server has removed a local user""" PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/remove" @@ -1075,6 +1142,10 @@ async def on_POST(self, origin, content, query, group_id, user_id): if get_domain_from_id(group_id) != origin: raise SynapseError(403, "user_id doesn't match origin") + assert isinstance( + self.handler, GroupsLocalHandler + ), "Workers cannot handle group removals." + new_content = await self.handler.user_removed_from_group( group_id, user_id, content ) @@ -1087,6 +1158,16 @@ class FederationGroupsRenewAttestaionServlet(BaseFederationServlet): PATH = "/groups/(?P[^/]*)/renew_attestation/(?P[^/]*)" + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_groups_attestation_renewer() + async def on_POST(self, origin, content, query, group_id, user_id): # We don't need to check auth here as we check the attestation signatures @@ -1097,7 +1178,7 @@ async def on_POST(self, origin, content, query, group_id, user_id): return 200, new_content -class FederationGroupsSummaryRoomsServlet(BaseFederationServlet): +class FederationGroupsSummaryRoomsServlet(BaseGroupsServerServlet): """Add/remove a room from the group summary, with optional category. Matches both: @@ -1154,7 +1235,7 @@ async def on_DELETE(self, origin, content, query, group_id, category_id, room_id return 200, resp -class FederationGroupsCategoriesServlet(BaseFederationServlet): +class FederationGroupsCategoriesServlet(BaseGroupsServerServlet): """Get all categories for a group""" PATH = "/groups/(?P[^/]*)/categories/?" @@ -1169,7 +1250,7 @@ async def on_GET(self, origin, content, query, group_id): return 200, resp -class FederationGroupsCategoryServlet(BaseFederationServlet): +class FederationGroupsCategoryServlet(BaseGroupsServerServlet): """Add/remove/get a category in a group""" PATH = "/groups/(?P[^/]*)/categories/(?P[^/]+)" @@ -1222,7 +1303,7 @@ async def on_DELETE(self, origin, content, query, group_id, category_id): return 200, resp -class FederationGroupsRolesServlet(BaseFederationServlet): +class FederationGroupsRolesServlet(BaseGroupsServerServlet): """Get roles in a group""" PATH = "/groups/(?P[^/]*)/roles/?" @@ -1237,7 +1318,7 @@ async def on_GET(self, origin, content, query, group_id): return 200, resp -class FederationGroupsRoleServlet(BaseFederationServlet): +class FederationGroupsRoleServlet(BaseGroupsServerServlet): """Add/remove/get a role in a group""" PATH = "/groups/(?P[^/]*)/roles/(?P[^/]+)" @@ -1290,7 +1371,7 @@ async def on_DELETE(self, origin, content, query, group_id, role_id): return 200, resp -class FederationGroupsSummaryUsersServlet(BaseFederationServlet): +class FederationGroupsSummaryUsersServlet(BaseGroupsServerServlet): """Add/remove a user from the group summary, with optional role. Matches both: @@ -1345,7 +1426,7 @@ async def on_DELETE(self, origin, content, query, group_id, role_id, user_id): return 200, resp -class FederationGroupsBulkPublicisedServlet(BaseFederationServlet): +class FederationGroupsBulkPublicisedServlet(BaseGroupsLocalServlet): """Get roles in a group""" PATH = "/get_groups_publicised" @@ -1358,7 +1439,7 @@ async def on_POST(self, origin, content, query): return 200, resp -class FederationGroupsSettingJoinPolicyServlet(BaseFederationServlet): +class FederationGroupsSettingJoinPolicyServlet(BaseGroupsServerServlet): """Sets whether a group is joinable without an invite or knock""" PATH = "/groups/(?P[^/]*)/settings/m.join_policy" @@ -1379,6 +1460,16 @@ class FederationSpaceSummaryServlet(BaseFederationServlet): PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" PATH = "/spaces/(?P[^/]*)" + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_space_summary_handler() + async def on_GET( self, origin: str, @@ -1444,16 +1535,25 @@ class RoomComplexityServlet(BaseFederationServlet): PATH = "/rooms/(?P[^/]*)/complexity" PREFIX = FEDERATION_UNSTABLE_PREFIX - async def on_GET(self, origin, content, query, room_id): - - store = self.handler.hs.get_datastore() + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self._store = self.hs.get_datastore() - is_public = await store.is_room_world_readable_or_publicly_joinable(room_id) + async def on_GET(self, origin, content, query, room_id): + is_public = await self._store.is_room_world_readable_or_publicly_joinable( + room_id + ) if not is_public: raise SynapseError(404, "Room not found", errcode=Codes.INVALID_PARAM) - complexity = await store.get_room_complexity(room_id) + complexity = await self._store.get_room_complexity(room_id) return 200, complexity @@ -1482,6 +1582,7 @@ async def on_GET(self, origin, content, query, room_id): On3pidBindServlet, FederationVersionServlet, RoomComplexityServlet, + FederationSpaceSummaryServlet, ) # type: Tuple[Type[BaseFederationServlet], ...] OPENID_SERVLET_CLASSES = ( @@ -1559,23 +1660,16 @@ def register_servlets( if "federation" in servlet_groups: for servletclass in FEDERATION_SERVLET_CLASSES: servletclass( - handler=hs.get_federation_server(), + hs=hs, authenticator=authenticator, ratelimiter=ratelimiter, server_name=hs.hostname, ).register(resource) - FederationSpaceSummaryServlet( - handler=hs.get_space_summary_handler(), - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) - if "openid" in servlet_groups: for servletclass in OPENID_SERVLET_CLASSES: servletclass( - handler=hs.get_federation_server(), + hs=hs, authenticator=authenticator, ratelimiter=ratelimiter, server_name=hs.hostname, @@ -1584,7 +1678,7 @@ def register_servlets( if "room_list" in servlet_groups: for servletclass in ROOM_LIST_CLASSES: servletclass( - handler=hs.get_room_list_handler(), + hs=hs, authenticator=authenticator, ratelimiter=ratelimiter, server_name=hs.hostname, @@ -1594,7 +1688,7 @@ def register_servlets( if "group_server" in servlet_groups: for servletclass in GROUP_SERVER_SERVLET_CLASSES: servletclass( - handler=hs.get_groups_server_handler(), + hs=hs, authenticator=authenticator, ratelimiter=ratelimiter, server_name=hs.hostname, @@ -1603,7 +1697,7 @@ def register_servlets( if "group_local" in servlet_groups: for servletclass in GROUP_LOCAL_SERVLET_CLASSES: servletclass( - handler=hs.get_groups_local_handler(), + hs=hs, authenticator=authenticator, ratelimiter=ratelimiter, server_name=hs.hostname, @@ -1612,7 +1706,7 @@ def register_servlets( if "group_attestation" in servlet_groups: for servletclass in GROUP_ATTESTATION_SERVLET_CLASSES: servletclass( - handler=hs.get_groups_attestation_renewer(), + hs=hs, authenticator=authenticator, ratelimiter=ratelimiter, server_name=hs.hostname, diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index 141c9c0444..0a26088d32 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -44,7 +44,7 @@ def __init__(self, hs: "HomeServer"): self.enable_room_list_search = hs.config.enable_room_list_search self.response_cache = ResponseCache( hs.get_clock(), "room_list" - ) # type: ResponseCache[Tuple[Optional[int], Optional[str], ThirdPartyInstanceID]] + ) # type: ResponseCache[Tuple[Optional[int], Optional[str], Optional[ThirdPartyInstanceID]]] self.remote_response_cache = ResponseCache( hs.get_clock(), "remote_room_list", timeout_ms=30 * 1000 ) # type: ResponseCache[Tuple[str, Optional[int], Optional[str], bool, Optional[str]]] @@ -54,7 +54,7 @@ async def get_local_public_room_list( limit: Optional[int] = None, since_token: Optional[str] = None, search_filter: Optional[dict] = None, - network_tuple: ThirdPartyInstanceID = EMPTY_THIRD_PARTY_ID, + network_tuple: Optional[ThirdPartyInstanceID] = EMPTY_THIRD_PARTY_ID, from_federation: bool = False, ) -> JsonDict: """Generate a local public room list. @@ -111,7 +111,7 @@ async def _get_public_room_list( limit: Optional[int] = None, since_token: Optional[str] = None, search_filter: Optional[dict] = None, - network_tuple: ThirdPartyInstanceID = EMPTY_THIRD_PARTY_ID, + network_tuple: Optional[ThirdPartyInstanceID] = EMPTY_THIRD_PARTY_ID, from_federation: bool = False, ) -> JsonDict: """Generate a public room list. diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index d61563d39b..72e2ec78db 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -295,6 +295,30 @@ def parse_strings_from_args( return default +@overload +def parse_string_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Optional[str] = None, + required: Literal[True] = True, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> str: + ... + + +@overload +def parse_string_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Optional[str] = None, + required: bool = False, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> Optional[str]: + ... + + def parse_string_from_args( args: Dict[bytes, List[bytes]], name: str, From 1bf83a191bc2b202db5c85eb972469cb27aefd09 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 9 Jun 2021 11:33:00 +0100 Subject: [PATCH 257/619] Clean up the interface for injecting opentracing over HTTP (#10143) * Remove unused helper functions * Clean up the interface for injecting opentracing over HTTP * changelog --- changelog.d/10143.misc | 1 + synapse/http/matrixfederationclient.py | 10 +-- synapse/logging/opentracing.py | 102 +++++-------------------- synapse/replication/http/_base.py | 5 +- 4 files changed, 26 insertions(+), 92 deletions(-) create mode 100644 changelog.d/10143.misc diff --git a/changelog.d/10143.misc b/changelog.d/10143.misc new file mode 100644 index 0000000000..37aa344db2 --- /dev/null +++ b/changelog.d/10143.misc @@ -0,0 +1 @@ +Clean up the interface for injecting opentracing over HTTP. diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 1998990a14..629373fc47 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -65,13 +65,9 @@ read_body_with_max_size, ) from synapse.http.federation.matrix_federation_agent import MatrixFederationAgent +from synapse.logging import opentracing from synapse.logging.context import make_deferred_yieldable -from synapse.logging.opentracing import ( - inject_active_span_byte_dict, - set_tag, - start_active_span, - tags, -) +from synapse.logging.opentracing import set_tag, start_active_span, tags from synapse.types import ISynapseReactor, JsonDict from synapse.util import json_decoder from synapse.util.async_helpers import timeout_deferred @@ -497,7 +493,7 @@ async def _send_request( # Inject the span into the headers headers_dict = {} # type: Dict[bytes, List[bytes]] - inject_active_span_byte_dict(headers_dict, request.destination) + opentracing.inject_header_dict(headers_dict, request.destination) headers_dict[b"User-Agent"] = [self.version_string_bytes] diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index dd9377340e..5b4725e035 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -168,7 +168,7 @@ def set_fates(clotho, lachesis, atropos, father="Zues", mother="Themis"): import logging import re from functools import wraps -from typing import TYPE_CHECKING, Dict, Optional, Pattern, Type +from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Type import attr @@ -574,59 +574,22 @@ def set_operation_name(operation_name): # Injection and extraction -@ensure_active_span("inject the span into a header") -def inject_active_span_twisted_headers(headers, destination, check_destination=True): +@ensure_active_span("inject the span into a header dict") +def inject_header_dict( + headers: Dict[bytes, List[bytes]], + destination: Optional[str] = None, + check_destination: bool = True, +) -> None: """ - Injects a span context into twisted headers in-place + Injects a span context into a dict of HTTP headers Args: - headers (twisted.web.http_headers.Headers) - destination (str): address of entity receiving the span context. If check_destination - is true the context will only be injected if the destination matches the - opentracing whitelist + headers: the dict to inject headers into + destination: address of entity receiving the span context. Must be given unless + check_destination is False. The context will only be injected if the + destination matches the opentracing whitelist check_destination (bool): If false, destination will be ignored and the context will always be injected. - span (opentracing.Span) - - Returns: - In-place modification of headers - - Note: - The headers set by the tracer are custom to the tracer implementation which - should be unique enough that they don't interfere with any headers set by - synapse or twisted. If we're still using jaeger these headers would be those - here: - https://github.com/jaegertracing/jaeger-client-python/blob/master/jaeger_client/constants.py - """ - - if check_destination and not whitelisted_homeserver(destination): - return - - span = opentracing.tracer.active_span - carrier = {} # type: Dict[str, str] - opentracing.tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, carrier) - - for key, value in carrier.items(): - headers.addRawHeaders(key, value) - - -@ensure_active_span("inject the span into a byte dict") -def inject_active_span_byte_dict(headers, destination, check_destination=True): - """ - Injects a span context into a dict where the headers are encoded as byte - strings - - Args: - headers (dict) - destination (str): address of entity receiving the span context. If check_destination - is true the context will only be injected if the destination matches the - opentracing whitelist - check_destination (bool): If false, destination will be ignored and the context - will always be injected. - span (opentracing.Span) - - Returns: - In-place modification of headers Note: The headers set by the tracer are custom to the tracer implementation which @@ -635,8 +598,13 @@ def inject_active_span_byte_dict(headers, destination, check_destination=True): here: https://github.com/jaegertracing/jaeger-client-python/blob/master/jaeger_client/constants.py """ - if check_destination and not whitelisted_homeserver(destination): - return + if check_destination: + if destination is None: + raise ValueError( + "destination must be given unless check_destination is False" + ) + if not whitelisted_homeserver(destination): + return span = opentracing.tracer.active_span @@ -647,38 +615,6 @@ def inject_active_span_byte_dict(headers, destination, check_destination=True): headers[key.encode()] = [value.encode()] -@ensure_active_span("inject the span into a text map") -def inject_active_span_text_map(carrier, destination, check_destination=True): - """ - Injects a span context into a dict - - Args: - carrier (dict) - destination (str): address of entity receiving the span context. If check_destination - is true the context will only be injected if the destination matches the - opentracing whitelist - check_destination (bool): If false, destination will be ignored and the context - will always be injected. - - Returns: - In-place modification of carrier - - Note: - The headers set by the tracer are custom to the tracer implementation which - should be unique enough that they don't interfere with any headers set by - synapse or twisted. If we're still using jaeger these headers would be those - here: - https://github.com/jaegertracing/jaeger-client-python/blob/master/jaeger_client/constants.py - """ - - if check_destination and not whitelisted_homeserver(destination): - return - - opentracing.tracer.inject( - opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier - ) - - @ensure_active_span("get the active span context as a dict", ret={}) def get_active_span_text_map(destination=None): """ diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index 5685cf2121..2a13026e9a 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -23,7 +23,8 @@ from synapse.api.errors import HttpResponseException, SynapseError from synapse.http import RequestTimedOutError -from synapse.logging.opentracing import inject_active_span_byte_dict, trace +from synapse.logging import opentracing +from synapse.logging.opentracing import trace from synapse.util.caches.response_cache import ResponseCache from synapse.util.stringutils import random_string @@ -235,7 +236,7 @@ async def send_request(*, instance_name="master", **kwargs): # Add an authorization header, if configured. if replication_secret: headers[b"Authorization"] = [b"Bearer " + replication_secret] - inject_active_span_byte_dict(headers, None, check_destination=False) + opentracing.inject_header_dict(headers, check_destination=False) try: result = await request_func(uri, data, headers=headers) break From 11846dff8c667cbe6861ddc821ca7c53e3e2d890 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 9 Jun 2021 07:05:32 -0400 Subject: [PATCH 258/619] Limit the number of in-flight /keys/query requests from a single device. (#10144) --- changelog.d/10144.misc | 1 + synapse/handlers/e2e_keys.py | 350 ++++++++++++++------------- synapse/rest/client/v2_alpha/keys.py | 5 +- tests/handlers/test_e2e_keys.py | 13 +- 4 files changed, 196 insertions(+), 173 deletions(-) create mode 100644 changelog.d/10144.misc diff --git a/changelog.d/10144.misc b/changelog.d/10144.misc new file mode 100644 index 0000000000..fe96d645d7 --- /dev/null +++ b/changelog.d/10144.misc @@ -0,0 +1 @@ +Limit the number of in-flight `/keys/query` requests from a single device. diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 974487800d..3972849d4d 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -79,9 +79,15 @@ def __init__(self, hs: "HomeServer"): "client_keys", self.on_federation_query_client_keys ) + # Limit the number of in-flight requests from a single device. + self._query_devices_linearizer = Linearizer( + name="query_devices", + max_count=10, + ) + @trace async def query_devices( - self, query_body: JsonDict, timeout: int, from_user_id: str + self, query_body: JsonDict, timeout: int, from_user_id: str, from_device_id: str ) -> JsonDict: """Handle a device key query from a client @@ -105,191 +111,197 @@ async def query_devices( from_user_id: the user making the query. This is used when adding cross-signing signatures to limit what signatures users can see. + from_device_id: the device making the query. This is used to limit + the number of in-flight queries at a time. """ - - device_keys_query = query_body.get( - "device_keys", {} - ) # type: Dict[str, Iterable[str]] - - # separate users by domain. - # make a map from domain to user_id to device_ids - local_query = {} - remote_queries = {} - - for user_id, device_ids in device_keys_query.items(): - # we use UserID.from_string to catch invalid user ids - if self.is_mine(UserID.from_string(user_id)): - local_query[user_id] = device_ids - else: - remote_queries[user_id] = device_ids - - set_tag("local_key_query", local_query) - set_tag("remote_key_query", remote_queries) - - # First get local devices. - # A map of destination -> failure response. - failures = {} # type: Dict[str, JsonDict] - results = {} - if local_query: - local_result = await self.query_local_devices(local_query) - for user_id, keys in local_result.items(): - if user_id in local_query: - results[user_id] = keys - - # Get cached cross-signing keys - cross_signing_keys = await self.get_cross_signing_keys_from_cache( - device_keys_query, from_user_id - ) - - # Now attempt to get any remote devices from our local cache. - # A map of destination -> user ID -> device IDs. - remote_queries_not_in_cache = {} # type: Dict[str, Dict[str, Iterable[str]]] - if remote_queries: - query_list = [] # type: List[Tuple[str, Optional[str]]] - for user_id, device_ids in remote_queries.items(): - if device_ids: - query_list.extend((user_id, device_id) for device_id in device_ids) + with await self._query_devices_linearizer.queue((from_user_id, from_device_id)): + device_keys_query = query_body.get( + "device_keys", {} + ) # type: Dict[str, Iterable[str]] + + # separate users by domain. + # make a map from domain to user_id to device_ids + local_query = {} + remote_queries = {} + + for user_id, device_ids in device_keys_query.items(): + # we use UserID.from_string to catch invalid user ids + if self.is_mine(UserID.from_string(user_id)): + local_query[user_id] = device_ids else: - query_list.append((user_id, None)) - - ( - user_ids_not_in_cache, - remote_results, - ) = await self.store.get_user_devices_from_cache(query_list) - for user_id, devices in remote_results.items(): - user_devices = results.setdefault(user_id, {}) - for device_id, device in devices.items(): - keys = device.get("keys", None) - device_display_name = device.get("device_display_name", None) - if keys: - result = dict(keys) - unsigned = result.setdefault("unsigned", {}) - if device_display_name: - unsigned["device_display_name"] = device_display_name - user_devices[device_id] = result - - # check for missing cross-signing keys. - for user_id in remote_queries.keys(): - cached_cross_master = user_id in cross_signing_keys["master_keys"] - cached_cross_selfsigning = ( - user_id in cross_signing_keys["self_signing_keys"] - ) - - # check if we are missing only one of cross-signing master or - # self-signing key, but the other one is cached. - # as we need both, this will issue a federation request. - # if we don't have any of the keys, either the user doesn't have - # cross-signing set up, or the cached device list - # is not (yet) updated. - if cached_cross_master ^ cached_cross_selfsigning: - user_ids_not_in_cache.add(user_id) - - # add those users to the list to fetch over federation. - for user_id in user_ids_not_in_cache: - domain = get_domain_from_id(user_id) - r = remote_queries_not_in_cache.setdefault(domain, {}) - r[user_id] = remote_queries[user_id] - - # Now fetch any devices that we don't have in our cache - @trace - async def do_remote_query(destination): - """This is called when we are querying the device list of a user on - a remote homeserver and their device list is not in the device list - cache. If we share a room with this user and we're not querying for - specific user we will update the cache with their device list. - """ - - destination_query = remote_queries_not_in_cache[destination] - - # We first consider whether we wish to update the device list cache with - # the users device list. We want to track a user's devices when the - # authenticated user shares a room with the queried user and the query - # has not specified a particular device. - # If we update the cache for the queried user we remove them from further - # queries. We use the more efficient batched query_client_keys for all - # remaining users - user_ids_updated = [] - for (user_id, device_list) in destination_query.items(): - if user_id in user_ids_updated: - continue - - if device_list: - continue + remote_queries[user_id] = device_ids + + set_tag("local_key_query", local_query) + set_tag("remote_key_query", remote_queries) + + # First get local devices. + # A map of destination -> failure response. + failures = {} # type: Dict[str, JsonDict] + results = {} + if local_query: + local_result = await self.query_local_devices(local_query) + for user_id, keys in local_result.items(): + if user_id in local_query: + results[user_id] = keys - room_ids = await self.store.get_rooms_for_user(user_id) - if not room_ids: - continue + # Get cached cross-signing keys + cross_signing_keys = await self.get_cross_signing_keys_from_cache( + device_keys_query, from_user_id + ) - # We've decided we're sharing a room with this user and should - # probably be tracking their device lists. However, we haven't - # done an initial sync on the device list so we do it now. - try: - if self._is_master: - user_devices = await self.device_handler.device_list_updater.user_device_resync( - user_id + # Now attempt to get any remote devices from our local cache. + # A map of destination -> user ID -> device IDs. + remote_queries_not_in_cache = ( + {} + ) # type: Dict[str, Dict[str, Iterable[str]]] + if remote_queries: + query_list = [] # type: List[Tuple[str, Optional[str]]] + for user_id, device_ids in remote_queries.items(): + if device_ids: + query_list.extend( + (user_id, device_id) for device_id in device_ids ) else: - user_devices = await self._user_device_resync_client( - user_id=user_id - ) - - user_devices = user_devices["devices"] - user_results = results.setdefault(user_id, {}) - for device in user_devices: - user_results[device["device_id"]] = device["keys"] - user_ids_updated.append(user_id) - except Exception as e: - failures[destination] = _exception_to_failure(e) - - if len(destination_query) == len(user_ids_updated): - # We've updated all the users in the query and we do not need to - # make any further remote calls. - return + query_list.append((user_id, None)) - # Remove all the users from the query which we have updated - for user_id in user_ids_updated: - destination_query.pop(user_id) + ( + user_ids_not_in_cache, + remote_results, + ) = await self.store.get_user_devices_from_cache(query_list) + for user_id, devices in remote_results.items(): + user_devices = results.setdefault(user_id, {}) + for device_id, device in devices.items(): + keys = device.get("keys", None) + device_display_name = device.get("device_display_name", None) + if keys: + result = dict(keys) + unsigned = result.setdefault("unsigned", {}) + if device_display_name: + unsigned["device_display_name"] = device_display_name + user_devices[device_id] = result + + # check for missing cross-signing keys. + for user_id in remote_queries.keys(): + cached_cross_master = user_id in cross_signing_keys["master_keys"] + cached_cross_selfsigning = ( + user_id in cross_signing_keys["self_signing_keys"] + ) - try: - remote_result = await self.federation.query_client_keys( - destination, {"device_keys": destination_query}, timeout=timeout - ) + # check if we are missing only one of cross-signing master or + # self-signing key, but the other one is cached. + # as we need both, this will issue a federation request. + # if we don't have any of the keys, either the user doesn't have + # cross-signing set up, or the cached device list + # is not (yet) updated. + if cached_cross_master ^ cached_cross_selfsigning: + user_ids_not_in_cache.add(user_id) + + # add those users to the list to fetch over federation. + for user_id in user_ids_not_in_cache: + domain = get_domain_from_id(user_id) + r = remote_queries_not_in_cache.setdefault(domain, {}) + r[user_id] = remote_queries[user_id] + + # Now fetch any devices that we don't have in our cache + @trace + async def do_remote_query(destination): + """This is called when we are querying the device list of a user on + a remote homeserver and their device list is not in the device list + cache. If we share a room with this user and we're not querying for + specific user we will update the cache with their device list. + """ + + destination_query = remote_queries_not_in_cache[destination] + + # We first consider whether we wish to update the device list cache with + # the users device list. We want to track a user's devices when the + # authenticated user shares a room with the queried user and the query + # has not specified a particular device. + # If we update the cache for the queried user we remove them from further + # queries. We use the more efficient batched query_client_keys for all + # remaining users + user_ids_updated = [] + for (user_id, device_list) in destination_query.items(): + if user_id in user_ids_updated: + continue + + if device_list: + continue + + room_ids = await self.store.get_rooms_for_user(user_id) + if not room_ids: + continue + + # We've decided we're sharing a room with this user and should + # probably be tracking their device lists. However, we haven't + # done an initial sync on the device list so we do it now. + try: + if self._is_master: + user_devices = await self.device_handler.device_list_updater.user_device_resync( + user_id + ) + else: + user_devices = await self._user_device_resync_client( + user_id=user_id + ) + + user_devices = user_devices["devices"] + user_results = results.setdefault(user_id, {}) + for device in user_devices: + user_results[device["device_id"]] = device["keys"] + user_ids_updated.append(user_id) + except Exception as e: + failures[destination] = _exception_to_failure(e) + + if len(destination_query) == len(user_ids_updated): + # We've updated all the users in the query and we do not need to + # make any further remote calls. + return + + # Remove all the users from the query which we have updated + for user_id in user_ids_updated: + destination_query.pop(user_id) - for user_id, keys in remote_result["device_keys"].items(): - if user_id in destination_query: - results[user_id] = keys + try: + remote_result = await self.federation.query_client_keys( + destination, {"device_keys": destination_query}, timeout=timeout + ) - if "master_keys" in remote_result: - for user_id, key in remote_result["master_keys"].items(): + for user_id, keys in remote_result["device_keys"].items(): if user_id in destination_query: - cross_signing_keys["master_keys"][user_id] = key + results[user_id] = keys - if "self_signing_keys" in remote_result: - for user_id, key in remote_result["self_signing_keys"].items(): - if user_id in destination_query: - cross_signing_keys["self_signing_keys"][user_id] = key + if "master_keys" in remote_result: + for user_id, key in remote_result["master_keys"].items(): + if user_id in destination_query: + cross_signing_keys["master_keys"][user_id] = key - except Exception as e: - failure = _exception_to_failure(e) - failures[destination] = failure - set_tag("error", True) - set_tag("reason", failure) + if "self_signing_keys" in remote_result: + for user_id, key in remote_result["self_signing_keys"].items(): + if user_id in destination_query: + cross_signing_keys["self_signing_keys"][user_id] = key - await make_deferred_yieldable( - defer.gatherResults( - [ - run_in_background(do_remote_query, destination) - for destination in remote_queries_not_in_cache - ], - consumeErrors=True, - ).addErrback(unwrapFirstError) - ) + except Exception as e: + failure = _exception_to_failure(e) + failures[destination] = failure + set_tag("error", True) + set_tag("reason", failure) + + await make_deferred_yieldable( + defer.gatherResults( + [ + run_in_background(do_remote_query, destination) + for destination in remote_queries_not_in_cache + ], + consumeErrors=True, + ).addErrback(unwrapFirstError) + ) - ret = {"device_keys": results, "failures": failures} + ret = {"device_keys": results, "failures": failures} - ret.update(cross_signing_keys) + ret.update(cross_signing_keys) - return ret + return ret async def get_cross_signing_keys_from_cache( self, query: Iterable[str], from_user_id: Optional[str] diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index a57ccbb5e5..4a28f2c072 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -160,9 +160,12 @@ def __init__(self, hs): async def on_POST(self, request): requester = await self.auth.get_user_by_req(request, allow_guest=True) user_id = requester.user.to_string() + device_id = requester.device_id timeout = parse_integer(request, "timeout", 10 * 1000) body = parse_json_object_from_request(request) - result = await self.e2e_keys_handler.query_devices(body, timeout, user_id) + result = await self.e2e_keys_handler.query_devices( + body, timeout, user_id, device_id + ) return 200, result diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index 61a00130b8..e0a24824cc 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -257,7 +257,9 @@ def test_replace_master_key(self): self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys2)) devices = self.get_success( - self.handler.query_devices({"device_keys": {local_user: []}}, 0, local_user) + self.handler.query_devices( + {"device_keys": {local_user: []}}, 0, local_user, "device123" + ) ) self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]}) @@ -357,7 +359,9 @@ def test_reupload_signatures(self): device_key_1["signatures"][local_user]["ed25519:abc"] = "base64+signature" device_key_2["signatures"][local_user]["ed25519:def"] = "base64+signature" devices = self.get_success( - self.handler.query_devices({"device_keys": {local_user: []}}, 0, local_user) + self.handler.query_devices( + {"device_keys": {local_user: []}}, 0, local_user, "device123" + ) ) del devices["device_keys"][local_user]["abc"]["unsigned"] del devices["device_keys"][local_user]["def"]["unsigned"] @@ -591,7 +595,10 @@ def test_upload_signatures(self): # fetch the signed keys/devices and make sure that the signatures are there ret = self.get_success( self.handler.query_devices( - {"device_keys": {local_user: [], other_user: []}}, 0, local_user + {"device_keys": {local_user: [], other_user: []}}, + 0, + local_user, + "device123", ) ) From d936371b698ea3085472ee83ae9a88ea7832280e Mon Sep 17 00:00:00 2001 From: Sorunome Date: Wed, 9 Jun 2021 20:39:51 +0200 Subject: [PATCH 259/619] Implement knock feature (#6739) This PR aims to implement the knock feature as proposed in https://github.com/matrix-org/matrix-doc/pull/2403 Signed-off-by: Sorunome mail@sorunome.de Signed-off-by: Andrew Morgan andrewm@element.io --- changelog.d/6739.feature | 1 + synapse/api/constants.py | 4 +- synapse/api/errors.py | 2 +- synapse/api/room_versions.py | 27 +- synapse/appservice/api.py | 11 +- synapse/config/account_validity.py | 1 - synapse/config/experimental.py | 7 + synapse/event_auth.py | 33 +- synapse/events/utils.py | 19 +- synapse/federation/federation_client.py | 72 ++++- synapse/federation/federation_server.py | 99 ++++++ synapse/federation/transport/client.py | 62 +++- synapse/federation/transport/server.py | 52 ++- synapse/handlers/federation.py | 186 ++++++++++- synapse/handlers/message.py | 30 +- synapse/handlers/room_member.py | 197 ++++++++++-- synapse/handlers/room_member_worker.py | 55 +++- synapse/handlers/stats.py | 7 +- synapse/handlers/sync.py | 87 +++-- synapse/http/servlet.py | 1 - synapse/replication/http/membership.py | 139 ++++++++ synapse/rest/__init__.py | 5 + synapse/rest/client/v1/room.py | 28 +- synapse/rest/client/v2_alpha/knock.py | 109 +++++++ synapse/rest/client/v2_alpha/sync.py | 82 ++++- synapse/storage/databases/main/stats.py | 1 + .../delta/59/11add_knock_members_to_stats.sql | 17 + tests/federation/transport/test_knocking.py | 302 ++++++++++++++++++ tests/rest/client/v2_alpha/test_sync.py | 95 +++++- 29 files changed, 1613 insertions(+), 118 deletions(-) create mode 100644 changelog.d/6739.feature create mode 100644 synapse/rest/client/v2_alpha/knock.py create mode 100644 synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql create mode 100644 tests/federation/transport/test_knocking.py diff --git a/changelog.d/6739.feature b/changelog.d/6739.feature new file mode 100644 index 0000000000..9c41140194 --- /dev/null +++ b/changelog.d/6739.feature @@ -0,0 +1 @@ +Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 3940da5c88..8d5b2177d2 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -41,7 +41,7 @@ class Membership: INVITE = "invite" JOIN = "join" - KNOCK = "knock" + KNOCK = "xyz.amorgan.knock" LEAVE = "leave" BAN = "ban" LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN) @@ -58,7 +58,7 @@ class PresenceState: class JoinRules: PUBLIC = "public" - KNOCK = "knock" + KNOCK = "xyz.amorgan.knock" INVITE = "invite" PRIVATE = "private" # As defined for MSC3083. diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 0231c79079..4cb8bbaf70 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -449,7 +449,7 @@ def __init__(self, room_version: str): super().__init__( code=400, msg="Your homeserver does not support the features required to " - "join this room", + "interact with this room", errcode=Codes.INCOMPATIBLE_ROOM_VERSION, ) diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 373a4669d0..3349f399ba 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -56,7 +56,7 @@ class RoomVersion: state_res = attr.ib(type=int) # one of the StateResolutionVersions enforce_key_validity = attr.ib(type=bool) - # Before MSC2261/MSC2432, m.room.aliases had special auth rules and redaction rules + # Before MSC2432, m.room.aliases had special auth rules and redaction rules special_case_aliases_auth = attr.ib(type=bool) # Strictly enforce canonicaljson, do not allow: # * Integers outside the range of [-2 ^ 53 + 1, 2 ^ 53 - 1] @@ -70,6 +70,9 @@ class RoomVersion: msc2176_redaction_rules = attr.ib(type=bool) # MSC3083: Support the 'restricted' join_rule. msc3083_join_rules = attr.ib(type=bool) + # MSC2403: Allows join_rules to be set to 'knock', changes auth rules to allow sending + # m.room.membership event with membership 'knock'. + msc2403_knocking = attr.ib(type=bool) class RoomVersions: @@ -84,6 +87,7 @@ class RoomVersions: limit_notifications_power_levels=False, msc2176_redaction_rules=False, msc3083_join_rules=False, + msc2403_knocking=False, ) V2 = RoomVersion( "2", @@ -96,6 +100,7 @@ class RoomVersions: limit_notifications_power_levels=False, msc2176_redaction_rules=False, msc3083_join_rules=False, + msc2403_knocking=False, ) V3 = RoomVersion( "3", @@ -108,6 +113,7 @@ class RoomVersions: limit_notifications_power_levels=False, msc2176_redaction_rules=False, msc3083_join_rules=False, + msc2403_knocking=False, ) V4 = RoomVersion( "4", @@ -120,6 +126,7 @@ class RoomVersions: limit_notifications_power_levels=False, msc2176_redaction_rules=False, msc3083_join_rules=False, + msc2403_knocking=False, ) V5 = RoomVersion( "5", @@ -132,6 +139,7 @@ class RoomVersions: limit_notifications_power_levels=False, msc2176_redaction_rules=False, msc3083_join_rules=False, + msc2403_knocking=False, ) V6 = RoomVersion( "6", @@ -144,6 +152,7 @@ class RoomVersions: limit_notifications_power_levels=True, msc2176_redaction_rules=False, msc3083_join_rules=False, + msc2403_knocking=False, ) MSC2176 = RoomVersion( "org.matrix.msc2176", @@ -156,6 +165,7 @@ class RoomVersions: limit_notifications_power_levels=True, msc2176_redaction_rules=True, msc3083_join_rules=False, + msc2403_knocking=False, ) MSC3083 = RoomVersion( "org.matrix.msc3083", @@ -168,6 +178,20 @@ class RoomVersions: limit_notifications_power_levels=True, msc2176_redaction_rules=False, msc3083_join_rules=True, + msc2403_knocking=False, + ) + MSC2403 = RoomVersion( + "xyz.amorgan.knock", + RoomDisposition.UNSTABLE, + EventFormatVersions.V3, + StateResolutionVersions.V2, + enforce_key_validity=True, + special_case_aliases_auth=False, + strict_canonicaljson=True, + limit_notifications_power_levels=True, + msc2176_redaction_rules=False, + msc3083_join_rules=False, + msc2403_knocking=True, ) @@ -183,4 +207,5 @@ class RoomVersions: RoomVersions.MSC2176, RoomVersions.MSC3083, ) + # Note that we do not include MSC2043 here unless it is enabled in the config. } # type: Dict[str, RoomVersion] diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index fe04d7a672..61152b2c46 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -17,7 +17,7 @@ from prometheus_client import Counter -from synapse.api.constants import EventTypes, ThirdPartyEntityKind +from synapse.api.constants import EventTypes, Membership, ThirdPartyEntityKind from synapse.api.errors import CodeMessageException from synapse.events import EventBase from synapse.events.utils import serialize_event @@ -247,9 +247,14 @@ def _serialize(self, service, events): e, time_now, as_client_event=True, - is_invite=( + # If this is an invite or a knock membership event, and we're interested + # in this user, then include any stripped state alongside the event. + include_stripped_room_state=( e.type == EventTypes.Member - and e.membership == "invite" + and ( + e.membership == Membership.INVITE + or e.membership == Membership.KNOCK + ) and service.is_interested_in_user(e.state_key) ), ) diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index c58a7d95a7..957de7f3a6 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 6ebce4b2f7..37668079e7 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.config._base import Config from synapse.types import JsonDict @@ -29,3 +30,9 @@ def read_config(self, config: JsonDict, **kwargs): # MSC3026 (busy presence state) self.msc3026_enabled = experimental.get("msc3026_enabled", False) # type: bool + + # MSC2403 (room knocking) + self.msc2403_enabled = experimental.get("msc2403_enabled", False) # type: bool + if self.msc2403_enabled: + # Enable the MSC2403 unstable room version + KNOWN_ROOM_VERSIONS[RoomVersions.MSC2403.identifier] = RoomVersions.MSC2403 diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 70c556566e..33d7c60241 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -160,6 +160,7 @@ def check( if logger.isEnabledFor(logging.DEBUG): logger.debug("Auth events: %s", [a.event_id for a in auth_events.values()]) + # 5. If type is m.room.membership if event.type == EventTypes.Member: _is_membership_change_allowed(room_version_obj, event, auth_events) logger.debug("Allowing! %s", event) @@ -257,6 +258,11 @@ def _is_membership_change_allowed( caller_in_room = caller and caller.membership == Membership.JOIN caller_invited = caller and caller.membership == Membership.INVITE + caller_knocked = ( + caller + and room_version.msc2403_knocking + and caller.membership == Membership.KNOCK + ) # get info about the target key = (EventTypes.Member, target_user_id) @@ -283,6 +289,7 @@ def _is_membership_change_allowed( { "caller_in_room": caller_in_room, "caller_invited": caller_invited, + "caller_knocked": caller_knocked, "target_banned": target_banned, "target_in_room": target_in_room, "membership": membership, @@ -299,9 +306,14 @@ def _is_membership_change_allowed( raise AuthError(403, "%s is banned from the room" % (target_user_id,)) return - if Membership.JOIN != membership: + # Require the user to be in the room for membership changes other than join/knock. + if Membership.JOIN != membership and ( + RoomVersion.msc2403_knocking and Membership.KNOCK != membership + ): + # If the user has been invited or has knocked, they are allowed to change their + # membership event to leave if ( - caller_invited + (caller_invited or caller_knocked) and Membership.LEAVE == membership and target_user_id == event.user_id ): @@ -339,7 +351,9 @@ def _is_membership_change_allowed( and join_rule == JoinRules.MSC3083_RESTRICTED ): pass - elif join_rule == JoinRules.INVITE: + elif join_rule == JoinRules.INVITE or ( + room_version.msc2403_knocking and join_rule == JoinRules.KNOCK + ): if not caller_in_room and not caller_invited: raise AuthError(403, "You are not invited to this room.") else: @@ -358,6 +372,17 @@ def _is_membership_change_allowed( elif Membership.BAN == membership: if user_level < ban_level or user_level <= target_level: raise AuthError(403, "You don't have permission to ban") + elif room_version.msc2403_knocking and Membership.KNOCK == membership: + if join_rule != JoinRules.KNOCK: + raise AuthError(403, "You don't have permission to knock") + elif target_user_id != event.user_id: + raise AuthError(403, "You cannot knock for other users") + elif target_in_room: + raise AuthError(403, "You cannot knock on a room you are already in") + elif caller_invited: + raise AuthError(403, "You are already invited to this room") + elif target_banned: + raise AuthError(403, "You are banned from this room") else: raise AuthError(500, "Unknown membership %s" % membership) @@ -718,7 +743,7 @@ def auth_types_for_event(event: EventBase) -> Set[Tuple[str, str]]: if event.type == EventTypes.Member: membership = event.content["membership"] - if membership in [Membership.JOIN, Membership.INVITE]: + if membership in [Membership.JOIN, Membership.INVITE, Membership.KNOCK]: auth_types.add((EventTypes.JoinRules, "")) auth_types.add((EventTypes.Member, event.state_key)) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index 7d7cd9aaee..ec96999e4e 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -242,6 +242,7 @@ def format_event_for_client_v1(d): "replaces_state", "prev_content", "invite_room_state", + "knock_room_state", ) for key in copy_keys: if key in d["unsigned"]: @@ -278,7 +279,7 @@ def serialize_event( event_format=format_event_for_client_v1, token_id=None, only_event_fields=None, - is_invite=False, + include_stripped_room_state=False, ): """Serialize event for clients @@ -289,8 +290,10 @@ def serialize_event( event_format token_id only_event_fields - is_invite (bool): Whether this is an invite that is being sent to the - invitee + include_stripped_room_state (bool): Some events can have stripped room state + stored in the `unsigned` field. This is required for invite and knock + functionality. If this option is False, that state will be removed from the + event before it is returned. Otherwise, it will be kept. Returns: dict @@ -322,11 +325,13 @@ def serialize_event( if txn_id is not None: d["unsigned"]["transaction_id"] = txn_id - # If this is an invite for somebody else, then we don't care about the - # invite_room_state as that's meant solely for the invitee. Other clients - # will already have the state since they're in the room. - if not is_invite: + # invite_room_state and knock_room_state are a list of stripped room state events + # that are meant to provide metadata about a room to an invitee/knocker. They are + # intended to only be included in specific circumstances, such as down sync, and + # should not be included in any other case. + if not include_stripped_room_state: d["unsigned"].pop("invite_room_state", None) + d["unsigned"].pop("knock_room_state", None) if as_client_event: d = event_format(d) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 1076ebc036..03ec14ce87 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1,4 +1,5 @@ -# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2015-2021 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -89,6 +90,7 @@ def __init__(self, hs: "HomeServer"): self._clock.looping_call(self._clear_tried_cache, 60 * 1000) self.state = hs.get_state_handler() self.transport_layer = hs.get_federation_transport_client() + self._msc2403_enabled = hs.config.experimental.msc2403_enabled self.hostname = hs.hostname self.signing_key = hs.signing_key @@ -620,6 +622,11 @@ async def make_membership_event( no servers successfully handle the request. """ valid_memberships = {Membership.JOIN, Membership.LEAVE} + + # Allow knocking if the feature is enabled + if self._msc2403_enabled: + valid_memberships.add(Membership.KNOCK) + if membership not in valid_memberships: raise RuntimeError( "make_membership_event called with membership='%s', must be one of %s" @@ -638,6 +645,13 @@ async def send_request(destination: str) -> Tuple[str, EventBase, RoomVersion]: if not room_version: raise UnsupportedRoomVersionError() + if not room_version.msc2403_knocking and membership == Membership.KNOCK: + raise SynapseError( + 400, + "This room version does not support knocking", + errcode=Codes.FORBIDDEN, + ) + pdu_dict = ret.get("event", None) if not isinstance(pdu_dict, dict): raise InvalidResponseError("Bad 'event' field in response") @@ -946,6 +960,62 @@ async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict: # content. return resp[1] + async def send_knock(self, destinations: List[str], pdu: EventBase) -> JsonDict: + """Attempts to send a knock event to given a list of servers. Iterates + through the list until one attempt succeeds. + + Doing so will cause the remote server to add the event to the graph, + and send the event out to the rest of the federation. + + Args: + destinations: A list of candidate homeservers which are likely to be + participating in the room. + pdu: The event to be sent. + + Returns: + The remote homeserver return some state from the room. The response + dictionary is in the form: + + {"knock_state_events": [, ...]} + + The list of state events may be empty. + + Raises: + SynapseError: If the chosen remote server returns a 3xx/4xx code. + RuntimeError: If no servers were reachable. + """ + + async def send_request(destination: str) -> JsonDict: + return await self._do_send_knock(destination, pdu) + + return await self._try_destination_list( + "xyz.amorgan.knock/send_knock", destinations, send_request + ) + + async def _do_send_knock(self, destination: str, pdu: EventBase) -> JsonDict: + """Send a knock event to a remote homeserver. + + Args: + destination: The homeserver to send to. + pdu: The event to send. + + Returns: + The remote homeserver can optionally return some state from the room. The response + dictionary is in the form: + + {"knock_state_events": [, ...]} + + The list of state events may be empty. + """ + time_now = self._clock.time_msec() + + return await self.transport_layer.send_knock_v1( + destination=destination, + room_id=pdu.room_id, + event_id=pdu.event_id, + content=pdu.get_pdu_json(time_now), + ) + async def get_public_rooms( self, remote_server: str, diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 86562cd04f..2b07f18529 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -138,6 +138,8 @@ def __init__(self, hs: "HomeServer"): hs.config.federation.federation_metrics_domains ) + self._room_prejoin_state_types = hs.config.api.room_prejoin_state + async def on_backfill_request( self, origin: str, room_id: str, versions: List[str], limit: int ) -> Tuple[int, Dict[str, Any]]: @@ -586,6 +588,103 @@ async def on_send_leave_request(self, origin: str, content: JsonDict) -> dict: await self.handler.on_send_leave_request(origin, pdu) return {} + async def on_make_knock_request( + self, origin: str, room_id: str, user_id: str, supported_versions: List[str] + ) -> Dict[str, Union[EventBase, str]]: + """We've received a /make_knock/ request, so we create a partial knock + event for the room and hand that back, along with the room version, to the knocking + homeserver. We do *not* persist or process this event until the other server has + signed it and sent it back. + + Args: + origin: The (verified) server name of the requesting server. + room_id: The room to create the knock event in. + user_id: The user to create the knock for. + supported_versions: The room versions supported by the requesting server. + + Returns: + The partial knock event. + """ + origin_host, _ = parse_server_name(origin) + await self.check_server_matches_acl(origin_host, room_id) + + room_version = await self.store.get_room_version(room_id) + + # Check that this room version is supported by the remote homeserver + if room_version.identifier not in supported_versions: + logger.warning( + "Room version %s not in %s", room_version.identifier, supported_versions + ) + raise IncompatibleRoomVersionError(room_version=room_version.identifier) + + # Check that this room supports knocking as defined by its room version + if not room_version.msc2403_knocking: + raise SynapseError( + 403, + "This room version does not support knocking", + errcode=Codes.FORBIDDEN, + ) + + pdu = await self.handler.on_make_knock_request(origin, room_id, user_id) + time_now = self._clock.time_msec() + return { + "event": pdu.get_pdu_json(time_now), + "room_version": room_version.identifier, + } + + async def on_send_knock_request( + self, + origin: str, + content: JsonDict, + room_id: str, + ) -> Dict[str, List[JsonDict]]: + """ + We have received a knock event for a room. Verify and send the event into the room + on the knocking homeserver's behalf. Then reply with some stripped state from the + room for the knockee. + + Args: + origin: The remote homeserver of the knocking user. + content: The content of the request. + room_id: The ID of the room to knock on. + + Returns: + The stripped room state. + """ + logger.debug("on_send_knock_request: content: %s", content) + + room_version = await self.store.get_room_version(room_id) + + # Check that this room supports knocking as defined by its room version + if not room_version.msc2403_knocking: + raise SynapseError( + 403, + "This room version does not support knocking", + errcode=Codes.FORBIDDEN, + ) + + pdu = event_from_pdu_json(content, room_version) + + origin_host, _ = parse_server_name(origin) + await self.check_server_matches_acl(origin_host, pdu.room_id) + + logger.debug("on_send_knock_request: pdu sigs: %s", pdu.signatures) + + pdu = await self._check_sigs_and_hash(room_version, pdu) + + # Handle the event, and retrieve the EventContext + event_context = await self.handler.on_send_knock_request(origin, pdu) + + # Retrieve stripped state events from the room and send them back to the remote + # server. This will allow the remote server's clients to display information + # related to the room while the knock request is pending. + stripped_room_state = ( + await self.store.get_stripped_room_state_from_event_context( + event_context, self._room_prejoin_state_types + ) + ) + return {"knock_state_events": stripped_room_state} + async def on_event_auth( self, origin: str, room_id: str, event_id: str ) -> Tuple[int, Dict[str, Any]]: diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 5b4f5d17f7..af0c679ed9 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1,5 +1,5 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ class TransportLayerClient: def __init__(self, hs): self.server_name = hs.hostname self.client = hs.get_federation_http_client() + self._msc2403_enabled = hs.config.experimental.msc2403_enabled @log_function def get_room_state_ids(self, destination, room_id, event_id): @@ -221,12 +222,28 @@ async def make_membership_event( is not in our federation whitelist """ valid_memberships = {Membership.JOIN, Membership.LEAVE} + + # Allow knocking if the feature is enabled + if self._msc2403_enabled: + valid_memberships.add(Membership.KNOCK) + if membership not in valid_memberships: raise RuntimeError( "make_membership_event called with membership='%s', must be one of %s" % (membership, ",".join(valid_memberships)) ) - path = _create_v1_path("/make_%s/%s/%s", membership, room_id, user_id) + + # Knock currently uses an unstable prefix + if membership == Membership.KNOCK: + # Create a path in the form of /unstable/xyz.amorgan.knock/make_knock/... + path = _create_path( + FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock", + "/make_knock/%s/%s", + room_id, + user_id, + ) + else: + path = _create_v1_path("/make_%s/%s/%s", membership, room_id, user_id) ignore_backoff = False retry_on_dns_fail = False @@ -321,6 +338,45 @@ async def send_leave_v2(self, destination, room_id, event_id, content): return response + @log_function + async def send_knock_v1( + self, + destination: str, + room_id: str, + event_id: str, + content: JsonDict, + ) -> JsonDict: + """ + Sends a signed knock membership event to a remote server. This is the second + step for knocking after make_knock. + + Args: + destination: The remote homeserver. + room_id: The ID of the room to knock on. + event_id: The ID of the knock membership event that we're sending. + content: The knock membership event that we're sending. Note that this is not the + `content` field of the membership event, but the entire signed membership event + itself represented as a JSON dict. + + Returns: + The remote homeserver can optionally return some state from the room. The response + dictionary is in the form: + + {"knock_state_events": [, ...]} + + The list of state events may be empty. + """ + path = _create_path( + FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock", + "/send_knock/%s/%s", + room_id, + event_id, + ) + + return await self.client.put_json( + destination=destination, path=path, data=content + ) + @log_function async def send_invite_v1(self, destination, room_id, event_id, content): path = _create_v1_path("/invite/%s/%s", room_id, event_id) diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 4bc7d2015b..fe5fb6bee7 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1,6 +1,5 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd -# Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,7 +12,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 functools import logging import re @@ -35,6 +33,7 @@ parse_integer_from_args, parse_json_object_from_request, parse_string_from_args, + parse_strings_from_args, ) from synapse.logging.context import run_in_background from synapse.logging.opentracing import ( @@ -565,6 +564,34 @@ async def on_PUT(self, origin, content, query, room_id, event_id): return 200, content +class FederationMakeKnockServlet(BaseFederationServerServlet): + PATH = "/make_knock/(?P[^/]*)/(?P[^/]*)" + + PREFIX = FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock" + + async def on_GET(self, origin, content, query, room_id, user_id): + try: + # Retrieve the room versions the remote homeserver claims to support + supported_versions = parse_strings_from_args(query, "ver", encoding="utf-8") + except KeyError: + raise SynapseError(400, "Missing required query parameter 'ver'") + + content = await self.handler.on_make_knock_request( + origin, room_id, user_id, supported_versions=supported_versions + ) + return 200, content + + +class FederationV1SendKnockServlet(BaseFederationServerServlet): + PATH = "/send_knock/(?P[^/]*)/(?P[^/]*)" + + PREFIX = FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock" + + async def on_PUT(self, origin, content, query, room_id, event_id): + content = await self.handler.on_send_knock_request(origin, content, room_id) + return 200, content + + class FederationEventAuthServlet(BaseFederationServerServlet): PATH = "/event_auth/(?P[^/]*)/(?P[^/]*)" @@ -1624,6 +1651,13 @@ async def on_GET(self, origin, content, query, room_id): FederationGroupsRenewAttestaionServlet, ) # type: Tuple[Type[BaseFederationServlet], ...] + +MSC2403_SERVLET_CLASSES = ( + FederationV1SendKnockServlet, + FederationMakeKnockServlet, +) + + DEFAULT_SERVLET_GROUPS = ( "federation", "room_list", @@ -1666,6 +1700,16 @@ def register_servlets( server_name=hs.hostname, ).register(resource) + # Register msc2403 (knocking) servlets if the feature is enabled + if hs.config.experimental.msc2403_enabled: + for servletclass in MSC2403_SERVLET_CLASSES: + servletclass( + hs=hs, + authenticator=authenticator, + ratelimiter=ratelimiter, + server_name=hs.hostname, + ).register(resource) + if "openid" in servlet_groups: for servletclass in OPENID_SERVLET_CLASSES: servletclass( diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index abbb71424d..6e40e2c216 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1,6 +1,5 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2017-2018 New Vector Ltd -# Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -1550,6 +1549,77 @@ async def do_invite_join( run_in_background(self._handle_queued_pdus, room_queue) + @log_function + async def do_knock( + self, + target_hosts: List[str], + room_id: str, + knockee: str, + content: JsonDict, + ) -> Tuple[str, int]: + """Sends the knock to the remote server. + + This first triggers a make_knock request that returns a partial + event that we can fill out and sign. This is then sent to the + remote server via send_knock. + + Knock events must be signed by the knockee's server before distributing. + + Args: + target_hosts: A list of hosts that we want to try knocking through. + room_id: The ID of the room to knock on. + knockee: The ID of the user who is knocking. + content: The content of the knock event. + + Returns: + A tuple of (event ID, stream ID). + + Raises: + SynapseError: If the chosen remote server returns a 3xx/4xx code. + RuntimeError: If no servers were reachable. + """ + logger.debug("Knocking on room %s on behalf of user %s", room_id, knockee) + + # Inform the remote server of the room versions we support + supported_room_versions = list(KNOWN_ROOM_VERSIONS.keys()) + + # Ask the remote server to create a valid knock event for us. Once received, + # we sign the event + params = {"ver": supported_room_versions} # type: Dict[str, Iterable[str]] + origin, event, event_format_version = await self._make_and_verify_event( + target_hosts, room_id, knockee, Membership.KNOCK, content, params=params + ) + + # Record the room ID and its version so that we have a record of the room + await self._maybe_store_room_on_outlier_membership( + room_id=event.room_id, room_version=event_format_version + ) + + # Initially try the host that we successfully called /make_knock on + try: + target_hosts.remove(origin) + target_hosts.insert(0, origin) + except ValueError: + pass + + # Send the signed event back to the room, and potentially receive some + # further information about the room in the form of partial state events + stripped_room_state = await self.federation_client.send_knock( + target_hosts, event + ) + + # Store any stripped room state events in the "unsigned" key of the event. + # This is a bit of a hack and is cribbing off of invites. Basically we + # store the room state here and retrieve it again when this event appears + # 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 = await self.state_handler.compute_event_context(event) + stream_id = await self.persist_events_and_notify( + event.room_id, [(event, context)] + ) + return event.event_id, stream_id + async def _handle_queued_pdus( self, room_queue: List[Tuple[EventBase, str]] ) -> None: @@ -1915,6 +1985,116 @@ async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: return None + @log_function + async def on_make_knock_request( + self, origin: str, room_id: str, user_id: str + ) -> EventBase: + """We've received a make_knock request, so we create a partial + knock event for the room and return that. We do *not* persist or + process it until the other server has signed it and sent it back. + + Args: + origin: The (verified) server name of the requesting server. + room_id: The room to create the knock event in. + user_id: The user to create the knock for. + + Returns: + The partial knock event. + """ + if get_domain_from_id(user_id) != origin: + logger.info( + "Get /xyz.amorgan.knock/make_knock request for user %r" + "from different origin %s, ignoring", + user_id, + origin, + ) + raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) + + room_version = await self.store.get_room_version_id(room_id) + + builder = self.event_builder_factory.new( + room_version, + { + "type": EventTypes.Member, + "content": {"membership": Membership.KNOCK}, + "room_id": room_id, + "sender": user_id, + "state_key": user_id, + }, + ) + + event, context = await self.event_creation_handler.create_new_client_event( + builder=builder + ) + + event_allowed = await self.third_party_event_rules.check_event_allowed( + event, context + ) + if not event_allowed: + logger.warning("Creation of knock %s forbidden by third-party rules", event) + raise SynapseError( + 403, "This event is not allowed in this context", Codes.FORBIDDEN + ) + + try: + # The remote hasn't signed it yet, obviously. We'll do the full checks + # when we get the event back in `on_send_knock_request` + await self.auth.check_from_context( + room_version, event, context, do_sig_check=False + ) + except AuthError as e: + logger.warning("Failed to create new knock %r because %s", event, e) + raise e + + return event + + @log_function + async def on_send_knock_request( + self, origin: str, event: EventBase + ) -> EventContext: + """ + We have received a knock event for a room. Verify that event and send it into the room + on the knocking homeserver's behalf. + + Args: + origin: The remote homeserver of the knocking user. + event: The knocking member event that has been signed by the remote homeserver. + + Returns: + The context of the event after inserting it into the room graph. + """ + logger.debug( + "on_send_knock_request: Got event: %s, signatures: %s", + event.event_id, + event.signatures, + ) + + if get_domain_from_id(event.sender) != origin: + logger.info( + "Got /xyz.amorgan.knock/send_knock request for user %r " + "from different origin %s", + event.sender, + origin, + ) + raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) + + event.internal_metadata.outlier = False + + context = await self.state_handler.compute_event_context(event) + + await self._auth_and_persist_event(origin, event, context) + + event_allowed = await self.third_party_event_rules.check_event_allowed( + event, context + ) + if not event_allowed: + logger.info("Sending of knock %s forbidden by third-party rules", event) + raise SynapseError( + 403, "This event is not allowed in this context", Codes.FORBIDDEN + ) + + return context + async def get_state_for_pdu(self, room_id: str, event_id: str) -> List[EventBase]: """Returns the state at the event. i.e. not including said event.""" diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 9f365eb5ad..4d2255bdf1 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1,6 +1,7 @@ # Copyright 2014-2016 OpenMarket Ltd # Copyright 2017-2018 New Vector Ltd -# Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2019-2020 The Matrix.org Foundation C.I.C. +# Copyrignt 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -398,13 +399,14 @@ def __init__(self, hs: "HomeServer"): self._events_shard_config = self.config.worker.events_shard_config self._instance_name = hs.get_instance_name() - self.room_invite_state_types = self.hs.config.api.room_prejoin_state + self.room_prejoin_state_types = self.hs.config.api.room_prejoin_state - self.membership_types_to_include_profile_data_in = ( - {Membership.JOIN, Membership.INVITE} - if self.hs.config.include_profile_data_on_invite - else {Membership.JOIN} - ) + self.membership_types_to_include_profile_data_in = { + Membership.JOIN, + Membership.KNOCK, + } + if self.hs.config.include_profile_data_on_invite: + self.membership_types_to_include_profile_data_in.add(Membership.INVITE) self.send_event = ReplicationSendEventRestServlet.make_client(hs) @@ -961,8 +963,8 @@ async def handle_new_client_event( room_version = await self.store.get_room_version_id(event.room_id) if event.internal_metadata.is_out_of_band_membership(): - # the only sort of out-of-band-membership events we expect to see here - # are invite rejections we have generated ourselves. + # the only sort of out-of-band-membership events we expect to see here are + # invite rejections and rescinded knocks that we have generated ourselves. assert event.type == EventTypes.Member assert event.content["membership"] == Membership.LEAVE else: @@ -1239,7 +1241,7 @@ async def persist_and_notify_client_event( "invite_room_state" ] = await self.store.get_stripped_room_state_from_event_context( context, - self.room_invite_state_types, + self.room_prejoin_state_types, membership_user_id=event.sender, ) @@ -1257,6 +1259,14 @@ async def persist_and_notify_client_event( # TODO: Make sure the signatures actually are correct. event.signatures.update(returned_invite.signatures) + if event.content["membership"] == Membership.KNOCK: + event.unsigned[ + "knock_room_state" + ] = await self.store.get_stripped_room_state_from_event_context( + context, + self.room_prejoin_state_types, + ) + if event.type == EventTypes.Redaction: original_event = await self.store.get_event( event.redacts, diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index d6fc43e798..c26963b1e1 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -1,4 +1,5 @@ # Copyright 2016-2020 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,7 +12,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 abc import logging import random @@ -30,7 +30,15 @@ from synapse.api.ratelimiting import Ratelimiter from synapse.events import EventBase from synapse.events.snapshot import EventContext -from synapse.types import JsonDict, Requester, RoomAlias, RoomID, StateMap, UserID +from synapse.types import ( + JsonDict, + Requester, + RoomAlias, + RoomID, + StateMap, + UserID, + get_domain_from_id, +) from synapse.util.async_helpers import Linearizer from synapse.util.distributor import user_left_room @@ -125,6 +133,24 @@ async def _remote_join( """ raise NotImplementedError() + @abc.abstractmethod + async def remote_knock( + self, + remote_room_hosts: List[str], + room_id: str, + user: UserID, + content: dict, + ) -> Tuple[str, int]: + """Try and knock on a room that this server is not in + + Args: + remote_room_hosts: List of servers that can be used to knock via. + room_id: Room that we are trying to knock on. + user: User who is trying to knock. + content: A dict that should be used as the content of the knock event. + """ + raise NotImplementedError() + @abc.abstractmethod async def remote_reject_invite( self, @@ -148,6 +174,27 @@ async def remote_reject_invite( """ raise NotImplementedError() + @abc.abstractmethod + async def remote_rescind_knock( + self, + knock_event_id: str, + txn_id: Optional[str], + requester: Requester, + content: JsonDict, + ) -> Tuple[str, int]: + """Rescind a local knock made on a remote room. + + Args: + knock_event_id: The ID of the knock event to rescind. + txn_id: An optional transaction ID supplied by the client. + requester: The user making the request, according to the access token. + content: The content of the generated leave event. + + Returns: + A tuple containing (event_id, stream_id of the leave event). + """ + raise NotImplementedError() + @abc.abstractmethod async def _user_left_room(self, target: UserID, room_id: str) -> None: """Notifies distributor on master process that the user has left the @@ -603,53 +650,82 @@ async def update_membership_locked( elif effective_membership_state == Membership.LEAVE: if not is_host_in_room: - # perhaps we've been invited + # Figure out the user's current membership state for the room ( current_membership_type, current_membership_event_id, ) = await self.store.get_local_current_membership_for_user_in_room( target.to_string(), room_id ) - if ( - current_membership_type != Membership.INVITE - or not current_membership_event_id - ): + if not current_membership_type or not current_membership_event_id: logger.info( "%s sent a leave request to %s, but that is not an active room " - "on this server, and there is no pending invite", + "on this server, or there is no pending invite or knock", target, room_id, ) raise SynapseError(404, "Not a known room") - invite = await self.store.get_event(current_membership_event_id) - logger.info( - "%s rejects invite to %s from %s", target, room_id, invite.sender - ) + # perhaps we've been invited + if current_membership_type == Membership.INVITE: + invite = await self.store.get_event(current_membership_event_id) + logger.info( + "%s rejects invite to %s from %s", + target, + room_id, + invite.sender, + ) - if not self.hs.is_mine_id(invite.sender): - # send the rejection to the inviter's HS (with fallback to - # local event) - return await self.remote_reject_invite( - invite.event_id, - txn_id, - requester, - content, + if not self.hs.is_mine_id(invite.sender): + # send the rejection to the inviter's HS (with fallback to + # local event) + return await self.remote_reject_invite( + invite.event_id, + txn_id, + requester, + content, + ) + + # the inviter was on our server, but has now left. Carry on + # with the normal rejection codepath, which will also send the + # rejection out to any other servers we believe are still in the room. + + # thanks to overzealous cleaning up of event_forward_extremities in + # `delete_old_current_state_events`, it's possible to end up with no + # forward extremities here. If that happens, let's just hang the + # rejection off the invite event. + # + # see: https://github.com/matrix-org/synapse/issues/7139 + if len(latest_event_ids) == 0: + latest_event_ids = [invite.event_id] + + # or perhaps this is a remote room that a local user has knocked on + elif current_membership_type == Membership.KNOCK: + knock = await self.store.get_event(current_membership_event_id) + return await self.remote_rescind_knock( + knock.event_id, txn_id, requester, content ) - # the inviter was on our server, but has now left. Carry on - # with the normal rejection codepath, which will also send the - # rejection out to any other servers we believe are still in the room. + elif ( + self.config.experimental.msc2403_enabled + and effective_membership_state == Membership.KNOCK + ): + if not is_host_in_room: + # The knock needs to be sent over federation instead + remote_room_hosts.append(get_domain_from_id(room_id)) - # thanks to overzealous cleaning up of event_forward_extremities in - # `delete_old_current_state_events`, it's possible to end up with no - # forward extremities here. If that happens, let's just hang the - # rejection off the invite event. - # - # see: https://github.com/matrix-org/synapse/issues/7139 - if len(latest_event_ids) == 0: - latest_event_ids = [invite.event_id] + content["membership"] = Membership.KNOCK + + profile = self.profile_handler + if "displayname" not in content: + content["displayname"] = await profile.get_displayname(target) + if "avatar_url" not in content: + content["avatar_url"] = await profile.get_avatar_url(target) + + return await self.remote_knock( + remote_room_hosts, room_id, target, content + ) return await self._local_membership_update( requester=requester, @@ -1209,6 +1285,35 @@ async def remote_reject_invite( invite_event, txn_id, requester, content ) + async def remote_rescind_knock( + self, + knock_event_id: str, + txn_id: Optional[str], + requester: Requester, + content: JsonDict, + ) -> Tuple[str, int]: + """ + Rescinds a local knock made on a remote room + + Args: + knock_event_id: The ID of the knock event to rescind. + txn_id: The transaction ID to use. + requester: The originator of the request. + content: The content of the leave event. + + Implements RoomMemberHandler.remote_rescind_knock + """ + # TODO: We don't yet support rescinding knocks over federation + # as we don't know which homeserver to send it to. An obvious + # candidate is the remote homeserver we originally knocked through, + # however we don't currently store that information. + + # Just rescind the knock locally + knock_event = await self.store.get_event(knock_event_id) + return await self._generate_local_out_of_band_leave( + knock_event, txn_id, requester, content + ) + async def _generate_local_out_of_band_leave( self, previous_membership_event: EventBase, @@ -1272,6 +1377,36 @@ async def _generate_local_out_of_band_leave( return result_event.event_id, result_event.internal_metadata.stream_ordering + async def remote_knock( + self, + remote_room_hosts: List[str], + room_id: str, + user: UserID, + content: dict, + ) -> Tuple[str, int]: + """Sends a knock to a room. Attempts to do so via one remote out of a given list. + + Args: + remote_room_hosts: A list of homeservers to try knocking through. + room_id: The ID of the room to knock on. + user: The user to knock on behalf of. + content: The content of the knock event. + + Returns: + A tuple of (event ID, stream ID). + """ + # filter ourselves out of remote_room_hosts + remote_room_hosts = [ + host for host in remote_room_hosts if host != self.hs.hostname + ] + + if len(remote_room_hosts) == 0: + raise SynapseError(404, "No known servers") + + return await self.federation_handler.do_knock( + remote_room_hosts, room_id, user.to_string(), content=content + ) + async def _user_left_room(self, target: UserID, room_id: str) -> None: """Implements RoomMemberHandler._user_left_room""" user_left_room(self.distributor, target, room_id) diff --git a/synapse/handlers/room_member_worker.py b/synapse/handlers/room_member_worker.py index 3e89dd2315..221552a2a6 100644 --- a/synapse/handlers/room_member_worker.py +++ b/synapse/handlers/room_member_worker.py @@ -1,4 +1,4 @@ -# Copyright 2018 New Vector Ltd +# Copyright 2018-2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,10 +19,12 @@ from synapse.handlers.room_member import RoomMemberHandler from synapse.replication.http.membership import ( ReplicationRemoteJoinRestServlet as ReplRemoteJoin, + ReplicationRemoteKnockRestServlet as ReplRemoteKnock, ReplicationRemoteRejectInviteRestServlet as ReplRejectInvite, + ReplicationRemoteRescindKnockRestServlet as ReplRescindKnock, ReplicationUserJoinedLeftRoomRestServlet as ReplJoinedLeft, ) -from synapse.types import Requester, UserID +from synapse.types import JsonDict, Requester, UserID if TYPE_CHECKING: from synapse.server import HomeServer @@ -35,7 +37,9 @@ def __init__(self, hs: "HomeServer"): super().__init__(hs) self._remote_join_client = ReplRemoteJoin.make_client(hs) + self._remote_knock_client = ReplRemoteKnock.make_client(hs) self._remote_reject_client = ReplRejectInvite.make_client(hs) + self._remote_rescind_client = ReplRescindKnock.make_client(hs) self._notify_change_client = ReplJoinedLeft.make_client(hs) async def _remote_join( @@ -80,6 +84,53 @@ async def remote_reject_invite( ) return ret["event_id"], ret["stream_id"] + async def remote_rescind_knock( + self, + knock_event_id: str, + txn_id: Optional[str], + requester: Requester, + content: JsonDict, + ) -> Tuple[str, int]: + """ + Rescinds a local knock made on a remote room + + Args: + knock_event_id: the knock event + txn_id: optional transaction ID supplied by the client + requester: user making the request, according to the access token + content: additional content to include in the leave event. + Normally an empty dict. + + Returns: + A tuple containing (event_id, stream_id of the leave event) + """ + ret = await self._remote_rescind_client( + knock_event_id=knock_event_id, + txn_id=txn_id, + requester=requester, + content=content, + ) + return ret["event_id"], ret["stream_id"] + + async def remote_knock( + self, + remote_room_hosts: List[str], + room_id: str, + user: UserID, + content: dict, + ) -> Tuple[str, int]: + """Sends a knock to a room. + + Implements RoomMemberHandler.remote_knock + """ + ret = await self._remote_knock_client( + remote_room_hosts=remote_room_hosts, + room_id=room_id, + user=user, + content=content, + ) + return ret["event_id"], ret["stream_id"] + async def _user_left_room(self, target: UserID, room_id: str) -> None: """Implements RoomMemberHandler._user_left_room""" await self._notify_change_client( diff --git a/synapse/handlers/stats.py b/synapse/handlers/stats.py index 383e34026e..4e45d1da57 100644 --- a/synapse/handlers/stats.py +++ b/synapse/handlers/stats.py @@ -1,4 +1,5 @@ -# Copyright 2018 New Vector Ltd +# Copyright 2018-2021 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -230,6 +231,8 @@ async def _handle_deltas( room_stats_delta["left_members"] -= 1 elif prev_membership == Membership.BAN: room_stats_delta["banned_members"] -= 1 + elif prev_membership == Membership.KNOCK: + room_stats_delta["knocked_members"] -= 1 else: raise ValueError( "%r is not a valid prev_membership" % (prev_membership,) @@ -251,6 +254,8 @@ async def _handle_deltas( room_stats_delta["left_members"] += 1 elif membership == Membership.BAN: room_stats_delta["banned_members"] += 1 + elif membership == Membership.KNOCK: + room_stats_delta["knocked_members"] += 1 else: raise ValueError("%r is not a valid membership" % (membership,)) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index b1c58ffdc8..7f2138d804 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -159,6 +159,16 @@ def __bool__(self) -> bool: return True +@attr.s(slots=True, frozen=True) +class KnockedSyncResult: + room_id = attr.ib(type=str) + knock = attr.ib(type=EventBase) + + def __bool__(self) -> bool: + """Knocked rooms should always be reported to the client""" + return True + + @attr.s(slots=True, frozen=True) class GroupsSyncResult: join = attr.ib(type=JsonDict) @@ -192,6 +202,7 @@ class _RoomChanges: room_entries = attr.ib(type=List["RoomSyncResultBuilder"]) invited = attr.ib(type=List[InvitedSyncResult]) + knocked = attr.ib(type=List[KnockedSyncResult]) newly_joined_rooms = attr.ib(type=List[str]) newly_left_rooms = attr.ib(type=List[str]) @@ -205,6 +216,7 @@ class SyncResult: account_data: List of account_data events for the user. joined: JoinedSyncResult for each joined room. invited: InvitedSyncResult for each invited room. + knocked: KnockedSyncResult for each knocked on room. archived: ArchivedSyncResult for each archived room. to_device: List of direct messages for the device. device_lists: List of user_ids whose devices have changed @@ -220,6 +232,7 @@ class SyncResult: account_data = attr.ib(type=List[JsonDict]) joined = attr.ib(type=List[JoinedSyncResult]) invited = attr.ib(type=List[InvitedSyncResult]) + knocked = attr.ib(type=List[KnockedSyncResult]) archived = attr.ib(type=List[ArchivedSyncResult]) to_device = attr.ib(type=List[JsonDict]) device_lists = attr.ib(type=DeviceLists) @@ -236,6 +249,7 @@ def __bool__(self) -> bool: self.presence or self.joined or self.invited + or self.knocked or self.archived or self.account_data or self.to_device @@ -1031,7 +1045,7 @@ async def generate_sync_result( res = await self._generate_sync_entry_for_rooms( sync_result_builder, account_data_by_room ) - newly_joined_rooms, newly_joined_or_invited_users, _, _ = res + newly_joined_rooms, newly_joined_or_invited_or_knocked_users, _, _ = res _, _, newly_left_rooms, newly_left_users = res block_all_presence_data = ( @@ -1040,7 +1054,9 @@ async def generate_sync_result( if self.hs_config.use_presence and not block_all_presence_data: logger.debug("Fetching presence data") await self._generate_sync_entry_for_presence( - sync_result_builder, newly_joined_rooms, newly_joined_or_invited_users + sync_result_builder, + newly_joined_rooms, + newly_joined_or_invited_or_knocked_users, ) logger.debug("Fetching to-device data") @@ -1049,7 +1065,7 @@ async def generate_sync_result( device_lists = await self._generate_sync_entry_for_device_list( sync_result_builder, newly_joined_rooms=newly_joined_rooms, - newly_joined_or_invited_users=newly_joined_or_invited_users, + newly_joined_or_invited_or_knocked_users=newly_joined_or_invited_or_knocked_users, newly_left_rooms=newly_left_rooms, newly_left_users=newly_left_users, ) @@ -1083,6 +1099,7 @@ async def generate_sync_result( account_data=sync_result_builder.account_data, joined=sync_result_builder.joined, invited=sync_result_builder.invited, + knocked=sync_result_builder.knocked, archived=sync_result_builder.archived, to_device=sync_result_builder.to_device, device_lists=device_lists, @@ -1142,7 +1159,7 @@ async def _generate_sync_entry_for_device_list( self, sync_result_builder: "SyncResultBuilder", newly_joined_rooms: Set[str], - newly_joined_or_invited_users: Set[str], + newly_joined_or_invited_or_knocked_users: Set[str], newly_left_rooms: Set[str], newly_left_users: Set[str], ) -> DeviceLists: @@ -1151,8 +1168,9 @@ async def _generate_sync_entry_for_device_list( Args: sync_result_builder newly_joined_rooms: Set of rooms user has joined since previous sync - newly_joined_or_invited_users: Set of users that have joined or - been invited to a room since previous sync. + newly_joined_or_invited_or_knocked_users: Set of users that have joined, + been invited to a room or are knocking on a room since + previous sync. newly_left_rooms: Set of rooms user has left since previous sync newly_left_users: Set of users that have left a room we're in since previous sync @@ -1163,7 +1181,9 @@ async def _generate_sync_entry_for_device_list( # We're going to mutate these fields, so lets copy them rather than # assume they won't get used later. - newly_joined_or_invited_users = set(newly_joined_or_invited_users) + newly_joined_or_invited_or_knocked_users = set( + newly_joined_or_invited_or_knocked_users + ) newly_left_users = set(newly_left_users) if since_token and since_token.device_list_key: @@ -1202,11 +1222,11 @@ async def _generate_sync_entry_for_device_list( # Step 1b, check for newly joined rooms for room_id in newly_joined_rooms: joined_users = await self.store.get_users_in_room(room_id) - newly_joined_or_invited_users.update(joined_users) + newly_joined_or_invited_or_knocked_users.update(joined_users) # TODO: Check that these users are actually new, i.e. either they # weren't in the previous sync *or* they left and rejoined. - users_that_have_changed.update(newly_joined_or_invited_users) + users_that_have_changed.update(newly_joined_or_invited_or_knocked_users) user_signatures_changed = ( await self.store.get_users_whose_signatures_changed( @@ -1452,6 +1472,7 @@ async def _generate_sync_entry_for_rooms( room_entries = room_changes.room_entries invited = room_changes.invited + knocked = room_changes.knocked newly_joined_rooms = room_changes.newly_joined_rooms newly_left_rooms = room_changes.newly_left_rooms @@ -1472,9 +1493,10 @@ async def handle_room_entries(room_entry): await concurrently_execute(handle_room_entries, room_entries, 10) sync_result_builder.invited.extend(invited) + sync_result_builder.knocked.extend(knocked) - # Now we want to get any newly joined or invited users - newly_joined_or_invited_users = set() + # Now we want to get any newly joined, invited or knocking users + newly_joined_or_invited_or_knocked_users = set() newly_left_users = set() if since_token: for joined_sync in sync_result_builder.joined: @@ -1486,19 +1508,22 @@ async def handle_room_entries(room_entry): if ( event.membership == Membership.JOIN or event.membership == Membership.INVITE + or event.membership == Membership.KNOCK ): - newly_joined_or_invited_users.add(event.state_key) + newly_joined_or_invited_or_knocked_users.add( + event.state_key + ) else: prev_content = event.unsigned.get("prev_content", {}) prev_membership = prev_content.get("membership", None) if prev_membership == Membership.JOIN: newly_left_users.add(event.state_key) - newly_left_users -= newly_joined_or_invited_users + newly_left_users -= newly_joined_or_invited_or_knocked_users return ( set(newly_joined_rooms), - newly_joined_or_invited_users, + newly_joined_or_invited_or_knocked_users, set(newly_left_rooms), newly_left_users, ) @@ -1553,6 +1578,7 @@ async def _get_rooms_changed( newly_left_rooms = [] room_entries = [] invited = [] + knocked = [] for room_id, events in mem_change_events_by_room_id.items(): logger.debug( "Membership changes in %s: [%s]", @@ -1632,9 +1658,17 @@ async def _get_rooms_changed( should_invite = non_joins[-1].membership == Membership.INVITE if should_invite: if event.sender not in ignored_users: - room_sync = InvitedSyncResult(room_id, invite=non_joins[-1]) - if room_sync: - invited.append(room_sync) + invite_room_sync = InvitedSyncResult(room_id, invite=non_joins[-1]) + if invite_room_sync: + invited.append(invite_room_sync) + + # Only bother if our latest membership in the room is knock (and we haven't + # been accepted/rejected in the meantime). + should_knock = non_joins[-1].membership == Membership.KNOCK + if should_knock: + knock_room_sync = KnockedSyncResult(room_id, knock=non_joins[-1]) + if knock_room_sync: + knocked.append(knock_room_sync) # Always include leave/ban events. Just take the last one. # TODO: How do we handle ban -> leave in same batch? @@ -1738,7 +1772,13 @@ async def _get_rooms_changed( ) room_entries.append(entry) - return _RoomChanges(room_entries, invited, newly_joined_rooms, newly_left_rooms) + return _RoomChanges( + room_entries, + invited, + knocked, + newly_joined_rooms, + newly_left_rooms, + ) async def _get_all_rooms( self, sync_result_builder: "SyncResultBuilder", ignored_users: FrozenSet[str] @@ -1758,6 +1798,7 @@ async def _get_all_rooms( membership_list = ( Membership.INVITE, + Membership.KNOCK, Membership.JOIN, Membership.LEAVE, Membership.BAN, @@ -1769,6 +1810,7 @@ async def _get_all_rooms( room_entries = [] invited = [] + knocked = [] for event in room_list: if event.membership == Membership.JOIN: @@ -1788,8 +1830,11 @@ async def _get_all_rooms( continue invite = await self.store.get_event(event.event_id) invited.append(InvitedSyncResult(room_id=event.room_id, invite=invite)) + elif event.membership == Membership.KNOCK: + knock = await self.store.get_event(event.event_id) + knocked.append(KnockedSyncResult(room_id=event.room_id, knock=knock)) elif event.membership in (Membership.LEAVE, Membership.BAN): - # Always send down rooms we were banned or kicked from. + # Always send down rooms we were banned from or kicked from. if not sync_config.filter_collection.include_leave: if event.membership == Membership.LEAVE: if user_id == event.sender: @@ -1810,7 +1855,7 @@ async def _get_all_rooms( ) ) - return _RoomChanges(room_entries, invited, [], []) + return _RoomChanges(room_entries, invited, knocked, [], []) async def _generate_room_entry( self, @@ -2101,6 +2146,7 @@ class SyncResultBuilder: account_data (list) joined (list[JoinedSyncResult]) invited (list[InvitedSyncResult]) + knocked (list[KnockedSyncResult]) archived (list[ArchivedSyncResult]) groups (GroupsSyncResult|None) to_device (list) @@ -2116,6 +2162,7 @@ class SyncResultBuilder: account_data = attr.ib(type=List[JsonDict], default=attr.Factory(list)) joined = attr.ib(type=List[JoinedSyncResult], default=attr.Factory(list)) invited = attr.ib(type=List[InvitedSyncResult], default=attr.Factory(list)) + knocked = attr.ib(type=List[KnockedSyncResult], default=attr.Factory(list)) archived = attr.ib(type=List[ArchivedSyncResult], default=attr.Factory(list)) groups = attr.ib(type=Optional[GroupsSyncResult], default=None) to_device = attr.ib(type=List[JsonDict], default=attr.Factory(list)) diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 72e2ec78db..3c43f32586 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -13,7 +13,6 @@ # limitations under the License. """ This module contains base REST classes for constructing REST servlets. """ - import logging from typing import Dict, Iterable, List, Optional, overload diff --git a/synapse/replication/http/membership.py b/synapse/replication/http/membership.py index 289a397d68..043c25f63d 100644 --- a/synapse/replication/http/membership.py +++ b/synapse/replication/http/membership.py @@ -97,6 +97,76 @@ async def _handle_request( # type: ignore return 200, {"event_id": event_id, "stream_id": stream_id} +class ReplicationRemoteKnockRestServlet(ReplicationEndpoint): + """Perform a remote knock for the given user on the given room + + Request format: + + POST /_synapse/replication/remote_knock/:room_id/:user_id + + { + "requester": ..., + "remote_room_hosts": [...], + "content": { ... } + } + """ + + NAME = "remote_knock" + PATH_ARGS = ("room_id", "user_id") + + def __init__(self, hs: "HomeServer"): + super().__init__(hs) + + self.federation_handler = hs.get_federation_handler() + self.store = hs.get_datastore() + self.clock = hs.get_clock() + + @staticmethod + async def _serialize_payload( # type: ignore + requester: Requester, + room_id: str, + user_id: str, + remote_room_hosts: List[str], + content: JsonDict, + ): + """ + Args: + requester: The user making the request, according to the access token. + room_id: The ID of the room to knock on. + user_id: The ID of the knocking user. + remote_room_hosts: Servers to try and send the knock via. + content: The event content to use for the knock event. + """ + return { + "requester": requester.serialize(), + "remote_room_hosts": remote_room_hosts, + "content": content, + } + + async def _handle_request( # type: ignore + self, + request: SynapseRequest, + room_id: str, + user_id: str, + ): + content = parse_json_object_from_request(request) + + remote_room_hosts = content["remote_room_hosts"] + event_content = content["content"] + + requester = Requester.deserialize(self.store, content["requester"]) + + request.requester = requester + + logger.debug("remote_knock: %s on room: %s", user_id, room_id) + + event_id, stream_id = await self.federation_handler.do_knock( + remote_room_hosts, room_id, user_id, event_content + ) + + return 200, {"event_id": event_id, "stream_id": stream_id} + + class ReplicationRemoteRejectInviteRestServlet(ReplicationEndpoint): """Rejects an out-of-band invite we have received from a remote server @@ -167,6 +237,75 @@ async def _handle_request( # type: ignore return 200, {"event_id": event_id, "stream_id": stream_id} +class ReplicationRemoteRescindKnockRestServlet(ReplicationEndpoint): + """Rescinds a local knock made on a remote room + + Request format: + + POST /_synapse/replication/remote_rescind_knock/:event_id + + { + "txn_id": ..., + "requester": ..., + "content": { ... } + } + """ + + NAME = "remote_rescind_knock" + PATH_ARGS = ("knock_event_id",) + + def __init__(self, hs: "HomeServer"): + super().__init__(hs) + + self.store = hs.get_datastore() + self.clock = hs.get_clock() + self.member_handler = hs.get_room_member_handler() + + @staticmethod + async def _serialize_payload( # type: ignore + knock_event_id: str, + txn_id: Optional[str], + requester: Requester, + content: JsonDict, + ): + """ + Args: + knock_event_id: The ID of the knock to be rescinded. + txn_id: An optional transaction ID supplied by the client. + requester: The user making the rescind request, according to the access token. + content: The content to include in the rescind event. + """ + return { + "txn_id": txn_id, + "requester": requester.serialize(), + "content": content, + } + + async def _handle_request( # type: ignore + self, + request: SynapseRequest, + knock_event_id: str, + ): + content = parse_json_object_from_request(request) + + txn_id = content["txn_id"] + event_content = content["content"] + + requester = Requester.deserialize(self.store, content["requester"]) + + request.requester = requester + + # hopefully we're now on the master, so this won't recurse! + event_id, stream_id = await self.member_handler.remote_rescind_knock( + knock_event_id, + txn_id, + requester, + event_content, + ) + + return 200, {"event_id": event_id, "stream_id": stream_id} + + class ReplicationUserJoinedLeftRoomRestServlet(ReplicationEndpoint): """Notifies that a user has joined or left the room diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 79d52d2dcb..138411ad19 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -38,6 +38,7 @@ filter, groups, keys, + knock, notifications, openid, password_policy, @@ -121,6 +122,10 @@ def register_servlets(client_resource, hs): relations.register_servlets(hs, client_resource) password_policy.register_servlets(hs, client_resource) + # Register msc2403 (knocking) servlets if the feature is enabled + if hs.config.experimental.msc2403_enabled: + knock.register_servlets(hs, client_resource) + # moving to /_synapse/admin admin.register_servlets_for_client_rest_resource(hs, client_resource) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 122105854a..16d087ea60 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -14,10 +14,9 @@ # limitations under the License. """ This module contains REST servlets to do with rooms: /rooms/ """ - import logging import re -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple from urllib import parse as urlparse from synapse.api.constants import EventTypes, Membership @@ -38,6 +37,7 @@ parse_integer, parse_json_object_from_request, parse_string, + parse_strings_from_args, ) from synapse.http.site import SynapseRequest from synapse.logging.opentracing import set_tag @@ -278,7 +278,12 @@ def register(self, http_server): PATTERNS = "/join/(?P[^/]*)" register_txn_path(self, PATTERNS, http_server) - async def on_POST(self, request, room_identifier, txn_id=None): + async def on_POST( + self, + request: SynapseRequest, + room_identifier: str, + txn_id: Optional[str] = None, + ): requester = await self.auth.get_user_by_req(request, allow_guest=True) try: @@ -290,17 +295,18 @@ async def on_POST(self, request, room_identifier, txn_id=None): if RoomID.is_valid(room_identifier): room_id = room_identifier - try: - remote_room_hosts = [ - x.decode("ascii") for x in request.args[b"server_name"] - ] # type: Optional[List[str]] - except Exception: - remote_room_hosts = None + + # twisted.web.server.Request.args is incorrectly defined as Optional[Any] + args: Dict[bytes, List[bytes]] = request.args # type: ignore + + remote_room_hosts = parse_strings_from_args( + args, "server_name", required=False + ) elif RoomAlias.is_valid(room_identifier): handler = self.room_member_handler room_alias = RoomAlias.from_string(room_identifier) - room_id, remote_room_hosts = await handler.lookup_room_alias(room_alias) - room_id = room_id.to_string() + room_id_obj, remote_room_hosts = await handler.lookup_room_alias(room_alias) + room_id = room_id_obj.to_string() else: raise SynapseError( 400, "%s was not legal room ID or room alias" % (room_identifier,) diff --git a/synapse/rest/client/v2_alpha/knock.py b/synapse/rest/client/v2_alpha/knock.py new file mode 100644 index 0000000000..f046bf9cb3 --- /dev/null +++ b/synapse/rest/client/v2_alpha/knock.py @@ -0,0 +1,109 @@ +# Copyright 2020 Sorunome +# Copyright 2020 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple + +from twisted.web.server import Request + +from synapse.api.constants import Membership +from synapse.api.errors import SynapseError +from synapse.http.servlet import ( + RestServlet, + parse_json_object_from_request, + parse_strings_from_args, +) +from synapse.http.site import SynapseRequest +from synapse.logging.opentracing import set_tag +from synapse.rest.client.transactions import HttpTransactionCache +from synapse.types import JsonDict, RoomAlias, RoomID + +if TYPE_CHECKING: + from synapse.app.homeserver import HomeServer + +from ._base import client_patterns + +logger = logging.getLogger(__name__) + + +class KnockRoomAliasServlet(RestServlet): + """ + POST /xyz.amorgan.knock/{roomIdOrAlias} + """ + + PATTERNS = client_patterns( + "/xyz.amorgan.knock/(?P[^/]*)", releases=() + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self.txns = HttpTransactionCache(hs) + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + + async def on_POST( + self, + request: SynapseRequest, + room_identifier: str, + txn_id: Optional[str] = None, + ) -> Tuple[int, JsonDict]: + requester = await self.auth.get_user_by_req(request) + + content = parse_json_object_from_request(request) + event_content = None + if "reason" in content: + event_content = {"reason": content["reason"]} + + if RoomID.is_valid(room_identifier): + room_id = room_identifier + + # twisted.web.server.Request.args is incorrectly defined as Optional[Any] + args: Dict[bytes, List[bytes]] = request.args # type: ignore + + remote_room_hosts = parse_strings_from_args( + args, "server_name", required=False + ) + elif RoomAlias.is_valid(room_identifier): + handler = self.room_member_handler + room_alias = RoomAlias.from_string(room_identifier) + room_id_obj, remote_room_hosts = await handler.lookup_room_alias(room_alias) + room_id = room_id_obj.to_string() + else: + raise SynapseError( + 400, "%s was not legal room ID or room alias" % (room_identifier,) + ) + + await self.room_member_handler.update_membership( + requester=requester, + target=requester.user, + room_id=room_id, + action=Membership.KNOCK, + txn_id=txn_id, + third_party_signed=None, + remote_room_hosts=remote_room_hosts, + content=event_content, + ) + + return 200, {"room_id": room_id} + + def on_PUT(self, request: Request, room_identifier: str, txn_id: str): + set_tag("txn_id", txn_id) + + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_identifier, txn_id + ) + + +def register_servlets(hs, http_server): + KnockRoomAliasServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index 95ee3f1b84..042e1788b6 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -11,12 +11,11 @@ # 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 itertools import logging -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple -from synapse.api.constants import PresenceState +from synapse.api.constants import Membership, PresenceState from synapse.api.errors import Codes, StoreError, SynapseError from synapse.api.filtering import DEFAULT_FILTER_COLLECTION, FilterCollection from synapse.events.utils import ( @@ -24,7 +23,7 @@ format_event_raw, ) from synapse.handlers.presence import format_user_presence_state -from synapse.handlers.sync import SyncConfig +from synapse.handlers.sync import KnockedSyncResult, SyncConfig from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string from synapse.http.site import SynapseRequest from synapse.types import JsonDict, StreamToken @@ -220,6 +219,10 @@ async def encode_response(self, time_now, sync_result, access_token_id, filter): sync_result.invited, time_now, access_token_id, event_formatter ) + knocked = await self.encode_knocked( + sync_result.knocked, time_now, access_token_id, event_formatter + ) + archived = await self.encode_archived( sync_result.archived, time_now, @@ -237,11 +240,16 @@ async def encode_response(self, time_now, sync_result, access_token_id, filter): "left": list(sync_result.device_lists.left), }, "presence": SyncRestServlet.encode_presence(sync_result.presence, time_now), - "rooms": {"join": joined, "invite": invited, "leave": archived}, + "rooms": { + Membership.JOIN: joined, + Membership.INVITE: invited, + Membership.KNOCK: knocked, + Membership.LEAVE: archived, + }, "groups": { - "join": sync_result.groups.join, - "invite": sync_result.groups.invite, - "leave": sync_result.groups.leave, + Membership.JOIN: sync_result.groups.join, + Membership.INVITE: sync_result.groups.invite, + Membership.LEAVE: sync_result.groups.leave, }, "device_one_time_keys_count": sync_result.device_one_time_keys_count, "org.matrix.msc2732.device_unused_fallback_key_types": sync_result.device_unused_fallback_key_types, @@ -303,7 +311,7 @@ async def encode_invited(self, rooms, time_now, token_id, event_formatter): Args: rooms(list[synapse.handlers.sync.InvitedSyncResult]): list of - sync results for rooms this user is joined to + sync results for rooms this user is invited to time_now(int): current time - used as a baseline for age calculations token_id(int): ID of the user's auth token - used for namespacing @@ -322,7 +330,7 @@ async def encode_invited(self, rooms, time_now, token_id, event_formatter): time_now, token_id=token_id, event_format=event_formatter, - is_invite=True, + include_stripped_room_state=True, ) unsigned = dict(invite.get("unsigned", {})) invite["unsigned"] = unsigned @@ -332,6 +340,60 @@ async def encode_invited(self, rooms, time_now, token_id, event_formatter): return invited + async def encode_knocked( + self, + rooms: List[KnockedSyncResult], + time_now: int, + token_id: int, + event_formatter: Callable[[Dict], Dict], + ) -> Dict[str, Dict[str, Any]]: + """ + Encode the rooms we've knocked on in a sync result. + + Args: + rooms: list of sync results for rooms this user is knocking on + time_now: current time - used as a baseline for age calculations + token_id: ID of the user's auth token - used for namespacing of transaction IDs + event_formatter: function to convert from federation format to client format + + Returns: + The list of rooms the user has knocked on, in our response format. + """ + knocked = {} + for room in rooms: + knock = await self._event_serializer.serialize_event( + room.knock, + time_now, + token_id=token_id, + event_format=event_formatter, + include_stripped_room_state=True, + ) + + # Extract the `unsigned` key from the knock event. + # This is where we (cheekily) store the knock state events + unsigned = knock.setdefault("unsigned", {}) + + # Duplicate the dictionary in order to avoid modifying the original + unsigned = dict(unsigned) + + # Extract the stripped room state from the unsigned dict + # This is for clients to get a little bit of information about + # the room they've knocked on, without revealing any sensitive information + knocked_state = list(unsigned.pop("knock_room_state", [])) + + # Append the actual knock membership event itself as well. This provides + # the client with: + # + # * A knock state event that they can use for easier internal tracking + # * The rough timestamp of when the knock occurred contained within the event + knocked_state.append(knock) + + # Build the `knock_state` dictionary, which will contain the state of the + # room that the client has knocked on + knocked[room.room_id] = {"knock_state": {"events": knocked_state}} + + return knocked + async def encode_archived( self, rooms, time_now, token_id, event_fields, event_formatter ): diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index ae9f880965..82a1833509 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -41,6 +41,7 @@ "current_state_events", "joined_members", "invited_members", + "knocked_members", "left_members", "banned_members", "local_users_in_room", diff --git a/synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql b/synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql new file mode 100644 index 0000000000..56c0ad0003 --- /dev/null +++ b/synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql @@ -0,0 +1,17 @@ +/* Copyright 2020 Sorunome + * + * 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. + */ + +ALTER TABLE room_stats_current ADD COLUMN knocked_members INT NOT NULL DEFAULT '0'; +ALTER TABLE room_stats_historical ADD COLUMN knocked_members BIGINT NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/tests/federation/transport/test_knocking.py b/tests/federation/transport/test_knocking.py new file mode 100644 index 0000000000..121aa88cfa --- /dev/null +++ b/tests/federation/transport/test_knocking.py @@ -0,0 +1,302 @@ +# Copyright 2020 Matrix.org Federation C.I.C +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import OrderedDict +from typing import Dict, List + +from synapse.api.constants import EventTypes, JoinRules, Membership +from synapse.api.room_versions import RoomVersions +from synapse.events import builder +from synapse.rest import admin +from synapse.rest.client.v1 import login, room +from synapse.server import HomeServer +from synapse.types import RoomAlias + +from tests.test_utils import event_injection +from tests.unittest import FederatingHomeserverTestCase, TestCase, override_config + +# An identifier to use while MSC2304 is not in a stable release of the spec +KNOCK_UNSTABLE_IDENTIFIER = "xyz.amorgan.knock" + + +class KnockingStrippedStateEventHelperMixin(TestCase): + def send_example_state_events_to_room( + self, + hs: "HomeServer", + room_id: str, + sender: str, + ) -> OrderedDict: + """Adds some state to a room. State events are those that should be sent to a knocking + user after they knock on the room, as well as some state that *shouldn't* be sent + to the knocking user. + + Args: + hs: The homeserver of the sender. + room_id: The ID of the room to send state into. + sender: The ID of the user to send state as. Must be in the room. + + Returns: + The OrderedDict of event types and content that a user is expected to see + after knocking on a room. + """ + # To set a canonical alias, we'll need to point an alias at the room first. + canonical_alias = "#fancy_alias:test" + self.get_success( + self.store.create_room_alias_association( + RoomAlias.from_string(canonical_alias), room_id, ["test"] + ) + ) + + # Send some state that we *don't* expect to be given to knocking users + self.get_success( + event_injection.inject_event( + hs, + room_version=RoomVersions.MSC2403.identifier, + room_id=room_id, + sender=sender, + type="com.example.secret", + state_key="", + content={"secret": "password"}, + ) + ) + + # We use an OrderedDict here to ensure that the knock membership appears last. + # Note that order only matters when sending stripped state to clients, not federated + # homeservers. + room_state = OrderedDict( + [ + # We need to set the room's join rules to allow knocking + ( + EventTypes.JoinRules, + {"content": {"join_rule": JoinRules.KNOCK}, "state_key": ""}, + ), + # Below are state events that are to be stripped and sent to clients + ( + EventTypes.Name, + {"content": {"name": "A cool room"}, "state_key": ""}, + ), + ( + EventTypes.RoomAvatar, + { + "content": { + "info": { + "h": 398, + "mimetype": "image/jpeg", + "size": 31037, + "w": 394, + }, + "url": "mxc://example.org/JWEIFJgwEIhweiWJE", + }, + "state_key": "", + }, + ), + ( + EventTypes.RoomEncryption, + {"content": {"algorithm": "m.megolm.v1.aes-sha2"}, "state_key": ""}, + ), + ( + EventTypes.CanonicalAlias, + { + "content": {"alias": canonical_alias, "alt_aliases": []}, + "state_key": "", + }, + ), + ] + ) + + for event_type, event_dict in room_state.items(): + event_content = event_dict["content"] + state_key = event_dict["state_key"] + + self.get_success( + event_injection.inject_event( + hs, + room_version=RoomVersions.MSC2403.identifier, + room_id=room_id, + sender=sender, + type=event_type, + state_key=state_key, + content=event_content, + ) + ) + + # Finally, we expect to see the m.room.create event of the room as part of the + # stripped state. We don't need to inject this event though. + room_state[EventTypes.Create] = { + "content": { + "creator": sender, + "room_version": RoomVersions.MSC2403.identifier, + }, + "state_key": "", + } + + return room_state + + def check_knock_room_state_against_room_state( + self, + knock_room_state: List[Dict], + expected_room_state: Dict, + ) -> None: + """Test a list of stripped room state events received over federation against a + dict of expected state events. + + Args: + knock_room_state: The list of room state that was received over federation. + expected_room_state: A dict containing the room state we expect to see in + `knock_room_state`. + """ + for event in knock_room_state: + event_type = event["type"] + + # Check that this event type is one of those that we expected. + # Note: This will also check that no excess state was included + self.assertIn(event_type, expected_room_state) + + # Check the state content matches + self.assertEquals( + expected_room_state[event_type]["content"], event["content"] + ) + + # Check the state key is correct + self.assertEqual( + expected_room_state[event_type]["state_key"], event["state_key"] + ) + + # Ensure the event has been stripped + self.assertNotIn("signatures", event) + + # Pop once we've found and processed a state event + expected_room_state.pop(event_type) + + # Check that all expected state events were accounted for + self.assertEqual(len(expected_room_state), 0) + + +class FederationKnockingTestCase( + FederatingHomeserverTestCase, KnockingStrippedStateEventHelperMixin +): + servlets = [ + admin.register_servlets, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, homeserver): + self.store = homeserver.get_datastore() + + # We're not going to be properly signing events as our remote homeserver is fake, + # therefore disable event signature checks. + # Note that these checks are not relevant to this test case. + + # Have this homeserver auto-approve all event signature checking. + async def approve_all_signature_checking(_, pdu): + return pdu + + homeserver.get_federation_server()._check_sigs_and_hash = ( + approve_all_signature_checking + ) + + # Have this homeserver skip event auth checks. This is necessary due to + # event auth checks ensuring that events were signed by the sender's homeserver. + async def _check_event_auth( + origin, event, context, state, auth_events, backfilled + ): + return context + + homeserver.get_federation_handler()._check_event_auth = _check_event_auth + + return super().prepare(reactor, clock, homeserver) + + @override_config({"experimental_features": {"msc2403_enabled": True}}) + def test_room_state_returned_when_knocking(self): + """ + Tests that specific, stripped state events from a room are returned after + a remote homeserver successfully knocks on a local room. + """ + user_id = self.register_user("u1", "you the one") + user_token = self.login("u1", "you the one") + + fake_knocking_user_id = "@user:other.example.com" + + # Create a room with a room version that includes knocking + room_id = self.helper.create_room_as( + "u1", + is_public=False, + room_version=RoomVersions.MSC2403.identifier, + tok=user_token, + ) + + # Update the join rules and add additional state to the room to check for later + expected_room_state = self.send_example_state_events_to_room( + self.hs, room_id, user_id + ) + + channel = self.make_request( + "GET", + "/_matrix/federation/unstable/%s/make_knock/%s/%s?ver=%s" + % ( + KNOCK_UNSTABLE_IDENTIFIER, + room_id, + fake_knocking_user_id, + # Inform the remote that we support the room version of the room we're + # knocking on + RoomVersions.MSC2403.identifier, + ), + ) + self.assertEquals(200, channel.code, channel.result) + + # Note: We don't expect the knock membership event to be sent over federation as + # part of the stripped room state, as the knocking homeserver already has that + # event. It is only done for clients during /sync + + # Extract the generated knock event json + knock_event = channel.json_body["event"] + + # Check that the event has things we expect in it + self.assertEquals(knock_event["room_id"], room_id) + self.assertEquals(knock_event["sender"], fake_knocking_user_id) + self.assertEquals(knock_event["state_key"], fake_knocking_user_id) + self.assertEquals(knock_event["type"], EventTypes.Member) + self.assertEquals(knock_event["content"]["membership"], Membership.KNOCK) + + # Turn the event json dict into a proper event. + # We won't sign it properly, but that's OK as we stub out event auth in `prepare` + signed_knock_event = builder.create_local_event_from_event_dict( + self.clock, + self.hs.hostname, + self.hs.signing_key, + room_version=RoomVersions.MSC2403, + event_dict=knock_event, + ) + + # Convert our proper event back to json dict format + signed_knock_event_json = signed_knock_event.get_pdu_json( + self.clock.time_msec() + ) + + # Send the signed knock event into the room + channel = self.make_request( + "PUT", + "/_matrix/federation/unstable/%s/send_knock/%s/%s" + % (KNOCK_UNSTABLE_IDENTIFIER, room_id, signed_knock_event.event_id), + signed_knock_event_json, + ) + self.assertEquals(200, channel.code, channel.result) + + # Check that we got the stripped room state in return + room_state_events = channel.json_body["knock_state_events"] + + # Validate the stripped room state events + self.check_knock_room_state_against_room_state( + room_state_events, expected_room_state + ) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index dbcbdf159a..be5737e420 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -17,10 +17,14 @@ import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, RelationTypes from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import read_marker, sync +from synapse.rest.client.v2_alpha import knock, read_marker, sync from tests import unittest +from tests.federation.transport.test_knocking import ( + KnockingStrippedStateEventHelperMixin, +) from tests.server import TimedOutException +from tests.unittest import override_config class FilterTestCase(unittest.HomeserverTestCase): @@ -305,6 +309,93 @@ def test_sync_backwards_typing(self): self.make_request("GET", sync_url % (access_token, next_batch)) +class SyncKnockTestCase( + unittest.HomeserverTestCase, KnockingStrippedStateEventHelperMixin +): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + sync.register_servlets, + knock.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store = hs.get_datastore() + self.url = "/sync?since=%s" + self.next_batch = "s0" + + # Register the first user (used to create the room to knock on). + self.user_id = self.register_user("kermit", "monkey") + self.tok = self.login("kermit", "monkey") + + # Create the room we'll knock on. + self.room_id = self.helper.create_room_as( + self.user_id, + is_public=False, + room_version="xyz.amorgan.knock", + tok=self.tok, + ) + + # Register the second user (used to knock on the room). + self.knocker = self.register_user("knocker", "monkey") + self.knocker_tok = self.login("knocker", "monkey") + + # Perform an initial sync for the knocking user. + channel = self.make_request( + "GET", + self.url % self.next_batch, + access_token=self.tok, + ) + self.assertEqual(channel.code, 200, channel.json_body) + + # Store the next batch for the next request. + self.next_batch = channel.json_body["next_batch"] + + # Set up some room state to test with. + self.expected_room_state = self.send_example_state_events_to_room( + hs, self.room_id, self.user_id + ) + + @override_config({"experimental_features": {"msc2403_enabled": True}}) + def test_knock_room_state(self): + """Tests that /sync returns state from a room after knocking on it.""" + # Knock on a room + channel = self.make_request( + "POST", + "/_matrix/client/unstable/xyz.amorgan.knock/%s" % (self.room_id,), + b"{}", + self.knocker_tok, + ) + self.assertEquals(200, channel.code, channel.result) + + # We expect to see the knock event in the stripped room state later + self.expected_room_state[EventTypes.Member] = { + "content": {"membership": "xyz.amorgan.knock", "displayname": "knocker"}, + "state_key": "@knocker:test", + } + + # Check that /sync includes stripped state from the room + channel = self.make_request( + "GET", + self.url % self.next_batch, + access_token=self.knocker_tok, + ) + self.assertEqual(channel.code, 200, channel.json_body) + + # Extract the stripped room state events from /sync + knock_entry = channel.json_body["rooms"]["xyz.amorgan.knock"] + room_state_events = knock_entry[self.room_id]["knock_state"]["events"] + + # Validate that the knock membership event came last + self.assertEqual(room_state_events[-1]["type"], EventTypes.Member) + + # Validate the stripped room state events + self.check_knock_room_state_against_room_state( + room_state_events, self.expected_room_state + ) + + class UnreadMessagesTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets, @@ -447,7 +538,7 @@ def test_unread_counts(self): ) self._check_unread_count(5) - def _check_unread_count(self, expected_count: True): + def _check_unread_count(self, expected_count: int): """Syncs and compares the unread count with the expected value.""" channel = self.make_request( From a7a37437bc0364d6cde93f3ec264e06ed6324068 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 9 Jun 2021 20:31:31 +0100 Subject: [PATCH 260/619] Integrate knock rooms with the public rooms directory (#9359) This PR implements the ["Changes regarding the Public Rooms Directory"](https://github.com/Sorunome/matrix-doc/blob/soru/knock/proposals/2403-knock.md#changes-regarding-the-public-rooms-directory) section of knocking MSC2403. Specifically, it: * Allows rooms with `join_rule` "knock" to be returned by the query behind the public rooms directory * Adds the field `join_rule` to each room entry returned by a public rooms directory query, so clients can know whether to attempt a join or knock on a room Based on https://github.com/matrix-org/synapse/issues/6739. Complement tests for this change: https://github.com/matrix-org/complement/pull/72 --- changelog.d/9359.feature | 1 + synapse/handlers/room_list.py | 1 + synapse/storage/databases/main/room.py | 14 +++++++++----- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 changelog.d/9359.feature diff --git a/changelog.d/9359.feature b/changelog.d/9359.feature new file mode 100644 index 0000000000..9c41140194 --- /dev/null +++ b/changelog.d/9359.feature @@ -0,0 +1 @@ +Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index 0a26088d32..5e3ef7ce3a 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -169,6 +169,7 @@ def build_room_entry(room): "world_readable": room["history_visibility"] == HistoryVisibility.WORLD_READABLE, "guest_can_join": room["guest_access"] == "can_join", + "join_rule": room["join_rules"], } # Filter out Nones – rather omit the field altogether diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 2a96bcd314..9f0d64a325 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -19,7 +19,7 @@ from enum import Enum from typing import Any, Dict, List, Optional, Tuple -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, JoinRules from synapse.api.errors import StoreError from synapse.api.room_versions import RoomVersion, RoomVersions from synapse.storage._base import SQLBaseStore, db_to_json @@ -177,11 +177,13 @@ def _count_public_rooms_txn(txn): INNER JOIN room_stats_current USING (room_id) WHERE ( - join_rules = 'public' OR history_visibility = 'world_readable' + join_rules = 'public' OR join_rules = '%(knock_join_rule)s' + OR history_visibility = 'world_readable' ) AND joined_members > 0 """ % { - "published_sql": published_sql + "published_sql": published_sql, + "knock_join_rule": JoinRules.KNOCK, } txn.execute(sql, query_args) @@ -303,7 +305,7 @@ async def get_largest_public_rooms( sql = """ SELECT room_id, name, topic, canonical_alias, joined_members, - avatar, history_visibility, joined_members, guest_access + avatar, history_visibility, guest_access, join_rules FROM ( %(published_sql)s ) published @@ -311,7 +313,8 @@ async def get_largest_public_rooms( INNER JOIN room_stats_current USING (room_id) WHERE ( - join_rules = 'public' OR history_visibility = 'world_readable' + join_rules = 'public' OR join_rules = '%(knock_join_rule)s' + OR history_visibility = 'world_readable' ) AND joined_members > 0 %(where_clause)s @@ -320,6 +323,7 @@ async def get_largest_public_rooms( "published_sql": published_sql, "where_clause": where_clause, "dir": "DESC" if forwards else "ASC", + "knock_join_rule": JoinRules.KNOCK, } if limit is not None: From aec2cf1c9832eac46e1d94a39e75490b02320555 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 9 Jun 2021 20:59:40 +0100 Subject: [PATCH 261/619] Update Complement run with Synapse-supported MSC-related build tags (#10155) This PR updates the build tags that we perform Complement runs with to match our [buildkite pipeline](https://github.com/matrix-org/pipelines/blob/618b3e90bcae8efd1a71502ae95b7913e6e24665/synapse/pipeline.yml#L570), as well as adding `msc2403` (as it will be required once #9359 is merged). Build tags are what we use to determine which tests to run in Complement (really it determines which test files are compiled into the final binary). I haven't put in a comment about updating the buildkite side here, as we've decided to migrate fully to GitHub Actions anyhow. --- .github/workflows/tests.yml | 2 +- changelog.d/10155.misc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10155.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 955beb4aa0..7c2f7d4b13 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -322,7 +322,7 @@ jobs: working-directory: complement/dockerfiles # Run Complement - - run: go test -v -tags synapse_blacklist ./tests + - run: go test -v -tags synapse_blacklist,msc2403,msc2946,msc3083 ./tests env: COMPLEMENT_BASE_IMAGE: complement-synapse:latest working-directory: complement diff --git a/changelog.d/10155.misc b/changelog.d/10155.misc new file mode 100644 index 0000000000..27b98e7fed --- /dev/null +++ b/changelog.d/10155.misc @@ -0,0 +1 @@ +Update the Complement build tags in GitHub Actions to test currently experimental features. \ No newline at end of file From e6245e6d48bcb0a1d426b73d010988e0f2d92b35 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Thu, 10 Jun 2021 05:40:24 -0500 Subject: [PATCH 262/619] Mention that you need to configure max upload size in reverse proxy as well (#10122) Signed-off-by: Aaron Raimist --- changelog.d/10122.doc | 1 + docs/sample_config.yaml | 4 ++++ synapse/config/repository.py | 4 ++++ 3 files changed, 9 insertions(+) create mode 100644 changelog.d/10122.doc diff --git a/changelog.d/10122.doc b/changelog.d/10122.doc new file mode 100644 index 0000000000..07a0d2520d --- /dev/null +++ b/changelog.d/10122.doc @@ -0,0 +1 @@ +Mention in the sample homeserver config that you may need to configure max upload size in your reverse proxy. Contributed by @aaronraimist. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 7b97f73a29..f8925a5e24 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -954,6 +954,10 @@ media_store_path: "DATADIR/media_store" # The largest allowed upload size in bytes # +# If you are using a reverse proxy you may also need to set this value in +# your reverse proxy's config. Notably Nginx has a small max body size by default. +# See https://matrix-org.github.io/synapse/develop/reverse_proxy.html. +# #max_upload_size: 50M # Maximum number of pixels that will be thumbnailed diff --git a/synapse/config/repository.py b/synapse/config/repository.py index c78a83abe1..2f77d6703d 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -248,6 +248,10 @@ def generate_config_section(self, data_dir_path, **kwargs): # The largest allowed upload size in bytes # + # If you are using a reverse proxy you may also need to set this value in + # your reverse proxy's config. Notably Nginx has a small max body size by default. + # See https://matrix-org.github.io/synapse/develop/reverse_proxy.html. + # #max_upload_size: 50M # Maximum number of pixels that will be thumbnailed From e21c3473324116e4a25346991aca08e3207778e1 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 11 Jun 2021 03:57:34 -0500 Subject: [PATCH 263/619] Document how to see logger output when running the twisted tests (#10148) --- CONTRIBUTING.md | 9 ++++++++- README.rst | 29 +++++++++++++++++------------ changelog.d/10148.misc | 1 + 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 changelog.d/10148.misc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6a70f7ffe..a4e6688042 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -173,12 +173,19 @@ source ./env/bin/activate trial tests.rest.admin.test_room tests.handlers.test_admin.ExfiltrateData.test_invite ``` -If your tests fail, you may wish to look at the logs: +If your tests fail, you may wish to look at the logs (the default log level is `ERROR`): ```sh less _trial_temp/test.log ``` +To increase the log level for the tests, set `SYNAPSE_TEST_LOG_LEVEL`: + +```sh +SYNAPSE_TEST_LOG_LEVEL=DEBUG trial tests +``` + + ## Run the integration tests. The integration tests are a more comprehensive suite of tests. They diff --git a/README.rst b/README.rst index a14a687fd1..1c9f05cc85 100644 --- a/README.rst +++ b/README.rst @@ -293,18 +293,6 @@ try installing the failing modules individually:: pip install -e "module-name" -Once this is done, you may wish to run Synapse's unit tests to -check that everything is installed correctly:: - - python -m twisted.trial tests - -This should end with a 'PASSED' result (note that exact numbers will -differ):: - - Ran 1337 tests in 716.064s - - PASSED (skips=15, successes=1322) - We recommend using the demo which starts 3 federated instances running on ports `8080` - `8082` ./demo/start.sh @@ -324,6 +312,23 @@ If you just want to start a single instance of the app and run it directly:: python -m synapse.app.homeserver --config-path homeserver.yaml +Running the unit tests +====================== + +After getting up and running, you may wish to run Synapse's unit tests to +check that everything is installed correctly:: + + trial tests + +This should end with a 'PASSED' result (note that exact numbers will +differ):: + + Ran 1337 tests in 716.064s + + PASSED (skips=15, successes=1322) + +For more tips on running the unit tests, like running a specific test or +to see the logging output, see the `CONTRIBUTING doc `_. Running the Integration Tests diff --git a/changelog.d/10148.misc b/changelog.d/10148.misc new file mode 100644 index 0000000000..5066392d40 --- /dev/null +++ b/changelog.d/10148.misc @@ -0,0 +1 @@ +Document `SYNAPSE_TEST_LOG_LEVEL` to see the logger output when running tests. From b31daac01c9cc757d7c73da2f23e1b7251c54b79 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 11 Jun 2021 04:12:35 -0500 Subject: [PATCH 264/619] Add metrics to track how often events are `soft_failed` (#10156) Spawned from missing messages we were seeing on `matrix.org` from a federated Gtiter bridged room, https://gitlab.com/gitterHQ/webapp/-/issues/2770. The underlying issue in Synapse is tracked by https://github.com/matrix-org/synapse/issues/10066 where the message and join event race and the message is `soft_failed` before the `join` event reaches the remote federated server. Less soft_failed events = better and usually this should only trigger for events where people are doing bad things and trying to fuzz and fake everything. --- changelog.d/10156.misc | 1 + synapse/handlers/federation.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 changelog.d/10156.misc diff --git a/changelog.d/10156.misc b/changelog.d/10156.misc new file mode 100644 index 0000000000..92a188b87b --- /dev/null +++ b/changelog.d/10156.misc @@ -0,0 +1 @@ +Add `synapse_federation_soft_failed_events_total` metric to track how often events are soft failed. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 6e40e2c216..6647063485 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -33,6 +33,7 @@ ) import attr +from prometheus_client import Counter from signedjson.key import decode_verify_key_bytes from signedjson.sign import verify_signed_json from unpaddedbase64 import decode_base64 @@ -101,6 +102,11 @@ logger = logging.getLogger(__name__) +soft_failed_event_counter = Counter( + "synapse_federation_soft_failed_events_total", + "Events received over federation that we marked as soft_failed", +) + @attr.s(slots=True) class _NewEventInfo: @@ -2498,6 +2504,7 @@ async def _check_for_soft_fail( event_auth.check(room_version_obj, event, auth_events=current_auth_events) except AuthError as e: logger.warning("Soft-failing %r because %s", event, e) + soft_failed_event_counter.inc() event.internal_metadata.soft_failed = True async def on_get_missing_events( From d26d15ba3d63867d777070185b96d06c2b3646f0 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 10:27:12 +0100 Subject: [PATCH 265/619] Fix bug when running presence off master (#10149) Hopefully fixes #10027. --- changelog.d/10149.bugfix | 1 + synapse/storage/databases/main/presence.py | 2 +- synapse/storage/util/id_generators.py | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10149.bugfix diff --git a/changelog.d/10149.bugfix b/changelog.d/10149.bugfix new file mode 100644 index 0000000000..cb2d2eedb3 --- /dev/null +++ b/changelog.d/10149.bugfix @@ -0,0 +1 @@ +Fix a bug which caused presence updates to stop working some time after restart, when using a presence writer worker. diff --git a/synapse/storage/databases/main/presence.py b/synapse/storage/databases/main/presence.py index 6a2baa7841..1388771c40 100644 --- a/synapse/storage/databases/main/presence.py +++ b/synapse/storage/databases/main/presence.py @@ -50,7 +50,7 @@ def __init__( instance_name=self._instance_name, tables=[("presence_stream", "instance_name", "stream_id")], sequence_name="presence_stream_sequence", - writers=hs.config.worker.writers.to_device, + writers=hs.config.worker.writers.presence, ) else: self._presence_id_gen = StreamIdGenerator( diff --git a/synapse/storage/util/id_generators.py b/synapse/storage/util/id_generators.py index b1bd3a52d9..f1e62f9e85 100644 --- a/synapse/storage/util/id_generators.py +++ b/synapse/storage/util/id_generators.py @@ -397,6 +397,11 @@ def get_next(self): # ... persist event ... """ + # If we have a list of instances that are allowed to write to this + # stream, make sure we're in it. + if self._writers and self._instance_name not in self._writers: + raise Exception("Tried to allocate stream ID on non-writer") + return _MultiWriterCtxManager(self) def get_next_mult(self, n: int): @@ -406,6 +411,11 @@ def get_next_mult(self, n: int): # ... persist events ... """ + # If we have a list of instances that are allowed to write to this + # stream, make sure we're in it. + if self._writers and self._instance_name not in self._writers: + raise Exception("Tried to allocate stream ID on non-writer") + return _MultiWriterCtxManager(self, n) def get_next_txn(self, txn: LoggingTransaction): @@ -416,6 +426,11 @@ def get_next_txn(self, txn: LoggingTransaction): # ... persist event ... """ + # If we have a list of instances that are allowed to write to this + # stream, make sure we're in it. + if self._writers and self._instance_name not in self._writers: + raise Exception("Tried to allocate stream ID on non-writer") + next_id = self._load_next_id_txn(txn) with self._lock: From a15a046c9302775cef52a85a8cdfd067e716ab67 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 11 Jun 2021 11:34:40 +0100 Subject: [PATCH 266/619] Clean up a broken import in admin_cmd.py (#10154) --- changelog.d/10154.bugfix | 1 + synapse/app/admin_cmd.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog.d/10154.bugfix diff --git a/changelog.d/10154.bugfix b/changelog.d/10154.bugfix new file mode 100644 index 0000000000..f70a3d47bc --- /dev/null +++ b/changelog.d/10154.bugfix @@ -0,0 +1 @@ +Remove a broken import line in Synapse's admin_cmd worker. Broke in 1.33.0. \ No newline at end of file diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 68ae19c977..2878d2c140 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -36,7 +36,6 @@ from synapse.replication.slave.storage.events import SlavedEventStore from synapse.replication.slave.storage.filtering import SlavedFilteringStore from synapse.replication.slave.storage.groups import SlavedGroupServerStore -from synapse.replication.slave.storage.presence import SlavedPresenceStore from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore @@ -54,7 +53,6 @@ class AdminCmdSlavedStore( SlavedApplicationServiceStore, SlavedRegistrationStore, SlavedFilteringStore, - SlavedPresenceStore, SlavedGroupServerStore, SlavedDeviceInboxStore, SlavedDeviceStore, From c8dd4db9eba5335924046a27c1156f0e18862bdd Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 13:08:30 +0100 Subject: [PATCH 267/619] Fix sending presence over federation when using workers (#10163) When using a federation sender we'd send out all local presence updates over federation even when they shouldn't be. Fixes #10153. --- changelog.d/10163.bugfix | 1 + synapse/handlers/presence.py | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10163.bugfix diff --git a/changelog.d/10163.bugfix b/changelog.d/10163.bugfix new file mode 100644 index 0000000000..7ccde66743 --- /dev/null +++ b/changelog.d/10163.bugfix @@ -0,0 +1 @@ +Fix a bug when using federation sender worker where it would send out more presence updates than necessary, leading to high resource usage. Broke in v1.33.0. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index f5a049d754..79508580ac 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -495,9 +495,6 @@ async def notify_from_replication( users=users_to_states.keys(), ) - # If this is a federation sender, notify about presence updates. - await self.maybe_send_presence_to_interested_destinations(states) - async def process_replication_rows( self, stream_name: str, instance_name: str, token: int, rows: list ): @@ -519,11 +516,27 @@ async def process_replication_rows( for row in rows ] - for state in states: - self.user_to_current_state[state.user_id] = state + # The list of states to notify sync streams and remote servers about. + # This is calculated by comparing the old and new states for each user + # using `should_notify(..)`. + # + # Note that this is necessary as the presence writer will periodically + # flush presence state changes that should not be notified about to the + # DB, and so will be sent over the replication stream. + state_to_notify = [] + + for new_state in states: + old_state = self.user_to_current_state.get(new_state.user_id) + self.user_to_current_state[new_state.user_id] = new_state + + if not old_state or should_notify(old_state, new_state): + state_to_notify.append(new_state) stream_id = token - await self.notify_from_replication(states, stream_id) + await self.notify_from_replication(state_to_notify, stream_id) + + # If this is a federation sender, notify about presence updates. + await self.maybe_send_presence_to_interested_destinations(state_to_notify) def get_currently_syncing_users_for_replication(self) -> Iterable[str]: return [ From a14884fbb050f5069b83e344e4ef12a54fe42111 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 11 Jun 2021 08:17:17 -0400 Subject: [PATCH 268/619] Use the matching complement branch when running tests in CI. (#10160) This implements similar behavior to sytest where a matching branch is used, if one exists. This is useful when needing to modify both application code and tests at the same time. The following rules are used to find a matching complement branch: 1. Search for the branch name of the pull request. (E.g. feature/foo.) 2. Search for the base branch of the pull request. (E.g. develop or release-vX.Y.) 3. Search for the reference branch of the commit. (E.g. master or release-vX.Y.) 4. Fallback to 'master', the default complement branch name. --- .github/workflows/tests.yml | 28 +++++++++++++++++++++++----- changelog.d/10160.misc | 1 + 2 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10160.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7c2f7d4b13..bf36ee1cdf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -305,11 +305,29 @@ jobs: with: path: synapse - - name: Run actions/checkout@v2 for complement - uses: actions/checkout@v2 - with: - repository: "matrix-org/complement" - path: complement + # Attempt to check out the same branch of Complement as the PR. If it + # doesn't exist, fallback to master. + - name: Checkout complement + shell: bash + run: | + mkdir -p complement + # Attempt to use the version of complement which best matches the current + # build. Depending on whether this is a PR or release, etc. we need to + # use different fallbacks. + # + # 1. First check if there's a similarly named branch (GITHUB_HEAD_REF + # for pull requests, otherwise GITHUB_REF). + # 2. Attempt to use the base branch, e.g. when merging into release-vX.Y + # (GITHUB_BASE_REF for pull requests). + # 3. Use the default complement branch ("master"). + for BRANCH_NAME in "$GITHUB_HEAD_REF" "$GITHUB_BASE_REF" "${GITHUB_REF#refs/heads/}" "master"; do + # Skip empty branch names and merge commits. + if [[ -z "$BRANCH_NAME" || $BRANCH_NAME =~ ^refs/pull/.* ]]; then + continue + fi + + (wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break + done # Build initial Synapse image - run: docker build -t matrixdotorg/synapse:latest -f docker/Dockerfile . diff --git a/changelog.d/10160.misc b/changelog.d/10160.misc new file mode 100644 index 0000000000..80f378130f --- /dev/null +++ b/changelog.d/10160.misc @@ -0,0 +1 @@ +Fetch the corresponding complement branch when performing CI. From c1b9922498dea4b2882d26a4eaef3e0a37e727fd Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 11 Jun 2021 14:45:53 +0100 Subject: [PATCH 269/619] Support for database schema version ranges (#9933) This is essentially an implementation of the proposal made at https://hackmd.io/@richvdh/BJYXQMQHO, though the details have ended up looking slightly different. --- changelog.d/9933.misc | 1 + docs/SUMMARY.md | 3 +- docs/development/database_schema.md | 95 ++++++++++++++ synapse/storage/prepare_database.py | 121 ++++++++++++------ synapse/storage/schema/README.md | 37 +----- synapse/storage/schema/__init__.py | 19 ++- .../storage/schema/common/schema_version.sql | 7 + 7 files changed, 206 insertions(+), 77 deletions(-) create mode 100644 changelog.d/9933.misc create mode 100644 docs/development/database_schema.md diff --git a/changelog.d/9933.misc b/changelog.d/9933.misc new file mode 100644 index 0000000000..0860026670 --- /dev/null +++ b/changelog.d/9933.misc @@ -0,0 +1 @@ +Update the database schema versioning to support gradual migration away from legacy tables. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8f39ae0270..af2c968c9a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -69,6 +69,7 @@ - [Git Usage](dev/git.md) - [Testing]() - [OpenTracing](opentracing.md) + - [Database Schemas](development/database_schema.md) - [Synapse Architecture]() - [Log Contexts](log_contexts.md) - [Replication](replication.md) @@ -84,4 +85,4 @@ - [Scripts]() # Other - - [Dependency Deprecation Policy](deprecation_policy.md) \ No newline at end of file + - [Dependency Deprecation Policy](deprecation_policy.md) diff --git a/docs/development/database_schema.md b/docs/development/database_schema.md new file mode 100644 index 0000000000..7fe8ec63e1 --- /dev/null +++ b/docs/development/database_schema.md @@ -0,0 +1,95 @@ +# Synapse database schema files + +Synapse's database schema is stored in the `synapse.storage.schema` module. + +## Logical databases + +Synapse supports splitting its datastore across multiple physical databases (which can +be useful for large installations), and the schema files are therefore split according +to the logical database they apply to. + +At the time of writing, the following "logical" databases are supported: + +* `state` - used to store Matrix room state (more specifically, `state_groups`, + their relationships and contents). +* `main` - stores everything else. + +Additionally, the `common` directory contains schema files for tables which must be +present on *all* physical databases. + +## Synapse schema versions + +Synapse manages its database schema via "schema versions". These are mainly used to +help avoid confusion if the Synapse codebase is rolled back after the database is +updated. They work as follows: + + * The Synapse codebase defines a constant `synapse.storage.schema.SCHEMA_VERSION` + which represents the expectations made about the database by that version. For + example, as of Synapse v1.36, this is `59`. + + * The database stores a "compatibility version" in + `schema_compat_version.compat_version` which defines the `SCHEMA_VERSION` of the + oldest version of Synapse which will work with the database. On startup, if + `compat_version` is found to be newer than `SCHEMA_VERSION`, Synapse will refuse to + start. + + Synapse automatically updates this field from + `synapse.storage.schema.SCHEMA_COMPAT_VERSION`. + + * Whenever a backwards-incompatible change is made to the database format (normally + via a `delta` file), `synapse.storage.schema.SCHEMA_COMPAT_VERSION` is also updated + so that administrators can not accidentally roll back to a too-old version of Synapse. + +Generally, the goal is to maintain compatibility with at least one or two previous +releases of Synapse, so any substantial change tends to require multiple releases and a +bit of forward-planning to get right. + +As a worked example: we want to remove the `room_stats_historical` table. Here is how it +might pan out. + + 1. Replace any code that *reads* from `room_stats_historical` with alternative + implementations, but keep writing to it in case of rollback to an earlier version. + Also, increase `synapse.storage.schema.SCHEMA_VERSION`. In this + instance, there is no existing code which reads from `room_stats_historical`, so + our starting point is: + + v1.36.0: `SCHEMA_VERSION=59`, `SCHEMA_COMPAT_VERSION=59` + + 2. Next (say in Synapse v1.37.0): remove the code that *writes* to + `room_stats_historical`, but don’t yet remove the table in case of rollback to + v1.36.0. Again, we increase `synapse.storage.schema.SCHEMA_VERSION`, but + because we have not broken compatibility with v1.36, we do not yet update + `SCHEMA_COMPAT_VERSION`. We now have: + + v1.37.0: `SCHEMA_VERSION=60`, `SCHEMA_COMPAT_VERSION=59`. + + 3. Later (say in Synapse v1.38.0): we can remove the table altogether. This will + break compatibility with v1.36.0, so we must update `SCHEMA_COMPAT_VERSION` accordingly. + There is no need to update `synapse.storage.schema.SCHEMA_VERSION`, since there is no + change to the Synapse codebase here. So we end up with: + + v1.38.0: `SCHEMA_VERSION=60`, `SCHEMA_COMPAT_VERSION=60`. + +If in doubt about whether to update `SCHEMA_VERSION` or not, it is generally best to +lean towards doing so. + +## Full schema dumps + +In the `full_schemas` directories, only the most recently-numbered snapshot is used +(`54` at the time of writing). Older snapshots (eg, `16`) are present for historical +reference only. + +### Building full schema dumps + +If you want to recreate these schemas, they need to be made from a database that +has had all background updates run. + +To do so, use `scripts-dev/make_full_schema.sh`. This will produce new +`full.sql.postgres` and `full.sql.sqlite` files. + +Ensure postgres is installed, then run: + + ./scripts-dev/make_full_schema.sh -p postgres_username -o output_dir/ + +NB at the time of writing, this script predates the split into separate `state`/`main` +databases so will require updates to handle that correctly. diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 3799d46734..683e5e3b90 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -1,5 +1,4 @@ -# Copyright 2014 - 2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2014 - 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,7 +25,7 @@ from synapse.storage.database import LoggingDatabaseConnection from synapse.storage.engines import BaseDatabaseEngine from synapse.storage.engines.postgres import PostgresEngine -from synapse.storage.schema import SCHEMA_VERSION +from synapse.storage.schema import SCHEMA_COMPAT_VERSION, SCHEMA_VERSION from synapse.storage.types import Cursor logger = logging.getLogger(__name__) @@ -59,6 +58,28 @@ class UpgradeDatabaseException(PrepareDatabaseException): ) +@attr.s +class _SchemaState: + current_version: int = attr.ib() + """The current schema version of the database""" + + compat_version: Optional[int] = attr.ib() + """The SCHEMA_VERSION of the oldest version of Synapse for this database + + If this is None, we have an old version of the database without the necessary + table. + """ + + applied_deltas: Collection[str] = attr.ib(factory=tuple) + """Any delta files for `current_version` which have already been applied""" + + upgraded: bool = attr.ib(default=False) + """Whether the current state was reached by applying deltas. + + If False, we have run the full schema for `current_version`, and have applied no + deltas since. If True, we have run some deltas since the original creation.""" + + def prepare_database( db_conn: LoggingDatabaseConnection, database_engine: BaseDatabaseEngine, @@ -96,12 +117,11 @@ def prepare_database( version_info = _get_or_create_schema_state(cur, database_engine) if version_info: - user_version, delta_files, upgraded = version_info logger.info( "%r: Existing schema is %i (+%i deltas)", databases, - user_version, - len(delta_files), + version_info.current_version, + len(version_info.applied_deltas), ) # config should only be None when we are preparing an in-memory SQLite db, @@ -113,16 +133,18 @@ def prepare_database( # if it's a worker app, refuse to upgrade the database, to avoid multiple # workers doing it at once. - if config.worker_app is not None and user_version != SCHEMA_VERSION: + if ( + config.worker_app is not None + and version_info.current_version != SCHEMA_VERSION + ): raise UpgradeDatabaseException( - OUTDATED_SCHEMA_ON_WORKER_ERROR % (SCHEMA_VERSION, user_version) + OUTDATED_SCHEMA_ON_WORKER_ERROR + % (SCHEMA_VERSION, version_info.current_version) ) _upgrade_existing_database( cur, - user_version, - delta_files, - upgraded, + version_info, database_engine, config, databases=databases, @@ -261,9 +283,7 @@ def _setup_new_database( _upgrade_existing_database( cur, - current_version=max_current_ver, - applied_delta_files=[], - upgraded=False, + _SchemaState(current_version=max_current_ver, compat_version=None), database_engine=database_engine, config=None, databases=databases, @@ -273,9 +293,7 @@ def _setup_new_database( def _upgrade_existing_database( cur: Cursor, - current_version: int, - applied_delta_files: List[str], - upgraded: bool, + current_schema_state: _SchemaState, database_engine: BaseDatabaseEngine, config: Optional[HomeServerConfig], databases: Collection[str], @@ -321,12 +339,8 @@ def _upgrade_existing_database( Args: cur - current_version: The current version of the schema. - applied_delta_files: A list of deltas that have already been applied. - upgraded: Whether the current version was generated by having - applied deltas or from full schema file. If `True` the function - will never apply delta files for the given `current_version`, since - the current_version wasn't generated by applying those delta files. + current_schema_state: The current version of the schema, as + returned by _get_or_create_schema_state database_engine config: None if we are initialising a blank database, otherwise the application @@ -337,13 +351,16 @@ def _upgrade_existing_database( upgrade portions of the delta scripts. """ if is_empty: - assert not applied_delta_files + assert not current_schema_state.applied_deltas else: assert config is_worker = config and config.worker_app is not None - if current_version > SCHEMA_VERSION: + if ( + current_schema_state.compat_version is not None + and current_schema_state.compat_version > SCHEMA_VERSION + ): raise ValueError( "Cannot use this database as it is too " + "new for the server to understand" @@ -357,14 +374,26 @@ def _upgrade_existing_database( assert config is not None check_database_before_upgrade(cur, database_engine, config) - start_ver = current_version + # update schema_compat_version before we run any upgrades, so that if synapse + # gets downgraded again, it won't try to run against the upgraded database. + if ( + current_schema_state.compat_version is None + or current_schema_state.compat_version < SCHEMA_COMPAT_VERSION + ): + cur.execute("DELETE FROM schema_compat_version") + cur.execute( + "INSERT INTO schema_compat_version(compat_version) VALUES (?)", + (SCHEMA_COMPAT_VERSION,), + ) + + start_ver = current_schema_state.current_version # if we got to this schema version by running a full_schema rather than a series # of deltas, we should not run the deltas for this version. - if not upgraded: + if not current_schema_state.upgraded: start_ver += 1 - logger.debug("applied_delta_files: %s", applied_delta_files) + logger.debug("applied_delta_files: %s", current_schema_state.applied_deltas) if isinstance(database_engine, PostgresEngine): specific_engine_extension = ".postgres" @@ -440,7 +469,7 @@ def _upgrade_existing_database( absolute_path = entry.absolute_path logger.debug("Found file: %s (%s)", relative_path, absolute_path) - if relative_path in applied_delta_files: + if relative_path in current_schema_state.applied_deltas: continue root_name, ext = os.path.splitext(file_name) @@ -621,7 +650,7 @@ def execute_statements_from_stream(cur: Cursor, f: TextIO) -> None: def _get_or_create_schema_state( txn: Cursor, database_engine: BaseDatabaseEngine -) -> Optional[Tuple[int, List[str], bool]]: +) -> Optional[_SchemaState]: # Bluntly try creating the schema_version tables. sql_path = os.path.join(schema_path, "common", "schema_version.sql") executescript(txn, sql_path) @@ -629,17 +658,31 @@ def _get_or_create_schema_state( txn.execute("SELECT version, upgraded FROM schema_version") row = txn.fetchone() + if row is None: + # new database + return None + + current_version = int(row[0]) + upgraded = bool(row[1]) + + compat_version: Optional[int] = None + txn.execute("SELECT compat_version FROM schema_compat_version") + row = txn.fetchone() if row is not None: - current_version = int(row[0]) - txn.execute( - "SELECT file FROM applied_schema_deltas WHERE version >= ?", - (current_version,), - ) - applied_deltas = [d for d, in txn] - upgraded = bool(row[1]) - return current_version, applied_deltas, upgraded + compat_version = int(row[0]) + + txn.execute( + "SELECT file FROM applied_schema_deltas WHERE version >= ?", + (current_version,), + ) + applied_deltas = tuple(d for d, in txn) - return None + return _SchemaState( + current_version=current_version, + compat_version=compat_version, + applied_deltas=applied_deltas, + upgraded=upgraded, + ) @attr.s(slots=True) diff --git a/synapse/storage/schema/README.md b/synapse/storage/schema/README.md index 030153db64..729f44ea6c 100644 --- a/synapse/storage/schema/README.md +++ b/synapse/storage/schema/README.md @@ -1,37 +1,4 @@ # Synapse Database Schemas -This directory contains the schema files used to build Synapse databases. - -Synapse supports splitting its datastore across multiple physical databases (which can -be useful for large installations), and the schema files are therefore split according -to the logical database they are apply to. - -At the time of writing, the following "logical" databases are supported: - -* `state` - used to store Matrix room state (more specifically, `state_groups`, - their relationships and contents.) -* `main` - stores everything else. - -Addionally, the `common` directory contains schema files for tables which must be -present on *all* physical databases. - -## Full schema dumps - -In the `full_schemas` directories, only the most recently-numbered snapshot is useful -(`54` at the time of writing). Older snapshots (eg, `16`) are present for historical -reference only. - -## Building full schema dumps - -If you want to recreate these schemas, they need to be made from a database that -has had all background updates run. - -To do so, use `scripts-dev/make_full_schema.sh`. This will produce new -`full.sql.postgres` and `full.sql.sqlite` files. - -Ensure postgres is installed, then run: - - ./scripts-dev/make_full_schema.sh -p postgres_username -o output_dir/ - -NB at the time of writing, this script predates the split into separate `state`/`main` -databases so will require updates to handle that correctly. +This directory contains the schema files used to build Synapse databases. For more +information, see /docs/development/database_schema.md. diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index f0d9f23167..d36ba1d773 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -12,6 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Remember to update this number every time a change is made to database -# schema files, so the users will be informed on server restarts. SCHEMA_VERSION = 59 +"""Represents the expectations made by the codebase about the database schema + +This should be incremented whenever the codebase changes its requirements on the +shape of the database schema (even if those requirements are backwards-compatible with +older versions of Synapse). + +See `README.md `_ for more information on how this +works. +""" + + +SCHEMA_COMPAT_VERSION = 59 +"""Limit on how far the synapse codebase can be rolled back without breaking db compat + +This value is stored in the database, and checked on startup. If the value in the +database is greater than SCHEMA_VERSION, then Synapse will refuse to start. +""" diff --git a/synapse/storage/schema/common/schema_version.sql b/synapse/storage/schema/common/schema_version.sql index 42e5cb6df5..f41fde5d2d 100644 --- a/synapse/storage/schema/common/schema_version.sql +++ b/synapse/storage/schema/common/schema_version.sql @@ -20,6 +20,13 @@ CREATE TABLE IF NOT EXISTS schema_version( CHECK (Lock='X') ); +CREATE TABLE IF NOT EXISTS schema_compat_version( + Lock CHAR(1) NOT NULL DEFAULT 'X' UNIQUE, -- Makes sure this table only has one row. + -- The SCHEMA_VERSION of the oldest synapse this database can be used with + compat_version INTEGER NOT NULL, + CHECK (Lock='X') +); + CREATE TABLE IF NOT EXISTS applied_schema_deltas( version INTEGER NOT NULL, file TEXT NOT NULL, From 968f8283b4479f65975ba8f4560ce6fb568f7328 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 15:19:42 +0100 Subject: [PATCH 270/619] Only send a presence state to a destination once (#10165) It turns out that we were sending the same presence state to a remote potentially multiple times. --- changelog.d/10165.bugfix | 1 + synapse/handlers/presence.py | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 changelog.d/10165.bugfix diff --git a/changelog.d/10165.bugfix b/changelog.d/10165.bugfix new file mode 100644 index 0000000000..8b1eeff352 --- /dev/null +++ b/changelog.d/10165.bugfix @@ -0,0 +1 @@ +Fix a bug where Synapse could send the same presence update to a remote twice. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 79508580ac..44ed7a0712 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -299,14 +299,14 @@ async def maybe_send_presence_to_interested_destinations( if not states: return - hosts_and_states = await get_interested_remotes( + hosts_to_states = await get_interested_remotes( self.store, self.presence_router, states, ) - for destinations, states in hosts_and_states: - self._federation.send_presence_to_destinations(states, destinations) + for destination, host_states in hosts_to_states.items(): + self._federation.send_presence_to_destinations(host_states, [destination]) async def send_full_presence_to_users(self, user_ids: Collection[str]): """ @@ -842,15 +842,15 @@ async def _update_states( if to_federation_ping: federation_presence_out_counter.inc(len(to_federation_ping)) - hosts_and_states = await get_interested_remotes( + hosts_to_states = await get_interested_remotes( self.store, self.presence_router, list(to_federation_ping.values()), ) - for destinations, states in hosts_and_states: + for destination, states in hosts_to_states.items(): self._federation_queue.send_presence_to_destinations( - states, destinations + states, [destination] ) async def _handle_timeouts(self) -> None: @@ -1975,7 +1975,7 @@ async def get_interested_remotes( store: DataStore, presence_router: PresenceRouter, states: List[UserPresenceState], -) -> List[Tuple[Collection[str], List[UserPresenceState]]]: +) -> Dict[str, Set[UserPresenceState]]: """Given a list of presence states figure out which remote servers should be sent which. @@ -1987,11 +1987,9 @@ async def get_interested_remotes( states: A list of incoming user presence updates. Returns: - A list of 2-tuples of destinations and states, where for - each tuple the list of UserPresenceState should be sent to each - destination + A map from destinations to presence states to send to that destination. """ - hosts_and_states = [] # type: List[Tuple[Collection[str], List[UserPresenceState]]] + hosts_and_states: Dict[str, Set[UserPresenceState]] = {} # First we look up the rooms each user is in (as well as any explicit # subscriptions), then for each distinct room we look up the remote @@ -2003,11 +2001,12 @@ async def get_interested_remotes( for room_id, states in room_ids_to_states.items(): user_ids = await store.get_users_in_room(room_id) hosts = {get_domain_from_id(user_id) for user_id in user_ids} - hosts_and_states.append((hosts, states)) + for host in hosts: + hosts_and_states.setdefault(host, set()).update(states) for user_id, states in users_to_states.items(): host = get_domain_from_id(user_id) - hosts_and_states.append(([host], states)) + hosts_and_states.setdefault(host, set()).update(states) return hosts_and_states From c955f22e2c88676944124a4a3c80112b35231035 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 10:27:12 +0100 Subject: [PATCH 271/619] Fix bug when running presence off master (#10149) Hopefully fixes #10027. --- changelog.d/10149.bugfix | 1 + synapse/storage/databases/main/presence.py | 2 +- synapse/storage/util/id_generators.py | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10149.bugfix diff --git a/changelog.d/10149.bugfix b/changelog.d/10149.bugfix new file mode 100644 index 0000000000..cb2d2eedb3 --- /dev/null +++ b/changelog.d/10149.bugfix @@ -0,0 +1 @@ +Fix a bug which caused presence updates to stop working some time after restart, when using a presence writer worker. diff --git a/synapse/storage/databases/main/presence.py b/synapse/storage/databases/main/presence.py index 6a2baa7841..1388771c40 100644 --- a/synapse/storage/databases/main/presence.py +++ b/synapse/storage/databases/main/presence.py @@ -50,7 +50,7 @@ def __init__( instance_name=self._instance_name, tables=[("presence_stream", "instance_name", "stream_id")], sequence_name="presence_stream_sequence", - writers=hs.config.worker.writers.to_device, + writers=hs.config.worker.writers.presence, ) else: self._presence_id_gen = StreamIdGenerator( diff --git a/synapse/storage/util/id_generators.py b/synapse/storage/util/id_generators.py index b1bd3a52d9..f1e62f9e85 100644 --- a/synapse/storage/util/id_generators.py +++ b/synapse/storage/util/id_generators.py @@ -397,6 +397,11 @@ def get_next(self): # ... persist event ... """ + # If we have a list of instances that are allowed to write to this + # stream, make sure we're in it. + if self._writers and self._instance_name not in self._writers: + raise Exception("Tried to allocate stream ID on non-writer") + return _MultiWriterCtxManager(self) def get_next_mult(self, n: int): @@ -406,6 +411,11 @@ def get_next_mult(self, n: int): # ... persist events ... """ + # If we have a list of instances that are allowed to write to this + # stream, make sure we're in it. + if self._writers and self._instance_name not in self._writers: + raise Exception("Tried to allocate stream ID on non-writer") + return _MultiWriterCtxManager(self, n) def get_next_txn(self, txn: LoggingTransaction): @@ -416,6 +426,11 @@ def get_next_txn(self, txn: LoggingTransaction): # ... persist event ... """ + # If we have a list of instances that are allowed to write to this + # stream, make sure we're in it. + if self._writers and self._instance_name not in self._writers: + raise Exception("Tried to allocate stream ID on non-writer") + next_id = self._load_next_id_txn(txn) with self._lock: From 5e0b4719ea6650596470f2d3bff91a19096067b8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 13:08:30 +0100 Subject: [PATCH 272/619] Fix sending presence over federation when using workers (#10163) When using a federation sender we'd send out all local presence updates over federation even when they shouldn't be. Fixes #10153. --- changelog.d/10163.bugfix | 1 + synapse/handlers/presence.py | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10163.bugfix diff --git a/changelog.d/10163.bugfix b/changelog.d/10163.bugfix new file mode 100644 index 0000000000..7ccde66743 --- /dev/null +++ b/changelog.d/10163.bugfix @@ -0,0 +1 @@ +Fix a bug when using federation sender worker where it would send out more presence updates than necessary, leading to high resource usage. Broke in v1.33.0. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index f5a049d754..79508580ac 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -495,9 +495,6 @@ async def notify_from_replication( users=users_to_states.keys(), ) - # If this is a federation sender, notify about presence updates. - await self.maybe_send_presence_to_interested_destinations(states) - async def process_replication_rows( self, stream_name: str, instance_name: str, token: int, rows: list ): @@ -519,11 +516,27 @@ async def process_replication_rows( for row in rows ] - for state in states: - self.user_to_current_state[state.user_id] = state + # The list of states to notify sync streams and remote servers about. + # This is calculated by comparing the old and new states for each user + # using `should_notify(..)`. + # + # Note that this is necessary as the presence writer will periodically + # flush presence state changes that should not be notified about to the + # DB, and so will be sent over the replication stream. + state_to_notify = [] + + for new_state in states: + old_state = self.user_to_current_state.get(new_state.user_id) + self.user_to_current_state[new_state.user_id] = new_state + + if not old_state or should_notify(old_state, new_state): + state_to_notify.append(new_state) stream_id = token - await self.notify_from_replication(states, stream_id) + await self.notify_from_replication(state_to_notify, stream_id) + + # If this is a federation sender, notify about presence updates. + await self.maybe_send_presence_to_interested_destinations(state_to_notify) def get_currently_syncing_users_for_replication(self) -> Iterable[str]: return [ From cdd985c64facb15b36fdc3bf479d25d6572f29a7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 15:19:42 +0100 Subject: [PATCH 273/619] Only send a presence state to a destination once (#10165) It turns out that we were sending the same presence state to a remote potentially multiple times. --- changelog.d/10165.bugfix | 1 + synapse/handlers/presence.py | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 changelog.d/10165.bugfix diff --git a/changelog.d/10165.bugfix b/changelog.d/10165.bugfix new file mode 100644 index 0000000000..8b1eeff352 --- /dev/null +++ b/changelog.d/10165.bugfix @@ -0,0 +1 @@ +Fix a bug where Synapse could send the same presence update to a remote twice. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 79508580ac..44ed7a0712 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -299,14 +299,14 @@ async def maybe_send_presence_to_interested_destinations( if not states: return - hosts_and_states = await get_interested_remotes( + hosts_to_states = await get_interested_remotes( self.store, self.presence_router, states, ) - for destinations, states in hosts_and_states: - self._federation.send_presence_to_destinations(states, destinations) + for destination, host_states in hosts_to_states.items(): + self._federation.send_presence_to_destinations(host_states, [destination]) async def send_full_presence_to_users(self, user_ids: Collection[str]): """ @@ -842,15 +842,15 @@ async def _update_states( if to_federation_ping: federation_presence_out_counter.inc(len(to_federation_ping)) - hosts_and_states = await get_interested_remotes( + hosts_to_states = await get_interested_remotes( self.store, self.presence_router, list(to_federation_ping.values()), ) - for destinations, states in hosts_and_states: + for destination, states in hosts_to_states.items(): self._federation_queue.send_presence_to_destinations( - states, destinations + states, [destination] ) async def _handle_timeouts(self) -> None: @@ -1975,7 +1975,7 @@ async def get_interested_remotes( store: DataStore, presence_router: PresenceRouter, states: List[UserPresenceState], -) -> List[Tuple[Collection[str], List[UserPresenceState]]]: +) -> Dict[str, Set[UserPresenceState]]: """Given a list of presence states figure out which remote servers should be sent which. @@ -1987,11 +1987,9 @@ async def get_interested_remotes( states: A list of incoming user presence updates. Returns: - A list of 2-tuples of destinations and states, where for - each tuple the list of UserPresenceState should be sent to each - destination + A map from destinations to presence states to send to that destination. """ - hosts_and_states = [] # type: List[Tuple[Collection[str], List[UserPresenceState]]] + hosts_and_states: Dict[str, Set[UserPresenceState]] = {} # First we look up the rooms each user is in (as well as any explicit # subscriptions), then for each distinct room we look up the remote @@ -2003,11 +2001,12 @@ async def get_interested_remotes( for room_id, states in room_ids_to_states.items(): user_ids = await store.get_users_in_room(room_id) hosts = {get_domain_from_id(user_id) for user_id in user_ids} - hosts_and_states.append((hosts, states)) + for host in hosts: + hosts_and_states.setdefault(host, set()).update(states) for user_id, states in users_to_states.items(): host = get_domain_from_id(user_id) - hosts_and_states.append(([host], states)) + hosts_and_states.setdefault(host, set()).update(states) return hosts_and_states From fb10a73e85ff4a5c090226d046b6b7ede7e57d6e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 15:21:34 +0100 Subject: [PATCH 274/619] 1.36.0rc2 --- CHANGES.md | 11 +++++++++++ changelog.d/10149.bugfix | 1 - changelog.d/10163.bugfix | 1 - changelog.d/10165.bugfix | 1 - synapse/__init__.py | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/10149.bugfix delete mode 100644 changelog.d/10163.bugfix delete mode 100644 changelog.d/10165.bugfix diff --git a/CHANGES.md b/CHANGES.md index 48e9b55c8a..cafb79124d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +Synapse 1.36.0rc2 (2021-06-11) +============================== + +Bugfixes +-------- + +- Fix a bug which caused presence updates to stop working some time after restart, when using a presence writer worker. ([\#10149](https://github.com/matrix-org/synapse/issues/10149)) +- Fix a bug when using federation sender worker where it would send out more presence updates than necessary, leading to high resource usage. Broke in v1.33.0. ([\#10163](https://github.com/matrix-org/synapse/issues/10163)) +- Fix a bug where Synapse could send the same presence update to a remote twice. ([\#10165](https://github.com/matrix-org/synapse/issues/10165)) + + Synapse 1.36.0rc1 (2021-06-08) ============================== diff --git a/changelog.d/10149.bugfix b/changelog.d/10149.bugfix deleted file mode 100644 index cb2d2eedb3..0000000000 --- a/changelog.d/10149.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which caused presence updates to stop working some time after restart, when using a presence writer worker. diff --git a/changelog.d/10163.bugfix b/changelog.d/10163.bugfix deleted file mode 100644 index 7ccde66743..0000000000 --- a/changelog.d/10163.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug when using federation sender worker where it would send out more presence updates than necessary, leading to high resource usage. Broke in v1.33.0. diff --git a/changelog.d/10165.bugfix b/changelog.d/10165.bugfix deleted file mode 100644 index 8b1eeff352..0000000000 --- a/changelog.d/10165.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug where Synapse could send the same presence update to a remote twice. diff --git a/synapse/__init__.py b/synapse/__init__.py index 58261d04ef..407ba14a76 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.36.0rc1" +__version__ = "1.36.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From cbf350db63f74b9eb3922a8ebe0284f71e248a3c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 11 Jun 2021 15:30:42 +0100 Subject: [PATCH 275/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index cafb79124d..aeec4fa5fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Synapse 1.36.0rc2 (2021-06-11) Bugfixes -------- -- Fix a bug which caused presence updates to stop working some time after restart, when using a presence writer worker. ([\#10149](https://github.com/matrix-org/synapse/issues/10149)) +- Fix a bug which caused presence updates to stop working some time after a restart, when using a presence writer worker. Broke in v1.33.0. ([\#10149](https://github.com/matrix-org/synapse/issues/10149)) - Fix a bug when using federation sender worker where it would send out more presence updates than necessary, leading to high resource usage. Broke in v1.33.0. ([\#10163](https://github.com/matrix-org/synapse/issues/10163)) - Fix a bug where Synapse could send the same presence update to a remote twice. ([\#10165](https://github.com/matrix-org/synapse/issues/10165)) From 13577aa55ebe6087e8b813c0643bbb53148e9510 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 11 Jun 2021 17:13:56 +0100 Subject: [PATCH 276/619] Notes on boolean columns in database schemas (#10164) --- changelog.d/10164.misc | 1 + docs/development/database_schema.md | 42 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 changelog.d/10164.misc diff --git a/changelog.d/10164.misc b/changelog.d/10164.misc new file mode 100644 index 0000000000..a98f1e7c7a --- /dev/null +++ b/changelog.d/10164.misc @@ -0,0 +1 @@ +Add some developer documentation about boolean columns in database schemas. diff --git a/docs/development/database_schema.md b/docs/development/database_schema.md index 7fe8ec63e1..20740cf5ac 100644 --- a/docs/development/database_schema.md +++ b/docs/development/database_schema.md @@ -93,3 +93,45 @@ Ensure postgres is installed, then run: NB at the time of writing, this script predates the split into separate `state`/`main` databases so will require updates to handle that correctly. + +## Boolean columns + +Boolean columns require special treatment, since SQLite treats booleans the +same as integers. + +There are three separate aspects to this: + + * Any new boolean column must be added to the `BOOLEAN_COLUMNS` list in + `scripts/synapse_port_db`. This tells the port script to cast the integer + value from SQLite to a boolean before writing the value to the postgres + database. + + * Before SQLite 3.23, `TRUE` and `FALSE` were not recognised as constants by + SQLite, and the `IS [NOT] TRUE`/`IS [NOT] FALSE` operators were not + supported. This makes it necessary to avoid using `TRUE` and `FALSE` + constants in SQL commands. + + For example, to insert a `TRUE` value into the database, write: + + ```python + txn.execute("INSERT INTO tbl(col) VALUES (?)", (True, )) + ``` + + * Default values for new boolean columns present a particular + difficulty. Generally it is best to create separate schema files for + Postgres and SQLite. For example: + + ```sql + # in 00delta.sql.postgres: + ALTER TABLE tbl ADD COLUMN col BOOLEAN DEFAULT FALSE; + ``` + + ```sql + # in 00delta.sql.sqlite: + ALTER TABLE tbl ADD COLUMN col BOOLEAN DEFAULT 0; + ``` + + Note that there is a particularly insidious failure mode here: the Postgres + flavour will be accepted by SQLite 3.22, but will give a column whose + default value is the **string** `"FALSE"` - which, when cast back to a boolean + in Python, evaluates to `True`. From d7808a2dde8a924d86791c71b864e7ab24b8d967 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 14 Jun 2021 10:26:09 +0100 Subject: [PATCH 277/619] Extend `ResponseCache` to pass a context object into the callback (#10157) This is the first of two PRs which seek to address #8518. This first PR lays the groundwork by extending ResponseCache; a second PR (#10158) will update the SyncHandler to actually use it, and fix the bug. The idea here is that we allow the callback given to ResponseCache.wrap to decide whether its result should be cached or not. We do that by (optionally) passing a ResponseCacheContext into it, which it can modify. --- changelog.d/10157.misc | 1 + synapse/replication/http/_base.py | 6 +- synapse/replication/http/membership.py | 2 +- synapse/util/caches/response_cache.py | 99 ++++++++++++++----- ...esponsecache.py => test_response_cache.py} | 75 ++++++++++++-- 5 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 changelog.d/10157.misc rename tests/util/caches/{test_responsecache.py => test_response_cache.py} (62%) diff --git a/changelog.d/10157.misc b/changelog.d/10157.misc new file mode 100644 index 0000000000..6c1d0e6e59 --- /dev/null +++ b/changelog.d/10157.misc @@ -0,0 +1 @@ +Extend `ResponseCache` to pass a context object into the callback. diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index 2a13026e9a..f13a7c23b4 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -285,7 +285,7 @@ def register(self, http_server): self.__class__.__name__, ) - def _check_auth_and_handle(self, request, **kwargs): + async def _check_auth_and_handle(self, request, **kwargs): """Called on new incoming requests when caching is enabled. Checks if there is a cached response for the request and returns that, otherwise calls `_handle_request` and caches its response. @@ -300,8 +300,8 @@ def _check_auth_and_handle(self, request, **kwargs): if self.CACHE: txn_id = kwargs.pop("txn_id") - return self.response_cache.wrap( + return await self.response_cache.wrap( txn_id, self._handle_request, request, **kwargs ) - return self._handle_request(request, **kwargs) + return await self._handle_request(request, **kwargs) diff --git a/synapse/replication/http/membership.py b/synapse/replication/http/membership.py index 043c25f63d..34206c5060 100644 --- a/synapse/replication/http/membership.py +++ b/synapse/replication/http/membership.py @@ -345,7 +345,7 @@ async def _serialize_payload( # type: ignore return {} - def _handle_request( # type: ignore + async def _handle_request( # type: ignore self, request: Request, room_id: str, user_id: str, change: str ) -> Tuple[int, JsonDict]: logger.info("user membership change: %s in %s", user_id, room_id) diff --git a/synapse/util/caches/response_cache.py b/synapse/util/caches/response_cache.py index 25ea1bcc91..34c662c4db 100644 --- a/synapse/util/caches/response_cache.py +++ b/synapse/util/caches/response_cache.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import Any, Callable, Dict, Generic, Optional, TypeVar +from typing import Any, Awaitable, Callable, Dict, Generic, Optional, TypeVar + +import attr from twisted.internet import defer @@ -23,10 +25,36 @@ logger = logging.getLogger(__name__) -T = TypeVar("T") +# the type of the key in the cache +KV = TypeVar("KV") + +# the type of the result from the operation +RV = TypeVar("RV") + +@attr.s(auto_attribs=True) +class ResponseCacheContext(Generic[KV]): + """Information about a missed ResponseCache hit -class ResponseCache(Generic[T]): + This object can be passed into the callback for additional feedback + """ + + cache_key: KV + """The cache key that caused the cache miss + + This should be considered read-only. + + TODO: in attrs 20.1, make it frozen with an on_setattr. + """ + + should_cache: bool = True + """Whether the result should be cached once the request completes. + + This can be modified by the callback if it decides its result should not be cached. + """ + + +class ResponseCache(Generic[KV]): """ This caches a deferred response. Until the deferred completes it will be returned from the cache. This means that if the client retries the request @@ -35,8 +63,10 @@ class ResponseCache(Generic[T]): """ def __init__(self, clock: Clock, name: str, timeout_ms: float = 0): - # Requests that haven't finished yet. - self.pending_result_cache = {} # type: Dict[T, ObservableDeferred] + # This is poorly-named: it includes both complete and incomplete results. + # We keep complete results rather than switching to absolute values because + # that makes it easier to cache Failure results. + self.pending_result_cache = {} # type: Dict[KV, ObservableDeferred] self.clock = clock self.timeout_sec = timeout_ms / 1000.0 @@ -50,16 +80,13 @@ def size(self) -> int: def __len__(self) -> int: return self.size() - def get(self, key: T) -> Optional[defer.Deferred]: + def get(self, key: KV) -> Optional[defer.Deferred]: """Look up the given key. - Can return either a new Deferred (which also doesn't follow the synapse - logcontext rules), or, if the request has completed, the actual - result. You will probably want to make_deferred_yieldable the result. + Returns a new Deferred (which also doesn't follow the synapse + logcontext rules). You will probably want to make_deferred_yieldable the result. - If there is no entry for the key, returns None. It is worth noting that - this means there is no way to distinguish a completed result of None - from an absent cache entry. + If there is no entry for the key, returns None. Args: key: key to get/set in the cache @@ -76,42 +103,56 @@ def get(self, key: T) -> Optional[defer.Deferred]: self._metrics.inc_misses() return None - def set(self, key: T, deferred: defer.Deferred) -> defer.Deferred: + def _set( + self, context: ResponseCacheContext[KV], deferred: defer.Deferred + ) -> defer.Deferred: """Set the entry for the given key to the given deferred. *deferred* should run its callbacks in the sentinel logcontext (ie, you should wrap normal synapse deferreds with synapse.logging.context.run_in_background). - Can return either a new Deferred (which also doesn't follow the synapse - logcontext rules), or, if *deferred* was already complete, the actual - result. You will probably want to make_deferred_yieldable the result. + Returns a new Deferred (which also doesn't follow the synapse logcontext rules). + You will probably want to make_deferred_yieldable the result. Args: - key: key to get/set in the cache + context: Information about the cache miss deferred: The deferred which resolves to the result. Returns: A new deferred which resolves to the actual result. """ result = ObservableDeferred(deferred, consumeErrors=True) + key = context.cache_key self.pending_result_cache[key] = result - def remove(r): - if self.timeout_sec: + def on_complete(r): + # if this cache has a non-zero timeout, and the callback has not cleared + # the should_cache bit, we leave it in the cache for now and schedule + # its removal later. + if self.timeout_sec and context.should_cache: self.clock.call_later( self.timeout_sec, self.pending_result_cache.pop, key, None ) else: + # otherwise, remove the result immediately. self.pending_result_cache.pop(key, None) return r - result.addBoth(remove) + # make sure we do this *after* adding the entry to pending_result_cache, + # in case the result is already complete (in which case flipping the order would + # leave us with a stuck entry in the cache). + result.addBoth(on_complete) return result.observe() - def wrap( - self, key: T, callback: Callable[..., Any], *args: Any, **kwargs: Any - ) -> defer.Deferred: + async def wrap( + self, + key: KV, + callback: Callable[..., Awaitable[RV]], + *args: Any, + cache_context: bool = False, + **kwargs: Any, + ) -> RV: """Wrap together a *get* and *set* call, taking care of logcontexts First looks up the key in the cache, and if it is present makes it @@ -140,22 +181,28 @@ async def handle_request(request): *args: positional parameters to pass to the callback, if it is used + cache_context: if set, the callback will be given a `cache_context` kw arg, + which will be a ResponseCacheContext object. + **kwargs: named parameters to pass to the callback, if it is used Returns: - Deferred which resolves to the result + The result of the callback (from the cache, or otherwise) """ result = self.get(key) if not result: logger.debug( "[%s]: no cached result for [%s], calculating new one", self._name, key ) + context = ResponseCacheContext(cache_key=key) + if cache_context: + kwargs["cache_context"] = context d = run_in_background(callback, *args, **kwargs) - result = self.set(key, d) + result = self._set(context, d) elif not isinstance(result, defer.Deferred) or result.called: logger.info("[%s]: using completed cached result for [%s]", self._name, key) else: logger.info( "[%s]: using incomplete cached result for [%s]", self._name, key ) - return make_deferred_yieldable(result) + return await make_deferred_yieldable(result) diff --git a/tests/util/caches/test_responsecache.py b/tests/util/caches/test_response_cache.py similarity index 62% rename from tests/util/caches/test_responsecache.py rename to tests/util/caches/test_response_cache.py index f9a187b8de..1e83ef2f33 100644 --- a/tests/util/caches/test_responsecache.py +++ b/tests/util/caches/test_response_cache.py @@ -11,14 +11,17 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from parameterized import parameterized -from synapse.util.caches.response_cache import ResponseCache +from twisted.internet import defer + +from synapse.util.caches.response_cache import ResponseCache, ResponseCacheContext from tests.server import get_clock from tests.unittest import TestCase -class DeferredCacheTestCase(TestCase): +class ResponseCacheTestCase(TestCase): """ A TestCase class for ResponseCache. @@ -48,7 +51,9 @@ def test_cache_hit(self): expected_result = "howdy" - wrap_d = cache.wrap(0, self.instant_return, expected_result) + wrap_d = defer.ensureDeferred( + cache.wrap(0, self.instant_return, expected_result) + ) self.assertEqual( expected_result, @@ -66,7 +71,9 @@ def test_cache_miss(self): expected_result = "howdy" - wrap_d = cache.wrap(0, self.instant_return, expected_result) + wrap_d = defer.ensureDeferred( + cache.wrap(0, self.instant_return, expected_result) + ) self.assertEqual( expected_result, @@ -80,7 +87,9 @@ def test_cache_expire(self): expected_result = "howdy" - wrap_d = cache.wrap(0, self.instant_return, expected_result) + wrap_d = defer.ensureDeferred( + cache.wrap(0, self.instant_return, expected_result) + ) self.assertEqual(expected_result, self.successResultOf(wrap_d)) self.assertEqual( @@ -99,7 +108,10 @@ def test_cache_wait_hit(self): expected_result = "howdy" - wrap_d = cache.wrap(0, self.delayed_return, expected_result) + wrap_d = defer.ensureDeferred( + cache.wrap(0, self.delayed_return, expected_result) + ) + self.assertNoResult(wrap_d) # function wakes up, returns result @@ -112,7 +124,9 @@ def test_cache_wait_expire(self): expected_result = "howdy" - wrap_d = cache.wrap(0, self.delayed_return, expected_result) + wrap_d = defer.ensureDeferred( + cache.wrap(0, self.delayed_return, expected_result) + ) self.assertNoResult(wrap_d) # stop at 1 second to callback cache eviction callLater at that time, then another to set time at 2 @@ -129,3 +143,50 @@ def test_cache_wait_expire(self): self.reactor.pump((2,)) self.assertIsNone(cache.get(0), "cache should not have the result now") + + @parameterized.expand([(True,), (False,)]) + def test_cache_context_nocache(self, should_cache: bool): + """If the callback clears the should_cache bit, the result should not be cached""" + cache = self.with_cache("medium_cache", ms=3000) + + expected_result = "howdy" + + call_count = 0 + + async def non_caching(o: str, cache_context: ResponseCacheContext[int]): + nonlocal call_count + call_count += 1 + await self.clock.sleep(1) + cache_context.should_cache = should_cache + return o + + wrap_d = defer.ensureDeferred( + cache.wrap(0, non_caching, expected_result, cache_context=True) + ) + # there should be no result to start with + self.assertNoResult(wrap_d) + + # a second call should also return a pending deferred + wrap2_d = defer.ensureDeferred( + cache.wrap(0, non_caching, expected_result, cache_context=True) + ) + self.assertNoResult(wrap2_d) + + # and there should have been exactly one call + self.assertEqual(call_count, 1) + + # let the call complete + self.reactor.advance(1) + + # both results should have completed + self.assertEqual(expected_result, self.successResultOf(wrap_d)) + self.assertEqual(expected_result, self.successResultOf(wrap2_d)) + + if should_cache: + self.assertEqual( + expected_result, + self.successResultOf(cache.get(0)), + "cache should still have the result", + ) + else: + self.assertIsNone(cache.get(0), "cache should not have the result") From 1dfdc87b9bb07cc3c958dde7f41f2af4322477e5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 14 Jun 2021 11:59:27 +0100 Subject: [PATCH 278/619] Refactor `EventPersistenceQueue` (#10145) some cleanup, pulled out of #10134. --- changelog.d/10145.misc | 1 + synapse/storage/persist_events.py | 165 ++++++++++++++++-------------- 2 files changed, 89 insertions(+), 77 deletions(-) create mode 100644 changelog.d/10145.misc diff --git a/changelog.d/10145.misc b/changelog.d/10145.misc new file mode 100644 index 0000000000..2f0c643b08 --- /dev/null +++ b/changelog.d/10145.misc @@ -0,0 +1 @@ +Refactor EventPersistenceQueue. diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 33dc752d8f..c11f6c5845 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -16,9 +16,23 @@ import itertools import logging -from collections import deque, namedtuple -from typing import Collection, Dict, Iterable, List, Optional, Set, Tuple +from collections import deque +from typing import ( + Awaitable, + Callable, + Collection, + Deque, + Dict, + Generic, + Iterable, + List, + Optional, + Set, + Tuple, + TypeVar, +) +import attr from prometheus_client import Counter, Histogram from twisted.internet import defer @@ -37,7 +51,7 @@ StateMap, get_domain_from_id, ) -from synapse.util.async_helpers import ObservableDeferred +from synapse.util.async_helpers import ObservableDeferred, yieldable_gather_results from synapse.util.metrics import Measure logger = logging.getLogger(__name__) @@ -89,25 +103,47 @@ ) -class _EventPeristenceQueue: +@attr.s(auto_attribs=True, frozen=True, slots=True) +class _EventPersistQueueItem: + events_and_contexts: List[Tuple[EventBase, EventContext]] + backfilled: bool + deferred: ObservableDeferred + + +_PersistResult = TypeVar("_PersistResult") + + +class _EventPeristenceQueue(Generic[_PersistResult]): """Queues up events so that they can be persisted in bulk with only one concurrent transaction per room. """ - _EventPersistQueueItem = namedtuple( - "_EventPersistQueueItem", ("events_and_contexts", "backfilled", "deferred") - ) + def __init__( + self, + per_item_callback: Callable[ + [List[Tuple[EventBase, EventContext]], bool], + Awaitable[_PersistResult], + ], + ): + """Create a new event persistence queue - def __init__(self): - self._event_persist_queues = {} - self._currently_persisting_rooms = set() + The per_item_callback will be called for each item added via add_to_queue, + and its result will be returned via the Deferreds returned from add_to_queue. + """ + self._event_persist_queues: Dict[str, Deque[_EventPersistQueueItem]] = {} + self._currently_persisting_rooms: Set[str] = set() + self._per_item_callback = per_item_callback - def add_to_queue(self, room_id, events_and_contexts, backfilled): + async def add_to_queue( + self, + room_id: str, + events_and_contexts: Iterable[Tuple[EventBase, EventContext]], + backfilled: bool, + ) -> _PersistResult: """Add events to the queue, with the given persist_event options. - NB: due to the normal usage pattern of this method, it does *not* - follow the synapse logcontext rules, and leaves the logcontext in - place whether or not the returned deferred is ready. + If we are not already processing events in this room, starts off a background + process to to so, calling the per_item_callback for each item. Args: room_id (str): @@ -115,38 +151,36 @@ def add_to_queue(self, room_id, events_and_contexts, backfilled): backfilled (bool): Returns: - defer.Deferred: a deferred which will resolve once the events are - persisted. Runs its callbacks *without* a logcontext. The result - is the same as that returned by the callback passed to - `handle_queue`. + the result returned by the `_per_item_callback` passed to + `__init__`. """ queue = self._event_persist_queues.setdefault(room_id, deque()) - if queue: - # if the last item in the queue has the same `backfilled` setting, - # we can just add these new events to that item. - end_item = queue[-1] - if end_item.backfilled == backfilled: - end_item.events_and_contexts.extend(events_and_contexts) - return end_item.deferred.observe() - deferred = ObservableDeferred(defer.Deferred(), consumeErrors=True) + # if the last item in the queue has the same `backfilled` setting, + # we can just add these new events to that item. + if queue and queue[-1].backfilled == backfilled: + end_item = queue[-1] + else: + # need to make a new queue item + deferred = ObservableDeferred(defer.Deferred(), consumeErrors=True) - queue.append( - self._EventPersistQueueItem( - events_and_contexts=events_and_contexts, + end_item = _EventPersistQueueItem( + events_and_contexts=[], backfilled=backfilled, deferred=deferred, ) - ) + queue.append(end_item) - return deferred.observe() + end_item.events_and_contexts.extend(events_and_contexts) + self._handle_queue(room_id) + return await make_deferred_yieldable(end_item.deferred.observe()) - def handle_queue(self, room_id, per_item_callback): + def _handle_queue(self, room_id): """Attempts to handle the queue for a room if not already being handled. - The given callback will be invoked with for each item in the queue, + The queue's callback will be invoked with for each item in the queue, of type _EventPersistQueueItem. The per_item_callback will continuously - be called with new items, unless the queue becomnes empty. The return + be called with new items, unless the queue becomes empty. The return value of the function will be given to the deferreds waiting on the item, exceptions will be passed to the deferreds as well. @@ -156,7 +190,6 @@ def handle_queue(self, room_id, per_item_callback): If another callback is currently handling the queue then it will not be invoked. """ - if room_id in self._currently_persisting_rooms: return @@ -167,7 +200,9 @@ async def handle_queue_loop(): queue = self._get_drainining_queue(room_id) for item in queue: try: - ret = await per_item_callback(item) + ret = await self._per_item_callback( + item.events_and_contexts, item.backfilled + ) except Exception: with PreserveLoggingContext(): item.deferred.errback() @@ -214,7 +249,7 @@ def __init__(self, hs, stores: Databases): self._clock = hs.get_clock() self._instance_name = hs.get_instance_name() self.is_mine_id = hs.is_mine_id - self._event_persist_queue = _EventPeristenceQueue() + self._event_persist_queue = _EventPeristenceQueue(self._persist_event_batch) self._state_resolution_handler = hs.get_state_resolution_handler() async def persist_events( @@ -241,26 +276,21 @@ async def persist_events( for event, ctx in events_and_contexts: partitioned.setdefault(event.room_id, []).append((event, ctx)) - deferreds = [] - for room_id, evs_ctxs in partitioned.items(): - d = self._event_persist_queue.add_to_queue( + async def enqueue(item): + room_id, evs_ctxs = item + return await self._event_persist_queue.add_to_queue( room_id, evs_ctxs, backfilled=backfilled ) - deferreds.append(d) - for room_id in partitioned: - self._maybe_start_persisting(room_id) + ret_vals = await yieldable_gather_results(enqueue, partitioned.items()) - # Each deferred returns a map from event ID to existing event ID if the - # event was deduplicated. (The dict may also include other entries if + # Each call to add_to_queue returns a map from event ID to existing event ID if + # the event was deduplicated. (The dict may also include other entries if # the event was persisted in a batch with other events). # - # Since we use `defer.gatherResults` we need to merge the returned list + # Since we use `yieldable_gather_results` we need to merge the returned list # of dicts into one. - ret_vals = await make_deferred_yieldable( - defer.gatherResults(deferreds, consumeErrors=True) - ) - replaced_events = {} + replaced_events: Dict[str, str] = {} for d in ret_vals: replaced_events.update(d) @@ -287,16 +317,12 @@ async def persist_event( event if it was deduplicated due to an existing event matching the transaction ID. """ - deferred = self._event_persist_queue.add_to_queue( - event.room_id, [(event, context)], backfilled=backfilled - ) - - self._maybe_start_persisting(event.room_id) - - # The deferred returns a map from event ID to existing event ID if the + # add_to_queue returns a map from event ID to existing event ID if the # event was deduplicated. (The dict may also include other entries if # the event was persisted in a batch with other events.) - replaced_events = await make_deferred_yieldable(deferred) + replaced_events = await self._event_persist_queue.add_to_queue( + event.room_id, [(event, context)], backfilled=backfilled + ) replaced_event = replaced_events.get(event.event_id) if replaced_event: event = await self.main_store.get_event(replaced_event) @@ -308,29 +334,14 @@ async def persist_event( pos = PersistedEventPosition(self._instance_name, event_stream_id) return event, pos, self.main_store.get_room_max_token() - def _maybe_start_persisting(self, room_id: str): - """Pokes the `_event_persist_queue` to start handling new items in the - queue, if not already in progress. - - Causes the deferreds returned by `add_to_queue` to resolve with: a - dictionary of event ID to event ID we didn't persist as we already had - another event persisted with the same TXN ID. - """ - - async def persisting_queue(item): - with Measure(self._clock, "persist_events"): - return await self._persist_events( - item.events_and_contexts, backfilled=item.backfilled - ) - - self._event_persist_queue.handle_queue(room_id, persisting_queue) - - async def _persist_events( + async def _persist_event_batch( self, events_and_contexts: List[Tuple[EventBase, EventContext]], backfilled: bool = False, ) -> Dict[str, str]: - """Calculates the change to current state and forward extremities, and + """Callback for the _event_persist_queue + + Calculates the change to current state and forward extremities, and persists the given events and with those updates. Returns: From aac2c49b9b8a241f7a13726cfa74bf3a67c9079f Mon Sep 17 00:00:00 2001 From: Michael Kutzner <65556178+mikure@users.noreply.github.com> Date: Tue, 15 Jun 2021 09:53:55 +0200 Subject: [PATCH 279/619] Fix 'ip_range_whitelist' not working for federation servers (#10115) Add 'federation_ip_range_whitelist'. This allows backwards-compatibility, If 'federation_ip_range_blacklist' is set. Otherwise 'ip_range_whitelist' will be used for federation servers. Signed-off-by: Michael Kutzner 1mikure@gmail.com --- changelog.d/10115.bugfix | 1 + synapse/config/server.py | 27 ++++++++++++++------------ synapse/http/matrixfederationclient.py | 4 +++- 3 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 changelog.d/10115.bugfix diff --git a/changelog.d/10115.bugfix b/changelog.d/10115.bugfix new file mode 100644 index 0000000000..e16f356e68 --- /dev/null +++ b/changelog.d/10115.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse v1.25.0 that prevented the `ip_range_whitelist` configuration option from working for federation and identity servers. Contributed by @mikure. diff --git a/synapse/config/server.py b/synapse/config/server.py index c290a35a92..0833a5f7bc 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -397,19 +397,22 @@ def read_config(self, config, **kwargs): self.ip_range_whitelist = generate_ip_set( config.get("ip_range_whitelist", ()), config_path=("ip_range_whitelist",) ) - # The federation_ip_range_blacklist is used for backwards-compatibility - # and only applies to federation and identity servers. If it is not given, - # default to ip_range_blacklist. - federation_ip_range_blacklist = config.get( - "federation_ip_range_blacklist", ip_range_blacklist - ) - # Always blacklist 0.0.0.0, :: - self.federation_ip_range_blacklist = generate_ip_set( - federation_ip_range_blacklist, - ["0.0.0.0", "::"], - config_path=("federation_ip_range_blacklist",), - ) + # and only applies to federation and identity servers. + if "federation_ip_range_blacklist" in config: + # Always blacklist 0.0.0.0, :: + self.federation_ip_range_blacklist = generate_ip_set( + config["federation_ip_range_blacklist"], + ["0.0.0.0", "::"], + config_path=("federation_ip_range_blacklist",), + ) + # 'federation_ip_range_whitelist' was never a supported configuration option. + self.federation_ip_range_whitelist = None + else: + # No backwards-compatiblity requrired, as federation_ip_range_blacklist + # is not given. Default to ip_range_blacklist and ip_range_whitelist. + self.federation_ip_range_blacklist = self.ip_range_blacklist + self.federation_ip_range_whitelist = self.ip_range_whitelist # (undocumented) option for torturing the worker-mode replication a bit, # for testing. The value defines the number of milliseconds to pause before diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 629373fc47..b8849c0150 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -318,7 +318,9 @@ def __init__(self, hs, tls_client_options_factory): # We need to use a DNS resolver which filters out blacklisted IP # addresses, to prevent DNS rebinding. self.reactor = BlacklistingReactorWrapper( - hs.get_reactor(), None, hs.config.federation_ip_range_blacklist + hs.get_reactor(), + hs.config.federation_ip_range_whitelist, + hs.config.federation_ip_range_blacklist, ) # type: ISynapseReactor user_agent = hs.version_string From 9e5ab6dd581389271b817d256e2fca113614a080 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 15 Jun 2021 07:45:14 -0400 Subject: [PATCH 280/619] Remove the experimental flag for knocking and use stable prefixes / endpoints. (#10167) * Room version 7 for knocking. * Stable prefixes and endpoints (both client and federation) for knocking. * Removes the experimental configuration flag. --- changelog.d/10167.feature | 1 + synapse/api/constants.py | 4 +-- synapse/api/room_versions.py | 7 +++--- synapse/config/experimental.py | 7 ------ synapse/federation/federation_client.py | 9 ++----- synapse/federation/transport/client.py | 27 +++------------------ synapse/federation/transport/server.py | 22 ++--------------- synapse/handlers/federation.py | 6 ++--- synapse/handlers/room_member.py | 5 +--- synapse/rest/__init__.py | 5 +--- synapse/rest/client/v2_alpha/knock.py | 6 ++--- tests/federation/transport/test_knocking.py | 22 +++++++---------- tests/rest/client/v2_alpha/test_sync.py | 8 +++--- 13 files changed, 33 insertions(+), 96 deletions(-) create mode 100644 changelog.d/10167.feature diff --git a/changelog.d/10167.feature b/changelog.d/10167.feature new file mode 100644 index 0000000000..9c41140194 --- /dev/null +++ b/changelog.d/10167.feature @@ -0,0 +1 @@ +Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 8d5b2177d2..3940da5c88 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -41,7 +41,7 @@ class Membership: INVITE = "invite" JOIN = "join" - KNOCK = "xyz.amorgan.knock" + KNOCK = "knock" LEAVE = "leave" BAN = "ban" LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN) @@ -58,7 +58,7 @@ class PresenceState: class JoinRules: PUBLIC = "public" - KNOCK = "xyz.amorgan.knock" + KNOCK = "knock" INVITE = "invite" PRIVATE = "private" # As defined for MSC3083. diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 3349f399ba..f6c1c97b40 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -180,9 +180,9 @@ class RoomVersions: msc3083_join_rules=True, msc2403_knocking=False, ) - MSC2403 = RoomVersion( - "xyz.amorgan.knock", - RoomDisposition.UNSTABLE, + V7 = RoomVersion( + "7", + RoomDisposition.STABLE, EventFormatVersions.V3, StateResolutionVersions.V2, enforce_key_validity=True, @@ -206,6 +206,7 @@ class RoomVersions: RoomVersions.V6, RoomVersions.MSC2176, RoomVersions.MSC3083, + RoomVersions.V7, ) # Note that we do not include MSC2043 here unless it is enabled in the config. } # type: Dict[str, RoomVersion] diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 37668079e7..6ebce4b2f7 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.config._base import Config from synapse.types import JsonDict @@ -30,9 +29,3 @@ def read_config(self, config: JsonDict, **kwargs): # MSC3026 (busy presence state) self.msc3026_enabled = experimental.get("msc3026_enabled", False) # type: bool - - # MSC2403 (room knocking) - self.msc2403_enabled = experimental.get("msc2403_enabled", False) # type: bool - if self.msc2403_enabled: - # Enable the MSC2403 unstable room version - KNOWN_ROOM_VERSIONS[RoomVersions.MSC2403.identifier] = RoomVersions.MSC2403 diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 03ec14ce87..ed09c6af1f 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -90,7 +90,6 @@ def __init__(self, hs: "HomeServer"): self._clock.looping_call(self._clear_tried_cache, 60 * 1000) self.state = hs.get_state_handler() self.transport_layer = hs.get_federation_transport_client() - self._msc2403_enabled = hs.config.experimental.msc2403_enabled self.hostname = hs.hostname self.signing_key = hs.signing_key @@ -621,11 +620,7 @@ async def make_membership_event( SynapseError: if the chosen remote server returns a 300/400 code, or no servers successfully handle the request. """ - valid_memberships = {Membership.JOIN, Membership.LEAVE} - - # Allow knocking if the feature is enabled - if self._msc2403_enabled: - valid_memberships.add(Membership.KNOCK) + valid_memberships = {Membership.JOIN, Membership.LEAVE, Membership.KNOCK} if membership not in valid_memberships: raise RuntimeError( @@ -989,7 +984,7 @@ async def send_request(destination: str) -> JsonDict: return await self._do_send_knock(destination, pdu) return await self._try_destination_list( - "xyz.amorgan.knock/send_knock", destinations, send_request + "send_knock", destinations, send_request ) async def _do_send_knock(self, destination: str, pdu: EventBase) -> JsonDict: diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index af0c679ed9..c9e7c57461 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -47,7 +47,6 @@ class TransportLayerClient: def __init__(self, hs): self.server_name = hs.hostname self.client = hs.get_federation_http_client() - self._msc2403_enabled = hs.config.experimental.msc2403_enabled @log_function def get_room_state_ids(self, destination, room_id, event_id): @@ -221,29 +220,14 @@ async def make_membership_event( Fails with ``FederationDeniedError`` if the remote destination is not in our federation whitelist """ - valid_memberships = {Membership.JOIN, Membership.LEAVE} - - # Allow knocking if the feature is enabled - if self._msc2403_enabled: - valid_memberships.add(Membership.KNOCK) + valid_memberships = {Membership.JOIN, Membership.LEAVE, Membership.KNOCK} if membership not in valid_memberships: raise RuntimeError( "make_membership_event called with membership='%s', must be one of %s" % (membership, ",".join(valid_memberships)) ) - - # Knock currently uses an unstable prefix - if membership == Membership.KNOCK: - # Create a path in the form of /unstable/xyz.amorgan.knock/make_knock/... - path = _create_path( - FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock", - "/make_knock/%s/%s", - room_id, - user_id, - ) - else: - path = _create_v1_path("/make_%s/%s/%s", membership, room_id, user_id) + path = _create_v1_path("/make_%s/%s/%s", membership, room_id, user_id) ignore_backoff = False retry_on_dns_fail = False @@ -366,12 +350,7 @@ async def send_knock_v1( The list of state events may be empty. """ - path = _create_path( - FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock", - "/send_knock/%s/%s", - room_id, - event_id, - ) + path = _create_v1_path("/send_knock/%s/%s", room_id, event_id) return await self.client.put_json( destination=destination, path=path, data=content diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index fe5fb6bee7..16d740cf58 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -567,8 +567,6 @@ async def on_PUT(self, origin, content, query, room_id, event_id): class FederationMakeKnockServlet(BaseFederationServerServlet): PATH = "/make_knock/(?P[^/]*)/(?P[^/]*)" - PREFIX = FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock" - async def on_GET(self, origin, content, query, room_id, user_id): try: # Retrieve the room versions the remote homeserver claims to support @@ -585,8 +583,6 @@ async def on_GET(self, origin, content, query, room_id, user_id): class FederationV1SendKnockServlet(BaseFederationServerServlet): PATH = "/send_knock/(?P[^/]*)/(?P[^/]*)" - PREFIX = FEDERATION_UNSTABLE_PREFIX + "/xyz.amorgan.knock" - async def on_PUT(self, origin, content, query, room_id, event_id): content = await self.handler.on_send_knock_request(origin, content, room_id) return 200, content @@ -1610,6 +1606,8 @@ async def on_GET(self, origin, content, query, room_id): FederationVersionServlet, RoomComplexityServlet, FederationSpaceSummaryServlet, + FederationV1SendKnockServlet, + FederationMakeKnockServlet, ) # type: Tuple[Type[BaseFederationServlet], ...] OPENID_SERVLET_CLASSES = ( @@ -1652,12 +1650,6 @@ async def on_GET(self, origin, content, query, room_id): ) # type: Tuple[Type[BaseFederationServlet], ...] -MSC2403_SERVLET_CLASSES = ( - FederationV1SendKnockServlet, - FederationMakeKnockServlet, -) - - DEFAULT_SERVLET_GROUPS = ( "federation", "room_list", @@ -1700,16 +1692,6 @@ def register_servlets( server_name=hs.hostname, ).register(resource) - # Register msc2403 (knocking) servlets if the feature is enabled - if hs.config.experimental.msc2403_enabled: - for servletclass in MSC2403_SERVLET_CLASSES: - servletclass( - hs=hs, - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) - if "openid" in servlet_groups: for servletclass in OPENID_SERVLET_CLASSES: servletclass( diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 6647063485..b3a93212f1 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2009,8 +2009,7 @@ async def on_make_knock_request( """ if get_domain_from_id(user_id) != origin: logger.info( - "Get /xyz.amorgan.knock/make_knock request for user %r" - "from different origin %s, ignoring", + "Get /make_knock request for user %r from different origin %s, ignoring", user_id, origin, ) @@ -2077,8 +2076,7 @@ async def on_send_knock_request( if get_domain_from_id(event.sender) != origin: logger.info( - "Got /xyz.amorgan.knock/send_knock request for user %r " - "from different origin %s", + "Got /send_knock request for user %r from different origin %s", event.sender, origin, ) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index c26963b1e1..a49a61a34c 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -707,10 +707,7 @@ async def update_membership_locked( knock.event_id, txn_id, requester, content ) - elif ( - self.config.experimental.msc2403_enabled - and effective_membership_state == Membership.KNOCK - ): + elif effective_membership_state == Membership.KNOCK: if not is_host_in_room: # The knock needs to be sent over federation instead remote_room_hosts.append(get_domain_from_id(room_id)) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 138411ad19..d29f2fea5e 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -121,10 +121,7 @@ def register_servlets(client_resource, hs): account_validity.register_servlets(hs, client_resource) relations.register_servlets(hs, client_resource) password_policy.register_servlets(hs, client_resource) - - # Register msc2403 (knocking) servlets if the feature is enabled - if hs.config.experimental.msc2403_enabled: - knock.register_servlets(hs, client_resource) + knock.register_servlets(hs, client_resource) # moving to /_synapse/admin admin.register_servlets_for_client_rest_resource(hs, client_resource) diff --git a/synapse/rest/client/v2_alpha/knock.py b/synapse/rest/client/v2_alpha/knock.py index f046bf9cb3..7d1bc40658 100644 --- a/synapse/rest/client/v2_alpha/knock.py +++ b/synapse/rest/client/v2_alpha/knock.py @@ -39,12 +39,10 @@ class KnockRoomAliasServlet(RestServlet): """ - POST /xyz.amorgan.knock/{roomIdOrAlias} + POST /knock/{roomIdOrAlias} """ - PATTERNS = client_patterns( - "/xyz.amorgan.knock/(?P[^/]*)", releases=() - ) + PATTERNS = client_patterns("/knock/(?P[^/]*)") def __init__(self, hs: "HomeServer"): super().__init__() diff --git a/tests/federation/transport/test_knocking.py b/tests/federation/transport/test_knocking.py index 121aa88cfa..8c215d50f2 100644 --- a/tests/federation/transport/test_knocking.py +++ b/tests/federation/transport/test_knocking.py @@ -25,9 +25,6 @@ from tests.test_utils import event_injection from tests.unittest import FederatingHomeserverTestCase, TestCase, override_config -# An identifier to use while MSC2304 is not in a stable release of the spec -KNOCK_UNSTABLE_IDENTIFIER = "xyz.amorgan.knock" - class KnockingStrippedStateEventHelperMixin(TestCase): def send_example_state_events_to_room( @@ -61,7 +58,7 @@ def send_example_state_events_to_room( self.get_success( event_injection.inject_event( hs, - room_version=RoomVersions.MSC2403.identifier, + room_version=RoomVersions.V7.identifier, room_id=room_id, sender=sender, type="com.example.secret", @@ -121,7 +118,7 @@ def send_example_state_events_to_room( self.get_success( event_injection.inject_event( hs, - room_version=RoomVersions.MSC2403.identifier, + room_version=RoomVersions.V7.identifier, room_id=room_id, sender=sender, type=event_type, @@ -135,7 +132,7 @@ def send_example_state_events_to_room( room_state[EventTypes.Create] = { "content": { "creator": sender, - "room_version": RoomVersions.MSC2403.identifier, + "room_version": RoomVersions.V7.identifier, }, "state_key": "", } @@ -232,7 +229,7 @@ def test_room_state_returned_when_knocking(self): room_id = self.helper.create_room_as( "u1", is_public=False, - room_version=RoomVersions.MSC2403.identifier, + room_version=RoomVersions.V7.identifier, tok=user_token, ) @@ -243,14 +240,13 @@ def test_room_state_returned_when_knocking(self): channel = self.make_request( "GET", - "/_matrix/federation/unstable/%s/make_knock/%s/%s?ver=%s" + "/_matrix/federation/v1/make_knock/%s/%s?ver=%s" % ( - KNOCK_UNSTABLE_IDENTIFIER, room_id, fake_knocking_user_id, # Inform the remote that we support the room version of the room we're # knocking on - RoomVersions.MSC2403.identifier, + RoomVersions.V7.identifier, ), ) self.assertEquals(200, channel.code, channel.result) @@ -275,7 +271,7 @@ def test_room_state_returned_when_knocking(self): self.clock, self.hs.hostname, self.hs.signing_key, - room_version=RoomVersions.MSC2403, + room_version=RoomVersions.V7, event_dict=knock_event, ) @@ -287,8 +283,8 @@ def test_room_state_returned_when_knocking(self): # Send the signed knock event into the room channel = self.make_request( "PUT", - "/_matrix/federation/unstable/%s/send_knock/%s/%s" - % (KNOCK_UNSTABLE_IDENTIFIER, room_id, signed_knock_event.event_id), + "/_matrix/federation/v1/send_knock/%s/%s" + % (room_id, signed_knock_event.event_id), signed_knock_event_json, ) self.assertEquals(200, channel.code, channel.result) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index be5737e420..b52f78ba69 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -333,7 +333,7 @@ def prepare(self, reactor, clock, hs): self.room_id = self.helper.create_room_as( self.user_id, is_public=False, - room_version="xyz.amorgan.knock", + room_version="7", tok=self.tok, ) @@ -363,7 +363,7 @@ def test_knock_room_state(self): # Knock on a room channel = self.make_request( "POST", - "/_matrix/client/unstable/xyz.amorgan.knock/%s" % (self.room_id,), + "/_matrix/client/r0/knock/%s" % (self.room_id,), b"{}", self.knocker_tok, ) @@ -371,7 +371,7 @@ def test_knock_room_state(self): # We expect to see the knock event in the stripped room state later self.expected_room_state[EventTypes.Member] = { - "content": {"membership": "xyz.amorgan.knock", "displayname": "knocker"}, + "content": {"membership": "knock", "displayname": "knocker"}, "state_key": "@knocker:test", } @@ -384,7 +384,7 @@ def test_knock_room_state(self): self.assertEqual(channel.code, 200, channel.json_body) # Extract the stripped room state events from /sync - knock_entry = channel.json_body["rooms"]["xyz.amorgan.knock"] + knock_entry = channel.json_body["rooms"]["knock"] room_state_events = knock_entry[self.room_id]["knock_state"]["events"] # Validate that the knock membership event came last From 4911f7931d6f5cd65a13f7b1b5d3edecbab7c123 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 15 Jun 2021 08:03:17 -0400 Subject: [PATCH 281/619] Remove support for unstable MSC1772 prefixes. (#10161) The stable prefixes have been supported since v1.34.0. The unstable prefixes are not supported by any known clients. --- changelog.d/10161.removal | 1 + synapse/api/constants.py | 3 --- synapse/handlers/space_summary.py | 16 +++------------- 3 files changed, 4 insertions(+), 16 deletions(-) create mode 100644 changelog.d/10161.removal diff --git a/changelog.d/10161.removal b/changelog.d/10161.removal new file mode 100644 index 0000000000..d4411464c7 --- /dev/null +++ b/changelog.d/10161.removal @@ -0,0 +1 @@ +Stop supporting the unstable spaces prefixes from MSC1772. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 3940da5c88..ca13843680 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -112,8 +112,6 @@ class EventTypes: SpaceChild = "m.space.child" SpaceParent = "m.space.parent" - MSC1772_SPACE_CHILD = "org.matrix.msc1772.space.child" - MSC1772_SPACE_PARENT = "org.matrix.msc1772.space.parent" class ToDeviceEventTypes: @@ -180,7 +178,6 @@ class EventContentFields: # cf https://github.com/matrix-org/matrix-doc/pull/1772 ROOM_TYPE = "type" - MSC1772_ROOM_TYPE = "org.matrix.msc1772.type" class RoomEncryptionAlgorithms: diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 046dba6fd8..73d2aab15c 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -402,10 +402,7 @@ async def _summarize_remote_room( return (), () return res.rooms, tuple( - ev.data - for ev in res.events - if ev.event_type == EventTypes.MSC1772_SPACE_CHILD - or ev.event_type == EventTypes.SpaceChild + ev.data for ev in res.events if ev.event_type == EventTypes.SpaceChild ) async def _is_room_accessible( @@ -514,11 +511,6 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: current_state_ids[(EventTypes.Create, "")] ) - # TODO: update once MSC1772 lands - room_type = create_event.content.get(EventContentFields.ROOM_TYPE) - if not room_type: - room_type = create_event.content.get(EventContentFields.MSC1772_ROOM_TYPE) - room_version = await self._store.get_room_version(room_id) allowed_spaces = None if await self._event_auth_handler.has_restricted_join_rules( @@ -540,7 +532,7 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: ), "guest_can_join": stats["guest_access"] == "can_join", "creation_ts": create_event.origin_server_ts, - "room_type": room_type, + "room_type": create_event.content.get(EventContentFields.ROOM_TYPE), "allowed_spaces": allowed_spaces, } @@ -569,9 +561,7 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: [ event_id for key, event_id in current_state_ids.items() - # TODO: update once MSC1772 has been FCP for a period of time. - if key[0] == EventTypes.MSC1772_SPACE_CHILD - or key[0] == EventTypes.SpaceChild + if key[0] == EventTypes.SpaceChild ] ) From 1c8045f67477599fabc5759205c018e44d770078 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 15 Jun 2021 15:42:02 +0100 Subject: [PATCH 282/619] 1.36.0 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index aeec4fa5fa..0f9798a4d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.36.0 (2021-06-15) +=========================== + +No significant changes. + + Synapse 1.36.0rc2 (2021-06-11) ============================== diff --git a/debian/changelog b/debian/changelog index 084e878def..e640dadde9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.36.0) stable; urgency=medium + + * New synapse release 1.36.0. + + -- Synapse Packaging team Tue, 15 Jun 2021 15:41:53 +0100 + matrix-synapse-py3 (1.35.1) stable; urgency=medium * New synapse release 1.35.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 407ba14a76..c3016fc6ed 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.36.0rc2" +__version__ = "1.36.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 9e405034e59569c00916a87f643d879a286a7a34 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 16 Jun 2021 11:41:15 +0100 Subject: [PATCH 283/619] Make opentracing trace into event persistence (#10134) * Trace event persistence When we persist a batch of events, set the parent opentracing span to the that from the request, so that we can trace all the way in. * changelog * When we force tracing, set a baggage item ... so that we can check again later. * Link in both directions between persist_events spans --- changelog.d/10134.misc | 1 + synapse/api/auth.py | 4 +-- synapse/logging/opentracing.py | 57 +++++++++++++++++++++++++++++-- synapse/storage/persist_events.py | 46 ++++++++++++++++++++++--- 4 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10134.misc diff --git a/changelog.d/10134.misc b/changelog.d/10134.misc new file mode 100644 index 0000000000..ce9702645d --- /dev/null +++ b/changelog.d/10134.misc @@ -0,0 +1 @@ +Improve OpenTracing for event persistence. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 26a3b38918..cf4333a923 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -207,7 +207,7 @@ async def get_user_by_req( request.requester = user_id if user_id in self._force_tracing_for_users: - opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) + opentracing.force_tracing() opentracing.set_tag("authenticated_entity", user_id) opentracing.set_tag("user_id", user_id) opentracing.set_tag("appservice_id", app_service.id) @@ -260,7 +260,7 @@ async def get_user_by_req( request.requester = requester if user_info.token_owner in self._force_tracing_for_users: - opentracing.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) + opentracing.force_tracing() opentracing.set_tag("authenticated_entity", user_info.token_owner) opentracing.set_tag("user_id", user_info.user_id) if device_id: diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index 5b4725e035..4f18792c99 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -168,7 +168,7 @@ def set_fates(clotho, lachesis, atropos, father="Zues", mother="Themis"): import logging import re from functools import wraps -from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Type +from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Pattern, Type import attr @@ -278,6 +278,10 @@ class SynapseTags: DB_TXN_ID = "db.txn_id" +class SynapseBaggage: + FORCE_TRACING = "synapse-force-tracing" + + # Block everything by default # A regex which matches the server_names to expose traces for. # None means 'block everything'. @@ -285,6 +289,8 @@ class SynapseTags: # Util methods +Sentinel = object() + def only_if_tracing(func): """Executes the function only if we're tracing. Otherwise returns None.""" @@ -447,12 +453,28 @@ def start_active_span( ) -def start_active_span_follows_from(operation_name, contexts): +def start_active_span_follows_from( + operation_name: str, contexts: Collection, inherit_force_tracing=False +): + """Starts an active opentracing span, with additional references to previous spans + + Args: + operation_name: name of the operation represented by the new span + contexts: the previous spans to inherit from + inherit_force_tracing: if set, and any of the previous contexts have had tracing + forced, the new span will also have tracing forced. + """ if opentracing is None: return noop_context_manager() references = [opentracing.follows_from(context) for context in contexts] scope = start_active_span(operation_name, references=references) + + if inherit_force_tracing and any( + is_context_forced_tracing(ctx) for ctx in contexts + ): + force_tracing(scope.span) + return scope @@ -551,6 +573,10 @@ def start_active_span_from_edu( # Opentracing setters for tags, logs, etc +@only_if_tracing +def active_span(): + """Get the currently active span, if any""" + return opentracing.tracer.active_span @ensure_active_span("set a tag") @@ -571,6 +597,33 @@ def set_operation_name(operation_name): opentracing.tracer.active_span.set_operation_name(operation_name) +@only_if_tracing +def force_tracing(span=Sentinel) -> None: + """Force sampling for the active/given span and its children. + + Args: + span: span to force tracing for. By default, the active span. + """ + if span is Sentinel: + span = opentracing.tracer.active_span + if span is None: + logger.error("No active span in force_tracing") + return + + span.set_tag(opentracing.tags.SAMPLING_PRIORITY, 1) + + # also set a bit of baggage, so that we have a way of figuring out if + # it is enabled later + span.set_baggage_item(SynapseBaggage.FORCE_TRACING, "1") + + +def is_context_forced_tracing(span_context) -> bool: + """Check if sampling has been force for the given span context.""" + if span_context is None: + return False + return span_context.baggage.get(SynapseBaggage.FORCE_TRACING) is not None + + # Injection and extraction diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index c11f6c5845..dc38942bb1 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -18,6 +18,7 @@ import logging from collections import deque from typing import ( + Any, Awaitable, Callable, Collection, @@ -40,6 +41,7 @@ from synapse.api.constants import EventTypes, Membership from synapse.events import EventBase from synapse.events.snapshot import EventContext +from synapse.logging import opentracing from synapse.logging.context import PreserveLoggingContext, make_deferred_yieldable from synapse.metrics.background_process_metrics import run_as_background_process from synapse.storage.databases import Databases @@ -103,12 +105,18 @@ ) -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, slots=True) class _EventPersistQueueItem: events_and_contexts: List[Tuple[EventBase, EventContext]] backfilled: bool deferred: ObservableDeferred + parent_opentracing_span_contexts: List = [] + """A list of opentracing spans waiting for this batch""" + + opentracing_span_context: Any = None + """The opentracing span under which the persistence actually happened""" + _PersistResult = TypeVar("_PersistResult") @@ -171,9 +179,27 @@ async def add_to_queue( ) queue.append(end_item) + # add our events to the queue item end_item.events_and_contexts.extend(events_and_contexts) + + # also add our active opentracing span to the item so that we get a link back + span = opentracing.active_span() + if span: + end_item.parent_opentracing_span_contexts.append(span.context) + + # start a processor for the queue, if there isn't one already self._handle_queue(room_id) - return await make_deferred_yieldable(end_item.deferred.observe()) + + # wait for the queue item to complete + res = await make_deferred_yieldable(end_item.deferred.observe()) + + # add another opentracing span which links to the persist trace. + with opentracing.start_active_span_follows_from( + "persist_event_batch_complete", (end_item.opentracing_span_context,) + ): + pass + + return res def _handle_queue(self, room_id): """Attempts to handle the queue for a room if not already being handled. @@ -200,9 +226,17 @@ async def handle_queue_loop(): queue = self._get_drainining_queue(room_id) for item in queue: try: - ret = await self._per_item_callback( - item.events_and_contexts, item.backfilled - ) + with opentracing.start_active_span_follows_from( + "persist_event_batch", + item.parent_opentracing_span_contexts, + inherit_force_tracing=True, + ) as scope: + if scope: + item.opentracing_span_context = scope.span.context + + ret = await self._per_item_callback( + item.events_and_contexts, item.backfilled + ) except Exception: with PreserveLoggingContext(): item.deferred.errback() @@ -252,6 +286,7 @@ def __init__(self, hs, stores: Databases): self._event_persist_queue = _EventPeristenceQueue(self._persist_event_batch) self._state_resolution_handler = hs.get_state_resolution_handler() + @opentracing.trace async def persist_events( self, events_and_contexts: Iterable[Tuple[EventBase, EventContext]], @@ -307,6 +342,7 @@ async def enqueue(item): self.main_store.get_room_max_token(), ) + @opentracing.trace async def persist_event( self, event: EventBase, context: EventContext, backfilled: bool = False ) -> Tuple[EventBase, PersistedEventPosition, RoomStreamToken]: From 0adc2882c1a67419207a500f00c41a94be51857a Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:15:52 +0200 Subject: [PATCH 284/619] Fix broken links in documentation (#10180) * Fix broken links in documentation * newsfile --- changelog.d/10180.doc | 1 + docs/admin_api/README.rst | 4 +-- docs/admin_api/delete_group.md | 2 +- docs/admin_api/event_reports.md | 4 +-- docs/admin_api/media_admin_api.md | 4 +-- docs/admin_api/purge_history_api.md | 2 +- docs/admin_api/room_membership.md | 2 +- docs/admin_api/rooms.md | 2 +- docs/admin_api/statistics.md | 2 +- docs/admin_api/user_admin_api.md | 40 ++++++++++++++--------------- docs/consent_tracking.md | 4 +-- docs/federate.md | 6 ++--- docs/message_retention_policies.md | 4 +-- docs/metrics-howto.md | 3 +-- docs/presence_router_module.md | 2 +- docs/reverse_proxy.md | 2 +- docs/sso_mapping_providers.md | 4 +-- docs/systemd-with-workers/README.md | 14 +++++----- docs/workers.md | 2 +- 19 files changed, 53 insertions(+), 51 deletions(-) create mode 100644 changelog.d/10180.doc diff --git a/changelog.d/10180.doc b/changelog.d/10180.doc new file mode 100644 index 0000000000..1568450198 --- /dev/null +++ b/changelog.d/10180.doc @@ -0,0 +1 @@ +Fix broken links in documentation. \ No newline at end of file diff --git a/docs/admin_api/README.rst b/docs/admin_api/README.rst index 37cee87d32..8d6e76580a 100644 --- a/docs/admin_api/README.rst +++ b/docs/admin_api/README.rst @@ -2,7 +2,7 @@ Admin APIs ========== **Note**: The latest documentation can be viewed `here `_. -See `docs/README.md <../docs/README.md>`_ for more information. +See `docs/README.md <../README.md>`_ for more information. **Please update links to point to the website instead.** Existing files in this directory are preserved to maintain historical links, but may be moved in the future. @@ -10,5 +10,5 @@ are preserved to maintain historical links, but may be moved in the future. This directory includes documentation for the various synapse specific admin APIs available. Updates to the existing Admin API documentation should still be made to these files, but any new documentation files should instead be placed under -`docs/usage/administration/admin_api <../docs/usage/administration/admin_api>`_. +`docs/usage/administration/admin_api <../usage/administration/admin_api>`_. diff --git a/docs/admin_api/delete_group.md b/docs/admin_api/delete_group.md index 9c335ff759..2e0a1d2474 100644 --- a/docs/admin_api/delete_group.md +++ b/docs/admin_api/delete_group.md @@ -11,4 +11,4 @@ POST /_synapse/admin/v1/delete_group/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [Admin API](../../usage/administration/admin_api). +server admin: see [Admin API](../usage/administration/admin_api). diff --git a/docs/admin_api/event_reports.md b/docs/admin_api/event_reports.md index 186139185e..3abb06099c 100644 --- a/docs/admin_api/event_reports.md +++ b/docs/admin_api/event_reports.md @@ -7,7 +7,7 @@ The api is: GET /_synapse/admin/v1/event_reports?from=0&limit=10 ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [Admin API](../../usage/administration/admin_api). +server admin: see [Admin API](../usage/administration/admin_api). It returns a JSON body like the following: @@ -95,7 +95,7 @@ The api is: GET /_synapse/admin/v1/event_reports/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [Admin API](../../usage/administration/admin_api). +server admin: see [Admin API](../usage/administration/admin_api). It returns a JSON body like the following: diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index 9ab5269881..b033fc03ef 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -28,7 +28,7 @@ The API is: GET /_synapse/admin/v1/room//media ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [Admin API](../../usage/administration/admin_api). +server admin: see [Admin API](../usage/administration/admin_api). The API returns a JSON body like the following: ```json @@ -311,7 +311,7 @@ The following fields are returned in the JSON response body: * `deleted`: integer - The number of media items successfully deleted To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [Admin API](../../usage/administration/admin_api). +server admin: see [Admin API](../usage/administration/admin_api). If the user re-requests purged remote media, synapse will re-request the media from the originating server. diff --git a/docs/admin_api/purge_history_api.md b/docs/admin_api/purge_history_api.md index 25decc3e61..13b991eacf 100644 --- a/docs/admin_api/purge_history_api.md +++ b/docs/admin_api/purge_history_api.md @@ -17,7 +17,7 @@ POST /_synapse/admin/v1/purge_history/[/] ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) By default, events sent by local users are not deleted, as they may represent the only copies of this content in existence. (Events sent by remote users are diff --git a/docs/admin_api/room_membership.md b/docs/admin_api/room_membership.md index ed40366099..8a5ce191df 100644 --- a/docs/admin_api/room_membership.md +++ b/docs/admin_api/room_membership.md @@ -24,7 +24,7 @@ POST /_synapse/admin/v1/join/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: see [Admin API](../../usage/administration/admin_api). +server admin: see [Admin API](../usage/administration/admin_api). Response: diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index dc007fa00e..bb7828a525 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -443,7 +443,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an ``access_token`` for a -server admin: see [Admin API](../../usage/administration/admin_api). +server admin: see [Admin API](../usage/administration/admin_api). A response body like the following is returned: diff --git a/docs/admin_api/statistics.md b/docs/admin_api/statistics.md index d93d52a3ac..1901f1eea0 100644 --- a/docs/admin_api/statistics.md +++ b/docs/admin_api/statistics.md @@ -10,7 +10,7 @@ GET /_synapse/admin/v1/statistics/users/media ``` To use it, you will need to authenticate by providing an `access_token` -for a server admin: see [Admin API](../../usage/administration/admin_api). +for a server admin: see [Admin API](../usage/administration/admin_api). A response body like the following is returned: diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index c835e4a0cd..ef1e735e33 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -11,7 +11,7 @@ GET /_synapse/admin/v2/users/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) It returns a JSON body like the following: @@ -78,7 +78,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) URL parameters: @@ -119,7 +119,7 @@ GET /_synapse/admin/v2/users?from=0&limit=10&guests=false ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -237,7 +237,7 @@ See also: [Client Server API Whois](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-admin-whois-userid). To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) It returns a JSON body like the following: @@ -294,7 +294,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) The erase parameter is optional and defaults to `false`. An empty body may be passed for backwards compatibility. @@ -339,7 +339,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) The parameter `new_password` is required. The parameter `logout_devices` is optional and defaults to `true`. @@ -354,7 +354,7 @@ GET /_synapse/admin/v1/users//admin ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -384,7 +384,7 @@ with a body of: ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) ## List room memberships of a user @@ -398,7 +398,7 @@ GET /_synapse/admin/v1/users//joined_rooms ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -443,7 +443,7 @@ GET /_synapse/admin/v1/users//media ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -591,7 +591,7 @@ GET /_synapse/admin/v2/users//devices ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -659,7 +659,7 @@ POST /_synapse/admin/v2/users//delete_devices ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) An empty JSON dict is returned. @@ -683,7 +683,7 @@ GET /_synapse/admin/v2/users//devices/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -731,7 +731,7 @@ PUT /_synapse/admin/v2/users//devices/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) An empty JSON dict is returned. @@ -760,7 +760,7 @@ DELETE /_synapse/admin/v2/users//devices/ ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) An empty JSON dict is returned. @@ -781,7 +781,7 @@ GET /_synapse/admin/v1/users//pushers ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -872,7 +872,7 @@ POST /_synapse/admin/v1/users//shadow_ban ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) An empty JSON dict is returned. @@ -897,7 +897,7 @@ GET /_synapse/admin/v1/users//override_ratelimit ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -939,7 +939,7 @@ POST /_synapse/admin/v1/users//override_ratelimit ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) A response body like the following is returned: @@ -984,7 +984,7 @@ DELETE /_synapse/admin/v1/users//override_ratelimit ``` To use it, you will need to authenticate by providing an `access_token` for a -server admin: [Admin API](../../usage/administration/admin_api) +server admin: [Admin API](../usage/administration/admin_api) An empty JSON dict is returned. diff --git a/docs/consent_tracking.md b/docs/consent_tracking.md index c586b5f0b6..3f997e5903 100644 --- a/docs/consent_tracking.md +++ b/docs/consent_tracking.md @@ -24,8 +24,8 @@ To enable this, first create templates for the policy and success pages. These should be stored on the local filesystem. These templates use the [Jinja2](http://jinja.pocoo.org) templating language, -and [docs/privacy_policy_templates](privacy_policy_templates) gives -examples of the sort of thing that can be done. +and [docs/privacy_policy_templates](https://github.com/matrix-org/synapse/tree/develop/docs/privacy_policy_templates/) +gives examples of the sort of thing that can be done. Note that the templates must be stored under a name giving the language of the template - currently this must always be `en` (for "English"); diff --git a/docs/federate.md b/docs/federate.md index b15cd724d1..89c2b19638 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -14,7 +14,7 @@ you set the `server_name` to match your machine's public DNS hostname. For this default configuration to work, you will need to listen for TLS connections on port 8448. The preferred way to do that is by using a -reverse proxy: see [reverse_proxy.md]() for instructions +reverse proxy: see [reverse_proxy.md](reverse_proxy.md) for instructions on how to correctly set one up. In some cases you might not want to run Synapse on the machine that has @@ -44,7 +44,7 @@ a complicated dance which requires connections in both directions). Another common problem is that people on other servers can't join rooms that you invite them to. This can be caused by an incorrectly-configured reverse -proxy: see [reverse_proxy.md]() for instructions on how to correctly +proxy: see [reverse_proxy.md](reverse_proxy.md) for instructions on how to correctly configure a reverse proxy. ### Known issues @@ -63,4 +63,4 @@ release of Synapse. If you want to get up and running quickly with a trio of homeservers in a private federation, there is a script in the `demo` directory. This is mainly -useful just for development purposes. See [demo/README](<../demo/README>). +useful just for development purposes. See [demo/README](https://github.com/matrix-org/synapse/tree/develop/demo/). diff --git a/docs/message_retention_policies.md b/docs/message_retention_policies.md index 75d2028e17..ea3d46cc10 100644 --- a/docs/message_retention_policies.md +++ b/docs/message_retention_policies.md @@ -51,7 +51,7 @@ clients. Support for this feature can be enabled and configured in the `retention` section of the Synapse configuration file (see the -[sample file](https://github.com/matrix-org/synapse/blob/v1.7.3/docs/sample_config.yaml#L332-L393)). +[sample file](https://github.com/matrix-org/synapse/blob/v1.36.0/docs/sample_config.yaml#L451-L518)). To enable support for message retention policies, set the setting `enabled` in this section to `true`. @@ -87,7 +87,7 @@ expired events from the database. They are only run if support for message retention policies is enabled in the server's configuration. If no configuration for purge jobs is configured by the server admin, Synapse will use a default configuration, which is described in the -[sample configuration file](https://github.com/matrix-org/synapse/blob/master/docs/sample_config.yaml#L332-L393). +[sample configuration file](https://github.com/matrix-org/synapse/blob/v1.36.0/docs/sample_config.yaml#L451-L518). Some server admins might want a finer control on when events are removed depending on an event's room's policy. This can be done by setting the diff --git a/docs/metrics-howto.md b/docs/metrics-howto.md index 6b84153274..4a77d5604c 100644 --- a/docs/metrics-howto.md +++ b/docs/metrics-howto.md @@ -72,8 +72,7 @@ ## Monitoring workers -To monitor a Synapse installation using -[workers](https://github.com/matrix-org/synapse/blob/master/docs/workers.md), +To monitor a Synapse installation using [workers](workers.md), every worker needs to be monitored independently, in addition to the main homeserver process. This is because workers don't send their metrics to the main homeserver process, but expose them diff --git a/docs/presence_router_module.md b/docs/presence_router_module.md index d2844915df..bf859e4254 100644 --- a/docs/presence_router_module.md +++ b/docs/presence_router_module.md @@ -30,7 +30,7 @@ presence to (for those users that the receiving user is considered interested in It does not include state for users who are currently offline, and it can only be called on workers that support sending federation. Additionally, this method must only be called from the process that has been configured to write to the -the [presence stream](https://github.com/matrix-org/synapse/blob/master/docs/workers.md#stream-writers). +the [presence stream](workers.md#stream-writers). By default, this is the main process, but another worker can be configured to do so. diff --git a/docs/reverse_proxy.md b/docs/reverse_proxy.md index cf1b835b9d..01db466f96 100644 --- a/docs/reverse_proxy.md +++ b/docs/reverse_proxy.md @@ -21,7 +21,7 @@ port 8448. Where these are different, we refer to the 'client port' and the 'federation port'. See [the Matrix specification](https://matrix.org/docs/spec/server_server/latest#resolving-server-names) for more details of the algorithm used for federation connections, and -[delegate.md]() for instructions on setting up delegation. +[delegate.md](delegate.md) for instructions on setting up delegation. **NOTE**: Your reverse proxy must not `canonicalise` or `normalise` the requested URI in any way (for example, by decoding `%xx` escapes). diff --git a/docs/sso_mapping_providers.md b/docs/sso_mapping_providers.md index 6db2dc8be5..7a407012e0 100644 --- a/docs/sso_mapping_providers.md +++ b/docs/sso_mapping_providers.md @@ -108,7 +108,7 @@ A custom mapping provider must specify the following methods: Synapse has a built-in OpenID mapping provider if a custom provider isn't specified in the config. It is located at -[`synapse.handlers.oidc.JinjaOidcMappingProvider`](../synapse/handlers/oidc.py). +[`synapse.handlers.oidc.JinjaOidcMappingProvider`](https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/oidc.py). ## SAML Mapping Providers @@ -194,4 +194,4 @@ A custom mapping provider must specify the following methods: Synapse has a built-in SAML mapping provider if a custom provider isn't specified in the config. It is located at -[`synapse.handlers.saml.DefaultSamlMappingProvider`](../synapse/handlers/saml.py). +[`synapse.handlers.saml.DefaultSamlMappingProvider`](https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/saml.py). diff --git a/docs/systemd-with-workers/README.md b/docs/systemd-with-workers/README.md index a1135e9ed5..a7de2de88a 100644 --- a/docs/systemd-with-workers/README.md +++ b/docs/systemd-with-workers/README.md @@ -6,16 +6,18 @@ well as a `matrix-synapse-worker@` service template for any workers you require. Additionally, to group the required services, it sets up a `matrix-synapse.target`. -See the folder [system](system) for the systemd unit files. +See the folder [system](https://github.com/matrix-org/synapse/tree/develop/docs/systemd-with-workers/system/) +for the systemd unit files. -The folder [workers](workers) contains an example configuration for the -`federation_reader` worker. +The folder [workers](https://github.com/matrix-org/synapse/tree/develop/docs/systemd-with-workers/workers/) +contains an example configuration for the `federation_reader` worker. ## Synapse configuration files See [workers.md](../workers.md) for information on how to set up the configuration files and reverse-proxy correctly. You can find an example worker -config in the [workers](workers) folder. +config in the [workers](https://github.com/matrix-org/synapse/tree/develop/docs/systemd-with-workers/workers/) +folder. Systemd manages daemonization itself, so ensure that none of the configuration files set either `daemonize` or `worker_daemonize`. @@ -29,8 +31,8 @@ There is no need for a separate configuration file for the master process. ## Set up 1. Adjust synapse configuration files as above. -1. Copy the `*.service` and `*.target` files in [system](system) to -`/etc/systemd/system`. +1. Copy the `*.service` and `*.target` files in [system](https://github.com/matrix-org/synapse/tree/develop/docs/systemd-with-workers/system/) +to `/etc/systemd/system`. 1. Run `systemctl daemon-reload` to tell systemd to load the new unit files. 1. Run `systemctl enable matrix-synapse.service`. This will configure the synapse master process to be started as part of the `matrix-synapse.target` diff --git a/docs/workers.md b/docs/workers.md index 46b5e4b737..797758ee84 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -16,7 +16,7 @@ workers only work with PostgreSQL-based Synapse deployments. SQLite should only be used for demo purposes and any admin considering workers should already be running PostgreSQL. -See also https://matrix.org/blog/2020/11/03/how-we-fixed-synapses-scalability +See also [Matrix.org blog post](https://matrix.org/blog/2020/11/03/how-we-fixed-synapses-scalability) for a higher level overview. ## Main process/worker communication From 2c240213f4c1d9d44d121441c3b9d4f893ed16cc Mon Sep 17 00:00:00 2001 From: Lukas Lihotzki Date: Wed, 16 Jun 2021 14:16:35 +0200 Subject: [PATCH 285/619] Fix requestOpenIdToken response: integer expires_in (#10175) `expires_in` must be an integer according to the OpenAPI spec: https://github.com/matrix-org/matrix-doc/blob/master/data/api/client-server/definitions/openid_token.yaml#L32 True division (`/`) returns a float instead (`"expires_in": 3600.0`). Floor division (`//`) returns an integer, so the response is spec compliant. Signed-off-by: Lukas Lihotzki --- changelog.d/10175.bugfix | 1 + synapse/rest/client/v2_alpha/openid.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10175.bugfix diff --git a/changelog.d/10175.bugfix b/changelog.d/10175.bugfix new file mode 100644 index 0000000000..42e8f749cc --- /dev/null +++ b/changelog.d/10175.bugfix @@ -0,0 +1 @@ +Fix a minor bug in the response to `/_matrix/client/r0/user/{user}/openid/request_token`. Contributed by @lukaslihotzki. diff --git a/synapse/rest/client/v2_alpha/openid.py b/synapse/rest/client/v2_alpha/openid.py index d3322acc38..e8d2673819 100644 --- a/synapse/rest/client/v2_alpha/openid.py +++ b/synapse/rest/client/v2_alpha/openid.py @@ -85,7 +85,7 @@ async def on_POST(self, request, user_id): "access_token": token, "token_type": "Bearer", "matrix_server_name": self.server_name, - "expires_in": self.EXPIRES_MS / 1000, + "expires_in": self.EXPIRES_MS // 1000, }, ) From 36c426e294a53d2192cc9f29ec5c93e84e222228 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 16 Jun 2021 13:29:54 +0100 Subject: [PATCH 286/619] Add debug logging when we enter/exit Measure block (#10183) It can be helpful to know when trying to track down slow requests. --- changelog.d/10183.misc | 1 + synapse/util/metrics.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 changelog.d/10183.misc diff --git a/changelog.d/10183.misc b/changelog.d/10183.misc new file mode 100644 index 0000000000..c0e01ad3db --- /dev/null +++ b/changelog.d/10183.misc @@ -0,0 +1 @@ +Add debug logging for when we enter and exit `Measure` blocks. diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 6d14351bd2..45353d41c5 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -133,12 +133,17 @@ def __enter__(self) -> "Measure": self.start = self.clock.time() self._logging_context.__enter__() in_flight.register((self.name,), self._update_in_flight) + + logger.debug("Entering block %s", self.name) + return self def __exit__(self, exc_type, exc_val, exc_tb): if self.start is None: raise RuntimeError("Measure() block exited without being entered") + logger.debug("Exiting block %s", self.name) + duration = self.clock.time() - self.start usage = self.get_resource_usage() From b8b282aa32063d712e276373b6bc90c39cecc353 Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Wed, 16 Jun 2021 13:31:55 +0100 Subject: [PATCH 287/619] A guide to the request log lines format. (#8436) This doc is short but a useful guide to what the request log lines mean. Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Erik Johnston Co-authored-by: Daniele Sluijters --- changelog.d/8436.doc | 1 + docs/SUMMARY.md | 1 + docs/usage/administration/request_log.md | 44 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 changelog.d/8436.doc create mode 100644 docs/usage/administration/request_log.md diff --git a/changelog.d/8436.doc b/changelog.d/8436.doc new file mode 100644 index 0000000000..77fc098200 --- /dev/null +++ b/changelog.d/8436.doc @@ -0,0 +1 @@ +Add a new guide to decoding request logs. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index af2c968c9a..01ef4ff600 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -61,6 +61,7 @@ - [Server Version](admin_api/version_api.md) - [Manhole](manhole.md) - [Monitoring](metrics-howto.md) + - [Request log format](usage/administration/request_log.md) - [Scripts]() # Development diff --git a/docs/usage/administration/request_log.md b/docs/usage/administration/request_log.md new file mode 100644 index 0000000000..316304c734 --- /dev/null +++ b/docs/usage/administration/request_log.md @@ -0,0 +1,44 @@ +# Request log format + +HTTP request logs are written by synapse (see [`site.py`](../synapse/http/site.py) for details). + +See the following for how to decode the dense data available from the default logging configuration. + +``` +2020-10-01 12:00:00,000 - synapse.access.http.8008 - 311 - INFO - PUT-1000- 192.168.0.1 - 8008 - {another-matrix-server.com} Processed request: 0.100sec/-0.000sec (0.000sec, 0.000sec) (0.001sec/0.090sec/3) 11B !200 "PUT /_matrix/federation/v1/send/1600000000000 HTTP/1.1" "Synapse/1.20.1" [0 dbevts] +-AAAAAAAAAAAAAAAAAAAAA- -BBBBBBBBBBBBBBBBBBBBBB- -C- -DD- -EEEEEE- -FFFFFFFFF- -GG- -HHHHHHHHHHHHHHHHHHHHHHH- -IIIIII- -JJJJJJJ- -KKKKKK-, -LLLLLL- -MMMMMMM- -NNNNNN- O -P- -QQ- -RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR- -SSSSSSSSSSSS- -TTTTTT- +``` + + +| Part | Explanation | +| ----- | ------------ | +| AAAA | Timestamp request was logged (not recieved) | +| BBBB | Logger name (`synapse.access.(http\|https).`, where 'tag' is defined in the `listeners` config section, normally the port) | +| CCCC | Line number in code | +| DDDD | Log Level | +| EEEE | Request Identifier (This identifier is shared by related log lines)| +| FFFF | Source IP (Or X-Forwarded-For if enabled) | +| GGGG | Server Port | +| HHHH | Federated Server or Local User making request (blank if unauthenticated or not supplied) | +| IIII | Total Time to process the request | +| JJJJ | Time to send response over network once generated (this may be negative if the socket is closed before the response is generated)| +| KKKK | Userland CPU time | +| LLLL | System CPU time | +| MMMM | Total time waiting for a free DB connection from the pool across all parallel DB work from this request | +| NNNN | Total time waiting for response to DB queries across all parallel DB work from this request | +| OOOO | Count of DB transactions performed | +| PPPP | Response body size | +| QQQQ | Response status code (prefixed with ! if the socket was closed before the response was generated) | +| RRRR | Request | +| SSSS | User-agent | +| TTTT | Events fetched from DB to service this request (note that this does not include events fetched from the cache) | + + +MMMM / NNNN can be greater than IIII if there are multiple slow database queries +running in parallel. + +Some actions can result in multiple identical http requests, which will return +the same data, but only the first request will report time/transactions in +`KKKK`/`LLLL`/`MMMM`/`NNNN`/`OOOO` - the others will be awaiting the first query to return a +response and will simultaneously return with the first request, but with very +small processing times. From 76f9c701c3920d83c0fe8f08b9197e2e92e12dad Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 16 Jun 2021 11:07:28 -0400 Subject: [PATCH 288/619] Always require users to re-authenticate for dangerous operations. (#10184) Dangerous actions means deactivating an account, modifying an account password, or adding a 3PID. Other actions (deleting devices, uploading keys) can re-use the same UI auth session if ui_auth.session_timeout is configured. --- changelog.d/10184.bugfix | 1 + docs/sample_config.yaml | 4 ++++ synapse/config/auth.py | 4 ++++ synapse/handlers/auth.py | 7 ++++++- synapse/rest/client/v2_alpha/devices.py | 6 ++++++ synapse/rest/client/v2_alpha/keys.py | 3 +++ 6 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10184.bugfix diff --git a/changelog.d/10184.bugfix b/changelog.d/10184.bugfix new file mode 100644 index 0000000000..6bf440d8f8 --- /dev/null +++ b/changelog.d/10184.bugfix @@ -0,0 +1 @@ +Always require users to re-authenticate for dangerous operations: deactivating an account, modifying an account password, and adding 3PIDs. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index f8925a5e24..2ab88eb14e 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -2318,6 +2318,10 @@ ui_auth: # the user-interactive authentication process, by allowing for multiple # (and potentially different) operations to use the same validation session. # + # This is ignored for potentially "dangerous" operations (including + # deactivating an account, modifying an account password, and + # adding a 3PID). + # # Uncomment below to allow for credential validation to last for 15 # seconds. # diff --git a/synapse/config/auth.py b/synapse/config/auth.py index e10d641a96..53809cee2e 100644 --- a/synapse/config/auth.py +++ b/synapse/config/auth.py @@ -103,6 +103,10 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # the user-interactive authentication process, by allowing for multiple # (and potentially different) operations to use the same validation session. # + # This is ignored for potentially "dangerous" operations (including + # deactivating an account, modifying an account password, and + # adding a 3PID). + # # Uncomment below to allow for credential validation to last for 15 # seconds. # diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 8a6666a4ad..1971e373ed 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -302,6 +302,7 @@ async def validate_user_via_ui_auth( request: SynapseRequest, request_body: Dict[str, Any], description: str, + can_skip_ui_auth: bool = False, ) -> Tuple[dict, Optional[str]]: """ Checks that the user is who they claim to be, via a UI auth. @@ -320,6 +321,10 @@ async def validate_user_via_ui_auth( description: A human readable string to be displayed to the user that describes the operation happening on their account. + can_skip_ui_auth: True if the UI auth session timeout applies this + action. Should be set to False for any "dangerous" + actions (e.g. deactivating an account). + Returns: A tuple of (params, session_id). @@ -343,7 +348,7 @@ async def validate_user_via_ui_auth( """ if not requester.access_token_id: raise ValueError("Cannot validate a user without an access token") - if self._ui_auth_session_timeout: + if can_skip_ui_auth and self._ui_auth_session_timeout: last_validated = await self.store.get_access_token_last_validated( requester.access_token_id ) diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/v2_alpha/devices.py index 9af05f9b11..8b9674db06 100644 --- a/synapse/rest/client/v2_alpha/devices.py +++ b/synapse/rest/client/v2_alpha/devices.py @@ -86,6 +86,9 @@ async def on_POST(self, request): request, body, "remove device(s) from your account", + # Users might call this multiple times in a row while cleaning up + # devices, allow a single UI auth session to be re-used. + can_skip_ui_auth=True, ) await self.device_handler.delete_devices( @@ -135,6 +138,9 @@ async def on_DELETE(self, request, device_id): request, body, "remove a device from your account", + # Users might call this multiple times in a row while cleaning up + # devices, allow a single UI auth session to be re-used. + can_skip_ui_auth=True, ) await self.device_handler.delete_device(requester.user.to_string(), device_id) diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 4a28f2c072..33cf8de186 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -277,6 +277,9 @@ async def on_POST(self, request): request, body, "add a device signing key to your account", + # Allow skipping of UI auth since this is frequently called directly + # after login and it is silly to ask users to re-auth immediately. + can_skip_ui_auth=True, ) result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) From 18edc9ab06d8ed07c1cac918057226fad18030ce Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 16 Jun 2021 14:18:02 -0400 Subject: [PATCH 289/619] Improve comments in the structured logging code. (#10188) --- changelog.d/10188.misc | 1 + synapse/logging/_terse_json.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/10188.misc diff --git a/changelog.d/10188.misc b/changelog.d/10188.misc new file mode 100644 index 0000000000..c1ea81c21a --- /dev/null +++ b/changelog.d/10188.misc @@ -0,0 +1 @@ +Improve comments in structured logging code. diff --git a/synapse/logging/_terse_json.py b/synapse/logging/_terse_json.py index 8002a250a2..6e82f7c7f1 100644 --- a/synapse/logging/_terse_json.py +++ b/synapse/logging/_terse_json.py @@ -20,8 +20,9 @@ _encoder = json.JSONEncoder(ensure_ascii=False, separators=(",", ":")) -# The properties of a standard LogRecord. -_LOG_RECORD_ATTRIBUTES = { +# The properties of a standard LogRecord that should be ignored when generating +# JSON logs. +_IGNORED_LOG_RECORD_ATTRIBUTES = { "args", "asctime", "created", @@ -59,9 +60,9 @@ def format(self, record: logging.LogRecord) -> str: return self._format(record, event) def _format(self, record: logging.LogRecord, event: dict) -> str: - # Add any extra attributes to the event. + # Add attributes specified via the extra keyword to the logged event. for key, value in record.__dict__.items(): - if key not in _LOG_RECORD_ATTRIBUTES: + if key not in _IGNORED_LOG_RECORD_ATTRIBUTES: event[key] = value return _encoder.encode(event) From 52c60bd0a96cd61583209a9ef6d8270425e8a902 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 17 Jun 2021 11:21:53 +0100 Subject: [PATCH 290/619] Fix persist_events to stop leaking opentracing contexts (#10193) --- changelog.d/10193.misc | 1 + synapse/storage/persist_events.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10193.misc diff --git a/changelog.d/10193.misc b/changelog.d/10193.misc new file mode 100644 index 0000000000..ce9702645d --- /dev/null +++ b/changelog.d/10193.misc @@ -0,0 +1 @@ +Improve OpenTracing for event persistence. diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index dc38942bb1..051095fea9 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -111,7 +111,7 @@ class _EventPersistQueueItem: backfilled: bool deferred: ObservableDeferred - parent_opentracing_span_contexts: List = [] + parent_opentracing_span_contexts: List = attr.ib(factory=list) """A list of opentracing spans waiting for this batch""" opentracing_span_context: Any = None From a911dd768bc0dc49df9a47ca864b737174345bb7 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 17 Jun 2021 08:59:45 -0500 Subject: [PATCH 291/619] Add fields to better debug where events are being soft_failed (#10168) Follow-up to https://github.com/matrix-org/synapse/pull/10156#discussion_r650292223 --- changelog.d/10168.misc | 1 + synapse/handlers/federation.py | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10168.misc diff --git a/changelog.d/10168.misc b/changelog.d/10168.misc new file mode 100644 index 0000000000..5ca7b89806 --- /dev/null +++ b/changelog.d/10168.misc @@ -0,0 +1 @@ +Add extra logging fields to better debug where events are being soft failed. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index b3a93212f1..1ecdafaadd 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2423,7 +2423,11 @@ async def _persist_auth_tree( ) async def _check_for_soft_fail( - self, event: EventBase, state: Optional[Iterable[EventBase]], backfilled: bool + self, + event: EventBase, + state: Optional[Iterable[EventBase]], + backfilled: bool, + origin: str, ) -> None: """Checks if we should soft fail the event; if so, marks the event as such. @@ -2432,6 +2436,7 @@ async def _check_for_soft_fail( event state: The state at the event if we don't have all the event's prev events backfilled: Whether the event is from backfill + origin: The host the event originates from. """ # For new (non-backfilled and non-outlier) events we check if the event # passes auth based on the current state. If it doesn't then we @@ -2501,7 +2506,17 @@ async def _check_for_soft_fail( try: event_auth.check(room_version_obj, event, auth_events=current_auth_events) except AuthError as e: - logger.warning("Soft-failing %r because %s", event, e) + logger.warning( + "Soft-failing %r (from %s) because %s", + event, + e, + origin, + extra={ + "room_id": event.room_id, + "mxid": event.sender, + "hs": origin, + }, + ) soft_failed_event_counter.inc() event.internal_metadata.soft_failed = True @@ -2614,7 +2629,7 @@ async def _check_event_auth( context.rejected = RejectedReason.AUTH_ERROR if not context.rejected: - await self._check_for_soft_fail(event, state, backfilled) + await self._check_for_soft_fail(event, state, backfilled, origin=origin) if event.type == EventTypes.GuestAccess and not context.rejected: await self.maybe_kick_guest_users(event) From 6f1a28de195445352bb1ffcc5d0a90581a348400 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 17 Jun 2021 15:04:26 +0100 Subject: [PATCH 292/619] Fix incorrect time magnitude on delayed call (#10195) Fixes https://github.com/matrix-org/synapse/issues/10030. We were expecting milliseconds where we should have provided a value in seconds. The impact of this bug isn't too bad. The code is intended to count the number of remote servers that the homeserver can see and report that as a metric. This metric is supposed to run initially 1 second after server startup, and every 60s as well. Instead, it ran 1,000 seconds after server startup, and every 60s after startup. This fix allows for the correct metrics to be collected immediately, as well as preventing a random collection 1,000s in the future after startup. --- changelog.d/10195.bugfix | 1 + synapse/storage/databases/main/roommember.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10195.bugfix diff --git a/changelog.d/10195.bugfix b/changelog.d/10195.bugfix new file mode 100644 index 0000000000..01cab1bda8 --- /dev/null +++ b/changelog.d/10195.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synpase 1.7.2 where remote server count metrics collection would be incorrectly delayed on startup. Found by @heftig. \ No newline at end of file diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 5fc3bb5a7d..2796354a1f 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -90,7 +90,7 @@ def __init__(self, database: DatabasePool, db_conn, hs): 60 * 1000, ) self.hs.get_clock().call_later( - 1000, + 1, self._count_known_servers, ) LaterGauge( From 8070b893dbc7b9e17b4843f70e4f4e32a52ce58a Mon Sep 17 00:00:00 2001 From: Marcus Date: Thu, 17 Jun 2021 16:20:06 +0200 Subject: [PATCH 293/619] update black to 21.6b0 (#10197) Reformat all files with the new version. Signed-off-by: Marcus Hoffmann --- changelog.d/10197.misc | 1 + contrib/experiments/cursesio.py | 8 ++++---- setup.py | 2 +- synapse/handlers/federation.py | 2 +- synapse/http/servlet.py | 2 +- synapse/replication/tcp/handler.py | 2 +- synapse/types.py | 4 ++-- tests/handlers/test_appservice.py | 2 +- tests/handlers/test_directory.py | 2 +- tests/handlers/test_profile.py | 2 +- tests/handlers/test_register.py | 2 +- tests/handlers/test_sync.py | 2 +- tests/rest/client/v1/test_events.py | 2 +- tests/rest/client/v1/test_presence.py | 2 +- tests/rest/client/v1/test_rooms.py | 16 ++++++++-------- tests/rest/client/v1/test_typing.py | 2 +- tests/storage/test_base.py | 2 +- 17 files changed, 28 insertions(+), 27 deletions(-) create mode 100644 changelog.d/10197.misc diff --git a/changelog.d/10197.misc b/changelog.d/10197.misc new file mode 100644 index 0000000000..cbb3b454be --- /dev/null +++ b/changelog.d/10197.misc @@ -0,0 +1 @@ +Upgrade `black` linting tool to 21.6b0. diff --git a/contrib/experiments/cursesio.py b/contrib/experiments/cursesio.py index cff73650e6..7695cc77ca 100644 --- a/contrib/experiments/cursesio.py +++ b/contrib/experiments/cursesio.py @@ -46,14 +46,14 @@ def set_callback(self, callback): self.callback = callback def fileno(self): - """ We want to select on FD 0 """ + """We want to select on FD 0""" return 0 def connectionLost(self, reason): self.close() def print_line(self, text): - """ add a line to the internal list of lines""" + """add a line to the internal list of lines""" self.lines.append(text) self.redraw() @@ -92,7 +92,7 @@ def printLogLine(self, text): ) def doRead(self): - """ Input is ready! """ + """Input is ready!""" curses.noecho() c = self.stdscr.getch() # read a character @@ -132,7 +132,7 @@ def logPrefix(self): return "CursesStdIO" def close(self): - """ clean up """ + """clean up""" curses.nocbreak() self.stdscr.keypad(0) diff --git a/setup.py b/setup.py index e2e488761d..1081548e00 100755 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ def exec_file(path_segments): # We pin black so that our tests don't start failing on new releases. CONDITIONAL_REQUIREMENTS["lint"] = [ "isort==5.7.0", - "black==20.8b1", + "black==21.6b0", "flake8-comprehensions", "flake8-bugbear==21.3.2", "flake8", diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 1ecdafaadd..0bfb25802a 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1961,7 +1961,7 @@ async def on_make_leave_request( return event async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: - """ We have received a leave event for a room. Fully process it.""" + """We have received a leave event for a room. Fully process it.""" event = pdu logger.debug( diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 3c43f32586..fda8da21b7 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -454,7 +454,7 @@ class attribute containing a pre-compiled regular expression. The automatic """ def register(self, http_server): - """ Register this servlet with the given HTTP server. """ + """Register this servlet with the given HTTP server.""" patterns = getattr(self, "PATTERNS", None) if patterns: for method in ("GET", "PUT", "POST", "DELETE"): diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index 7ced4c543c..2ad7a200bb 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -571,7 +571,7 @@ async def _process_position( def on_REMOTE_SERVER_UP( self, conn: IReplicationConnection, cmd: RemoteServerUpCommand ): - """"Called when get a new REMOTE_SERVER_UP command.""" + """Called when get a new REMOTE_SERVER_UP command.""" self._replication_data_handler.on_remote_server_up(cmd.data) self._notifier.notify_remote_server_up(cmd.data) diff --git a/synapse/types.py b/synapse/types.py index e52cd7ffd4..0bdf32659c 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -284,14 +284,14 @@ class RoomAlias(DomainSpecificString): @attr.s(slots=True, frozen=True, repr=False) class RoomID(DomainSpecificString): - """Structure representing a room id. """ + """Structure representing a room id.""" SIGIL = "!" @attr.s(slots=True, frozen=True, repr=False) class EventID(DomainSpecificString): - """Structure representing an event id. """ + """Structure representing an event id.""" SIGIL = "$" diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py index 5d6cc2885f..024c5e963c 100644 --- a/tests/handlers/test_appservice.py +++ b/tests/handlers/test_appservice.py @@ -26,7 +26,7 @@ class AppServiceHandlerTestCase(unittest.TestCase): - """ Tests the ApplicationServicesHandler. """ + """Tests the ApplicationServicesHandler.""" def setUp(self): self.mock_store = Mock() diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 1908d3c2c6..7a8041ab44 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -27,7 +27,7 @@ class DirectoryTestCase(unittest.HomeserverTestCase): - """ Tests the directory service. """ + """Tests the directory service.""" def make_homeserver(self, reactor, clock): self.mock_federation = Mock() diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 5330a9b34e..cdb41101b3 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -23,7 +23,7 @@ class ProfileTestCase(unittest.HomeserverTestCase): - """ Tests profile management. """ + """Tests profile management.""" def make_homeserver(self, reactor, clock): self.mock_federation = Mock() diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index bd43190523..c51763f41a 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -28,7 +28,7 @@ class RegistrationTestCase(unittest.HomeserverTestCase): - """ Tests the RegistrationHandler. """ + """Tests the RegistrationHandler.""" def make_homeserver(self, reactor, clock): hs_config = self.default_config() diff --git a/tests/handlers/test_sync.py b/tests/handlers/test_sync.py index c8b43305f4..84f05f6c58 100644 --- a/tests/handlers/test_sync.py +++ b/tests/handlers/test_sync.py @@ -22,7 +22,7 @@ class SyncTestCase(tests.unittest.HomeserverTestCase): - """ Tests Sync Handler. """ + """Tests Sync Handler.""" def prepare(self, reactor, clock, hs): self.hs = hs diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py index 852bda408c..2789d51546 100644 --- a/tests/rest/client/v1/test_events.py +++ b/tests/rest/client/v1/test_events.py @@ -23,7 +23,7 @@ class EventStreamPermissionsTestCase(unittest.HomeserverTestCase): - """ Tests event streaming (GET /events). """ + """Tests event streaming (GET /events).""" servlets = [ events.register_servlets, diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 409f3949dc..597e4c67de 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -24,7 +24,7 @@ class PresenceTestCase(unittest.HomeserverTestCase): - """ Tests presence REST API. """ + """Tests presence REST API.""" user_id = "@sid:red" diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 5b1096d091..e94566ffd7 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -64,7 +64,7 @@ async def _insert_client_ip(*args, **kwargs): class RoomPermissionsTestCase(RoomBase): - """ Tests room permissions. """ + """Tests room permissions.""" user_id = "@sid1:red" rmcreator_id = "@notme:red" @@ -377,7 +377,7 @@ def test_leave_permissions(self): class RoomsMemberListTestCase(RoomBase): - """ Tests /rooms/$room_id/members/list REST events.""" + """Tests /rooms/$room_id/members/list REST events.""" user_id = "@sid1:red" @@ -416,7 +416,7 @@ def test_get_member_list_mixed_memberships(self): class RoomsCreateTestCase(RoomBase): - """ Tests /rooms and /rooms/$room_id REST events. """ + """Tests /rooms and /rooms/$room_id REST events.""" user_id = "@sid1:red" @@ -502,7 +502,7 @@ def test_post_room_invitees_ratelimit(self): class RoomTopicTestCase(RoomBase): - """ Tests /rooms/$room_id/topic REST events. """ + """Tests /rooms/$room_id/topic REST events.""" user_id = "@sid1:red" @@ -566,7 +566,7 @@ def test_rooms_topic_with_extra_keys(self): class RoomMemberStateTestCase(RoomBase): - """ Tests /rooms/$room_id/members/$user_id/state REST events. """ + """Tests /rooms/$room_id/members/$user_id/state REST events.""" user_id = "@sid1:red" @@ -790,7 +790,7 @@ def test_autojoin_rooms(self): class RoomMessagesTestCase(RoomBase): - """ Tests /rooms/$room_id/messages/$user_id/$msg_id REST events. """ + """Tests /rooms/$room_id/messages/$user_id/$msg_id REST events.""" user_id = "@sid1:red" @@ -838,7 +838,7 @@ def test_rooms_messages_sent(self): class RoomInitialSyncTestCase(RoomBase): - """ Tests /rooms/$room_id/initialSync. """ + """Tests /rooms/$room_id/initialSync.""" user_id = "@sid1:red" @@ -879,7 +879,7 @@ def test_initial_sync(self): class RoomMessageListTestCase(RoomBase): - """ Tests /rooms/$room_id/messages REST events. """ + """Tests /rooms/$room_id/messages REST events.""" user_id = "@sid1:red" diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 0aad48a162..44e22ca999 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -26,7 +26,7 @@ class RoomTypingTestCase(unittest.HomeserverTestCase): - """ Tests /rooms/$room_id/typing/$user_id REST API. """ + """Tests /rooms/$room_id/typing/$user_id REST API.""" user_id = "@sid:red" diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index 3b45a7efd8..ddad44bd6c 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -27,7 +27,7 @@ class SQLBaseStoreTestCase(unittest.TestCase): - """ Test the "simple" SQL generating methods in SQLBaseStore. """ + """Test the "simple" SQL generating methods in SQLBaseStore.""" def setUp(self): self.db_pool = Mock(spec=["runInteraction"]) From 9cf6e0eae759ce0b6197ba4afc636c4f431ab606 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 17 Jun 2021 16:22:41 +0100 Subject: [PATCH 294/619] Rip out the DNS lookup limiter (#10190) As I've written in various places in the past (#7113, #9865) I'm pretty sure this is doing nothing useful at all. --- changelog.d/10190.misc | 1 + synapse/app/_base.py | 104 ----------------------------------------- 2 files changed, 1 insertion(+), 104 deletions(-) create mode 100644 changelog.d/10190.misc diff --git a/changelog.d/10190.misc b/changelog.d/10190.misc new file mode 100644 index 0000000000..388ed3ffb6 --- /dev/null +++ b/changelog.d/10190.misc @@ -0,0 +1 @@ +Remove redundant DNS lookup limiter. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 1329af2e2b..575bd30d27 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -38,7 +38,6 @@ from synapse.logging.context import PreserveLoggingContext from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.metrics.jemalloc import setup_jemalloc_stats -from synapse.util.async_helpers import Linearizer from synapse.util.daemonize import daemonize_process from synapse.util.rlimit import change_resource_limit from synapse.util.versionstring import get_version_string @@ -112,8 +111,6 @@ def start_reactor( run_command (Callable[]): callable that actually runs the reactor """ - install_dns_limiter(reactor) - def run(): logger.info("Running") setup_jemalloc_stats() @@ -398,107 +395,6 @@ def setup_sdnotify(hs): ) -def install_dns_limiter(reactor, max_dns_requests_in_flight=100): - """Replaces the resolver with one that limits the number of in flight DNS - requests. - - This is to workaround https://twistedmatrix.com/trac/ticket/9620, where we - can run out of file descriptors and infinite loop if we attempt to do too - many DNS queries at once - - XXX: I'm confused by this. reactor.nameResolver does not use twisted.names unless - you explicitly install twisted.names as the resolver; rather it uses a GAIResolver - backed by the reactor's default threadpool (which is limited to 10 threads). So - (a) I don't understand why twisted ticket 9620 is relevant, and (b) I don't - understand why we would run out of FDs if we did too many lookups at once. - -- richvdh 2020/08/29 - """ - new_resolver = _LimitedHostnameResolver( - reactor.nameResolver, max_dns_requests_in_flight - ) - - reactor.installNameResolver(new_resolver) - - -class _LimitedHostnameResolver: - """Wraps a IHostnameResolver, limiting the number of in-flight DNS lookups.""" - - def __init__(self, resolver, max_dns_requests_in_flight): - self._resolver = resolver - self._limiter = Linearizer( - name="dns_client_limiter", max_count=max_dns_requests_in_flight - ) - - def resolveHostName( - self, - resolutionReceiver, - hostName, - portNumber=0, - addressTypes=None, - transportSemantics="TCP", - ): - # We need this function to return `resolutionReceiver` so we do all the - # actual logic involving deferreds in a separate function. - - # even though this is happening within the depths of twisted, we need to drop - # our logcontext before starting _resolve, otherwise: (a) _resolve will drop - # the logcontext if it returns an incomplete deferred; (b) _resolve will - # call the resolutionReceiver *with* a logcontext, which it won't be expecting. - with PreserveLoggingContext(): - self._resolve( - resolutionReceiver, - hostName, - portNumber, - addressTypes, - transportSemantics, - ) - - return resolutionReceiver - - @defer.inlineCallbacks - def _resolve( - self, - resolutionReceiver, - hostName, - portNumber=0, - addressTypes=None, - transportSemantics="TCP", - ): - - with (yield self._limiter.queue(())): - # resolveHostName doesn't return a Deferred, so we need to hook into - # the receiver interface to get told when resolution has finished. - - deferred = defer.Deferred() - receiver = _DeferredResolutionReceiver(resolutionReceiver, deferred) - - self._resolver.resolveHostName( - receiver, hostName, portNumber, addressTypes, transportSemantics - ) - - yield deferred - - -class _DeferredResolutionReceiver: - """Wraps a IResolutionReceiver and simply resolves the given deferred when - resolution is complete - """ - - def __init__(self, receiver, deferred): - self._receiver = receiver - self._deferred = deferred - - def resolutionBegan(self, resolutionInProgress): - self._receiver.resolutionBegan(resolutionInProgress) - - def addressResolved(self, address): - self._receiver.addressResolved(address) - - def resolutionComplete(self): - self._deferred.callback(()) - self._receiver.resolutionComplete() - - sdnotify_sockaddr = os.getenv("NOTIFY_SOCKET") From fcf3c7032b96ab454120f86f3f070160c409d599 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 17 Jun 2021 16:23:11 +0100 Subject: [PATCH 295/619] Ensure that we do not cache empty sync responses after a timeout (#10158) Fixes #8518 by telling the ResponseCache not to cache the /sync response if the next_batch param is the same as the since token. --- changelog.d/10157.bugfix | 1 + changelog.d/10157.misc | 1 - changelog.d/10158.bugfix | 1 + synapse/handlers/sync.py | 36 +++++++++++++----- synapse/python_dependencies.py | 6 +-- synapse/types.py | 2 +- tests/rest/client/v2_alpha/test_sync.py | 50 +++++++++++++++++++++++++ tests/server.py | 8 ++-- 8 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 changelog.d/10157.bugfix delete mode 100644 changelog.d/10157.misc create mode 100644 changelog.d/10158.bugfix diff --git a/changelog.d/10157.bugfix b/changelog.d/10157.bugfix new file mode 100644 index 0000000000..6eaaa05b80 --- /dev/null +++ b/changelog.d/10157.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.21.0 which could cause `/sync` to return immediately with an empty response. diff --git a/changelog.d/10157.misc b/changelog.d/10157.misc deleted file mode 100644 index 6c1d0e6e59..0000000000 --- a/changelog.d/10157.misc +++ /dev/null @@ -1 +0,0 @@ -Extend `ResponseCache` to pass a context object into the callback. diff --git a/changelog.d/10158.bugfix b/changelog.d/10158.bugfix new file mode 100644 index 0000000000..6eaaa05b80 --- /dev/null +++ b/changelog.d/10158.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.21.0 which could cause `/sync` to return immediately with an empty response. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 7f2138d804..b9a0361059 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -49,7 +49,7 @@ from synapse.util.async_helpers import concurrently_execute from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.caches.lrucache import LruCache -from synapse.util.caches.response_cache import ResponseCache +from synapse.util.caches.response_cache import ResponseCache, ResponseCacheContext from synapse.util.metrics import Measure, measure_func from synapse.visibility import filter_events_for_client @@ -83,12 +83,15 @@ LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE = 100 +SyncRequestKey = Tuple[Any, ...] + + @attr.s(slots=True, frozen=True) class SyncConfig: user = attr.ib(type=UserID) filter_collection = attr.ib(type=FilterCollection) is_guest = attr.ib(type=bool) - request_key = attr.ib(type=Tuple[Any, ...]) + request_key = attr.ib(type=SyncRequestKey) device_id = attr.ib(type=Optional[str]) @@ -266,9 +269,9 @@ def __init__(self, hs: "HomeServer"): self.presence_handler = hs.get_presence_handler() self.event_sources = hs.get_event_sources() self.clock = hs.get_clock() - self.response_cache = ResponseCache( + self.response_cache: ResponseCache[SyncRequestKey] = ResponseCache( hs.get_clock(), "sync" - ) # type: ResponseCache[Tuple[Any, ...]] + ) self.state = hs.get_state_handler() self.auth = hs.get_auth() self.storage = hs.get_storage() @@ -307,6 +310,7 @@ async def wait_for_sync_for_user( since_token, timeout, full_state, + cache_context=True, ) logger.debug("Returning sync response for %s", user_id) return res @@ -314,9 +318,10 @@ async def wait_for_sync_for_user( async def _wait_for_sync_for_user( self, sync_config: SyncConfig, - since_token: Optional[StreamToken] = None, - timeout: int = 0, - full_state: bool = False, + since_token: Optional[StreamToken], + timeout: int, + full_state: bool, + cache_context: ResponseCacheContext[SyncRequestKey], ) -> SyncResult: if since_token is None: sync_type = "initial_sync" @@ -343,13 +348,13 @@ async def _wait_for_sync_for_user( if timeout == 0 or since_token is None or full_state: # we are going to return immediately, so don't bother calling # notifier.wait_for_events. - result = await self.current_sync_for_user( + result: SyncResult = await self.current_sync_for_user( sync_config, since_token, full_state=full_state ) else: - def current_sync_callback(before_token, after_token): - return self.current_sync_for_user(sync_config, since_token) + async def current_sync_callback(before_token, after_token) -> SyncResult: + return await self.current_sync_for_user(sync_config, since_token) result = await self.notifier.wait_for_events( sync_config.user.to_string(), @@ -358,6 +363,17 @@ def current_sync_callback(before_token, after_token): from_token=since_token, ) + # if nothing has happened in any of the users' rooms since /sync was called, + # the resultant next_batch will be the same as since_token (since the result + # is generated when wait_for_events is first called, and not regenerated + # when wait_for_events times out). + # + # If that happens, we mustn't cache it, so that when the client comes back + # with the same cache token, we don't immediately return the same empty + # result, causing a tightloop. (#8518) + if result.next_batch == since_token: + cache_context.should_cache = False + if result: if sync_config.filter_collection.lazy_load_members(): lazy_loaded = "true" diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 546231bec0..bf361c42d6 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -75,11 +75,9 @@ "phonenumbers>=8.2.0", # we use GaugeHistogramMetric, which was added in prom-client 0.4.0. "prometheus_client>=0.4.0", - # we use attr.validators.deep_iterable, which arrived in 19.1.0 (Note: - # Fedora 31 only has 19.1, so if we want to upgrade we should wait until 33 - # is out in November.) + # we use `order`, which arrived in attrs 19.2.0. # Note: 21.1.0 broke `/sync`, see #9936 - "attrs>=19.1.0,!=21.1.0", + "attrs>=19.2.0,!=21.1.0", "netaddr>=0.7.18", "Jinja2>=2.9", "bleach>=1.4.3", diff --git a/synapse/types.py b/synapse/types.py index 0bdf32659c..8d2fa00f71 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -404,7 +404,7 @@ def f2(m): return username.decode("ascii") -@attr.s(frozen=True, slots=True, cmp=False) +@attr.s(frozen=True, slots=True, order=False) class RoomStreamToken: """Tokens are positions between events. The token "s1" comes after event 1. diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index b52f78ba69..012910f136 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -558,3 +558,53 @@ def _check_unread_count(self, expected_count: int): # Store the next batch for the next request. self.next_batch = channel.json_body["next_batch"] + + +class SyncCacheTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + sync.register_servlets, + ] + + def test_noop_sync_does_not_tightloop(self): + """If the sync times out, we shouldn't cache the result + + Essentially a regression test for #8518. + """ + self.user_id = self.register_user("kermit", "monkey") + self.tok = self.login("kermit", "monkey") + + # we should immediately get an initial sync response + channel = self.make_request("GET", "/sync", access_token=self.tok) + self.assertEqual(channel.code, 200, channel.json_body) + + # now, make an incremental sync request, with a timeout + next_batch = channel.json_body["next_batch"] + channel = self.make_request( + "GET", + f"/sync?since={next_batch}&timeout=10000", + access_token=self.tok, + await_result=False, + ) + # that should block for 10 seconds + with self.assertRaises(TimedOutException): + channel.await_result(timeout_ms=9900) + channel.await_result(timeout_ms=200) + self.assertEqual(channel.code, 200, channel.json_body) + + # we expect the next_batch in the result to be the same as before + self.assertEqual(channel.json_body["next_batch"], next_batch) + + # another incremental sync should also block. + channel = self.make_request( + "GET", + f"/sync?since={next_batch}&timeout=10000", + access_token=self.tok, + await_result=False, + ) + # that should block for 10 seconds + with self.assertRaises(TimedOutException): + channel.await_result(timeout_ms=9900) + channel.await_result(timeout_ms=200) + self.assertEqual(channel.code, 200, channel.json_body) diff --git a/tests/server.py b/tests/server.py index 9df8cda24f..f32d8dc375 100644 --- a/tests/server.py +++ b/tests/server.py @@ -138,21 +138,19 @@ def isSecure(self): def transport(self): return self - def await_result(self, timeout: int = 100) -> None: + def await_result(self, timeout_ms: int = 1000) -> None: """ Wait until the request is finished. """ + end_time = self._reactor.seconds() + timeout_ms / 1000.0 self._reactor.run() - x = 0 while not self.is_finished(): # If there's a producer, tell it to resume producing so we get content if self._producer: self._producer.resumeProducing() - x += 1 - - if x > timeout: + if self._reactor.seconds() > end_time: raise TimedOutException("Timed out waiting for request to finish.") self._reactor.advance(0.1) From 8c97d5863f352e48cb4e64a5b663411a7779686d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 17 Jun 2021 12:53:27 -0400 Subject: [PATCH 296/619] Update MSC3083 support per changes in the MSC. (#10189) Adds a "type" field and generalize "space" to "room_id". --- changelog.d/10189.misc | 1 + synapse/api/constants.py | 6 +++++ synapse/handlers/event_auth.py | 45 ++++++++++++++++++------------- synapse/handlers/space_summary.py | 26 +++++++++--------- 4 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 changelog.d/10189.misc diff --git a/changelog.d/10189.misc b/changelog.d/10189.misc new file mode 100644 index 0000000000..df0e636c7d --- /dev/null +++ b/changelog.d/10189.misc @@ -0,0 +1 @@ +Update MSC3083 support for modifications in the MSC. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index ca13843680..6c3958f7ab 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -65,6 +65,12 @@ class JoinRules: MSC3083_RESTRICTED = "restricted" +class RestrictedJoinRuleTypes: + """Understood types for the allow rules in restricted join rules.""" + + ROOM_MEMBERSHIP = "m.room_membership" + + class LoginType: PASSWORD = "m.login.password" EMAIL_IDENTITY = "m.login.email.identity" diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index a0df16a32f..989996b628 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -13,7 +13,12 @@ # limitations under the License. from typing import TYPE_CHECKING, Collection, Optional -from synapse.api.constants import EventTypes, JoinRules, Membership +from synapse.api.constants import ( + EventTypes, + JoinRules, + Membership, + RestrictedJoinRuleTypes, +) from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersion from synapse.events import EventBase @@ -42,7 +47,7 @@ async def check_restricted_join_rules( Check whether a user can join a room without an invite due to restricted join rules. When joining a room with restricted joined rules (as defined in MSC3083), - the membership of spaces must be checked during a room join. + the membership of rooms must be checked during a room join. Args: state_ids: The state of the room as it currently is. @@ -67,20 +72,20 @@ async def check_restricted_join_rules( if not await self.has_restricted_join_rules(state_ids, room_version): return - # Get the spaces which allow access to this room and check if the user is + # Get the rooms which allow access to this room and check if the user is # in any of them. - allowed_spaces = await self.get_spaces_that_allow_join(state_ids) - if not await self.is_user_in_rooms(allowed_spaces, user_id): + allowed_rooms = await self.get_rooms_that_allow_join(state_ids) + if not await self.is_user_in_rooms(allowed_rooms, user_id): raise AuthError( 403, - "You do not belong to any of the required spaces to join this room.", + "You do not belong to any of the required rooms to join this room.", ) async def has_restricted_join_rules( self, state_ids: StateMap[str], room_version: RoomVersion ) -> bool: """ - Return if the room has the proper join rules set for access via spaces. + Return if the room has the proper join rules set for access via rooms. Args: state_ids: The state of the room as it currently is. @@ -102,17 +107,17 @@ async def has_restricted_join_rules( join_rules_event = await self._store.get_event(join_rules_event_id) return join_rules_event.content.get("join_rule") == JoinRules.MSC3083_RESTRICTED - async def get_spaces_that_allow_join( + async def get_rooms_that_allow_join( self, state_ids: StateMap[str] ) -> Collection[str]: """ - Generate a list of spaces which allow access to a room. + Generate a list of rooms in which membership allows access to a room. Args: - state_ids: The state of the room as it currently is. + state_ids: The current state of the room the user wishes to join Returns: - A collection of spaces which provide membership to the room. + A collection of room IDs. Membership in any of the rooms in the list grants the ability to join the target room. """ # If there's no join rule, then it defaults to invite (so this doesn't apply). join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) @@ -123,21 +128,25 @@ async def get_spaces_that_allow_join( join_rules_event = await self._store.get_event(join_rules_event_id) # If allowed is of the wrong form, then only allow invited users. - allowed_spaces = join_rules_event.content.get("allow", []) - if not isinstance(allowed_spaces, list): + allow_list = join_rules_event.content.get("allow", []) + if not isinstance(allow_list, list): return () # Pull out the other room IDs, invalid data gets filtered. result = [] - for space in allowed_spaces: - if not isinstance(space, dict): + for allow in allow_list: + if not isinstance(allow, dict): + continue + + # If the type is unexpected, skip it. + if allow.get("type") != RestrictedJoinRuleTypes.ROOM_MEMBERSHIP: continue - space_id = space.get("space") - if not isinstance(space_id, str): + room_id = allow.get("room_id") + if not isinstance(room_id, str): continue - result.append(space_id) + result.append(room_id) return result diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 73d2aab15c..e953a8afe6 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -160,14 +160,14 @@ async def get_space_summary( # Check if the user is a member of any of the allowed spaces # from the response. - allowed_spaces = room.get("allowed_spaces") + allowed_rooms = room.get("allowed_spaces") if ( not include_room - and allowed_spaces - and isinstance(allowed_spaces, list) + and allowed_rooms + and isinstance(allowed_rooms, list) ): include_room = await self._event_auth_handler.is_user_in_rooms( - allowed_spaces, requester + allowed_rooms, requester ) # Finally, if this isn't the requested room, check ourselves @@ -455,11 +455,11 @@ async def _is_room_accessible( if self._event_auth_handler.has_restricted_join_rules( state_ids, room_version ): - allowed_spaces = ( - await self._event_auth_handler.get_spaces_that_allow_join(state_ids) + allowed_rooms = ( + await self._event_auth_handler.get_rooms_that_allow_join(state_ids) ) if await self._event_auth_handler.is_user_in_rooms( - allowed_spaces, requester + allowed_rooms, requester ): return True @@ -475,10 +475,10 @@ async def _is_room_accessible( if await self._event_auth_handler.has_restricted_join_rules( state_ids, room_version ): - allowed_spaces = ( - await self._event_auth_handler.get_spaces_that_allow_join(state_ids) + allowed_rooms = ( + await self._event_auth_handler.get_rooms_that_allow_join(state_ids) ) - for space_id in allowed_spaces: + for space_id in allowed_rooms: if await self._auth.check_host_in_room(space_id, origin): return True @@ -512,11 +512,11 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: ) room_version = await self._store.get_room_version(room_id) - allowed_spaces = None + allowed_rooms = None if await self._event_auth_handler.has_restricted_join_rules( current_state_ids, room_version ): - allowed_spaces = await self._event_auth_handler.get_spaces_that_allow_join( + allowed_rooms = await self._event_auth_handler.get_rooms_that_allow_join( current_state_ids ) @@ -533,7 +533,7 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: "guest_can_join": stats["guest_access"] == "can_join", "creation_ts": create_event.origin_server_ts, "room_type": create_event.content.get(EventContentFields.ROOM_TYPE), - "allowed_spaces": allowed_spaces, + "allowed_spaces": allowed_rooms, } # Filter out Nones – rather omit the field altogether From 08c84693227de9571412fa18a7d82818a370c655 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 17 Jun 2021 19:56:48 +0200 Subject: [PATCH 297/619] Remove support for ACME v1 (#10194) Fixes #9778 ACME v1 has been fully decommissioned for existing installs on June 1st 2021(see https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430/27), so we can now safely remove it from Synapse. --- INSTALL.md | 5 +- README.rst | 7 - changelog.d/10194.removal | 1 + docker/conf/homeserver.yaml | 6 - docs/ACME.md | 161 ----------------------- docs/MSC1711_certificates_FAQ.md | 28 +--- docs/sample_config.yaml | 84 +----------- mypy.ini | 3 - synapse/app/_base.py | 3 +- synapse/app/homeserver.py | 48 ------- synapse/config/_base.py | 5 - synapse/config/_base.pyi | 1 - synapse/config/tls.py | 151 +-------------------- synapse/handlers/acme.py | 117 ---------------- synapse/handlers/acme_issuing_service.py | 127 ------------------ synapse/python_dependencies.py | 5 - synapse/server.py | 5 - tests/config/test_tls.py | 97 -------------- 18 files changed, 18 insertions(+), 836 deletions(-) create mode 100644 changelog.d/10194.removal delete mode 100644 docs/ACME.md delete mode 100644 synapse/handlers/acme.py delete mode 100644 synapse/handlers/acme_issuing_service.py diff --git a/INSTALL.md b/INSTALL.md index 3c498edd29..b0697052c1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -442,10 +442,7 @@ so, you will need to edit `homeserver.yaml`, as follows: - You will also need to uncomment the `tls_certificate_path` and `tls_private_key_path` lines under the `TLS` section. You will need to manage - provisioning of these certificates yourself — Synapse had built-in ACME - support, but the ACMEv1 protocol Synapse implements is deprecated, not - allowed by LetsEncrypt for new sites, and will break for existing sites in - late 2020. See [ACME.md](docs/ACME.md). + provisioning of these certificates yourself. If you are using your own certificate, be sure to use a `.pem` file that includes the full certificate chain including any intermediate certificates diff --git a/README.rst b/README.rst index 1c9f05cc85..2ecc93c8a7 100644 --- a/README.rst +++ b/README.rst @@ -142,13 +142,6 @@ the form of:: As when logging in, you will need to specify a "Custom server". Specify your desired ``localpart`` in the 'User name' box. -ACME setup -========== - -For details on having Synapse manage your federation TLS certificates -automatically, please see ``_. - - Security note ============= diff --git a/changelog.d/10194.removal b/changelog.d/10194.removal new file mode 100644 index 0000000000..74874df4eb --- /dev/null +++ b/changelog.d/10194.removal @@ -0,0 +1 @@ +Remove Synapse's support for automatically fetching and renewing certificates using the ACME v1 protocol. This protocol has been fully turned off by Let's Encrypt for existing install on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. diff --git a/docker/conf/homeserver.yaml b/docker/conf/homeserver.yaml index 2b23d7f428..3cba594d02 100644 --- a/docker/conf/homeserver.yaml +++ b/docker/conf/homeserver.yaml @@ -7,12 +7,6 @@ tls_certificate_path: "/data/{{ SYNAPSE_SERVER_NAME }}.tls.crt" tls_private_key_path: "/data/{{ SYNAPSE_SERVER_NAME }}.tls.key" -{% if SYNAPSE_ACME %} -acme: - enabled: true - port: 8009 -{% endif %} - {% endif %} ## Server ## diff --git a/docs/ACME.md b/docs/ACME.md deleted file mode 100644 index a7a498f575..0000000000 --- a/docs/ACME.md +++ /dev/null @@ -1,161 +0,0 @@ -# ACME - -From version 1.0 (June 2019) onwards, Synapse requires valid TLS -certificates for communication between servers (by default on port -`8448`) in addition to those that are client-facing (port `443`). To -help homeserver admins fulfil this new requirement, Synapse v0.99.0 -introduced support for automatically provisioning certificates through -[Let's Encrypt](https://letsencrypt.org/) using the ACME protocol. - -## Deprecation of ACME v1 - -In [March 2019](https://community.letsencrypt.org/t/end-of-life-plan-for-acmev1/88430), -Let's Encrypt announced that they were deprecating version 1 of the ACME -protocol, with the plan to disable the use of it for new accounts in -November 2019, for new domains in June 2020, and for existing accounts and -domains in June 2021. - -Synapse doesn't currently support version 2 of the ACME protocol, which -means that: - -* for existing installs, Synapse's built-in ACME support will continue - to work until June 2021. -* for new installs, this feature will not work at all. - -Either way, it is recommended to move from Synapse's ACME support -feature to an external automated tool such as [certbot](https://github.com/certbot/certbot) -(or browse [this list](https://letsencrypt.org/fr/docs/client-options/) -for an alternative ACME client). - -It's also recommended to use a reverse proxy for the server-facing -communications (more documentation about this can be found -[here](/docs/reverse_proxy.md)) as well as the client-facing ones and -have it serve the certificates. - -In case you can't do that and need Synapse to serve them itself, make -sure to set the `tls_certificate_path` configuration setting to the path -of the certificate (make sure to use the certificate containing the full -certification chain, e.g. `fullchain.pem` if using certbot) and -`tls_private_key_path` to the path of the matching private key. Note -that in this case you will need to restart Synapse after each -certificate renewal so that Synapse stops using the old certificate. - -If you still want to use Synapse's built-in ACME support, the rest of -this document explains how to set it up. - -## Initial setup - -In the case that your `server_name` config variable is the same as -the hostname that the client connects to, then the same certificate can be -used between client and federation ports without issue. - -If your configuration file does not already have an `acme` section, you can -generate an example config by running the `generate_config` executable. For -example: - -``` -~/synapse/env3/bin/generate_config -``` - -You will need to provide Let's Encrypt (or another ACME provider) access to -your Synapse ACME challenge responder on port 80, at the domain of your -homeserver. This requires you to either change the port of the ACME listener -provided by Synapse to a high port and reverse proxy to it, or use a tool -like `authbind` to allow Synapse to listen on port 80 without root access. -(Do not run Synapse with root permissions!) Detailed instructions are -available under "ACME setup" below. - -If you already have certificates, you will need to back up or delete them -(files `example.com.tls.crt` and `example.com.tls.key` in Synapse's root -directory), Synapse's ACME implementation will not overwrite them. - -## ACME setup - -The main steps for enabling ACME support in short summary are: - -1. Allow Synapse to listen for incoming ACME challenges. -1. Enable ACME support in `homeserver.yaml`. -1. Move your old certificates (files `example.com.tls.crt` and `example.com.tls.key` out of the way if they currently exist at the paths specified in `homeserver.yaml`. -1. Restart Synapse. - -Detailed instructions for each step are provided below. - -### Listening on port 80 - -In order for Synapse to complete the ACME challenge to provision a -certificate, it needs access to port 80. Typically listening on port 80 is -only granted to applications running as root. There are thus two solutions to -this problem. - -#### Using a reverse proxy - -A reverse proxy such as Apache or nginx allows a single process (the web -server) to listen on port 80 and proxy traffic to the appropriate program -running on your server. It is the recommended method for setting up ACME as -it allows you to use your existing webserver while also allowing Synapse to -provision certificates as needed. - -For nginx users, add the following line to your existing `server` block: - -``` -location /.well-known/acme-challenge { - proxy_pass http://localhost:8009; -} -``` - -For Apache, add the following to your existing webserver config: - -``` -ProxyPass /.well-known/acme-challenge http://localhost:8009/.well-known/acme-challenge -``` - -Make sure to restart/reload your webserver after making changes. - -Now make the relevant changes in `homeserver.yaml` to enable ACME support: - -``` -acme: - enabled: true - port: 8009 -``` - -#### Authbind - -`authbind` allows a program which does not run as root to bind to -low-numbered ports in a controlled way. The setup is simpler, but requires a -webserver not to already be running on port 80. **This includes every time -Synapse renews a certificate**, which may be cumbersome if you usually run a -web server on port 80. Nevertheless, if you're sure port 80 is not being used -for any other purpose then all that is necessary is the following: - -Install `authbind`. For example, on Debian/Ubuntu: - -``` -sudo apt-get install authbind -``` - -Allow `authbind` to bind port 80: - -``` -sudo touch /etc/authbind/byport/80 -sudo chmod 777 /etc/authbind/byport/80 -``` - -When Synapse is started, use the following syntax: - -``` -authbind --deep -``` - -Make the relevant changes in `homeserver.yaml` to enable ACME support: - -``` -acme: - enabled: true -``` - -### (Re)starting synapse - -Ensure that the certificate paths specified in `homeserver.yaml` (`tls_certificate_path` and `tls_private_key_path`) do not currently point to any files. Synapse will not provision certificates if files exist, as it does not want to overwrite existing certificates. - -Finally, start/restart Synapse. diff --git a/docs/MSC1711_certificates_FAQ.md b/docs/MSC1711_certificates_FAQ.md index 80bd1294c7..ce8189d4ed 100644 --- a/docs/MSC1711_certificates_FAQ.md +++ b/docs/MSC1711_certificates_FAQ.md @@ -101,15 +101,6 @@ In this case, your `server_name` points to the host where your Synapse is running. There is no need to create a `.well-known` URI or an SRV record, but you will need to give Synapse a valid, signed, certificate. -The easiest way to do that is with Synapse's built-in ACME (Let's Encrypt) -support. Full details are in [ACME.md](./ACME.md) but, in a nutshell: - - 1. Allow Synapse to listen on port 80 with `authbind`, or forward it from a - reverse proxy. - 2. Enable acme support in `homeserver.yaml`. - 3. Move your old certificates out of the way. - 4. Restart Synapse. - ### If you do have an SRV record currently If you are using an SRV record, your matrix domain (`server_name`) may not @@ -130,15 +121,9 @@ In this situation, you have three choices for how to proceed: #### Option 1: give Synapse a certificate for your matrix domain Synapse 1.0 will expect your server to present a TLS certificate for your -`server_name` (`example.com` in the above example). You can achieve this by -doing one of the following: - - * Acquire a certificate for the `server_name` yourself (for example, using - `certbot`), and give it and the key to Synapse via `tls_certificate_path` - and `tls_private_key_path`, or: - - * Use Synapse's [ACME support](./ACME.md), and forward port 80 on the - `server_name` domain to your Synapse instance. +`server_name` (`example.com` in the above example). You can achieve this by acquiring a +certificate for the `server_name` yourself (for example, using `certbot`), and giving it +and the key to Synapse via `tls_certificate_path` and `tls_private_key_path`. #### Option 2: run Synapse behind a reverse proxy @@ -161,10 +146,9 @@ You can do this with a `.well-known` file as follows: with Synapse 0.34 and earlier. 2. Give Synapse a certificate corresponding to the target domain - (`customer.example.net` in the above example). You can either use Synapse's - built-in [ACME support](./ACME.md) for this (via the `domain` parameter in - the `acme` section), or acquire a certificate yourself and give it to - Synapse via `tls_certificate_path` and `tls_private_key_path`. + (`customer.example.net` in the above example). You can do this by acquire a + certificate for the target domain and giving it to Synapse via `tls_certificate_path` + and `tls_private_key_path`. 3. Restart Synapse to ensure the new certificate is loaded. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 2ab88eb14e..307f8cd3c8 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -552,13 +552,9 @@ retention: # This certificate, as of Synapse 1.0, will need to be a valid and verifiable # certificate, signed by a recognised Certificate Authority. # -# See 'ACME support' below to enable auto-provisioning this certificate via -# Let's Encrypt. -# -# If supplying your own, be sure to use a `.pem` file that includes the -# full certificate chain including any intermediate certificates (for -# instance, if using certbot, use `fullchain.pem` as your certificate, -# not `cert.pem`). +# Be sure to use a `.pem` file that includes the full certificate chain including +# any intermediate certificates (for instance, if using certbot, use +# `fullchain.pem` as your certificate, not `cert.pem`). # #tls_certificate_path: "CONFDIR/SERVERNAME.tls.crt" @@ -609,80 +605,6 @@ retention: # - myCA2.pem # - myCA3.pem -# ACME support: This will configure Synapse to request a valid TLS certificate -# for your configured `server_name` via Let's Encrypt. -# -# Note that ACME v1 is now deprecated, and Synapse currently doesn't support -# ACME v2. This means that this feature currently won't work with installs set -# up after November 2019. For more info, and alternative solutions, see -# https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 -# -# Note that provisioning a certificate in this way requires port 80 to be -# routed to Synapse so that it can complete the http-01 ACME challenge. -# By default, if you enable ACME support, Synapse will attempt to listen on -# port 80 for incoming http-01 challenges - however, this will likely fail -# with 'Permission denied' or a similar error. -# -# There are a couple of potential solutions to this: -# -# * If you already have an Apache, Nginx, or similar listening on port 80, -# you can configure Synapse to use an alternate port, and have your web -# server forward the requests. For example, assuming you set 'port: 8009' -# below, on Apache, you would write: -# -# ProxyPass /.well-known/acme-challenge http://localhost:8009/.well-known/acme-challenge -# -# * Alternatively, you can use something like `authbind` to give Synapse -# permission to listen on port 80. -# -acme: - # ACME support is disabled by default. Set this to `true` and uncomment - # tls_certificate_path and tls_private_key_path above to enable it. - # - enabled: false - - # Endpoint to use to request certificates. If you only want to test, - # use Let's Encrypt's staging url: - # https://acme-staging.api.letsencrypt.org/directory - # - #url: https://acme-v01.api.letsencrypt.org/directory - - # Port number to listen on for the HTTP-01 challenge. Change this if - # you are forwarding connections through Apache/Nginx/etc. - # - port: 80 - - # Local addresses to listen on for incoming connections. - # Again, you may want to change this if you are forwarding connections - # through Apache/Nginx/etc. - # - bind_addresses: ['::', '0.0.0.0'] - - # How many days remaining on a certificate before it is renewed. - # - reprovision_threshold: 30 - - # The domain that the certificate should be for. Normally this - # should be the same as your Matrix domain (i.e., 'server_name'), but, - # by putting a file at 'https:///.well-known/matrix/server', - # you can delegate incoming traffic to another server. If you do that, - # you should give the target of the delegation here. - # - # For example: if your 'server_name' is 'example.com', but - # 'https://example.com/.well-known/matrix/server' delegates to - # 'matrix.example.com', you should put 'matrix.example.com' here. - # - # If not set, defaults to your 'server_name'. - # - domain: matrix.example.com - - # file to use for the account key. This will be generated if it doesn't - # exist. - # - # If unspecified, we will use CONFDIR/client.key. - # - account_key_file: DATADIR/acme_account.key - ## Federation ## diff --git a/mypy.ini b/mypy.ini index 1ab9001831..c4ff0e6618 100644 --- a/mypy.ini +++ b/mypy.ini @@ -176,9 +176,6 @@ ignore_missing_imports = True [mypy-josepy.*] ignore_missing_imports = True -[mypy-txacme.*] -ignore_missing_imports = True - [mypy-pympler.*] ignore_missing_imports = True diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 575bd30d27..1dde9d7173 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -289,8 +289,7 @@ async def start(hs: "synapse.server.HomeServer"): """ Start a Synapse server or worker. - Should be called once the reactor is running and (if we're using ACME) the - TLS certificates are in place. + Should be called once the reactor is running. Will start the main HTTP listeners and do some other startup tasks, and then notify systemd. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index b2501ee4d7..fb16bceff8 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -363,55 +363,7 @@ def setup(config_options): except UpgradeDatabaseException as e: quit_with_error("Failed to upgrade database: %s" % (e,)) - async def do_acme() -> bool: - """ - Reprovision an ACME certificate, if it's required. - - Returns: - Whether the cert has been updated. - """ - acme = hs.get_acme_handler() - - # Check how long the certificate is active for. - cert_days_remaining = hs.config.is_disk_cert_valid(allow_self_signed=False) - - # We want to reprovision if cert_days_remaining is None (meaning no - # certificate exists), or the days remaining number it returns - # is less than our re-registration threshold. - provision = False - - if ( - cert_days_remaining is None - or cert_days_remaining < hs.config.acme_reprovision_threshold - ): - provision = True - - if provision: - await acme.provision_certificate() - - return provision - - async def reprovision_acme(): - """ - Provision a certificate from ACME, if required, and reload the TLS - certificate if it's renewed. - """ - reprovisioned = await do_acme() - if reprovisioned: - _base.refresh_certificate(hs) - async def start(): - # Run the ACME provisioning code, if it's enabled. - if hs.config.acme_enabled: - acme = hs.get_acme_handler() - # Start up the webservices which we will respond to ACME - # challenges with, and then provision. - await acme.start_listening() - await do_acme() - - # Check if it needs to be reprovisioned every day. - hs.get_clock().looping_call(reprovision_acme, 24 * 60 * 60 * 1000) - # Load the OIDC provider metadatas, if OIDC is enabled. if hs.config.oidc_enabled: oidc = hs.get_oidc_handler() diff --git a/synapse/config/_base.py b/synapse/config/_base.py index 08e2c2c543..d6ec618f8f 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -405,7 +405,6 @@ def generate_config( listeners=None, tls_certificate_path=None, tls_private_key_path=None, - acme_domain=None, ): """ Build a default configuration file @@ -457,9 +456,6 @@ def generate_config( tls_private_key_path (str|None): The path to the tls private key. - acme_domain (str|None): The domain acme will try to validate. If - specified acme will be enabled. - Returns: str: the yaml config file """ @@ -477,7 +473,6 @@ def generate_config( listeners=listeners, tls_certificate_path=tls_certificate_path, tls_private_key_path=tls_private_key_path, - acme_domain=acme_domain, ).values() ) diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index ff9abbc232..4e7bfa8b3b 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -111,7 +111,6 @@ class RootConfig: database_conf: Optional[Any] = ..., tls_certificate_path: Optional[str] = ..., tls_private_key_path: Optional[str] = ..., - acme_domain: Optional[str] = ..., ): ... @classmethod def load_or_generate_config(cls, description: Any, argv: Any): ... diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 0e9bba53c9..9a16a8fbae 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -14,7 +14,6 @@ import logging import os -import warnings from datetime import datetime from typing import List, Optional, Pattern @@ -26,45 +25,12 @@ logger = logging.getLogger(__name__) -ACME_SUPPORT_ENABLED_WARN = """\ -This server uses Synapse's built-in ACME support. Note that ACME v1 has been -deprecated by Let's Encrypt, and that Synapse doesn't currently support ACME v2, -which means that this feature will not work with Synapse installs set up after -November 2019, and that it may stop working on June 2020 for installs set up -before that date. - -For more info and alternative solutions, see -https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 ---------------------------------------------------------------------------------""" - class TlsConfig(Config): section = "tls" def read_config(self, config: dict, config_dir_path: str, **kwargs): - acme_config = config.get("acme", None) - if acme_config is None: - acme_config = {} - - self.acme_enabled = acme_config.get("enabled", False) - - if self.acme_enabled: - logger.warning(ACME_SUPPORT_ENABLED_WARN) - - # hyperlink complains on py2 if this is not a Unicode - self.acme_url = str( - acme_config.get("url", "https://acme-v01.api.letsencrypt.org/directory") - ) - self.acme_port = acme_config.get("port", 80) - self.acme_bind_addresses = acme_config.get("bind_addresses", ["::", "0.0.0.0"]) - self.acme_reprovision_threshold = acme_config.get("reprovision_threshold", 30) - self.acme_domain = acme_config.get("domain", config.get("server_name")) - - self.acme_account_key_file = self.abspath( - acme_config.get("account_key_file", config_dir_path + "/client.key") - ) - self.tls_certificate_file = self.abspath(config.get("tls_certificate_path")) self.tls_private_key_file = self.abspath(config.get("tls_private_key_path")) @@ -229,11 +195,9 @@ def generate_config_section( data_dir_path, tls_certificate_path, tls_private_key_path, - acme_domain, **kwargs, ): - """If the acme_domain is specified acme will be enabled. - If the TLS paths are not specified the default will be certs in the + """If the TLS paths are not specified the default will be certs in the config directory""" base_key_name = os.path.join(config_dir_path, server_name) @@ -243,28 +207,15 @@ def generate_config_section( "Please specify both a cert path and a key path or neither." ) - tls_enabled = ( - "" if tls_certificate_path and tls_private_key_path or acme_domain else "#" - ) + tls_enabled = "" if tls_certificate_path and tls_private_key_path else "#" if not tls_certificate_path: tls_certificate_path = base_key_name + ".tls.crt" if not tls_private_key_path: tls_private_key_path = base_key_name + ".tls.key" - acme_enabled = bool(acme_domain) - acme_domain = "matrix.example.com" - - default_acme_account_file = os.path.join(data_dir_path, "acme_account.key") - - # this is to avoid the max line length. Sorrynotsorry - proxypassline = ( - "ProxyPass /.well-known/acme-challenge " - "http://localhost:8009/.well-known/acme-challenge" - ) - # flake8 doesn't recognise that variables are used in the below string - _ = tls_enabled, proxypassline, acme_enabled, default_acme_account_file + _ = tls_enabled return ( """\ @@ -274,13 +225,9 @@ def generate_config_section( # This certificate, as of Synapse 1.0, will need to be a valid and verifiable # certificate, signed by a recognised Certificate Authority. # - # See 'ACME support' below to enable auto-provisioning this certificate via - # Let's Encrypt. - # - # If supplying your own, be sure to use a `.pem` file that includes the - # full certificate chain including any intermediate certificates (for - # instance, if using certbot, use `fullchain.pem` as your certificate, - # not `cert.pem`). + # Be sure to use a `.pem` file that includes the full certificate chain including + # any intermediate certificates (for instance, if using certbot, use + # `fullchain.pem` as your certificate, not `cert.pem`). # %(tls_enabled)stls_certificate_path: "%(tls_certificate_path)s" @@ -330,80 +277,6 @@ def generate_config_section( # - myCA1.pem # - myCA2.pem # - myCA3.pem - - # ACME support: This will configure Synapse to request a valid TLS certificate - # for your configured `server_name` via Let's Encrypt. - # - # Note that ACME v1 is now deprecated, and Synapse currently doesn't support - # ACME v2. This means that this feature currently won't work with installs set - # up after November 2019. For more info, and alternative solutions, see - # https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 - # - # Note that provisioning a certificate in this way requires port 80 to be - # routed to Synapse so that it can complete the http-01 ACME challenge. - # By default, if you enable ACME support, Synapse will attempt to listen on - # port 80 for incoming http-01 challenges - however, this will likely fail - # with 'Permission denied' or a similar error. - # - # There are a couple of potential solutions to this: - # - # * If you already have an Apache, Nginx, or similar listening on port 80, - # you can configure Synapse to use an alternate port, and have your web - # server forward the requests. For example, assuming you set 'port: 8009' - # below, on Apache, you would write: - # - # %(proxypassline)s - # - # * Alternatively, you can use something like `authbind` to give Synapse - # permission to listen on port 80. - # - acme: - # ACME support is disabled by default. Set this to `true` and uncomment - # tls_certificate_path and tls_private_key_path above to enable it. - # - enabled: %(acme_enabled)s - - # Endpoint to use to request certificates. If you only want to test, - # use Let's Encrypt's staging url: - # https://acme-staging.api.letsencrypt.org/directory - # - #url: https://acme-v01.api.letsencrypt.org/directory - - # Port number to listen on for the HTTP-01 challenge. Change this if - # you are forwarding connections through Apache/Nginx/etc. - # - port: 80 - - # Local addresses to listen on for incoming connections. - # Again, you may want to change this if you are forwarding connections - # through Apache/Nginx/etc. - # - bind_addresses: ['::', '0.0.0.0'] - - # How many days remaining on a certificate before it is renewed. - # - reprovision_threshold: 30 - - # The domain that the certificate should be for. Normally this - # should be the same as your Matrix domain (i.e., 'server_name'), but, - # by putting a file at 'https:///.well-known/matrix/server', - # you can delegate incoming traffic to another server. If you do that, - # you should give the target of the delegation here. - # - # For example: if your 'server_name' is 'example.com', but - # 'https://example.com/.well-known/matrix/server' delegates to - # 'matrix.example.com', you should put 'matrix.example.com' here. - # - # If not set, defaults to your 'server_name'. - # - domain: %(acme_domain)s - - # file to use for the account key. This will be generated if it doesn't - # exist. - # - # If unspecified, we will use CONFDIR/client.key. - # - account_key_file: %(default_acme_account_file)s """ # Lowercase the string representation of boolean values % { @@ -415,8 +288,6 @@ def generate_config_section( def read_tls_certificate(self) -> crypto.X509: """Reads the TLS certificate from the configured file, and returns it - Also checks if it is self-signed, and warns if so - Returns: The certificate """ @@ -425,16 +296,6 @@ def read_tls_certificate(self) -> crypto.X509: cert_pem = self.read_file(cert_path, "tls_certificate_path") cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem) - # Check if it is self-signed, and issue a warning if so. - if cert.get_issuer() == cert.get_subject(): - warnings.warn( - ( - "Self-signed TLS certificates will not be accepted by Synapse 1.0. " - "Please either provide a valid certificate, or use Synapse's ACME " - "support to provision one." - ) - ) - return cert def read_tls_private_key(self) -> crypto.PKey: diff --git a/synapse/handlers/acme.py b/synapse/handlers/acme.py deleted file mode 100644 index 16ab93f580..0000000000 --- a/synapse/handlers/acme.py +++ /dev/null @@ -1,117 +0,0 @@ -# Copyright 2019 New Vector 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 - -import twisted -import twisted.internet.error -from twisted.web import server, static -from twisted.web.resource import Resource - -from synapse.app import check_bind_error - -if TYPE_CHECKING: - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - -ACME_REGISTER_FAIL_ERROR = """ --------------------------------------------------------------------------------- -Failed to register with the ACME provider. This is likely happening because the installation -is new, and ACME v1 has been deprecated by Let's Encrypt and disabled for -new installations since November 2019. -At the moment, Synapse doesn't support ACME v2. For more information and alternative -solutions, please read https://github.com/matrix-org/synapse/blob/master/docs/ACME.md#deprecation-of-acme-v1 ---------------------------------------------------------------------------------""" - - -class AcmeHandler: - def __init__(self, hs: "HomeServer"): - self.hs = hs - self.reactor = hs.get_reactor() - self._acme_domain = hs.config.acme_domain - - async def start_listening(self) -> None: - from synapse.handlers import acme_issuing_service - - # Configure logging for txacme, if you need to debug - # from eliot import add_destinations - # from eliot.twisted import TwistedDestination - # - # add_destinations(TwistedDestination()) - - well_known = Resource() - - self._issuer = acme_issuing_service.create_issuing_service( - self.reactor, - acme_url=self.hs.config.acme_url, - account_key_file=self.hs.config.acme_account_key_file, - well_known_resource=well_known, - ) - - responder_resource = Resource() - responder_resource.putChild(b".well-known", well_known) - responder_resource.putChild(b"check", static.Data(b"OK", b"text/plain")) - srv = server.Site(responder_resource) - - bind_addresses = self.hs.config.acme_bind_addresses - for host in bind_addresses: - logger.info( - "Listening for ACME requests on %s:%i", host, self.hs.config.acme_port - ) - try: - self.reactor.listenTCP( - self.hs.config.acme_port, srv, backlog=50, interface=host - ) - except twisted.internet.error.CannotListenError as e: - check_bind_error(e, host, bind_addresses) - - # Make sure we are registered to the ACME server. There's no public API - # for this, it is usually triggered by startService, but since we don't - # want it to control where we save the certificates, we have to reach in - # and trigger the registration machinery ourselves. - self._issuer._registered = False - - try: - await self._issuer._ensure_registered() - except Exception: - logger.error(ACME_REGISTER_FAIL_ERROR) - raise - - async def provision_certificate(self) -> None: - - logger.warning("Reprovisioning %s", self._acme_domain) - - try: - await self._issuer.issue_cert(self._acme_domain) - except Exception: - logger.exception("Fail!") - raise - logger.warning("Reprovisioned %s, saving.", self._acme_domain) - cert_chain = self._issuer.cert_store.certs[self._acme_domain] - - try: - with open(self.hs.config.tls_private_key_file, "wb") as private_key_file: - for x in cert_chain: - if x.startswith(b"-----BEGIN RSA PRIVATE KEY-----"): - private_key_file.write(x) - - with open(self.hs.config.tls_certificate_file, "wb") as certificate_file: - for x in cert_chain: - if x.startswith(b"-----BEGIN CERTIFICATE-----"): - certificate_file.write(x) - except Exception: - logger.exception("Failed saving!") - raise diff --git a/synapse/handlers/acme_issuing_service.py b/synapse/handlers/acme_issuing_service.py deleted file mode 100644 index a972d3fa0a..0000000000 --- a/synapse/handlers/acme_issuing_service.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright 2019 New Vector Ltd -# Copyright 2019 The Matrix.org Foundation C.I.C. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Utility function to create an ACME issuing service. - -This file contains the unconditional imports on the acme and cryptography bits that we -only need (and may only have available) if we are doing ACME, so is designed to be -imported conditionally. -""" -import logging -from typing import Dict, Iterable, List - -import attr -import pem -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -from josepy import JWKRSA -from josepy.jwa import RS256 -from txacme.challenges import HTTP01Responder -from txacme.client import Client -from txacme.interfaces import ICertificateStore -from txacme.service import AcmeIssuingService -from txacme.util import generate_private_key -from zope.interface import implementer - -from twisted.internet import defer -from twisted.internet.interfaces import IReactorTCP -from twisted.python.filepath import FilePath -from twisted.python.url import URL -from twisted.web.resource import IResource - -logger = logging.getLogger(__name__) - - -def create_issuing_service( - reactor: IReactorTCP, - acme_url: str, - account_key_file: str, - well_known_resource: IResource, -) -> AcmeIssuingService: - """Create an ACME issuing service, and attach it to a web Resource - - Args: - reactor: twisted reactor - acme_url: URL to use to request certificates - account_key_file: where to store the account key - well_known_resource: web resource for .well-known. - we will attach a child resource for "acme-challenge". - - Returns: - AcmeIssuingService - """ - responder = HTTP01Responder() - - well_known_resource.putChild(b"acme-challenge", responder.resource) - - store = ErsatzStore() - - return AcmeIssuingService( - cert_store=store, - client_creator=( - lambda: Client.from_url( - reactor=reactor, - url=URL.from_text(acme_url), - key=load_or_create_client_key(account_key_file), - alg=RS256, - ) - ), - clock=reactor, - responders=[responder], - ) - - -@attr.s(slots=True) -@implementer(ICertificateStore) -class ErsatzStore: - """ - A store that only stores in memory. - """ - - certs = attr.ib(type=Dict[bytes, List[bytes]], default=attr.Factory(dict)) - - def store( - self, server_name: bytes, pem_objects: Iterable[pem.AbstractPEMObject] - ) -> defer.Deferred: - self.certs[server_name] = [o.as_bytes() for o in pem_objects] - return defer.succeed(None) - - -def load_or_create_client_key(key_file: str) -> JWKRSA: - """Load the ACME account key from a file, creating it if it does not exist. - - Args: - key_file: name of the file to use as the account key - """ - # this is based on txacme.endpoint.load_or_create_client_key, but doesn't - # hardcode the 'client.key' filename - acme_key_file = FilePath(key_file) - if acme_key_file.exists(): - logger.info("Loading ACME account key from '%s'", acme_key_file) - key = serialization.load_pem_private_key( - acme_key_file.getContent(), password=None, backend=default_backend() - ) - else: - logger.info("Saving new ACME account key to '%s'", acme_key_file) - key = generate_private_key("rsa") - acme_key_file.setContent( - key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - ) - return JWKRSA(key=key) diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index bf361c42d6..271c17c226 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -96,11 +96,6 @@ "psycopg2cffi>=2.8 ; platform_python_implementation == 'PyPy'", "psycopg2cffi-compat==1.1 ; platform_python_implementation == 'PyPy'", ], - # ACME support is required to provision TLS certificates from authorities - # that use the protocol, such as Let's Encrypt. - "acme": [ - "txacme>=0.9.2", - ], "saml2": [ "pysaml2>=4.5.0", ], diff --git a/synapse/server.py b/synapse/server.py index fec0024c89..e8dd2fa9f2 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -66,7 +66,6 @@ from synapse.groups.groups_server import GroupsServerHandler, GroupsServerWorkerHandler from synapse.handlers.account_data import AccountDataHandler from synapse.handlers.account_validity import AccountValidityHandler -from synapse.handlers.acme import AcmeHandler from synapse.handlers.admin import AdminHandler from synapse.handlers.appservice import ApplicationServicesHandler from synapse.handlers.auth import AuthHandler, MacaroonGenerator @@ -494,10 +493,6 @@ def get_e2e_keys_handler(self) -> E2eKeysHandler: def get_e2e_room_keys_handler(self) -> E2eRoomKeysHandler: return E2eRoomKeysHandler(self) - @cache_in_self - def get_acme_handler(self) -> AcmeHandler: - return AcmeHandler(self) - @cache_in_self def get_admin_handler(self) -> AdminHandler: return AdminHandler(self) diff --git a/tests/config/test_tls.py b/tests/config/test_tls.py index dcf336416c..b6bc1876b5 100644 --- a/tests/config/test_tls.py +++ b/tests/config/test_tls.py @@ -13,10 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os - import idna -import yaml from OpenSSL import SSL @@ -39,58 +36,6 @@ class TestConfig(RootConfig): class TLSConfigTests(TestCase): - def test_warn_self_signed(self): - """ - Synapse will give a warning when it loads a self-signed certificate. - """ - config_dir = self.mktemp() - os.mkdir(config_dir) - with open(os.path.join(config_dir, "cert.pem"), "w") as f: - f.write( - """-----BEGIN CERTIFICATE----- -MIID6DCCAtACAws9CjANBgkqhkiG9w0BAQUFADCBtzELMAkGA1UEBhMCVFIxDzAN -BgNVBAgMBsOHb3J1bTEUMBIGA1UEBwwLQmHFn21ha8OnxLExEjAQBgNVBAMMCWxv -Y2FsaG9zdDEcMBoGA1UECgwTVHdpc3RlZCBNYXRyaXggTGFiczEkMCIGA1UECwwb -QXV0b21hdGVkIFRlc3RpbmcgQXV0aG9yaXR5MSkwJwYJKoZIhvcNAQkBFhpzZWN1 -cml0eUB0d2lzdGVkbWF0cml4LmNvbTAgFw0xNzA3MTIxNDAxNTNaGA8yMTE3MDYx -ODE0MDE1M1owgbcxCzAJBgNVBAYTAlRSMQ8wDQYDVQQIDAbDh29ydW0xFDASBgNV -BAcMC0JhxZ9tYWvDp8SxMRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAaBgNVBAoME1R3 -aXN0ZWQgTWF0cml4IExhYnMxJDAiBgNVBAsMG0F1dG9tYXRlZCBUZXN0aW5nIEF1 -dGhvcml0eTEpMCcGCSqGSIb3DQEJARYac2VjdXJpdHlAdHdpc3RlZG1hdHJpeC5j -b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDwT6kbqtMUI0sMkx4h -I+L780dA59KfksZCqJGmOsMD6hte9EguasfkZzvCF3dk3NhwCjFSOvKx6rCwiteo -WtYkVfo+rSuVNmt7bEsOUDtuTcaxTzIFB+yHOYwAaoz3zQkyVW0c4pzioiLCGCmf -FLdiDBQGGp74tb+7a0V6kC3vMLFoM3L6QWq5uYRB5+xLzlPJ734ltyvfZHL3Us6p -cUbK+3WTWvb4ER0W2RqArAj6Bc/ERQKIAPFEiZi9bIYTwvBH27OKHRz+KoY/G8zY -+l+WZoJqDhupRAQAuh7O7V/y6bSP+KNxJRie9QkZvw1PSaGSXtGJI3WWdO12/Ulg -epJpAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAJXEq5P9xwvP9aDkXIqzcD0L8sf8 -ewlhlxTQdeqt2Nace0Yk18lIo2oj1t86Y8jNbpAnZJeI813Rr5M7FbHCXoRc/SZG -I8OtG1xGwcok53lyDuuUUDexnK4O5BkjKiVlNPg4HPim5Kuj2hRNFfNt/F2BVIlj -iZupikC5MT1LQaRwidkSNxCku1TfAyueiBwhLnFwTmIGNnhuDCutEVAD9kFmcJN2 -SznugAcPk4doX2+rL+ila+ThqgPzIkwTUHtnmjI0TI6xsDUlXz5S3UyudrE2Qsfz -s4niecZKPBizL6aucT59CsunNmmb5Glq8rlAcU+1ZTZZzGYqVYhF6axB9Qg= ------END CERTIFICATE-----""" - ) - - config = { - "tls_certificate_path": os.path.join(config_dir, "cert.pem"), - } - - t = TestConfig() - t.read_config(config, config_dir_path="", data_dir_path="") - t.read_tls_certificate() - - warnings = self.flushWarnings() - self.assertEqual(len(warnings), 1) - self.assertEqual( - warnings[0]["message"], - ( - "Self-signed TLS certificates will not be accepted by " - "Synapse 1.0. Please either provide a valid certificate, " - "or use Synapse's ACME support to provision one." - ), - ) - def test_tls_client_minimum_default(self): """ The default client TLS version is 1.0. @@ -202,48 +147,6 @@ def test_tls_client_minimum_set_passed_through_1_0(self): self.assertEqual(options & SSL.OP_NO_TLSv1_1, 0) self.assertEqual(options & SSL.OP_NO_TLSv1_2, 0) - def test_acme_disabled_in_generated_config_no_acme_domain_provied(self): - """ - Checks acme is disabled by default. - """ - conf = TestConfig() - conf.read_config( - yaml.safe_load( - TestConfig().generate_config( - "/config_dir_path", - "my_super_secure_server", - "/data_dir_path", - tls_certificate_path="/tls_cert_path", - tls_private_key_path="tls_private_key", - acme_domain=None, # This is the acme_domain - ) - ), - "/config_dir_path", - ) - - self.assertFalse(conf.acme_enabled) - - def test_acme_enabled_in_generated_config_domain_provided(self): - """ - Checks acme is enabled if the acme_domain arg is set to some string. - """ - conf = TestConfig() - conf.read_config( - yaml.safe_load( - TestConfig().generate_config( - "/config_dir_path", - "my_super_secure_server", - "/data_dir_path", - tls_certificate_path="/tls_cert_path", - tls_private_key_path="tls_private_key", - acme_domain="my_supe_secure_server", # This is the acme_domain - ) - ), - "/config_dir_path", - ) - - self.assertTrue(conf.acme_enabled) - def test_whitelist_idna_failure(self): """ The federation certificate whitelist will not allow IDNA domain names. From 91fa9cca99f7cd1ba96baaf3f2c1b5c045dd1a7c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:43:22 +0100 Subject: [PATCH 298/619] Expose opentracing trace id in response headers (#10199) Fixes: #9480 --- changelog.d/10199.misc | 1 + synapse/federation/transport/server.py | 3 +++ synapse/logging/opentracing.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 changelog.d/10199.misc diff --git a/changelog.d/10199.misc b/changelog.d/10199.misc new file mode 100644 index 0000000000..69b18aeacc --- /dev/null +++ b/changelog.d/10199.misc @@ -0,0 +1 @@ +Expose opentracing trace id in response headers. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 16d740cf58..bed47f8abd 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -35,6 +35,7 @@ parse_string_from_args, parse_strings_from_args, ) +from synapse.logging import opentracing from synapse.logging.context import run_in_background from synapse.logging.opentracing import ( SynapseTags, @@ -345,6 +346,8 @@ async def new_func(request, *args, **kwargs): ) with scope: + opentracing.inject_response_headers(request.responseHeaders) + if origin and self.RATELIMIT: with ratelimiter.ratelimit(origin) as d: await d diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index 4f18792c99..140ed711e3 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -173,6 +173,7 @@ def set_fates(clotho, lachesis, atropos, father="Zues", mother="Themis"): import attr from twisted.internet import defer +from twisted.web.http_headers import Headers from synapse.config import ConfigError from synapse.util import json_decoder, json_encoder @@ -668,6 +669,25 @@ def inject_header_dict( headers[key.encode()] = [value.encode()] +def inject_response_headers(response_headers: Headers) -> None: + """Inject the current trace id into the HTTP response headers""" + if not opentracing: + return + span = opentracing.tracer.active_span + if not span: + return + + # This is a bit implementation-specific. + # + # Jaeger's Spans have a trace_id property; other implementations (including the + # dummy opentracing.span.Span which we use if init_tracer is not called) do not + # expose it + trace_id = getattr(span, "trace_id", None) + + if trace_id is not None: + response_headers.addRawHeader("Synapse-Trace-Id", f"{trace_id:x}") + + @ensure_active_span("get the active span context as a dict", ret={}) def get_active_span_text_map(destination=None): """ @@ -843,6 +863,7 @@ def trace_servlet(request: "SynapseRequest", extract_context: bool = False): scope = start_active_span(request_name) with scope: + inject_response_headers(request.responseHeaders) try: yield finally: From 1b3e398bea8129fa7ae6fe28fd3a395fcd427ad9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 18 Jun 2021 13:15:52 +0200 Subject: [PATCH 299/619] Standardise the module interface (#10062) This PR adds a common configuration section for all modules (see docs). These modules are then loaded at startup by the homeserver. Modules register their hooks and web resources using the new `register_[...]_callbacks` and `register_web_resource` methods of the module API. --- UPGRADE.rst | 17 ++ changelog.d/10062.feature | 1 + changelog.d/10062.removal | 1 + docs/SUMMARY.md | 2 +- docs/modules.md | 258 ++++++++++++++++++ docs/sample_config.yaml | 29 +- docs/spam_checker.md | 4 + synapse/app/_base.py | 9 + synapse/app/generic_worker.py | 4 + synapse/app/homeserver.py | 4 + synapse/config/_base.pyi | 2 + synapse/config/homeserver.py | 5 +- synapse/config/modules.py | 49 ++++ synapse/config/spam_checker.py | 15 -- synapse/events/spamcheck.py | 306 +++++++++++++++------- synapse/handlers/register.py | 2 +- synapse/module_api/__init__.py | 30 ++- synapse/module_api/errors.py | 1 + synapse/server.py | 39 ++- synapse/util/module_loader.py | 35 +-- tests/handlers/test_register.py | 120 ++++++--- tests/handlers/test_user_directory.py | 21 +- tests/rest/media/v1/test_media_storage.py | 3 + 23 files changed, 769 insertions(+), 188 deletions(-) create mode 100644 changelog.d/10062.feature create mode 100644 changelog.d/10062.removal create mode 100644 docs/modules.md create mode 100644 synapse/config/modules.py diff --git a/UPGRADE.rst b/UPGRADE.rst index 9f61aad412..ee8b4fa60b 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -85,6 +85,23 @@ for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb +Upgrading to v1.37.0 +==================== + +Deprecation of the current spam checker interface +------------------------------------------------- + +The current spam checker interface is deprecated in favour of a new generic modules system. +Authors of spam checker modules can refer to `this documentation `_ +to update their modules. Synapse administrators can refer to `this documentation `_ +to update their configuration once the modules they are using have been updated. + +We plan to remove support for the current spam checker interface in August 2021. + +More module interfaces will be ported over to this new generic system in future versions +of Synapse. + + Upgrading to v1.34.0 ==================== diff --git a/changelog.d/10062.feature b/changelog.d/10062.feature new file mode 100644 index 0000000000..97474f030c --- /dev/null +++ b/changelog.d/10062.feature @@ -0,0 +1 @@ +Standardised the module interface. diff --git a/changelog.d/10062.removal b/changelog.d/10062.removal new file mode 100644 index 0000000000..7f0cbdae2e --- /dev/null +++ b/changelog.d/10062.removal @@ -0,0 +1 @@ +The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 01ef4ff600..98969bdd2d 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -35,7 +35,7 @@ - [URL Previews](url_previews.md) - [User Directory](user_directory.md) - [Message Retention Policies](message_retention_policies.md) - - [Pluggable Modules]() + - [Pluggable Modules](modules.md) - [Third Party Rules]() - [Spam Checker](spam_checker.md) - [Presence Router](presence_router_module.md) diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 0000000000..d64ec4493d --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,258 @@ +# Modules + +Synapse supports extending its functionality by configuring external modules. + +## Using modules + +To use a module on Synapse, add it to the `modules` section of the configuration file: + +```yaml +modules: + - module: my_super_module.MySuperClass + config: + do_thing: true + - module: my_other_super_module.SomeClass + config: {} +``` + +Each module is defined by a path to a Python class as well as a configuration. This +information for a given module should be available in the module's own documentation. + +**Note**: When using third-party modules, you effectively allow someone else to run +custom code on your Synapse homeserver. Server admins are encouraged to verify the +provenance of the modules they use on their homeserver and make sure the modules aren't +running malicious code on their instance. + +Also note that we are currently in the process of migrating module interfaces to this +system. While some interfaces might be compatible with it, others still require +configuring modules in another part of Synapse's configuration file. Currently, only the +spam checker interface is compatible with this new system. + +## Writing a module + +A module is a Python class that uses Synapse's module API to interact with the +homeserver. It can register callbacks that Synapse will call on specific operations, as +well as web resources to attach to Synapse's web server. + +When instantiated, a module is given its parsed configuration as well as an instance of +the `synapse.module_api.ModuleApi` class. The configuration is a dictionary, and is +either the output of the module's `parse_config` static method (see below), or the +configuration associated with the module in Synapse's configuration file. + +See the documentation for the `ModuleApi` class +[here](https://github.com/matrix-org/synapse/blob/master/synapse/module_api/__init__.py). + +### Handling the module's configuration + +A module can implement the following static method: + +```python +@staticmethod +def parse_config(config: dict) -> dict +``` + +This method is given a dictionary resulting from parsing the YAML configuration for the +module. It may modify it (for example by parsing durations expressed as strings (e.g. +"5d") into milliseconds, etc.), and return the modified dictionary. It may also verify +that the configuration is correct, and raise an instance of +`synapse.module_api.errors.ConfigError` if not. + +### Registering a web resource + +Modules can register web resources onto Synapse's web server using the following module +API method: + +```python +def ModuleApi.register_web_resource(path: str, resource: IResource) +``` + +The path is the full absolute path to register the resource at. For example, if you +register a resource for the path `/_synapse/client/my_super_module/say_hello`, Synapse +will serve it at `http(s)://[HS_URL]/_synapse/client/my_super_module/say_hello`. Note +that Synapse does not allow registering resources for several sub-paths in the `/_matrix` +namespace (such as anything under `/_matrix/client` for example). It is strongly +recommended that modules register their web resources under the `/_synapse/client` +namespace. + +The provided resource is a Python class that implements Twisted's [IResource](https://twistedmatrix.com/documents/current/api/twisted.web.resource.IResource.html) +interface (such as [Resource](https://twistedmatrix.com/documents/current/api/twisted.web.resource.Resource.html)). + +Only one resource can be registered for a given path. If several modules attempt to +register a resource for the same path, the module that appears first in Synapse's +configuration file takes priority. + +Modules **must** register their web resources in their `__init__` method. + +### Registering a callback + +Modules can use Synapse's module API to register callbacks. Callbacks are functions that +Synapse will call when performing specific actions. Callbacks must be asynchronous, and +are split in categories. A single module may implement callbacks from multiple categories, +and is under no obligation to implement all callbacks from the categories it registers +callbacks for. + +#### Spam checker callbacks + +To register one of the callbacks described in this section, a module needs to use the +module API's `register_spam_checker_callbacks` method. The callback functions are passed +to `register_spam_checker_callbacks` as keyword arguments, with the callback name as the +argument name and the function as its value. This is demonstrated in the example below. + +The available spam checker callbacks are: + +```python +def check_event_for_spam(event: "synapse.events.EventBase") -> Union[bool, str] +``` + +Called when receiving an event from a client or via federation. The module can return +either a `bool` to indicate whether the event must be rejected because of spam, or a `str` +to indicate the event must be rejected because of spam and to give a rejection reason to +forward to clients. + +```python +def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool +``` + +Called when processing an invitation. The module must return a `bool` indicating whether +the inviter can invite the invitee to the given room. Both inviter and invitee are +represented by their Matrix user ID (i.e. `@alice:example.com`). + +```python +def user_may_create_room(user: str) -> bool +``` + +Called when processing a room creation request. The module must return a `bool` indicating +whether the given user (represented by their Matrix user ID) is allowed to create a room. + +```python +def user_may_create_room_alias(user: str, room_alias: "synapse.types.RoomAlias") -> bool +``` + +Called when trying to associate an alias with an existing room. The module must return a +`bool` indicating whether the given user (represented by their Matrix user ID) is allowed +to set the given alias. + +```python +def user_may_publish_room(user: str, room_id: str) -> bool +``` + +Called when trying to publish a room to the homeserver's public rooms directory. The +module must return a `bool` indicating whether the given user (represented by their +Matrix user ID) is allowed to publish the given room. + +```python +def check_username_for_spam(user_profile: Dict[str, str]) -> bool +``` + +Called when computing search results in the user directory. The module must return a +`bool` indicating whether the given user profile can appear in search results. The profile +is represented as a dictionary with the following keys: + +* `user_id`: The Matrix ID for this user. +* `display_name`: The user's display name. +* `avatar_url`: The `mxc://` URL to the user's avatar. + +The module is given a copy of the original dictionary, so modifying it from within the +module cannot modify a user's profile when included in user directory search results. + +```python +def check_registration_for_spam( + email_threepid: Optional[dict], + username: Optional[str], + request_info: Collection[Tuple[str, str]], + auth_provider_id: Optional[str] = None, +) -> "synapse.spam_checker_api.RegistrationBehaviour" +``` + +Called when registering a new user. The module must return a `RegistrationBehaviour` +indicating whether the registration can go through or must be denied, or whether the user +may be allowed to register but will be shadow banned. + +The arguments passed to this callback are: + +* `email_threepid`: The email address used for registering, if any. +* `username`: The username the user would like to register. Can be `None`, meaning that + Synapse will generate one later. +* `request_info`: A collection of tuples, which first item is a user agent, and which + second item is an IP address. These user agents and IP addresses are the ones that were + used during the registration process. +* `auth_provider_id`: The identifier of the SSO authentication provider, if any. + +```python +def check_media_file_for_spam( + file_wrapper: "synapse.rest.media.v1.media_storage.ReadableFileWrapper", + file_info: "synapse.rest.media.v1._base.FileInfo" +) -> bool +``` + +Called when storing a local or remote file. The module must return a boolean indicating +whether the given file can be stored in the homeserver's media store. + +### Porting an existing module that uses the old interface + +In order to port a module that uses Synapse's old module interface, its author needs to: + +* ensure the module's callbacks are all asynchronous. +* register their callbacks using one or more of the `register_[...]_callbacks` methods + from the `ModuleApi` class in the module's `__init__` method (see [this section](#registering-a-web-resource) + for more info). + +Additionally, if the module is packaged with an additional web resource, the module +should register this resource in its `__init__` method using the `register_web_resource` +method from the `ModuleApi` class (see [this section](#registering-a-web-resource) for +more info). + +The module's author should also update any example in the module's configuration to only +use the new `modules` section in Synapse's configuration file (see [this section](#using-modules) +for more info). + +### Example + +The example below is a module that implements the spam checker callback +`user_may_create_room` to deny room creation to user `@evilguy:example.com`, and registers +a web resource to the path `/_synapse/client/demo/hello` that returns a JSON object. + +```python +import json + +from twisted.web.resource import Resource +from twisted.web.server import Request + +from synapse.module_api import ModuleApi + + +class DemoResource(Resource): + def __init__(self, config): + super(DemoResource, self).__init__() + self.config = config + + def render_GET(self, request: Request): + name = request.args.get(b"name")[0] + request.setHeader(b"Content-Type", b"application/json") + return json.dumps({"hello": name}) + + +class DemoModule: + def __init__(self, config: dict, api: ModuleApi): + self.config = config + self.api = api + + self.api.register_web_resource( + path="/_synapse/client/demo/hello", + resource=DemoResource(self.config), + ) + + self.api.register_spam_checker_callbacks( + user_may_create_room=self.user_may_create_room, + ) + + @staticmethod + def parse_config(config): + return config + + async def user_may_create_room(self, user: str) -> bool: + if user == "@evilguy:example.com": + return False + + return True +``` diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 307f8cd3c8..19505c7fd2 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -31,6 +31,22 @@ # # [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html + +## Modules ## + +# Server admins can expand Synapse's functionality with external modules. +# +# See https://matrix-org.github.io/synapse/develop/modules.html for more +# documentation on how to configure or create custom modules for Synapse. +# +modules: + # - module: my_super_module.MySuperClass + # config: + # do_thing: true + # - module: my_other_super_module.SomeClass + # config: {} + + ## Server ## # The public-facing domain of the server @@ -2491,19 +2507,6 @@ push: #group_unread_count_by_room: false -# Spam checkers are third-party modules that can block specific actions -# of local users, such as creating rooms and registering undesirable -# usernames, as well as remote users by redacting incoming events. -# -spam_checker: - #- module: "my_custom_project.SuperSpamChecker" - # config: - # example_option: 'things' - #- module: "some_other_project.BadEventStopper" - # config: - # example_stop_events_from: ['@bad:example.com'] - - ## Rooms ## # Controls whether locally-created rooms should be end-to-end encrypted by diff --git a/docs/spam_checker.md b/docs/spam_checker.md index 52947f605e..c16914e61d 100644 --- a/docs/spam_checker.md +++ b/docs/spam_checker.md @@ -1,3 +1,7 @@ +**Note: this page of the Synapse documentation is now deprecated. For up to date +documentation on setting up or writing a spam checker module, please see +[this page](https://matrix-org.github.io/synapse/develop/modules.html).** + # Handling spam in Synapse Synapse has support to customize spam checking behavior. It can plug into a diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 1dde9d7173..00ab67e7e4 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -35,6 +35,7 @@ from synapse.app.phone_stats_home import start_phone_stats_home from synapse.config.homeserver import HomeServerConfig from synapse.crypto import context_factory +from synapse.events.spamcheck import load_legacy_spam_checkers from synapse.logging.context import PreserveLoggingContext from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.metrics.jemalloc import setup_jemalloc_stats @@ -330,6 +331,14 @@ def run_sighup(*args, **kwargs): # Start the tracer synapse.logging.opentracing.init_tracer(hs) # type: ignore[attr-defined] # noqa + # Instantiate the modules so they can register their web resources to the module API + # before we start the listeners. + module_api = hs.get_module_api() + for module, config in hs.config.modules.loaded_modules: + module(config=config, api=module_api) + + load_legacy_spam_checkers(hs) + # It is now safe to start your Synapse. hs.start_listening() hs.get_datastore().db_pool.start_profiling() diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 57c2fc2e88..8e648c6ee0 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -354,6 +354,10 @@ def _listen_http(self, listener_config: ListenerConfig): if name == "replication": resources[REPLICATION_PREFIX] = ReplicationRestResource(self) + # Attach additional resources registered by modules. + resources.update(self._module_web_resources) + self._module_web_resources_consumed = True + root_resource = create_resource_tree(resources, OptionsResource()) _base.listen_tcp( diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index fb16bceff8..f31467bde7 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -124,6 +124,10 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf ) resources[path] = resource + # Attach additional resources registered by modules. + resources.update(self._module_web_resources) + self._module_web_resources_consumed = True + # try to find something useful to redirect '/' to if WEB_CLIENT_PREFIX in resources: root_resource = RootOptionsRedirectResource(WEB_CLIENT_PREFIX) diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index 4e7bfa8b3b..844ecd4708 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -16,6 +16,7 @@ from synapse.config import ( key, logger, metrics, + modules, oidc, password_auth_providers, push, @@ -85,6 +86,7 @@ class RootConfig: thirdpartyrules: third_party_event_rules.ThirdPartyRulesConfig tracer: tracer.TracerConfig redis: redis.RedisConfig + modules: modules.ModulesConfig config_classes: List = ... def __init__(self) -> None: ... diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 5ae0f55bcc..1f42a51857 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -1,5 +1,4 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2018 New Vector Ltd +# Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,6 +29,7 @@ from .key import KeyConfig from .logger import LoggingConfig from .metrics import MetricsConfig +from .modules import ModulesConfig from .oidc import OIDCConfig from .password_auth_providers import PasswordAuthProviderConfig from .push import PushConfig @@ -56,6 +56,7 @@ class HomeServerConfig(RootConfig): config_classes = [ + ModulesConfig, ServerConfig, TlsConfig, FederationConfig, diff --git a/synapse/config/modules.py b/synapse/config/modules.py new file mode 100644 index 0000000000..3209e1c492 --- /dev/null +++ b/synapse/config/modules.py @@ -0,0 +1,49 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Dict, List, Tuple + +from synapse.config._base import Config, ConfigError +from synapse.util.module_loader import load_module + + +class ModulesConfig(Config): + section = "modules" + + def read_config(self, config: dict, **kwargs): + self.loaded_modules: List[Tuple[Any, Dict]] = [] + + configured_modules = config.get("modules") or [] + for i, module in enumerate(configured_modules): + config_path = ("modules", "" % i) + if not isinstance(module, dict): + raise ConfigError("expected a mapping", config_path) + + self.loaded_modules.append(load_module(module, config_path)) + + def generate_config_section(self, **kwargs): + return """ + ## Modules ## + + # Server admins can expand Synapse's functionality with external modules. + # + # See https://matrix-org.github.io/synapse/develop/modules.html for more + # documentation on how to configure or create custom modules for Synapse. + # + modules: + # - module: my_super_module.MySuperClass + # config: + # do_thing: true + # - module: my_other_super_module.SomeClass + # config: {} + """ diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py index 447ba3303b..c24165eb8a 100644 --- a/synapse/config/spam_checker.py +++ b/synapse/config/spam_checker.py @@ -42,18 +42,3 @@ def read_config(self, config, **kwargs): self.spam_checkers.append(load_module(spam_checker, config_path)) else: raise ConfigError("spam_checker syntax is incorrect") - - def generate_config_section(self, **kwargs): - return """\ - # Spam checkers are third-party modules that can block specific actions - # of local users, such as creating rooms and registering undesirable - # usernames, as well as remote users by redacting incoming events. - # - spam_checker: - #- module: "my_custom_project.SuperSpamChecker" - # config: - # example_option: 'things' - #- module: "some_other_project.BadEventStopper" - # config: - # example_stop_events_from: ['@bad:example.com'] - """ diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index d5fa195094..45ec96dfc1 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -15,7 +15,18 @@ import inspect import logging -from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Collection, + Dict, + List, + Optional, + Tuple, + Union, +) from synapse.rest.media.v1._base import FileInfo from synapse.rest.media.v1.media_storage import ReadableFileWrapper @@ -29,20 +40,186 @@ logger = logging.getLogger(__name__) +CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[ + ["synapse.events.EventBase"], + Awaitable[Union[bool, str]], +] +USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]] +USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] +USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]] +USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]] +CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[Dict[str, str]], Awaitable[bool]] +LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ + [ + Optional[dict], + Optional[str], + Collection[Tuple[str, str]], + ], + Awaitable[RegistrationBehaviour], +] +CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ + [ + Optional[dict], + Optional[str], + Collection[Tuple[str, str]], + Optional[str], + ], + Awaitable[RegistrationBehaviour], +] +CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[ + [ReadableFileWrapper, FileInfo], + Awaitable[bool], +] + + +def load_legacy_spam_checkers(hs: "synapse.server.HomeServer"): + """Wrapper that loads spam checkers configured using the old configuration, and + registers the spam checker hooks they implement. + """ + spam_checkers = [] # type: List[Any] + api = hs.get_module_api() + for module, config in hs.config.spam_checkers: + # Older spam checkers don't accept the `api` argument, so we + # try and detect support. + spam_args = inspect.getfullargspec(module) + if "api" in spam_args.args: + spam_checkers.append(module(config=config, api=api)) + else: + spam_checkers.append(module(config=config)) + + # The known spam checker hooks. If a spam checker module implements a method + # which name appears in this set, we'll want to register it. + spam_checker_methods = { + "check_event_for_spam", + "user_may_invite", + "user_may_create_room", + "user_may_create_room_alias", + "user_may_publish_room", + "check_username_for_spam", + "check_registration_for_spam", + "check_media_file_for_spam", + } + + for spam_checker in spam_checkers: + # Methods on legacy spam checkers might not be async, so we wrap them around a + # wrapper that will call maybe_awaitable on the result. + def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]: + # f might be None if the callback isn't implemented by the module. In this + # case we don't want to register a callback at all so we return None. + if f is None: + return None + + if f.__name__ == "check_registration_for_spam": + checker_args = inspect.signature(f) + if len(checker_args.parameters) == 3: + # Backwards compatibility; some modules might implement a hook that + # doesn't expect a 4th argument. In this case, wrap it in a function + # that gives it only 3 arguments and drops the auth_provider_id on + # the floor. + def wrapper( + email_threepid: Optional[dict], + username: Optional[str], + request_info: Collection[Tuple[str, str]], + auth_provider_id: Optional[str], + ) -> Union[Awaitable[RegistrationBehaviour], RegistrationBehaviour]: + # We've already made sure f is not None above, but mypy doesn't + # do well across function boundaries so we need to tell it f is + # definitely not None. + assert f is not None + + return f( + email_threepid, + username, + request_info, + ) + + f = wrapper + elif len(checker_args.parameters) != 4: + raise RuntimeError( + "Bad signature for callback check_registration_for_spam", + ) + + def run(*args, **kwargs): + # We've already made sure f is not None above, but mypy doesn't do well + # across function boundaries so we need to tell it f is definitely not + # None. + assert f is not None + + return maybe_awaitable(f(*args, **kwargs)) + + return run + + # Register the hooks through the module API. + hooks = { + hook: async_wrapper(getattr(spam_checker, hook, None)) + for hook in spam_checker_methods + } + + api.register_spam_checker_callbacks(**hooks) + class SpamChecker: - def __init__(self, hs: "synapse.server.HomeServer"): - self.spam_checkers = [] # type: List[Any] - api = hs.get_module_api() - - for module, config in hs.config.spam_checkers: - # Older spam checkers don't accept the `api` argument, so we - # try and detect support. - spam_args = inspect.getfullargspec(module) - if "api" in spam_args.args: - self.spam_checkers.append(module(config=config, api=api)) - else: - self.spam_checkers.append(module(config=config)) + def __init__(self): + self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = [] + self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = [] + self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = [] + self._user_may_create_room_alias_callbacks: List[ + USER_MAY_CREATE_ROOM_ALIAS_CALLBACK + ] = [] + self._user_may_publish_room_callbacks: List[USER_MAY_PUBLISH_ROOM_CALLBACK] = [] + self._check_username_for_spam_callbacks: List[ + CHECK_USERNAME_FOR_SPAM_CALLBACK + ] = [] + self._check_registration_for_spam_callbacks: List[ + CHECK_REGISTRATION_FOR_SPAM_CALLBACK + ] = [] + self._check_media_file_for_spam_callbacks: List[ + CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK + ] = [] + + def register_callbacks( + self, + check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None, + user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, + user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, + user_may_create_room_alias: Optional[ + USER_MAY_CREATE_ROOM_ALIAS_CALLBACK + ] = None, + user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None, + check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None, + check_registration_for_spam: Optional[ + CHECK_REGISTRATION_FOR_SPAM_CALLBACK + ] = None, + check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None, + ): + """Register callbacks from module for each hook.""" + if check_event_for_spam is not None: + self._check_event_for_spam_callbacks.append(check_event_for_spam) + + if user_may_invite is not None: + self._user_may_invite_callbacks.append(user_may_invite) + + if user_may_create_room is not None: + self._user_may_create_room_callbacks.append(user_may_create_room) + + if user_may_create_room_alias is not None: + self._user_may_create_room_alias_callbacks.append( + user_may_create_room_alias, + ) + + if user_may_publish_room is not None: + self._user_may_publish_room_callbacks.append(user_may_publish_room) + + if check_username_for_spam is not None: + self._check_username_for_spam_callbacks.append(check_username_for_spam) + + if check_registration_for_spam is not None: + self._check_registration_for_spam_callbacks.append( + check_registration_for_spam, + ) + + if check_media_file_for_spam is not None: + self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam) async def check_event_for_spam( self, event: "synapse.events.EventBase" @@ -60,9 +237,10 @@ async def check_event_for_spam( True or a string if the event is spammy. If a string is returned it will be used as the error message returned to the user. """ - for spam_checker in self.spam_checkers: - if await maybe_awaitable(spam_checker.check_event_for_spam(event)): - return True + for callback in self._check_event_for_spam_callbacks: + res = await callback(event) # type: Union[bool, str] + if res: + return res return False @@ -81,15 +259,8 @@ async def user_may_invite( Returns: True if the user may send an invite, otherwise False """ - for spam_checker in self.spam_checkers: - if ( - await maybe_awaitable( - spam_checker.user_may_invite( - inviter_userid, invitee_userid, room_id - ) - ) - is False - ): + for callback in self._user_may_invite_callbacks: + if await callback(inviter_userid, invitee_userid, room_id) is False: return False return True @@ -105,11 +276,8 @@ async def user_may_create_room(self, userid: str) -> bool: Returns: True if the user may create a room, otherwise False """ - for spam_checker in self.spam_checkers: - if ( - await maybe_awaitable(spam_checker.user_may_create_room(userid)) - is False - ): + for callback in self._user_may_create_room_callbacks: + if await callback(userid) is False: return False return True @@ -128,13 +296,8 @@ async def user_may_create_room_alias( Returns: True if the user may create a room alias, otherwise False """ - for spam_checker in self.spam_checkers: - if ( - await maybe_awaitable( - spam_checker.user_may_create_room_alias(userid, room_alias) - ) - is False - ): + for callback in self._user_may_create_room_alias_callbacks: + if await callback(userid, room_alias) is False: return False return True @@ -151,13 +314,8 @@ async def user_may_publish_room(self, userid: str, room_id: str) -> bool: Returns: True if the user may publish the room, otherwise False """ - for spam_checker in self.spam_checkers: - if ( - await maybe_awaitable( - spam_checker.user_may_publish_room(userid, room_id) - ) - is False - ): + for callback in self._user_may_publish_room_callbacks: + if await callback(userid, room_id) is False: return False return True @@ -177,15 +335,11 @@ async def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool: Returns: True if the user is spammy. """ - for spam_checker in self.spam_checkers: - # For backwards compatibility, only run if the method exists on the - # spam checker - checker = getattr(spam_checker, "check_username_for_spam", None) - if checker: - # Make a copy of the user profile object to ensure the spam checker - # cannot modify it. - if await maybe_awaitable(checker(user_profile.copy())): - return True + for callback in self._check_username_for_spam_callbacks: + # Make a copy of the user profile object to ensure the spam checker cannot + # modify it. + if await callback(user_profile.copy()): + return True return False @@ -211,33 +365,13 @@ async def check_registration_for_spam( Enum for how the request should be handled """ - for spam_checker in self.spam_checkers: - # For backwards compatibility, only run if the method exists on the - # spam checker - checker = getattr(spam_checker, "check_registration_for_spam", None) - if checker: - # Provide auth_provider_id if the function supports it - checker_args = inspect.signature(checker) - if len(checker_args.parameters) == 4: - d = checker( - email_threepid, - username, - request_info, - auth_provider_id, - ) - elif len(checker_args.parameters) == 3: - d = checker(email_threepid, username, request_info) - else: - logger.error( - "Invalid signature for %s.check_registration_for_spam. Denying registration", - spam_checker.__module__, - ) - return RegistrationBehaviour.DENY - - behaviour = await maybe_awaitable(d) - assert isinstance(behaviour, RegistrationBehaviour) - if behaviour != RegistrationBehaviour.ALLOW: - return behaviour + for callback in self._check_registration_for_spam_callbacks: + behaviour = await ( + callback(email_threepid, username, request_info, auth_provider_id) + ) + assert isinstance(behaviour, RegistrationBehaviour) + if behaviour != RegistrationBehaviour.ALLOW: + return behaviour return RegistrationBehaviour.ALLOW @@ -275,13 +409,9 @@ async def check_media_file_for_spam( allowed. """ - for spam_checker in self.spam_checkers: - # For backwards compatibility, only run if the method exists on the - # spam checker - checker = getattr(spam_checker, "check_media_file_for_spam", None) - if checker: - spam = await maybe_awaitable(checker(file_wrapper, file_info)) - if spam: - return True + for callback in self._check_media_file_for_spam_callbacks: + spam = await callback(file_wrapper, file_info) + if spam: + return True return False diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 4ceef3fab3..ca1ed6a5c0 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -195,7 +195,7 @@ async def register_user( bind_emails: list of emails to bind to this account. by_admin: True if this registration is being made via the admin api, otherwise False. - user_agent_ips: Tuples of IP addresses and user-agents used + user_agent_ips: Tuples of user-agents and IP addresses used during the registration process. auth_provider_id: The SSO IdP the user used, if any. Returns: diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index cecdc96bf5..58b255eb1b 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, Tuple from twisted.internet import defer +from twisted.web.resource import IResource from synapse.events import EventBase from synapse.http.client import SimpleHttpClient @@ -42,7 +43,7 @@ class ModuleApi: can register new users etc if necessary. """ - def __init__(self, hs, auth_handler): + def __init__(self, hs: "HomeServer", auth_handler): self._hs = hs self._store = hs.get_datastore() @@ -56,6 +57,33 @@ def __init__(self, hs, auth_handler): self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient self._public_room_list_manager = PublicRoomListManager(hs) + self._spam_checker = hs.get_spam_checker() + + ################################################################################# + # The following methods should only be called during the module's initialisation. + + @property + def register_spam_checker_callbacks(self): + """Registers callbacks for spam checking capabilities.""" + return self._spam_checker.register_callbacks + + def register_web_resource(self, path: str, resource: IResource): + """Registers a web resource to be served at the given path. + + This function should be called during initialisation of the module. + + If multiple modules register a resource for the same path, the module that + appears the highest in the configuration file takes priority. + + Args: + path: The path to register the resource for. + resource: The resource to attach to this path. + """ + self._hs.register_module_web_resource(path, resource) + + ######################################################################### + # The following methods can be called by the module at any point in time. + @property def http_client(self): """Allows making outbound HTTP requests to remote resources. diff --git a/synapse/module_api/errors.py b/synapse/module_api/errors.py index d24864c549..02bbb0be39 100644 --- a/synapse/module_api/errors.py +++ b/synapse/module_api/errors.py @@ -15,3 +15,4 @@ """Exception types which are exposed as part of the stable module API""" from synapse.api.errors import RedirectException, SynapseError # noqa: F401 +from synapse.config._base import ConfigError # noqa: F401 diff --git a/synapse/server.py b/synapse/server.py index e8dd2fa9f2..2c27d2a7e8 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -1,6 +1,4 @@ -# Copyright 2014-2016 OpenMarket Ltd -# Copyright 2017-2018 New Vector Ltd -# Copyright 2019 The Matrix.org Foundation C.I.C. +# Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,6 +37,7 @@ from twisted.internet import defer from twisted.mail.smtp import sendmail from twisted.web.iweb import IPolicyForHTTPS +from twisted.web.resource import IResource from synapse.api.auth import Auth from synapse.api.filtering import Filtering @@ -258,6 +257,38 @@ def __init__( self.datastores = None # type: Optional[Databases] + self._module_web_resources: Dict[str, IResource] = {} + self._module_web_resources_consumed = False + + def register_module_web_resource(self, path: str, resource: IResource): + """Allows a module to register a web resource to be served at the given path. + + If multiple modules register a resource for the same path, the module that + appears the highest in the configuration file takes priority. + + Args: + path: The path to register the resource for. + resource: The resource to attach to this path. + + Raises: + SynapseError(500): A module tried to register a web resource after the HTTP + listeners have been started. + """ + if self._module_web_resources_consumed: + raise RuntimeError( + "Tried to register a web resource from a module after startup", + ) + + # Don't register a resource that's already been registered. + if path not in self._module_web_resources.keys(): + self._module_web_resources[path] = resource + else: + logger.warning( + "Module tried to register a web resource for path %s but another module" + " has already registered a resource for this path.", + path, + ) + def get_instance_id(self) -> str: """A unique ID for this synapse process instance. @@ -646,7 +677,7 @@ def get_stats_handler(self) -> StatsHandler: @cache_in_self def get_spam_checker(self) -> SpamChecker: - return SpamChecker(self) + return SpamChecker() @cache_in_self def get_third_party_event_rules(self) -> ThirdPartyEventRules: diff --git a/synapse/util/module_loader.py b/synapse/util/module_loader.py index cbfbd097f9..5a638c6e9a 100644 --- a/synapse/util/module_loader.py +++ b/synapse/util/module_loader.py @@ -51,21 +51,26 @@ def load_module(provider: dict, config_path: Iterable[str]) -> Tuple[Type, Any]: # Load the module config. If None, pass an empty dictionary instead module_config = provider.get("config") or {} - try: - provider_config = provider_class.parse_config(module_config) - except jsonschema.ValidationError as e: - raise json_error_to_config_error(e, itertools.chain(config_path, ("config",))) - except ConfigError as e: - raise _wrap_config_error( - "Failed to parse config for module %r" % (modulename,), - prefix=itertools.chain(config_path, ("config",)), - e=e, - ) - except Exception as e: - raise ConfigError( - "Failed to parse config for module %r" % (modulename,), - path=itertools.chain(config_path, ("config",)), - ) from e + if hasattr(provider_class, "parse_config"): + try: + provider_config = provider_class.parse_config(module_config) + except jsonschema.ValidationError as e: + raise json_error_to_config_error( + e, itertools.chain(config_path, ("config",)) + ) + except ConfigError as e: + raise _wrap_config_error( + "Failed to parse config for module %r" % (modulename,), + prefix=itertools.chain(config_path, ("config",)), + e=e, + ) + except Exception as e: + raise ConfigError( + "Failed to parse config for module %r" % (modulename,), + path=itertools.chain(config_path, ("config",)), + ) from e + else: + provider_config = module_config return provider_class, provider_config diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index c51763f41a..a9fd3036dc 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -27,6 +27,58 @@ from .. import unittest +class TestSpamChecker: + def __init__(self, config, api): + api.register_spam_checker_callbacks( + check_registration_for_spam=self.check_registration_for_spam, + ) + + @staticmethod + def parse_config(config): + return config + + async def check_registration_for_spam( + self, + email_threepid, + username, + request_info, + auth_provider_id, + ): + pass + + +class DenyAll(TestSpamChecker): + async def check_registration_for_spam( + self, + email_threepid, + username, + request_info, + auth_provider_id, + ): + return RegistrationBehaviour.DENY + + +class BanAll(TestSpamChecker): + async def check_registration_for_spam( + self, + email_threepid, + username, + request_info, + auth_provider_id, + ): + return RegistrationBehaviour.SHADOW_BAN + + +class BanBadIdPUser(TestSpamChecker): + async def check_registration_for_spam( + self, email_threepid, username, request_info, auth_provider_id=None + ): + # Reject any user coming from CAS and whose username contains profanity + if auth_provider_id == "cas" and "flimflob" in username: + return RegistrationBehaviour.DENY + return RegistrationBehaviour.ALLOW + + class RegistrationTestCase(unittest.HomeserverTestCase): """Tests the RegistrationHandler.""" @@ -42,6 +94,11 @@ def make_homeserver(self, reactor, clock): hs_config["limit_usage_by_mau"] = True hs = self.setup_test_homeserver(config=hs_config) + + module_api = hs.get_module_api() + for module, config in hs.config.modules.loaded_modules: + module(config=config, api=module_api) + return hs def prepare(self, reactor, clock, hs): @@ -465,34 +522,30 @@ def test_invalid_user_id_length(self): self.handler.register_user(localpart=invalid_user_id), SynapseError ) + @override_config( + { + "modules": [ + { + "module": TestSpamChecker.__module__ + ".DenyAll", + } + ] + } + ) def test_spam_checker_deny(self): """A spam checker can deny registration, which results in an error.""" - - class DenyAll: - def check_registration_for_spam( - self, email_threepid, username, request_info - ): - return RegistrationBehaviour.DENY - - # Configure a spam checker that denies all users. - spam_checker = self.hs.get_spam_checker() - spam_checker.spam_checkers = [DenyAll()] - self.get_failure(self.handler.register_user(localpart="user"), SynapseError) + @override_config( + { + "modules": [ + { + "module": TestSpamChecker.__module__ + ".BanAll", + } + ] + } + ) def test_spam_checker_shadow_ban(self): """A spam checker can choose to shadow-ban a user, which allows registration to succeed.""" - - class BanAll: - def check_registration_for_spam( - self, email_threepid, username, request_info - ): - return RegistrationBehaviour.SHADOW_BAN - - # Configure a spam checker that denies all users. - spam_checker = self.hs.get_spam_checker() - spam_checker.spam_checkers = [BanAll()] - user_id = self.get_success(self.handler.register_user(localpart="user")) # Get an access token. @@ -512,22 +565,17 @@ def check_registration_for_spam( self.assertTrue(requester.shadow_banned) + @override_config( + { + "modules": [ + { + "module": TestSpamChecker.__module__ + ".BanBadIdPUser", + } + ] + } + ) def test_spam_checker_receives_sso_type(self): """Test rejecting registration based on SSO type""" - - class BanBadIdPUser: - def check_registration_for_spam( - self, email_threepid, username, request_info, auth_provider_id=None - ): - # Reject any user coming from CAS and whose username contains profanity - if auth_provider_id == "cas" and "flimflob" in username: - return RegistrationBehaviour.DENY - return RegistrationBehaviour.ALLOW - - # Configure a spam checker that denies a certain user on a specific IdP - spam_checker = self.hs.get_spam_checker() - spam_checker.spam_checkers = [BanBadIdPUser()] - f = self.get_failure( self.handler.register_user(localpart="bobflimflob", auth_provider_id="cas"), SynapseError, diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index daac37abd8..549876dc85 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -312,15 +312,13 @@ def test_spam_checker(self): s = self.get_success(self.handler.search_users(u1, "user2", 10)) self.assertEqual(len(s["results"]), 1) + async def allow_all(user_profile): + # Allow all users. + return False + # Configure a spam checker that does not filter any users. spam_checker = self.hs.get_spam_checker() - - class AllowAll: - async def check_username_for_spam(self, user_profile): - # Allow all users. - return False - - spam_checker.spam_checkers = [AllowAll()] + spam_checker._check_username_for_spam_callbacks = [allow_all] # The results do not change: # We get one search result when searching for user2 by user1. @@ -328,12 +326,11 @@ async def check_username_for_spam(self, user_profile): self.assertEqual(len(s["results"]), 1) # Configure a spam checker that filters all users. - class BlockAll: - async def check_username_for_spam(self, user_profile): - # All users are spammy. - return True + async def block_all(user_profile): + # All users are spammy. + return True - spam_checker.spam_checkers = [BlockAll()] + spam_checker._check_username_for_spam_callbacks = [block_all] # User1 now gets no search results for any of the other users. s = self.get_success(self.handler.search_users(u1, "user2", 10)) diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index 4a213d13dd..95e7075841 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -27,6 +27,7 @@ from twisted.internet import defer from twisted.internet.defer import Deferred +from synapse.events.spamcheck import load_legacy_spam_checkers from synapse.logging.context import make_deferred_yieldable from synapse.rest import admin from synapse.rest.client.v1 import login @@ -535,6 +536,8 @@ def prepare(self, reactor, clock, hs): self.download_resource = self.media_repo.children[b"download"] self.upload_resource = self.media_repo.children[b"upload"] + load_legacy_spam_checkers(hs) + def default_config(self): config = default_config("test") From e9f2ad86034d27068941379f678e19bf280ed308 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 18 Jun 2021 16:55:53 +0200 Subject: [PATCH 300/619] Describe callbacks signatures as async in new modules doc (#10206) --- changelog.d/10206.feature | 1 + docs/modules.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 changelog.d/10206.feature diff --git a/changelog.d/10206.feature b/changelog.d/10206.feature new file mode 100644 index 0000000000..97474f030c --- /dev/null +++ b/changelog.d/10206.feature @@ -0,0 +1 @@ +Standardised the module interface. diff --git a/docs/modules.md b/docs/modules.md index d64ec4493d..3a9fab61b8 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -101,7 +101,7 @@ argument name and the function as its value. This is demonstrated in the example The available spam checker callbacks are: ```python -def check_event_for_spam(event: "synapse.events.EventBase") -> Union[bool, str] +async def check_event_for_spam(event: "synapse.events.EventBase") -> Union[bool, str] ``` Called when receiving an event from a client or via federation. The module can return @@ -110,7 +110,7 @@ to indicate the event must be rejected because of spam and to give a rejection r forward to clients. ```python -def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool +async def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool ``` Called when processing an invitation. The module must return a `bool` indicating whether @@ -118,14 +118,14 @@ the inviter can invite the invitee to the given room. Both inviter and invitee a represented by their Matrix user ID (i.e. `@alice:example.com`). ```python -def user_may_create_room(user: str) -> bool +async def user_may_create_room(user: str) -> bool ``` Called when processing a room creation request. The module must return a `bool` indicating whether the given user (represented by their Matrix user ID) is allowed to create a room. ```python -def user_may_create_room_alias(user: str, room_alias: "synapse.types.RoomAlias") -> bool +async def user_may_create_room_alias(user: str, room_alias: "synapse.types.RoomAlias") -> bool ``` Called when trying to associate an alias with an existing room. The module must return a @@ -133,7 +133,7 @@ Called when trying to associate an alias with an existing room. The module must to set the given alias. ```python -def user_may_publish_room(user: str, room_id: str) -> bool +async def user_may_publish_room(user: str, room_id: str) -> bool ``` Called when trying to publish a room to the homeserver's public rooms directory. The @@ -141,7 +141,7 @@ module must return a `bool` indicating whether the given user (represented by th Matrix user ID) is allowed to publish the given room. ```python -def check_username_for_spam(user_profile: Dict[str, str]) -> bool +async def check_username_for_spam(user_profile: Dict[str, str]) -> bool ``` Called when computing search results in the user directory. The module must return a @@ -156,7 +156,7 @@ The module is given a copy of the original dictionary, so modifying it from with module cannot modify a user's profile when included in user directory search results. ```python -def check_registration_for_spam( +async def check_registration_for_spam( email_threepid: Optional[dict], username: Optional[str], request_info: Collection[Tuple[str, str]], @@ -179,7 +179,7 @@ The arguments passed to this callback are: * `auth_provider_id`: The identifier of the SSO authentication provider, if any. ```python -def check_media_file_for_spam( +async def check_media_file_for_spam( file_wrapper: "synapse.rest.media.v1.media_storage.ReadableFileWrapper", file_info: "synapse.rest.media.v1._base.FileInfo" ) -> bool From 0bd968921c03dd61c1487f85dd884c4ed11ff486 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 18 Jun 2021 13:41:33 -0400 Subject: [PATCH 301/619] Fix a missing await when in the spaces summary. (#10208) This could cause a minor data leak if someone defined a non-restricted join rule with an allow key or used a restricted join rule in an older room version, but this is unlikely. Additionally this starts adding unit tests to the spaces summary handler. --- changelog.d/10208.bugfix | 1 + synapse/handlers/space_summary.py | 3 +- tests/handlers/test_space_summary.py | 99 +++++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10208.bugfix diff --git a/changelog.d/10208.bugfix b/changelog.d/10208.bugfix new file mode 100644 index 0000000000..32b6465717 --- /dev/null +++ b/changelog.d/10208.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.35.1 where an `allow` key of a `m.room.join_rules` event could be applied for incorrect room versions and configurations. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index e953a8afe6..17fc47ce16 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -445,14 +445,13 @@ async def _is_room_accessible( member_event_id = state_ids.get((EventTypes.Member, requester), None) # If they're in the room they can see info on it. - member_event = None if member_event_id: member_event = await self._store.get_event(member_event_id) if member_event.membership in (Membership.JOIN, Membership.INVITE): return True # Otherwise, check if they should be allowed access via membership in a space. - if self._event_auth_handler.has_restricted_join_rules( + if await self._event_auth_handler.has_restricted_join_rules( state_ids, room_version ): allowed_rooms = ( diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 2c5e81531b..131d362ccc 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -11,10 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Optional +from typing import Any, Iterable, Optional, Tuple from unittest import mock +from synapse.api.errors import AuthError from synapse.handlers.space_summary import _child_events_comparison_key +from synapse.rest import admin +from synapse.rest.client.v1 import login, room +from synapse.server import HomeServer +from synapse.types import JsonDict from tests import unittest @@ -79,3 +84,95 @@ def test_invalid_ordering_value(self): ev1 = _create_event("!abc:test", "a" * 51) self.assertEqual([ev2, ev1], _order(ev1, ev2)) + + +class SpaceSummaryTestCase(unittest.HomeserverTestCase): + servlets = [ + admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, hs: HomeServer): + self.hs = hs + self.handler = self.hs.get_space_summary_handler() + + self.user = self.register_user("user", "pass") + self.token = self.login("user", "pass") + + def _add_child(self, space_id: str, room_id: str, token: str) -> None: + """Add a child room to a space.""" + self.helper.send_state( + space_id, + event_type="m.space.child", + body={"via": [self.hs.hostname]}, + tok=token, + state_key=room_id, + ) + + def _assert_rooms(self, result: JsonDict, rooms: Iterable[str]) -> None: + """Assert that the expected room IDs are in the response.""" + self.assertCountEqual([room.get("room_id") for room in result["rooms"]], rooms) + + def _assert_events( + self, result: JsonDict, events: Iterable[Tuple[str, str]] + ) -> None: + """Assert that the expected parent / child room IDs are in the response.""" + self.assertCountEqual( + [ + (event.get("room_id"), event.get("state_key")) + for event in result["events"] + ], + events, + ) + + def test_simple_space(self): + """Test a simple space with a single room.""" + space = self.helper.create_room_as(self.user, tok=self.token) + room = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(space, room, self.token) + + result = self.get_success(self.handler.get_space_summary(self.user, space)) + # The result should have the space and the room in it, along with a link + # from space -> room. + self._assert_rooms(result, [space, room]) + self._assert_events(result, [(space, room)]) + + def test_visibility(self): + """A user not in a space cannot inspect it.""" + space = self.helper.create_room_as(self.user, tok=self.token) + room = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(space, room, self.token) + + user2 = self.register_user("user2", "pass") + token2 = self.login("user2", "pass") + + # The user cannot see the space. + self.get_failure(self.handler.get_space_summary(user2, space), AuthError) + + # Joining the room causes it to be visible. + self.helper.join(space, user2, tok=token2) + result = self.get_success(self.handler.get_space_summary(user2, space)) + + # The result should only have the space, but includes the link to the room. + self._assert_rooms(result, [space]) + self._assert_events(result, [(space, room)]) + + def test_world_readable(self): + """A world-readable room is visible to everyone.""" + space = self.helper.create_room_as(self.user, tok=self.token) + room = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(space, room, self.token) + self.helper.send_state( + space, + event_type="m.room.history_visibility", + body={"history_visibility": "world_readable"}, + tok=self.token, + ) + + user2 = self.register_user("user2", "pass") + + # The space should be visible, as well as the link to the room. + result = self.get_success(self.handler.get_space_summary(user2, space)) + self._assert_rooms(result, [space]) + self._assert_events(result, [(space, room)]) From 7c536d0fefe778499a5a7a24d88578c4c62815f8 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 18 Jun 2021 19:26:25 +0100 Subject: [PATCH 302/619] Deploy a documentation version for each new Synapse release (#10198) This PR will run a new "Deploy release-specific documentation" job whenever a push to a branch name matching `release-v*` occurs. Doing so will create/add to a folder named `vX.Y` on the `gh-pages` branch. Doing so will allow us to build up `major.minor` releases of the docs as we release Synapse. This is especially useful for having a mechanism for keeping around documentation of old/removed features (for those running older versions of Synapse), without needing to clutter the latest copy of the docs. After a [discussion](https://matrix.to/#/!XaqDhxuTIlvldquJaV:matrix.org/$rKmkBmQle8OwTlGcoyu0BkcWXdnHW3_oap8BMgclwIY?via=matrix.org&via=vector.modular.im&via=envs.net) in #synapse-dev, we wanted to use tags to trigger the documentation deployments, which I agreed with. However, I soon realised that the bash-foo required to turn a tag of `v1.2.3rc1` into `1.2` was a lot more complex than the branch's `release-v1.2`. So, I've gone with the latter for simplicity. In the future we'll have some UI on the website to switch between versions, but for now you can simply just change 'develop' to 'v1.2' in the URL. --- .github/workflows/docs.yaml | 33 +++++++++++++++++++++++++++++++++ changelog.d/10198.doc | 1 + 2 files changed, 34 insertions(+) create mode 100644 changelog.d/10198.doc diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index a746ae6de3..23b8d7f909 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -3,7 +3,10 @@ name: Deploy the documentation on: push: branches: + # For bleeding-edge documentation - develop + # For documentation specific to a release + - 'release-v*' workflow_dispatch: @@ -22,6 +25,7 @@ jobs: - name: Build the documentation run: mdbook build + # Deploy to the latest documentation directories - name: Deploy latest documentation uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 with: @@ -29,3 +33,32 @@ jobs: keep_files: true publish_dir: ./book destination_dir: ./develop + + - name: Get the current Synapse version + id: vars + # The $GITHUB_REF value for a branch looks like `refs/heads/release-v1.2`. We do some + # shell magic to remove the "refs/heads/release-v" bit from this, to end up with "1.2", + # our major/minor version number, and set this to a var called `branch-version`. + # + # We then use some python to get Synapse's full version string, which may look + # like "1.2.3rc4". We set this to a var called `synapse-version`. We use this + # to determine if this release is still an RC, and if so block deployment. + run: | + echo ::set-output name=branch-version::${GITHUB_REF#refs/heads/release-v} + echo ::set-output name=synapse-version::`python3 -c 'import synapse; print(synapse.__version__)'` + + # Deploy to the version-specific directory + - name: Deploy release-specific documentation + # We only carry out this step if we're running on a release branch, + # and the current Synapse version does not have "rc" in the name. + # + # The result is that only full releases are deployed, but can be + # updated if the release branch gets retroactive fixes. + if: ${{ startsWith( github.ref, 'refs/heads/release-v' ) && !contains( steps.vars.outputs.synapse-version, 'rc') }} + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + keep_files: true + publish_dir: ./book + # The resulting documentation will end up in a directory named `vX.Y`. + destination_dir: ./v${{ steps.vars.outputs.branch-version }} diff --git a/changelog.d/10198.doc b/changelog.d/10198.doc new file mode 100644 index 0000000000..8d1aeab1a7 --- /dev/null +++ b/changelog.d/10198.doc @@ -0,0 +1 @@ +Deploy a snapshot of the documentation website upon each new Synapse release. From 107c06081f46b0cda2128265bdae5f4280b1645f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 21 Jun 2021 11:41:25 +0100 Subject: [PATCH 303/619] Ensure that errors during startup are written to the logs and the console. (#10191) * Defer stdio redirection until we are about to start the reactor * Catch and handle exceptions during startup --- changelog.d/10191.feature | 1 + synapse/app/_base.py | 28 +++++++++++++++++++++++++++- synapse/app/generic_worker.py | 22 +++++++++++++++++----- synapse/app/homeserver.py | 16 +++++++++------- synapse/config/logger.py | 4 +--- 5 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 changelog.d/10191.feature diff --git a/changelog.d/10191.feature b/changelog.d/10191.feature new file mode 100644 index 0000000000..40f306c421 --- /dev/null +++ b/changelog.d/10191.feature @@ -0,0 +1 @@ +Ensure that errors during startup are written to the logs and the console. diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 00ab67e7e4..8879136881 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -26,7 +26,9 @@ from cryptography.utils import CryptographyDeprecationWarning from typing_extensions import NoReturn +import twisted from twisted.internet import defer, error, reactor +from twisted.logger import LoggingFile, LogLevel from twisted.protocols.tls import TLSMemoryBIOFactory import synapse @@ -139,7 +141,7 @@ def run(): def quit_with_error(error_string: str) -> NoReturn: message_lines = error_string.split("\n") - line_length = max(len(line) for line in message_lines if len(line) < 80) + 2 + line_length = min(max(len(line) for line in message_lines), 80) + 2 sys.stderr.write("*" * line_length + "\n") for line in message_lines: sys.stderr.write(" %s\n" % (line.rstrip(),)) @@ -147,6 +149,30 @@ def quit_with_error(error_string: str) -> NoReturn: sys.exit(1) +def handle_startup_exception(e: Exception) -> NoReturn: + # Exceptions that occur between setting up the logging and forking or starting + # the reactor are written to the logs, followed by a summary to stderr. + logger.exception("Exception during startup") + quit_with_error( + f"Error during initialisation:\n {e}\nThere may be more information in the logs." + ) + + +def redirect_stdio_to_logs() -> None: + streams = [("stdout", LogLevel.info), ("stderr", LogLevel.error)] + + for (stream, level) in streams: + oldStream = getattr(sys, stream) + loggingFile = LoggingFile( + logger=twisted.logger.Logger(namespace=stream), + level=level, + encoding=getattr(oldStream, "encoding", None), + ) + setattr(sys, stream, loggingFile) + + print("Redirected stdout/stderr to logs") + + def register_start(cb: Callable[..., Awaitable], *args, **kwargs) -> None: """Register a callback with the reactor, to be called once it is running diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 8e648c6ee0..af8a1833f3 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -32,7 +32,12 @@ SERVER_KEY_V2_PREFIX, ) from synapse.app import _base -from synapse.app._base import max_request_body_size, register_start +from synapse.app._base import ( + handle_startup_exception, + max_request_body_size, + redirect_stdio_to_logs, + register_start, +) from synapse.config._base import ConfigError from synapse.config.homeserver import HomeServerConfig from synapse.config.logger import setup_logging @@ -469,14 +474,21 @@ def start(config_options): setup_logging(hs, config, use_worker_options=True) - hs.setup() + try: + hs.setup() - # Ensure the replication streamer is always started in case we write to any - # streams. Will no-op if no streams can be written to by this worker. - hs.get_replication_streamer() + # Ensure the replication streamer is always started in case we write to any + # streams. Will no-op if no streams can be written to by this worker. + hs.get_replication_streamer() + except Exception as e: + handle_startup_exception(e) register_start(_base.start, hs) + # redirect stdio to the logs, if configured. + if not hs.config.no_redirect_stdio: + redirect_stdio_to_logs() + _base.start_worker_reactor("synapse-generic-worker", config) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index f31467bde7..7af56ac136 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -37,10 +37,11 @@ ) from synapse.app import _base from synapse.app._base import ( + handle_startup_exception, listen_ssl, listen_tcp, max_request_body_size, - quit_with_error, + redirect_stdio_to_logs, register_start, ) from synapse.config._base import ConfigError @@ -69,8 +70,6 @@ from synapse.rest.well_known import WellKnownResource from synapse.server import HomeServer from synapse.storage import DataStore -from synapse.storage.engines import IncorrectDatabaseSetup -from synapse.storage.prepare_database import UpgradeDatabaseException from synapse.util.httpresourcetree import create_resource_tree from synapse.util.module_loader import load_module from synapse.util.versionstring import get_version_string @@ -362,10 +361,8 @@ def setup(config_options): try: hs.setup() - except IncorrectDatabaseSetup as e: - quit_with_error(str(e)) - except UpgradeDatabaseException as e: - quit_with_error("Failed to upgrade database: %s" % (e,)) + except Exception as e: + handle_startup_exception(e) async def start(): # Load the OIDC provider metadatas, if OIDC is enabled. @@ -456,6 +453,11 @@ def main(): # check base requirements check_requirements() hs = setup(sys.argv[1:]) + + # redirect stdio to the logs, if configured. + if not hs.config.no_redirect_stdio: + redirect_stdio_to_logs() + run(hs) diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 813076dfe2..91d9bcf32e 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -259,9 +259,7 @@ def _log(event: dict) -> None: finally: threadlocal.active = False - logBeginner.beginLoggingTo([_log], redirectStandardIO=not config.no_redirect_stdio) - if not config.no_redirect_stdio: - print("Redirected stdout/stderr to logs") + logBeginner.beginLoggingTo([_log], redirectStandardIO=False) def _load_logging_config(log_config_path: str) -> None: From 182147195b707ce10af165ccd72a5bb2f3ecab38 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 21 Jun 2021 11:57:09 +0100 Subject: [PATCH 304/619] Check third party rules before persisting knocks over federation (#10212) An accidental mis-ordering of operations during #6739 technically allowed an incoming knock event over federation in before checking it against any configured Third Party Access Rules modules. This PR corrects that by performing the TPAR check *before* persisting the event. --- changelog.d/10212.feature | 1 + synapse/handlers/federation.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10212.feature diff --git a/changelog.d/10212.feature b/changelog.d/10212.feature new file mode 100644 index 0000000000..9c41140194 --- /dev/null +++ b/changelog.d/10212.feature @@ -0,0 +1 @@ +Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 0bfb25802a..1b566dbf2d 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2086,8 +2086,6 @@ async def on_send_knock_request( context = await self.state_handler.compute_event_context(event) - await self._auth_and_persist_event(origin, event, context) - event_allowed = await self.third_party_event_rules.check_event_allowed( event, context ) @@ -2097,6 +2095,8 @@ async def on_send_knock_request( 403, "This event is not allowed in this context", Codes.FORBIDDEN ) + await self._auth_and_persist_event(origin, event, context) + return context async def get_state_for_pdu(self, room_id: str, event_id: str) -> List[EventBase]: From a5cd05beeeac80df0352bd50c2ad2e017664665c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 21 Jun 2021 14:38:59 +0100 Subject: [PATCH 305/619] Fix performance of responding to user key requests over federation (#10221) We were repeatedly looking up a config option in a loop (using the unclassed config style), which is expensive enough that it can cause large CPU usage. --- changelog.d/10221.bugfix | 1 + synapse/config/_base.pyi | 2 ++ synapse/storage/databases/main/end_to_end_keys.py | 9 ++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10221.bugfix diff --git a/changelog.d/10221.bugfix b/changelog.d/10221.bugfix new file mode 100644 index 0000000000..8853a9bf4e --- /dev/null +++ b/changelog.d/10221.bugfix @@ -0,0 +1 @@ +Fix performance regression in responding to user key requests over federation. Introduced in v1.34.0rc1. diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index 844ecd4708..23ca0c83c1 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -11,6 +11,7 @@ from synapse.config import ( database, emailconfig, experimental, + federation, groups, jwt, key, @@ -87,6 +88,7 @@ class RootConfig: tracer: tracer.TracerConfig redis: redis.RedisConfig modules: modules.ModulesConfig + federation: federation.FederationConfig config_classes: List = ... def __init__(self) -> None: ... diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index 9ba5778a88..0e3dd4e9ca 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -62,6 +62,13 @@ def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer" class EndToEndKeyWorkerStore(EndToEndKeyBackgroundStore): + def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer"): + super().__init__(database, db_conn, hs) + + self._allow_device_name_lookup_over_federation = ( + self.hs.config.federation.allow_device_name_lookup_over_federation + ) + async def get_e2e_device_keys_for_federation_query( self, user_id: str ) -> Tuple[int, List[JsonDict]]: @@ -85,7 +92,7 @@ async def get_e2e_device_keys_for_federation_query( result["keys"] = keys device_display_name = None - if self.hs.config.allow_device_name_lookup_over_federation: + if self._allow_device_name_lookup_over_federation: device_display_name = device.display_name if device_display_name: result["device_display_name"] = device_display_name From 756fd513dfaebddd28bf783eafa95b4505ce8745 Mon Sep 17 00:00:00 2001 From: jkanefendt <43998479+jkanefendt@users.noreply.github.com> Date: Tue, 22 Jun 2021 00:48:57 +0200 Subject: [PATCH 306/619] Implement config option `sso.update_profile_information` (#10108) Implemented config option sso.update_profile_information to keep user's display name in sync with the SSO displayname. Signed-off-by: Johannes Kanefendt --- changelog.d/10108.feature | 1 + docs/sample_config.yaml | 11 +++++++++++ synapse/config/sso.py | 15 +++++++++++++++ synapse/handlers/sso.py | 25 ++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10108.feature diff --git a/changelog.d/10108.feature b/changelog.d/10108.feature new file mode 100644 index 0000000000..4930a5acf5 --- /dev/null +++ b/changelog.d/10108.feature @@ -0,0 +1 @@ +Implement config option `sso.update_profile_information` to sync SSO users' profile information with the identity provider each time they login. Currently only displayname is supported. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 19505c7fd2..6fcc022b47 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1975,6 +1975,17 @@ sso: # - https://riot.im/develop # - https://my.custom.client/ + # Uncomment to keep a user's profile fields in sync with information from + # the identity provider. Currently only syncing the displayname is + # supported. Fields are checked on every SSO login, and are updated + # if necessary. + # + # Note that enabling this option will override user profile information, + # regardless of whether users have opted-out of syncing that + # information when first signing in. Defaults to false. + # + #update_profile_information: true + # Directory in which Synapse will try to find the template files below. # If not set, or the files named below are not found within the template # directory, default templates from within the Synapse package will be used. diff --git a/synapse/config/sso.py b/synapse/config/sso.py index af645c930d..e4346e02aa 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -74,6 +74,10 @@ def read_config(self, config, **kwargs): self.sso_client_whitelist = sso_config.get("client_whitelist") or [] + self.sso_update_profile_information = ( + sso_config.get("update_profile_information") or False + ) + # Attempt to also whitelist the server's login fallback, since that fallback sets # the redirect URL to itself (so it can process the login token then return # gracefully to the client). This would make it pointless to ask the user for @@ -111,6 +115,17 @@ def generate_config_section(self, **kwargs): # - https://riot.im/develop # - https://my.custom.client/ + # Uncomment to keep a user's profile fields in sync with information from + # the identity provider. Currently only syncing the displayname is + # supported. Fields are checked on every SSO login, and are updated + # if necessary. + # + # Note that enabling this option will override user profile information, + # regardless of whether users have opted-out of syncing that + # information when first signing in. Defaults to false. + # + #update_profile_information: true + # Directory in which Synapse will try to find the template files below. # If not set, or the files named below are not found within the template # directory, default templates from within the Synapse package will be used. diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py index 044ff06d84..0b297e54c4 100644 --- a/synapse/handlers/sso.py +++ b/synapse/handlers/sso.py @@ -41,7 +41,12 @@ from synapse.http import get_request_user_agent from synapse.http.server import respond_with_html, respond_with_redirect from synapse.http.site import SynapseRequest -from synapse.types import JsonDict, UserID, contains_invalid_mxid_characters +from synapse.types import ( + JsonDict, + UserID, + contains_invalid_mxid_characters, + create_requester, +) from synapse.util.async_helpers import Linearizer from synapse.util.stringutils import random_string @@ -185,11 +190,14 @@ def __init__(self, hs: "HomeServer"): self._auth_handler = hs.get_auth_handler() self._error_template = hs.config.sso_error_template self._bad_user_template = hs.config.sso_auth_bad_user_template + self._profile_handler = hs.get_profile_handler() # The following template is shown after a successful user interactive # authentication session. It tells the user they can close the window. self._sso_auth_success_template = hs.config.sso_auth_success_template + self._sso_update_profile_information = hs.config.sso_update_profile_information + # a lock on the mappings self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock()) @@ -458,6 +466,21 @@ async def complete_sso_login_request( request.getClientIP(), ) new_user = True + elif self._sso_update_profile_information: + attributes = await self._call_attribute_mapper(sso_to_matrix_id_mapper) + if attributes.display_name: + user_id_obj = UserID.from_string(user_id) + profile_display_name = await self._profile_handler.get_displayname( + user_id_obj + ) + if profile_display_name != attributes.display_name: + requester = create_requester( + user_id, + authenticated_entity=user_id, + ) + await self._profile_handler.set_displayname( + user_id_obj, requester, attributes.display_name, True + ) await self._auth_handler.complete_sso_login( user_id, From 96f6293de51c2fcf530bb6ca3705cf596c19656f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 22 Jun 2021 04:02:53 -0500 Subject: [PATCH 307/619] Add endpoints for backfilling history (MSC2716) (#9247) Work on https://github.com/matrix-org/matrix-doc/pull/2716 --- changelog.d/9247.feature | 1 + scripts-dev/complement.sh | 2 +- synapse/api/auth.py | 7 +- synapse/api/constants.py | 15 + synapse/config/experimental.py | 3 + synapse/events/__init__.py | 9 + synapse/events/builder.py | 17 +- synapse/handlers/message.py | 104 ++++++- synapse/handlers/room_member.py | 90 ++++++ synapse/rest/client/v1/room.py | 288 +++++++++++++++++- .../databases/main/event_federation.py | 50 ++- tests/handlers/test_presence.py | 4 +- .../test_federation_sender_shard.py | 4 +- tests/storage/test_redaction.py | 13 +- 14 files changed, 584 insertions(+), 23 deletions(-) create mode 100644 changelog.d/9247.feature diff --git a/changelog.d/9247.feature b/changelog.d/9247.feature new file mode 100644 index 0000000000..c687acf102 --- /dev/null +++ b/changelog.d/9247.feature @@ -0,0 +1 @@ +Add experimental support for backfilling history into rooms ([MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index 0043964673..ba060104c3 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -65,4 +65,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -go test -v -tags synapse_blacklist,msc2946,msc3083 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests +go test -v -tags synapse_blacklist,msc2946,msc3083,msc2716 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests diff --git a/synapse/api/auth.py b/synapse/api/auth.py index cf4333a923..edf1b918eb 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -92,11 +92,8 @@ def __init__(self, hs: "HomeServer"): async def check_from_context( self, room_version: str, event, context, do_sig_check=True ) -> None: - prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self.compute_auth_events( - event, prev_state_ids, for_verification=True - ) - auth_events_by_id = await self.store.get_events(auth_events_ids) + auth_event_ids = event.auth_event_ids() + auth_events_by_id = await self.store.get_events(auth_event_ids) auth_events = {(e.type, e.state_key): e for e in auth_events_by_id.values()} room_version_obj = KNOWN_ROOM_VERSIONS[room_version] diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 6c3958f7ab..414e4c019a 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -119,6 +119,9 @@ class EventTypes: SpaceChild = "m.space.child" SpaceParent = "m.space.parent" + MSC2716_INSERTION = "org.matrix.msc2716.insertion" + MSC2716_MARKER = "org.matrix.msc2716.marker" + class ToDeviceEventTypes: RoomKeyRequest = "m.room_key_request" @@ -185,6 +188,18 @@ class EventContentFields: # cf https://github.com/matrix-org/matrix-doc/pull/1772 ROOM_TYPE = "type" + # Used on normal messages to indicate they were historically imported after the fact + MSC2716_HISTORICAL = "org.matrix.msc2716.historical" + # For "insertion" events + MSC2716_NEXT_CHUNK_ID = "org.matrix.msc2716.next_chunk_id" + # Used on normal message events to indicate where the chunk connects to + MSC2716_CHUNK_ID = "org.matrix.msc2716.chunk_id" + # For "marker" events + MSC2716_MARKER_INSERTION = "org.matrix.msc2716.marker.insertion" + MSC2716_MARKER_INSERTION_PREV_EVENTS = ( + "org.matrix.msc2716.marker.insertion_prev_events" + ) + class RoomEncryptionAlgorithms: MEGOLM_V1_AES_SHA2 = "m.megolm.v1.aes-sha2" diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 6ebce4b2f7..7fb1f7021f 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -29,3 +29,6 @@ def read_config(self, config: JsonDict, **kwargs): # MSC3026 (busy presence state) self.msc3026_enabled = experimental.get("msc3026_enabled", False) # type: bool + + # MSC2716 (backfill existing history) + self.msc2716_enabled = experimental.get("msc2716_enabled", False) # type: bool diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index c8b52cbc7a..0cb9c1cc1e 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -119,6 +119,7 @@ def __init__(self, internal_metadata_dict: JsonDict): redacted = DictProperty("redacted") # type: bool txn_id = DictProperty("txn_id") # type: str token_id = DictProperty("token_id") # type: str + historical = DictProperty("historical") # type: bool # XXX: These are set by StreamWorkerStore._set_before_and_after. # I'm pretty sure that these are never persisted to the database, so shouldn't @@ -204,6 +205,14 @@ def is_redacted(self): """ return self._dict.get("redacted", False) + def is_historical(self) -> bool: + """Whether this is a historical message. + This is used by the batchsend historical message endpoint and + is needed to and mark the event as backfilled and skip some checks + like push notifications. + """ + return self._dict.get("historical", False) + class EventBase(metaclass=abc.ABCMeta): @property diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 5793553a88..81bf8615b7 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import Any, Dict, List, Optional, Tuple, Union import attr @@ -33,6 +34,8 @@ from synapse.util import Clock from synapse.util.stringutils import random_string +logger = logging.getLogger(__name__) + @attr.s(slots=True, cmp=False, frozen=True) class EventBuilder: @@ -100,6 +103,7 @@ async def build( self, prev_event_ids: List[str], auth_event_ids: Optional[List[str]], + depth: Optional[int] = None, ) -> EventBase: """Transform into a fully signed and hashed event @@ -108,6 +112,9 @@ async def build( auth_event_ids: The event IDs to use as the auth events. Should normally be set to None, which will cause them to be calculated based on the room state at the prev_events. + depth: Override the depth used to order the event in the DAG. + Should normally be set to None, which will cause the depth to be calculated + based on the prev_events. Returns: The signed and hashed event. @@ -131,8 +138,14 @@ async def build( auth_events = auth_event_ids prev_events = prev_event_ids - old_depth = await self._store.get_max_depth_of(prev_event_ids) - depth = old_depth + 1 + # Otherwise, progress the depth as normal + if depth is None: + ( + _, + most_recent_prev_event_depth, + ) = await self._store.get_max_depth_of(prev_event_ids) + + depth = most_recent_prev_event_depth + 1 # we cap depth of generated events, to ensure that they are not # rejected by other servers (and so that they can be persisted in diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 4d2255bdf1..db12abd59d 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -482,6 +482,9 @@ async def create_event( prev_event_ids: Optional[List[str]] = None, auth_event_ids: Optional[List[str]] = None, require_consent: bool = True, + outlier: bool = False, + historical: bool = False, + depth: Optional[int] = None, ) -> Tuple[EventBase, EventContext]: """ Given a dict from a client, create a new event. @@ -508,6 +511,14 @@ async def create_event( require_consent: Whether to check if the requester has consented to the privacy policy. + + outlier: Indicates whether the event is an `outlier`, i.e. if + it's from an arbitrary point and floating in the DAG as + opposed to being inline with the current DAG. + depth: Override the depth used to order the event in the DAG. + Should normally be set to None, which will cause the depth to be calculated + based on the prev_events. + Raises: ResourceLimitError if server is blocked to some resource being exceeded @@ -563,11 +574,36 @@ async def create_event( if txn_id is not None: builder.internal_metadata.txn_id = txn_id + builder.internal_metadata.outlier = outlier + + builder.internal_metadata.historical = historical + + # Strip down the auth_event_ids to only what we need to auth the event. + # For example, we don't need extra m.room.member that don't match event.sender + if auth_event_ids is not None: + temp_event = await builder.build( + prev_event_ids=prev_event_ids, + auth_event_ids=auth_event_ids, + depth=depth, + ) + auth_events = await self.store.get_events_as_list(auth_event_ids) + # Create a StateMap[str] + auth_event_state_map = { + (e.type, e.state_key): e.event_id for e in auth_events + } + # Actually strip down and use the necessary auth events + auth_event_ids = self.auth.compute_auth_events( + event=temp_event, + current_state_ids=auth_event_state_map, + for_verification=False, + ) + event, context = await self.create_new_client_event( builder=builder, requester=requester, prev_event_ids=prev_event_ids, auth_event_ids=auth_event_ids, + depth=depth, ) # In an ideal world we wouldn't need the second part of this condition. However, @@ -724,9 +760,13 @@ async def create_and_send_nonmember_event( self, requester: Requester, event_dict: dict, + prev_event_ids: Optional[List[str]] = None, + auth_event_ids: Optional[List[str]] = None, ratelimit: bool = True, txn_id: Optional[str] = None, ignore_shadow_ban: bool = False, + outlier: bool = False, + depth: Optional[int] = None, ) -> Tuple[EventBase, int]: """ Creates an event, then sends it. @@ -736,10 +776,24 @@ async def create_and_send_nonmember_event( Args: requester: The requester sending the event. event_dict: An entire event. + prev_event_ids: + The event IDs to use as the prev events. + Should normally be left as None to automatically request them + from the database. + auth_event_ids: + The event ids to use as the auth_events for the new event. + Should normally be left as None, which will cause them to be calculated + based on the room state at the prev_events. ratelimit: Whether to rate limit this send. txn_id: The transaction ID. ignore_shadow_ban: True if shadow-banned users should be allowed to send this event. + outlier: Indicates whether the event is an `outlier`, i.e. if + it's from an arbitrary point and floating in the DAG as + opposed to being inline with the current DAG. + depth: Override the depth used to order the event in the DAG. + Should normally be set to None, which will cause the depth to be calculated + based on the prev_events. Returns: The event, and its stream ordering (if deduplication happened, @@ -779,7 +833,13 @@ async def create_and_send_nonmember_event( return event, event.internal_metadata.stream_ordering event, context = await self.create_event( - requester, event_dict, txn_id=txn_id + requester, + event_dict, + txn_id=txn_id, + prev_event_ids=prev_event_ids, + auth_event_ids=auth_event_ids, + outlier=outlier, + depth=depth, ) assert self.hs.is_mine_id(event.sender), "User must be our own: %s" % ( @@ -811,6 +871,7 @@ async def create_new_client_event( requester: Optional[Requester] = None, prev_event_ids: Optional[List[str]] = None, auth_event_ids: Optional[List[str]] = None, + depth: Optional[int] = None, ) -> Tuple[EventBase, EventContext]: """Create a new event for a local client @@ -828,6 +889,10 @@ async def create_new_client_event( Should normally be left as None, which will cause them to be calculated based on the room state at the prev_events. + depth: Override the depth used to order the event in the DAG. + Should normally be set to None, which will cause the depth to be calculated + based on the prev_events. + Returns: Tuple of created event, context """ @@ -851,9 +916,24 @@ async def create_new_client_event( ), "Attempting to create an event with no prev_events" event = await builder.build( - prev_event_ids=prev_event_ids, auth_event_ids=auth_event_ids + prev_event_ids=prev_event_ids, + auth_event_ids=auth_event_ids, + depth=depth, ) - context = await self.state.compute_event_context(event) + + old_state = None + + # Pass on the outlier property from the builder to the event + # after it is created + if builder.internal_metadata.outlier: + event.internal_metadata.outlier = builder.internal_metadata.outlier + + # Calculate the state for outliers that pass in their own `auth_event_ids` + if auth_event_ids: + old_state = await self.store.get_events_as_list(auth_event_ids) + + context = await self.state.compute_event_context(event, old_state=old_state) + if requester: context.app_service = requester.app_service @@ -1018,7 +1098,13 @@ async def _persist_event( the arguments. """ - await self.action_generator.handle_push_actions_for_event(event, context) + # Skip push notification actions for historical messages + # because we don't want to notify people about old history back in time. + # The historical messages also do not have the proper `context.current_state_ids` + # 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) try: # If we're a worker we need to hit out to the master. @@ -1317,13 +1403,21 @@ async def persist_and_notify_client_event( if prev_state_ids: raise AuthError(403, "Changing the room create event is forbidden") + # Mark any `m.historical` messages as backfilled so they don't appear + # in `/sync` and have the proper decrementing `stream_ordering` as we import + backfilled = False + if event.internal_metadata.is_historical(): + backfilled = True + # Note that this returns the event that was persisted, which may not be # the same as we passed in if it was deduplicated due transaction IDs. ( event, event_pos, max_stream_token, - ) = await self.storage.persistence.persist_event(event, context=context) + ) = await self.storage.persistence.persist_event( + event, context=context, backfilled=backfilled + ) if self._ephemeral_events_enabled: # If there's an expiry timestamp on the event, schedule its expiry. diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index a49a61a34c..1192591609 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -257,11 +257,42 @@ async def _local_membership_update( room_id: str, membership: str, prev_event_ids: List[str], + auth_event_ids: Optional[List[str]] = None, txn_id: Optional[str] = None, ratelimit: bool = True, content: Optional[dict] = None, require_consent: bool = True, + outlier: bool = False, ) -> Tuple[str, int]: + """ + Internal membership update function to get an existing event or create + and persist a new event for the new membership change. + + Args: + requester: + target: + room_id: + membership: + prev_event_ids: The event IDs to use as the prev events + + auth_event_ids: + The event ids to use as the auth_events for the new event. + Should normally be left as None, which will cause them to be calculated + based on the room state at the prev_events. + + txn_id: + ratelimit: + content: + require_consent: + + outlier: Indicates whether the event is an `outlier`, i.e. if + it's from an arbitrary point and floating in the DAG as + opposed to being inline with the current DAG. + + Returns: + Tuple of event ID and stream ordering position + """ + user_id = target.to_string() if content is None: @@ -298,7 +329,9 @@ async def _local_membership_update( }, txn_id=txn_id, prev_event_ids=prev_event_ids, + auth_event_ids=auth_event_ids, require_consent=require_consent, + outlier=outlier, ) prev_state_ids = await context.get_prev_state_ids() @@ -399,6 +432,9 @@ async def update_membership( ratelimit: bool = True, content: Optional[dict] = None, require_consent: bool = True, + outlier: bool = False, + prev_event_ids: Optional[List[str]] = None, + auth_event_ids: Optional[List[str]] = None, ) -> Tuple[str, int]: """Update a user's membership in a room. @@ -413,6 +449,14 @@ async def update_membership( ratelimit: Whether to rate limit the request. content: The content of the created event. require_consent: Whether consent is required. + outlier: Indicates whether the event is an `outlier`, i.e. if + it's from an arbitrary point and floating in the DAG as + opposed to being inline with the current DAG. + prev_event_ids: The event IDs to use as the prev events + auth_event_ids: + The event ids to use as the auth_events for the new event. + Should normally be left as None, which will cause them to be calculated + based on the room state at the prev_events. Returns: A tuple of the new event ID and stream ID. @@ -439,6 +483,9 @@ async def update_membership( ratelimit=ratelimit, content=content, require_consent=require_consent, + outlier=outlier, + prev_event_ids=prev_event_ids, + auth_event_ids=auth_event_ids, ) return result @@ -455,10 +502,36 @@ async def update_membership_locked( ratelimit: bool = True, content: Optional[dict] = None, require_consent: bool = True, + outlier: bool = False, + prev_event_ids: Optional[List[str]] = None, + auth_event_ids: Optional[List[str]] = None, ) -> Tuple[str, int]: """Helper for update_membership. Assumes that the membership linearizer is already held for the room. + + Args: + requester: + target: + room_id: + action: + txn_id: + remote_room_hosts: + third_party_signed: + ratelimit: + content: + require_consent: + outlier: Indicates whether the event is an `outlier`, i.e. if + it's from an arbitrary point and floating in the DAG as + opposed to being inline with the current DAG. + prev_event_ids: The event IDs to use as the prev events + auth_event_ids: + The event ids to use as the auth_events for the new event. + Should normally be left as None, which will cause them to be calculated + based on the room state at the prev_events. + + Returns: + A tuple of the new event ID and stream ID. """ content_specified = bool(content) if content is None: @@ -543,6 +616,21 @@ async def update_membership_locked( if block_invite: raise SynapseError(403, "Invites have been disabled on this server") + if prev_event_ids: + return await self._local_membership_update( + requester=requester, + target=target, + room_id=room_id, + membership=effective_membership_state, + txn_id=txn_id, + ratelimit=ratelimit, + prev_event_ids=prev_event_ids, + auth_event_ids=auth_event_ids, + content=content, + require_consent=require_consent, + outlier=outlier, + ) + latest_event_ids = await self.store.get_prev_events_for_room(room_id) current_state_ids = await self.state_handler.get_current_state_ids( @@ -732,8 +820,10 @@ async def update_membership_locked( txn_id=txn_id, ratelimit=ratelimit, prev_event_ids=latest_event_ids, + auth_event_ids=auth_event_ids, content=content, require_consent=require_consent, + outlier=outlier, ) async def transfer_room_state_on_room_upgrade( diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 16d087ea60..92ebe838fd 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple from urllib import parse as urlparse -from synapse.api.constants import EventTypes, Membership +from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.api.errors import ( AuthError, Codes, @@ -266,6 +266,288 @@ def on_PUT(self, request, room_id, event_type, txn_id): ) +class RoomBatchSendEventRestServlet(TransactionRestServlet): + """ + API endpoint which can insert a chunk of events historically back in time + next to the given `prev_event`. + + `chunk_id` comes from `next_chunk_id `in the response of the batch send + endpoint and is derived from the "insertion" events added to each chunk. + It's not required for the first batch send. + + `state_events_at_start` is used to define the historical state events + needed to auth the events like join events. These events will float + outside of the normal DAG as outlier's and won't be visible in the chat + history which also allows us to insert multiple chunks without having a bunch + of `@mxid joined the room` noise between each chunk. + + `events` is chronological chunk/list of events you want to insert. + There is a reverse-chronological constraint on chunks so once you insert + some messages, you can only insert older ones after that. + tldr; Insert chunks from your most recent history -> oldest history. + + POST /_matrix/client/unstable/org.matrix.msc2716/rooms//batch_send?prev_event=&chunk_id= + { + "events": [ ... ], + "state_events_at_start": [ ... ] + } + """ + + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/org.matrix.msc2716" + "/rooms/(?P[^/]*)/batch_send$" + ), + ) + + def __init__(self, hs): + super().__init__(hs) + self.hs = hs + self.store = hs.get_datastore() + self.state_store = hs.get_storage().state + self.event_creation_handler = hs.get_event_creation_handler() + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + + async def inherit_depth_from_prev_ids(self, prev_event_ids) -> int: + ( + most_recent_prev_event_id, + most_recent_prev_event_depth, + ) = await self.store.get_max_depth_of(prev_event_ids) + + # We want to insert the historical event after the `prev_event` but before the successor event + # + # We inherit depth from the successor event instead of the `prev_event` + # because events returned from `/messages` are first sorted by `topological_ordering` + # which is just the `depth` and then tie-break with `stream_ordering`. + # + # We mark these inserted historical events as "backfilled" which gives them a + # negative `stream_ordering`. If we use the same depth as the `prev_event`, + # then our historical event will tie-break and be sorted before the `prev_event` + # when it should come after. + # + # We want to use the successor event depth so they appear after `prev_event` because + # it has a larger `depth` but before the successor event because the `stream_ordering` + # is negative before the successor event. + successor_event_ids = await self.store.get_successor_events( + [most_recent_prev_event_id] + ) + + # If we can't find any successor events, then it's a forward extremity of + # historical messages and we can just inherit from the previous historical + # event which we can already assume has the correct depth where we want + # to insert into. + if not successor_event_ids: + depth = most_recent_prev_event_depth + else: + ( + _, + oldest_successor_depth, + ) = await self.store.get_min_depth_of(successor_event_ids) + + depth = oldest_successor_depth + + return depth + + async def on_POST(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=False) + + if not requester.app_service: + raise AuthError( + 403, + "Only application services can use the /batchsend endpoint", + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["state_events_at_start", "events"]) + + prev_events_from_query = parse_strings_from_args(request.args, "prev_event") + chunk_id_from_query = parse_string(request, "chunk_id", default=None) + + if prev_events_from_query is None: + raise SynapseError( + 400, + "prev_event query parameter is required when inserting historical messages back in time", + errcode=Codes.MISSING_PARAM, + ) + + # For the event we are inserting next to (`prev_events_from_query`), + # find the most recent auth events (derived from state events) that + # allowed that message to be sent. We will use that as a base + # to auth our historical messages against. + ( + most_recent_prev_event_id, + _, + ) = await self.store.get_max_depth_of(prev_events_from_query) + # mapping from (type, state_key) -> state_event_id + prev_state_map = await self.state_store.get_state_ids_for_event( + most_recent_prev_event_id + ) + # List of state event ID's + prev_state_ids = list(prev_state_map.values()) + auth_event_ids = prev_state_ids + + for state_event in body["state_events_at_start"]: + assert_params_in_dict( + state_event, ["type", "origin_server_ts", "content", "sender"] + ) + + logger.debug( + "RoomBatchSendEventRestServlet inserting state_event=%s, auth_event_ids=%s", + state_event, + auth_event_ids, + ) + + event_dict = { + "type": state_event["type"], + "origin_server_ts": state_event["origin_server_ts"], + "content": state_event["content"], + "room_id": room_id, + "sender": state_event["sender"], + "state_key": state_event["state_key"], + } + + # Make the state events float off on their own + fake_prev_event_id = "$" + random_string(43) + + # TODO: This is pretty much the same as some other code to handle inserting state in this file + if event_dict["type"] == EventTypes.Member: + membership = event_dict["content"].get("membership", None) + event_id, _ = await self.room_member_handler.update_membership( + requester, + target=UserID.from_string(event_dict["state_key"]), + room_id=room_id, + action=membership, + content=event_dict["content"], + outlier=True, + prev_event_ids=[fake_prev_event_id], + # Make sure to use a copy of this list because we modify it + # later in the loop here. Otherwise it will be the same + # reference and also update in the event when we append later. + auth_event_ids=auth_event_ids.copy(), + ) + else: + # TODO: Add some complement tests that adds state that is not member joins + # and will use this code path. Maybe we only want to support join state events + # and can get rid of this `else`? + ( + event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + requester, + event_dict, + outlier=True, + prev_event_ids=[fake_prev_event_id], + # Make sure to use a copy of this list because we modify it + # later in the loop here. Otherwise it will be the same + # reference and also update in the event when we append later. + auth_event_ids=auth_event_ids.copy(), + ) + event_id = event.event_id + + auth_event_ids.append(event_id) + + events_to_create = body["events"] + + # If provided, connect the chunk to the last insertion point + # The chunk ID passed in comes from the chunk_id in the + # "insertion" event from the previous chunk. + if chunk_id_from_query: + last_event_in_chunk = events_to_create[-1] + last_event_in_chunk["content"][ + EventContentFields.MSC2716_CHUNK_ID + ] = chunk_id_from_query + + # Add an "insertion" event to the start of each chunk (next to the oldest + # event in the chunk) so the next chunk can be connected to this one. + next_chunk_id = random_string(64) + insertion_event = { + "type": EventTypes.MSC2716_INSERTION, + "sender": requester.user.to_string(), + "content": { + EventContentFields.MSC2716_NEXT_CHUNK_ID: next_chunk_id, + EventContentFields.MSC2716_HISTORICAL: True, + }, + # Since the insertion event is put at the start of the chunk, + # where the oldest event is, copy the origin_server_ts from + # the first event we're inserting + "origin_server_ts": events_to_create[0]["origin_server_ts"], + } + # Prepend the insertion event to the start of the chunk + events_to_create = [insertion_event] + events_to_create + + inherited_depth = await self.inherit_depth_from_prev_ids(prev_events_from_query) + + event_ids = [] + prev_event_ids = prev_events_from_query + events_to_persist = [] + for ev in events_to_create: + assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"]) + + # Mark all events as historical + # This has important semantics within the Synapse internals to backfill properly + ev["content"][EventContentFields.MSC2716_HISTORICAL] = True + + event_dict = { + "type": ev["type"], + "origin_server_ts": ev["origin_server_ts"], + "content": ev["content"], + "room_id": room_id, + "sender": ev["sender"], # requester.user.to_string(), + "prev_events": prev_event_ids.copy(), + } + + event, context = await self.event_creation_handler.create_event( + requester, + event_dict, + prev_event_ids=event_dict.get("prev_events"), + auth_event_ids=auth_event_ids, + historical=True, + depth=inherited_depth, + ) + logger.debug( + "RoomBatchSendEventRestServlet inserting event=%s, prev_event_ids=%s, auth_event_ids=%s", + event, + prev_event_ids, + auth_event_ids, + ) + + assert self.hs.is_mine_id(event.sender), "User must be our own: %s" % ( + event.sender, + ) + + events_to_persist.append((event, context)) + event_id = event.event_id + + event_ids.append(event_id) + prev_event_ids = [event_id] + + # Persist events in reverse-chronological order so they have the + # correct stream_ordering as they are backfilled (which decrements). + # Events are sorted by (topological_ordering, stream_ordering) + # where topological_ordering is just depth. + for (event, context) in reversed(events_to_persist): + ev = await self.event_creation_handler.handle_new_client_event( + requester=requester, + event=event, + context=context, + ) + + return 200, { + "state_events": auth_event_ids, + "events": event_ids, + "next_chunk_id": next_chunk_id, + } + + def on_GET(self, request, room_id): + return 501, "Not implemented" + + def on_PUT(self, request, room_id): + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id + ) + + # TODO: Needs unit testing for room ID + alias joins class JoinRoomAliasServlet(TransactionRestServlet): def __init__(self, hs): @@ -1054,6 +1336,8 @@ async def on_POST( def register_servlets(hs: "HomeServer", http_server, is_worker=False): + msc2716_enabled = hs.config.experimental.msc2716_enabled + RoomStateEventRestServlet(hs).register(http_server) RoomMemberListRestServlet(hs).register(http_server) JoinedRoomMemberListRestServlet(hs).register(http_server) @@ -1061,6 +1345,8 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False): JoinRoomAliasServlet(hs).register(http_server) RoomMembershipRestServlet(hs).register(http_server) RoomSendEventRestServlet(hs).register(http_server) + if msc2716_enabled: + RoomBatchSendEventRestServlet(hs).register(http_server) PublicRoomListRestServlet(hs).register(http_server) RoomStateRestServlet(hs).register(http_server) RoomRedactEventRestServlet(hs).register(http_server) diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index ff81d5cd17..c0ea445550 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -16,6 +16,7 @@ from queue import Empty, PriorityQueue from typing import Collection, Dict, Iterable, List, Set, Tuple +from synapse.api.constants import MAX_DEPTH from synapse.api.errors import StoreError from synapse.events import EventBase from synapse.metrics.background_process_metrics import wrap_as_background_process @@ -670,8 +671,8 @@ def get_oldest_events_with_depth_in_room_txn(self, txn, room_id): return dict(txn) - async def get_max_depth_of(self, event_ids: List[str]) -> int: - """Returns the max depth of a set of event IDs + async def get_max_depth_of(self, event_ids: List[str]) -> Tuple[str, int]: + """Returns the event ID and depth for the event that has the max depth from a set of event IDs Args: event_ids: The event IDs to calculate the max depth of. @@ -680,14 +681,53 @@ async def get_max_depth_of(self, event_ids: List[str]) -> int: table="events", column="event_id", iterable=event_ids, - retcols=("depth",), + retcols=( + "event_id", + "depth", + ), desc="get_max_depth_of", ) if not rows: - return 0 + return None, 0 else: - return max(row["depth"] for row in rows) + max_depth_event_id = "" + current_max_depth = 0 + for row in rows: + if row["depth"] > current_max_depth: + max_depth_event_id = row["event_id"] + current_max_depth = row["depth"] + + return max_depth_event_id, current_max_depth + + async def get_min_depth_of(self, event_ids: List[str]) -> Tuple[str, int]: + """Returns the event ID and depth for the event that has the min depth from a set of event IDs + + Args: + event_ids: The event IDs to calculate the max depth of. + """ + rows = await self.db_pool.simple_select_many_batch( + table="events", + column="event_id", + iterable=event_ids, + retcols=( + "event_id", + "depth", + ), + desc="get_min_depth_of", + ) + + if not rows: + return None, 0 + else: + min_depth_event_id = "" + current_min_depth = MAX_DEPTH + for row in rows: + if row["depth"] < current_min_depth: + min_depth_event_id = row["event_id"] + current_min_depth = row["depth"] + + return min_depth_event_id, current_min_depth async def get_prev_events_for_room(self, room_id: str) -> List[str]: """ diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index d90a9fec91..dfb9b3a0fa 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -863,7 +863,9 @@ def _add_new_user(self, room_id, user_id): self.store.get_latest_event_ids_in_room(room_id) ) - event = self.get_success(builder.build(prev_event_ids, None)) + event = self.get_success( + builder.build(prev_event_ids=prev_event_ids, auth_event_ids=None) + ) self.get_success(self.federation_handler.on_receive_pdu(hostname, event)) diff --git a/tests/replication/test_federation_sender_shard.py b/tests/replication/test_federation_sender_shard.py index 48ab3aa4e3..584da58371 100644 --- a/tests/replication/test_federation_sender_shard.py +++ b/tests/replication/test_federation_sender_shard.py @@ -224,7 +224,9 @@ def create_room_with_remote_server(self, user, token, remote_server="other_serve } builder = factory.for_room_version(room_version, event_dict) - join_event = self.get_success(builder.build(prev_event_ids, None)) + join_event = self.get_success( + builder.build(prev_event_ids=prev_event_ids, auth_event_ids=None) + ) self.get_success(federation.on_send_join_request(remote_server, join_event)) self.replicate() diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index bb31ab756d..dbacce4380 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -232,9 +232,14 @@ def __init__(self, base_builder, event_id): self._base_builder = base_builder self._event_id = event_id - async def build(self, prev_event_ids, auth_event_ids): + async def build( + self, + prev_event_ids, + auth_event_ids, + depth: Optional[int] = None, + ): built_event = await self._base_builder.build( - prev_event_ids, auth_event_ids + prev_event_ids=prev_event_ids, auth_event_ids=auth_event_ids ) built_event._event_id = self._event_id @@ -251,6 +256,10 @@ def room_id(self): def type(self): return self._base_builder.type + @property + def internal_metadata(self): + return self._base_builder.internal_metadata + event_1, context_1 = self.get_success( self.event_creation_handler.create_new_client_event( EventIdManglingBuilder( From 34db6bb9f56de6db6283a1b74815315e9de051bf Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 22 Jun 2021 12:24:10 +0200 Subject: [PATCH 308/619] Warn users trying to use the deprecated spam checker interface (#10210) So admins aren't surprised if things break when we remove this code in a couple of months. --- changelog.d/10210.removal | 1 + synapse/config/spam_checker.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 changelog.d/10210.removal diff --git a/changelog.d/10210.removal b/changelog.d/10210.removal new file mode 100644 index 0000000000..5fb7bfb47e --- /dev/null +++ b/changelog.d/10210.removal @@ -0,0 +1 @@ +The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py index c24165eb8a..d0311d6468 100644 --- a/synapse/config/spam_checker.py +++ b/synapse/config/spam_checker.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import Any, Dict, List, Tuple from synapse.config import ConfigError @@ -19,6 +20,15 @@ from ._base import Config +logger = logging.getLogger(__name__) + +LEGACY_SPAM_CHECKER_WARNING = """ +This server is using a spam checker module that is implementing the deprecated spam +checker interface. Please check with the module's maintainer to see if a new version +supporting Synapse's generic modules system is available. +For more information, please see https://matrix-org.github.io/synapse/develop/modules.html +---------------------------------------------------------------------------------------""" + class SpamCheckerConfig(Config): section = "spamchecker" @@ -42,3 +52,8 @@ def read_config(self, config, **kwargs): self.spam_checkers.append(load_module(spam_checker, config_path)) else: raise ConfigError("spam_checker syntax is incorrect") + + # If this configuration is being used in any way, warn the admin that it is going + # away soon. + if self.spam_checkers: + logger.warning(LEGACY_SPAM_CHECKER_WARNING) From 33701dc11650b2df31adb7babac63c5a818648d9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 22 Jun 2021 12:00:45 +0100 Subject: [PATCH 309/619] Fix schema delta to not take as long on large servers (#10227) Introduced in #6739 --- changelog.d/10227.feature | 1 + .../schema/main/delta/59/11add_knock_members_to_stats.sql | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10227.feature diff --git a/changelog.d/10227.feature b/changelog.d/10227.feature new file mode 100644 index 0000000000..9c41140194 --- /dev/null +++ b/changelog.d/10227.feature @@ -0,0 +1 @@ +Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql b/synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql index 56c0ad0003..8eb2196f6a 100644 --- a/synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql +++ b/synapse/storage/schema/main/delta/59/11add_knock_members_to_stats.sql @@ -13,5 +13,8 @@ * limitations under the License. */ -ALTER TABLE room_stats_current ADD COLUMN knocked_members INT NOT NULL DEFAULT '0'; -ALTER TABLE room_stats_historical ADD COLUMN knocked_members BIGINT NOT NULL DEFAULT '0'; \ No newline at end of file +-- Existing rows will default to NULL, so anything reading from these tables +-- needs to interpret NULL as 0. This is fine here as no existing rooms can have +-- any knocked members. +ALTER TABLE room_stats_current ADD COLUMN knocked_members INT; +ALTER TABLE room_stats_historical ADD COLUMN knocked_members BIGINT; From 9ec45aca1fbea55475f3a47c37b01058a0eafe98 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 23 Jun 2021 09:38:27 +0100 Subject: [PATCH 310/619] 1.37.0rc1 --- CHANGES.md | 66 +++++++++++++++++++++++++++++++++++++++ changelog.d/10062.feature | 1 - changelog.d/10062.removal | 1 - changelog.d/10080.misc | 1 - changelog.d/10108.feature | 1 - changelog.d/10115.bugfix | 1 - changelog.d/10122.doc | 1 - changelog.d/10134.misc | 1 - changelog.d/10143.misc | 1 - changelog.d/10144.misc | 1 - changelog.d/10145.misc | 1 - changelog.d/10148.misc | 1 - changelog.d/10154.bugfix | 1 - changelog.d/10155.misc | 1 - changelog.d/10156.misc | 1 - changelog.d/10157.bugfix | 1 - changelog.d/10158.bugfix | 1 - changelog.d/10160.misc | 1 - changelog.d/10161.removal | 1 - changelog.d/10164.misc | 1 - changelog.d/10167.feature | 1 - changelog.d/10168.misc | 1 - changelog.d/10175.bugfix | 1 - changelog.d/10180.doc | 1 - changelog.d/10183.misc | 1 - changelog.d/10184.bugfix | 1 - changelog.d/10188.misc | 1 - changelog.d/10189.misc | 1 - changelog.d/10190.misc | 1 - changelog.d/10191.feature | 1 - changelog.d/10193.misc | 1 - changelog.d/10194.removal | 1 - changelog.d/10195.bugfix | 1 - changelog.d/10197.misc | 1 - changelog.d/10198.doc | 1 - changelog.d/10199.misc | 1 - changelog.d/10206.feature | 1 - changelog.d/10208.bugfix | 1 - changelog.d/10210.removal | 1 - changelog.d/10212.feature | 1 - changelog.d/10221.bugfix | 1 - changelog.d/10227.feature | 1 - changelog.d/6739.feature | 1 - changelog.d/8436.doc | 1 - changelog.d/9247.feature | 1 - changelog.d/9359.feature | 1 - changelog.d/9933.misc | 1 - synapse/__init__.py | 2 +- 48 files changed, 67 insertions(+), 47 deletions(-) delete mode 100644 changelog.d/10062.feature delete mode 100644 changelog.d/10062.removal delete mode 100644 changelog.d/10080.misc delete mode 100644 changelog.d/10108.feature delete mode 100644 changelog.d/10115.bugfix delete mode 100644 changelog.d/10122.doc delete mode 100644 changelog.d/10134.misc delete mode 100644 changelog.d/10143.misc delete mode 100644 changelog.d/10144.misc delete mode 100644 changelog.d/10145.misc delete mode 100644 changelog.d/10148.misc delete mode 100644 changelog.d/10154.bugfix delete mode 100644 changelog.d/10155.misc delete mode 100644 changelog.d/10156.misc delete mode 100644 changelog.d/10157.bugfix delete mode 100644 changelog.d/10158.bugfix delete mode 100644 changelog.d/10160.misc delete mode 100644 changelog.d/10161.removal delete mode 100644 changelog.d/10164.misc delete mode 100644 changelog.d/10167.feature delete mode 100644 changelog.d/10168.misc delete mode 100644 changelog.d/10175.bugfix delete mode 100644 changelog.d/10180.doc delete mode 100644 changelog.d/10183.misc delete mode 100644 changelog.d/10184.bugfix delete mode 100644 changelog.d/10188.misc delete mode 100644 changelog.d/10189.misc delete mode 100644 changelog.d/10190.misc delete mode 100644 changelog.d/10191.feature delete mode 100644 changelog.d/10193.misc delete mode 100644 changelog.d/10194.removal delete mode 100644 changelog.d/10195.bugfix delete mode 100644 changelog.d/10197.misc delete mode 100644 changelog.d/10198.doc delete mode 100644 changelog.d/10199.misc delete mode 100644 changelog.d/10206.feature delete mode 100644 changelog.d/10208.bugfix delete mode 100644 changelog.d/10210.removal delete mode 100644 changelog.d/10212.feature delete mode 100644 changelog.d/10221.bugfix delete mode 100644 changelog.d/10227.feature delete mode 100644 changelog.d/6739.feature delete mode 100644 changelog.d/8436.doc delete mode 100644 changelog.d/9247.feature delete mode 100644 changelog.d/9359.feature delete mode 100644 changelog.d/9933.misc diff --git a/CHANGES.md b/CHANGES.md index 0f9798a4d3..3cf1814264 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,69 @@ +Synapse 1.37.0rc1 (2021-06-23) +============================== + +Features +-------- + +- Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. ([\#6739](https://github.com/matrix-org/synapse/issues/6739), [\#9359](https://github.com/matrix-org/synapse/issues/9359), [\#10167](https://github.com/matrix-org/synapse/issues/10167), [\#10212](https://github.com/matrix-org/synapse/issues/10212), [\#10227](https://github.com/matrix-org/synapse/issues/10227)) +- Add experimental support for backfilling history into rooms ([MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#9247](https://github.com/matrix-org/synapse/issues/9247)) +- Standardised the module interface. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10206](https://github.com/matrix-org/synapse/issues/10206)) +- Implement config option `sso.update_profile_information` to sync SSO users' profile information with the identity provider each time they login. Currently only displayname is supported. ([\#10108](https://github.com/matrix-org/synapse/issues/10108)) +- Ensure that errors during startup are written to the logs and the console. ([\#10191](https://github.com/matrix-org/synapse/issues/10191)) + + +Bugfixes +-------- + +- Fix a bug introduced in Synapse v1.25.0 that prevented the `ip_range_whitelist` configuration option from working for federation and identity servers. Contributed by @mikure. ([\#10115](https://github.com/matrix-org/synapse/issues/10115)) +- Remove a broken import line in Synapse's admin_cmd worker. Broke in 1.33.0. ([\#10154](https://github.com/matrix-org/synapse/issues/10154)) +- Fix a bug introduced in v1.21.0 which could cause `/sync` to return immediately with an empty response. ([\#10157](https://github.com/matrix-org/synapse/issues/10157), [\#10158](https://github.com/matrix-org/synapse/issues/10158)) +- Fix a minor bug in the response to `/_matrix/client/r0/user/{user}/openid/request_token`. Contributed by @lukaslihotzki. ([\#10175](https://github.com/matrix-org/synapse/issues/10175)) +- Always require users to re-authenticate for dangerous operations: deactivating an account, modifying an account password, and adding 3PIDs. ([\#10184](https://github.com/matrix-org/synapse/issues/10184)) +- Fix a bug introduced in Synpase 1.7.2 where remote server count metrics collection would be incorrectly delayed on startup. Found by @heftig. ([\#10195](https://github.com/matrix-org/synapse/issues/10195)) +- Fix a bug introduced in v1.35.1 where an `allow` key of a `m.room.join_rules` event could be applied for incorrect room versions and configurations. ([\#10208](https://github.com/matrix-org/synapse/issues/10208)) +- Fix performance regression in responding to user key requests over federation. Introduced in v1.34.0rc1. ([\#10221](https://github.com/matrix-org/synapse/issues/10221)) + + +Improved Documentation +---------------------- + +- Add a new guide to decoding request logs. ([\#8436](https://github.com/matrix-org/synapse/issues/8436)) +- Mention in the sample homeserver config that you may need to configure max upload size in your reverse proxy. Contributed by @aaronraimist. ([\#10122](https://github.com/matrix-org/synapse/issues/10122)) +- Fix broken links in documentation. ([\#10180](https://github.com/matrix-org/synapse/issues/10180)) +- Deploy a snapshot of the documentation website upon each new Synapse release. ([\#10198](https://github.com/matrix-org/synapse/issues/10198)) + + +Deprecations and Removals +------------------------- + +- The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10210](https://github.com/matrix-org/synapse/issues/10210)) +- Stop supporting the unstable spaces prefixes from MSC1772. ([\#10161](https://github.com/matrix-org/synapse/issues/10161)) +- Remove Synapse's support for automatically fetching and renewing certificates using the ACME v1 protocol. This protocol has been fully turned off by Let's Encrypt for existing install on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. ([\#10194](https://github.com/matrix-org/synapse/issues/10194)) + + +Internal Changes +---------------- + +- Update the database schema versioning to support gradual migration away from legacy tables. ([\#9933](https://github.com/matrix-org/synapse/issues/9933)) +- Add type hints to the federation servlets. ([\#10080](https://github.com/matrix-org/synapse/issues/10080)) +- Improve OpenTracing for event persistence. ([\#10134](https://github.com/matrix-org/synapse/issues/10134), [\#10193](https://github.com/matrix-org/synapse/issues/10193)) +- Clean up the interface for injecting opentracing over HTTP. ([\#10143](https://github.com/matrix-org/synapse/issues/10143)) +- Limit the number of in-flight `/keys/query` requests from a single device. ([\#10144](https://github.com/matrix-org/synapse/issues/10144)) +- Refactor EventPersistenceQueue. ([\#10145](https://github.com/matrix-org/synapse/issues/10145)) +- Document `SYNAPSE_TEST_LOG_LEVEL` to see the logger output when running tests. ([\#10148](https://github.com/matrix-org/synapse/issues/10148)) +- Update the Complement build tags in GitHub Actions to test currently experimental features. ([\#10155](https://github.com/matrix-org/synapse/issues/10155)) +- Add `synapse_federation_soft_failed_events_total` metric to track how often events are soft failed. ([\#10156](https://github.com/matrix-org/synapse/issues/10156)) +- Fetch the corresponding complement branch when performing CI. ([\#10160](https://github.com/matrix-org/synapse/issues/10160)) +- Add some developer documentation about boolean columns in database schemas. ([\#10164](https://github.com/matrix-org/synapse/issues/10164)) +- Add extra logging fields to better debug where events are being soft failed. ([\#10168](https://github.com/matrix-org/synapse/issues/10168)) +- Add debug logging for when we enter and exit `Measure` blocks. ([\#10183](https://github.com/matrix-org/synapse/issues/10183)) +- Improve comments in structured logging code. ([\#10188](https://github.com/matrix-org/synapse/issues/10188)) +- Update MSC3083 support for modifications in the MSC. ([\#10189](https://github.com/matrix-org/synapse/issues/10189)) +- Remove redundant DNS lookup limiter. ([\#10190](https://github.com/matrix-org/synapse/issues/10190)) +- Upgrade `black` linting tool to 21.6b0. ([\#10197](https://github.com/matrix-org/synapse/issues/10197)) +- Expose opentracing trace id in response headers. ([\#10199](https://github.com/matrix-org/synapse/issues/10199)) + + Synapse 1.36.0 (2021-06-15) =========================== diff --git a/changelog.d/10062.feature b/changelog.d/10062.feature deleted file mode 100644 index 97474f030c..0000000000 --- a/changelog.d/10062.feature +++ /dev/null @@ -1 +0,0 @@ -Standardised the module interface. diff --git a/changelog.d/10062.removal b/changelog.d/10062.removal deleted file mode 100644 index 7f0cbdae2e..0000000000 --- a/changelog.d/10062.removal +++ /dev/null @@ -1 +0,0 @@ -The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. \ No newline at end of file diff --git a/changelog.d/10080.misc b/changelog.d/10080.misc deleted file mode 100644 index 9adb0fbd02..0000000000 --- a/changelog.d/10080.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to the federation servlets. diff --git a/changelog.d/10108.feature b/changelog.d/10108.feature deleted file mode 100644 index 4930a5acf5..0000000000 --- a/changelog.d/10108.feature +++ /dev/null @@ -1 +0,0 @@ -Implement config option `sso.update_profile_information` to sync SSO users' profile information with the identity provider each time they login. Currently only displayname is supported. diff --git a/changelog.d/10115.bugfix b/changelog.d/10115.bugfix deleted file mode 100644 index e16f356e68..0000000000 --- a/changelog.d/10115.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse v1.25.0 that prevented the `ip_range_whitelist` configuration option from working for federation and identity servers. Contributed by @mikure. diff --git a/changelog.d/10122.doc b/changelog.d/10122.doc deleted file mode 100644 index 07a0d2520d..0000000000 --- a/changelog.d/10122.doc +++ /dev/null @@ -1 +0,0 @@ -Mention in the sample homeserver config that you may need to configure max upload size in your reverse proxy. Contributed by @aaronraimist. diff --git a/changelog.d/10134.misc b/changelog.d/10134.misc deleted file mode 100644 index ce9702645d..0000000000 --- a/changelog.d/10134.misc +++ /dev/null @@ -1 +0,0 @@ -Improve OpenTracing for event persistence. diff --git a/changelog.d/10143.misc b/changelog.d/10143.misc deleted file mode 100644 index 37aa344db2..0000000000 --- a/changelog.d/10143.misc +++ /dev/null @@ -1 +0,0 @@ -Clean up the interface for injecting opentracing over HTTP. diff --git a/changelog.d/10144.misc b/changelog.d/10144.misc deleted file mode 100644 index fe96d645d7..0000000000 --- a/changelog.d/10144.misc +++ /dev/null @@ -1 +0,0 @@ -Limit the number of in-flight `/keys/query` requests from a single device. diff --git a/changelog.d/10145.misc b/changelog.d/10145.misc deleted file mode 100644 index 2f0c643b08..0000000000 --- a/changelog.d/10145.misc +++ /dev/null @@ -1 +0,0 @@ -Refactor EventPersistenceQueue. diff --git a/changelog.d/10148.misc b/changelog.d/10148.misc deleted file mode 100644 index 5066392d40..0000000000 --- a/changelog.d/10148.misc +++ /dev/null @@ -1 +0,0 @@ -Document `SYNAPSE_TEST_LOG_LEVEL` to see the logger output when running tests. diff --git a/changelog.d/10154.bugfix b/changelog.d/10154.bugfix deleted file mode 100644 index f70a3d47bc..0000000000 --- a/changelog.d/10154.bugfix +++ /dev/null @@ -1 +0,0 @@ -Remove a broken import line in Synapse's admin_cmd worker. Broke in 1.33.0. \ No newline at end of file diff --git a/changelog.d/10155.misc b/changelog.d/10155.misc deleted file mode 100644 index 27b98e7fed..0000000000 --- a/changelog.d/10155.misc +++ /dev/null @@ -1 +0,0 @@ -Update the Complement build tags in GitHub Actions to test currently experimental features. \ No newline at end of file diff --git a/changelog.d/10156.misc b/changelog.d/10156.misc deleted file mode 100644 index 92a188b87b..0000000000 --- a/changelog.d/10156.misc +++ /dev/null @@ -1 +0,0 @@ -Add `synapse_federation_soft_failed_events_total` metric to track how often events are soft failed. diff --git a/changelog.d/10157.bugfix b/changelog.d/10157.bugfix deleted file mode 100644 index 6eaaa05b80..0000000000 --- a/changelog.d/10157.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.21.0 which could cause `/sync` to return immediately with an empty response. diff --git a/changelog.d/10158.bugfix b/changelog.d/10158.bugfix deleted file mode 100644 index 6eaaa05b80..0000000000 --- a/changelog.d/10158.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.21.0 which could cause `/sync` to return immediately with an empty response. diff --git a/changelog.d/10160.misc b/changelog.d/10160.misc deleted file mode 100644 index 80f378130f..0000000000 --- a/changelog.d/10160.misc +++ /dev/null @@ -1 +0,0 @@ -Fetch the corresponding complement branch when performing CI. diff --git a/changelog.d/10161.removal b/changelog.d/10161.removal deleted file mode 100644 index d4411464c7..0000000000 --- a/changelog.d/10161.removal +++ /dev/null @@ -1 +0,0 @@ -Stop supporting the unstable spaces prefixes from MSC1772. diff --git a/changelog.d/10164.misc b/changelog.d/10164.misc deleted file mode 100644 index a98f1e7c7a..0000000000 --- a/changelog.d/10164.misc +++ /dev/null @@ -1 +0,0 @@ -Add some developer documentation about boolean columns in database schemas. diff --git a/changelog.d/10167.feature b/changelog.d/10167.feature deleted file mode 100644 index 9c41140194..0000000000 --- a/changelog.d/10167.feature +++ /dev/null @@ -1 +0,0 @@ -Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/changelog.d/10168.misc b/changelog.d/10168.misc deleted file mode 100644 index 5ca7b89806..0000000000 --- a/changelog.d/10168.misc +++ /dev/null @@ -1 +0,0 @@ -Add extra logging fields to better debug where events are being soft failed. diff --git a/changelog.d/10175.bugfix b/changelog.d/10175.bugfix deleted file mode 100644 index 42e8f749cc..0000000000 --- a/changelog.d/10175.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a minor bug in the response to `/_matrix/client/r0/user/{user}/openid/request_token`. Contributed by @lukaslihotzki. diff --git a/changelog.d/10180.doc b/changelog.d/10180.doc deleted file mode 100644 index 1568450198..0000000000 --- a/changelog.d/10180.doc +++ /dev/null @@ -1 +0,0 @@ -Fix broken links in documentation. \ No newline at end of file diff --git a/changelog.d/10183.misc b/changelog.d/10183.misc deleted file mode 100644 index c0e01ad3db..0000000000 --- a/changelog.d/10183.misc +++ /dev/null @@ -1 +0,0 @@ -Add debug logging for when we enter and exit `Measure` blocks. diff --git a/changelog.d/10184.bugfix b/changelog.d/10184.bugfix deleted file mode 100644 index 6bf440d8f8..0000000000 --- a/changelog.d/10184.bugfix +++ /dev/null @@ -1 +0,0 @@ -Always require users to re-authenticate for dangerous operations: deactivating an account, modifying an account password, and adding 3PIDs. diff --git a/changelog.d/10188.misc b/changelog.d/10188.misc deleted file mode 100644 index c1ea81c21a..0000000000 --- a/changelog.d/10188.misc +++ /dev/null @@ -1 +0,0 @@ -Improve comments in structured logging code. diff --git a/changelog.d/10189.misc b/changelog.d/10189.misc deleted file mode 100644 index df0e636c7d..0000000000 --- a/changelog.d/10189.misc +++ /dev/null @@ -1 +0,0 @@ -Update MSC3083 support for modifications in the MSC. diff --git a/changelog.d/10190.misc b/changelog.d/10190.misc deleted file mode 100644 index 388ed3ffb6..0000000000 --- a/changelog.d/10190.misc +++ /dev/null @@ -1 +0,0 @@ -Remove redundant DNS lookup limiter. diff --git a/changelog.d/10191.feature b/changelog.d/10191.feature deleted file mode 100644 index 40f306c421..0000000000 --- a/changelog.d/10191.feature +++ /dev/null @@ -1 +0,0 @@ -Ensure that errors during startup are written to the logs and the console. diff --git a/changelog.d/10193.misc b/changelog.d/10193.misc deleted file mode 100644 index ce9702645d..0000000000 --- a/changelog.d/10193.misc +++ /dev/null @@ -1 +0,0 @@ -Improve OpenTracing for event persistence. diff --git a/changelog.d/10194.removal b/changelog.d/10194.removal deleted file mode 100644 index 74874df4eb..0000000000 --- a/changelog.d/10194.removal +++ /dev/null @@ -1 +0,0 @@ -Remove Synapse's support for automatically fetching and renewing certificates using the ACME v1 protocol. This protocol has been fully turned off by Let's Encrypt for existing install on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. diff --git a/changelog.d/10195.bugfix b/changelog.d/10195.bugfix deleted file mode 100644 index 01cab1bda8..0000000000 --- a/changelog.d/10195.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synpase 1.7.2 where remote server count metrics collection would be incorrectly delayed on startup. Found by @heftig. \ No newline at end of file diff --git a/changelog.d/10197.misc b/changelog.d/10197.misc deleted file mode 100644 index cbb3b454be..0000000000 --- a/changelog.d/10197.misc +++ /dev/null @@ -1 +0,0 @@ -Upgrade `black` linting tool to 21.6b0. diff --git a/changelog.d/10198.doc b/changelog.d/10198.doc deleted file mode 100644 index 8d1aeab1a7..0000000000 --- a/changelog.d/10198.doc +++ /dev/null @@ -1 +0,0 @@ -Deploy a snapshot of the documentation website upon each new Synapse release. diff --git a/changelog.d/10199.misc b/changelog.d/10199.misc deleted file mode 100644 index 69b18aeacc..0000000000 --- a/changelog.d/10199.misc +++ /dev/null @@ -1 +0,0 @@ -Expose opentracing trace id in response headers. diff --git a/changelog.d/10206.feature b/changelog.d/10206.feature deleted file mode 100644 index 97474f030c..0000000000 --- a/changelog.d/10206.feature +++ /dev/null @@ -1 +0,0 @@ -Standardised the module interface. diff --git a/changelog.d/10208.bugfix b/changelog.d/10208.bugfix deleted file mode 100644 index 32b6465717..0000000000 --- a/changelog.d/10208.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.35.1 where an `allow` key of a `m.room.join_rules` event could be applied for incorrect room versions and configurations. diff --git a/changelog.d/10210.removal b/changelog.d/10210.removal deleted file mode 100644 index 5fb7bfb47e..0000000000 --- a/changelog.d/10210.removal +++ /dev/null @@ -1 +0,0 @@ -The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. diff --git a/changelog.d/10212.feature b/changelog.d/10212.feature deleted file mode 100644 index 9c41140194..0000000000 --- a/changelog.d/10212.feature +++ /dev/null @@ -1 +0,0 @@ -Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/changelog.d/10221.bugfix b/changelog.d/10221.bugfix deleted file mode 100644 index 8853a9bf4e..0000000000 --- a/changelog.d/10221.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix performance regression in responding to user key requests over federation. Introduced in v1.34.0rc1. diff --git a/changelog.d/10227.feature b/changelog.d/10227.feature deleted file mode 100644 index 9c41140194..0000000000 --- a/changelog.d/10227.feature +++ /dev/null @@ -1 +0,0 @@ -Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/changelog.d/6739.feature b/changelog.d/6739.feature deleted file mode 100644 index 9c41140194..0000000000 --- a/changelog.d/6739.feature +++ /dev/null @@ -1 +0,0 @@ -Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/changelog.d/8436.doc b/changelog.d/8436.doc deleted file mode 100644 index 77fc098200..0000000000 --- a/changelog.d/8436.doc +++ /dev/null @@ -1 +0,0 @@ -Add a new guide to decoding request logs. diff --git a/changelog.d/9247.feature b/changelog.d/9247.feature deleted file mode 100644 index c687acf102..0000000000 --- a/changelog.d/9247.feature +++ /dev/null @@ -1 +0,0 @@ -Add experimental support for backfilling history into rooms ([MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). diff --git a/changelog.d/9359.feature b/changelog.d/9359.feature deleted file mode 100644 index 9c41140194..0000000000 --- a/changelog.d/9359.feature +++ /dev/null @@ -1 +0,0 @@ -Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. \ No newline at end of file diff --git a/changelog.d/9933.misc b/changelog.d/9933.misc deleted file mode 100644 index 0860026670..0000000000 --- a/changelog.d/9933.misc +++ /dev/null @@ -1 +0,0 @@ -Update the database schema versioning to support gradual migration away from legacy tables. diff --git a/synapse/__init__.py b/synapse/__init__.py index c3016fc6ed..6d1c6d6f72 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.36.0" +__version__ = "1.37.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 27c06a6e0699f92bcd02b9e930dc8191ab87305e Mon Sep 17 00:00:00 2001 From: "Michael[tm] Smith" Date: Wed, 23 Jun 2021 19:25:03 +0900 Subject: [PATCH 311/619] Drop Origin & Accept from Access-Control-Allow-Headers value (#10114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Drop Origin & Accept from Access-Control-Allow-Headers value This change drops the Origin and Accept header names from the value of the Access-Control-Allow-Headers response header sent by Synapse. Per the CORS protocol, it’s not necessary or useful to include those header names. Details: Per-spec at https://fetch.spec.whatwg.org/#forbidden-header-name, Origin is a “forbidden header name” set by the browser and that frontend JavaScript code is never allowed to set. So the value of Access-Control-Allow-Headers isn’t relevant to Origin or in general to other headers set by the browser itself — the browser never ever consults the Access-Control-Allow-Headers value to confirm that it’s OK for the request to include an Origin header. And per-spec at https://fetch.spec.whatwg.org/#cors-safelisted-request-header, Accept is a “CORS-safelisted request-header”, which means that browsers allow requests to contain the Accept header regardless of whether the Access-Control-Allow-Headers value contains "Accept". So it’s unnecessary for the Access-Control-Allow-Headers to explicitly include Accept. Browsers will not perform a CORS preflight for requests containing an Accept request header. Related: https://github.com/matrix-org/matrix-doc/pull/3225 Signed-off-by: Michael[tm] Smith --- changelog.d/10114.misc | 1 + synapse/http/server.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10114.misc diff --git a/changelog.d/10114.misc b/changelog.d/10114.misc new file mode 100644 index 0000000000..808548f7c7 --- /dev/null +++ b/changelog.d/10114.misc @@ -0,0 +1 @@ +Drop Origin and Accept from the value of the Access-Control-Allow-Headers response header. diff --git a/synapse/http/server.py b/synapse/http/server.py index 845651e606..efbc6d5b25 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -728,7 +728,7 @@ def set_cors_headers(request: Request): ) request.setHeader( b"Access-Control-Allow-Headers", - b"Origin, X-Requested-With, Content-Type, Accept, Authorization, Date", + b"X-Requested-With, Content-Type, Authorization, Date", ) From 8beead66ae48aa11f1e25da42256eb92b8bce099 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:54:50 +0100 Subject: [PATCH 312/619] Send out invite rejections and knocks over federation (#10223) ensure that events sent via `send_leave` and `send_knock` are sent on to the rest of the federation. --- changelog.d/10223.bugfix | 1 + scripts-dev/complement.sh | 2 +- synapse/handlers/federation.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10223.bugfix diff --git a/changelog.d/10223.bugfix b/changelog.d/10223.bugfix new file mode 100644 index 0000000000..4e42f6b608 --- /dev/null +++ b/changelog.d/10223.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug which meant that invite rejections and knocks were not sent out over federation in a timely manner. diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index ba060104c3..aca32edc17 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -65,4 +65,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -go test -v -tags synapse_blacklist,msc2946,msc3083,msc2716 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests +go test -v -tags synapse_blacklist,msc2946,msc3083,msc2716,msc2403 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 1b566dbf2d..74d169a2ac 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1980,6 +1980,13 @@ async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: event.internal_metadata.outlier = False + # Send this event on behalf of the other server. + # + # The remote server isn't a full participant in the room at this point, so + # may not have an up-to-date list of the other homeservers participating in + # the room, so we send it on their behalf. + event.internal_metadata.send_on_behalf_of = origin + context = await self.state_handler.compute_event_context(event) await self._auth_and_persist_event(origin, event, context) @@ -2084,6 +2091,13 @@ async def on_send_knock_request( event.internal_metadata.outlier = False + # Send this event on behalf of the other server. + # + # The remote server isn't a full participant in the room at this point, so + # may not have an up-to-date list of the other homeservers participating in + # the room, so we send it on their behalf. + event.internal_metadata.send_on_behalf_of = origin + context = await self.state_handler.compute_event_context(event) event_allowed = await self.third_party_event_rules.check_event_allowed( From e19e3d452d7553cad974556c723b7a17e6f11a9d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 23 Jun 2021 16:14:52 +0200 Subject: [PATCH 313/619] Improve the reliability of auto-joining remote rooms (#10237) If a room is remote and we don't have a user in it, always try to join it. It might fail if the room is invite-only, but we don't have a user to invite with, so at this point it's the best we can do. Fixes #10233 (at least to some extent) --- changelog.d/10237.misc | 1 + synapse/handlers/register.py | 63 ++++++++++++++++++++++++--------- tests/handlers/test_register.py | 49 ++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 changelog.d/10237.misc diff --git a/changelog.d/10237.misc b/changelog.d/10237.misc new file mode 100644 index 0000000000..d76c119a41 --- /dev/null +++ b/changelog.d/10237.misc @@ -0,0 +1 @@ +Improve the reliability of auto-joining remote rooms. diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index ca1ed6a5c0..4b4b579741 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -386,11 +386,32 @@ async def _create_and_join_rooms(self, user_id: str) -> None: room_alias = RoomAlias.from_string(r) if self.hs.hostname != room_alias.domain: - logger.warning( - "Cannot create room alias %s, " - "it does not match server domain", + # If the alias is remote, try to join the room. This might fail + # because the room might be invite only, but we don't have any local + # user in the room to invite this one with, so at this point that's + # the best we can do. + logger.info( + "Cannot automatically create room with alias %s as it isn't" + " local, trying to join the room instead", r, ) + + ( + room, + remote_room_hosts, + ) = await room_member_handler.lookup_room_alias(room_alias) + room_id = room.to_string() + + await room_member_handler.update_membership( + requester=create_requester( + user_id, authenticated_entity=self._server_name + ), + target=UserID.from_string(user_id), + room_id=room_id, + remote_room_hosts=remote_room_hosts, + action="join", + ratelimit=False, + ) else: # A shallow copy is OK here since the only key that is # modified is room_alias_name. @@ -448,22 +469,32 @@ async def _join_rooms(self, user_id: str) -> None: ) # Calculate whether the room requires an invite or can be - # joined directly. Note that unless a join rule of public exists, - # it is treated as requiring an invite. - requires_invite = True - - state = await self.store.get_filtered_current_state_ids( - room_id, StateFilter.from_types([(EventTypes.JoinRules, "")]) + # joined directly. By default, we consider the room as requiring an + # invite if the homeserver is in the room (unless told otherwise by the + # join rules). Otherwise we consider it as being joinable, at the risk of + # failing to join, but in this case there's little more we can do since + # we don't have a local user in the room to craft up an invite with. + requires_invite = await self.store.is_host_joined( + room_id, + self.server_name, ) - event_id = state.get((EventTypes.JoinRules, "")) - if event_id: - join_rules_event = await self.store.get_event( - event_id, allow_none=True + if requires_invite: + # If the server is in the room, check if the room is public. + state = await self.store.get_filtered_current_state_ids( + room_id, StateFilter.from_types([(EventTypes.JoinRules, "")]) ) - if join_rules_event: - join_rule = join_rules_event.content.get("join_rule", None) - requires_invite = join_rule and join_rule != JoinRules.PUBLIC + + event_id = state.get((EventTypes.JoinRules, "")) + if event_id: + join_rules_event = await self.store.get_event( + event_id, allow_none=True + ) + if join_rules_event: + join_rule = join_rules_event.content.get("join_rule", None) + requires_invite = ( + join_rule and join_rule != JoinRules.PUBLIC + ) # Send the invite, if necessary. if requires_invite: diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index a9fd3036dc..c901003225 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -18,7 +18,7 @@ from synapse.api.constants import UserTypes from synapse.api.errors import Codes, ResourceLimitError, SynapseError from synapse.spam_checker_api import RegistrationBehaviour -from synapse.types import RoomAlias, UserID, create_requester +from synapse.types import RoomAlias, RoomID, UserID, create_requester from tests.test_utils import make_awaitable from tests.unittest import override_config @@ -643,3 +643,50 @@ async def get_or_create_user( ) return user_id, token + + +class RemoteAutoJoinTestCase(unittest.HomeserverTestCase): + """Tests auto-join on remote rooms.""" + + def make_homeserver(self, reactor, clock): + self.room_id = "!roomid:remotetest" + + async def update_membership(*args, **kwargs): + pass + + async def lookup_room_alias(*args, **kwargs): + return RoomID.from_string(self.room_id), ["remotetest"] + + self.room_member_handler = Mock(spec=["update_membership", "lookup_room_alias"]) + self.room_member_handler.update_membership.side_effect = update_membership + self.room_member_handler.lookup_room_alias.side_effect = lookup_room_alias + + hs = self.setup_test_homeserver(room_member_handler=self.room_member_handler) + return hs + + def prepare(self, reactor, clock, hs): + self.handler = self.hs.get_registration_handler() + self.store = self.hs.get_datastore() + + @override_config({"auto_join_rooms": ["#room:remotetest"]}) + def test_auto_create_auto_join_remote_room(self): + """Tests that we don't attempt to create remote rooms, and that we don't attempt + to invite ourselves to rooms we're not in.""" + + # Register a first user; this should call _create_and_join_rooms + self.get_success(self.handler.register_user(localpart="jeff")) + + _, kwargs = self.room_member_handler.update_membership.call_args + + self.assertEqual(kwargs["room_id"], self.room_id) + self.assertEqual(kwargs["action"], "join") + self.assertEqual(kwargs["remote_room_hosts"], ["remotetest"]) + + # Register a second user; this should call _join_rooms + self.get_success(self.handler.register_user(localpart="jeff2")) + + _, kwargs = self.room_member_handler.update_membership.call_args + + self.assertEqual(kwargs["room_id"], self.room_id) + self.assertEqual(kwargs["action"], "join") + self.assertEqual(kwargs["remote_room_hosts"], ["remotetest"]) From 394673055db4df49bfd58c2f6118834a6d928563 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 23 Jun 2021 15:57:41 +0100 Subject: [PATCH 314/619] Re-introduce "Leave out optional keys from /sync" change (#10214) Required some fixes due to merge conflicts with #6739, but nothing too hairy. The first commit is the same as the original (after merge conflict resolution) then two more for compatibility with the latest sync code. --- changelog.d/10214.feature | 1 + synapse/rest/client/v2_alpha/sync.py | 69 ++++++++++++------- tests/rest/client/v2_alpha/test_sync.py | 30 +------- .../test_resource_limits_server_notices.py | 8 ++- 4 files changed, 53 insertions(+), 55 deletions(-) create mode 100644 changelog.d/10214.feature diff --git a/changelog.d/10214.feature b/changelog.d/10214.feature new file mode 100644 index 0000000000..a3818c9d25 --- /dev/null +++ b/changelog.d/10214.feature @@ -0,0 +1 @@ +Omit empty fields from the `/sync` response. Contributed by @deepbluev7. \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index 042e1788b6..ecbbcf3851 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -13,6 +13,7 @@ # limitations under the License. import itertools import logging +from collections import defaultdict from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple from synapse.api.constants import Membership, PresenceState @@ -232,29 +233,51 @@ async def encode_response(self, time_now, sync_result, access_token_id, filter): ) logger.debug("building sync response dict") - return { - "account_data": {"events": sync_result.account_data}, - "to_device": {"events": sync_result.to_device}, - "device_lists": { - "changed": list(sync_result.device_lists.changed), - "left": list(sync_result.device_lists.left), - }, - "presence": SyncRestServlet.encode_presence(sync_result.presence, time_now), - "rooms": { - Membership.JOIN: joined, - Membership.INVITE: invited, - Membership.KNOCK: knocked, - Membership.LEAVE: archived, - }, - "groups": { - Membership.JOIN: sync_result.groups.join, - Membership.INVITE: sync_result.groups.invite, - Membership.LEAVE: sync_result.groups.leave, - }, - "device_one_time_keys_count": sync_result.device_one_time_keys_count, - "org.matrix.msc2732.device_unused_fallback_key_types": sync_result.device_unused_fallback_key_types, - "next_batch": await sync_result.next_batch.to_string(self.store), - } + + response: dict = defaultdict(dict) + response["next_batch"] = await sync_result.next_batch.to_string(self.store) + + if sync_result.account_data: + response["account_data"] = {"events": sync_result.account_data} + if sync_result.presence: + response["presence"] = SyncRestServlet.encode_presence( + sync_result.presence, time_now + ) + + if sync_result.to_device: + response["to_device"] = {"events": sync_result.to_device} + + if sync_result.device_lists.changed: + response["device_lists"]["changed"] = list(sync_result.device_lists.changed) + if sync_result.device_lists.left: + response["device_lists"]["left"] = list(sync_result.device_lists.left) + + if sync_result.device_one_time_keys_count: + response[ + "device_one_time_keys_count" + ] = sync_result.device_one_time_keys_count + if sync_result.device_unused_fallback_key_types: + response[ + "org.matrix.msc2732.device_unused_fallback_key_types" + ] = sync_result.device_unused_fallback_key_types + + if joined: + response["rooms"][Membership.JOIN] = joined + if invited: + response["rooms"][Membership.INVITE] = invited + if knocked: + response["rooms"][Membership.KNOCK] = knocked + if archived: + response["rooms"][Membership.LEAVE] = archived + + if sync_result.groups.join: + response["groups"][Membership.JOIN] = sync_result.groups.join + if sync_result.groups.invite: + response["groups"][Membership.INVITE] = sync_result.groups.invite + if sync_result.groups.leave: + response["groups"][Membership.LEAVE] = sync_result.groups.leave + + return response @staticmethod def encode_presence(events, time_now): diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 012910f136..cdca3a3e23 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -41,35 +41,7 @@ def test_sync_argless(self): channel = self.make_request("GET", "/sync") self.assertEqual(channel.code, 200) - self.assertTrue( - { - "next_batch", - "rooms", - "presence", - "account_data", - "to_device", - "device_lists", - }.issubset(set(channel.json_body.keys())) - ) - - def test_sync_presence_disabled(self): - """ - When presence is disabled, the key does not appear in /sync. - """ - self.hs.config.use_presence = False - - channel = self.make_request("GET", "/sync") - - self.assertEqual(channel.code, 200) - self.assertTrue( - { - "next_batch", - "rooms", - "account_data", - "to_device", - "device_lists", - }.issubset(set(channel.json_body.keys())) - ) + self.assertIn("next_batch", channel.json_body) class SyncFilterTestCase(unittest.HomeserverTestCase): diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index d46521ccdc..3245aa91ca 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -306,8 +306,9 @@ def test_no_invite_without_notice(self): channel = self.make_request("GET", "/sync?timeout=0", access_token=tok) - invites = channel.json_body["rooms"]["invite"] - self.assertEqual(len(invites), 0, invites) + self.assertNotIn( + "rooms", channel.json_body, "Got invites without server notice" + ) def test_invite_with_notice(self): """Tests that, if the MAU limit is hit, the server notices user invites each user @@ -364,7 +365,8 @@ def _trigger_notice_and_join(self): # We could also pick another user and sync with it, which would return an # invite to a system notices room, but it doesn't matter which user we're # using so we use the last one because it saves us an extra sync. - invites = channel.json_body["rooms"]["invite"] + if "rooms" in channel.json_body: + invites = channel.json_body["rooms"]["invite"] # Make sure we have an invite to process. self.assertEqual(len(invites), 1, invites) From c955e378683708acd5b88e9cb1980291e06dd9a7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 23 Jun 2021 17:22:08 +0200 Subject: [PATCH 315/619] Fix wrapping of legacy check_registration_for_spam (#10238) Fixes #10234 --- changelog.d/10238.removal | 1 + synapse/events/spamcheck.py | 13 +++--- tests/handlers/test_register.py | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10238.removal diff --git a/changelog.d/10238.removal b/changelog.d/10238.removal new file mode 100644 index 0000000000..5fb7bfb47e --- /dev/null +++ b/changelog.d/10238.removal @@ -0,0 +1 @@ +The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index 45ec96dfc1..efec16c226 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -109,6 +109,8 @@ def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]: if f is None: return None + wrapped_func = f + if f.__name__ == "check_registration_for_spam": checker_args = inspect.signature(f) if len(checker_args.parameters) == 3: @@ -133,19 +135,18 @@ def wrapper( request_info, ) - f = wrapper + wrapped_func = wrapper elif len(checker_args.parameters) != 4: raise RuntimeError( "Bad signature for callback check_registration_for_spam", ) def run(*args, **kwargs): - # We've already made sure f is not None above, but mypy doesn't do well - # across function boundaries so we need to tell it f is definitely not - # None. - assert f is not None + # mypy doesn't do well across function boundaries so we need to tell it + # wrapped_func is definitely not None. + assert wrapped_func is not None - return maybe_awaitable(f(*args, **kwargs)) + return maybe_awaitable(wrapped_func(*args, **kwargs)) return run diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py index a9fd3036dc..c5f6bc3c75 100644 --- a/tests/handlers/test_register.py +++ b/tests/handlers/test_register.py @@ -17,6 +17,7 @@ from synapse.api.auth import Auth from synapse.api.constants import UserTypes from synapse.api.errors import Codes, ResourceLimitError, SynapseError +from synapse.events.spamcheck import load_legacy_spam_checkers from synapse.spam_checker_api import RegistrationBehaviour from synapse.types import RoomAlias, UserID, create_requester @@ -79,6 +80,39 @@ async def check_registration_for_spam( return RegistrationBehaviour.ALLOW +class TestLegacyRegistrationSpamChecker: + def __init__(self, config, api): + pass + + async def check_registration_for_spam( + self, + email_threepid, + username, + request_info, + ): + pass + + +class LegacyAllowAll(TestLegacyRegistrationSpamChecker): + async def check_registration_for_spam( + self, + email_threepid, + username, + request_info, + ): + return RegistrationBehaviour.ALLOW + + +class LegacyDenyAll(TestLegacyRegistrationSpamChecker): + async def check_registration_for_spam( + self, + email_threepid, + username, + request_info, + ): + return RegistrationBehaviour.DENY + + class RegistrationTestCase(unittest.HomeserverTestCase): """Tests the RegistrationHandler.""" @@ -95,6 +129,8 @@ def make_homeserver(self, reactor, clock): hs = self.setup_test_homeserver(config=hs_config) + load_legacy_spam_checkers(hs) + module_api = hs.get_module_api() for module, config in hs.config.modules.loaded_modules: module(config=config, api=module_api) @@ -535,6 +571,46 @@ def test_spam_checker_deny(self): """A spam checker can deny registration, which results in an error.""" self.get_failure(self.handler.register_user(localpart="user"), SynapseError) + @override_config( + { + "spam_checker": [ + { + "module": TestSpamChecker.__module__ + ".LegacyAllowAll", + } + ] + } + ) + def test_spam_checker_legacy_allow(self): + """Tests that a legacy spam checker implementing the legacy 3-arg version of the + check_registration_for_spam callback is correctly called. + + In this test and the following one we test both success and failure to make sure + any failure comes from the spam checker (and not something else failing in the + call stack) and any success comes from the spam checker (and not because a + misconfiguration prevented it from being loaded). + """ + self.get_success(self.handler.register_user(localpart="user")) + + @override_config( + { + "spam_checker": [ + { + "module": TestSpamChecker.__module__ + ".LegacyDenyAll", + } + ] + } + ) + def test_spam_checker_legacy_deny(self): + """Tests that a legacy spam checker implementing the legacy 3-arg version of the + check_registration_for_spam callback is correctly called. + + In this test and the previous one we test both success and failure to make sure + any failure comes from the spam checker (and not something else failing in the + call stack) and any success comes from the spam checker (and not because a + misconfiguration prevented it from being loaded). + """ + self.get_failure(self.handler.register_user(localpart="user"), SynapseError) + @override_config( { "modules": [ From d731ed70d92bb6809d0dc648f9865ec46d275424 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 23 Jun 2021 17:55:26 +0200 Subject: [PATCH 316/619] Fixes to the release script (#10239) * rename major/minor into the right semver terminology minor/patch (since this was something that got me very confused the first couple of times I've used the script) * name the release branch based on the new version, not the previous one --- changelog.d/10239.misc | 1 + scripts-dev/release.py | 52 ++++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 25 deletions(-) create mode 100644 changelog.d/10239.misc diff --git a/changelog.d/10239.misc b/changelog.d/10239.misc new file mode 100644 index 0000000000..d05f1c4411 --- /dev/null +++ b/changelog.d/10239.misc @@ -0,0 +1 @@ +Update the release script to use the semver terminology and determine the release branch based on the next version. diff --git a/scripts-dev/release.py b/scripts-dev/release.py index fc3df9071c..5bfaa4ad2f 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -83,12 +83,6 @@ def run(): if current_version.pre: # If the current version is an RC we don't need to bump any of the # version numbers (other than the RC number). - base_version = "{}.{}.{}".format( - current_version.major, - current_version.minor, - current_version.micro, - ) - if rc: new_version = "{}.{}.{}rc{}".format( current_version.major, @@ -97,49 +91,57 @@ def run(): current_version.pre[1] + 1, ) else: - new_version = base_version + new_version = "{}.{}.{}".format( + current_version.major, + current_version.minor, + current_version.micro, + ) else: - # If this is a new release cycle then we need to know if its a major - # version bump or a hotfix. + # If this is a new release cycle then we need to know if it's a minor + # or a patch version bump. release_type = click.prompt( "Release type", - type=click.Choice(("major", "hotfix")), + type=click.Choice(("minor", "patch")), show_choices=True, - default="major", + default="minor", ) - if release_type == "major": - base_version = new_version = "{}.{}.{}".format( - current_version.major, - current_version.minor + 1, - 0, - ) + if release_type == "minor": if rc: new_version = "{}.{}.{}rc1".format( current_version.major, current_version.minor + 1, 0, ) - + else: + new_version = "{}.{}.{}".format( + current_version.major, + current_version.minor + 1, + 0, + ) else: - base_version = new_version = "{}.{}.{}".format( - current_version.major, - current_version.minor, - current_version.micro + 1, - ) if rc: new_version = "{}.{}.{}rc1".format( current_version.major, current_version.minor, current_version.micro + 1, ) + else: + new_version = "{}.{}.{}".format( + current_version.major, + current_version.minor, + current_version.micro + 1, + ) # Confirm the calculated version is OK. if not click.confirm(f"Create new version: {new_version}?", default=True): click.get_current_context().abort() # Switch to the release branch. - release_branch_name = f"release-v{current_version.major}.{current_version.minor}" + parsed_new_version = version.parse(new_version) + release_branch_name = ( + f"release-v{parsed_new_version.major}.{parsed_new_version.minor}" + ) release_branch = find_ref(repo, release_branch_name) if release_branch: if release_branch.is_remote(): @@ -153,7 +155,7 @@ def run(): # release type. if current_version.is_prerelease: default = release_branch_name - elif release_type == "major": + elif release_type == "minor": default = "develop" else: default = "master" From 7f25d7385909ace3a84ee621f014d56734fecd44 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 23 Jun 2021 16:57:57 +0100 Subject: [PATCH 317/619] Convert UPGRADE.rst to markdown (#10166) This PR: * Converts UPGRADE.rst to markdown and moves the contents into the `docs/` directory. * Updates the contents of UPGRADE.rst to point to the website instead. * Updates links around the codebase that point to UPGRADE.rst. `pandoc` + some manual editing was used to convert from RST to md. --- CHANGES.md | 37 +- README.rst | 4 +- UPGRADE.rst | 1340 +----------------------------------- changelog.d/10062.removal | 2 +- changelog.d/10166.doc | 1 + docs/SUMMARY.md | 2 +- docs/upgrade.md | 1353 +++++++++++++++++++++++++++++++++++++ docs/upgrading/README.md | 7 - 8 files changed, 1381 insertions(+), 1365 deletions(-) create mode 100644 changelog.d/10166.doc create mode 100644 docs/upgrade.md delete mode 100644 docs/upgrading/README.md diff --git a/CHANGES.md b/CHANGES.md index 0f9798a4d3..f21d14d9e0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -685,7 +685,7 @@ Internal Changes Synapse 1.29.0 (2021-03-08) =========================== -Note that synapse now expects an `X-Forwarded-Proto` header when used with a reverse proxy. Please see [UPGRADE.rst](UPGRADE.rst#upgrading-to-v1290) for more details on this change. +Note that synapse now expects an `X-Forwarded-Proto` header when used with a reverse proxy. Please see the [upgrade notes](docs/upgrade.md#upgrading-to-v1290) for more details on this change. No significant changes. @@ -750,7 +750,7 @@ Synapse 1.28.0 (2021-02-25) Note that this release drops support for ARMv7 in the official Docker images, due to repeated problems building for ARMv7 (and the associated maintenance burden this entails). -This release also fixes the documentation included in v1.27.0 around the callback URI for SAML2 identity providers. If your server is configured to use single sign-on via a SAML2 IdP, you may need to make configuration changes. Please review [UPGRADE.rst](UPGRADE.rst) for more details on these changes. +This release also fixes the documentation included in v1.27.0 around the callback URI for SAML2 identity providers. If your server is configured to use single sign-on via a SAML2 IdP, you may need to make configuration changes. Please review the [upgrade notes](docs/upgrade.md) for more details on these changes. Internal Changes @@ -849,9 +849,9 @@ Synapse 1.27.0 (2021-02-16) Note that this release includes a change in Synapse to use Redis as a cache ─ as well as a pub/sub mechanism ─ if Redis support is enabled for workers. No action is needed by server administrators, and we do not expect resource usage of the Redis instance to change dramatically. -This release also changes the callback URI for OpenID Connect (OIDC) and SAML2 identity providers. If your server is configured to use single sign-on via an OIDC/OAuth2 or SAML2 IdP, you may need to make configuration changes. Please review [UPGRADE.rst](UPGRADE.rst) for more details on these changes. +This release also changes the callback URI for OpenID Connect (OIDC) and SAML2 identity providers. If your server is configured to use single sign-on via an OIDC/OAuth2 or SAML2 IdP, you may need to make configuration changes. Please review the [upgrade notes](docs/upgrade.md) for more details on these changes. -This release also changes escaping of variables in the HTML templates for SSO or email notifications. If you have customised these templates, please review [UPGRADE.rst](UPGRADE.rst) for more details on these changes. +This release also changes escaping of variables in the HTML templates for SSO or email notifications. If you have customised these templates, please review the [upgrade notes](docs/upgrade.md) for more details on these changes. Bugfixes @@ -955,7 +955,7 @@ Synapse 1.26.0 (2021-01-27) =========================== This release brings a new schema version for Synapse and rolling back to a previous -version is not trivial. Please review [UPGRADE.rst](UPGRADE.rst) for more details +version is not trivial. Please review the [upgrade notes](docs/upgrade.md) for more details on these changes and for general upgrade guidance. No significant changes since 1.26.0rc2. @@ -982,7 +982,7 @@ Synapse 1.26.0rc1 (2021-01-20) ============================== This release brings a new schema version for Synapse and rolling back to a previous -version is not trivial. Please review [UPGRADE.rst](UPGRADE.rst) for more details +version is not trivial. Please review the [upgrade notes](docs/upgrade.md) for more details on these changes and for general upgrade guidance. Features @@ -1388,7 +1388,7 @@ Internal Changes Synapse 1.23.0 (2020-11-18) =========================== -This release changes the way structured logging is configured. See the [upgrade notes](UPGRADE.rst#upgrading-to-v1230) for details. +This release changes the way structured logging is configured. See the [upgrade notes](docs/upgrade.md#upgrading-to-v1230) for details. **Note**: We are aware of a trivially exploitable denial of service vulnerability in versions of Synapse prior to 1.20.0. Complete details will be disclosed on Monday, November 23rd. If you have not upgraded recently, please do so. @@ -1991,7 +1991,10 @@ No significant changes since 1.19.0rc1. Removal warning --------------- -As outlined in the [previous release](https://github.com/matrix-org/synapse/releases/tag/v1.18.0), we are no longer publishing Docker images with the `-py3` tag suffix. On top of that, we have also removed the `latest-py3` tag. Please see [the announcement in the upgrade notes for 1.18.0](https://github.com/matrix-org/synapse/blob/develop/UPGRADE.rst#upgrading-to-v1180). +As outlined in the [previous release](https://github.com/matrix-org/synapse/releases/tag/v1.18.0), +we are no longer publishing Docker images with the `-py3` tag suffix. On top of that, we have also removed the +`latest-py3` tag. Please see +[the announcement in the upgrade notes for 1.18.0](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md#upgrading-to-v1180). Synapse 1.19.0rc1 (2020-08-13) @@ -2022,7 +2025,7 @@ Bugfixes Updates to the Docker image --------------------------- -- We no longer publish Docker images with the `-py3` tag suffix, as [announced in the upgrade notes](https://github.com/matrix-org/synapse/blob/develop/UPGRADE.rst#upgrading-to-v1180). ([\#8056](https://github.com/matrix-org/synapse/issues/8056)) +- We no longer publish Docker images with the `-py3` tag suffix, as [announced in the upgrade notes](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md#upgrading-to-v1180). ([\#8056](https://github.com/matrix-org/synapse/issues/8056)) Improved Documentation @@ -2580,7 +2583,7 @@ configurations of Synapse: to be incomplete or empty if Synapse was upgraded directly from v1.2.1 or earlier, to versions between v1.4.0 and v1.12.x. -Please review [UPGRADE.rst](UPGRADE.rst) for more details on these changes +Please review the [upgrade notes](docs/upgrade.md) for more details on these changes and for general upgrade guidance. @@ -2681,7 +2684,7 @@ Bugfixes - Fix bad error handling that would cause Synapse to crash if it's provided with a YAML configuration file that's either empty or doesn't parse into a key-value map. ([\#7341](https://github.com/matrix-org/synapse/issues/7341)) - Fix incorrect metrics reporting for `renew_attestations` background task. ([\#7344](https://github.com/matrix-org/synapse/issues/7344)) - Prevent non-federating rooms from appearing in responses to federated `POST /publicRoom` requests when a filter was included. ([\#7367](https://github.com/matrix-org/synapse/issues/7367)) -- Fix a bug which would cause the room durectory to be incorrectly populated if Synapse was upgraded directly from v1.2.1 or earlier to v1.4.0 or later. Note that this fix does not apply retrospectively; see the [upgrade notes](UPGRADE.rst#upgrading-to-v1130) for more information. ([\#7387](https://github.com/matrix-org/synapse/issues/7387)) +- Fix a bug which would cause the room durectory to be incorrectly populated if Synapse was upgraded directly from v1.2.1 or earlier to v1.4.0 or later. Note that this fix does not apply retrospectively; see the [upgrade notes](docs/upgrade.md#upgrading-to-v1130) for more information. ([\#7387](https://github.com/matrix-org/synapse/issues/7387)) - Fix bug in `EventContext.deserialize`. ([\#7393](https://github.com/matrix-org/synapse/issues/7393)) @@ -2831,7 +2834,7 @@ Synapse 1.12.0 includes a database update which is run as part of the upgrade, and which may take some time (several hours in the case of a large server). Synapse will not respond to HTTP requests while this update is taking place. For imformation on seeing if you are affected, and workaround if you -are, see the [upgrade notes](UPGRADE.rst#upgrading-to-v1120). +are, see the [upgrade notes](docs/upgrade.md#upgrading-to-v1120). Security advisory ----------------- @@ -3384,7 +3387,7 @@ Bugfixes Synapse 1.7.0 (2019-12-13) ========================== -This release changes the default settings so that only local authenticated users can query the server's room directory. See the [upgrade notes](UPGRADE.rst#upgrading-to-v170) for details. +This release changes the default settings so that only local authenticated users can query the server's room directory. See the [upgrade notes](docs/upgrade.md#upgrading-to-v170) for details. Support for SQLite versions before 3.11 is now deprecated. A future release will refuse to start if used with an SQLite version before 3.11. @@ -3748,7 +3751,7 @@ Synapse 1.4.0rc1 (2019-09-26) ============================= Note that this release includes significant changes around 3pid -verification. Administrators are reminded to review the [upgrade notes](UPGRADE.rst#upgrading-to-v140). +verification. Administrators are reminded to review the [upgrade notes](docs/upgrade.md#upgrading-to-v140). Features -------- @@ -4124,7 +4127,7 @@ Synapse 1.1.0 (2019-07-04) ========================== As of v1.1.0, Synapse no longer supports Python 2, nor Postgres version 9.4. -See the [upgrade notes](UPGRADE.rst#upgrading-to-v110) for more details. +See the [upgrade notes](docs/upgrade.md#upgrading-to-v110) for more details. This release also deprecates the use of environment variables to configure the docker image. See the [docker README](https://github.com/matrix-org/synapse/blob/release-v1.1.0/docker/README.md#legacy-dynamic-configuration-file-support) @@ -4154,7 +4157,7 @@ Synapse 1.1.0rc1 (2019-07-02) ============================= As of v1.1.0, Synapse no longer supports Python 2, nor Postgres version 9.4. -See the [upgrade notes](UPGRADE.rst#upgrading-to-v110) for more details. +See the [upgrade notes](docs/upgrade.md#upgrading-to-v110) for more details. Features -------- @@ -4926,7 +4929,7 @@ run on Python versions 3.5 or 3.6 (as well as 2.7). Support for Python 3.7 remains experimental. We recommend upgrading to Python 3, but make sure to read the [upgrade -notes](UPGRADE.rst#upgrading-to-v0340) when doing so. +notes](docs/upgrade.md#upgrading-to-v0340) when doing so. Features -------- diff --git a/README.rst b/README.rst index 2ecc93c8a7..1244aab10b 100644 --- a/README.rst +++ b/README.rst @@ -186,11 +186,11 @@ impact to other applications will be minimal. Upgrading an existing Synapse ============================= -The instructions for upgrading synapse are in `UPGRADE.rst`_. +The instructions for upgrading synapse are in `the upgrade notes`_. Please check these instructions as upgrading may require extra steps for some versions of synapse. -.. _UPGRADE.rst: UPGRADE.rst +.. _the upgrade notes: https://matrix-org.github.io/synapse/develop/upgrade.html .. _reverse-proxy: diff --git a/UPGRADE.rst b/UPGRADE.rst index ee8b4fa60b..82548ac850 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -1,1341 +1,7 @@ Upgrading Synapse ================= -Before upgrading check if any special steps are required to upgrade from the -version you currently have installed to the current version of Synapse. The extra -instructions that may be required are listed later in this document. +This document has moved to the `Synapse documentation website `_. +Please update your links. -* Check that your versions of Python and PostgreSQL are still supported. - - Synapse follows upstream lifecycles for `Python`_ and `PostgreSQL`_, and - removes support for versions which are no longer maintained. - - The website https://endoflife.date also offers convenient summaries. - - .. _Python: https://devguide.python.org/devcycle/#end-of-life-branches - .. _PostgreSQL: https://www.postgresql.org/support/versioning/ - -* If Synapse was installed using `prebuilt packages - `_, you will need to follow the normal process - for upgrading those packages. - -* If Synapse was installed from source, then: - - 1. Activate the virtualenv before upgrading. For example, if Synapse is - installed in a virtualenv in ``~/synapse/env`` then run: - - .. code:: bash - - source ~/synapse/env/bin/activate - - 2. If Synapse was installed using pip then upgrade to the latest version by - running: - - .. code:: bash - - pip install --upgrade matrix-synapse - - If Synapse was installed using git then upgrade to the latest version by - running: - - .. code:: bash - - git pull - pip install --upgrade . - - 3. Restart Synapse: - - .. code:: bash - - ./synctl restart - -To check whether your update was successful, you can check the running server -version with: - -.. code:: bash - - # you may need to replace 'localhost:8008' if synapse is not configured - # to listen on port 8008. - - curl http://localhost:8008/_synapse/admin/v1/server_version - -Rolling back to older versions ------------------------------- - -Rolling back to previous releases can be difficult, due to database schema -changes between releases. Where we have been able to test the rollback process, -this will be noted below. - -In general, you will need to undo any changes made during the upgrade process, -for example: - -* pip: - - .. code:: bash - - source env/bin/activate - # replace `1.3.0` accordingly: - pip install matrix-synapse==1.3.0 - -* Debian: - - .. code:: bash - - # replace `1.3.0` and `stretch` accordingly: - wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb - dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb - -Upgrading to v1.37.0 -==================== - -Deprecation of the current spam checker interface -------------------------------------------------- - -The current spam checker interface is deprecated in favour of a new generic modules system. -Authors of spam checker modules can refer to `this documentation `_ -to update their modules. Synapse administrators can refer to `this documentation `_ -to update their configuration once the modules they are using have been updated. - -We plan to remove support for the current spam checker interface in August 2021. - -More module interfaces will be ported over to this new generic system in future versions -of Synapse. - - -Upgrading to v1.34.0 -==================== - -``room_invite_state_types`` configuration setting ------------------------------------------------ - -The ``room_invite_state_types`` configuration setting has been deprecated and -replaced with ``room_prejoin_state``. See the `sample configuration file `_. - -If you have set ``room_invite_state_types`` to the default value you should simply -remove it from your configuration file. The default value used to be: - -.. code:: yaml - - room_invite_state_types: - - "m.room.join_rules" - - "m.room.canonical_alias" - - "m.room.avatar" - - "m.room.encryption" - - "m.room.name" - -If you have customised this value, you should remove ``room_invite_state_types`` and -configure ``room_prejoin_state`` instead. - - - -Upgrading to v1.33.0 -==================== - -Account Validity HTML templates can now display a user's expiration date ------------------------------------------------------------------------- - -This may affect you if you have enabled the account validity feature, and have made use of a -custom HTML template specified by the ``account_validity.template_dir`` or ``account_validity.account_renewed_html_path`` -Synapse config options. - -The template can now accept an ``expiration_ts`` variable, which represents the unix timestamp in milliseconds for the -future date of which their account has been renewed until. See the -`default template `_ -for an example of usage. - -ALso note that a new HTML template, ``account_previously_renewed.html``, has been added. This is is shown to users -when they attempt to renew their account with a valid renewal token that has already been used before. The default -template contents can been found -`here `_, -and can also accept an ``expiration_ts`` variable. This template replaces the error message users would previously see -upon attempting to use a valid renewal token more than once. - - -Upgrading to v1.32.0 -==================== - -Regression causing connected Prometheus instances to become overwhelmed ------------------------------------------------------------------------ - -This release introduces `a regression `_ -that can overwhelm connected Prometheus instances. This issue is not present in -Synapse v1.32.0rc1. - -If you have been affected, please downgrade to 1.31.0. You then may need to -remove excess writeahead logs in order for Prometheus to recover. Instructions -for doing so are provided -`here `_. - -Dropping support for old Python, Postgres and SQLite versions -------------------------------------------------------------- - -In line with our `deprecation policy `_, -we've dropped support for Python 3.5 and PostgreSQL 9.5, as they are no longer supported upstream. - -This release of Synapse requires Python 3.6+ and PostgresSQL 9.6+ or SQLite 3.22+. - -Removal of old List Accounts Admin API --------------------------------------- - -The deprecated v1 "list accounts" admin API (``GET /_synapse/admin/v1/users/``) has been removed in this version. - -The `v2 list accounts API `_ -has been available since Synapse 1.7.0 (2019-12-13), and is accessible under ``GET /_synapse/admin/v2/users``. - -The deprecation of the old endpoint was announced with Synapse 1.28.0 (released on 2021-02-25). - -Application Services must use type ``m.login.application_service`` when registering users ------------------------------------------------------------------------------------------ - -In compliance with the -`Application Service spec `_, -Application Services are now required to use the ``m.login.application_service`` type when registering users via the -``/_matrix/client/r0/register`` endpoint. This behaviour was deprecated in Synapse v1.30.0. - -Please ensure your Application Services are up to date. - -Upgrading to v1.29.0 -==================== - -Requirement for X-Forwarded-Proto header ----------------------------------------- - -When using Synapse with a reverse proxy (in particular, when using the -`x_forwarded` option on an HTTP listener), Synapse now expects to receive an -`X-Forwarded-Proto` header on incoming HTTP requests. If it is not set, Synapse -will log a warning on each received request. - -To avoid the warning, administrators using a reverse proxy should ensure that -the reverse proxy sets `X-Forwarded-Proto` header to `https` or `http` to -indicate the protocol used by the client. - -Synapse also requires the `Host` header to be preserved. - -See the `reverse proxy documentation `_, where the -example configurations have been updated to show how to set these headers. - -(Users of `Caddy `_ are unaffected, since we believe it -sets `X-Forwarded-Proto` by default.) - -Upgrading to v1.27.0 -==================== - -Changes to callback URI for OAuth2 / OpenID Connect and SAML2 -------------------------------------------------------------- - -This version changes the URI used for callbacks from OAuth2 and SAML2 identity providers: - -* If your server is configured for single sign-on via an OpenID Connect or OAuth2 identity - provider, you will need to add ``[synapse public baseurl]/_synapse/client/oidc/callback`` - to the list of permitted "redirect URIs" at the identity provider. - - See `docs/openid.md `_ for more information on setting up OpenID - Connect. - -* If your server is configured for single sign-on via a SAML2 identity provider, you will - need to add ``[synapse public baseurl]/_synapse/client/saml2/authn_response`` as a permitted - "ACS location" (also known as "allowed callback URLs") at the identity provider. - - The "Issuer" in the "AuthnRequest" to the SAML2 identity provider is also updated to - ``[synapse public baseurl]/_synapse/client/saml2/metadata.xml``. If your SAML2 identity - provider uses this property to validate or otherwise identify Synapse, its configuration - will need to be updated to use the new URL. Alternatively you could create a new, separate - "EntityDescriptor" in your SAML2 identity provider with the new URLs and leave the URLs in - the existing "EntityDescriptor" as they were. - -Changes to HTML templates -------------------------- - -The HTML templates for SSO and email notifications now have `Jinja2's autoescape `_ -enabled for files ending in ``.html``, ``.htm``, and ``.xml``. If you have customised -these templates and see issues when viewing them you might need to update them. -It is expected that most configurations will need no changes. - -If you have customised the templates *names* for these templates, it is recommended -to verify they end in ``.html`` to ensure autoescape is enabled. - -The above applies to the following templates: - -* ``add_threepid.html`` -* ``add_threepid_failure.html`` -* ``add_threepid_success.html`` -* ``notice_expiry.html`` -* ``notice_expiry.html`` -* ``notif_mail.html`` (which, by default, includes ``room.html`` and ``notif.html``) -* ``password_reset.html`` -* ``password_reset_confirmation.html`` -* ``password_reset_failure.html`` -* ``password_reset_success.html`` -* ``registration.html`` -* ``registration_failure.html`` -* ``registration_success.html`` -* ``sso_account_deactivated.html`` -* ``sso_auth_bad_user.html`` -* ``sso_auth_confirm.html`` -* ``sso_auth_success.html`` -* ``sso_error.html`` -* ``sso_login_idp_picker.html`` -* ``sso_redirect_confirm.html`` - -Upgrading to v1.26.0 -==================== - -Rolling back to v1.25.0 after a failed upgrade ----------------------------------------------- - -v1.26.0 includes a lot of large changes. If something problematic occurs, you -may want to roll-back to a previous version of Synapse. Because v1.26.0 also -includes a new database schema version, reverting that version is also required -alongside the generic rollback instructions mentioned above. In short, to roll -back to v1.25.0 you need to: - -1. Stop the server -2. Decrease the schema version in the database: - - .. code:: sql - - UPDATE schema_version SET version = 58; - -3. Delete the ignored users & chain cover data: - - .. code:: sql - - DROP TABLE IF EXISTS ignored_users; - UPDATE rooms SET has_auth_chain_index = false; - - For PostgreSQL run: - - .. code:: sql - - TRUNCATE event_auth_chain_links; - TRUNCATE event_auth_chains; - - For SQLite run: - - .. code:: sql - - DELETE FROM event_auth_chain_links; - DELETE FROM event_auth_chains; - -4. Mark the deltas as not run (so they will re-run on upgrade). - - .. code:: sql - - DELETE FROM applied_schema_deltas WHERE version = 59 AND file = "59/01ignored_user.py"; - DELETE FROM applied_schema_deltas WHERE version = 59 AND file = "59/06chain_cover_index.sql"; - -5. Downgrade Synapse by following the instructions for your installation method - in the "Rolling back to older versions" section above. - -Upgrading to v1.25.0 -==================== - -Last release supporting Python 3.5 ----------------------------------- - -This is the last release of Synapse which guarantees support with Python 3.5, -which passed its upstream End of Life date several months ago. - -We will attempt to maintain support through March 2021, but without guarantees. - -In the future, Synapse will follow upstream schedules for ending support of -older versions of Python and PostgreSQL. Please upgrade to at least Python 3.6 -and PostgreSQL 9.6 as soon as possible. - -Blacklisting IP ranges ----------------------- - -Synapse v1.25.0 includes new settings, ``ip_range_blacklist`` and -``ip_range_whitelist``, for controlling outgoing requests from Synapse for federation, -identity servers, push, and for checking key validity for third-party invite events. -The previous setting, ``federation_ip_range_blacklist``, is deprecated. The new -``ip_range_blacklist`` defaults to private IP ranges if it is not defined. - -If you have never customised ``federation_ip_range_blacklist`` it is recommended -that you remove that setting. - -If you have customised ``federation_ip_range_blacklist`` you should update the -setting name to ``ip_range_blacklist``. - -If you have a custom push server that is reached via private IP space you may -need to customise ``ip_range_blacklist`` or ``ip_range_whitelist``. - -Upgrading to v1.24.0 -==================== - -Custom OpenID Connect mapping provider breaking change ------------------------------------------------------- - -This release allows the OpenID Connect mapping provider to perform normalisation -of the localpart of the Matrix ID. This allows for the mapping provider to -specify different algorithms, instead of the [default way](https://matrix.org/docs/spec/appendices#mapping-from-other-character-sets). - -If your Synapse configuration uses a custom mapping provider -(`oidc_config.user_mapping_provider.module` is specified and not equal to -`synapse.handlers.oidc_handler.JinjaOidcMappingProvider`) then you *must* ensure -that `map_user_attributes` of the mapping provider performs some normalisation -of the `localpart` returned. To match previous behaviour you can use the -`map_username_to_mxid_localpart` function provided by Synapse. An example is -shown below: - -.. code-block:: python - - from synapse.types import map_username_to_mxid_localpart - - class MyMappingProvider: - def map_user_attributes(self, userinfo, token): - # ... your custom logic ... - sso_user_id = ... - localpart = map_username_to_mxid_localpart(sso_user_id) - - return {"localpart": localpart} - -Removal historical Synapse Admin API ------------------------------------- - -Historically, the Synapse Admin API has been accessible under: - -* ``/_matrix/client/api/v1/admin`` -* ``/_matrix/client/unstable/admin`` -* ``/_matrix/client/r0/admin`` -* ``/_synapse/admin/v1`` - -The endpoints with ``/_matrix/client/*`` prefixes have been removed as of v1.24.0. -The Admin API is now only accessible under: - -* ``/_synapse/admin/v1`` - -The only exception is the `/admin/whois` endpoint, which is -`also available via the client-server API `_. - -The deprecation of the old endpoints was announced with Synapse 1.20.0 (released -on 2020-09-22) and makes it easier for homeserver admins to lock down external -access to the Admin API endpoints. - -Upgrading to v1.23.0 -==================== - -Structured logging configuration breaking changes -------------------------------------------------- - -This release deprecates use of the ``structured: true`` logging configuration for -structured logging. If your logging configuration contains ``structured: true`` -then it should be modified based on the `structured logging documentation -`_. - -The ``structured`` and ``drains`` logging options are now deprecated and should -be replaced by standard logging configuration of ``handlers`` and ``formatters``. - -A future will release of Synapse will make using ``structured: true`` an error. - -Upgrading to v1.22.0 -==================== - -ThirdPartyEventRules breaking changes -------------------------------------- - -This release introduces a backwards-incompatible change to modules making use of -``ThirdPartyEventRules`` in Synapse. If you make use of a module defined under the -``third_party_event_rules`` config option, please make sure it is updated to handle -the below change: - -The ``http_client`` argument is no longer passed to modules as they are initialised. Instead, -modules are expected to make use of the ``http_client`` property on the ``ModuleApi`` class. -Modules are now passed a ``module_api`` argument during initialisation, which is an instance of -``ModuleApi``. ``ModuleApi`` instances have a ``http_client`` property which acts the same as -the ``http_client`` argument previously passed to ``ThirdPartyEventRules`` modules. - -Upgrading to v1.21.0 -==================== - -Forwarding ``/_synapse/client`` through your reverse proxy ----------------------------------------------------------- - -The `reverse proxy documentation -`_ has been updated -to include reverse proxy directives for ``/_synapse/client/*`` endpoints. As the user password -reset flow now uses endpoints under this prefix, **you must update your reverse proxy -configurations for user password reset to work**. - -Additionally, note that the `Synapse worker documentation -`_ has been updated to - state that the ``/_synapse/client/password_reset/email/submit_token`` endpoint can be handled -by all workers. If you make use of Synapse's worker feature, please update your reverse proxy -configuration to reflect this change. - -New HTML templates ------------------- - -A new HTML template, -`password_reset_confirmation.html `_, -has been added to the ``synapse/res/templates`` directory. If you are using a -custom template directory, you may want to copy the template over and modify it. - -Note that as of v1.20.0, templates do not need to be included in custom template -directories for Synapse to start. The default templates will be used if a custom -template cannot be found. - -This page will appear to the user after clicking a password reset link that has -been emailed to them. - -To complete password reset, the page must include a way to make a `POST` -request to -``/_synapse/client/password_reset/{medium}/submit_token`` -with the query parameters from the original link, presented as a URL-encoded form. See the file -itself for more details. - -Updated Single Sign-on HTML Templates -------------------------------------- - -The ``saml_error.html`` template was removed from Synapse and replaced with the -``sso_error.html`` template. If your Synapse is configured to use SAML and a -custom ``sso_redirect_confirm_template_dir`` configuration then any customisations -of the ``saml_error.html`` template will need to be merged into the ``sso_error.html`` -template. These templates are similar, but the parameters are slightly different: - -* The ``msg`` parameter should be renamed to ``error_description``. -* There is no longer a ``code`` parameter for the response code. -* A string ``error`` parameter is available that includes a short hint of why a - user is seeing the error page. - -Upgrading to v1.18.0 -==================== - -Docker `-py3` suffix will be removed in future versions -------------------------------------------------------- - -From 10th August 2020, we will no longer publish Docker images with the `-py3` tag suffix. The images tagged with the `-py3` suffix have been identical to the non-suffixed tags since release 0.99.0, and the suffix is obsolete. - -On 10th August, we will remove the `latest-py3` tag. Existing per-release tags (such as `v1.18.0-py3`) will not be removed, but no new `-py3` tags will be added. - -Scripts relying on the `-py3` suffix will need to be updated. - -Redis replication is now recommended in lieu of TCP replication ---------------------------------------------------------------- - -When setting up worker processes, we now recommend the use of a Redis server for replication. **The old direct TCP connection method is deprecated and will be removed in a future release.** -See `docs/workers.md `_ for more details. - -Upgrading to v1.14.0 -==================== - -This version includes a database update which is run as part of the upgrade, -and which may take a couple of minutes in the case of a large server. Synapse -will not respond to HTTP requests while this update is taking place. - -Upgrading to v1.13.0 -==================== - -Incorrect database migration in old synapse versions ----------------------------------------------------- - -A bug was introduced in Synapse 1.4.0 which could cause the room directory to -be incomplete or empty if Synapse was upgraded directly from v1.2.1 or -earlier, to versions between v1.4.0 and v1.12.x. - -This will *not* be a problem for Synapse installations which were: - * created at v1.4.0 or later, - * upgraded via v1.3.x, or - * upgraded straight from v1.2.1 or earlier to v1.13.0 or later. - -If completeness of the room directory is a concern, installations which are -affected can be repaired as follows: - -1. Run the following sql from a `psql` or `sqlite3` console: - - .. code:: sql - - INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES - ('populate_stats_process_rooms', '{}', 'current_state_events_membership'); - - INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES - ('populate_stats_process_users', '{}', 'populate_stats_process_rooms'); - -2. Restart synapse. - -New Single Sign-on HTML Templates ---------------------------------- - -New templates (``sso_auth_confirm.html``, ``sso_auth_success.html``, and -``sso_account_deactivated.html``) were added to Synapse. If your Synapse is -configured to use SSO and a custom ``sso_redirect_confirm_template_dir`` -configuration then these templates will need to be copied from -`synapse/res/templates `_ into that directory. - -Synapse SSO Plugins Method Deprecation --------------------------------------- - -Plugins using the ``complete_sso_login`` method of -``synapse.module_api.ModuleApi`` should update to using the async/await -version ``complete_sso_login_async`` which includes additional checks. The -non-async version is considered deprecated. - -Rolling back to v1.12.4 after a failed upgrade ----------------------------------------------- - -v1.13.0 includes a lot of large changes. If something problematic occurs, you -may want to roll-back to a previous version of Synapse. Because v1.13.0 also -includes a new database schema version, reverting that version is also required -alongside the generic rollback instructions mentioned above. In short, to roll -back to v1.12.4 you need to: - -1. Stop the server -2. Decrease the schema version in the database: - - .. code:: sql - - UPDATE schema_version SET version = 57; - -3. Downgrade Synapse by following the instructions for your installation method - in the "Rolling back to older versions" section above. - - -Upgrading to v1.12.0 -==================== - -This version includes a database update which is run as part of the upgrade, -and which may take some time (several hours in the case of a large -server). Synapse will not respond to HTTP requests while this update is taking -place. - -This is only likely to be a problem in the case of a server which is -participating in many rooms. - -0. As with all upgrades, it is recommended that you have a recent backup of - your database which can be used for recovery in the event of any problems. - -1. As an initial check to see if you will be affected, you can try running the - following query from the `psql` or `sqlite3` console. It is safe to run it - while Synapse is still running. - - .. code:: sql - - SELECT MAX(q.v) FROM ( - SELECT ( - SELECT ej.json AS v - FROM state_events se INNER JOIN event_json ej USING (event_id) - WHERE se.room_id=rooms.room_id AND se.type='m.room.create' AND se.state_key='' - LIMIT 1 - ) FROM rooms WHERE rooms.room_version IS NULL - ) q; - - This query will take about the same amount of time as the upgrade process: ie, - if it takes 5 minutes, then it is likely that Synapse will be unresponsive for - 5 minutes during the upgrade. - - If you consider an outage of this duration to be acceptable, no further - action is necessary and you can simply start Synapse 1.12.0. - - If you would prefer to reduce the downtime, continue with the steps below. - -2. The easiest workaround for this issue is to manually - create a new index before upgrading. On PostgreSQL, his can be done as follows: - - .. code:: sql - - CREATE INDEX CONCURRENTLY tmp_upgrade_1_12_0_index - ON state_events(room_id) WHERE type = 'm.room.create'; - - The above query may take some time, but is also safe to run while Synapse is - running. - - We assume that no SQLite users have databases large enough to be - affected. If you *are* affected, you can run a similar query, omitting the - ``CONCURRENTLY`` keyword. Note however that this operation may in itself cause - Synapse to stop running for some time. Synapse admins are reminded that - `SQLite is not recommended for use outside a test - environment `_. - -3. Once the index has been created, the ``SELECT`` query in step 1 above should - complete quickly. It is therefore safe to upgrade to Synapse 1.12.0. - -4. Once Synapse 1.12.0 has successfully started and is responding to HTTP - requests, the temporary index can be removed: - - .. code:: sql - - DROP INDEX tmp_upgrade_1_12_0_index; - -Upgrading to v1.10.0 -==================== - -Synapse will now log a warning on start up if used with a PostgreSQL database -that has a non-recommended locale set. - -See `docs/postgres.md `_ for details. - - -Upgrading to v1.8.0 -=================== - -Specifying a ``log_file`` config option will now cause Synapse to refuse to -start, and should be replaced by with the ``log_config`` option. Support for -the ``log_file`` option was removed in v1.3.0 and has since had no effect. - - -Upgrading to v1.7.0 -=================== - -In an attempt to configure Synapse in a privacy preserving way, the default -behaviours of ``allow_public_rooms_without_auth`` and -``allow_public_rooms_over_federation`` have been inverted. This means that by -default, only authenticated users querying the Client/Server API will be able -to query the room directory, and relatedly that the server will not share -room directory information with other servers over federation. - -If your installation does not explicitly set these settings one way or the other -and you want either setting to be ``true`` then it will necessary to update -your homeserver configuration file accordingly. - -For more details on the surrounding context see our `explainer -`_. - - -Upgrading to v1.5.0 -=================== - -This release includes a database migration which may take several minutes to -complete if there are a large number (more than a million or so) of entries in -the ``devices`` table. This is only likely to a be a problem on very large -installations. - - -Upgrading to v1.4.0 -=================== - -New custom templates --------------------- - -If you have configured a custom template directory with the -``email.template_dir`` option, be aware that there are new templates regarding -registration and threepid management (see below) that must be included. - -* ``registration.html`` and ``registration.txt`` -* ``registration_success.html`` and ``registration_failure.html`` -* ``add_threepid.html`` and ``add_threepid.txt`` -* ``add_threepid_failure.html`` and ``add_threepid_success.html`` - -Synapse will expect these files to exist inside the configured template -directory, and **will fail to start** if they are absent. -To view the default templates, see `synapse/res/templates -`_. - -3pid verification changes -------------------------- - -**Note: As of this release, users will be unable to add phone numbers or email -addresses to their accounts, without changes to the Synapse configuration. This -includes adding an email address during registration.** - -It is possible for a user to associate an email address or phone number -with their account, for a number of reasons: - -* for use when logging in, as an alternative to the user id. -* in the case of email, as an alternative contact to help with account recovery. -* in the case of email, to receive notifications of missed messages. - -Before an email address or phone number can be added to a user's account, -or before such an address is used to carry out a password-reset, Synapse must -confirm the operation with the owner of the email address or phone number. -It does this by sending an email or text giving the user a link or token to confirm -receipt. This process is known as '3pid verification'. ('3pid', or 'threepid', -stands for third-party identifier, and we use it to refer to external -identifiers such as email addresses and phone numbers.) - -Previous versions of Synapse delegated the task of 3pid verification to an -identity server by default. In most cases this server is ``vector.im`` or -``matrix.org``. - -In Synapse 1.4.0, for security and privacy reasons, the homeserver will no -longer delegate this task to an identity server by default. Instead, -the server administrator will need to explicitly decide how they would like the -verification messages to be sent. - -In the medium term, the ``vector.im`` and ``matrix.org`` identity servers will -disable support for delegated 3pid verification entirely. However, in order to -ease the transition, they will retain the capability for a limited -period. Delegated email verification will be disabled on Monday 2nd December -2019 (giving roughly 2 months notice). Disabling delegated SMS verification -will follow some time after that once SMS verification support lands in -Synapse. - -Once delegated 3pid verification support has been disabled in the ``vector.im`` and -``matrix.org`` identity servers, all Synapse versions that depend on those -instances will be unable to verify email and phone numbers through them. There -are no imminent plans to remove delegated 3pid verification from Sydent -generally. (Sydent is the identity server project that backs the ``vector.im`` and -``matrix.org`` instances). - -Email -~~~~~ -Following upgrade, to continue verifying email (e.g. as part of the -registration process), admins can either:- - -* Configure Synapse to use an email server. -* Run or choose an identity server which allows delegated email verification - and delegate to it. - -Configure SMTP in Synapse -+++++++++++++++++++++++++ - -To configure an SMTP server for Synapse, modify the configuration section -headed ``email``, and be sure to have at least the ``smtp_host, smtp_port`` -and ``notif_from`` fields filled out. - -You may also need to set ``smtp_user``, ``smtp_pass``, and -``require_transport_security``. - -See the `sample configuration file `_ for more details -on these settings. - -Delegate email to an identity server -++++++++++++++++++++++++++++++++++++ - -Some admins will wish to continue using email verification as part of the -registration process, but will not immediately have an appropriate SMTP server -at hand. - -To this end, we will continue to support email verification delegation via the -``vector.im`` and ``matrix.org`` identity servers for two months. Support for -delegated email verification will be disabled on Monday 2nd December. - -The ``account_threepid_delegates`` dictionary defines whether the homeserver -should delegate an external server (typically an `identity server -`_) to handle sending -confirmation messages via email and SMS. - -So to delegate email verification, in ``homeserver.yaml``, set -``account_threepid_delegates.email`` to the base URL of an identity server. For -example: - -.. code:: yaml - - account_threepid_delegates: - email: https://example.com # Delegate email sending to example.com - -Note that ``account_threepid_delegates.email`` replaces the deprecated -``email.trust_identity_server_for_password_resets``: if -``email.trust_identity_server_for_password_resets`` is set to ``true``, and -``account_threepid_delegates.email`` is not set, then the first entry in -``trusted_third_party_id_servers`` will be used as the -``account_threepid_delegate`` for email. This is to ensure compatibility with -existing Synapse installs that set up external server handling for these tasks -before v1.4.0. If ``email.trust_identity_server_for_password_resets`` is -``true`` and no trusted identity server domains are configured, Synapse will -report an error and refuse to start. - -If ``email.trust_identity_server_for_password_resets`` is ``false`` or absent -and no ``email`` delegate is configured in ``account_threepid_delegates``, -then Synapse will send email verification messages itself, using the configured -SMTP server (see above). -that type. - -Phone numbers -~~~~~~~~~~~~~ - -Synapse does not support phone-number verification itself, so the only way to -maintain the ability for users to add phone numbers to their accounts will be -by continuing to delegate phone number verification to the ``matrix.org`` and -``vector.im`` identity servers (or another identity server that supports SMS -sending). - -The ``account_threepid_delegates`` dictionary defines whether the homeserver -should delegate an external server (typically an `identity server -`_) to handle sending -confirmation messages via email and SMS. - -So to delegate phone number verification, in ``homeserver.yaml``, set -``account_threepid_delegates.msisdn`` to the base URL of an identity -server. For example: - -.. code:: yaml - - account_threepid_delegates: - msisdn: https://example.com # Delegate sms sending to example.com - -The ``matrix.org`` and ``vector.im`` identity servers will continue to support -delegated phone number verification via SMS until such time as it is possible -for admins to configure their servers to perform phone number verification -directly. More details will follow in a future release. - -Rolling back to v1.3.1 ----------------------- - -If you encounter problems with v1.4.0, it should be possible to roll back to -v1.3.1, subject to the following: - -* The 'room statistics' engine was heavily reworked in this release (see - `#5971 `_), including - significant changes to the database schema, which are not easily - reverted. This will cause the room statistics engine to stop updating when - you downgrade. - - The room statistics are essentially unused in v1.3.1 (in future versions of - Synapse, they will be used to populate the room directory), so there should - be no loss of functionality. However, the statistics engine will write errors - to the logs, which can be avoided by setting the following in - `homeserver.yaml`: - - .. code:: yaml - - stats: - enabled: false - - Don't forget to re-enable it when you upgrade again, in preparation for its - use in the room directory! - -Upgrading to v1.2.0 -=================== - -Some counter metrics have been renamed, with the old names deprecated. See -`the metrics documentation `_ -for details. - -Upgrading to v1.1.0 -=================== - -Synapse v1.1.0 removes support for older Python and PostgreSQL versions, as -outlined in `our deprecation notice `_. - -Minimum Python Version ----------------------- - -Synapse v1.1.0 has a minimum Python requirement of Python 3.5. Python 3.6 or -Python 3.7 are recommended as they have improved internal string handling, -significantly reducing memory usage. - -If you use current versions of the Matrix.org-distributed Debian packages or -Docker images, action is not required. - -If you install Synapse in a Python virtual environment, please see "Upgrading to -v0.34.0" for notes on setting up a new virtualenv under Python 3. - -Minimum PostgreSQL Version --------------------------- - -If using PostgreSQL under Synapse, you will need to use PostgreSQL 9.5 or above. -Please see the -`PostgreSQL documentation `_ -for more details on upgrading your database. - -Upgrading to v1.0 -================= - -Validation of TLS certificates ------------------------------- - -Synapse v1.0 is the first release to enforce -validation of TLS certificates for the federation API. It is therefore -essential that your certificates are correctly configured. See the `FAQ -`_ for more information. - -Note, v1.0 installations will also no longer be able to federate with servers -that have not correctly configured their certificates. - -In rare cases, it may be desirable to disable certificate checking: for -example, it might be essential to be able to federate with a given legacy -server in a closed federation. This can be done in one of two ways:- - -* Configure the global switch ``federation_verify_certificates`` to ``false``. -* Configure a whitelist of server domains to trust via ``federation_certificate_verification_whitelist``. - -See the `sample configuration file `_ -for more details on these settings. - -Email ------ -When a user requests a password reset, Synapse will send an email to the -user to confirm the request. - -Previous versions of Synapse delegated the job of sending this email to an -identity server. If the identity server was somehow malicious or became -compromised, it would be theoretically possible to hijack an account through -this means. - -Therefore, by default, Synapse v1.0 will send the confirmation email itself. If -Synapse is not configured with an SMTP server, password reset via email will be -disabled. - -To configure an SMTP server for Synapse, modify the configuration section -headed ``email``, and be sure to have at least the ``smtp_host``, ``smtp_port`` -and ``notif_from`` fields filled out. You may also need to set ``smtp_user``, -``smtp_pass``, and ``require_transport_security``. - -If you are absolutely certain that you wish to continue using an identity -server for password resets, set ``trust_identity_server_for_password_resets`` to ``true``. - -See the `sample configuration file `_ -for more details on these settings. - -New email templates ---------------- -Some new templates have been added to the default template directory for the purpose of the -homeserver sending its own password reset emails. If you have configured a custom -``template_dir`` in your Synapse config, these files will need to be added. - -``password_reset.html`` and ``password_reset.txt`` are HTML and plain text templates -respectively that contain the contents of what will be emailed to the user upon attempting to -reset their password via email. ``password_reset_success.html`` and -``password_reset_failure.html`` are HTML files that the content of which (assuming no redirect -URL is set) will be shown to the user after they attempt to click the link in the email sent -to them. - -Upgrading to v0.99.0 -==================== - -Please be aware that, before Synapse v1.0 is released around March 2019, you -will need to replace any self-signed certificates with those verified by a -root CA. Information on how to do so can be found at `the ACME docs -`_. - -For more information on configuring TLS certificates see the `FAQ `_. - -Upgrading to v0.34.0 -==================== - -1. This release is the first to fully support Python 3. Synapse will now run on - Python versions 3.5, or 3.6 (as well as 2.7). We recommend switching to - Python 3, as it has been shown to give performance improvements. - - For users who have installed Synapse into a virtualenv, we recommend doing - this by creating a new virtualenv. For example:: - - virtualenv -p python3 ~/synapse/env3 - source ~/synapse/env3/bin/activate - pip install matrix-synapse - - You can then start synapse as normal, having activated the new virtualenv:: - - cd ~/synapse - source env3/bin/activate - synctl start - - Users who have installed from distribution packages should see the relevant - package documentation. See below for notes on Debian packages. - - * When upgrading to Python 3, you **must** make sure that your log files are - configured as UTF-8, by adding ``encoding: utf8`` to the - ``RotatingFileHandler`` configuration (if you have one) in your - ``.log.config`` file. For example, if your ``log.config`` file - contains:: - - handlers: - file: - class: logging.handlers.RotatingFileHandler - formatter: precise - filename: homeserver.log - maxBytes: 104857600 - backupCount: 10 - filters: [context] - console: - class: logging.StreamHandler - formatter: precise - filters: [context] - - Then you should update this to be:: - - handlers: - file: - class: logging.handlers.RotatingFileHandler - formatter: precise - filename: homeserver.log - maxBytes: 104857600 - backupCount: 10 - filters: [context] - encoding: utf8 - console: - class: logging.StreamHandler - formatter: precise - filters: [context] - - There is no need to revert this change if downgrading to Python 2. - - We are also making available Debian packages which will run Synapse on - Python 3. You can switch to these packages with ``apt-get install - matrix-synapse-py3``, however, please read `debian/NEWS - `_ - before doing so. The existing ``matrix-synapse`` packages will continue to - use Python 2 for the time being. - -2. This release removes the ``riot.im`` from the default list of trusted - identity servers. - - If ``riot.im`` is in your homeserver's list of - ``trusted_third_party_id_servers``, you should remove it. It was added in - case a hypothetical future identity server was put there. If you don't - remove it, users may be unable to deactivate their accounts. - -3. This release no longer installs the (unmaintained) Matrix Console web client - as part of the default installation. It is possible to re-enable it by - installing it separately and setting the ``web_client_location`` config - option, but please consider switching to another client. - -Upgrading to v0.33.7 -==================== - -This release removes the example email notification templates from -``res/templates`` (they are now internal to the python package). This should -only affect you if you (a) deploy your Synapse instance from a git checkout or -a github snapshot URL, and (b) have email notifications enabled. - -If you have email notifications enabled, you should ensure that -``email.template_dir`` is either configured to point at a directory where you -have installed customised templates, or leave it unset to use the default -templates. - -Upgrading to v0.27.3 -==================== - -This release expands the anonymous usage stats sent if the opt-in -``report_stats`` configuration is set to ``true``. We now capture RSS memory -and cpu use at a very coarse level. This requires administrators to install -the optional ``psutil`` python module. - -We would appreciate it if you could assist by ensuring this module is available -and ``report_stats`` is enabled. This will let us see if performance changes to -synapse are having an impact to the general community. - -Upgrading to v0.15.0 -==================== - -If you want to use the new URL previewing API (/_matrix/media/r0/preview_url) -then you have to explicitly enable it in the config and update your dependencies -dependencies. See README.rst for details. - - -Upgrading to v0.11.0 -==================== - -This release includes the option to send anonymous usage stats to matrix.org, -and requires that administrators explictly opt in or out by setting the -``report_stats`` option to either ``true`` or ``false``. - -We would really appreciate it if you could help our project out by reporting -anonymized usage statistics from your homeserver. Only very basic aggregate -data (e.g. number of users) will be reported, but it helps us to track the -growth of the Matrix community, and helps us to make Matrix a success, as well -as to convince other networks that they should peer with us. - - -Upgrading to v0.9.0 -=================== - -Application services have had a breaking API change in this version. - -They can no longer register themselves with a home server using the AS HTTP API. This -decision was made because a compromised application service with free reign to register -any regex in effect grants full read/write access to the home server if a regex of ``.*`` -is used. An attack where a compromised AS re-registers itself with ``.*`` was deemed too -big of a security risk to ignore, and so the ability to register with the HS remotely has -been removed. - -It has been replaced by specifying a list of application service registrations in -``homeserver.yaml``:: - - app_service_config_files: ["registration-01.yaml", "registration-02.yaml"] - -Where ``registration-01.yaml`` looks like:: - - url: # e.g. "https://my.application.service.com" - as_token: - hs_token: - sender_localpart: # This is a new field which denotes the user_id localpart when using the AS token - namespaces: - users: - - exclusive: - regex: # e.g. "@prefix_.*" - aliases: - - exclusive: - regex: - rooms: - - exclusive: - regex: - -Upgrading to v0.8.0 -=================== - -Servers which use captchas will need to add their public key to:: - - static/client/register/register_config.js - - window.matrixRegistrationConfig = { - recaptcha_public_key: "YOUR_PUBLIC_KEY" - }; - -This is required in order to support registration fallback (typically used on -mobile devices). - - -Upgrading to v0.7.0 -=================== - -New dependencies are: - -- pydenticon -- simplejson -- syutil -- matrix-angular-sdk - -To pull in these dependencies in a virtual env, run:: - - python synapse/python_dependencies.py | xargs -n 1 pip install - -Upgrading to v0.6.0 -=================== - -To pull in new dependencies, run:: - - python setup.py develop --user - -This update includes a change to the database schema. To upgrade you first need -to upgrade the database by running:: - - python scripts/upgrade_db_to_v0.6.0.py - -Where `` is the location of the database, `` is the -server name as specified in the synapse configuration, and `` is -the location of the signing key as specified in the synapse configuration. - -This may take some time to complete. Failures of signatures and content hashes -can safely be ignored. - - -Upgrading to v0.5.1 -=================== - -Depending on precisely when you installed v0.5.0 you may have ended up with -a stale release of the reference matrix webclient installed as a python module. -To uninstall it and ensure you are depending on the latest module, please run:: - - $ pip uninstall syweb - -Upgrading to v0.5.0 -=================== - -The webclient has been split out into a seperate repository/pacakage in this -release. Before you restart your homeserver you will need to pull in the -webclient package by running:: - - python setup.py develop --user - -This release completely changes the database schema and so requires upgrading -it before starting the new version of the homeserver. - -The script "database-prepare-for-0.5.0.sh" should be used to upgrade the -database. This will save all user information, such as logins and profiles, -but will otherwise purge the database. This includes messages, which -rooms the home server was a member of and room alias mappings. - -If you would like to keep your history, please take a copy of your database -file and ask for help in #matrix:matrix.org. The upgrade process is, -unfortunately, non trivial and requires human intervention to resolve any -resulting conflicts during the upgrade process. - -Before running the command the homeserver should be first completely -shutdown. To run it, simply specify the location of the database, e.g.: - - ./scripts/database-prepare-for-0.5.0.sh "homeserver.db" - -Once this has successfully completed it will be safe to restart the -homeserver. You may notice that the homeserver takes a few seconds longer to -restart than usual as it reinitializes the database. - -On startup of the new version, users can either rejoin remote rooms using room -aliases or by being reinvited. Alternatively, if any other homeserver sends a -message to a room that the homeserver was previously in the local HS will -automatically rejoin the room. - -Upgrading to v0.4.0 -=================== - -This release needs an updated syutil version. Run:: - - python setup.py develop - -You will also need to upgrade your configuration as the signing key format has -changed. Run:: - - python -m synapse.app.homeserver --config-path --generate-config - - -Upgrading to v0.3.0 -=================== - -This registration API now closely matches the login API. This introduces a bit -more backwards and forwards between the HS and the client, but this improves -the overall flexibility of the API. You can now GET on /register to retrieve a list -of valid registration flows. Upon choosing one, they are submitted in the same -way as login, e.g:: - - { - type: m.login.password, - user: foo, - password: bar - } - -The default HS supports 2 flows, with and without Identity Server email -authentication. Enabling captcha on the HS will add in an extra step to all -flows: ``m.login.recaptcha`` which must be completed before you can transition -to the next stage. There is a new login type: ``m.login.email.identity`` which -contains the ``threepidCreds`` key which were previously sent in the original -register request. For more information on this, see the specification. - -Web Client ----------- - -The VoIP specification has changed between v0.2.0 and v0.3.0. Users should -refresh any browser tabs to get the latest web client code. Users on -v0.2.0 of the web client will not be able to call those on v0.3.0 and -vice versa. - - -Upgrading to v0.2.0 -=================== - -The home server now requires setting up of SSL config before it can run. To -automatically generate default config use:: - - $ python synapse/app/homeserver.py \ - --server-name machine.my.domain.name \ - --bind-port 8448 \ - --config-path homeserver.config \ - --generate-config - -This config can be edited if desired, for example to specify a different SSL -certificate to use. Once done you can run the home server using:: - - $ python synapse/app/homeserver.py --config-path homeserver.config - -See the README.rst for more information. - -Also note that some config options have been renamed, including: - -- "host" to "server-name" -- "database" to "database-path" -- "port" to "bind-port" and "unsecure-port" - - -Upgrading to v0.0.1 -=================== - -This release completely changes the database schema and so requires upgrading -it before starting the new version of the homeserver. - -The script "database-prepare-for-0.0.1.sh" should be used to upgrade the -database. This will save all user information, such as logins and profiles, -but will otherwise purge the database. This includes messages, which -rooms the home server was a member of and room alias mappings. - -Before running the command the homeserver should be first completely -shutdown. To run it, simply specify the location of the database, e.g.: - - ./scripts/database-prepare-for-0.0.1.sh "homeserver.db" - -Once this has successfully completed it will be safe to restart the -homeserver. You may notice that the homeserver takes a few seconds longer to -restart than usual as it reinitializes the database. - -On startup of the new version, users can either rejoin remote rooms using room -aliases or by being reinvited. Alternatively, if any other homeserver sends a -message to a room that the homeserver was previously in the local HS will -automatically rejoin the room. +The markdown source is available in `docs/upgrade.md `_. diff --git a/changelog.d/10062.removal b/changelog.d/10062.removal index 7f0cbdae2e..617785df5f 100644 --- a/changelog.d/10062.removal +++ b/changelog.d/10062.removal @@ -1 +1 @@ -The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. \ No newline at end of file +The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/docs/upgrade.md#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. \ No newline at end of file diff --git a/changelog.d/10166.doc b/changelog.d/10166.doc new file mode 100644 index 0000000000..8d1710c132 --- /dev/null +++ b/changelog.d/10166.doc @@ -0,0 +1 @@ +Move the upgrade notes to [docs/upgrade.md](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md) and convert them to markdown. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 98969bdd2d..db4ef1a44e 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -11,7 +11,7 @@ - [Delegation](delegate.md) # Upgrading - - [Upgrading between Synapse Versions](upgrading/README.md) + - [Upgrading between Synapse Versions](upgrade.md) - [Upgrading from pre-Synapse 1.0](MSC1711_certificates_FAQ.md) # Usage diff --git a/docs/upgrade.md b/docs/upgrade.md new file mode 100644 index 0000000000..a44960c2b8 --- /dev/null +++ b/docs/upgrade.md @@ -0,0 +1,1353 @@ +# Upgrading Synapse + +Before upgrading check if any special steps are required to upgrade from +the version you currently have installed to the current version of +Synapse. The extra instructions that may be required are listed later in +this document. + +- Check that your versions of Python and PostgreSQL are still + supported. + + Synapse follows upstream lifecycles for [Python](https://endoflife.date/python) and + [PostgreSQL](https://endoflife.date/postgresql), and removes support for versions + which are no longer maintained. + + The website also offers convenient + summaries. + +- If Synapse was installed using [prebuilt + packages](../setup/INSTALL.md#prebuilt-packages), you will need to follow the + normal process for upgrading those packages. + +- If Synapse was installed from source, then: + + 1. Activate the virtualenv before upgrading. For example, if + Synapse is installed in a virtualenv in `~/synapse/env` then + run: + + ```bash + source ~/synapse/env/bin/activate + ``` + + 2. If Synapse was installed using pip then upgrade to the latest + version by running: + + ```bash + pip install --upgrade matrix-synapse + ``` + + If Synapse was installed using git then upgrade to the latest + version by running: + + ```bash + git pull + pip install --upgrade . + ``` + + 3. Restart Synapse: + + ```bash + ./synctl restart + ``` + +To check whether your update was successful, you can check the running +server version with: + +```bash +# you may need to replace 'localhost:8008' if synapse is not configured +# to listen on port 8008. + +curl http://localhost:8008/_synapse/admin/v1/server_version +``` + +## Rolling back to older versions + +Rolling back to previous releases can be difficult, due to database +schema changes between releases. Where we have been able to test the +rollback process, this will be noted below. + +In general, you will need to undo any changes made during the upgrade +process, for example: + +- pip: + + ```bash + source env/bin/activate + # replace `1.3.0` accordingly: + pip install matrix-synapse==1.3.0 + ``` + +- Debian: + + ```bash + # replace `1.3.0` and `stretch` accordingly: + wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb + dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb + ``` + +# Upgrading to v1.37.0 + +## Deprecation of the current spam checker interface + +The current spam checker interface is deprecated in favour of a new generic modules system. +Authors of spam checker modules can refer to [this +documentation](https://matrix-org.github.io/synapse/develop/modules.html#porting-an-existing-module-that-uses-the-old-interface) +to update their modules. Synapse administrators can refer to [this +documentation](https://matrix-org.github.io/synapse/develop/modules.html#using-modules) +to update their configuration once the modules they are using have been updated. + +We plan to remove support for the current spam checker interface in August 2021. + +More module interfaces will be ported over to this new generic system in future versions +of Synapse. + + +# Upgrading to v1.34.0 + +## `room_invite_state_types` configuration setting + +The `room_invite_state_types` configuration setting has been deprecated +and replaced with `room_prejoin_state`. See the [sample configuration +file](https://github.com/matrix-org/synapse/blob/v1.34.0/docs/sample_config.yaml#L1515). + +If you have set `room_invite_state_types` to the default value you +should simply remove it from your configuration file. The default value +used to be: + +```yaml +room_invite_state_types: + - "m.room.join_rules" + - "m.room.canonical_alias" + - "m.room.avatar" + - "m.room.encryption" + - "m.room.name" +``` + +If you have customised this value, you should remove +`room_invite_state_types` and configure `room_prejoin_state` instead. + +# Upgrading to v1.33.0 + +## Account Validity HTML templates can now display a user's expiration date + +This may affect you if you have enabled the account validity feature, +and have made use of a custom HTML template specified by the +`account_validity.template_dir` or +`account_validity.account_renewed_html_path` Synapse config options. + +The template can now accept an `expiration_ts` variable, which +represents the unix timestamp in milliseconds for the future date of +which their account has been renewed until. See the [default +template](https://github.com/matrix-org/synapse/blob/release-v1.33.0/synapse/res/templates/account_renewed.html) +for an example of usage. + +ALso note that a new HTML template, `account_previously_renewed.html`, +has been added. This is is shown to users when they attempt to renew +their account with a valid renewal token that has already been used +before. The default template contents can been found +[here](https://github.com/matrix-org/synapse/blob/release-v1.33.0/synapse/res/templates/account_previously_renewed.html), +and can also accept an `expiration_ts` variable. This template replaces +the error message users would previously see upon attempting to use a +valid renewal token more than once. + +# Upgrading to v1.32.0 + +## Regression causing connected Prometheus instances to become overwhelmed + +This release introduces [a +regression](https://github.com/matrix-org/synapse/issues/9853) that can +overwhelm connected Prometheus instances. This issue is not present in +Synapse v1.32.0rc1. + +If you have been affected, please downgrade to 1.31.0. You then may need +to remove excess writeahead logs in order for Prometheus to recover. +Instructions for doing so are provided +[here](https://github.com/matrix-org/synapse/pull/9854#issuecomment-823472183). + +## Dropping support for old Python, Postgres and SQLite versions + +In line with our [deprecation +policy](https://github.com/matrix-org/synapse/blob/release-v1.32.0/docs/deprecation_policy.md), +we've dropped support for Python 3.5 and PostgreSQL 9.5, as they are no +longer supported upstream. + +This release of Synapse requires Python 3.6+ and PostgresSQL 9.6+ or +SQLite 3.22+. + +## Removal of old List Accounts Admin API + +The deprecated v1 "list accounts" admin API +(`GET /_synapse/admin/v1/users/`) has been removed in this +version. + +The [v2 list accounts +API](https://github.com/matrix-org/synapse/blob/master/docs/admin_api/user_admin_api.rst#list-accounts) +has been available since Synapse 1.7.0 (2019-12-13), and is accessible +under `GET /_synapse/admin/v2/users`. + +The deprecation of the old endpoint was announced with Synapse 1.28.0 +(released on 2021-02-25). + +## Application Services must use type `m.login.application_service` when registering users + +In compliance with the [Application Service +spec](https://matrix.org/docs/spec/application_service/r0.1.2#server-admin-style-permissions), +Application Services are now required to use the +`m.login.application_service` type when registering users via the +`/_matrix/client/r0/register` endpoint. This behaviour was deprecated in +Synapse v1.30.0. + +Please ensure your Application Services are up to date. + +# Upgrading to v1.29.0 + +## Requirement for X-Forwarded-Proto header + +When using Synapse with a reverse proxy (in particular, when using the +[x_forwarded]{.title-ref} option on an HTTP listener), Synapse now +expects to receive an [X-Forwarded-Proto]{.title-ref} header on incoming +HTTP requests. If it is not set, Synapse will log a warning on each +received request. + +To avoid the warning, administrators using a reverse proxy should ensure +that the reverse proxy sets [X-Forwarded-Proto]{.title-ref} header to +[https]{.title-ref} or [http]{.title-ref} to indicate the protocol used +by the client. + +Synapse also requires the [Host]{.title-ref} header to be preserved. + +See the [reverse proxy documentation](../reverse_proxy.md), where the +example configurations have been updated to show how to set these +headers. + +(Users of [Caddy](https://caddyserver.com/) are unaffected, since we +believe it sets [X-Forwarded-Proto]{.title-ref} by default.) + +# Upgrading to v1.27.0 + +## Changes to callback URI for OAuth2 / OpenID Connect and SAML2 + +This version changes the URI used for callbacks from OAuth2 and SAML2 +identity providers: + +- If your server is configured for single sign-on via an OpenID + Connect or OAuth2 identity provider, you will need to add + `[synapse public baseurl]/_synapse/client/oidc/callback` to the list + of permitted "redirect URIs" at the identity provider. + + See the [OpenID docs](../openid.md) for more information on setting + up OpenID Connect. + +- If your server is configured for single sign-on via a SAML2 identity + provider, you will need to add + `[synapse public baseurl]/_synapse/client/saml2/authn_response` as a + permitted "ACS location" (also known as "allowed callback URLs") + at the identity provider. + + The "Issuer" in the "AuthnRequest" to the SAML2 identity + provider is also updated to + `[synapse public baseurl]/_synapse/client/saml2/metadata.xml`. If + your SAML2 identity provider uses this property to validate or + otherwise identify Synapse, its configuration will need to be + updated to use the new URL. Alternatively you could create a new, + separate "EntityDescriptor" in your SAML2 identity provider with + the new URLs and leave the URLs in the existing "EntityDescriptor" + as they were. + +## Changes to HTML templates + +The HTML templates for SSO and email notifications now have [Jinja2's +autoescape](https://jinja.palletsprojects.com/en/2.11.x/api/#autoescaping) +enabled for files ending in `.html`, `.htm`, and `.xml`. If you have +customised these templates and see issues when viewing them you might +need to update them. It is expected that most configurations will need +no changes. + +If you have customised the templates *names* for these templates, it is +recommended to verify they end in `.html` to ensure autoescape is +enabled. + +The above applies to the following templates: + +- `add_threepid.html` +- `add_threepid_failure.html` +- `add_threepid_success.html` +- `notice_expiry.html` +- `notice_expiry.html` +- `notif_mail.html` (which, by default, includes `room.html` and + `notif.html`) +- `password_reset.html` +- `password_reset_confirmation.html` +- `password_reset_failure.html` +- `password_reset_success.html` +- `registration.html` +- `registration_failure.html` +- `registration_success.html` +- `sso_account_deactivated.html` +- `sso_auth_bad_user.html` +- `sso_auth_confirm.html` +- `sso_auth_success.html` +- `sso_error.html` +- `sso_login_idp_picker.html` +- `sso_redirect_confirm.html` + +# Upgrading to v1.26.0 + +## Rolling back to v1.25.0 after a failed upgrade + +v1.26.0 includes a lot of large changes. If something problematic +occurs, you may want to roll-back to a previous version of Synapse. +Because v1.26.0 also includes a new database schema version, reverting +that version is also required alongside the generic rollback +instructions mentioned above. In short, to roll back to v1.25.0 you need +to: + +1. Stop the server + +2. Decrease the schema version in the database: + + ```sql + UPDATE schema_version SET version = 58; + ``` + +3. Delete the ignored users & chain cover data: + + ```sql + DROP TABLE IF EXISTS ignored_users; + UPDATE rooms SET has_auth_chain_index = false; + ``` + + For PostgreSQL run: + + ```sql + TRUNCATE event_auth_chain_links; + TRUNCATE event_auth_chains; + ``` + + For SQLite run: + + ```sql + DELETE FROM event_auth_chain_links; + DELETE FROM event_auth_chains; + ``` + +4. Mark the deltas as not run (so they will re-run on upgrade). + + ```sql + DELETE FROM applied_schema_deltas WHERE version = 59 AND file = "59/01ignored_user.py"; + DELETE FROM applied_schema_deltas WHERE version = 59 AND file = "59/06chain_cover_index.sql"; + ``` + +5. Downgrade Synapse by following the instructions for your + installation method in the "Rolling back to older versions" + section above. + +# Upgrading to v1.25.0 + +## Last release supporting Python 3.5 + +This is the last release of Synapse which guarantees support with Python +3.5, which passed its upstream End of Life date several months ago. + +We will attempt to maintain support through March 2021, but without +guarantees. + +In the future, Synapse will follow upstream schedules for ending support +of older versions of Python and PostgreSQL. Please upgrade to at least +Python 3.6 and PostgreSQL 9.6 as soon as possible. + +## Blacklisting IP ranges + +Synapse v1.25.0 includes new settings, `ip_range_blacklist` and +`ip_range_whitelist`, for controlling outgoing requests from Synapse for +federation, identity servers, push, and for checking key validity for +third-party invite events. The previous setting, +`federation_ip_range_blacklist`, is deprecated. The new +`ip_range_blacklist` defaults to private IP ranges if it is not defined. + +If you have never customised `federation_ip_range_blacklist` it is +recommended that you remove that setting. + +If you have customised `federation_ip_range_blacklist` you should update +the setting name to `ip_range_blacklist`. + +If you have a custom push server that is reached via private IP space +you may need to customise `ip_range_blacklist` or `ip_range_whitelist`. + +# Upgrading to v1.24.0 + +## Custom OpenID Connect mapping provider breaking change + +This release allows the OpenID Connect mapping provider to perform +normalisation of the localpart of the Matrix ID. This allows for the +mapping provider to specify different algorithms, instead of the +[default +way](). + +If your Synapse configuration uses a custom mapping provider +([oidc_config.user_mapping_provider.module]{.title-ref} is specified and +not equal to +[synapse.handlers.oidc_handler.JinjaOidcMappingProvider]{.title-ref}) +then you *must* ensure that [map_user_attributes]{.title-ref} of the +mapping provider performs some normalisation of the +[localpart]{.title-ref} returned. To match previous behaviour you can +use the [map_username_to_mxid_localpart]{.title-ref} function provided +by Synapse. An example is shown below: + +```python +from synapse.types import map_username_to_mxid_localpart + +class MyMappingProvider: + def map_user_attributes(self, userinfo, token): + # ... your custom logic ... + sso_user_id = ... + localpart = map_username_to_mxid_localpart(sso_user_id) + + return {"localpart": localpart} +``` + +## Removal historical Synapse Admin API + +Historically, the Synapse Admin API has been accessible under: + +- `/_matrix/client/api/v1/admin` +- `/_matrix/client/unstable/admin` +- `/_matrix/client/r0/admin` +- `/_synapse/admin/v1` + +The endpoints with `/_matrix/client/*` prefixes have been removed as of +v1.24.0. The Admin API is now only accessible under: + +- `/_synapse/admin/v1` + +The only exception is the [/admin/whois]{.title-ref} endpoint, which is +[also available via the client-server +API](https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-admin-whois-userid). + +The deprecation of the old endpoints was announced with Synapse 1.20.0 +(released on 2020-09-22) and makes it easier for homeserver admins to +lock down external access to the Admin API endpoints. + +# Upgrading to v1.23.0 + +## Structured logging configuration breaking changes + +This release deprecates use of the `structured: true` logging +configuration for structured logging. If your logging configuration +contains `structured: true` then it should be modified based on the +[structured logging +documentation](../structured_logging.md). + +The `structured` and `drains` logging options are now deprecated and +should be replaced by standard logging configuration of `handlers` and +`formatters`. + +A future will release of Synapse will make using `structured: true` an +error. + +# Upgrading to v1.22.0 + +## ThirdPartyEventRules breaking changes + +This release introduces a backwards-incompatible change to modules +making use of `ThirdPartyEventRules` in Synapse. If you make use of a +module defined under the `third_party_event_rules` config option, please +make sure it is updated to handle the below change: + +The `http_client` argument is no longer passed to modules as they are +initialised. Instead, modules are expected to make use of the +`http_client` property on the `ModuleApi` class. Modules are now passed +a `module_api` argument during initialisation, which is an instance of +`ModuleApi`. `ModuleApi` instances have a `http_client` property which +acts the same as the `http_client` argument previously passed to +`ThirdPartyEventRules` modules. + +# Upgrading to v1.21.0 + +## Forwarding `/_synapse/client` through your reverse proxy + +The [reverse proxy +documentation](https://github.com/matrix-org/synapse/blob/develop/docs/reverse_proxy.md) +has been updated to include reverse proxy directives for +`/_synapse/client/*` endpoints. As the user password reset flow now uses +endpoints under this prefix, **you must update your reverse proxy +configurations for user password reset to work**. + +Additionally, note that the [Synapse worker documentation](https://github.com/matrix-org/synapse/blob/develop/docs/workers.md) has been updated to + +: state that the `/_synapse/client/password_reset/email/submit_token` + endpoint can be handled + +by all workers. If you make use of Synapse's worker feature, please +update your reverse proxy configuration to reflect this change. + +## New HTML templates + +A new HTML template, +[password_reset_confirmation.html](https://github.com/matrix-org/synapse/blob/develop/synapse/res/templates/password_reset_confirmation.html), +has been added to the `synapse/res/templates` directory. If you are +using a custom template directory, you may want to copy the template +over and modify it. + +Note that as of v1.20.0, templates do not need to be included in custom +template directories for Synapse to start. The default templates will be +used if a custom template cannot be found. + +This page will appear to the user after clicking a password reset link +that has been emailed to them. + +To complete password reset, the page must include a way to make a +[POST]{.title-ref} request to +`/_synapse/client/password_reset/{medium}/submit_token` with the query +parameters from the original link, presented as a URL-encoded form. See +the file itself for more details. + +## Updated Single Sign-on HTML Templates + +The `saml_error.html` template was removed from Synapse and replaced +with the `sso_error.html` template. If your Synapse is configured to use +SAML and a custom `sso_redirect_confirm_template_dir` configuration then +any customisations of the `saml_error.html` template will need to be +merged into the `sso_error.html` template. These templates are similar, +but the parameters are slightly different: + +- The `msg` parameter should be renamed to `error_description`. +- There is no longer a `code` parameter for the response code. +- A string `error` parameter is available that includes a short hint + of why a user is seeing the error page. + +# Upgrading to v1.18.0 + +## Docker [-py3]{.title-ref} suffix will be removed in future versions + +From 10th August 2020, we will no longer publish Docker images with the +[-py3]{.title-ref} tag suffix. The images tagged with the +[-py3]{.title-ref} suffix have been identical to the non-suffixed tags +since release 0.99.0, and the suffix is obsolete. + +On 10th August, we will remove the [latest-py3]{.title-ref} tag. +Existing per-release tags (such as [v1.18.0-py3]{.title-ref}) will not +be removed, but no new [-py3]{.title-ref} tags will be added. + +Scripts relying on the [-py3]{.title-ref} suffix will need to be +updated. + +## Redis replication is now recommended in lieu of TCP replication + +When setting up worker processes, we now recommend the use of a Redis +server for replication. **The old direct TCP connection method is +deprecated and will be removed in a future release.** See +[workers](../workers.md) for more details. + +# Upgrading to v1.14.0 + +This version includes a database update which is run as part of the +upgrade, and which may take a couple of minutes in the case of a large +server. Synapse will not respond to HTTP requests while this update is +taking place. + +# Upgrading to v1.13.0 + +## Incorrect database migration in old synapse versions + +A bug was introduced in Synapse 1.4.0 which could cause the room +directory to be incomplete or empty if Synapse was upgraded directly +from v1.2.1 or earlier, to versions between v1.4.0 and v1.12.x. + +This will *not* be a problem for Synapse installations which were: + +: - created at v1.4.0 or later, + - upgraded via v1.3.x, or + - upgraded straight from v1.2.1 or earlier to v1.13.0 or later. + +If completeness of the room directory is a concern, installations which +are affected can be repaired as follows: + +1. Run the following sql from a [psql]{.title-ref} or + [sqlite3]{.title-ref} console: + + ```sql + INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES + ('populate_stats_process_rooms', '{}', 'current_state_events_membership'); + + INSERT INTO background_updates (update_name, progress_json, depends_on) VALUES + ('populate_stats_process_users', '{}', 'populate_stats_process_rooms'); + ``` + +2. Restart synapse. + +## New Single Sign-on HTML Templates + +New templates (`sso_auth_confirm.html`, `sso_auth_success.html`, and +`sso_account_deactivated.html`) were added to Synapse. If your Synapse +is configured to use SSO and a custom +`sso_redirect_confirm_template_dir` configuration then these templates +will need to be copied from +[synapse/res/templates](synapse/res/templates) into that directory. + +## Synapse SSO Plugins Method Deprecation + +Plugins using the `complete_sso_login` method of +`synapse.module_api.ModuleApi` should update to using the async/await +version `complete_sso_login_async` which includes additional checks. The +non-async version is considered deprecated. + +## Rolling back to v1.12.4 after a failed upgrade + +v1.13.0 includes a lot of large changes. If something problematic +occurs, you may want to roll-back to a previous version of Synapse. +Because v1.13.0 also includes a new database schema version, reverting +that version is also required alongside the generic rollback +instructions mentioned above. In short, to roll back to v1.12.4 you need +to: + +1. Stop the server + +2. Decrease the schema version in the database: + + ```sql + UPDATE schema_version SET version = 57; + ``` + +3. Downgrade Synapse by following the instructions for your + installation method in the "Rolling back to older versions" + section above. + +# Upgrading to v1.12.0 + +This version includes a database update which is run as part of the +upgrade, and which may take some time (several hours in the case of a +large server). Synapse will not respond to HTTP requests while this +update is taking place. + +This is only likely to be a problem in the case of a server which is +participating in many rooms. + +0. As with all upgrades, it is recommended that you have a recent + backup of your database which can be used for recovery in the event + of any problems. + +1. As an initial check to see if you will be affected, you can try + running the following query from the [psql]{.title-ref} or + [sqlite3]{.title-ref} console. It is safe to run it while Synapse is + still running. + + ```sql + SELECT MAX(q.v) FROM ( + SELECT ( + SELECT ej.json AS v + FROM state_events se INNER JOIN event_json ej USING (event_id) + WHERE se.room_id=rooms.room_id AND se.type='m.room.create' AND se.state_key='' + LIMIT 1 + ) FROM rooms WHERE rooms.room_version IS NULL + ) q; + ``` + + This query will take about the same amount of time as the upgrade + process: ie, if it takes 5 minutes, then it is likely that Synapse + will be unresponsive for 5 minutes during the upgrade. + + If you consider an outage of this duration to be acceptable, no + further action is necessary and you can simply start Synapse 1.12.0. + + If you would prefer to reduce the downtime, continue with the steps + below. + +2. The easiest workaround for this issue is to manually create a new + index before upgrading. On PostgreSQL, his can be done as follows: + + ```sql + CREATE INDEX CONCURRENTLY tmp_upgrade_1_12_0_index + ON state_events(room_id) WHERE type = 'm.room.create'; + ``` + + The above query may take some time, but is also safe to run while + Synapse is running. + + We assume that no SQLite users have databases large enough to be + affected. If you *are* affected, you can run a similar query, + omitting the `CONCURRENTLY` keyword. Note however that this + operation may in itself cause Synapse to stop running for some time. + Synapse admins are reminded that [SQLite is not recommended for use + outside a test + environment](https://github.com/matrix-org/synapse/blob/master/README.rst#using-postgresql). + +3. Once the index has been created, the `SELECT` query in step 1 above + should complete quickly. It is therefore safe to upgrade to Synapse + 1.12.0. + +4. Once Synapse 1.12.0 has successfully started and is responding to + HTTP requests, the temporary index can be removed: + + ```sql + DROP INDEX tmp_upgrade_1_12_0_index; + ``` + +# Upgrading to v1.10.0 + +Synapse will now log a warning on start up if used with a PostgreSQL +database that has a non-recommended locale set. + +See [Postgres](../postgres.md) for details. + +# Upgrading to v1.8.0 + +Specifying a `log_file` config option will now cause Synapse to refuse +to start, and should be replaced by with the `log_config` option. +Support for the `log_file` option was removed in v1.3.0 and has since +had no effect. + +# Upgrading to v1.7.0 + +In an attempt to configure Synapse in a privacy preserving way, the +default behaviours of `allow_public_rooms_without_auth` and +`allow_public_rooms_over_federation` have been inverted. This means that +by default, only authenticated users querying the Client/Server API will +be able to query the room directory, and relatedly that the server will +not share room directory information with other servers over federation. + +If your installation does not explicitly set these settings one way or +the other and you want either setting to be `true` then it will +necessary to update your homeserver configuration file accordingly. + +For more details on the surrounding context see our +[explainer](https://matrix.org/blog/2019/11/09/avoiding-unwelcome-visitors-on-private-matrix-servers). + +# Upgrading to v1.5.0 + +This release includes a database migration which may take several +minutes to complete if there are a large number (more than a million or +so) of entries in the `devices` table. This is only likely to a be a +problem on very large installations. + +# Upgrading to v1.4.0 + +## New custom templates + +If you have configured a custom template directory with the +`email.template_dir` option, be aware that there are new templates +regarding registration and threepid management (see below) that must be +included. + +- `registration.html` and `registration.txt` +- `registration_success.html` and `registration_failure.html` +- `add_threepid.html` and `add_threepid.txt` +- `add_threepid_failure.html` and `add_threepid_success.html` + +Synapse will expect these files to exist inside the configured template +directory, and **will fail to start** if they are absent. To view the +default templates, see +[synapse/res/templates](https://github.com/matrix-org/synapse/tree/master/synapse/res/templates). + +## 3pid verification changes + +**Note: As of this release, users will be unable to add phone numbers or +email addresses to their accounts, without changes to the Synapse +configuration. This includes adding an email address during +registration.** + +It is possible for a user to associate an email address or phone number +with their account, for a number of reasons: + +- for use when logging in, as an alternative to the user id. +- in the case of email, as an alternative contact to help with account + recovery. +- in the case of email, to receive notifications of missed messages. + +Before an email address or phone number can be added to a user's +account, or before such an address is used to carry out a +password-reset, Synapse must confirm the operation with the owner of the +email address or phone number. It does this by sending an email or text +giving the user a link or token to confirm receipt. This process is +known as '3pid verification'. ('3pid', or 'threepid', stands for +third-party identifier, and we use it to refer to external identifiers +such as email addresses and phone numbers.) + +Previous versions of Synapse delegated the task of 3pid verification to +an identity server by default. In most cases this server is `vector.im` +or `matrix.org`. + +In Synapse 1.4.0, for security and privacy reasons, the homeserver will +no longer delegate this task to an identity server by default. Instead, +the server administrator will need to explicitly decide how they would +like the verification messages to be sent. + +In the medium term, the `vector.im` and `matrix.org` identity servers +will disable support for delegated 3pid verification entirely. However, +in order to ease the transition, they will retain the capability for a +limited period. Delegated email verification will be disabled on Monday +2nd December 2019 (giving roughly 2 months notice). Disabling delegated +SMS verification will follow some time after that once SMS verification +support lands in Synapse. + +Once delegated 3pid verification support has been disabled in the +`vector.im` and `matrix.org` identity servers, all Synapse versions that +depend on those instances will be unable to verify email and phone +numbers through them. There are no imminent plans to remove delegated +3pid verification from Sydent generally. (Sydent is the identity server +project that backs the `vector.im` and `matrix.org` instances). + +### Email + +Following upgrade, to continue verifying email (e.g. as part of the +registration process), admins can either:- + +- Configure Synapse to use an email server. +- Run or choose an identity server which allows delegated email + verification and delegate to it. + +#### Configure SMTP in Synapse + +To configure an SMTP server for Synapse, modify the configuration +section headed `email`, and be sure to have at least the +`smtp_host, smtp_port` and `notif_from` fields filled out. + +You may also need to set `smtp_user`, `smtp_pass`, and +`require_transport_security`. + +See the [sample configuration file](docs/sample_config.yaml) for more +details on these settings. + +#### Delegate email to an identity server + +Some admins will wish to continue using email verification as part of +the registration process, but will not immediately have an appropriate +SMTP server at hand. + +To this end, we will continue to support email verification delegation +via the `vector.im` and `matrix.org` identity servers for two months. +Support for delegated email verification will be disabled on Monday 2nd +December. + +The `account_threepid_delegates` dictionary defines whether the +homeserver should delegate an external server (typically an [identity +server](https://matrix.org/docs/spec/identity_service/r0.2.1)) to handle +sending confirmation messages via email and SMS. + +So to delegate email verification, in `homeserver.yaml`, set +`account_threepid_delegates.email` to the base URL of an identity +server. For example: + +```yaml +account_threepid_delegates: + email: https://example.com # Delegate email sending to example.com +``` + +Note that `account_threepid_delegates.email` replaces the deprecated +`email.trust_identity_server_for_password_resets`: if +`email.trust_identity_server_for_password_resets` is set to `true`, and +`account_threepid_delegates.email` is not set, then the first entry in +`trusted_third_party_id_servers` will be used as the +`account_threepid_delegate` for email. This is to ensure compatibility +with existing Synapse installs that set up external server handling for +these tasks before v1.4.0. If +`email.trust_identity_server_for_password_resets` is `true` and no +trusted identity server domains are configured, Synapse will report an +error and refuse to start. + +If `email.trust_identity_server_for_password_resets` is `false` or +absent and no `email` delegate is configured in +`account_threepid_delegates`, then Synapse will send email verification +messages itself, using the configured SMTP server (see above). that +type. + +### Phone numbers + +Synapse does not support phone-number verification itself, so the only +way to maintain the ability for users to add phone numbers to their +accounts will be by continuing to delegate phone number verification to +the `matrix.org` and `vector.im` identity servers (or another identity +server that supports SMS sending). + +The `account_threepid_delegates` dictionary defines whether the +homeserver should delegate an external server (typically an [identity +server](https://matrix.org/docs/spec/identity_service/r0.2.1)) to handle +sending confirmation messages via email and SMS. + +So to delegate phone number verification, in `homeserver.yaml`, set +`account_threepid_delegates.msisdn` to the base URL of an identity +server. For example: + +```yaml +account_threepid_delegates: + msisdn: https://example.com # Delegate sms sending to example.com +``` + +The `matrix.org` and `vector.im` identity servers will continue to +support delegated phone number verification via SMS until such time as +it is possible for admins to configure their servers to perform phone +number verification directly. More details will follow in a future +release. + +## Rolling back to v1.3.1 + +If you encounter problems with v1.4.0, it should be possible to roll +back to v1.3.1, subject to the following: + +- The 'room statistics' engine was heavily reworked in this release + (see [#5971](https://github.com/matrix-org/synapse/pull/5971)), + including significant changes to the database schema, which are not + easily reverted. This will cause the room statistics engine to stop + updating when you downgrade. + + The room statistics are essentially unused in v1.3.1 (in future + versions of Synapse, they will be used to populate the room + directory), so there should be no loss of functionality. However, + the statistics engine will write errors to the logs, which can be + avoided by setting the following in `homeserver.yaml`: + + ```yaml + stats: + enabled: false + ``` + + Don't forget to re-enable it when you upgrade again, in preparation + for its use in the room directory! + +# Upgrading to v1.2.0 + +Some counter metrics have been renamed, with the old names deprecated. +See [the metrics +documentation](../metrics-howto.md#renaming-of-metrics--deprecation-of-old-names-in-12) +for details. + +# Upgrading to v1.1.0 + +Synapse v1.1.0 removes support for older Python and PostgreSQL versions, +as outlined in [our deprecation +notice](https://matrix.org/blog/2019/04/08/synapse-deprecating-postgres-9-4-and-python-2-x). + +## Minimum Python Version + +Synapse v1.1.0 has a minimum Python requirement of Python 3.5. Python +3.6 or Python 3.7 are recommended as they have improved internal string +handling, significantly reducing memory usage. + +If you use current versions of the Matrix.org-distributed Debian +packages or Docker images, action is not required. + +If you install Synapse in a Python virtual environment, please see +"Upgrading to v0.34.0" for notes on setting up a new virtualenv under +Python 3. + +## Minimum PostgreSQL Version + +If using PostgreSQL under Synapse, you will need to use PostgreSQL 9.5 +or above. Please see the [PostgreSQL +documentation](https://www.postgresql.org/docs/11/upgrading.html) for +more details on upgrading your database. + +# Upgrading to v1.0 + +## Validation of TLS certificates + +Synapse v1.0 is the first release to enforce validation of TLS +certificates for the federation API. It is therefore essential that your +certificates are correctly configured. See the +[FAQ](../MSC1711_certificates_FAQ.md) for more information. + +Note, v1.0 installations will also no longer be able to federate with +servers that have not correctly configured their certificates. + +In rare cases, it may be desirable to disable certificate checking: for +example, it might be essential to be able to federate with a given +legacy server in a closed federation. This can be done in one of two +ways:- + +- Configure the global switch `federation_verify_certificates` to + `false`. +- Configure a whitelist of server domains to trust via + `federation_certificate_verification_whitelist`. + +See the [sample configuration file](docs/sample_config.yaml) for more +details on these settings. + +## Email + +When a user requests a password reset, Synapse will send an email to the +user to confirm the request. + +Previous versions of Synapse delegated the job of sending this email to +an identity server. If the identity server was somehow malicious or +became compromised, it would be theoretically possible to hijack an +account through this means. + +Therefore, by default, Synapse v1.0 will send the confirmation email +itself. If Synapse is not configured with an SMTP server, password reset +via email will be disabled. + +To configure an SMTP server for Synapse, modify the configuration +section headed `email`, and be sure to have at least the `smtp_host`, +`smtp_port` and `notif_from` fields filled out. You may also need to set +`smtp_user`, `smtp_pass`, and `require_transport_security`. + +If you are absolutely certain that you wish to continue using an +identity server for password resets, set +`trust_identity_server_for_password_resets` to `true`. + +See the [sample configuration file](docs/sample_config.yaml) for more +details on these settings. + +## New email templates + +Some new templates have been added to the default template directory for the purpose of +the homeserver sending its own password reset emails. If you have configured a +custom `template_dir` in your Synapse config, these files will need to be added. + +`password_reset.html` and `password_reset.txt` are HTML and plain text +templates respectively that contain the contents of what will be emailed +to the user upon attempting to reset their password via email. +`password_reset_success.html` and `password_reset_failure.html` are HTML +files that the content of which (assuming no redirect URL is set) will +be shown to the user after they attempt to click the link in the email +sent to them. + +# Upgrading to v0.99.0 + +Please be aware that, before Synapse v1.0 is released around March 2019, +you will need to replace any self-signed certificates with those +verified by a root CA. Information on how to do so can be found at [the +ACME docs](../ACME.md). + +For more information on configuring TLS certificates see the +[FAQ](../MSC1711_certificates_FAQ.md). + +# Upgrading to v0.34.0 + +1. This release is the first to fully support Python 3. Synapse will + now run on Python versions 3.5, or 3.6 (as well as 2.7). We + recommend switching to Python 3, as it has been shown to give + performance improvements. + + For users who have installed Synapse into a virtualenv, we recommend + doing this by creating a new virtualenv. For example: + + virtualenv -p python3 ~/synapse/env3 + source ~/synapse/env3/bin/activate + pip install matrix-synapse + + You can then start synapse as normal, having activated the new + virtualenv: + + cd ~/synapse + source env3/bin/activate + synctl start + + Users who have installed from distribution packages should see the + relevant package documentation. See below for notes on Debian + packages. + + - When upgrading to Python 3, you **must** make sure that your log + files are configured as UTF-8, by adding `encoding: utf8` to the + `RotatingFileHandler` configuration (if you have one) in your + `.log.config` file. For example, if your `log.config` + file contains: + + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: precise + filename: homeserver.log + maxBytes: 104857600 + backupCount: 10 + filters: [context] + console: + class: logging.StreamHandler + formatter: precise + filters: [context] + + Then you should update this to be: + + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: precise + filename: homeserver.log + maxBytes: 104857600 + backupCount: 10 + filters: [context] + encoding: utf8 + console: + class: logging.StreamHandler + formatter: precise + filters: [context] + + There is no need to revert this change if downgrading to + Python 2. + + We are also making available Debian packages which will run Synapse + on Python 3. You can switch to these packages with + `apt-get install matrix-synapse-py3`, however, please read + [debian/NEWS](https://github.com/matrix-org/synapse/blob/release-v0.34.0/debian/NEWS) + before doing so. The existing `matrix-synapse` packages will + continue to use Python 2 for the time being. + +2. This release removes the `riot.im` from the default list of trusted + identity servers. + + If `riot.im` is in your homeserver's list of + `trusted_third_party_id_servers`, you should remove it. It was added + in case a hypothetical future identity server was put there. If you + don't remove it, users may be unable to deactivate their accounts. + +3. This release no longer installs the (unmaintained) Matrix Console + web client as part of the default installation. It is possible to + re-enable it by installing it separately and setting the + `web_client_location` config option, but please consider switching + to another client. + +# Upgrading to v0.33.7 + +This release removes the example email notification templates from +`res/templates` (they are now internal to the python package). This +should only affect you if you (a) deploy your Synapse instance from a +git checkout or a github snapshot URL, and (b) have email notifications +enabled. + +If you have email notifications enabled, you should ensure that +`email.template_dir` is either configured to point at a directory where +you have installed customised templates, or leave it unset to use the +default templates. + +# Upgrading to v0.27.3 + +This release expands the anonymous usage stats sent if the opt-in +`report_stats` configuration is set to `true`. We now capture RSS memory +and cpu use at a very coarse level. This requires administrators to +install the optional `psutil` python module. + +We would appreciate it if you could assist by ensuring this module is +available and `report_stats` is enabled. This will let us see if +performance changes to synapse are having an impact to the general +community. + +# Upgrading to v0.15.0 + +If you want to use the new URL previewing API +(`/_matrix/media/r0/preview_url`) then you have to explicitly enable it +in the config and update your dependencies dependencies. See README.rst +for details. + +# Upgrading to v0.11.0 + +This release includes the option to send anonymous usage stats to +matrix.org, and requires that administrators explictly opt in or out by +setting the `report_stats` option to either `true` or `false`. + +We would really appreciate it if you could help our project out by +reporting anonymized usage statistics from your homeserver. Only very +basic aggregate data (e.g. number of users) will be reported, but it +helps us to track the growth of the Matrix community, and helps us to +make Matrix a success, as well as to convince other networks that they +should peer with us. + +# Upgrading to v0.9.0 + +Application services have had a breaking API change in this version. + +They can no longer register themselves with a home server using the AS +HTTP API. This decision was made because a compromised application +service with free reign to register any regex in effect grants full +read/write access to the home server if a regex of `.*` is used. An +attack where a compromised AS re-registers itself with `.*` was deemed +too big of a security risk to ignore, and so the ability to register +with the HS remotely has been removed. + +It has been replaced by specifying a list of application service +registrations in `homeserver.yaml`: + + app_service_config_files: ["registration-01.yaml", "registration-02.yaml"] + +Where `registration-01.yaml` looks like: + + url: # e.g. "https://my.application.service.com" + as_token: + hs_token: + sender_localpart: # This is a new field which denotes the user_id localpart when using the AS token + namespaces: + users: + - exclusive: + regex: # e.g. "@prefix_.*" + aliases: + - exclusive: + regex: + rooms: + - exclusive: + regex: + +# Upgrading to v0.8.0 + +Servers which use captchas will need to add their public key to: + + static/client/register/register_config.js + + window.matrixRegistrationConfig = { + recaptcha_public_key: "YOUR_PUBLIC_KEY" + }; + +This is required in order to support registration fallback (typically +used on mobile devices). + +# Upgrading to v0.7.0 + +New dependencies are: + +- pydenticon +- simplejson +- syutil +- matrix-angular-sdk + +To pull in these dependencies in a virtual env, run: + + python synapse/python_dependencies.py | xargs -n 1 pip install + +# Upgrading to v0.6.0 + +To pull in new dependencies, run: + + python setup.py develop --user + +This update includes a change to the database schema. To upgrade you +first need to upgrade the database by running: + + python scripts/upgrade_db_to_v0.6.0.py + +Where []{.title-ref} is the location of the database, +[]{.title-ref} is the server name as specified in the +synapse configuration, and []{.title-ref} is the location +of the signing key as specified in the synapse configuration. + +This may take some time to complete. Failures of signatures and content +hashes can safely be ignored. + +# Upgrading to v0.5.1 + +Depending on precisely when you installed v0.5.0 you may have ended up +with a stale release of the reference matrix webclient installed as a +python module. To uninstall it and ensure you are depending on the +latest module, please run: + + $ pip uninstall syweb + +# Upgrading to v0.5.0 + +The webclient has been split out into a seperate repository/pacakage in +this release. Before you restart your homeserver you will need to pull +in the webclient package by running: + + python setup.py develop --user + +This release completely changes the database schema and so requires +upgrading it before starting the new version of the homeserver. + +The script "database-prepare-for-0.5.0.sh" should be used to upgrade +the database. This will save all user information, such as logins and +profiles, but will otherwise purge the database. This includes messages, +which rooms the home server was a member of and room alias mappings. + +If you would like to keep your history, please take a copy of your +database file and ask for help in #matrix:matrix.org. The upgrade +process is, unfortunately, non trivial and requires human intervention +to resolve any resulting conflicts during the upgrade process. + +Before running the command the homeserver should be first completely +shutdown. To run it, simply specify the location of the database, e.g.: + +> ./scripts/database-prepare-for-0.5.0.sh "homeserver.db" + +Once this has successfully completed it will be safe to restart the +homeserver. You may notice that the homeserver takes a few seconds +longer to restart than usual as it reinitializes the database. + +On startup of the new version, users can either rejoin remote rooms +using room aliases or by being reinvited. Alternatively, if any other +homeserver sends a message to a room that the homeserver was previously +in the local HS will automatically rejoin the room. + +# Upgrading to v0.4.0 + +This release needs an updated syutil version. Run: + + python setup.py develop + +You will also need to upgrade your configuration as the signing key +format has changed. Run: + + python -m synapse.app.homeserver --config-path --generate-config + +# Upgrading to v0.3.0 + +This registration API now closely matches the login API. This introduces +a bit more backwards and forwards between the HS and the client, but +this improves the overall flexibility of the API. You can now GET on +/register to retrieve a list of valid registration flows. Upon choosing +one, they are submitted in the same way as login, e.g: + + { + type: m.login.password, + user: foo, + password: bar + } + +The default HS supports 2 flows, with and without Identity Server email +authentication. Enabling captcha on the HS will add in an extra step to +all flows: `m.login.recaptcha` which must be completed before you can +transition to the next stage. There is a new login type: +`m.login.email.identity` which contains the `threepidCreds` key which +were previously sent in the original register request. For more +information on this, see the specification. + +## Web Client + +The VoIP specification has changed between v0.2.0 and v0.3.0. Users +should refresh any browser tabs to get the latest web client code. Users +on v0.2.0 of the web client will not be able to call those on v0.3.0 and +vice versa. + +# Upgrading to v0.2.0 + +The home server now requires setting up of SSL config before it can run. +To automatically generate default config use: + + $ python synapse/app/homeserver.py \ + --server-name machine.my.domain.name \ + --bind-port 8448 \ + --config-path homeserver.config \ + --generate-config + +This config can be edited if desired, for example to specify a different +SSL certificate to use. Once done you can run the home server using: + + $ python synapse/app/homeserver.py --config-path homeserver.config + +See the README.rst for more information. + +Also note that some config options have been renamed, including: + +- "host" to "server-name" +- "database" to "database-path" +- "port" to "bind-port" and "unsecure-port" + +# Upgrading to v0.0.1 + +This release completely changes the database schema and so requires +upgrading it before starting the new version of the homeserver. + +The script "database-prepare-for-0.0.1.sh" should be used to upgrade +the database. This will save all user information, such as logins and +profiles, but will otherwise purge the database. This includes messages, +which rooms the home server was a member of and room alias mappings. + +Before running the command the homeserver should be first completely +shutdown. To run it, simply specify the location of the database, e.g.: + +> ./scripts/database-prepare-for-0.0.1.sh "homeserver.db" + +Once this has successfully completed it will be safe to restart the +homeserver. You may notice that the homeserver takes a few seconds +longer to restart than usual as it reinitializes the database. + +On startup of the new version, users can either rejoin remote rooms +using room aliases or by being reinvited. Alternatively, if any other +homeserver sends a message to a room that the homeserver was previously +in the local HS will automatically rejoin the room. diff --git a/docs/upgrading/README.md b/docs/upgrading/README.md deleted file mode 100644 index 258e58cf15..0000000000 --- a/docs/upgrading/README.md +++ /dev/null @@ -1,7 +0,0 @@ - -{{#include ../../UPGRADE.rst}} \ No newline at end of file From acac4535c5ce8eec9615375c933bae4a0ed9c058 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 24 Jun 2021 10:57:39 +0100 Subject: [PATCH 318/619] Tweak changelog --- CHANGES.md | 33 +++++++++++++++++++-------------- changelog.d/10238.removal | 1 - 2 files changed, 19 insertions(+), 15 deletions(-) delete mode 100644 changelog.d/10238.removal diff --git a/CHANGES.md b/CHANGES.md index 3cf1814264..1fdfeef266 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,17 @@ Synapse 1.37.0rc1 (2021-06-23) ============================== +This release deprecates the current spam checker interface. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/develop/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new generic module interface. + +This release also removes support for fetching and renewing TLS certificate using the ACME v1 protocol, which has been fully decomissioned by Let's Encrypt on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. + + Features -------- -- Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by Sorunome and anoa. ([\#6739](https://github.com/matrix-org/synapse/issues/6739), [\#9359](https://github.com/matrix-org/synapse/issues/9359), [\#10167](https://github.com/matrix-org/synapse/issues/10167), [\#10212](https://github.com/matrix-org/synapse/issues/10212), [\#10227](https://github.com/matrix-org/synapse/issues/10227)) +- Implement "room knocking" as per [MSC2403](https://github.com/matrix-org/matrix-doc/pull/2403). Contributed by @Sorunome and anoa. ([\#6739](https://github.com/matrix-org/synapse/issues/6739), [\#9359](https://github.com/matrix-org/synapse/issues/9359), [\#10167](https://github.com/matrix-org/synapse/issues/10167), [\#10212](https://github.com/matrix-org/synapse/issues/10212), [\#10227](https://github.com/matrix-org/synapse/issues/10227)) - Add experimental support for backfilling history into rooms ([MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#9247](https://github.com/matrix-org/synapse/issues/9247)) -- Standardised the module interface. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10206](https://github.com/matrix-org/synapse/issues/10206)) +- Implement a generic interface for third-party plugin modules. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10206](https://github.com/matrix-org/synapse/issues/10206)) - Implement config option `sso.update_profile_information` to sync SSO users' profile information with the identity provider each time they login. Currently only displayname is supported. ([\#10108](https://github.com/matrix-org/synapse/issues/10108)) - Ensure that errors during startup are written to the logs and the console. ([\#10191](https://github.com/matrix-org/synapse/issues/10191)) @@ -15,13 +20,13 @@ Bugfixes -------- - Fix a bug introduced in Synapse v1.25.0 that prevented the `ip_range_whitelist` configuration option from working for federation and identity servers. Contributed by @mikure. ([\#10115](https://github.com/matrix-org/synapse/issues/10115)) -- Remove a broken import line in Synapse's admin_cmd worker. Broke in 1.33.0. ([\#10154](https://github.com/matrix-org/synapse/issues/10154)) -- Fix a bug introduced in v1.21.0 which could cause `/sync` to return immediately with an empty response. ([\#10157](https://github.com/matrix-org/synapse/issues/10157), [\#10158](https://github.com/matrix-org/synapse/issues/10158)) -- Fix a minor bug in the response to `/_matrix/client/r0/user/{user}/openid/request_token`. Contributed by @lukaslihotzki. ([\#10175](https://github.com/matrix-org/synapse/issues/10175)) +- Remove a broken import line in Synapse's `admin_cmd` worker. Broke in Synapse v1.33.0. ([\#10154](https://github.com/matrix-org/synapse/issues/10154)) +- Fix a bug introduced in Synapse v1.21.0 which could cause `/sync` to return immediately with an empty response. ([\#10157](https://github.com/matrix-org/synapse/issues/10157), [\#10158](https://github.com/matrix-org/synapse/issues/10158)) +- Fix a minor bug in the response to `/_matrix/client/r0/user/{user}/openid/request_token` causing `expires_in` to be a float instead of an integer. Contributed by @lukaslihotzki. ([\#10175](https://github.com/matrix-org/synapse/issues/10175)) - Always require users to re-authenticate for dangerous operations: deactivating an account, modifying an account password, and adding 3PIDs. ([\#10184](https://github.com/matrix-org/synapse/issues/10184)) -- Fix a bug introduced in Synpase 1.7.2 where remote server count metrics collection would be incorrectly delayed on startup. Found by @heftig. ([\#10195](https://github.com/matrix-org/synapse/issues/10195)) -- Fix a bug introduced in v1.35.1 where an `allow` key of a `m.room.join_rules` event could be applied for incorrect room versions and configurations. ([\#10208](https://github.com/matrix-org/synapse/issues/10208)) -- Fix performance regression in responding to user key requests over federation. Introduced in v1.34.0rc1. ([\#10221](https://github.com/matrix-org/synapse/issues/10221)) +- Fix a bug introduced in Synpase v1.7.2 where remote server count metrics collection would be incorrectly delayed on startup. Found by @heftig. ([\#10195](https://github.com/matrix-org/synapse/issues/10195)) +- Fix a bug introduced in Synapse v1.35.1 where an `allow` key of a `m.room.join_rules` event could be applied for incorrect room versions and configurations. ([\#10208](https://github.com/matrix-org/synapse/issues/10208)) +- Fix performance regression in responding to user key requests over federation. Introduced in Synapse v1.34.0rc1. ([\#10221](https://github.com/matrix-org/synapse/issues/10221)) Improved Documentation @@ -36,9 +41,9 @@ Improved Documentation Deprecations and Removals ------------------------- -- The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10210](https://github.com/matrix-org/synapse/issues/10210)) +- The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/develop/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10210](https://github.com/matrix-org/synapse/issues/10210), [\#10238](https://github.com/matrix-org/synapse/issues/10238)) - Stop supporting the unstable spaces prefixes from MSC1772. ([\#10161](https://github.com/matrix-org/synapse/issues/10161)) -- Remove Synapse's support for automatically fetching and renewing certificates using the ACME v1 protocol. This protocol has been fully turned off by Let's Encrypt for existing install on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. ([\#10194](https://github.com/matrix-org/synapse/issues/10194)) +- Remove Synapse's support for automatically fetching and renewing certificates using the ACME v1 protocol. This protocol has been fully turned off by Let's Encrypt for existing installations on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. ([\#10194](https://github.com/matrix-org/synapse/issues/10194)) Internal Changes @@ -47,21 +52,21 @@ Internal Changes - Update the database schema versioning to support gradual migration away from legacy tables. ([\#9933](https://github.com/matrix-org/synapse/issues/9933)) - Add type hints to the federation servlets. ([\#10080](https://github.com/matrix-org/synapse/issues/10080)) - Improve OpenTracing for event persistence. ([\#10134](https://github.com/matrix-org/synapse/issues/10134), [\#10193](https://github.com/matrix-org/synapse/issues/10193)) -- Clean up the interface for injecting opentracing over HTTP. ([\#10143](https://github.com/matrix-org/synapse/issues/10143)) +- Clean up the interface for injecting OpenTracing over HTTP. ([\#10143](https://github.com/matrix-org/synapse/issues/10143)) - Limit the number of in-flight `/keys/query` requests from a single device. ([\#10144](https://github.com/matrix-org/synapse/issues/10144)) - Refactor EventPersistenceQueue. ([\#10145](https://github.com/matrix-org/synapse/issues/10145)) - Document `SYNAPSE_TEST_LOG_LEVEL` to see the logger output when running tests. ([\#10148](https://github.com/matrix-org/synapse/issues/10148)) - Update the Complement build tags in GitHub Actions to test currently experimental features. ([\#10155](https://github.com/matrix-org/synapse/issues/10155)) -- Add `synapse_federation_soft_failed_events_total` metric to track how often events are soft failed. ([\#10156](https://github.com/matrix-org/synapse/issues/10156)) +- Add a `synapse_federation_soft_failed_events_total` metric to track how often events are soft failed. ([\#10156](https://github.com/matrix-org/synapse/issues/10156)) - Fetch the corresponding complement branch when performing CI. ([\#10160](https://github.com/matrix-org/synapse/issues/10160)) - Add some developer documentation about boolean columns in database schemas. ([\#10164](https://github.com/matrix-org/synapse/issues/10164)) - Add extra logging fields to better debug where events are being soft failed. ([\#10168](https://github.com/matrix-org/synapse/issues/10168)) - Add debug logging for when we enter and exit `Measure` blocks. ([\#10183](https://github.com/matrix-org/synapse/issues/10183)) - Improve comments in structured logging code. ([\#10188](https://github.com/matrix-org/synapse/issues/10188)) -- Update MSC3083 support for modifications in the MSC. ([\#10189](https://github.com/matrix-org/synapse/issues/10189)) +- Update [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) support with modifications from the MSC. ([\#10189](https://github.com/matrix-org/synapse/issues/10189)) - Remove redundant DNS lookup limiter. ([\#10190](https://github.com/matrix-org/synapse/issues/10190)) - Upgrade `black` linting tool to 21.6b0. ([\#10197](https://github.com/matrix-org/synapse/issues/10197)) -- Expose opentracing trace id in response headers. ([\#10199](https://github.com/matrix-org/synapse/issues/10199)) +- Expose OpenTracing trace id in response headers. ([\#10199](https://github.com/matrix-org/synapse/issues/10199)) Synapse 1.36.0 (2021-06-15) diff --git a/changelog.d/10238.removal b/changelog.d/10238.removal deleted file mode 100644 index 5fb7bfb47e..0000000000 --- a/changelog.d/10238.removal +++ /dev/null @@ -1 +0,0 @@ -The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/master/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. From 7e0cd502c745f6ae1b63bde5ef1a785b53308658 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 24 Jun 2021 10:59:45 +0100 Subject: [PATCH 319/619] Fix date in changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1fdfeef266..1b3c280cc5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -Synapse 1.37.0rc1 (2021-06-23) +Synapse 1.37.0rc1 (2021-06-24) ============================== This release deprecates the current spam checker interface. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/develop/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new generic module interface. From bb472f3a9417286571e6646be4dca3f617fb9fee Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 24 Jun 2021 11:14:46 +0100 Subject: [PATCH 320/619] Incorportate review comments --- CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1b3c280cc5..2c7f24487c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,9 @@ Synapse 1.37.0rc1 (2021-06-24) ============================== -This release deprecates the current spam checker interface. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/develop/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new generic module interface. +This release deprecates the current spam checker interface. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new generic module interface. -This release also removes support for fetching and renewing TLS certificate using the ACME v1 protocol, which has been fully decomissioned by Let's Encrypt on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. +This release also removes support for fetching and renewing TLS certificates using the ACME v1 protocol, which has been fully decommissioned by Let's Encrypt on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. Features @@ -41,7 +41,7 @@ Improved Documentation Deprecations and Removals ------------------------- -- The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://github.com/matrix-org/synapse/blob/develop/UPGRADE.rst#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10210](https://github.com/matrix-org/synapse/issues/10210), [\#10238](https://github.com/matrix-org/synapse/issues/10238)) +- The current spam checker interface is deprecated in favour of a new generic modules system. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new system. ([\#10062](https://github.com/matrix-org/synapse/issues/10062), [\#10210](https://github.com/matrix-org/synapse/issues/10210), [\#10238](https://github.com/matrix-org/synapse/issues/10238)) - Stop supporting the unstable spaces prefixes from MSC1772. ([\#10161](https://github.com/matrix-org/synapse/issues/10161)) - Remove Synapse's support for automatically fetching and renewing certificates using the ACME v1 protocol. This protocol has been fully turned off by Let's Encrypt for existing installations on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. ([\#10194](https://github.com/matrix-org/synapse/issues/10194)) From bd4919fb72b2a75f1c0a7f0c78bd619fd2ae30e8 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Jun 2021 15:33:20 +0200 Subject: [PATCH 321/619] MSC2918 Refresh tokens implementation (#9450) This implements refresh tokens, as defined by MSC2918 This MSC has been implemented client side in Hydrogen Web: vector-im/hydrogen-web#235 The basics of the MSC works: requesting refresh tokens on login, having the access tokens expire, and using the refresh token to get a new one. Signed-off-by: Quentin Gliech --- changelog.d/9450.feature | 1 + scripts/synapse_port_db | 4 +- synapse/api/auth.py | 5 + synapse/config/registration.py | 21 ++ synapse/handlers/auth.py | 132 ++++++++++- synapse/handlers/register.py | 52 ++++- synapse/module_api/__init__.py | 2 +- synapse/replication/http/login.py | 13 +- synapse/rest/client/v1/login.py | 171 ++++++++++++-- synapse/rest/client/v2_alpha/register.py | 88 +++++-- .../storage/databases/main/registration.py | 207 +++++++++++++++- .../schema/main/delta/59/14refresh_tokens.sql | 34 +++ tests/api/test_auth.py | 1 + tests/handlers/test_device.py | 2 +- tests/rest/client/v2_alpha/test_auth.py | 220 +++++++++++++++++- 15 files changed, 892 insertions(+), 61 deletions(-) create mode 100644 changelog.d/9450.feature create mode 100644 synapse/storage/schema/main/delta/59/14refresh_tokens.sql diff --git a/changelog.d/9450.feature b/changelog.d/9450.feature new file mode 100644 index 0000000000..455936a41d --- /dev/null +++ b/changelog.d/9450.feature @@ -0,0 +1 @@ +Implement refresh tokens as specified by [MSC2918](https://github.com/matrix-org/matrix-doc/pull/2918). diff --git a/scripts/synapse_port_db b/scripts/synapse_port_db index 86eb76cbca..2bbaf5557d 100755 --- a/scripts/synapse_port_db +++ b/scripts/synapse_port_db @@ -93,6 +93,7 @@ BOOLEAN_COLUMNS = { "local_media_repository": ["safe_from_quarantine"], "users": ["shadow_banned"], "e2e_fallback_keys_json": ["used"], + "access_tokens": ["used"], } @@ -307,7 +308,8 @@ class Porter(object): information_schema.table_constraints AS tc INNER JOIN information_schema.constraint_column_usage AS ccu USING (table_schema, constraint_name) - WHERE tc.constraint_type = 'FOREIGN KEY'; + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_name != ccu.table_name; """ txn.execute(sql) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index edf1b918eb..29cf257633 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -245,6 +245,11 @@ async def get_user_by_req( errcode=Codes.GUEST_ACCESS_FORBIDDEN, ) + # Mark the token as used. This is used to invalidate old refresh + # tokens after some time. + if not user_info.token_used and token_id is not None: + await self.store.mark_access_token_as_used(token_id) + requester = create_requester( user_info.user_id, token_id, diff --git a/synapse/config/registration.py b/synapse/config/registration.py index d9dc55a0c3..0ad919b139 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -119,6 +119,27 @@ def read_config(self, config, **kwargs): session_lifetime = self.parse_duration(session_lifetime) self.session_lifetime = session_lifetime + # The `access_token_lifetime` applies for tokens that can be renewed + # using a refresh token, as per MSC2918. If it is `None`, the refresh + # token mechanism is disabled. + # + # Since it is incompatible with the `session_lifetime` mechanism, it is set to + # `None` by default if a `session_lifetime` is set. + access_token_lifetime = config.get( + "access_token_lifetime", "5m" if session_lifetime is None else None + ) + if access_token_lifetime is not None: + access_token_lifetime = self.parse_duration(access_token_lifetime) + self.access_token_lifetime = access_token_lifetime + + if session_lifetime is not None and access_token_lifetime is not None: + raise ConfigError( + "The refresh token mechanism is incompatible with the " + "`session_lifetime` option. Consider disabling the " + "`session_lifetime` option or disabling the refresh token " + "mechanism by removing the `access_token_lifetime` option." + ) + # The success template used during fallback auth. self.fallback_success_template = self.read_template("auth_success.html") diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 1971e373ed..e2ac595a62 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -30,6 +30,7 @@ Optional, Tuple, Union, + cast, ) import attr @@ -72,6 +73,7 @@ from synapse.util.threepids import canonicalise_email if TYPE_CHECKING: + from synapse.rest.client.v1.login import LoginResponse from synapse.server import HomeServer logger = logging.getLogger(__name__) @@ -777,6 +779,108 @@ def _auth_dict_for_flows( "params": params, } + async def refresh_token( + self, + refresh_token: str, + valid_until_ms: Optional[int], + ) -> Tuple[str, str]: + """ + Consumes a refresh token and generate both a new access token and a new refresh token from it. + + The consumed refresh token is considered invalid after the first use of the new access token or the new refresh token. + + Args: + refresh_token: The token to consume. + valid_until_ms: The expiration timestamp of the new access token. + + Returns: + A tuple containing the new access token and refresh token + """ + + # Verify the token signature first before looking up the token + if not self._verify_refresh_token(refresh_token): + raise SynapseError(401, "invalid refresh token", Codes.UNKNOWN_TOKEN) + + existing_token = await self.store.lookup_refresh_token(refresh_token) + if existing_token is None: + raise SynapseError(401, "refresh token does not exist", Codes.UNKNOWN_TOKEN) + + if ( + existing_token.has_next_access_token_been_used + or existing_token.has_next_refresh_token_been_refreshed + ): + raise SynapseError( + 403, "refresh token isn't valid anymore", Codes.FORBIDDEN + ) + + ( + new_refresh_token, + new_refresh_token_id, + ) = await self.get_refresh_token_for_user_id( + user_id=existing_token.user_id, device_id=existing_token.device_id + ) + access_token = await self.get_access_token_for_user_id( + user_id=existing_token.user_id, + device_id=existing_token.device_id, + valid_until_ms=valid_until_ms, + refresh_token_id=new_refresh_token_id, + ) + await self.store.replace_refresh_token( + existing_token.token_id, new_refresh_token_id + ) + return access_token, new_refresh_token + + def _verify_refresh_token(self, token: str) -> bool: + """ + Verifies the shape of a refresh token. + + Args: + token: The refresh token to verify + + Returns: + Whether the token has the right shape + """ + parts = token.split("_", maxsplit=4) + if len(parts) != 4: + return False + + type, localpart, rand, crc = parts + + # Refresh tokens are prefixed by "syr_", let's check that + if type != "syr": + return False + + # Check the CRC + base = f"{type}_{localpart}_{rand}" + expected_crc = base62_encode(crc32(base.encode("ascii")), minwidth=6) + if crc != expected_crc: + return False + + return True + + async def get_refresh_token_for_user_id( + self, + user_id: str, + device_id: str, + ) -> Tuple[str, int]: + """ + Creates a new refresh token for the user with the given user ID. + + Args: + user_id: canonical user ID + device_id: the device ID to associate with the token. + + Returns: + The newly created refresh token and its ID in the database + """ + refresh_token = self.generate_refresh_token(UserID.from_string(user_id)) + refresh_token_id = await self.store.add_refresh_token_to_user( + user_id=user_id, + token=refresh_token, + device_id=device_id, + ) + return refresh_token, refresh_token_id + async def get_access_token_for_user_id( self, user_id: str, @@ -784,6 +888,7 @@ async def get_access_token_for_user_id( valid_until_ms: Optional[int], puppets_user_id: Optional[str] = None, is_appservice_ghost: bool = False, + refresh_token_id: Optional[int] = None, ) -> str: """ Creates a new access token for the user with the given user ID. @@ -801,6 +906,8 @@ async def get_access_token_for_user_id( valid_until_ms: when the token is valid until. None for no expiry. is_appservice_ghost: Whether the user is an application ghost user + refresh_token_id: the refresh token ID that will be associated with + this access token. Returns: The access token for the user's session. Raises: @@ -836,6 +943,7 @@ async def get_access_token_for_user_id( device_id=device_id, valid_until_ms=valid_until_ms, puppets_user_id=puppets_user_id, + refresh_token_id=refresh_token_id, ) # the device *should* have been registered before we got here; however, @@ -928,7 +1036,7 @@ async def validate_login( self, login_submission: Dict[str, Any], ratelimit: bool = False, - ) -> Tuple[str, Optional[Callable[[Dict[str, str]], Awaitable[None]]]]: + ) -> Tuple[str, Optional[Callable[["LoginResponse"], Awaitable[None]]]]: """Authenticates the user for the /login API Also used by the user-interactive auth flow to validate auth types which don't @@ -1073,7 +1181,7 @@ async def _validate_userid_login( self, username: str, login_submission: Dict[str, Any], - ) -> Tuple[str, Optional[Callable[[Dict[str, str]], Awaitable[None]]]]: + ) -> Tuple[str, Optional[Callable[["LoginResponse"], Awaitable[None]]]]: """Helper for validate_login Handles login, once we've mapped 3pids onto userids @@ -1151,7 +1259,7 @@ async def _validate_userid_login( async def check_password_provider_3pid( self, medium: str, address: str, password: str - ) -> Tuple[Optional[str], Optional[Callable[[Dict[str, str]], Awaitable[None]]]]: + ) -> Tuple[Optional[str], Optional[Callable[["LoginResponse"], Awaitable[None]]]]: """Check if a password provider is able to validate a thirdparty login Args: @@ -1215,6 +1323,19 @@ def generate_access_token(self, for_user: UserID) -> str: crc = base62_encode(crc32(base.encode("ascii")), minwidth=6) return f"{base}_{crc}" + def generate_refresh_token(self, for_user: UserID) -> str: + """Generates an opaque string, for use as a refresh token""" + + # we use the following format for refresh tokens: + # syr___ + + b64local = unpaddedbase64.encode_base64(for_user.localpart.encode("utf-8")) + random_string = stringutils.random_string(20) + base = f"syr_{b64local}_{random_string}" + + crc = base62_encode(crc32(base.encode("ascii")), minwidth=6) + return f"{base}_{crc}" + async def validate_short_term_login_token( self, login_token: str ) -> LoginTokenAttributes: @@ -1563,7 +1684,7 @@ def _complete_sso_login( ) respond_with_html(request, 200, html) - async def _sso_login_callback(self, login_result: JsonDict) -> None: + async def _sso_login_callback(self, login_result: "LoginResponse") -> None: """ A login callback which might add additional attributes to the login response. @@ -1577,7 +1698,8 @@ async def _sso_login_callback(self, login_result: JsonDict) -> None: extra_attributes = self._extra_attributes.get(login_result["user_id"]) if extra_attributes: - login_result.update(extra_attributes.extra_attributes) + login_result_dict = cast(Dict[str, Any], login_result) + login_result_dict.update(extra_attributes.extra_attributes) def _expire_sso_extra_attributes(self) -> None: """ diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 4b4b579741..26ef016179 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -15,9 +15,10 @@ """Contains functions for registering clients.""" import logging -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Tuple +from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple from prometheus_client import Counter +from typing_extensions import TypedDict from synapse import types from synapse.api.constants import MAX_USERID_LENGTH, EventTypes, JoinRules, LoginType @@ -54,6 +55,16 @@ ["guest", "auth_provider"], ) +LoginDict = TypedDict( + "LoginDict", + { + "device_id": str, + "access_token": str, + "valid_until_ms": Optional[int], + "refresh_token": Optional[str], + }, +) + class RegistrationHandler(BaseHandler): def __init__(self, hs: "HomeServer"): @@ -85,6 +96,7 @@ def __init__(self, hs: "HomeServer"): self.pusher_pool = hs.get_pusherpool() self.session_lifetime = hs.config.session_lifetime + self.access_token_lifetime = hs.config.access_token_lifetime async def check_username( self, @@ -696,7 +708,8 @@ async def register_device( is_guest: bool = False, is_appservice_ghost: bool = False, auth_provider_id: Optional[str] = None, - ) -> Tuple[str, str]: + should_issue_refresh_token: bool = False, + ) -> Tuple[str, str, Optional[int], Optional[str]]: """Register a device for a user and generate an access token. The access token will be limited by the homeserver's session_lifetime config. @@ -708,8 +721,9 @@ async def register_device( is_guest: Whether this is a guest account auth_provider_id: The SSO IdP the user used, if any (just used for the prometheus metrics). + should_issue_refresh_token: Whether it should also issue a refresh token Returns: - Tuple of device ID and access token + Tuple of device ID, access token, access token expiration time and refresh token """ res = await self._register_device_client( user_id=user_id, @@ -717,6 +731,7 @@ async def register_device( initial_display_name=initial_display_name, is_guest=is_guest, is_appservice_ghost=is_appservice_ghost, + should_issue_refresh_token=should_issue_refresh_token, ) login_counter.labels( @@ -724,7 +739,12 @@ async def register_device( auth_provider=(auth_provider_id or ""), ).inc() - return res["device_id"], res["access_token"] + return ( + res["device_id"], + res["access_token"], + res["valid_until_ms"], + res["refresh_token"], + ) async def register_device_inner( self, @@ -733,7 +753,8 @@ async def register_device_inner( initial_display_name: Optional[str], is_guest: bool = False, is_appservice_ghost: bool = False, - ) -> Dict[str, str]: + should_issue_refresh_token: bool = False, + ) -> LoginDict: """Helper for register_device Does the bits that need doing on the main process. Not for use outside this @@ -748,6 +769,9 @@ class and RegisterDeviceReplicationServlet. ) valid_until_ms = self.clock.time_msec() + self.session_lifetime + refresh_token = None + refresh_token_id = None + registered_device_id = await self.device_handler.check_device_registered( user_id, device_id, initial_display_name ) @@ -755,14 +779,30 @@ class and RegisterDeviceReplicationServlet. assert valid_until_ms is None access_token = self.macaroon_gen.generate_guest_access_token(user_id) else: + if should_issue_refresh_token: + ( + refresh_token, + refresh_token_id, + ) = await self._auth_handler.get_refresh_token_for_user_id( + user_id, + device_id=registered_device_id, + ) + valid_until_ms = self.clock.time_msec() + self.access_token_lifetime + access_token = await self._auth_handler.get_access_token_for_user_id( user_id, device_id=registered_device_id, valid_until_ms=valid_until_ms, is_appservice_ghost=is_appservice_ghost, + refresh_token_id=refresh_token_id, ) - return {"device_id": registered_device_id, "access_token": access_token} + return { + "device_id": registered_device_id, + "access_token": access_token, + "valid_until_ms": valid_until_ms, + "refresh_token": refresh_token, + } async def post_registration_actions( self, user_id: str, auth_result: dict, access_token: Optional[str] diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 58b255eb1b..721c45abac 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -168,7 +168,7 @@ def register(self, localpart, displayname=None, emails: Optional[List[str]] = No "Using deprecated ModuleApi.register which creates a dummy user device." ) user_id = yield self.register_user(localpart, displayname, emails or []) - _, access_token = yield self.register_device(user_id) + _, access_token, _, _ = yield self.register_device(user_id) return user_id, access_token def register_user( diff --git a/synapse/replication/http/login.py b/synapse/replication/http/login.py index c2e8c00293..550bd5c95f 100644 --- a/synapse/replication/http/login.py +++ b/synapse/replication/http/login.py @@ -36,20 +36,29 @@ def __init__(self, hs): @staticmethod async def _serialize_payload( - user_id, device_id, initial_display_name, is_guest, is_appservice_ghost + user_id, + device_id, + initial_display_name, + is_guest, + is_appservice_ghost, + should_issue_refresh_token, ): """ Args: + user_id (int) device_id (str|None): Device ID to use, if None a new one is generated. initial_display_name (str|None) is_guest (bool) + is_appservice_ghost (bool) + should_issue_refresh_token (bool) """ return { "device_id": device_id, "initial_display_name": initial_display_name, "is_guest": is_guest, "is_appservice_ghost": is_appservice_ghost, + "should_issue_refresh_token": should_issue_refresh_token, } async def _handle_request(self, request, user_id): @@ -59,6 +68,7 @@ async def _handle_request(self, request, user_id): initial_display_name = content["initial_display_name"] is_guest = content["is_guest"] is_appservice_ghost = content["is_appservice_ghost"] + should_issue_refresh_token = content["should_issue_refresh_token"] res = await self.registration_handler.register_device_inner( user_id, @@ -66,6 +76,7 @@ async def _handle_request(self, request, user_id): initial_display_name, is_guest, is_appservice_ghost=is_appservice_ghost, + should_issue_refresh_token=should_issue_refresh_token, ) return 200, res diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index f6be5f1020..cbcb60fe31 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -14,7 +14,9 @@ import logging import re -from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional + +from typing_extensions import TypedDict from synapse.api.errors import Codes, LoginError, SynapseError from synapse.api.ratelimiting import Ratelimiter @@ -25,6 +27,8 @@ from synapse.http.server import HttpServer, finish_request from synapse.http.servlet import ( RestServlet, + assert_params_in_dict, + parse_boolean, parse_bytes_from_args, parse_json_object_from_request, parse_string, @@ -40,6 +44,21 @@ logger = logging.getLogger(__name__) +LoginResponse = TypedDict( + "LoginResponse", + { + "user_id": str, + "access_token": str, + "home_server": str, + "expires_in_ms": Optional[int], + "refresh_token": Optional[str], + "device_id": str, + "well_known": Optional[Dict[str, Any]], + }, + total=False, +) + + class LoginRestServlet(RestServlet): PATTERNS = client_patterns("/login$", v1=True) CAS_TYPE = "m.login.cas" @@ -48,6 +67,7 @@ class LoginRestServlet(RestServlet): JWT_TYPE = "org.matrix.login.jwt" JWT_TYPE_DEPRECATED = "m.login.jwt" APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service" + REFRESH_TOKEN_PARAM = "org.matrix.msc2918.refresh_token" def __init__(self, hs: "HomeServer"): super().__init__() @@ -65,9 +85,12 @@ def __init__(self, hs: "HomeServer"): self.cas_enabled = hs.config.cas_enabled self.oidc_enabled = hs.config.oidc_enabled self._msc2858_enabled = hs.config.experimental.msc2858_enabled + self._msc2918_enabled = hs.config.access_token_lifetime is not None self.auth = hs.get_auth() + self.clock = hs.get_clock() + self.auth_handler = self.hs.get_auth_handler() self.registration_handler = hs.get_registration_handler() self._sso_handler = hs.get_sso_handler() @@ -138,6 +161,15 @@ def on_GET(self, request: SynapseRequest): async def on_POST(self, request: SynapseRequest): login_submission = parse_json_object_from_request(request) + if self._msc2918_enabled: + # Check if this login should also issue a refresh token, as per + # MSC2918 + should_issue_refresh_token = parse_boolean( + request, name=LoginRestServlet.REFRESH_TOKEN_PARAM, default=False + ) + else: + should_issue_refresh_token = False + try: if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE: appservice = self.auth.get_appservice_by_req(request) @@ -147,19 +179,32 @@ async def on_POST(self, request: SynapseRequest): None, request.getClientIP() ) - result = await self._do_appservice_login(login_submission, appservice) + result = await self._do_appservice_login( + login_submission, + appservice, + should_issue_refresh_token=should_issue_refresh_token, + ) elif self.jwt_enabled and ( login_submission["type"] == LoginRestServlet.JWT_TYPE or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED ): await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_jwt_login(login_submission) + result = await self._do_jwt_login( + login_submission, + should_issue_refresh_token=should_issue_refresh_token, + ) elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE: await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_token_login(login_submission) + result = await self._do_token_login( + login_submission, + should_issue_refresh_token=should_issue_refresh_token, + ) else: await self._address_ratelimiter.ratelimit(None, request.getClientIP()) - result = await self._do_other_login(login_submission) + result = await self._do_other_login( + login_submission, + should_issue_refresh_token=should_issue_refresh_token, + ) except KeyError: raise SynapseError(400, "Missing JSON keys.") @@ -169,7 +214,10 @@ async def on_POST(self, request: SynapseRequest): return 200, result async def _do_appservice_login( - self, login_submission: JsonDict, appservice: ApplicationService + self, + login_submission: JsonDict, + appservice: ApplicationService, + should_issue_refresh_token: bool = False, ): identifier = login_submission.get("identifier") logger.info("Got appservice login request with identifier: %r", identifier) @@ -198,14 +246,21 @@ async def _do_appservice_login( raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN) return await self._complete_login( - qualified_user_id, login_submission, ratelimit=appservice.is_rate_limited() + qualified_user_id, + login_submission, + ratelimit=appservice.is_rate_limited(), + should_issue_refresh_token=should_issue_refresh_token, ) - async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]: + async def _do_other_login( + self, login_submission: JsonDict, should_issue_refresh_token: bool = False + ) -> LoginResponse: """Handle non-token/saml/jwt logins Args: login_submission: + should_issue_refresh_token: True if this login should issue + a refresh token alongside the access token. Returns: HTTP response @@ -224,7 +279,10 @@ async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]: login_submission, ratelimit=True ) result = await self._complete_login( - canonical_user_id, login_submission, callback + canonical_user_id, + login_submission, + callback, + should_issue_refresh_token=should_issue_refresh_token, ) return result @@ -232,11 +290,12 @@ async def _complete_login( self, user_id: str, login_submission: JsonDict, - callback: Optional[Callable[[Dict[str, str]], Awaitable[None]]] = None, + callback: Optional[Callable[[LoginResponse], Awaitable[None]]] = None, create_non_existent_users: bool = False, ratelimit: bool = True, auth_provider_id: Optional[str] = None, - ) -> Dict[str, str]: + should_issue_refresh_token: bool = False, + ) -> LoginResponse: """Called when we've successfully authed the user and now need to actually login them in (e.g. create devices). This gets called on all successful logins. @@ -253,6 +312,8 @@ async def _complete_login( ratelimit: Whether to ratelimit the login request. auth_provider_id: The SSO IdP the user used, if any (just used for the prometheus metrics). + should_issue_refresh_token: True if this login should issue + a refresh token alongside the access token. Returns: result: Dictionary of account information after successful login. @@ -274,28 +335,48 @@ async def _complete_login( device_id = login_submission.get("device_id") initial_display_name = login_submission.get("initial_device_display_name") - device_id, access_token = await self.registration_handler.register_device( - user_id, device_id, initial_display_name, auth_provider_id=auth_provider_id + ( + device_id, + access_token, + valid_until_ms, + refresh_token, + ) = await self.registration_handler.register_device( + user_id, + device_id, + initial_display_name, + auth_provider_id=auth_provider_id, + should_issue_refresh_token=should_issue_refresh_token, ) - result = { - "user_id": user_id, - "access_token": access_token, - "home_server": self.hs.hostname, - "device_id": device_id, - } + result = LoginResponse( + user_id=user_id, + access_token=access_token, + home_server=self.hs.hostname, + device_id=device_id, + ) + + if valid_until_ms is not None: + expires_in_ms = valid_until_ms - self.clock.time_msec() + result["expires_in_ms"] = expires_in_ms + + if refresh_token is not None: + result["refresh_token"] = refresh_token if callback is not None: await callback(result) return result - async def _do_token_login(self, login_submission: JsonDict) -> Dict[str, str]: + async def _do_token_login( + self, login_submission: JsonDict, should_issue_refresh_token: bool = False + ) -> LoginResponse: """ Handle the final stage of SSO login. Args: - login_submission: The JSON request body. + login_submission: The JSON request body. + should_issue_refresh_token: True if this login should issue + a refresh token alongside the access token. Returns: The body of the JSON response. @@ -309,9 +390,12 @@ async def _do_token_login(self, login_submission: JsonDict) -> Dict[str, str]: login_submission, self.auth_handler._sso_login_callback, auth_provider_id=res.auth_provider_id, + should_issue_refresh_token=should_issue_refresh_token, ) - async def _do_jwt_login(self, login_submission: JsonDict) -> Dict[str, str]: + async def _do_jwt_login( + self, login_submission: JsonDict, should_issue_refresh_token: bool = False + ) -> LoginResponse: token = login_submission.get("token", None) if token is None: raise LoginError( @@ -342,7 +426,10 @@ async def _do_jwt_login(self, login_submission: JsonDict) -> Dict[str, str]: user_id = UserID(user, self.hs.hostname).to_string() result = await self._complete_login( - user_id, login_submission, create_non_existent_users=True + user_id, + login_submission, + create_non_existent_users=True, + should_issue_refresh_token=should_issue_refresh_token, ) return result @@ -371,6 +458,42 @@ def _get_auth_flow_dict_for_idp( return e +class RefreshTokenServlet(RestServlet): + PATTERNS = client_patterns( + "/org.matrix.msc2918.refresh_token/refresh$", releases=(), unstable=True + ) + + def __init__(self, hs: "HomeServer"): + self._auth_handler = hs.get_auth_handler() + self._clock = hs.get_clock() + self.access_token_lifetime = hs.config.access_token_lifetime + + async def on_POST( + self, + request: SynapseRequest, + ): + refresh_submission = parse_json_object_from_request(request) + + assert_params_in_dict(refresh_submission, ["refresh_token"]) + token = refresh_submission["refresh_token"] + if not isinstance(token, str): + raise SynapseError(400, "Invalid param: refresh_token", Codes.INVALID_PARAM) + + valid_until_ms = self._clock.time_msec() + self.access_token_lifetime + access_token, refresh_token = await self._auth_handler.refresh_token( + token, valid_until_ms + ) + expires_in_ms = valid_until_ms - self._clock.time_msec() + return ( + 200, + { + "access_token": access_token, + "refresh_token": refresh_token, + "expires_in_ms": expires_in_ms, + }, + ) + + class SsoRedirectServlet(RestServlet): PATTERNS = list(client_patterns("/login/(cas|sso)/redirect$", v1=True)) + [ re.compile( @@ -477,6 +600,8 @@ async def on_GET(self, request: SynapseRequest) -> None: def register_servlets(hs, http_server): LoginRestServlet(hs).register(http_server) + if hs.config.access_token_lifetime is not None: + RefreshTokenServlet(hs).register(http_server) SsoRedirectServlet(hs).register(http_server) if hs.config.cas_enabled: CasTicketServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index a30a5df1b1..4d31584acd 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -41,11 +41,13 @@ from synapse.http.servlet import ( RestServlet, assert_params_in_dict, + parse_boolean, parse_json_object_from_request, parse_string, ) from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer +from synapse.types import JsonDict from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.ratelimitutils import FederationRateLimiter from synapse.util.stringutils import assert_valid_client_secret, random_string @@ -399,6 +401,7 @@ def __init__(self, hs): self.password_policy_handler = hs.get_password_policy_handler() self.clock = hs.get_clock() self._registration_enabled = self.hs.config.enable_registration + self._msc2918_enabled = hs.config.access_token_lifetime is not None self._registration_flows = _calculate_registration_flows( hs.config, self.auth_handler @@ -424,6 +427,15 @@ async def on_POST(self, request): "Do not understand membership kind: %s" % (kind.decode("utf8"),) ) + if self._msc2918_enabled: + # Check if this registration should also issue a refresh token, as + # per MSC2918 + should_issue_refresh_token = parse_boolean( + request, name="org.matrix.msc2918.refresh_token", default=False + ) + else: + should_issue_refresh_token = False + # Pull out the provided username and do basic sanity checks early since # the auth layer will store these in sessions. desired_username = None @@ -462,7 +474,10 @@ async def on_POST(self, request): raise SynapseError(400, "Desired Username is missing or not a string") result = await self._do_appservice_registration( - desired_username, access_token, body + desired_username, + access_token, + body, + should_issue_refresh_token=should_issue_refresh_token, ) return 200, result @@ -665,7 +680,9 @@ async def on_POST(self, request): registered = True return_dict = await self._create_registration_details( - registered_user_id, params + registered_user_id, + params, + should_issue_refresh_token=should_issue_refresh_token, ) if registered: @@ -677,7 +694,9 @@ async def on_POST(self, request): return 200, return_dict - async def _do_appservice_registration(self, username, as_token, body): + async def _do_appservice_registration( + self, username, as_token, body, should_issue_refresh_token: bool = False + ): user_id = await self.registration_handler.appservice_register( username, as_token ) @@ -685,19 +704,27 @@ async def _do_appservice_registration(self, username, as_token, body): user_id, body, is_appservice_ghost=True, + should_issue_refresh_token=should_issue_refresh_token, ) async def _create_registration_details( - self, user_id, params, is_appservice_ghost=False + self, + user_id: str, + params: JsonDict, + is_appservice_ghost: bool = False, + should_issue_refresh_token: bool = False, ): """Complete registration of newly-registered user Allocates device_id if one was not given; also creates access_token. Args: - (str) user_id: full canonical @user:id - (object) params: registration parameters, from which we pull - device_id, initial_device_name and inhibit_login + user_id: full canonical @user:id + params: registration parameters, from which we pull device_id, + initial_device_name and inhibit_login + is_appservice_ghost + should_issue_refresh_token: True if this registration should issue + a refresh token alongside the access token. Returns: dictionary for response from /register """ @@ -705,15 +732,29 @@ async def _create_registration_details( if not params.get("inhibit_login", False): device_id = params.get("device_id") initial_display_name = params.get("initial_device_display_name") - device_id, access_token = await self.registration_handler.register_device( + ( + device_id, + access_token, + valid_until_ms, + refresh_token, + ) = await self.registration_handler.register_device( user_id, device_id, initial_display_name, is_guest=False, is_appservice_ghost=is_appservice_ghost, + should_issue_refresh_token=should_issue_refresh_token, ) result.update({"access_token": access_token, "device_id": device_id}) + + if valid_until_ms is not None: + expires_in_ms = valid_until_ms - self.clock.time_msec() + result["expires_in_ms"] = expires_in_ms + + if refresh_token is not None: + result["refresh_token"] = refresh_token + return result async def _do_guest_registration(self, params, address=None): @@ -727,19 +768,30 @@ async def _do_guest_registration(self, params, address=None): # we have nowhere to store it. device_id = synapse.api.auth.GUEST_DEVICE_ID initial_display_name = params.get("initial_device_display_name") - device_id, access_token = await self.registration_handler.register_device( + ( + device_id, + access_token, + valid_until_ms, + refresh_token, + ) = await self.registration_handler.register_device( user_id, device_id, initial_display_name, is_guest=True ) - return ( - 200, - { - "user_id": user_id, - "device_id": device_id, - "access_token": access_token, - "home_server": self.hs.hostname, - }, - ) + result = { + "user_id": user_id, + "device_id": device_id, + "access_token": access_token, + "home_server": self.hs.hostname, + } + + if valid_until_ms is not None: + expires_in_ms = valid_until_ms - self.clock.time_msec() + result["expires_in_ms"] = expires_in_ms + + if refresh_token is not None: + result["refresh_token"] = refresh_token + + return 200, result def _calculate_registration_flows( diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index e5c5cf8ff0..e31c5864ac 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -53,6 +53,9 @@ class TokenLookupResult: valid_until_ms: The timestamp the token expires, if any. token_owner: The "owner" of the token. This is either the same as the user, or a server admin who is logged in as the user. + token_used: True if this token was used at least once in a request. + This field can be out of date since `get_user_by_access_token` is + cached. """ user_id = attr.ib(type=str) @@ -62,6 +65,7 @@ class TokenLookupResult: device_id = attr.ib(type=Optional[str], default=None) valid_until_ms = attr.ib(type=Optional[int], default=None) token_owner = attr.ib(type=str) + token_used = attr.ib(type=bool, default=False) # Make the token owner default to the user ID, which is the common case. @token_owner.default @@ -69,6 +73,29 @@ def _default_token_owner(self): return self.user_id +@attr.s(frozen=True, slots=True) +class RefreshTokenLookupResult: + """Result of looking up a refresh token.""" + + user_id = attr.ib(type=str) + """The user this token belongs to.""" + + device_id = attr.ib(type=str) + """The device associated with this refresh token.""" + + token_id = attr.ib(type=int) + """The ID of this refresh token.""" + + next_token_id = attr.ib(type=Optional[int]) + """The ID of the refresh token which replaced this one.""" + + has_next_refresh_token_been_refreshed = attr.ib(type=bool) + """True if the next refresh token was used for another refresh.""" + + has_next_access_token_been_used = attr.ib(type=bool) + """True if the next access token was already used at least once.""" + + class RegistrationWorkerStore(CacheInvalidationWorkerStore): def __init__( self, @@ -441,7 +468,8 @@ def _query_for_auth(self, txn, token: str) -> Optional[TokenLookupResult]: access_tokens.id as token_id, access_tokens.device_id, access_tokens.valid_until_ms, - access_tokens.user_id as token_owner + access_tokens.user_id as token_owner, + access_tokens.used as token_used FROM users INNER JOIN access_tokens on users.name = COALESCE(puppets_user_id, access_tokens.user_id) WHERE token = ? @@ -449,8 +477,15 @@ def _query_for_auth(self, txn, token: str) -> Optional[TokenLookupResult]: txn.execute(sql, (token,)) rows = self.db_pool.cursor_to_dict(txn) + if rows: - return TokenLookupResult(**rows[0]) + row = rows[0] + + # This field is nullable, ensure it comes out as a boolean + if row["token_used"] is None: + row["token_used"] = False + + return TokenLookupResult(**row) return None @@ -1072,6 +1107,111 @@ async def update_access_token_last_validated(self, token_id: int) -> None: desc="update_access_token_last_validated", ) + @cached() + async def mark_access_token_as_used(self, token_id: int) -> None: + """ + Mark the access token as used, which invalidates the refresh token used + to obtain it. + + Because get_user_by_access_token is cached, this function might be + called multiple times for the same token, effectively doing unnecessary + SQL updates. Because updating the `used` field only goes one way (from + False to True) it is safe to cache this function as well to avoid this + issue. + + Args: + token_id: The ID of the access token to update. + Raises: + StoreError if there was a problem updating this. + """ + await self.db_pool.simple_update_one( + "access_tokens", + {"id": token_id}, + {"used": True}, + desc="mark_access_token_as_used", + ) + + async def lookup_refresh_token( + self, token: str + ) -> Optional[RefreshTokenLookupResult]: + """Lookup a refresh token with hints about its validity.""" + + def _lookup_refresh_token_txn(txn) -> Optional[RefreshTokenLookupResult]: + txn.execute( + """ + SELECT + rt.id token_id, + rt.user_id, + rt.device_id, + rt.next_token_id, + (nrt.next_token_id IS NOT NULL) has_next_refresh_token_been_refreshed, + at.used has_next_access_token_been_used + FROM refresh_tokens rt + LEFT JOIN refresh_tokens nrt ON rt.next_token_id = nrt.id + LEFT JOIN access_tokens at ON at.refresh_token_id = nrt.id + WHERE rt.token = ? + """, + (token,), + ) + row = txn.fetchone() + + if row is None: + return None + + return RefreshTokenLookupResult( + token_id=row[0], + user_id=row[1], + device_id=row[2], + next_token_id=row[3], + has_next_refresh_token_been_refreshed=row[4], + # This column is nullable, ensure it's a boolean + has_next_access_token_been_used=(row[5] or False), + ) + + return await self.db_pool.runInteraction( + "lookup_refresh_token", _lookup_refresh_token_txn + ) + + async def replace_refresh_token(self, token_id: int, next_token_id: int) -> None: + """ + Set the successor of a refresh token, removing the existing successor + if any. + + Args: + token_id: ID of the refresh token to update. + next_token_id: ID of its successor. + """ + + def _replace_refresh_token_txn(txn) -> None: + # First check if there was an existing refresh token + old_next_token_id = self.db_pool.simple_select_one_onecol_txn( + txn, + "refresh_tokens", + {"id": token_id}, + "next_token_id", + allow_none=True, + ) + + self.db_pool.simple_update_one_txn( + txn, + "refresh_tokens", + {"id": token_id}, + {"next_token_id": next_token_id}, + ) + + # Delete the old "next" token if it exists. This should cascade and + # delete the associated access_token + if old_next_token_id is not None: + self.db_pool.simple_delete_one_txn( + txn, + "refresh_tokens", + {"id": old_next_token_id}, + ) + + await self.db_pool.runInteraction( + "replace_refresh_token", _replace_refresh_token_txn + ) + class RegistrationBackgroundUpdateStore(RegistrationWorkerStore): def __init__( @@ -1263,6 +1403,7 @@ def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer" self._ignore_unknown_session_error = hs.config.request_token_inhibit_3pid_errors self._access_tokens_id_gen = IdGenerator(db_conn, "access_tokens", "id") + self._refresh_tokens_id_gen = IdGenerator(db_conn, "refresh_tokens", "id") async def add_access_token_to_user( self, @@ -1271,14 +1412,18 @@ async def add_access_token_to_user( device_id: Optional[str], valid_until_ms: Optional[int], puppets_user_id: Optional[str] = None, + refresh_token_id: Optional[int] = None, ) -> int: """Adds an access token for the given user. Args: user_id: The user ID. token: The new access token to add. - device_id: ID of the device to associate with the access token + device_id: ID of the device to associate with the access token. valid_until_ms: when the token is valid until. None for no expiry. + puppets_user_id + refresh_token_id: ID of the refresh token generated alongside this + access token. Raises: StoreError if there was a problem adding this. Returns: @@ -1297,12 +1442,47 @@ async def add_access_token_to_user( "valid_until_ms": valid_until_ms, "puppets_user_id": puppets_user_id, "last_validated": now, + "refresh_token_id": refresh_token_id, + "used": False, }, desc="add_access_token_to_user", ) return next_id + async def add_refresh_token_to_user( + self, + user_id: str, + token: str, + device_id: Optional[str], + ) -> int: + """Adds a refresh token for the given user. + + Args: + user_id: The user ID. + token: The new access token to add. + device_id: ID of the device to associate with the refresh token. + Raises: + StoreError if there was a problem adding this. + Returns: + The token ID + """ + next_id = self._refresh_tokens_id_gen.get_next() + + await self.db_pool.simple_insert( + "refresh_tokens", + { + "id": next_id, + "user_id": user_id, + "device_id": device_id, + "token": token, + "next_token_id": None, + }, + desc="add_refresh_token_to_user", + ) + + return next_id + def _set_device_for_access_token_txn(self, txn, token: str, device_id: str) -> str: old_device_id = self.db_pool.simple_select_one_onecol_txn( txn, "access_tokens", {"token": token}, "device_id" @@ -1545,7 +1725,7 @@ async def user_delete_access_tokens( device_id: Optional[str] = None, ) -> List[Tuple[str, int, Optional[str]]]: """ - Invalidate access tokens belonging to a user + Invalidate access and refresh tokens belonging to a user Args: user_id: ID of user the tokens belong to @@ -1565,7 +1745,13 @@ def f(txn): items = keyvalues.items() where_clause = " AND ".join(k + " = ?" for k, _ in items) values = [v for _, v in items] # type: List[Union[str, int]] + # Conveniently, refresh_tokens and access_tokens both use the user_id and device_id fields. Only caveat + # is the `except_token_id` param that is tricky to get right, so for now we're just using the same where + # clause and values before we handle that. This seems to be only used in the "set password" handler. + refresh_where_clause = where_clause + refresh_values = values.copy() if except_token_id: + # TODO: support that for refresh tokens where_clause += " AND id != ?" values.append(except_token_id) @@ -1583,6 +1769,11 @@ def f(txn): txn.execute("DELETE FROM access_tokens WHERE %s" % where_clause, values) + txn.execute( + "DELETE FROM refresh_tokens WHERE %s" % refresh_where_clause, + refresh_values, + ) + return tokens_and_devices return await self.db_pool.runInteraction("user_delete_access_tokens", f) @@ -1599,6 +1790,14 @@ def f(txn): await self.db_pool.runInteraction("delete_access_token", f) + async def delete_refresh_token(self, refresh_token: str) -> None: + def f(txn): + self.db_pool.simple_delete_one_txn( + txn, table="refresh_tokens", keyvalues={"token": refresh_token} + ) + + await self.db_pool.runInteraction("delete_refresh_token", f) + async def add_user_pending_deactivation(self, user_id: str) -> None: """ Adds a user to the table of users who need to be parted from all the rooms they're diff --git a/synapse/storage/schema/main/delta/59/14refresh_tokens.sql b/synapse/storage/schema/main/delta/59/14refresh_tokens.sql new file mode 100644 index 0000000000..9a6bce1e3e --- /dev/null +++ b/synapse/storage/schema/main/delta/59/14refresh_tokens.sql @@ -0,0 +1,34 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Holds MSC2918 refresh tokens +CREATE TABLE refresh_tokens ( + id BIGINT PRIMARY KEY, + user_id TEXT NOT NULL, + device_id TEXT NOT NULL, + token TEXT NOT NULL, + -- When consumed, a new refresh token is generated, which is tracked by + -- this foreign key + next_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, + UNIQUE(token) +); + +-- Add a reference to the refresh token generated alongside each access token +ALTER TABLE "access_tokens" + ADD COLUMN refresh_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE; + +-- Add a flag whether the token was already used or not +ALTER TABLE "access_tokens" + ADD COLUMN used BOOLEAN; diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 1b0a815757..f76fea4f66 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -58,6 +58,7 @@ def test_get_user_by_req_user_valid_token(self): user_id=self.test_user, token_id=5, device_id="device" ) self.store.get_user_by_access_token = simple_async_mock(user_info) + self.store.mark_access_token_as_used = simple_async_mock(None) request = Mock(args={}) request.args[b"access_token"] = [self.test_token] diff --git a/tests/handlers/test_device.py b/tests/handlers/test_device.py index 84c38b295d..3ac48e5e95 100644 --- a/tests/handlers/test_device.py +++ b/tests/handlers/test_device.py @@ -257,7 +257,7 @@ def test_dehydrate_and_rehydrate_device(self): self.assertEqual(device_data, {"device_data": {"foo": "bar"}}) # Create a new login for the user and dehydrated the device - device_id, access_token = self.get_success( + device_id, access_token, _expiration_time, _refresh_token = self.get_success( self.registration.register_device( user_id=user_id, device_id=None, diff --git a/tests/rest/client/v2_alpha/test_auth.py b/tests/rest/client/v2_alpha/test_auth.py index 485e3650c3..6b90f838b6 100644 --- a/tests/rest/client/v2_alpha/test_auth.py +++ b/tests/rest/client/v2_alpha/test_auth.py @@ -20,7 +20,7 @@ from synapse.api.constants import LoginType from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker from synapse.rest.client.v1 import login -from synapse.rest.client.v2_alpha import auth, devices, register +from synapse.rest.client.v2_alpha import account, auth, devices, register from synapse.rest.synapse.client import build_synapse_client_resource_tree from synapse.types import JsonDict, UserID @@ -498,3 +498,221 @@ def test_ui_auth_fails_for_incorrect_sso_user(self): self.delete_device( self.user_tok, self.device_id, 403, body={"auth": {"session": session_id}} ) + + +class RefreshAuthTests(unittest.HomeserverTestCase): + servlets = [ + auth.register_servlets, + account.register_servlets, + login.register_servlets, + synapse.rest.admin.register_servlets_for_client_rest_resource, + register.register_servlets, + ] + hijack_auth = False + + def prepare(self, reactor, clock, hs): + self.user_pass = "pass" + self.user = self.register_user("test", self.user_pass) + + def test_login_issue_refresh_token(self): + """ + A login response should include a refresh_token only if asked. + """ + # Test login + body = {"type": "m.login.password", "user": "test", "password": self.user_pass} + + login_without_refresh = self.make_request( + "POST", "/_matrix/client/r0/login", body + ) + self.assertEqual(login_without_refresh.code, 200, login_without_refresh.result) + self.assertNotIn("refresh_token", login_without_refresh.json_body) + + login_with_refresh = self.make_request( + "POST", + "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true", + body, + ) + self.assertEqual(login_with_refresh.code, 200, login_with_refresh.result) + self.assertIn("refresh_token", login_with_refresh.json_body) + self.assertIn("expires_in_ms", login_with_refresh.json_body) + + def test_register_issue_refresh_token(self): + """ + A register response should include a refresh_token only if asked. + """ + register_without_refresh = self.make_request( + "POST", + "/_matrix/client/r0/register", + { + "username": "test2", + "password": self.user_pass, + "auth": {"type": LoginType.DUMMY}, + }, + ) + self.assertEqual( + register_without_refresh.code, 200, register_without_refresh.result + ) + self.assertNotIn("refresh_token", register_without_refresh.json_body) + + register_with_refresh = self.make_request( + "POST", + "/_matrix/client/r0/register?org.matrix.msc2918.refresh_token=true", + { + "username": "test3", + "password": self.user_pass, + "auth": {"type": LoginType.DUMMY}, + }, + ) + self.assertEqual(register_with_refresh.code, 200, register_with_refresh.result) + self.assertIn("refresh_token", register_with_refresh.json_body) + self.assertIn("expires_in_ms", register_with_refresh.json_body) + + def test_token_refresh(self): + """ + A refresh token can be used to issue a new access token. + """ + body = {"type": "m.login.password", "user": "test", "password": self.user_pass} + login_response = self.make_request( + "POST", + "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true", + body, + ) + self.assertEqual(login_response.code, 200, login_response.result) + + refresh_response = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh", + {"refresh_token": login_response.json_body["refresh_token"]}, + ) + self.assertEqual(refresh_response.code, 200, refresh_response.result) + self.assertIn("access_token", refresh_response.json_body) + self.assertIn("refresh_token", refresh_response.json_body) + self.assertIn("expires_in_ms", refresh_response.json_body) + + # The access and refresh tokens should be different from the original ones after refresh + self.assertNotEqual( + login_response.json_body["access_token"], + refresh_response.json_body["access_token"], + ) + self.assertNotEqual( + login_response.json_body["refresh_token"], + refresh_response.json_body["refresh_token"], + ) + + @override_config({"access_token_lifetime": "1m"}) + def test_refresh_token_expiration(self): + """ + The access token should have some time as specified in the config. + """ + body = {"type": "m.login.password", "user": "test", "password": self.user_pass} + login_response = self.make_request( + "POST", + "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true", + body, + ) + self.assertEqual(login_response.code, 200, login_response.result) + self.assertApproximates( + login_response.json_body["expires_in_ms"], 60 * 1000, 100 + ) + + refresh_response = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh", + {"refresh_token": login_response.json_body["refresh_token"]}, + ) + self.assertEqual(refresh_response.code, 200, refresh_response.result) + self.assertApproximates( + refresh_response.json_body["expires_in_ms"], 60 * 1000, 100 + ) + + def test_refresh_token_invalidation(self): + """Refresh tokens are invalidated after first use of the next token. + + A refresh token is considered invalid if: + - it was already used at least once + - and either + - the next access token was used + - the next refresh token was used + + The chain of tokens goes like this: + + login -|-> first_refresh -> third_refresh (fails) + |-> second_refresh -> fifth_refresh + |-> fourth_refresh (fails) + """ + + body = {"type": "m.login.password", "user": "test", "password": self.user_pass} + login_response = self.make_request( + "POST", + "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true", + body, + ) + self.assertEqual(login_response.code, 200, login_response.result) + + # This first refresh should work properly + first_refresh_response = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh", + {"refresh_token": login_response.json_body["refresh_token"]}, + ) + self.assertEqual( + first_refresh_response.code, 200, first_refresh_response.result + ) + + # This one as well, since the token in the first one was never used + second_refresh_response = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh", + {"refresh_token": login_response.json_body["refresh_token"]}, + ) + self.assertEqual( + second_refresh_response.code, 200, second_refresh_response.result + ) + + # This one should not, since the token from the first refresh is not valid anymore + third_refresh_response = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh", + {"refresh_token": first_refresh_response.json_body["refresh_token"]}, + ) + self.assertEqual( + third_refresh_response.code, 401, third_refresh_response.result + ) + + # The associated access token should also be invalid + whoami_response = self.make_request( + "GET", + "/_matrix/client/r0/account/whoami", + access_token=first_refresh_response.json_body["access_token"], + ) + self.assertEqual(whoami_response.code, 401, whoami_response.result) + + # But all other tokens should work (they will expire after some time) + for access_token in [ + second_refresh_response.json_body["access_token"], + login_response.json_body["access_token"], + ]: + whoami_response = self.make_request( + "GET", "/_matrix/client/r0/account/whoami", access_token=access_token + ) + self.assertEqual(whoami_response.code, 200, whoami_response.result) + + # Now that the access token from the last valid refresh was used once, refreshing with the N-1 token should fail + fourth_refresh_response = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh", + {"refresh_token": login_response.json_body["refresh_token"]}, + ) + self.assertEqual( + fourth_refresh_response.code, 403, fourth_refresh_response.result + ) + + # But refreshing from the last valid refresh token still works + fifth_refresh_response = self.make_request( + "POST", + "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh", + {"refresh_token": second_refresh_response.json_body["refresh_token"]}, + ) + self.assertEqual( + fifth_refresh_response.code, 200, fifth_refresh_response.result + ) From 6e8fb42be7657f9d4958c02d87cff865225714d2 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 24 Jun 2021 15:30:49 +0100 Subject: [PATCH 322/619] Improve validation for `send_{join,leave,knock}` (#10225) The idea here is to stop people sending things that aren't joins/leaves/knocks through these endpoints: previously you could send anything you liked through them. I wasn't able to find any security holes from doing so, but it doesn't sound like a good thing. --- changelog.d/10225.feature | 1 + synapse/federation/federation_server.py | 121 +++++++----- synapse/federation/transport/server.py | 12 +- synapse/handlers/federation.py | 177 +++++------------- tests/handlers/test_federation.py | 2 +- .../test_federation_sender_shard.py | 2 +- 6 files changed, 132 insertions(+), 183 deletions(-) create mode 100644 changelog.d/10225.feature diff --git a/changelog.d/10225.feature b/changelog.d/10225.feature new file mode 100644 index 0000000000..d16f66ffe9 --- /dev/null +++ b/changelog.d/10225.feature @@ -0,0 +1 @@ +Improve validation on federation `send_{join,leave,knock}` endpoints. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 2b07f18529..341965047a 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -34,7 +34,7 @@ from twisted.internet.abstract import isIPAddress from twisted.python import failure -from synapse.api.constants import EduTypes, EventTypes +from synapse.api.constants import EduTypes, EventTypes, Membership from synapse.api.errors import ( AuthError, Codes, @@ -46,6 +46,7 @@ ) from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.events import EventBase +from synapse.events.snapshot import EventContext from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.federation.persistence import TransactionActions from synapse.federation.units import Edu, Transaction @@ -537,26 +538,21 @@ async def on_invite_request( return {"event": ret_pdu.get_pdu_json(time_now)} async def on_send_join_request( - self, origin: str, content: JsonDict + self, origin: str, content: JsonDict, room_id: str ) -> Dict[str, Any]: - logger.debug("on_send_join_request: content: %s", content) - - assert_params_in_dict(content, ["room_id"]) - room_version = await self.store.get_room_version(content["room_id"]) - pdu = event_from_pdu_json(content, room_version) - - origin_host, _ = parse_server_name(origin) - await self.check_server_matches_acl(origin_host, pdu.room_id) - - logger.debug("on_send_join_request: pdu sigs: %s", pdu.signatures) + context = await self._on_send_membership_event( + origin, content, Membership.JOIN, room_id + ) - pdu = await self._check_sigs_and_hash(room_version, pdu) + prev_state_ids = await context.get_prev_state_ids() + state_ids = list(prev_state_ids.values()) + auth_chain = await self.store.get_auth_chain(room_id, state_ids) + state = await self.store.get_events(state_ids) - res_pdus = await self.handler.on_send_join_request(origin, pdu) time_now = self._clock.time_msec() return { - "state": [p.get_pdu_json(time_now) for p in res_pdus["state"]], - "auth_chain": [p.get_pdu_json(time_now) for p in res_pdus["auth_chain"]], + "state": [p.get_pdu_json(time_now) for p in state.values()], + "auth_chain": [p.get_pdu_json(time_now) for p in auth_chain], } async def on_make_leave_request( @@ -571,21 +567,11 @@ async def on_make_leave_request( time_now = self._clock.time_msec() return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} - async def on_send_leave_request(self, origin: str, content: JsonDict) -> dict: + async def on_send_leave_request( + self, origin: str, content: JsonDict, room_id: str + ) -> dict: logger.debug("on_send_leave_request: content: %s", content) - - assert_params_in_dict(content, ["room_id"]) - room_version = await self.store.get_room_version(content["room_id"]) - pdu = event_from_pdu_json(content, room_version) - - origin_host, _ = parse_server_name(origin) - await self.check_server_matches_acl(origin_host, pdu.room_id) - - logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures) - - pdu = await self._check_sigs_and_hash(room_version, pdu) - - await self.handler.on_send_leave_request(origin, pdu) + await self._on_send_membership_event(origin, content, Membership.LEAVE, room_id) return {} async def on_make_knock_request( @@ -651,39 +637,76 @@ async def on_send_knock_request( Returns: The stripped room state. """ - logger.debug("on_send_knock_request: content: %s", content) + event_context = await self._on_send_membership_event( + origin, content, Membership.KNOCK, room_id + ) + + # Retrieve stripped state events from the room and send them back to the remote + # server. This will allow the remote server's clients to display information + # related to the room while the knock request is pending. + stripped_room_state = ( + await self.store.get_stripped_room_state_from_event_context( + event_context, self._room_prejoin_state_types + ) + ) + return {"knock_state_events": stripped_room_state} + + async def _on_send_membership_event( + self, origin: str, content: JsonDict, membership_type: str, room_id: str + ) -> EventContext: + """Handle an on_send_{join,leave,knock} request + + Does some preliminary validation before passing the request on to the + federation handler. + + Args: + origin: The (authenticated) requesting server + content: The body of the send_* request - a complete membership event + membership_type: The expected membership type (join or leave, depending + on the endpoint) + room_id: The room_id from the request, to be validated against the room_id + in the event + + Returns: + The context of the event after inserting it into the room graph. + + Raises: + SynapseError if there is a problem with the request, including things like + the room_id not matching or the event not being authorized. + """ + assert_params_in_dict(content, ["room_id"]) + if content["room_id"] != room_id: + raise SynapseError( + 400, + "Room ID in body does not match that in request path", + Codes.BAD_JSON, + ) room_version = await self.store.get_room_version(room_id) - # Check that this room supports knocking as defined by its room version - if not room_version.msc2403_knocking: + if membership_type == Membership.KNOCK and not room_version.msc2403_knocking: raise SynapseError( 403, "This room version does not support knocking", errcode=Codes.FORBIDDEN, ) - pdu = event_from_pdu_json(content, room_version) + event = event_from_pdu_json(content, room_version) - origin_host, _ = parse_server_name(origin) - await self.check_server_matches_acl(origin_host, pdu.room_id) + if event.type != EventTypes.Member or not event.is_state(): + raise SynapseError(400, "Not an m.room.member event", Codes.BAD_JSON) - logger.debug("on_send_knock_request: pdu sigs: %s", pdu.signatures) + if event.content.get("membership") != membership_type: + raise SynapseError(400, "Not a %s event" % membership_type, Codes.BAD_JSON) - pdu = await self._check_sigs_and_hash(room_version, pdu) + origin_host, _ = parse_server_name(origin) + await self.check_server_matches_acl(origin_host, event.room_id) - # Handle the event, and retrieve the EventContext - event_context = await self.handler.on_send_knock_request(origin, pdu) + logger.debug("_on_send_membership_event: pdu sigs: %s", event.signatures) - # Retrieve stripped state events from the room and send them back to the remote - # server. This will allow the remote server's clients to display information - # related to the room while the knock request is pending. - stripped_room_state = ( - await self.store.get_stripped_room_state_from_event_context( - event_context, self._room_prejoin_state_types - ) - ) - return {"knock_state_events": stripped_room_state} + event = await self._check_sigs_and_hash(room_version, event) + + return await self.handler.on_send_membership_event(origin, event) async def on_event_auth( self, origin: str, room_id: str, event_id: str diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index bed47f8abd..676fbd3750 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -553,7 +553,7 @@ class FederationV1SendLeaveServlet(BaseFederationServerServlet): PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id, event_id): - content = await self.handler.on_send_leave_request(origin, content) + content = await self.handler.on_send_leave_request(origin, content, room_id) return 200, (200, content) @@ -563,7 +563,7 @@ class FederationV2SendLeaveServlet(BaseFederationServerServlet): PREFIX = FEDERATION_V2_PREFIX async def on_PUT(self, origin, content, query, room_id, event_id): - content = await self.handler.on_send_leave_request(origin, content) + content = await self.handler.on_send_leave_request(origin, content, room_id) return 200, content @@ -602,9 +602,9 @@ class FederationV1SendJoinServlet(BaseFederationServerServlet): PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" async def on_PUT(self, origin, content, query, room_id, event_id): - # TODO(paul): assert that room_id/event_id parsed from path actually + # TODO(paul): assert that event_id parsed from path actually # match those given in content - content = await self.handler.on_send_join_request(origin, content) + content = await self.handler.on_send_join_request(origin, content, room_id) return 200, (200, content) @@ -614,9 +614,9 @@ class FederationV2SendJoinServlet(BaseFederationServerServlet): PREFIX = FEDERATION_V2_PREFIX async def on_PUT(self, origin, content, query, room_id, event_id): - # TODO(paul): assert that room_id/event_id parsed from path actually + # TODO(paul): assert that event_id parsed from path actually # match those given in content - content = await self.handler.on_send_join_request(origin, content) + content = await self.handler.on_send_join_request(origin, content, room_id) return 200, content diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 74d169a2ac..12f3d85342 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1711,80 +1711,6 @@ async def on_make_join_request( return event - async def on_send_join_request(self, origin: str, pdu: EventBase) -> JsonDict: - """We have received a join event for a room. Fully process it and - respond with the current state and auth chains. - """ - event = pdu - - logger.debug( - "on_send_join_request from %s: Got event: %s, signatures: %s", - origin, - event.event_id, - event.signatures, - ) - - if get_domain_from_id(event.sender) != origin: - logger.info( - "Got /send_join request for user %r from different origin %s", - event.sender, - origin, - ) - raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) - - event.internal_metadata.outlier = False - # Send this event on behalf of the origin server. - # - # The reasons we have the destination server rather than the origin - # server send it are slightly mysterious: the origin server should have - # all the necessary state once it gets the response to the send_join, - # so it could send the event itself if it wanted to. It may be that - # doing it this way reduces failure modes, or avoids certain attacks - # where a new server selectively tells a subset of the federation that - # it has joined. - # - # The fact is that, as of the current writing, Synapse doesn't send out - # the join event over federation after joining, and changing it now - # would introduce the danger of backwards-compatibility problems. - event.internal_metadata.send_on_behalf_of = origin - - # Calculate the event context. - context = await self.state_handler.compute_event_context(event) - - # Get the state before the new event. - prev_state_ids = await context.get_prev_state_ids() - - # Check if the user is already in the room or invited to the room. - user_id = event.state_key - prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None) - prev_member_event = None - if prev_member_event_id: - prev_member_event = await self.store.get_event(prev_member_event_id) - - # Check if the member should be allowed access via membership in a space. - await self._event_auth_handler.check_restricted_join_rules( - prev_state_ids, - event.room_version, - user_id, - prev_member_event, - ) - - # Persist the event. - await self._auth_and_persist_event(origin, event, context) - - logger.debug( - "on_send_join_request: After _auth_and_persist_event: %s, sigs: %s", - event.event_id, - event.signatures, - ) - - state_ids = list(prev_state_ids.values()) - auth_chain = await self.store.get_auth_chain(event.room_id, state_ids) - - state = await self.store.get_events(list(prev_state_ids.values())) - - return {"state": list(state.values()), "auth_chain": auth_chain} - async def on_invite_request( self, origin: str, event: EventBase, room_version: RoomVersion ) -> EventBase: @@ -1960,44 +1886,6 @@ async def on_make_leave_request( return event - async def on_send_leave_request(self, origin: str, pdu: EventBase) -> None: - """We have received a leave event for a room. Fully process it.""" - event = pdu - - logger.debug( - "on_send_leave_request: Got event: %s, signatures: %s", - event.event_id, - event.signatures, - ) - - if get_domain_from_id(event.sender) != origin: - logger.info( - "Got /send_leave request for user %r from different origin %s", - event.sender, - origin, - ) - raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) - - event.internal_metadata.outlier = False - - # Send this event on behalf of the other server. - # - # The remote server isn't a full participant in the room at this point, so - # may not have an up-to-date list of the other homeservers participating in - # the room, so we send it on their behalf. - event.internal_metadata.send_on_behalf_of = origin - - context = await self.state_handler.compute_event_context(event) - await self._auth_and_persist_event(origin, event, context) - - logger.debug( - "on_send_leave_request: After _auth_and_persist_event: %s, sigs: %s", - event.event_id, - event.signatures, - ) - - return None - @log_function async def on_make_knock_request( self, origin: str, room_id: str, user_id: str @@ -2061,34 +1949,38 @@ async def on_make_knock_request( return event @log_function - async def on_send_knock_request( + async def on_send_membership_event( self, origin: str, event: EventBase ) -> EventContext: """ - We have received a knock event for a room. Verify that event and send it into the room - on the knocking homeserver's behalf. + We have received a join/leave/knock event for a room. + + Verify that event and send it into the room on the remote homeserver's behalf. Args: - origin: The remote homeserver of the knocking user. - event: The knocking member event that has been signed by the remote homeserver. + origin: The homeserver of the remote (joining/invited/knocking) user. + event: The member event that has been signed by the remote homeserver. Returns: The context of the event after inserting it into the room graph. """ logger.debug( - "on_send_knock_request: Got event: %s, signatures: %s", + "on_send_membership_event: Got event: %s, signatures: %s", event.event_id, event.signatures, ) if get_domain_from_id(event.sender) != origin: logger.info( - "Got /send_knock request for user %r from different origin %s", + "Got send_membership request for user %r from different origin %s", event.sender, origin, ) raise SynapseError(403, "User not from origin", Codes.FORBIDDEN) + if event.sender != event.state_key: + raise SynapseError(400, "state_key and sender must match", Codes.BAD_JSON) + event.internal_metadata.outlier = False # Send this event on behalf of the other server. @@ -2100,19 +1992,52 @@ async def on_send_knock_request( context = await self.state_handler.compute_event_context(event) - event_allowed = await self.third_party_event_rules.check_event_allowed( - event, context - ) - if not event_allowed: - logger.info("Sending of knock %s forbidden by third-party rules", event) - raise SynapseError( - 403, "This event is not allowed in this context", Codes.FORBIDDEN + # for joins, we need to check the restrictions of restricted rooms + if event.membership == Membership.JOIN: + await self._check_join_restrictions(context, event) + + # for knock events, we run the third-party event rules. It's not entirely clear + # why we don't do this for other sorts of membership events. + if event.membership == Membership.KNOCK: + event_allowed = await self.third_party_event_rules.check_event_allowed( + event, context ) + if not event_allowed: + logger.info("Sending of knock %s forbidden by third-party rules", event) + raise SynapseError( + 403, "This event is not allowed in this context", Codes.FORBIDDEN + ) await self._auth_and_persist_event(origin, event, context) return context + async def _check_join_restrictions( + self, context: EventContext, event: EventBase + ) -> None: + """Check that restrictions in restricted join rules are matched + + Called when we receive a join event via send_join. + + Raises an auth error if the restrictions are not matched. + """ + prev_state_ids = await context.get_prev_state_ids() + + # Check if the user is already in the room or invited to the room. + user_id = event.state_key + prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None) + prev_member_event = None + if prev_member_event_id: + prev_member_event = await self.store.get_event(prev_member_event_id) + + # Check if the member should be allowed access via membership in a space. + await self._event_auth_handler.check_restricted_join_rules( + prev_state_ids, + event.room_version, + user_id, + prev_member_event, + ) + async def get_state_for_pdu(self, room_id: str, event_id: str) -> List[EventBase]: """Returns the state at the event. i.e. not including said event.""" diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index 8796af45ed..ba8cf44f46 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -251,7 +251,7 @@ def _build_and_send_join_event(self, other_server, other_user, room_id): join_event.signatures[other_server] = {"x": "y"} with LoggingContext("send_join"): d = run_in_background( - self.handler.on_send_join_request, other_server, join_event + self.handler.on_send_membership_event, other_server, join_event ) self.get_success(d) diff --git a/tests/replication/test_federation_sender_shard.py b/tests/replication/test_federation_sender_shard.py index 584da58371..a0c710f855 100644 --- a/tests/replication/test_federation_sender_shard.py +++ b/tests/replication/test_federation_sender_shard.py @@ -228,7 +228,7 @@ def create_room_with_remote_server(self, user, token, remote_server="other_serve builder.build(prev_event_ids=prev_event_ids, auth_event_ids=None) ) - self.get_success(federation.on_send_join_request(remote_server, join_event)) + self.get_success(federation.on_send_membership_event(remote_server, join_event)) self.replicate() return room From 8165ba48b1d7d6a265683b06e32d08935f41fa69 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 24 Jun 2021 16:00:08 +0100 Subject: [PATCH 323/619] Return errors from `send_join` etc if the event is rejected (#10243) Rather than persisting rejected events via `send_join` and friends, raise a 403 if someone tries to pull a fast one. --- changelog.d/10243.feature | 1 + synapse/handlers/federation.py | 46 +++++++++++++++++---- tests/federation/transport/test_knocking.py | 4 +- 3 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10243.feature diff --git a/changelog.d/10243.feature b/changelog.d/10243.feature new file mode 100644 index 0000000000..d16f66ffe9 --- /dev/null +++ b/changelog.d/10243.feature @@ -0,0 +1 @@ +Improve validation on federation `send_{join,leave,knock}` endpoints. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 12f3d85342..d929c65131 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1953,16 +1953,31 @@ async def on_send_membership_event( self, origin: str, event: EventBase ) -> EventContext: """ - We have received a join/leave/knock event for a room. + We have received a join/leave/knock event for a room via send_join/leave/knock. Verify that event and send it into the room on the remote homeserver's behalf. + This is quite similar to on_receive_pdu, with the following principal + differences: + * only membership events are permitted (and only events with + sender==state_key -- ie, no kicks or bans) + * *We* send out the event on behalf of the remote server. + * We enforce the membership restrictions of restricted rooms. + * Rejected events result in an exception rather than being stored. + + There are also other differences, however it is not clear if these are by + design or omission. In particular, we do not attempt to backfill any missing + prev_events. + Args: origin: The homeserver of the remote (joining/invited/knocking) user. event: The member event that has been signed by the remote homeserver. Returns: The context of the event after inserting it into the room graph. + + Raises: + SynapseError if the event is not accepted into the room """ logger.debug( "on_send_membership_event: Got event: %s, signatures: %s", @@ -1981,7 +1996,7 @@ async def on_send_membership_event( if event.sender != event.state_key: raise SynapseError(400, "state_key and sender must match", Codes.BAD_JSON) - event.internal_metadata.outlier = False + assert not event.internal_metadata.outlier # Send this event on behalf of the other server. # @@ -1991,6 +2006,11 @@ async def on_send_membership_event( event.internal_metadata.send_on_behalf_of = origin context = await self.state_handler.compute_event_context(event) + context = await self._check_event_auth(origin, event, context) + if context.rejected: + raise SynapseError( + 403, f"{event.membership} event was rejected", Codes.FORBIDDEN + ) # for joins, we need to check the restrictions of restricted rooms if event.membership == Membership.JOIN: @@ -2008,8 +2028,8 @@ async def on_send_membership_event( 403, "This event is not allowed in this context", Codes.FORBIDDEN ) - await self._auth_and_persist_event(origin, event, context) - + # all looks good, we can persist the event. + await self._run_push_actions_and_persist_event(event, context) return context async def _check_join_restrictions( @@ -2179,6 +2199,18 @@ async def _auth_and_persist_event( backfilled=backfilled, ) + await self._run_push_actions_and_persist_event(event, context, backfilled) + + async def _run_push_actions_and_persist_event( + self, event: EventBase, context: EventContext, backfilled: bool = False + ): + """Run the push actions for a received event, and persist it. + + Args: + event: The event itself. + context: The event context. + backfilled: True if the event was backfilled. + """ try: if ( not event.internal_metadata.is_outlier() @@ -2492,9 +2524,9 @@ async def _check_event_auth( origin: str, event: EventBase, context: EventContext, - state: Optional[Iterable[EventBase]], - auth_events: Optional[MutableStateMap[EventBase]], - backfilled: bool, + state: Optional[Iterable[EventBase]] = None, + auth_events: Optional[MutableStateMap[EventBase]] = None, + backfilled: bool = False, ) -> EventContext: """ Checks whether an event should be rejected (for failing auth checks). diff --git a/tests/federation/transport/test_knocking.py b/tests/federation/transport/test_knocking.py index 8c215d50f2..aab44bce4a 100644 --- a/tests/federation/transport/test_knocking.py +++ b/tests/federation/transport/test_knocking.py @@ -205,9 +205,7 @@ async def approve_all_signature_checking(_, pdu): # Have this homeserver skip event auth checks. This is necessary due to # event auth checks ensuring that events were signed by the sender's homeserver. - async def _check_event_auth( - origin, event, context, state, auth_events, backfilled - ): + async def _check_event_auth(origin, event, context, *args, **kwargs): return context homeserver.get_federation_handler()._check_event_auth = _check_event_auth From f0e02f5df2bc3ae779ac8c18578deebdfecc7e97 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 24 Jun 2021 18:00:56 +0100 Subject: [PATCH 324/619] Create an index.html file when generating a docs build (#10242) Currently when a new build of the docs is created, an `index.html` file does not exist. Typically this would be generated from a`docs/README.md` file - which we have - however we're currently using [docs/README.md](https://github.com/matrix-org/synapse/blob/394673055db4df49bfd58c2f6118834a6d928563/docs/README.md) to explain the docs and point to the website. It is not part of the content of the website. So we end up not having an `index.html` file, which will result in a 404 page if one tries to navigate to `https://matrix-org.github.io/synapse//index.html`. This isn't a really problem for the default version of the documentation (currently `develop`), as [navigating to the top-level root](https://matrix-org.github.io/synapse/) of the website (without specifying a version) will [redirect](https://github.com/matrix-org/synapse/blob/a77e6925f26597958eccf0ef9956cb13c536e57e/index.html#L2) you to the Welcome and Overview page of the `develop` docs version. However, ideally once we add a GUI for switching between versions, we'll want to send the user to `matrix-org.github.io/synapse//index.html`, which currently isn't generated. This PR modifies the CI that builds the docs to simply copy the rendered [Welcome & Overview page](https://matrix-org.github.io/synapse/develop/welcome_and_overview.html) to `index.html`. --- .github/workflows/docs.yaml | 7 ++++++- changelog.d/10242.doc | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10242.doc diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 23b8d7f909..c239130c57 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -23,7 +23,12 @@ jobs: mdbook-version: '0.4.9' - name: Build the documentation - run: mdbook build + # mdbook will only create an index.html if we're including docs/README.md in SUMMARY.md. + # However, we're using docs/README.md for other purposes and need to pick a new page + # as the default. Let's opt for the welcome page instead. + run: | + mdbook build + cp book/welcome_and_overview.html book/index.html # Deploy to the latest documentation directories - name: Deploy latest documentation diff --git a/changelog.d/10242.doc b/changelog.d/10242.doc new file mode 100644 index 0000000000..2241b28547 --- /dev/null +++ b/changelog.d/10242.doc @@ -0,0 +1 @@ +Choose Welcome & Overview as the default page for synapse documentation website. From 717f73c41136c2cfbb6f4429a6e8358c163200f4 Mon Sep 17 00:00:00 2001 From: Felix Kronlage-Dammers Date: Mon, 28 Jun 2021 11:07:25 +0200 Subject: [PATCH 325/619] Adjust the URL in the README.rst file to point to LiberaChat instead of freenode (#10258) --- README.rst | 2 +- changelog.d/10258.doc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10258.doc diff --git a/README.rst b/README.rst index 1244aab10b..6d3cf6c1a5 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ The overall architecture is:: ``#matrix:matrix.org`` is the official support room for Matrix, and can be accessed by any client from https://matrix.org/docs/projects/try-matrix-now.html or -via IRC bridge at irc://irc.freenode.net/matrix. +via IRC bridge at irc://irc.libera.chat/matrix. Synapse is currently in rapid development, but as of version 0.5 we believe it is sufficiently stable to be run as an internet-facing service for real usage! diff --git a/changelog.d/10258.doc b/changelog.d/10258.doc new file mode 100644 index 0000000000..1549786c0c --- /dev/null +++ b/changelog.d/10258.doc @@ -0,0 +1 @@ +Adjust the URL in the README.rst file to point to irc.libera.chat. From 0555d7b0dc18fff489a31afccb47b79afa082113 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 28 Jun 2021 07:36:41 -0400 Subject: [PATCH 326/619] Add additional types to the federation transport server. (#10213) --- changelog.d/10213.misc | 1 + synapse/federation/transport/server.py | 588 ++++++++++++++++++++----- synapse/http/servlet.py | 50 ++- 3 files changed, 521 insertions(+), 118 deletions(-) create mode 100644 changelog.d/10213.misc diff --git a/changelog.d/10213.misc b/changelog.d/10213.misc new file mode 100644 index 0000000000..9adb0fbd02 --- /dev/null +++ b/changelog.d/10213.misc @@ -0,0 +1 @@ +Add type hints to the federation servlets. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 676fbd3750..d37d9565fc 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -15,7 +15,19 @@ import functools import logging import re -from typing import Container, Mapping, Optional, Sequence, Tuple, Type +from typing import ( + Container, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, +) + +from typing_extensions import Literal import synapse from synapse.api.constants import MAX_GROUP_CATEGORYID_LENGTH, MAX_GROUP_ROLEID_LENGTH @@ -56,15 +68,15 @@ class TransportLayerServer(JsonResource): """Handles incoming federation HTTP requests""" - def __init__(self, hs, servlet_groups=None): + def __init__(self, hs: HomeServer, servlet_groups: Optional[List[str]] = None): """Initialize the TransportLayerServer Will by default register all servlets. For custom behaviour, pass in a list of servlet_groups to register. Args: - hs (synapse.server.HomeServer): homeserver - servlet_groups (list[str], optional): List of servlet groups to register. + hs: homeserver + servlet_groups: List of servlet groups to register. Defaults to ``DEFAULT_SERVLET_GROUPS``. """ self.hs = hs @@ -78,7 +90,7 @@ def __init__(self, hs, servlet_groups=None): self.register_servlets() - def register_servlets(self): + def register_servlets(self) -> None: register_servlets( self.hs, resource=self, @@ -91,14 +103,10 @@ def register_servlets(self): class AuthenticationError(SynapseError): """There was a problem authenticating the request""" - pass - class NoAuthenticationError(AuthenticationError): """The request had no authentication information""" - pass - class Authenticator: def __init__(self, hs: HomeServer): @@ -410,13 +418,18 @@ class FederationSendServlet(BaseFederationServerServlet): RATELIMIT = False # This is when someone is trying to send us a bunch of data. - async def on_PUT(self, origin, content, query, transaction_id): + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + transaction_id: str, + ) -> Tuple[int, JsonDict]: """Called on PUT /send// Args: - request (twisted.web.http.Request): The HTTP request. - transaction_id (str): The transaction_id associated with this - request. This is *not* None. + transaction_id: The transaction_id associated with this request. This + is *not* None. Returns: Tuple of `(code, response)`, where @@ -461,7 +474,13 @@ class FederationEventServlet(BaseFederationServerServlet): PATH = "/event/(?P[^/]*)/?" # This is when someone asks for a data item for a given server data_id pair. - async def on_GET(self, origin, content, query, event_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + event_id: str, + ) -> Tuple[int, Union[JsonDict, str]]: return await self.handler.on_pdu_request(origin, event_id) @@ -469,7 +488,13 @@ class FederationStateV1Servlet(BaseFederationServerServlet): PATH = "/state/(?P[^/]*)/?" # This is when someone asks for all data for a given room. - async def on_GET(self, origin, content, query, room_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: return await self.handler.on_room_state_request( origin, room_id, @@ -480,7 +505,13 @@ async def on_GET(self, origin, content, query, room_id): class FederationStateIdsServlet(BaseFederationServerServlet): PATH = "/state_ids/(?P[^/]*)/?" - async def on_GET(self, origin, content, query, room_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: return await self.handler.on_state_ids_request( origin, room_id, @@ -491,7 +522,13 @@ async def on_GET(self, origin, content, query, room_id): class FederationBackfillServlet(BaseFederationServerServlet): PATH = "/backfill/(?P[^/]*)/?" - async def on_GET(self, origin, content, query, room_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: versions = [x.decode("ascii") for x in query[b"v"]] limit = parse_integer_from_args(query, "limit", None) @@ -505,7 +542,13 @@ class FederationQueryServlet(BaseFederationServerServlet): PATH = "/query/(?P[^/]*)" # This is when we receive a server-server Query - async def on_GET(self, origin, content, query, query_type): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + query_type: str, + ) -> Tuple[int, JsonDict]: args = {k.decode("utf8"): v[0].decode("utf-8") for k, v in query.items()} args["origin"] = origin return await self.handler.on_query_request(query_type, args) @@ -514,47 +557,66 @@ async def on_GET(self, origin, content, query, query_type): class FederationMakeJoinServlet(BaseFederationServerServlet): PATH = "/make_join/(?P[^/]*)/(?P[^/]*)" - async def on_GET(self, origin, _content, query, room_id, user_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: """ Args: - origin (unicode): The authenticated server_name of the calling server + origin: The authenticated server_name of the calling server - _content (None): (GETs don't have bodies) + content: (GETs don't have bodies) - query (dict[bytes, list[bytes]]): Query params from the request. + query: Query params from the request. - **kwargs (dict[unicode, unicode]): the dict mapping keys to path - components as specified in the path match regexp. + **kwargs: the dict mapping keys to path components as specified in + the path match regexp. Returns: - Tuple[int, object]: (response code, response object) + Tuple of (response code, response object) """ - versions = query.get(b"ver") - if versions is not None: - supported_versions = [v.decode("utf-8") for v in versions] - else: + supported_versions = parse_strings_from_args(query, "ver", encoding="utf-8") + if supported_versions is None: supported_versions = ["1"] - content = await self.handler.on_make_join_request( + result = await self.handler.on_make_join_request( origin, room_id, user_id, supported_versions=supported_versions ) - return 200, content + return 200, result class FederationMakeLeaveServlet(BaseFederationServerServlet): PATH = "/make_leave/(?P[^/]*)/(?P[^/]*)" - async def on_GET(self, origin, content, query, room_id, user_id): - content = await self.handler.on_make_leave_request(origin, room_id, user_id) - return 200, content + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + result = await self.handler.on_make_leave_request(origin, room_id, user_id) + return 200, result class FederationV1SendLeaveServlet(BaseFederationServerServlet): PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" - async def on_PUT(self, origin, content, query, room_id, event_id): - content = await self.handler.on_send_leave_request(origin, content, room_id) - return 200, (200, content) + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, Tuple[int, JsonDict]]: + result = await self.handler.on_send_leave_request(origin, content, room_id) + return 200, (200, result) class FederationV2SendLeaveServlet(BaseFederationServerServlet): @@ -562,50 +624,84 @@ class FederationV2SendLeaveServlet(BaseFederationServerServlet): PREFIX = FEDERATION_V2_PREFIX - async def on_PUT(self, origin, content, query, room_id, event_id): - content = await self.handler.on_send_leave_request(origin, content, room_id) - return 200, content + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: + result = await self.handler.on_send_leave_request(origin, content, room_id) + return 200, result class FederationMakeKnockServlet(BaseFederationServerServlet): PATH = "/make_knock/(?P[^/]*)/(?P[^/]*)" - async def on_GET(self, origin, content, query, room_id, user_id): - try: - # Retrieve the room versions the remote homeserver claims to support - supported_versions = parse_strings_from_args(query, "ver", encoding="utf-8") - except KeyError: - raise SynapseError(400, "Missing required query parameter 'ver'") + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + # Retrieve the room versions the remote homeserver claims to support + supported_versions = parse_strings_from_args( + query, "ver", required=True, encoding="utf-8" + ) - content = await self.handler.on_make_knock_request( + result = await self.handler.on_make_knock_request( origin, room_id, user_id, supported_versions=supported_versions ) - return 200, content + return 200, result class FederationV1SendKnockServlet(BaseFederationServerServlet): PATH = "/send_knock/(?P[^/]*)/(?P[^/]*)" - async def on_PUT(self, origin, content, query, room_id, event_id): - content = await self.handler.on_send_knock_request(origin, content, room_id) - return 200, content + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: + result = await self.handler.on_send_knock_request(origin, content, room_id) + return 200, result class FederationEventAuthServlet(BaseFederationServerServlet): PATH = "/event_auth/(?P[^/]*)/(?P[^/]*)" - async def on_GET(self, origin, content, query, room_id, event_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: return await self.handler.on_event_auth(origin, room_id, event_id) class FederationV1SendJoinServlet(BaseFederationServerServlet): PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" - async def on_PUT(self, origin, content, query, room_id, event_id): + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, Tuple[int, JsonDict]]: # TODO(paul): assert that event_id parsed from path actually # match those given in content - content = await self.handler.on_send_join_request(origin, content, room_id) - return 200, (200, content) + result = await self.handler.on_send_join_request(origin, content, room_id) + return 200, (200, result) class FederationV2SendJoinServlet(BaseFederationServerServlet): @@ -613,28 +709,42 @@ class FederationV2SendJoinServlet(BaseFederationServerServlet): PREFIX = FEDERATION_V2_PREFIX - async def on_PUT(self, origin, content, query, room_id, event_id): + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: # TODO(paul): assert that event_id parsed from path actually # match those given in content - content = await self.handler.on_send_join_request(origin, content, room_id) - return 200, content + result = await self.handler.on_send_join_request(origin, content, room_id) + return 200, result class FederationV1InviteServlet(BaseFederationServerServlet): PATH = "/invite/(?P[^/]*)/(?P[^/]*)" - async def on_PUT(self, origin, content, query, room_id, event_id): + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, Tuple[int, JsonDict]]: # We don't get a room version, so we have to assume its EITHER v1 or # v2. This is "fine" as the only difference between V1 and V2 is the # state resolution algorithm, and we don't use that for processing # invites - content = await self.handler.on_invite_request( + result = await self.handler.on_invite_request( origin, content, room_version_id=RoomVersions.V1.identifier ) # V1 federation API is defined to return a content of `[200, {...}]` # due to a historical bug. - return 200, (200, content) + return 200, (200, result) class FederationV2InviteServlet(BaseFederationServerServlet): @@ -642,7 +752,14 @@ class FederationV2InviteServlet(BaseFederationServerServlet): PREFIX = FEDERATION_V2_PREFIX - async def on_PUT(self, origin, content, query, room_id, event_id): + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: # TODO(paul): assert that room_id/event_id parsed from path actually # match those given in content @@ -655,16 +772,22 @@ async def on_PUT(self, origin, content, query, room_id, event_id): event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state - content = await self.handler.on_invite_request( + result = await self.handler.on_invite_request( origin, event, room_version_id=room_version ) - return 200, content + return 200, result class FederationThirdPartyInviteExchangeServlet(BaseFederationServerServlet): PATH = "/exchange_third_party_invite/(?P[^/]*)" - async def on_PUT(self, origin, content, query, room_id): + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: await self.handler.on_exchange_third_party_invite_request(content) return 200, {} @@ -672,21 +795,31 @@ async def on_PUT(self, origin, content, query, room_id): class FederationClientKeysQueryServlet(BaseFederationServerServlet): PATH = "/user/keys/query" - async def on_POST(self, origin, content, query): + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: return await self.handler.on_query_client_keys(origin, content) class FederationUserDevicesQueryServlet(BaseFederationServerServlet): PATH = "/user/devices/(?P[^/]*)" - async def on_GET(self, origin, content, query, user_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + user_id: str, + ) -> Tuple[int, JsonDict]: return await self.handler.on_query_user_devices(origin, user_id) class FederationClientKeysClaimServlet(BaseFederationServerServlet): PATH = "/user/keys/claim" - async def on_POST(self, origin, content, query): + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: response = await self.handler.on_claim_client_keys(origin, content) return 200, response @@ -695,12 +828,18 @@ class FederationGetMissingEventsServlet(BaseFederationServerServlet): # TODO(paul): Why does this path alone end with "/?" optional? PATH = "/get_missing_events/(?P[^/]*)/?" - async def on_POST(self, origin, content, query, room_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: limit = int(content.get("limit", 10)) earliest_events = content.get("earliest_events", []) latest_events = content.get("latest_events", []) - content = await self.handler.on_get_missing_events( + result = await self.handler.on_get_missing_events( origin, room_id=room_id, earliest_events=earliest_events, @@ -708,7 +847,7 @@ async def on_POST(self, origin, content, query, room_id): limit=limit, ) - return 200, content + return 200, result class On3pidBindServlet(BaseFederationServerServlet): @@ -716,7 +855,9 @@ class On3pidBindServlet(BaseFederationServerServlet): REQUIRE_AUTH = False - async def on_POST(self, origin, content, query): + async def on_POST( + self, origin: Optional[str], content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: if "invites" in content: last_exception = None for invite in content["invites"]: @@ -762,15 +903,20 @@ class OpenIdUserInfo(BaseFederationServerServlet): REQUIRE_AUTH = False - async def on_GET(self, origin, content, query): - token = query.get(b"access_token", [None])[0] + async def on_GET( + self, + origin: Optional[str], + content: Literal[None], + query: Dict[bytes, List[bytes]], + ) -> Tuple[int, JsonDict]: + token = parse_string_from_args(query, "access_token") if token is None: return ( 401, {"errcode": "M_MISSING_TOKEN", "error": "Access Token required"}, ) - user_id = await self.handler.on_openid_userinfo(token.decode("ascii")) + user_id = await self.handler.on_openid_userinfo(token) if user_id is None: return ( @@ -829,7 +975,9 @@ def __init__( self.handler = hs.get_room_list_handler() self.allow_access = allow_access - async def on_GET(self, origin, content, query): + async def on_GET( + self, origin: str, content: Literal[None], query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: if not self.allow_access: raise FederationDeniedError(origin) @@ -858,7 +1006,9 @@ async def on_GET(self, origin, content, query): ) return 200, data - async def on_POST(self, origin, content, query): + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: # This implements MSC2197 (Search Filtering over Federation) if not self.allow_access: raise FederationDeniedError(origin) @@ -904,7 +1054,12 @@ class FederationVersionServlet(BaseFederationServlet): REQUIRE_AUTH = False - async def on_GET(self, origin, content, query): + async def on_GET( + self, + origin: Optional[str], + content: Literal[None], + query: Dict[bytes, List[bytes]], + ) -> Tuple[int, JsonDict]: return ( 200, {"server": {"name": "Synapse", "version": get_version_string(synapse)}}, @@ -933,7 +1088,13 @@ class FederationGroupsProfileServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/profile" - async def on_GET(self, origin, content, query, group_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -942,7 +1103,13 @@ async def on_GET(self, origin, content, query, group_id): return 200, new_content - async def on_POST(self, origin, content, query, group_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -957,7 +1124,13 @@ async def on_POST(self, origin, content, query, group_id): class FederationGroupsSummaryServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/summary" - async def on_GET(self, origin, content, query, group_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -972,7 +1145,13 @@ class FederationGroupsRoomsServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/rooms" - async def on_GET(self, origin, content, query, group_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -987,7 +1166,14 @@ class FederationGroupsAddRoomsServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/room/(?P[^/]*)" - async def on_POST(self, origin, content, query, group_id, room_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -998,7 +1184,14 @@ async def on_POST(self, origin, content, query, group_id, room_id): return 200, new_content - async def on_DELETE(self, origin, content, query, group_id, room_id): + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1018,7 +1211,15 @@ class FederationGroupsAddRoomsConfigServlet(BaseGroupsServerServlet): "/config/(?P[^/]*)" ) - async def on_POST(self, origin, content, query, group_id, room_id, config_key): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + room_id: str, + config_key: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1035,7 +1236,13 @@ class FederationGroupsUsersServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/users" - async def on_GET(self, origin, content, query, group_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1050,7 +1257,13 @@ class FederationGroupsInvitedUsersServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/invited_users" - async def on_GET(self, origin, content, query, group_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1067,7 +1280,14 @@ class FederationGroupsInviteServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/invite" - async def on_POST(self, origin, content, query, group_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1084,7 +1304,14 @@ class FederationGroupsAcceptInviteServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/accept_invite" - async def on_POST(self, origin, content, query, group_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: if get_domain_from_id(user_id) != origin: raise SynapseError(403, "user_id doesn't match origin") @@ -1098,7 +1325,14 @@ class FederationGroupsJoinServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/join" - async def on_POST(self, origin, content, query, group_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: if get_domain_from_id(user_id) != origin: raise SynapseError(403, "user_id doesn't match origin") @@ -1112,7 +1346,14 @@ class FederationGroupsRemoveUserServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/remove" - async def on_POST(self, origin, content, query, group_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1146,7 +1387,14 @@ class FederationGroupsLocalInviteServlet(BaseGroupsLocalServlet): PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/invite" - async def on_POST(self, origin, content, query, group_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: if get_domain_from_id(group_id) != origin: raise SynapseError(403, "group_id doesn't match origin") @@ -1164,7 +1412,14 @@ class FederationGroupsRemoveLocalUserServlet(BaseGroupsLocalServlet): PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/remove" - async def on_POST(self, origin, content, query, group_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, None]: if get_domain_from_id(group_id) != origin: raise SynapseError(403, "user_id doesn't match origin") @@ -1172,11 +1427,9 @@ async def on_POST(self, origin, content, query, group_id, user_id): self.handler, GroupsLocalHandler ), "Workers cannot handle group removals." - new_content = await self.handler.user_removed_from_group( - group_id, user_id, content - ) + await self.handler.user_removed_from_group(group_id, user_id, content) - return 200, new_content + return 200, None class FederationGroupsRenewAttestaionServlet(BaseFederationServlet): @@ -1194,7 +1447,14 @@ def __init__( super().__init__(hs, authenticator, ratelimiter, server_name) self.handler = hs.get_groups_attestation_renewer() - async def on_POST(self, origin, content, query, group_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: # We don't need to check auth here as we check the attestation signatures new_content = await self.handler.on_renew_attestation( @@ -1218,7 +1478,15 @@ class FederationGroupsSummaryRoomsServlet(BaseGroupsServerServlet): "/rooms/(?P[^/]*)" ) - async def on_POST(self, origin, content, query, group_id, category_id, room_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1246,7 +1514,15 @@ async def on_POST(self, origin, content, query, group_id, category_id, room_id): return 200, resp - async def on_DELETE(self, origin, content, query, group_id, category_id, room_id): + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1266,7 +1542,13 @@ class FederationGroupsCategoriesServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/categories/?" - async def on_GET(self, origin, content, query, group_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1281,7 +1563,14 @@ class FederationGroupsCategoryServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/categories/(?P[^/]+)" - async def on_GET(self, origin, content, query, group_id, category_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1292,7 +1581,14 @@ async def on_GET(self, origin, content, query, group_id, category_id): return 200, resp - async def on_POST(self, origin, content, query, group_id, category_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1314,7 +1610,14 @@ async def on_POST(self, origin, content, query, group_id, category_id): return 200, resp - async def on_DELETE(self, origin, content, query, group_id, category_id): + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1334,7 +1637,13 @@ class FederationGroupsRolesServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/roles/?" - async def on_GET(self, origin, content, query, group_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1349,7 +1658,14 @@ class FederationGroupsRoleServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/roles/(?P[^/]+)" - async def on_GET(self, origin, content, query, group_id, role_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1358,7 +1674,14 @@ async def on_GET(self, origin, content, query, group_id, role_id): return 200, resp - async def on_POST(self, origin, content, query, group_id, role_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1382,7 +1705,14 @@ async def on_POST(self, origin, content, query, group_id, role_id): return 200, resp - async def on_DELETE(self, origin, content, query, group_id, role_id): + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1411,7 +1741,15 @@ class FederationGroupsSummaryUsersServlet(BaseGroupsServerServlet): "/users/(?P[^/]*)" ) - async def on_POST(self, origin, content, query, group_id, role_id, user_id): + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1437,7 +1775,15 @@ async def on_POST(self, origin, content, query, group_id, role_id, user_id): return 200, resp - async def on_DELETE(self, origin, content, query, group_id, role_id, user_id): + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1457,7 +1803,9 @@ class FederationGroupsBulkPublicisedServlet(BaseGroupsLocalServlet): PATH = "/get_groups_publicised" - async def on_POST(self, origin, content, query): + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: resp = await self.handler.bulk_get_publicised_groups( content["user_ids"], proxy=False ) @@ -1470,7 +1818,13 @@ class FederationGroupsSettingJoinPolicyServlet(BaseGroupsServerServlet): PATH = "/groups/(?P[^/]*)/settings/m.join_policy" - async def on_PUT(self, origin, content, query, group_id): + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: requester_user_id = parse_string_from_args(query, "requester_user_id") if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1499,7 +1853,7 @@ def __init__( async def on_GET( self, origin: str, - content: JsonDict, + content: Literal[None], query: Mapping[bytes, Sequence[bytes]], room_id: str, ) -> Tuple[int, JsonDict]: @@ -1571,7 +1925,13 @@ def __init__( super().__init__(hs, authenticator, ratelimiter, server_name) self._store = self.hs.get_datastore() - async def on_GET(self, origin, content, query, room_id): + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: is_public = await self._store.is_room_world_readable_or_publicly_joinable( room_id ) diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index fda8da21b7..6ba2ce1e53 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -109,12 +109,22 @@ def parse_boolean_from_args(args, name, default=None, required=False): return default +@overload +def parse_bytes_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Optional[bytes] = None, +) -> Optional[bytes]: + ... + + @overload def parse_bytes_from_args( args: Dict[bytes, List[bytes]], name: str, default: Literal[None] = None, - required: Literal[True] = True, + *, + required: Literal[True], ) -> bytes: ... @@ -197,7 +207,12 @@ def parse_string( """ args = request.args # type: Dict[bytes, List[bytes]] # type: ignore return parse_string_from_args( - args, name, default, required, allowed_values, encoding + args, + name, + default, + required=required, + allowed_values=allowed_values, + encoding=encoding, ) @@ -227,7 +242,20 @@ def parse_strings_from_args( args: Dict[bytes, List[bytes]], name: str, default: Optional[List[str]] = None, - required: Literal[True] = True, + *, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> Optional[List[str]]: + ... + + +@overload +def parse_strings_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Optional[List[str]] = None, + *, + required: Literal[True], allowed_values: Optional[Iterable[str]] = None, encoding: str = "ascii", ) -> List[str]: @@ -239,6 +267,7 @@ def parse_strings_from_args( args: Dict[bytes, List[bytes]], name: str, default: Optional[List[str]] = None, + *, required: bool = False, allowed_values: Optional[Iterable[str]] = None, encoding: str = "ascii", @@ -299,7 +328,20 @@ def parse_string_from_args( args: Dict[bytes, List[bytes]], name: str, default: Optional[str] = None, - required: Literal[True] = True, + *, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> Optional[str]: + ... + + +@overload +def parse_string_from_args( + args: Dict[bytes, List[bytes]], + name: str, + default: Optional[str] = None, + *, + required: Literal[True], allowed_values: Optional[Iterable[str]] = None, encoding: str = "ascii", ) -> str: From cdf569e46811cb498e17eccf81d8f7d645aa60e9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 29 Jun 2021 10:15:34 +0100 Subject: [PATCH 327/619] 1.37.0 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 2c7f24487c..5b924e2471 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.37.0 (2021-06-29) +=========================== + +No significant changes. + + Synapse 1.37.0rc1 (2021-06-24) ============================== diff --git a/debian/changelog b/debian/changelog index e640dadde9..cf190b7dba 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.37.0) stable; urgency=medium + + * New synapse release 1.37.0. + + -- Synapse Packaging team Tue, 29 Jun 2021 10:15:25 +0100 + matrix-synapse-py3 (1.36.0) stable; urgency=medium * New synapse release 1.36.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 6d1c6d6f72..c865d2e100 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.37.0rc1" +__version__ = "1.37.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 88f9e8d62e0573b5b6f1c3a8bfe4d87f9aebde47 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 29 Jun 2021 10:16:43 +0100 Subject: [PATCH 328/619] Move deprecation notices to the top of the changelog --- CHANGES.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5b924e2471..eac91ffe02 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,16 +1,12 @@ Synapse 1.37.0 (2021-06-29) =========================== -No significant changes. - - -Synapse 1.37.0rc1 (2021-06-24) -============================== - This release deprecates the current spam checker interface. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade#deprecation-of-the-current-spam-checker-interface) for more information on how to update to the new generic module interface. This release also removes support for fetching and renewing TLS certificates using the ACME v1 protocol, which has been fully decommissioned by Let's Encrypt on June 1st 2021. Admins previously using this feature should use a [reverse proxy](https://matrix-org.github.io/synapse/develop/reverse_proxy.html) to handle TLS termination, or use an external ACME client (such as [certbot](https://certbot.eff.org/)) to retrieve a certificate and key and provide them to Synapse using the `tls_certificate_path` and `tls_private_key_path` configuration settings. +Synapse 1.37.0rc1 (2021-06-24) +============================== Features -------- From a0ed0f363eb84f273b2cc706fcc5542d77a94463 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:08:06 +0100 Subject: [PATCH 329/619] Soft-fail spammy events received over federation (#10263) --- changelog.d/10263.feature | 1 + synapse/federation/federation_base.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10263.feature diff --git a/changelog.d/10263.feature b/changelog.d/10263.feature new file mode 100644 index 0000000000..7b1d2fe60f --- /dev/null +++ b/changelog.d/10263.feature @@ -0,0 +1 @@ +Mark events received over federation which fail a spam check as "soft-failed". diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index c066617b92..2bfe6a3d37 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -89,12 +89,12 @@ async def _check_sigs_and_hash( result = await self.spam_checker.check_event_for_spam(pdu) if result: - logger.warning( - "Event contains spam, redacting %s: %s", - pdu.event_id, - pdu.get_pdu_json(), - ) - return prune_event(pdu) + logger.warning("Event contains spam, soft-failing %s", pdu.event_id) + # we redact (to save disk space) as well as soft-failing (to stop + # using the event in prev_events). + redacted_event = prune_event(pdu) + redacted_event.internal_metadata.soft_failed = True + return redacted_event return pdu From 60efc51a2bbc31f18a71ad1338afc430bfa65597 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:25:34 +0100 Subject: [PATCH 330/619] Migrate stream_ordering to a bigint (#10264) * Move background update names out to a separate class `EventsBackgroundUpdatesStore` gets inherited and we don't really want to further pollute the namespace. * Migrate stream_ordering to a bigint * changelog --- changelog.d/10264.bugfix | 1 + .../databases/main/events_bg_updates.py | 136 ++++++++++++++++-- synapse/storage/schema/__init__.py | 2 +- .../01recreate_stream_ordering.sql.postgres | 40 ++++++ 4 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 changelog.d/10264.bugfix create mode 100644 synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres diff --git a/changelog.d/10264.bugfix b/changelog.d/10264.bugfix new file mode 100644 index 0000000000..7ebda7cdc2 --- /dev/null +++ b/changelog.d/10264.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index cbe4be1437..39aaee743c 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -29,6 +29,25 @@ logger = logging.getLogger(__name__) +_REPLACE_STREAM_ORDRING_SQL_COMMANDS = ( + # there should be no leftover rows without a stream_ordering2, but just in case... + "UPDATE events SET stream_ordering2 = stream_ordering WHERE stream_ordering2 IS NULL", + # finally, we can drop the rule and switch the columns + "DROP RULE populate_stream_ordering2 ON events", + "ALTER TABLE events DROP COLUMN stream_ordering", + "ALTER TABLE events RENAME COLUMN stream_ordering2 TO stream_ordering", +) + + +class _BackgroundUpdates: + EVENT_ORIGIN_SERVER_TS_NAME = "event_origin_server_ts" + EVENT_FIELDS_SENDER_URL_UPDATE_NAME = "event_fields_sender_url" + DELETE_SOFT_FAILED_EXTREMITIES = "delete_soft_failed_extremities" + POPULATE_STREAM_ORDERING2 = "populate_stream_ordering2" + INDEX_STREAM_ORDERING2 = "index_stream_ordering2" + REPLACE_STREAM_ORDERING_COLUMN = "replace_stream_ordering_column" + + @attr.s(slots=True, frozen=True) class _CalculateChainCover: """Return value for _calculate_chain_cover_txn.""" @@ -48,19 +67,15 @@ class _CalculateChainCover: class EventsBackgroundUpdatesStore(SQLBaseStore): - - EVENT_ORIGIN_SERVER_TS_NAME = "event_origin_server_ts" - EVENT_FIELDS_SENDER_URL_UPDATE_NAME = "event_fields_sender_url" - DELETE_SOFT_FAILED_EXTREMITIES = "delete_soft_failed_extremities" - def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) self.db_pool.updates.register_background_update_handler( - self.EVENT_ORIGIN_SERVER_TS_NAME, self._background_reindex_origin_server_ts + _BackgroundUpdates.EVENT_ORIGIN_SERVER_TS_NAME, + self._background_reindex_origin_server_ts, ) self.db_pool.updates.register_background_update_handler( - self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, + _BackgroundUpdates.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, self._background_reindex_fields_sender, ) @@ -85,7 +100,8 @@ def __init__(self, database: DatabasePool, db_conn, hs): ) self.db_pool.updates.register_background_update_handler( - self.DELETE_SOFT_FAILED_EXTREMITIES, self._cleanup_extremities_bg_update + _BackgroundUpdates.DELETE_SOFT_FAILED_EXTREMITIES, + self._cleanup_extremities_bg_update, ) self.db_pool.updates.register_background_update_handler( @@ -139,6 +155,24 @@ def __init__(self, database: DatabasePool, db_conn, hs): self._purged_chain_cover_index, ) + # bg updates for replacing stream_ordering with a BIGINT + # (these only run on postgres.) + self.db_pool.updates.register_background_update_handler( + _BackgroundUpdates.POPULATE_STREAM_ORDERING2, + self._background_populate_stream_ordering2, + ) + self.db_pool.updates.register_background_index_update( + _BackgroundUpdates.INDEX_STREAM_ORDERING2, + index_name="events_stream_ordering", + table="events", + columns=["stream_ordering2"], + unique=True, + ) + self.db_pool.updates.register_background_update_handler( + _BackgroundUpdates.REPLACE_STREAM_ORDERING_COLUMN, + self._background_replace_stream_ordering_column, + ) + async def _background_reindex_fields_sender(self, progress, batch_size): target_min_stream_id = progress["target_min_stream_id_inclusive"] max_stream_id = progress["max_stream_id_exclusive"] @@ -190,18 +224,18 @@ def reindex_txn(txn): } self.db_pool.updates._background_update_progress_txn( - txn, self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, progress + txn, _BackgroundUpdates.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, progress ) return len(rows) result = await self.db_pool.runInteraction( - self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, reindex_txn + _BackgroundUpdates.EVENT_FIELDS_SENDER_URL_UPDATE_NAME, reindex_txn ) if not result: await self.db_pool.updates._end_background_update( - self.EVENT_FIELDS_SENDER_URL_UPDATE_NAME + _BackgroundUpdates.EVENT_FIELDS_SENDER_URL_UPDATE_NAME ) return result @@ -264,18 +298,18 @@ def reindex_search_txn(txn): } self.db_pool.updates._background_update_progress_txn( - txn, self.EVENT_ORIGIN_SERVER_TS_NAME, progress + txn, _BackgroundUpdates.EVENT_ORIGIN_SERVER_TS_NAME, progress ) return len(rows_to_update) result = await self.db_pool.runInteraction( - self.EVENT_ORIGIN_SERVER_TS_NAME, reindex_search_txn + _BackgroundUpdates.EVENT_ORIGIN_SERVER_TS_NAME, reindex_search_txn ) if not result: await self.db_pool.updates._end_background_update( - self.EVENT_ORIGIN_SERVER_TS_NAME + _BackgroundUpdates.EVENT_ORIGIN_SERVER_TS_NAME ) return result @@ -454,7 +488,7 @@ def _cleanup_extremities_bg_update_txn(txn): if not num_handled: await self.db_pool.updates._end_background_update( - self.DELETE_SOFT_FAILED_EXTREMITIES + _BackgroundUpdates.DELETE_SOFT_FAILED_EXTREMITIES ) def _drop_table_txn(txn): @@ -1009,3 +1043,75 @@ def purged_chain_cover_txn(txn) -> int: await self.db_pool.updates._end_background_update("purged_chain_cover") return result + + async def _background_populate_stream_ordering2( + self, progress: JsonDict, batch_size: int + ) -> int: + """Populate events.stream_ordering2, then replace stream_ordering + + This is to deal with the fact that stream_ordering was initially created as a + 32-bit integer field. + """ + batch_size = max(batch_size, 1) + + def process(txn: Cursor) -> int: + # if this is the first pass, find the minimum stream ordering + last_stream = progress.get("last_stream") + if last_stream is None: + txn.execute( + """ + SELECT stream_ordering FROM events ORDER BY stream_ordering LIMIT 1 + """ + ) + rows = txn.fetchall() + if not rows: + return 0 + last_stream = rows[0][0] - 1 + + txn.execute( + """ + UPDATE events SET stream_ordering2=stream_ordering + WHERE stream_ordering > ? AND stream_ordering <= ? + """, + (last_stream, last_stream + batch_size), + ) + row_count = txn.rowcount + + self.db_pool.updates._background_update_progress_txn( + txn, + _BackgroundUpdates.POPULATE_STREAM_ORDERING2, + {"last_stream": last_stream + batch_size}, + ) + return row_count + + result = await self.db_pool.runInteraction( + "_background_populate_stream_ordering2", process + ) + + if result != 0: + return result + + await self.db_pool.updates._end_background_update( + _BackgroundUpdates.POPULATE_STREAM_ORDERING2 + ) + return 0 + + async def _background_replace_stream_ordering_column( + self, progress: JsonDict, batch_size: int + ) -> int: + """Drop the old 'stream_ordering' column and rename 'stream_ordering2' into its place.""" + + def process(txn: Cursor) -> None: + for sql in _REPLACE_STREAM_ORDRING_SQL_COMMANDS: + logger.info("completing stream_ordering migration: %s", sql) + txn.execute(sql) + + await self.db_pool.runInteraction( + "_background_replace_stream_ordering_column", process + ) + + await self.db_pool.updates._end_background_update( + _BackgroundUpdates.REPLACE_STREAM_ORDERING_COLUMN + ) + + return 0 diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index d36ba1d773..0a53b73ccc 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -SCHEMA_VERSION = 59 +SCHEMA_VERSION = 60 """Represents the expectations made by the codebase about the database schema This should be incremented whenever the codebase changes its requirements on the diff --git a/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres b/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres new file mode 100644 index 0000000000..88c9f8bd0d --- /dev/null +++ b/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres @@ -0,0 +1,40 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- This migration handles the process of changing the type of `stream_ordering` to +-- a BIGINT. +-- +-- Note that this is only a problem on postgres as sqlite only has one "integer" type +-- which can cope with values up to 2^63. + +-- First add a new column to contain the bigger stream_ordering +ALTER TABLE events ADD COLUMN stream_ordering2 BIGINT; + +-- Create a rule which will populate it for new rows. +CREATE OR REPLACE RULE "populate_stream_ordering2" AS + ON INSERT TO events + DO UPDATE events SET stream_ordering2=NEW.stream_ordering WHERE stream_ordering=NEW.stream_ordering; + +-- Start a bg process to populate it for old events +INSERT INTO background_updates (ordering, update_name, progress_json) VALUES + (6001, 'populate_stream_ordering2', '{}'); + +-- ... and another to build an index on it +INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES + (6001, 'index_stream_ordering2', '{}', 'populate_stream_ordering2'); + +-- ... and another to do the switcheroo +INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES + (6001, 'replace_stream_ordering_column', '{}', 'index_stream_ordering2'); From 7647b0337fb5d936c88c5949fa92c07bf2137ad0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 29 Jun 2021 12:43:36 +0100 Subject: [PATCH 331/619] Fix `populate_stream_ordering2` background job (#10267) It was possible for us not to find any rows in a batch, and hence conclude that we had finished. Let's not do that. --- changelog.d/10267.bugfix | 1 + .../databases/main/events_bg_updates.py | 28 ++++++++----------- 2 files changed, 13 insertions(+), 16 deletions(-) create mode 100644 changelog.d/10267.bugfix diff --git a/changelog.d/10267.bugfix b/changelog.d/10267.bugfix new file mode 100644 index 0000000000..7ebda7cdc2 --- /dev/null +++ b/changelog.d/10267.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index 39aaee743c..da3a7df27b 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -1055,32 +1055,28 @@ async def _background_populate_stream_ordering2( batch_size = max(batch_size, 1) def process(txn: Cursor) -> int: - # if this is the first pass, find the minimum stream ordering - last_stream = progress.get("last_stream") - if last_stream is None: - txn.execute( - """ - SELECT stream_ordering FROM events ORDER BY stream_ordering LIMIT 1 - """ - ) - rows = txn.fetchall() - if not rows: - return 0 - last_stream = rows[0][0] - 1 - + last_stream = progress.get("last_stream", -(1 << 31)) txn.execute( """ UPDATE events SET stream_ordering2=stream_ordering - WHERE stream_ordering > ? AND stream_ordering <= ? + WHERE stream_ordering IN ( + SELECT stream_ordering FROM events WHERE stream_ordering > ? + ORDER BY stream_ordering LIMIT ? + ) + RETURNING stream_ordering; """, - (last_stream, last_stream + batch_size), + (last_stream, batch_size), ) row_count = txn.rowcount + if row_count == 0: + return 0 + last_stream = max(row[0] for row in txn) + logger.info("populated stream_ordering2 up to %i", last_stream) self.db_pool.updates._background_update_progress_txn( txn, _BackgroundUpdates.POPULATE_STREAM_ORDERING2, - {"last_stream": last_stream + batch_size}, + {"last_stream": last_stream}, ) return row_count From f55836929d3c64f3f8d883d8f3643a88b6c9cbca Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 29 Jun 2021 12:00:04 -0400 Subject: [PATCH 332/619] Do not recurse into non-spaces in the spaces summary. (#10256) Previously m.child.room events in non-space rooms would be treated as part of the room graph, but this is no longer supported. --- changelog.d/10256.misc | 1 + synapse/api/constants.py | 6 ++++ synapse/handlers/space_summary.py | 11 +++++-- tests/handlers/test_space_summary.py | 48 +++++++++++++++------------- tests/rest/client/v1/utils.py | 3 +- 5 files changed, 43 insertions(+), 26 deletions(-) create mode 100644 changelog.d/10256.misc diff --git a/changelog.d/10256.misc b/changelog.d/10256.misc new file mode 100644 index 0000000000..adef12fcb9 --- /dev/null +++ b/changelog.d/10256.misc @@ -0,0 +1 @@ +Improve the performance of the spaces summary endpoint by only recursing into spaces (and not rooms in general). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 414e4c019a..8363c2bb0f 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -201,6 +201,12 @@ class EventContentFields: ) +class RoomTypes: + """Understood values of the room_type field of m.room.create events.""" + + SPACE = "m.space" + + class RoomEncryptionAlgorithms: MEGOLM_V1_AES_SHA2 = "m.megolm.v1.aes-sha2" DEFAULT = MEGOLM_V1_AES_SHA2 diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 17fc47ce16..266f369883 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -25,6 +25,7 @@ EventTypes, HistoryVisibility, Membership, + RoomTypes, ) from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 @@ -318,7 +319,8 @@ async def _summarize_local_room( Returns: A tuple of: - An iterable of a single value of the room. + The room information, if the room should be returned to the + user. None, otherwise. An iterable of the sorted children events. This may be limited to a maximum size or may include all children. @@ -328,7 +330,11 @@ async def _summarize_local_room( room_entry = await self._build_room_entry(room_id) - # look for child rooms/spaces. + # If the room is not a space, return just the room information. + if room_entry.get("room_type") != RoomTypes.SPACE: + return room_entry, () + + # Otherwise, look for child rooms/spaces. child_events = await self._get_child_events(room_id) if suggested_only: @@ -348,6 +354,7 @@ async def _summarize_local_room( event_format=format_event_for_client_v2, ) ) + return room_entry, events_result async def _summarize_remote_room( diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 131d362ccc..9771d3fb3b 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -14,6 +14,7 @@ from typing import Any, Iterable, Optional, Tuple from unittest import mock +from synapse.api.constants import EventContentFields, RoomTypes from synapse.api.errors import AuthError from synapse.handlers.space_summary import _child_events_comparison_key from synapse.rest import admin @@ -97,9 +98,21 @@ def prepare(self, reactor, clock, hs: HomeServer): self.hs = hs self.handler = self.hs.get_space_summary_handler() + # Create a user. self.user = self.register_user("user", "pass") self.token = self.login("user", "pass") + # Create a space and a child room. + self.space = self.helper.create_room_as( + self.user, + tok=self.token, + extra_content={ + "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} + }, + ) + self.room = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(self.space, self.room, self.token) + def _add_child(self, space_id: str, room_id: str, token: str) -> None: """Add a child room to a space.""" self.helper.send_state( @@ -128,43 +141,32 @@ def _assert_events( def test_simple_space(self): """Test a simple space with a single room.""" - space = self.helper.create_room_as(self.user, tok=self.token) - room = self.helper.create_room_as(self.user, tok=self.token) - self._add_child(space, room, self.token) - - result = self.get_success(self.handler.get_space_summary(self.user, space)) + result = self.get_success(self.handler.get_space_summary(self.user, self.space)) # The result should have the space and the room in it, along with a link # from space -> room. - self._assert_rooms(result, [space, room]) - self._assert_events(result, [(space, room)]) + self._assert_rooms(result, [self.space, self.room]) + self._assert_events(result, [(self.space, self.room)]) def test_visibility(self): """A user not in a space cannot inspect it.""" - space = self.helper.create_room_as(self.user, tok=self.token) - room = self.helper.create_room_as(self.user, tok=self.token) - self._add_child(space, room, self.token) - user2 = self.register_user("user2", "pass") token2 = self.login("user2", "pass") # The user cannot see the space. - self.get_failure(self.handler.get_space_summary(user2, space), AuthError) + self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError) # Joining the room causes it to be visible. - self.helper.join(space, user2, tok=token2) - result = self.get_success(self.handler.get_space_summary(user2, space)) + self.helper.join(self.space, user2, tok=token2) + result = self.get_success(self.handler.get_space_summary(user2, self.space)) # The result should only have the space, but includes the link to the room. - self._assert_rooms(result, [space]) - self._assert_events(result, [(space, room)]) + self._assert_rooms(result, [self.space]) + self._assert_events(result, [(self.space, self.room)]) def test_world_readable(self): """A world-readable room is visible to everyone.""" - space = self.helper.create_room_as(self.user, tok=self.token) - room = self.helper.create_room_as(self.user, tok=self.token) - self._add_child(space, room, self.token) self.helper.send_state( - space, + self.space, event_type="m.room.history_visibility", body={"history_visibility": "world_readable"}, tok=self.token, @@ -173,6 +175,6 @@ def test_world_readable(self): user2 = self.register_user("user2", "pass") # The space should be visible, as well as the link to the room. - result = self.get_success(self.handler.get_space_summary(user2, space)) - self._assert_rooms(result, [space]) - self._assert_events(result, [(space, room)]) + result = self.get_success(self.handler.get_space_summary(user2, self.space)) + self._assert_rooms(result, [self.space]) + self._assert_events(result, [(self.space, self.room)]) diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index ed55a640af..69798e95c3 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -52,6 +52,7 @@ def create_room_as( room_version: str = None, tok: str = None, expect_code: int = 200, + extra_content: Optional[Dict] = None, ) -> str: """ Create a room. @@ -72,7 +73,7 @@ def create_room_as( temp_id = self.auth_user_id self.auth_user_id = room_creator path = "/_matrix/client/r0/createRoom" - content = {} + content = extra_content or {} if not is_public: content["visibility"] = "private" if room_version: From 85d237eba789a667109ced140026d2494b210310 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Jun 2021 19:15:47 +0100 Subject: [PATCH 333/619] Add a distributed lock (#10269) This adds a simple best effort locking mechanism that works cross workers. --- changelog.d/10269.misc | 1 + synapse/app/generic_worker.py | 2 + synapse/storage/databases/main/__init__.py | 2 + synapse/storage/databases/main/lock.py | 334 ++++++++++++++++++ .../storage/schema/main/delta/59/15locks.sql | 37 ++ tests/storage/databases/main/test_lock.py | 100 ++++++ 6 files changed, 476 insertions(+) create mode 100644 changelog.d/10269.misc create mode 100644 synapse/storage/databases/main/lock.py create mode 100644 synapse/storage/schema/main/delta/59/15locks.sql create mode 100644 tests/storage/databases/main/test_lock.py diff --git a/changelog.d/10269.misc b/changelog.d/10269.misc new file mode 100644 index 0000000000..23e590490c --- /dev/null +++ b/changelog.d/10269.misc @@ -0,0 +1 @@ +Add a distributed lock implementation. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index af8a1833f3..5b041fcaad 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -108,6 +108,7 @@ from synapse.storage.databases.main.censor_events import CensorEventsStore from synapse.storage.databases.main.client_ips import ClientIpWorkerStore from synapse.storage.databases.main.e2e_room_keys import EndToEndRoomKeyStore +from synapse.storage.databases.main.lock import LockStore from synapse.storage.databases.main.media_repository import MediaRepositoryStore from synapse.storage.databases.main.metrics import ServerMetricsStore from synapse.storage.databases.main.monthly_active_users import ( @@ -249,6 +250,7 @@ class GenericWorkerSlavedStore( ServerMetricsStore, SearchStore, TransactionWorkerStore, + LockStore, BaseSlavedStore, ): pass diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index 9cce62ae6c..a3fddea042 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -46,6 +46,7 @@ from .filtering import FilteringStore from .group_server import GroupServerStore from .keys import KeyStore +from .lock import LockStore from .media_repository import MediaRepositoryStore from .metrics import ServerMetricsStore from .monthly_active_users import MonthlyActiveUsersStore @@ -119,6 +120,7 @@ class DataStore( CacheInvalidationWorkerStore, ServerMetricsStore, EventForwardExtremitiesStore, + LockStore, ): def __init__(self, database: DatabasePool, db_conn, hs): self.hs = hs diff --git a/synapse/storage/databases/main/lock.py b/synapse/storage/databases/main/lock.py new file mode 100644 index 0000000000..e76188328c --- /dev/null +++ b/synapse/storage/databases/main/lock.py @@ -0,0 +1,334 @@ +# Copyright 2021 Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from types import TracebackType +from typing import TYPE_CHECKING, Dict, Optional, Tuple, Type + +from twisted.internet.interfaces import IReactorCore + +from synapse.metrics.background_process_metrics import wrap_as_background_process +from synapse.storage._base import SQLBaseStore +from synapse.storage.database import DatabasePool, LoggingTransaction +from synapse.storage.types import Connection +from synapse.util import Clock +from synapse.util.stringutils import random_string + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +logger = logging.getLogger(__name__) + + +# How often to renew an acquired lock by updating the `last_renewed_ts` time in +# the lock table. +_RENEWAL_INTERVAL_MS = 30 * 1000 + +# How long before an acquired lock times out. +_LOCK_TIMEOUT_MS = 2 * 60 * 1000 + + +class LockStore(SQLBaseStore): + """Provides a best effort distributed lock between worker instances. + + Locks are identified by a name and key. A lock is acquired by inserting into + the `worker_locks` table if a) there is no existing row for the name/key or + b) the existing row has a `last_renewed_ts` older than `_LOCK_TIMEOUT_MS`. + + When a lock is taken out the instance inserts a random `token`, the instance + that holds that token holds the lock until it drops (or times out). + + The instance that holds the lock should regularly update the + `last_renewed_ts` column with the current time. + """ + + def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer"): + super().__init__(database, db_conn, hs) + + self._reactor = hs.get_reactor() + self._instance_name = hs.get_instance_id() + + # A map from `(lock_name, lock_key)` to the token of any locks that we + # think we currently hold. + self._live_tokens: Dict[Tuple[str, str], str] = {} + + # When we shut down we want to remove the locks. Technically this can + # lead to a race, as we may drop the lock while we are still processing. + # However, a) it should be a small window, b) the lock is best effort + # anyway and c) we want to really avoid leaking locks when we restart. + hs.get_reactor().addSystemEventTrigger( + "before", + "shutdown", + self._on_shutdown, + ) + + @wrap_as_background_process("LockStore._on_shutdown") + async def _on_shutdown(self) -> None: + """Called when the server is shutting down""" + logger.info("Dropping held locks due to shutdown") + + for (lock_name, lock_key), token in self._live_tokens.items(): + await self._drop_lock(lock_name, lock_key, token) + + logger.info("Dropped locks due to shutdown") + + async def try_acquire_lock(self, lock_name: str, lock_key: str) -> Optional["Lock"]: + """Try to acquire a lock for the given name/key. Will return an async + context manager if the lock is successfully acquired, which *must* be + used (otherwise the lock will leak). + """ + + now = self._clock.time_msec() + token = random_string(6) + + if self.db_pool.engine.can_native_upsert: + + def _try_acquire_lock_txn(txn: LoggingTransaction) -> bool: + # We take out the lock if either a) there is no row for the lock + # already or b) the existing row has timed out. + sql = """ + INSERT INTO worker_locks (lock_name, lock_key, instance_name, token, last_renewed_ts) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT (lock_name, lock_key) + DO UPDATE + SET + token = EXCLUDED.token, + instance_name = EXCLUDED.instance_name, + last_renewed_ts = EXCLUDED.last_renewed_ts + WHERE + worker_locks.last_renewed_ts < ? + """ + txn.execute( + sql, + ( + lock_name, + lock_key, + self._instance_name, + token, + now, + now - _LOCK_TIMEOUT_MS, + ), + ) + + # We only acquired the lock if we inserted or updated the table. + return bool(txn.rowcount) + + did_lock = await self.db_pool.runInteraction( + "try_acquire_lock", + _try_acquire_lock_txn, + # We can autocommit here as we're executing a single query, this + # will avoid serialization errors. + db_autocommit=True, + ) + if not did_lock: + return None + + else: + # If we're on an old SQLite we emulate the above logic by first + # clearing out any existing stale locks and then upserting. + + def _try_acquire_lock_emulated_txn(txn: LoggingTransaction) -> bool: + sql = """ + DELETE FROM worker_locks + WHERE + lock_name = ? + AND lock_key = ? + AND last_renewed_ts < ? + """ + txn.execute( + sql, + (lock_name, lock_key, now - _LOCK_TIMEOUT_MS), + ) + + inserted = self.db_pool.simple_upsert_txn_emulated( + txn, + table="worker_locks", + keyvalues={ + "lock_name": lock_name, + "lock_key": lock_key, + }, + values={}, + insertion_values={ + "token": token, + "last_renewed_ts": self._clock.time_msec(), + "instance_name": self._instance_name, + }, + ) + + return inserted + + did_lock = await self.db_pool.runInteraction( + "try_acquire_lock_emulated", _try_acquire_lock_emulated_txn + ) + + if not did_lock: + return None + + self._live_tokens[(lock_name, lock_key)] = token + + return Lock( + self._reactor, + self._clock, + self, + lock_name=lock_name, + lock_key=lock_key, + token=token, + ) + + async def _is_lock_still_valid( + self, lock_name: str, lock_key: str, token: str + ) -> bool: + """Checks whether this instance still holds the lock.""" + last_renewed_ts = await self.db_pool.simple_select_one_onecol( + table="worker_locks", + keyvalues={ + "lock_name": lock_name, + "lock_key": lock_key, + "token": token, + }, + retcol="last_renewed_ts", + allow_none=True, + desc="is_lock_still_valid", + ) + return ( + last_renewed_ts is not None + and self._clock.time_msec() - _LOCK_TIMEOUT_MS < last_renewed_ts + ) + + async def _renew_lock(self, lock_name: str, lock_key: str, token: str) -> None: + """Attempt to renew the lock if we still hold it.""" + await self.db_pool.simple_update( + table="worker_locks", + keyvalues={ + "lock_name": lock_name, + "lock_key": lock_key, + "token": token, + }, + updatevalues={"last_renewed_ts": self._clock.time_msec()}, + desc="renew_lock", + ) + + async def _drop_lock(self, lock_name: str, lock_key: str, token: str) -> None: + """Attempt to drop the lock, if we still hold it""" + await self.db_pool.simple_delete( + table="worker_locks", + keyvalues={ + "lock_name": lock_name, + "lock_key": lock_key, + "token": token, + }, + desc="drop_lock", + ) + + self._live_tokens.pop((lock_name, lock_key), None) + + +class Lock: + """An async context manager that manages an acquired lock, ensuring it is + regularly renewed and dropping it when the context manager exits. + + The lock object has an `is_still_valid` method which can be used to + double-check the lock is still valid, if e.g. processing work in a loop. + + For example: + + lock = await self.store.try_acquire_lock(...) + if not lock: + return + + async with lock: + for item in work: + await process(item) + + if not await lock.is_still_valid(): + break + """ + + def __init__( + self, + reactor: IReactorCore, + clock: Clock, + store: LockStore, + lock_name: str, + lock_key: str, + token: str, + ) -> None: + self._reactor = reactor + self._clock = clock + self._store = store + self._lock_name = lock_name + self._lock_key = lock_key + + self._token = token + + self._looping_call = clock.looping_call( + self._renew, _RENEWAL_INTERVAL_MS, store, lock_name, lock_key, token + ) + + self._dropped = False + + @staticmethod + @wrap_as_background_process("Lock._renew") + async def _renew( + store: LockStore, + lock_name: str, + lock_key: str, + token: str, + ) -> None: + """Renew the lock. + + Note: this is a static method, rather than using self.*, so that we + don't end up with a reference to `self` in the reactor, which would stop + this from being cleaned up if we dropped the context manager. + """ + await store._renew_lock(lock_name, lock_key, token) + + async def is_still_valid(self) -> bool: + """Check if the lock is still held by us""" + return await self._store._is_lock_still_valid( + self._lock_name, self._lock_key, self._token + ) + + async def __aenter__(self) -> None: + if self._dropped: + raise Exception("Cannot reuse a Lock object") + + async def __aexit__( + self, + _exctype: Optional[Type[BaseException]], + _excinst: Optional[BaseException], + _exctb: Optional[TracebackType], + ) -> bool: + if self._looping_call.running: + self._looping_call.stop() + + await self._store._drop_lock(self._lock_name, self._lock_key, self._token) + self._dropped = True + + return False + + def __del__(self) -> None: + if not self._dropped: + # We should not be dropped without the lock being released (unless + # we're shutting down), but if we are then let's at least stop + # renewing the lock. + if self._looping_call.running: + self._looping_call.stop() + + if self._reactor.running: + logger.error( + "Lock for (%s, %s) dropped without being released", + self._lock_name, + self._lock_key, + ) diff --git a/synapse/storage/schema/main/delta/59/15locks.sql b/synapse/storage/schema/main/delta/59/15locks.sql new file mode 100644 index 0000000000..8b2999ff3e --- /dev/null +++ b/synapse/storage/schema/main/delta/59/15locks.sql @@ -0,0 +1,37 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +-- A noddy implementation of a distributed lock across workers. While a worker +-- has taken a lock out they should regularly update the `last_renewed_ts` +-- column, a lock will be considered dropped if `last_renewed_ts` is from ages +-- ago. +CREATE TABLE worker_locks ( + lock_name TEXT NOT NULL, + lock_key TEXT NOT NULL, + -- We write the instance name to ease manual debugging, we don't ever read + -- from it. + -- Note: instance names aren't guarenteed to be unique. + instance_name TEXT NOT NULL, + -- A random string generated each time an instance takes out a lock. Used by + -- the instance to tell whether the lock is still held by it (e.g. in the + -- case where the process stalls for a long time the lock may time out and + -- be taken out by another instance, at which point the original instance + -- can tell it no longer holds the lock as the tokens no longer match). + token TEXT NOT NULL, + last_renewed_ts BIGINT NOT NULL +); + +CREATE UNIQUE INDEX worker_locks_key ON worker_locks (lock_name, lock_key); diff --git a/tests/storage/databases/main/test_lock.py b/tests/storage/databases/main/test_lock.py new file mode 100644 index 0000000000..9ca70e7367 --- /dev/null +++ b/tests/storage/databases/main/test_lock.py @@ -0,0 +1,100 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.server import HomeServer +from synapse.storage.databases.main.lock import _LOCK_TIMEOUT_MS + +from tests import unittest + + +class LockTestCase(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, hs: HomeServer): + self.store = hs.get_datastore() + + def test_simple_lock(self): + """Test that we can take out a lock and that while we hold it nobody + else can take it out. + """ + # First to acquire this lock, so it should complete + lock = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNotNone(lock) + + # Enter the context manager + self.get_success(lock.__aenter__()) + + # Attempting to acquire the lock again fails. + lock2 = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNone(lock2) + + # Calling `is_still_valid` reports true. + self.assertTrue(self.get_success(lock.is_still_valid())) + + # Drop the lock + self.get_success(lock.__aexit__(None, None, None)) + + # We can now acquire the lock again. + lock3 = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNotNone(lock3) + self.get_success(lock3.__aenter__()) + self.get_success(lock3.__aexit__(None, None, None)) + + def test_maintain_lock(self): + """Test that we don't time out locks while they're still active""" + + lock = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNotNone(lock) + + self.get_success(lock.__aenter__()) + + # Wait for ages with the lock, we should not be able to get the lock. + self.reactor.advance(5 * _LOCK_TIMEOUT_MS / 1000) + + lock2 = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNone(lock2) + + self.get_success(lock.__aexit__(None, None, None)) + + def test_timeout_lock(self): + """Test that we time out locks if they're not updated for ages""" + + lock = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNotNone(lock) + + self.get_success(lock.__aenter__()) + + # We simulate the process getting stuck by cancelling the looping call + # that keeps the lock active. + lock._looping_call.stop() + + # Wait for the lock to timeout. + self.reactor.advance(2 * _LOCK_TIMEOUT_MS / 1000) + + lock2 = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNotNone(lock2) + + self.assertFalse(self.get_success(lock.is_still_valid())) + + def test_drop(self): + """Test that dropping the context manager means we stop renewing the lock""" + + lock = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNotNone(lock) + + del lock + + # Wait for the lock to timeout. + self.reactor.advance(2 * _LOCK_TIMEOUT_MS / 1000) + + lock2 = self.get_success(self.store.try_acquire_lock("name", "key")) + self.assertIsNotNone(lock2) From c54db67d0ea5b5967b7ea918c66a222a75b8ced1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 29 Jun 2021 19:55:22 +0100 Subject: [PATCH 334/619] Handle inbound events from federation asynchronously (#10272) Fixes #9490 This will break a couple of SyTest that are expecting failures to be added to the response of a federation /send, which obviously doesn't happen now that things are asynchronous. Two drawbacks: Currently there is no logic to handle any events left in the staging area after restart, and so they'll only be handled on the next incoming event in that room. That can be fixed separately. We now only process one event per room at a time. This can be fixed up further down the line. --- changelog.d/10272.bugfix | 1 + synapse/federation/federation_server.py | 98 +++++++++++++++- .../databases/main/event_federation.py | 109 +++++++++++++++++- .../delta/59/16federation_inbound_staging.sql | 32 +++++ sytest-blacklist | 6 + 5 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10272.bugfix create mode 100644 synapse/storage/schema/main/delta/59/16federation_inbound_staging.sql diff --git a/changelog.d/10272.bugfix b/changelog.d/10272.bugfix new file mode 100644 index 0000000000..3cefa05788 --- /dev/null +++ b/changelog.d/10272.bugfix @@ -0,0 +1 @@ +Handle inbound events from federation asynchronously. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 2b07f18529..1d050e54e2 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -44,7 +44,7 @@ SynapseError, UnsupportedRoomVersionError, ) -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.events import EventBase from synapse.federation.federation_base import FederationBase, event_from_pdu_json from synapse.federation.persistence import TransactionActions @@ -57,10 +57,12 @@ ) from synapse.logging.opentracing import log_kv, start_active_span_from_edu, trace from synapse.logging.utils import log_function +from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.replication.http.federation import ( ReplicationFederationSendEduRestServlet, ReplicationGetQueryRestServlet, ) +from synapse.storage.databases.main.lock import Lock from synapse.types import JsonDict from synapse.util import glob_to_regex, json_decoder, unwrapFirstError from synapse.util.async_helpers import Linearizer, concurrently_execute @@ -96,6 +98,11 @@ ) +# The name of the lock to use when process events in a room received over +# federation. +_INBOUND_EVENT_HANDLING_LOCK_NAME = "federation_inbound_pdu" + + class FederationServer(FederationBase): def __init__(self, hs: "HomeServer"): super().__init__(hs) @@ -834,7 +841,94 @@ async def _handle_received_pdu(self, origin: str, pdu: EventBase) -> None: except SynapseError as e: raise FederationError("ERROR", e.code, e.msg, affected=pdu.event_id) - await self.handler.on_receive_pdu(origin, pdu, sent_to_us_directly=True) + # Add the event to our staging area + await self.store.insert_received_event_to_staging(origin, pdu) + + # Try and acquire the processing lock for the room, if we get it start a + # background process for handling the events in the room. + lock = await self.store.try_acquire_lock( + _INBOUND_EVENT_HANDLING_LOCK_NAME, pdu.room_id + ) + if lock: + self._process_incoming_pdus_in_room_inner( + pdu.room_id, room_version, lock, origin, pdu + ) + + @wrap_as_background_process("_process_incoming_pdus_in_room_inner") + async def _process_incoming_pdus_in_room_inner( + self, + room_id: str, + room_version: RoomVersion, + lock: Lock, + latest_origin: str, + latest_event: EventBase, + ) -> None: + """Process events in the staging area for the given room. + + The latest_origin and latest_event args are the latest origin and event + received. + """ + + # The common path is for the event we just received be the only event in + # the room, so instead of pulling the event out of the DB and parsing + # the event we just pull out the next event ID and check if that matches. + next_origin, next_event_id = await self.store.get_next_staged_event_id_for_room( + room_id + ) + if next_origin == latest_origin and next_event_id == latest_event.event_id: + origin = latest_origin + event = latest_event + else: + next = await self.store.get_next_staged_event_for_room( + room_id, room_version + ) + if not next: + return + + origin, event = next + + # We loop round until there are no more events in the room in the + # staging area, or we fail to get the lock (which means another process + # has started processing). + while True: + async with lock: + try: + await self.handler.on_receive_pdu( + origin, event, sent_to_us_directly=True + ) + except FederationError as e: + # XXX: Ideally we'd inform the remote we failed to process + # the event, but we can't return an error in the transaction + # response (as we've already responded). + logger.warning("Error handling PDU %s: %s", event.event_id, e) + except Exception: + f = failure.Failure() + logger.error( + "Failed to handle PDU %s", + event.event_id, + exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore + ) + + await self.store.remove_received_event_from_staging( + origin, event.event_id + ) + + # We need to do this check outside the lock to avoid a race between + # a new event being inserted by another instance and it attempting + # to acquire the lock. + next = await self.store.get_next_staged_event_for_room( + room_id, room_version + ) + if not next: + break + + origin, event = next + + lock = await self.store.try_acquire_lock( + _INBOUND_EVENT_HANDLING_LOCK_NAME, room_id + ) + if not lock: + return def __str__(self) -> str: return "" % self.server_name diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index c0ea445550..f23f8c6ecf 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -14,18 +14,20 @@ import itertools import logging from queue import Empty, PriorityQueue -from typing import Collection, Dict, Iterable, List, Set, Tuple +from typing import Collection, Dict, Iterable, List, Optional, Set, Tuple from synapse.api.constants import MAX_DEPTH from synapse.api.errors import StoreError -from synapse.events import EventBase +from synapse.api.room_versions import RoomVersion +from synapse.events import EventBase, make_event_from_dict from synapse.metrics.background_process_metrics import wrap_as_background_process -from synapse.storage._base import SQLBaseStore, make_in_list_sql_clause +from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause from synapse.storage.database import DatabasePool, LoggingTransaction from synapse.storage.databases.main.events_worker import EventsWorkerStore from synapse.storage.databases.main.signatures import SignatureWorkerStore from synapse.storage.engines import PostgresEngine from synapse.storage.types import Cursor +from synapse.util import json_encoder from synapse.util.caches.descriptors import cached from synapse.util.caches.lrucache import LruCache from synapse.util.iterutils import batch_iter @@ -1044,6 +1046,107 @@ def _delete_old_forward_extrem_cache_txn(txn): _delete_old_forward_extrem_cache_txn, ) + async def insert_received_event_to_staging( + self, origin: str, event: EventBase + ) -> None: + """Insert a newly received event from federation into the staging area.""" + + # We use an upsert here to handle the case where we see the same event + # from the same server multiple times. + await self.db_pool.simple_upsert( + table="federation_inbound_events_staging", + keyvalues={ + "origin": origin, + "event_id": event.event_id, + }, + values={}, + insertion_values={ + "room_id": event.room_id, + "received_ts": self._clock.time_msec(), + "event_json": json_encoder.encode(event.get_dict()), + "internal_metadata": json_encoder.encode( + event.internal_metadata.get_dict() + ), + }, + desc="insert_received_event_to_staging", + ) + + async def remove_received_event_from_staging( + self, + origin: str, + event_id: str, + ) -> None: + """Remove the given event from the staging area""" + await self.db_pool.simple_delete( + table="federation_inbound_events_staging", + keyvalues={ + "origin": origin, + "event_id": event_id, + }, + desc="remove_received_event_from_staging", + ) + + async def get_next_staged_event_id_for_room( + self, + room_id: str, + ) -> Optional[Tuple[str, str]]: + """Get the next event ID in the staging area for the given room.""" + + def _get_next_staged_event_id_for_room_txn(txn): + sql = """ + SELECT origin, event_id + FROM federation_inbound_events_staging + WHERE room_id = ? + ORDER BY received_ts ASC + LIMIT 1 + """ + + txn.execute(sql, (room_id,)) + + return txn.fetchone() + + return await self.db_pool.runInteraction( + "get_next_staged_event_id_for_room", _get_next_staged_event_id_for_room_txn + ) + + async def get_next_staged_event_for_room( + self, + room_id: str, + room_version: RoomVersion, + ) -> Optional[Tuple[str, EventBase]]: + """Get the next event in the staging area for the given room.""" + + def _get_next_staged_event_for_room_txn(txn): + sql = """ + SELECT event_json, internal_metadata, origin + FROM federation_inbound_events_staging + WHERE room_id = ? + ORDER BY received_ts ASC + LIMIT 1 + """ + txn.execute(sql, (room_id,)) + + return txn.fetchone() + + row = await self.db_pool.runInteraction( + "get_next_staged_event_for_room", _get_next_staged_event_for_room_txn + ) + + if not row: + return None + + event_d = db_to_json(row[0]) + internal_metadata_d = db_to_json(row[1]) + origin = row[2] + + event = make_event_from_dict( + event_dict=event_d, + room_version=room_version, + internal_metadata_dict=internal_metadata_d, + ) + + return origin, event + class EventFederationStore(EventFederationWorkerStore): """Responsible for storing and serving up the various graphs associated diff --git a/synapse/storage/schema/main/delta/59/16federation_inbound_staging.sql b/synapse/storage/schema/main/delta/59/16federation_inbound_staging.sql new file mode 100644 index 0000000000..43bc5c025f --- /dev/null +++ b/synapse/storage/schema/main/delta/59/16federation_inbound_staging.sql @@ -0,0 +1,32 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +-- A staging area for newly received events over federation. +-- +-- Note we may store the same event multiple times if it comes from different +-- servers; this is to handle the case if we get a redacted and non-redacted +-- versions of the event. +CREATE TABLE federation_inbound_events_staging ( + origin TEXT NOT NULL, + room_id TEXT NOT NULL, + event_id TEXT NOT NULL, + received_ts BIGINT NOT NULL, + event_json TEXT NOT NULL, + internal_metadata TEXT NOT NULL +); + +CREATE INDEX federation_inbound_events_staging_room ON federation_inbound_events_staging(room_id, received_ts); +CREATE UNIQUE INDEX federation_inbound_events_staging_instance_event ON federation_inbound_events_staging(origin, event_id); diff --git a/sytest-blacklist b/sytest-blacklist index de9986357b..89c4e828fd 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -41,3 +41,9 @@ We can't peek into rooms with invited history_visibility We can't peek into rooms with joined history_visibility Local users can peek by room alias Peeked rooms only turn up in the sync for the device who peeked them + + +# Blacklisted due to changes made in #10272 +Outbound federation will ignore a missing event with bad JSON for room version 6 +Backfilled events whose prev_events are in a different room do not allow cross-room back-pagination +Federation rejects inbound events where the prev_events cannot be found From f99e9cc2da6afe49ed7a1fbe18ab08e68befa614 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 29 Jun 2021 19:58:25 +0100 Subject: [PATCH 335/619] v1.37.1a1 --- synapse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/__init__.py b/synapse/__init__.py index c865d2e100..0900492619 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.37.0" +__version__ = "1.37.1a1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From ba9b744bb22e5698572cf2278904412168a7d3fc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 29 Jun 2021 20:02:39 +0100 Subject: [PATCH 336/619] Update newsfiles --- changelog.d/10269.bugfix | 1 + changelog.d/10269.misc | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/10269.bugfix delete mode 100644 changelog.d/10269.misc diff --git a/changelog.d/10269.bugfix b/changelog.d/10269.bugfix new file mode 100644 index 0000000000..3cefa05788 --- /dev/null +++ b/changelog.d/10269.bugfix @@ -0,0 +1 @@ +Handle inbound events from federation asynchronously. diff --git a/changelog.d/10269.misc b/changelog.d/10269.misc deleted file mode 100644 index 23e590490c..0000000000 --- a/changelog.d/10269.misc +++ /dev/null @@ -1 +0,0 @@ -Add a distributed lock implementation. From d561367c18db3300804dee182e74b4a8fb7998e6 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 29 Jun 2021 21:39:30 +0100 Subject: [PATCH 337/619] 1.37.1rc1 --- CHANGES.md | 9 +++++++++ changelog.d/10269.bugfix | 1 - changelog.d/10272.bugfix | 1 - synapse/__init__.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/10269.bugfix delete mode 100644 changelog.d/10272.bugfix diff --git a/CHANGES.md b/CHANGES.md index eac91ffe02..8de3bad906 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.37.1rc1 (2021-06-29) +============================== + +Features +-------- + +- Handle inbound events from federation asynchronously. ([\#10269](https://github.com/matrix-org/synapse/issues/10269), [\#10272](https://github.com/matrix-org/synapse/issues/10272)) + + Synapse 1.37.0 (2021-06-29) =========================== diff --git a/changelog.d/10269.bugfix b/changelog.d/10269.bugfix deleted file mode 100644 index 3cefa05788..0000000000 --- a/changelog.d/10269.bugfix +++ /dev/null @@ -1 +0,0 @@ -Handle inbound events from federation asynchronously. diff --git a/changelog.d/10272.bugfix b/changelog.d/10272.bugfix deleted file mode 100644 index 3cefa05788..0000000000 --- a/changelog.d/10272.bugfix +++ /dev/null @@ -1 +0,0 @@ -Handle inbound events from federation asynchronously. diff --git a/synapse/__init__.py b/synapse/__init__.py index 0900492619..2070724c34 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.37.1a1" +__version__ = "1.37.1rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 329ef5c715d81b538e8b071de046c698a82eae10 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Jun 2021 12:07:16 +0100 Subject: [PATCH 338/619] Fix the inbound PDU metric (#10279) This broke in #10272 --- changelog.d/10279.bugfix | 1 + synapse/federation/federation_server.py | 37 ++++++----- .../databases/main/event_federation.py | 66 ++++++++++++++++--- synapse/storage/engines/_base.py | 6 ++ synapse/storage/engines/postgres.py | 5 ++ synapse/storage/engines/sqlite.py | 5 ++ 6 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 changelog.d/10279.bugfix diff --git a/changelog.d/10279.bugfix b/changelog.d/10279.bugfix new file mode 100644 index 0000000000..ac8b64ead9 --- /dev/null +++ b/changelog.d/10279.bugfix @@ -0,0 +1 @@ +Fix the prometheus `synapse_federation_server_pdu_process_time` metric. Broke in v1.37.1. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 742d29291e..e93b7577fe 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -369,22 +369,21 @@ async def process_pdus_for_room(room_id: str): async def process_pdu(pdu: EventBase) -> JsonDict: event_id = pdu.event_id - with pdu_process_time.time(): - with nested_logging_context(event_id): - try: - await self._handle_received_pdu(origin, pdu) - return {} - except FederationError as e: - logger.warning("Error handling PDU %s: %s", event_id, e) - return {"error": str(e)} - except Exception as e: - f = failure.Failure() - logger.error( - "Failed to handle PDU %s", - event_id, - exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore - ) - return {"error": str(e)} + with nested_logging_context(event_id): + try: + await self._handle_received_pdu(origin, pdu) + return {} + except FederationError as e: + logger.warning("Error handling PDU %s: %s", event_id, e) + return {"error": str(e)} + except Exception as e: + f = failure.Failure() + logger.error( + "Failed to handle PDU %s", + event_id, + exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore + ) + return {"error": str(e)} await concurrently_execute( process_pdus_for_room, pdus_by_room.keys(), TRANSACTION_CONCURRENCY_LIMIT @@ -932,9 +931,13 @@ async def _process_incoming_pdus_in_room_inner( exc_info=(f.type, f.value, f.getTracebackObject()), # type: ignore ) - await self.store.remove_received_event_from_staging( + received_ts = await self.store.remove_received_event_from_staging( origin, event.event_id ) + if received_ts is not None: + pdu_process_time.observe( + (self._clock.time_msec() - received_ts) / 1000 + ) # We need to do this check outside the lock to avoid a race between # a new event being inserted by another instance and it attempting diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index f23f8c6ecf..f2d27ee893 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -1075,16 +1075,62 @@ async def remove_received_event_from_staging( self, origin: str, event_id: str, - ) -> None: - """Remove the given event from the staging area""" - await self.db_pool.simple_delete( - table="federation_inbound_events_staging", - keyvalues={ - "origin": origin, - "event_id": event_id, - }, - desc="remove_received_event_from_staging", - ) + ) -> Optional[int]: + """Remove the given event from the staging area. + + Returns: + The received_ts of the row that was deleted, if any. + """ + if self.db_pool.engine.supports_returning: + + def _remove_received_event_from_staging_txn(txn): + sql = """ + DELETE FROM federation_inbound_events_staging + WHERE origin = ? AND event_id = ? + RETURNING received_ts + """ + + txn.execute(sql, (origin, event_id)) + return txn.fetchone() + + row = await self.db_pool.runInteraction( + "remove_received_event_from_staging", + _remove_received_event_from_staging_txn, + db_autocommit=True, + ) + if row is None: + return None + + return row[0] + + else: + + def _remove_received_event_from_staging_txn(txn): + received_ts = self.db_pool.simple_select_one_onecol_txn( + txn, + table="federation_inbound_events_staging", + keyvalues={ + "origin": origin, + "event_id": event_id, + }, + retcol="received_ts", + allow_none=True, + ) + self.db_pool.simple_delete_txn( + txn, + table="federation_inbound_events_staging", + keyvalues={ + "origin": origin, + "event_id": event_id, + }, + ) + + return received_ts + + return await self.db_pool.runInteraction( + "remove_received_event_from_staging", + _remove_received_event_from_staging_txn, + ) async def get_next_staged_event_id_for_room( self, diff --git a/synapse/storage/engines/_base.py b/synapse/storage/engines/_base.py index 1882bfd9cf..20cd63c330 100644 --- a/synapse/storage/engines/_base.py +++ b/synapse/storage/engines/_base.py @@ -49,6 +49,12 @@ def supports_using_any_list(self) -> bool: """ ... + @property + @abc.abstractmethod + def supports_returning(self) -> bool: + """Do we support the `RETURNING` clause in insert/update/delete?""" + ... + @abc.abstractmethod def check_database( self, db_conn: ConnectionType, allow_outdated_version: bool = False diff --git a/synapse/storage/engines/postgres.py b/synapse/storage/engines/postgres.py index 21411c5fea..30f948a0f7 100644 --- a/synapse/storage/engines/postgres.py +++ b/synapse/storage/engines/postgres.py @@ -133,6 +133,11 @@ def supports_using_any_list(self): """Do we support using `a = ANY(?)` and passing a list""" return True + @property + def supports_returning(self) -> bool: + """Do we support the `RETURNING` clause in insert/update/delete?""" + return True + def is_deadlock(self, error): if isinstance(error, self.module.DatabaseError): # https://www.postgresql.org/docs/current/static/errcodes-appendix.html diff --git a/synapse/storage/engines/sqlite.py b/synapse/storage/engines/sqlite.py index 5fe1b205e1..70d17d4f2c 100644 --- a/synapse/storage/engines/sqlite.py +++ b/synapse/storage/engines/sqlite.py @@ -60,6 +60,11 @@ def supports_using_any_list(self): """Do we support using `a = ANY(?)` and passing a list""" return False + @property + def supports_returning(self) -> bool: + """Do we support the `RETURNING` clause in insert/update/delete?""" + return self.module.sqlite_version_info >= (3, 35, 0) + def check_database(self, db_conn, allow_outdated_version: bool = False): if not allow_outdated_version: version = self.module.sqlite_version_info From aaf7d1acb8804ddeeb007e21c2b2c915bd494898 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 30 Jun 2021 07:08:42 -0400 Subject: [PATCH 339/619] Correct type hints for synapse.event_auth. (#10253) --- changelog.d/10253.misc | 1 + synapse/api/auth.py | 5 +-- synapse/event_auth.py | 5 +-- synapse/events/__init__.py | 2 +- synapse/events/builder.py | 69 +++++++++++++++++++------------------ synapse/handlers/message.py | 7 ++++ 6 files changed, 51 insertions(+), 38 deletions(-) create mode 100644 changelog.d/10253.misc diff --git a/changelog.d/10253.misc b/changelog.d/10253.misc new file mode 100644 index 0000000000..44d9217245 --- /dev/null +++ b/changelog.d/10253.misc @@ -0,0 +1 @@ +Fix type hints for computing auth events. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 29cf257633..f8b068e563 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union import pymacaroons from netaddr import IPAddress @@ -31,6 +31,7 @@ from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.appservice import ApplicationService from synapse.events import EventBase +from synapse.events.builder import EventBuilder from synapse.http import get_request_user_agent from synapse.http.site import SynapseRequest from synapse.logging import opentracing as opentracing @@ -490,7 +491,7 @@ async def is_server_admin(self, user: UserID) -> bool: def compute_auth_events( self, - event, + event: Union[EventBase, EventBuilder], current_state_ids: StateMap[str], for_verification: bool = False, ) -> List[str]: diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 33d7c60241..89bcf81515 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -14,7 +14,7 @@ # limitations under the License. import logging -from typing import Any, Dict, List, Optional, Set, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple, Union from canonicaljson import encode_canonical_json from signedjson.key import decode_verify_key_bytes @@ -29,6 +29,7 @@ RoomVersion, ) from synapse.events import EventBase +from synapse.events.builder import EventBuilder from synapse.types import StateMap, UserID, get_domain_from_id logger = logging.getLogger(__name__) @@ -724,7 +725,7 @@ def get_public_keys(invite_event: EventBase) -> List[Dict[str, Any]]: return public_keys -def auth_types_for_event(event: EventBase) -> Set[Tuple[str, str]]: +def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str, str]]: """Given an event, return a list of (EventType, StateKey) that may be needed to auth the event. The returned list may be a superset of what would actually be required depending on the full state of the room. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 0cb9c1cc1e..6286ad999a 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -118,7 +118,7 @@ def __init__(self, internal_metadata_dict: JsonDict): proactively_send = DictProperty("proactively_send") # type: bool redacted = DictProperty("redacted") # type: bool txn_id = DictProperty("txn_id") # type: str - token_id = DictProperty("token_id") # type: str + token_id = DictProperty("token_id") # type: int historical = DictProperty("historical") # type: bool # XXX: These are set by StreamWorkerStore._set_before_and_after. diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 81bf8615b7..fb48ec8541 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -12,12 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union import attr from nacl.signing import SigningKey -from synapse.api.auth import Auth from synapse.api.constants import MAX_DEPTH from synapse.api.errors import UnsupportedRoomVersionError from synapse.api.room_versions import ( @@ -34,10 +33,14 @@ from synapse.util import Clock from synapse.util.stringutils import random_string +if TYPE_CHECKING: + from synapse.api.auth import Auth + from synapse.server import HomeServer + logger = logging.getLogger(__name__) -@attr.s(slots=True, cmp=False, frozen=True) +@attr.s(slots=True, cmp=False, frozen=True, auto_attribs=True) class EventBuilder: """A format independent event builder used to build up the event content before signing the event. @@ -62,31 +65,30 @@ class EventBuilder: _signing_key: The signing key to use to sign the event as the server """ - _state = attr.ib(type=StateHandler) - _auth = attr.ib(type=Auth) - _store = attr.ib(type=DataStore) - _clock = attr.ib(type=Clock) - _hostname = attr.ib(type=str) - _signing_key = attr.ib(type=SigningKey) + _state: StateHandler + _auth: "Auth" + _store: DataStore + _clock: Clock + _hostname: str + _signing_key: SigningKey - room_version = attr.ib(type=RoomVersion) + room_version: RoomVersion - room_id = attr.ib(type=str) - type = attr.ib(type=str) - sender = attr.ib(type=str) + room_id: str + type: str + sender: str - content = attr.ib(default=attr.Factory(dict), type=JsonDict) - unsigned = attr.ib(default=attr.Factory(dict), type=JsonDict) + content: JsonDict = attr.Factory(dict) + unsigned: JsonDict = attr.Factory(dict) # These only exist on a subset of events, so they raise AttributeError if # someone tries to get them when they don't exist. - _state_key = attr.ib(default=None, type=Optional[str]) - _redacts = attr.ib(default=None, type=Optional[str]) - _origin_server_ts = attr.ib(default=None, type=Optional[int]) + _state_key: Optional[str] = None + _redacts: Optional[str] = None + _origin_server_ts: Optional[int] = None - internal_metadata = attr.ib( - default=attr.Factory(lambda: _EventInternalMetadata({})), - type=_EventInternalMetadata, + internal_metadata: _EventInternalMetadata = attr.Factory( + lambda: _EventInternalMetadata({}) ) @property @@ -184,7 +186,7 @@ async def build( class EventBuilderFactory: - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.hostname = hs.hostname self.signing_key = hs.signing_key @@ -193,15 +195,14 @@ def __init__(self, hs): self.state = hs.get_state_handler() self.auth = hs.get_auth() - def new(self, room_version, key_values): + def new(self, room_version: str, key_values: dict) -> EventBuilder: """Generate an event builder appropriate for the given room version Deprecated: use for_room_version with a RoomVersion object instead Args: - room_version (str): Version of the room that we're creating an event builder - for - key_values (dict): Fields used as the basis of the new event + room_version: Version of the room that we're creating an event builder for + key_values: Fields used as the basis of the new event Returns: EventBuilder @@ -212,13 +213,15 @@ def new(self, room_version, key_values): raise UnsupportedRoomVersionError() return self.for_room_version(v, key_values) - def for_room_version(self, room_version, key_values): + def for_room_version( + self, room_version: RoomVersion, key_values: dict + ) -> EventBuilder: """Generate an event builder appropriate for the given room version Args: - room_version (synapse.api.room_versions.RoomVersion): + room_version: Version of the room that we're creating an event builder for - key_values (dict): Fields used as the basis of the new event + key_values: Fields used as the basis of the new event Returns: EventBuilder @@ -286,15 +289,15 @@ def create_local_event_from_event_dict( _event_id_counter = 0 -def _create_event_id(clock, hostname): +def _create_event_id(clock: Clock, hostname: str) -> str: """Create a new event ID Args: - clock (Clock) - hostname (str): The server name for the event ID + clock + hostname: The server name for the event ID Returns: - str + The new event ID """ global _event_id_counter diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index db12abd59d..364c5cd2d3 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -509,6 +509,8 @@ async def create_event( Should normally be left as None, which will cause them to be calculated based on the room state at the prev_events. + If non-None, prev_event_ids must also be provided. + require_consent: Whether to check if the requester has consented to the privacy policy. @@ -581,6 +583,9 @@ async def create_event( # Strip down the auth_event_ids to only what we need to auth the event. # For example, we don't need extra m.room.member that don't match event.sender if auth_event_ids is not None: + # If auth events are provided, prev events must be also. + assert prev_event_ids is not None + temp_event = await builder.build( prev_event_ids=prev_event_ids, auth_event_ids=auth_event_ids, @@ -784,6 +789,8 @@ async def create_and_send_nonmember_event( The event ids to use as the auth_events for the new event. Should normally be left as None, which will cause them to be calculated based on the room state at the prev_events. + + If non-None, prev_event_ids must also be provided. ratelimit: Whether to rate limit this send. txn_id: The transaction ID. ignore_shadow_ban: True if shadow-banned users should be allowed to From f193034d591f6fc38d6588a1c4e4ac86543e9a1b Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Jun 2021 12:24:13 +0100 Subject: [PATCH 340/619] 1.37.1 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8de3bad906..defec46f33 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.37.1 (2021-06-30) +=========================== + +No significant changes. + + Synapse 1.37.1rc1 (2021-06-29) ============================== diff --git a/debian/changelog b/debian/changelog index cf190b7dba..35a0cddeaf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.37.1) stable; urgency=medium + + * New synapse release 1.37.1. + + -- Synapse Packaging team Wed, 30 Jun 2021 12:24:06 +0100 + matrix-synapse-py3 (1.37.0) stable; urgency=medium * New synapse release 1.37.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 2070724c34..1bd03462ac 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.37.1rc1" +__version__ = "1.37.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From ad36cb35882eec99e0044698265d86700e477363 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Jun 2021 14:45:09 +0100 Subject: [PATCH 341/619] Add note to changelog --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index defec46f33..bf76d3f0ec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,9 @@ Synapse 1.37.1 (2021-06-30) =========================== -No significant changes. +This release resolves issues (such as #9490) where one busy room could cause head-of-line blocking, starving Synapse from processing events in other rooms, and causing all federated traffic to fall behind. Synapse 1.37.1 processes inbound federation traffic asynchronously, ensuring that one busy room won't impact others. Please upgrade to Synapse 1.37.1 as soon as possible, in order to increase resilience to other traffic spikes. + +No significant changes since v1.37.1rc1. Synapse 1.37.1rc1 (2021-06-29) From c45246153f65bf7e028d876727117b1ddf178979 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 30 Jun 2021 14:47:06 +0100 Subject: [PATCH 342/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index bf76d3f0ec..7b6e052aca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Synapse 1.37.1 (2021-06-30) =========================== -This release resolves issues (such as #9490) where one busy room could cause head-of-line blocking, starving Synapse from processing events in other rooms, and causing all federated traffic to fall behind. Synapse 1.37.1 processes inbound federation traffic asynchronously, ensuring that one busy room won't impact others. Please upgrade to Synapse 1.37.1 as soon as possible, in order to increase resilience to other traffic spikes. +This release resolves issues (such as [#9490](https://github.com/matrix-org/synapse/issues/9490)) where one busy room could cause head-of-line blocking, starving Synapse from processing events in other rooms, and causing all federated traffic to fall behind. Synapse 1.37.1 processes inbound federation traffic asynchronously, ensuring that one busy room won't impact others. Please upgrade to Synapse 1.37.1 as soon as possible, in order to increase resilience to other traffic spikes. No significant changes since v1.37.1rc1. From 859dc05b3692a3672c1a0db8deaaa9274b6aa6f5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 30 Jun 2021 15:01:24 +0100 Subject: [PATCH 343/619] Rebuild other indexes using `stream_ordering` (#10282) We need to rebuild *all* of the indexes that use the current `stream_ordering` column. --- changelog.d/10282.bugfix | 1 + .../databases/main/events_bg_updates.py | 50 +++++++++++++++++-- .../01recreate_stream_ordering.sql.postgres | 11 ++-- 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10282.bugfix diff --git a/changelog.d/10282.bugfix b/changelog.d/10282.bugfix new file mode 100644 index 0000000000..7ebda7cdc2 --- /dev/null +++ b/changelog.d/10282.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index da3a7df27b..1c95c66648 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -29,13 +29,18 @@ logger = logging.getLogger(__name__) -_REPLACE_STREAM_ORDRING_SQL_COMMANDS = ( +_REPLACE_STREAM_ORDERING_SQL_COMMANDS = ( # there should be no leftover rows without a stream_ordering2, but just in case... "UPDATE events SET stream_ordering2 = stream_ordering WHERE stream_ordering2 IS NULL", - # finally, we can drop the rule and switch the columns + # now we can drop the rule and switch the columns "DROP RULE populate_stream_ordering2 ON events", "ALTER TABLE events DROP COLUMN stream_ordering", "ALTER TABLE events RENAME COLUMN stream_ordering2 TO stream_ordering", + # ... and finally, rename the indexes into place for consistency with sqlite + "ALTER INDEX event_contains_url_index2 RENAME TO event_contains_url_index", + "ALTER INDEX events_order_room2 RENAME TO events_order_room", + "ALTER INDEX events_room_stream2 RENAME TO events_room_stream", + "ALTER INDEX events_ts2 RENAME TO events_ts", ) @@ -45,6 +50,10 @@ class _BackgroundUpdates: DELETE_SOFT_FAILED_EXTREMITIES = "delete_soft_failed_extremities" POPULATE_STREAM_ORDERING2 = "populate_stream_ordering2" INDEX_STREAM_ORDERING2 = "index_stream_ordering2" + INDEX_STREAM_ORDERING2_CONTAINS_URL = "index_stream_ordering2_contains_url" + INDEX_STREAM_ORDERING2_ROOM_ORDER = "index_stream_ordering2_room_order" + INDEX_STREAM_ORDERING2_ROOM_STREAM = "index_stream_ordering2_room_stream" + INDEX_STREAM_ORDERING2_TS = "index_stream_ordering2_ts" REPLACE_STREAM_ORDERING_COLUMN = "replace_stream_ordering_column" @@ -155,12 +164,16 @@ def __init__(self, database: DatabasePool, db_conn, hs): self._purged_chain_cover_index, ) + ################################################################################ + # bg updates for replacing stream_ordering with a BIGINT # (these only run on postgres.) + self.db_pool.updates.register_background_update_handler( _BackgroundUpdates.POPULATE_STREAM_ORDERING2, self._background_populate_stream_ordering2, ) + # CREATE UNIQUE INDEX events_stream_ordering ON events(stream_ordering2); self.db_pool.updates.register_background_index_update( _BackgroundUpdates.INDEX_STREAM_ORDERING2, index_name="events_stream_ordering", @@ -168,11 +181,42 @@ def __init__(self, database: DatabasePool, db_conn, hs): columns=["stream_ordering2"], unique=True, ) + # CREATE INDEX event_contains_url_index ON events(room_id, topological_ordering, stream_ordering) WHERE contains_url = true AND outlier = false; + self.db_pool.updates.register_background_index_update( + _BackgroundUpdates.INDEX_STREAM_ORDERING2_CONTAINS_URL, + index_name="event_contains_url_index2", + table="events", + columns=["room_id", "topological_ordering", "stream_ordering2"], + where_clause="contains_url = true AND outlier = false", + ) + # CREATE INDEX events_order_room ON events(room_id, topological_ordering, stream_ordering); + self.db_pool.updates.register_background_index_update( + _BackgroundUpdates.INDEX_STREAM_ORDERING2_ROOM_ORDER, + index_name="events_order_room2", + table="events", + columns=["room_id", "topological_ordering", "stream_ordering2"], + ) + # CREATE INDEX events_room_stream ON events(room_id, stream_ordering); + self.db_pool.updates.register_background_index_update( + _BackgroundUpdates.INDEX_STREAM_ORDERING2_ROOM_STREAM, + index_name="events_room_stream2", + table="events", + columns=["room_id", "stream_ordering2"], + ) + # CREATE INDEX events_ts ON events(origin_server_ts, stream_ordering); + self.db_pool.updates.register_background_index_update( + _BackgroundUpdates.INDEX_STREAM_ORDERING2_TS, + index_name="events_ts2", + table="events", + columns=["origin_server_ts", "stream_ordering2"], + ) self.db_pool.updates.register_background_update_handler( _BackgroundUpdates.REPLACE_STREAM_ORDERING_COLUMN, self._background_replace_stream_ordering_column, ) + ################################################################################ + async def _background_reindex_fields_sender(self, progress, batch_size): target_min_stream_id = progress["target_min_stream_id_inclusive"] max_stream_id = progress["max_stream_id_exclusive"] @@ -1098,7 +1142,7 @@ async def _background_replace_stream_ordering_column( """Drop the old 'stream_ordering' column and rename 'stream_ordering2' into its place.""" def process(txn: Cursor) -> None: - for sql in _REPLACE_STREAM_ORDRING_SQL_COMMANDS: + for sql in _REPLACE_STREAM_ORDERING_SQL_COMMANDS: logger.info("completing stream_ordering migration: %s", sql) txn.execute(sql) diff --git a/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres b/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres index 88c9f8bd0d..b5fb763ddd 100644 --- a/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres +++ b/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres @@ -31,10 +31,15 @@ CREATE OR REPLACE RULE "populate_stream_ordering2" AS INSERT INTO background_updates (ordering, update_name, progress_json) VALUES (6001, 'populate_stream_ordering2', '{}'); --- ... and another to build an index on it +-- ... and some more to build indexes on it. These aren't really interdependent +-- but the backround_updates manager can only handle a single dependency per update. INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES - (6001, 'index_stream_ordering2', '{}', 'populate_stream_ordering2'); + (6001, 'index_stream_ordering2', '{}', 'populate_stream_ordering2'), + (6001, 'index_stream_ordering2_room_order', '{}', 'index_stream_ordering2'), + (6001, 'index_stream_ordering2_contains_url', '{}', 'index_stream_ordering2_room_order'), + (6001, 'index_stream_ordering2_room_stream', '{}', 'index_stream_ordering2_contains_url'), + (6001, 'index_stream_ordering2_ts', '{}', 'index_stream_ordering2_room_stream'); -- ... and another to do the switcheroo INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES - (6001, 'replace_stream_ordering_column', '{}', 'index_stream_ordering2'); + (6003, 'replace_stream_ordering_column', '{}', 'index_stream_ordering2_ts'); From b6dbf89fae74af25ce1a6993de74e0e50705f105 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 30 Jun 2021 17:27:20 +0100 Subject: [PATCH 344/619] Change more stream_ordering columns to BIGINT (#10286) --- changelog.d/10286.bugfix | 1 + ...hange_stream_ordering_columns.sql.postgres | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 changelog.d/10286.bugfix create mode 100644 synapse/storage/schema/main/delta/60/02change_stream_ordering_columns.sql.postgres diff --git a/changelog.d/10286.bugfix b/changelog.d/10286.bugfix new file mode 100644 index 0000000000..7ebda7cdc2 --- /dev/null +++ b/changelog.d/10286.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/synapse/storage/schema/main/delta/60/02change_stream_ordering_columns.sql.postgres b/synapse/storage/schema/main/delta/60/02change_stream_ordering_columns.sql.postgres new file mode 100644 index 0000000000..630c24fd9e --- /dev/null +++ b/synapse/storage/schema/main/delta/60/02change_stream_ordering_columns.sql.postgres @@ -0,0 +1,30 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- This migration is closely related to '01recreate_stream_ordering.sql.postgres'. +-- +-- It updates the other tables which use an INTEGER to refer to a stream ordering. +-- These tables are all small enough that a re-create is tractable. +ALTER TABLE pushers ALTER COLUMN last_stream_ordering SET DATA TYPE BIGINT; +ALTER TABLE federation_stream_position ALTER COLUMN stream_id SET DATA TYPE BIGINT; + +-- these aren't actually event stream orderings, but they are numbers where 2 billion +-- is a bit limiting, application_services_state is tiny, and I don't want to ever have +-- to do this again. +ALTER TABLE application_services_state ALTER COLUMN last_txn SET DATA TYPE BIGINT; +ALTER TABLE application_services_state ALTER COLUMN read_receipt_stream_id SET DATA TYPE BIGINT; +ALTER TABLE application_services_state ALTER COLUMN presence_stream_id SET DATA TYPE BIGINT; + + From 04c8f308f453ee3d4fde453ed10c500cdc06b89e Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 30 Jun 2021 23:43:58 +0100 Subject: [PATCH 345/619] Fix the homeserver config example in presence router docs (#10288) The presence router docs include some sample homeserver config. At some point we changed the name of the [config option](https://github.com/matrix-org/synapse/blob/859dc05b3692a3672c1a0db8deaaa9274b6aa6f5/docs/sample_config.yaml#L104-L113), but forgot to update the docs. I've also added `presence.enabled: true` to the example, as that's the new way to enable presence (the `presence_enabled` option has been deprecated). --- changelog.d/10288.doc | 1 + docs/presence_router_module.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10288.doc diff --git a/changelog.d/10288.doc b/changelog.d/10288.doc new file mode 100644 index 0000000000..0739687b92 --- /dev/null +++ b/changelog.d/10288.doc @@ -0,0 +1 @@ +Fix homeserver config option name in presence router documentation. diff --git a/docs/presence_router_module.md b/docs/presence_router_module.md index bf859e4254..4a3e720240 100644 --- a/docs/presence_router_module.md +++ b/docs/presence_router_module.md @@ -222,7 +222,9 @@ Synapse, amend your homeserver config file with the following. ```yaml presence: - routing_module: + enabled: true + + presence_router: module: my_module.ExamplePresenceRouter config: # Any configuration options for your module. The below is an example. From 76addadd7c807a3412e6a104db0fdc9b79888688 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 1 Jul 2021 10:18:25 +0100 Subject: [PATCH 346/619] Add some metrics to staging area (#10284) --- changelog.d/10284.feature | 1 + .../databases/main/event_federation.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 changelog.d/10284.feature diff --git a/changelog.d/10284.feature b/changelog.d/10284.feature new file mode 100644 index 0000000000..379155e8cf --- /dev/null +++ b/changelog.d/10284.feature @@ -0,0 +1 @@ +Add metrics for new inbound federation staging area. diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index f2d27ee893..08d75b0d41 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -16,6 +16,8 @@ from queue import Empty, PriorityQueue from typing import Collection, Dict, Iterable, List, Optional, Set, Tuple +from prometheus_client import Gauge + from synapse.api.constants import MAX_DEPTH from synapse.api.errors import StoreError from synapse.api.room_versions import RoomVersion @@ -32,6 +34,16 @@ from synapse.util.caches.lrucache import LruCache from synapse.util.iterutils import batch_iter +oldest_pdu_in_federation_staging = Gauge( + "synapse_federation_server_oldest_inbound_pdu_in_staging", + "The age in seconds since we received the oldest pdu in the federation staging area", +) + +number_pdus_in_federation_queue = Gauge( + "synapse_federation_server_number_inbound_pdu_in_staging", + "The total number of events in the inbound federation staging", +) + logger = logging.getLogger(__name__) @@ -54,6 +66,8 @@ def __init__(self, database: DatabasePool, db_conn, hs): 500000, "_event_auth_cache", size_callback=len ) # type: LruCache[str, List[Tuple[str, int]]] + self._clock.looping_call(self._get_stats_for_federation_staging, 30 * 1000) + async def get_auth_chain( self, room_id: str, event_ids: Collection[str], include_given: bool = False ) -> List[EventBase]: @@ -1193,6 +1207,31 @@ def _get_next_staged_event_for_room_txn(txn): return origin, event + @wrap_as_background_process("_get_stats_for_federation_staging") + async def _get_stats_for_federation_staging(self): + """Update the prometheus metrics for the inbound federation staging area.""" + + def _get_stats_for_federation_staging_txn(txn): + txn.execute( + "SELECT coalesce(count(*), 0) FROM federation_inbound_events_staging" + ) + (count,) = txn.fetchone() + + txn.execute( + "SELECT coalesce(min(received_ts), 0) FROM federation_inbound_events_staging" + ) + + (age,) = txn.fetchone() + + return count, age + + count, age = await self.db_pool.runInteraction( + "_get_stats_for_federation_staging", _get_stats_for_federation_staging_txn + ) + + number_pdus_in_federation_queue.set(count) + oldest_pdu_in_federation_staging.set(age) + class EventFederationStore(EventFederationWorkerStore): """Responsible for storing and serving up the various graphs associated From 6c02cca95f8136010062b6af0fa36a2906a96a6b Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 1 Jul 2021 11:26:24 +0200 Subject: [PATCH 347/619] Add SSO `external_ids` to Query User Account admin API (#10261) Related to #10251 --- changelog.d/10261.feature | 1 + docs/admin_api/user_admin_api.md | 12 +- synapse/handlers/admin.py | 7 + tests/rest/admin/test_user.py | 224 +++++++++++++++++++------------ 4 files changed, 159 insertions(+), 85 deletions(-) create mode 100644 changelog.d/10261.feature diff --git a/changelog.d/10261.feature b/changelog.d/10261.feature new file mode 100644 index 0000000000..cd55cecbd5 --- /dev/null +++ b/changelog.d/10261.feature @@ -0,0 +1 @@ +Add SSO `external_ids` to the Query User Account admin API. diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index ef1e735e33..4a65d0c3bc 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -36,7 +36,17 @@ It returns a JSON body like the following: "creation_ts": 1560432506, "appservice_id": null, "consent_server_notice_sent": null, - "consent_version": null + "consent_version": null, + "external_ids": [ + { + "auth_provider": "", + "external_id": "" + }, + { + "auth_provider": "", + "external_id": "" + } + ] } ``` diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index f72ded038e..d75a8b15c3 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -62,9 +62,16 @@ async def get_user(self, user: UserID) -> Optional[JsonDict]: if ret: profile = await self.store.get_profileinfo(user.localpart) threepids = await self.store.user_get_threepids(user.to_string()) + external_ids = [ + ({"auth_provider": auth_provider, "external_id": external_id}) + for auth_provider, external_id in await self.store.get_external_ids_by_user( + user.to_string() + ) + ] ret["displayname"] = profile.display_name ret["avatar_url"] = profile.avatar_url ret["threepids"] = threepids + ret["external_ids"] = external_ids return ret async def export_user_data(self, user_id: str, writer: "ExfiltrationWriter") -> Any: diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index d599a4c984..a34d051734 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -1150,7 +1150,7 @@ def test_requester_is_no_admin(self): access_token=self.other_user_token, ) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual("You are not a server admin", channel.json_body["error"]) channel = self.make_request( @@ -1160,7 +1160,7 @@ def test_requester_is_no_admin(self): content=b"{}", ) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual("You are not a server admin", channel.json_body["error"]) def test_user_does_not_exist(self): @@ -1177,6 +1177,58 @@ def test_user_does_not_exist(self): self.assertEqual(404, channel.code, msg=channel.json_body) self.assertEqual("M_NOT_FOUND", channel.json_body["errcode"]) + def test_get_user(self): + """ + Test a simple get of a user. + """ + channel = self.make_request( + "GET", + self.url_other_user, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("User", channel.json_body["displayname"]) + self._check_fields(channel.json_body) + + def test_get_user_with_sso(self): + """ + Test get a user with SSO details. + """ + self.get_success( + self.store.record_user_external_id( + "auth_provider1", "external_id1", self.other_user + ) + ) + self.get_success( + self.store.record_user_external_id( + "auth_provider2", "external_id2", self.other_user + ) + ) + + channel = self.make_request( + "GET", + self.url_other_user, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual( + "external_id1", channel.json_body["external_ids"][0]["external_id"] + ) + self.assertEqual( + "auth_provider1", channel.json_body["external_ids"][0]["auth_provider"] + ) + self.assertEqual( + "external_id2", channel.json_body["external_ids"][1]["external_id"] + ) + self.assertEqual( + "auth_provider2", channel.json_body["external_ids"][1]["auth_provider"] + ) + self._check_fields(channel.json_body) + def test_create_server_admin(self): """ Check that a new admin user is created successfully. @@ -1184,30 +1236,29 @@ def test_create_server_admin(self): url = "/_synapse/admin/v2/users/@bob:test" # Create user (server admin) - body = json.dumps( - { - "password": "abc123", - "admin": True, - "displayname": "Bob's name", - "threepids": [{"medium": "email", "address": "bob@bob.bob"}], - "avatar_url": "mxc://fibble/wibble", - } - ) + body = { + "password": "abc123", + "admin": True, + "displayname": "Bob's name", + "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + "avatar_url": "mxc://fibble/wibble", + } channel = self.make_request( "PUT", url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content=body, ) - self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(201, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("Bob's name", channel.json_body["displayname"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) self.assertTrue(channel.json_body["admin"]) self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"]) + self._check_fields(channel.json_body) # Get user channel = self.make_request( @@ -1216,7 +1267,7 @@ def test_create_server_admin(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("Bob's name", channel.json_body["displayname"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) @@ -1225,6 +1276,7 @@ def test_create_server_admin(self): self.assertFalse(channel.json_body["is_guest"]) self.assertFalse(channel.json_body["deactivated"]) self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"]) + self._check_fields(channel.json_body) def test_create_user(self): """ @@ -1233,30 +1285,29 @@ def test_create_user(self): url = "/_synapse/admin/v2/users/@bob:test" # Create user - body = json.dumps( - { - "password": "abc123", - "admin": False, - "displayname": "Bob's name", - "threepids": [{"medium": "email", "address": "bob@bob.bob"}], - "avatar_url": "mxc://fibble/wibble", - } - ) + body = { + "password": "abc123", + "admin": False, + "displayname": "Bob's name", + "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + "avatar_url": "mxc://fibble/wibble", + } channel = self.make_request( "PUT", url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content=body, ) - self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(201, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("Bob's name", channel.json_body["displayname"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) self.assertFalse(channel.json_body["admin"]) self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"]) + self._check_fields(channel.json_body) # Get user channel = self.make_request( @@ -1265,7 +1316,7 @@ def test_create_user(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("Bob's name", channel.json_body["displayname"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) @@ -1275,6 +1326,7 @@ def test_create_user(self): self.assertFalse(channel.json_body["deactivated"]) self.assertFalse(channel.json_body["shadow_banned"]) self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"]) + self._check_fields(channel.json_body) @override_config( {"limit_usage_by_mau": True, "max_mau_value": 2, "mau_trial_days": 0} @@ -1311,16 +1363,14 @@ def test_create_user_mau_limit_reached_active_admin(self): url = "/_synapse/admin/v2/users/@bob:test" # Create user - body = json.dumps({"password": "abc123", "admin": False}) - channel = self.make_request( "PUT", url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content={"password": "abc123", "admin": False}, ) - self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(201, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertFalse(channel.json_body["admin"]) @@ -1350,17 +1400,15 @@ def test_create_user_mau_limit_reached_passive_admin(self): url = "/_synapse/admin/v2/users/@bob:test" # Create user - body = json.dumps({"password": "abc123", "admin": False}) - channel = self.make_request( "PUT", url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content={"password": "abc123", "admin": False}, ) # Admin user is not blocked by mau anymore - self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(201, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertFalse(channel.json_body["admin"]) @@ -1382,21 +1430,19 @@ def test_create_user_email_notif_for_new_users(self): url = "/_synapse/admin/v2/users/@bob:test" # Create user - body = json.dumps( - { - "password": "abc123", - "threepids": [{"medium": "email", "address": "bob@bob.bob"}], - } - ) + body = { + "password": "abc123", + "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + } channel = self.make_request( "PUT", url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content=body, ) - self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(201, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) @@ -1426,21 +1472,19 @@ def test_create_user_email_no_notif_for_new_users(self): url = "/_synapse/admin/v2/users/@bob:test" # Create user - body = json.dumps( - { - "password": "abc123", - "threepids": [{"medium": "email", "address": "bob@bob.bob"}], - } - ) + body = { + "password": "abc123", + "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + } channel = self.make_request( "PUT", url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content=body, ) - self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(201, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) @@ -1457,16 +1501,15 @@ def test_set_password(self): """ # Change password - body = json.dumps({"password": "hahaha"}) - channel = self.make_request( "PUT", self.url_other_user, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content={"password": "hahaha"}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) + self._check_fields(channel.json_body) def test_set_displayname(self): """ @@ -1474,16 +1517,14 @@ def test_set_displayname(self): """ # Modify user - body = json.dumps({"displayname": "foobar"}) - channel = self.make_request( "PUT", self.url_other_user, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content={"displayname": "foobar"}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual("foobar", channel.json_body["displayname"]) @@ -1494,7 +1535,7 @@ def test_set_displayname(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual("foobar", channel.json_body["displayname"]) @@ -1504,18 +1545,14 @@ def test_set_threepid(self): """ # Delete old and add new threepid to user - body = json.dumps( - {"threepids": [{"medium": "email", "address": "bob3@bob.bob"}]} - ) - channel = self.make_request( "PUT", self.url_other_user, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content={"threepids": [{"medium": "email", "address": "bob3@bob.bob"}]}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"]) @@ -1527,7 +1564,7 @@ def test_set_threepid(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"]) @@ -1552,7 +1589,7 @@ def test_deactivate_user(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertFalse(channel.json_body["deactivated"]) self.assertEqual("foo@bar.com", channel.json_body["threepids"][0]["address"]) @@ -1567,7 +1604,7 @@ def test_deactivate_user(self): content={"deactivated": True}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertTrue(channel.json_body["deactivated"]) self.assertIsNone(channel.json_body["password_hash"]) @@ -1583,7 +1620,7 @@ def test_deactivate_user(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertTrue(channel.json_body["deactivated"]) self.assertIsNone(channel.json_body["password_hash"]) @@ -1610,7 +1647,7 @@ def test_change_name_deactivate_user_user_directory(self): content={"deactivated": True}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertTrue(channel.json_body["deactivated"]) @@ -1626,7 +1663,7 @@ def test_change_name_deactivate_user_user_directory(self): content={"displayname": "Foobar"}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertTrue(channel.json_body["deactivated"]) self.assertEqual("Foobar", channel.json_body["displayname"]) @@ -1650,7 +1687,7 @@ def test_reactivate_user(self): access_token=self.admin_user_tok, content={"deactivated": False}, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) # Reactivate the user. channel = self.make_request( @@ -1659,7 +1696,7 @@ def test_reactivate_user(self): access_token=self.admin_user_tok, content={"deactivated": False, "password": "foo"}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertFalse(channel.json_body["deactivated"]) self.assertIsNotNone(channel.json_body["password_hash"]) @@ -1681,7 +1718,7 @@ def test_reactivate_user_localdb_disabled(self): access_token=self.admin_user_tok, content={"deactivated": False, "password": "foo"}, ) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) # Reactivate the user without a password. @@ -1691,7 +1728,7 @@ def test_reactivate_user_localdb_disabled(self): access_token=self.admin_user_tok, content={"deactivated": False}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertFalse(channel.json_body["deactivated"]) self.assertIsNone(channel.json_body["password_hash"]) @@ -1713,7 +1750,7 @@ def test_reactivate_user_password_disabled(self): access_token=self.admin_user_tok, content={"deactivated": False, "password": "foo"}, ) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) # Reactivate the user without a password. @@ -1723,7 +1760,7 @@ def test_reactivate_user_password_disabled(self): access_token=self.admin_user_tok, content={"deactivated": False}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertFalse(channel.json_body["deactivated"]) self.assertIsNone(channel.json_body["password_hash"]) @@ -1742,7 +1779,7 @@ def test_set_user_as_admin(self): content={"admin": True}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertTrue(channel.json_body["admin"]) @@ -1753,7 +1790,7 @@ def test_set_user_as_admin(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertTrue(channel.json_body["admin"]) @@ -1772,7 +1809,7 @@ def test_accidental_deactivation_prevention(self): content={"password": "abc123"}, ) - self.assertEqual(201, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(201, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("bob", channel.json_body["displayname"]) @@ -1783,7 +1820,7 @@ def test_accidental_deactivation_prevention(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("bob", channel.json_body["displayname"]) self.assertEqual(0, channel.json_body["deactivated"]) @@ -1796,7 +1833,7 @@ def test_accidental_deactivation_prevention(self): content={"password": "abc123", "deactivated": "false"}, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) # Check user is not deactivated channel = self.make_request( @@ -1805,7 +1842,7 @@ def test_accidental_deactivation_prevention(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["name"]) self.assertEqual("bob", channel.json_body["displayname"]) @@ -1830,7 +1867,7 @@ def _deactivate_user(self, user_id: str) -> None: access_token=self.admin_user_tok, content={"deactivated": True}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertTrue(channel.json_body["deactivated"]) self.assertIsNone(channel.json_body["password_hash"]) self._is_erased(user_id, False) @@ -1838,6 +1875,25 @@ def _deactivate_user(self, user_id: str) -> None: self.assertIsNone(self.get_success(d)) self._is_erased(user_id, True) + def _check_fields(self, content: JsonDict): + """Checks that the expected user attributes are present in content + + Args: + content: Content dictionary to check + """ + self.assertIn("displayname", content) + self.assertIn("threepids", content) + self.assertIn("avatar_url", content) + self.assertIn("admin", content) + self.assertIn("deactivated", content) + self.assertIn("shadow_banned", content) + self.assertIn("password_hash", content) + self.assertIn("creation_ts", content) + self.assertIn("appservice_id", content) + self.assertIn("consent_server_notice_sent", content) + self.assertIn("consent_version", content) + self.assertIn("external_ids", content) + class UserMembershipRestTestCase(unittest.HomeserverTestCase): From e72c287418c21b9cfed6cf6ce509da57bc285af3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 1 Jul 2021 12:21:58 +0100 Subject: [PATCH 348/619] Reenable 'Backfilled events whose prev_events...' sytest (#10292) Now that we've fixed it. --- changelog.d/10292.misc | 1 + sytest-blacklist | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/10292.misc diff --git a/changelog.d/10292.misc b/changelog.d/10292.misc new file mode 100644 index 0000000000..9e87d8682c --- /dev/null +++ b/changelog.d/10292.misc @@ -0,0 +1 @@ +Reenable a SyTest after it has been fixed. diff --git a/sytest-blacklist b/sytest-blacklist index 89c4e828fd..566ef96711 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -45,5 +45,4 @@ Peeked rooms only turn up in the sync for the device who peeked them # Blacklisted due to changes made in #10272 Outbound federation will ignore a missing event with bad JSON for room version 6 -Backfilled events whose prev_events are in a different room do not allow cross-room back-pagination Federation rejects inbound events where the prev_events cannot be found From 0aab50c772e9b0df2bf31a5f9381ccb69d060e9c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 1 Jul 2021 18:45:55 +0100 Subject: [PATCH 349/619] fix ordering of bg update (#10291) this was a typo introduced in #10282. We don't want to end up doing the `replace_stream_ordering_column` update after anything that comes up in migration 60/03. --- changelog.d/10291.bugfix | 1 + .../main/delta/60/01recreate_stream_ordering.sql.postgres | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10291.bugfix diff --git a/changelog.d/10291.bugfix b/changelog.d/10291.bugfix new file mode 100644 index 0000000000..7ebda7cdc2 --- /dev/null +++ b/changelog.d/10291.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres b/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres index b5fb763ddd..0edc9fe7a2 100644 --- a/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres +++ b/synapse/storage/schema/main/delta/60/01recreate_stream_ordering.sql.postgres @@ -42,4 +42,4 @@ INSERT INTO background_updates (ordering, update_name, progress_json, depends_on -- ... and another to do the switcheroo INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES - (6003, 'replace_stream_ordering_column', '{}', 'index_stream_ordering2_ts'); + (6001, 'replace_stream_ordering_column', '{}', 'index_stream_ordering2_ts'); From 8d609435c0053fc4decbc3f9c3603e728912749c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 1 Jul 2021 14:25:37 -0400 Subject: [PATCH 350/619] Move methods involving event authentication to EventAuthHandler. (#10268) Instead of mixing them with user authentication methods. --- changelog.d/10268.misc | 1 + synapse/api/auth.py | 75 +----------------------- synapse/events/builder.py | 12 ++-- synapse/federation/federation_server.py | 6 +- synapse/handlers/event_auth.py | 62 +++++++++++++++++++- synapse/handlers/federation.py | 36 ++++++++---- synapse/handlers/message.py | 9 ++- synapse/handlers/room.py | 3 +- synapse/handlers/space_summary.py | 6 +- synapse/push/bulk_push_rule_evaluator.py | 4 +- tests/handlers/test_presence.py | 4 +- 11 files changed, 112 insertions(+), 106 deletions(-) create mode 100644 changelog.d/10268.misc diff --git a/changelog.d/10268.misc b/changelog.d/10268.misc new file mode 100644 index 0000000000..9e3f60c72f --- /dev/null +++ b/changelog.d/10268.misc @@ -0,0 +1 @@ +Move event authentication methods from `Auth` to `EventAuthHandler`. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index f8b068e563..307f5f9a94 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Optional, Tuple import pymacaroons from netaddr import IPAddress @@ -28,10 +28,8 @@ InvalidClientTokenError, MissingClientTokenError, ) -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS from synapse.appservice import ApplicationService from synapse.events import EventBase -from synapse.events.builder import EventBuilder from synapse.http import get_request_user_agent from synapse.http.site import SynapseRequest from synapse.logging import opentracing as opentracing @@ -39,7 +37,6 @@ from synapse.types import Requester, StateMap, UserID, create_requester from synapse.util.caches.lrucache import LruCache from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry -from synapse.util.metrics import Measure if TYPE_CHECKING: from synapse.server import HomeServer @@ -47,15 +44,6 @@ logger = logging.getLogger(__name__) -AuthEventTypes = ( - EventTypes.Create, - EventTypes.Member, - EventTypes.PowerLevels, - EventTypes.JoinRules, - EventTypes.RoomHistoryVisibility, - EventTypes.ThirdPartyInvite, -) - # guests always get this device id. GUEST_DEVICE_ID = "guest_device" @@ -66,9 +54,7 @@ class _InvalidMacaroonException(Exception): class Auth: """ - FIXME: This class contains a mix of functions for authenticating users - of our client-server API and authenticating events added to room graphs. - The latter should be moved to synapse.handlers.event_auth.EventAuthHandler. + This class contains functions for authenticating users of our client-server API. """ def __init__(self, hs: "HomeServer"): @@ -90,18 +76,6 @@ def __init__(self, hs: "HomeServer"): self._macaroon_secret_key = hs.config.macaroon_secret_key self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users - async def check_from_context( - self, room_version: str, event, context, do_sig_check=True - ) -> None: - auth_event_ids = event.auth_event_ids() - auth_events_by_id = await self.store.get_events(auth_event_ids) - auth_events = {(e.type, e.state_key): e for e in auth_events_by_id.values()} - - room_version_obj = KNOWN_ROOM_VERSIONS[room_version] - event_auth.check( - room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check - ) - async def check_user_in_room( self, room_id: str, @@ -152,13 +126,6 @@ async def check_user_in_room( raise AuthError(403, "User %s not in room %s" % (user_id, room_id)) - async def check_host_in_room(self, room_id: str, host: str) -> bool: - with Measure(self.clock, "check_host_in_room"): - return await self.store.is_host_joined(room_id, host) - - def get_public_keys(self, invite_event: EventBase) -> List[Dict[str, Any]]: - return event_auth.get_public_keys(invite_event) - async def get_user_by_req( self, request: SynapseRequest, @@ -489,44 +456,6 @@ async def is_server_admin(self, user: UserID) -> bool: """ return await self.store.is_server_admin(user) - def compute_auth_events( - self, - event: Union[EventBase, EventBuilder], - current_state_ids: StateMap[str], - for_verification: bool = False, - ) -> List[str]: - """Given an event and current state return the list of event IDs used - to auth an event. - - If `for_verification` is False then only return auth events that - should be added to the event's `auth_events`. - - Returns: - List of event IDs. - """ - - if event.type == EventTypes.Create: - return [] - - # Currently we ignore the `for_verification` flag even though there are - # some situations where we can drop particular auth events when adding - # to the event's `auth_events` (e.g. joins pointing to previous joins - # when room is publicly joinable). Dropping event IDs has the - # advantage that the auth chain for the room grows slower, but we use - # the auth chain in state resolution v2 to order events, which means - # care must be taken if dropping events to ensure that it doesn't - # introduce undesirable "state reset" behaviour. - # - # All of which sounds a bit tricky so we don't bother for now. - - auth_ids = [] - for etype, state_key in event_auth.auth_types_for_event(event): - auth_ev_id = current_state_ids.get((etype, state_key)) - if auth_ev_id: - auth_ids.append(auth_ev_id) - - return auth_ids - async def check_can_change_room_list(self, room_id: str, user: UserID) -> bool: """Determine whether the user is allowed to edit the room's entry in the published room list. diff --git a/synapse/events/builder.py b/synapse/events/builder.py index fb48ec8541..26e3950859 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -34,7 +34,7 @@ from synapse.util.stringutils import random_string if TYPE_CHECKING: - from synapse.api.auth import Auth + from synapse.handlers.event_auth import EventAuthHandler from synapse.server import HomeServer logger = logging.getLogger(__name__) @@ -66,7 +66,7 @@ class EventBuilder: """ _state: StateHandler - _auth: "Auth" + _event_auth_handler: "EventAuthHandler" _store: DataStore _clock: Clock _hostname: str @@ -125,7 +125,9 @@ async def build( state_ids = await self._state.get_current_state_ids( self.room_id, prev_event_ids ) - auth_event_ids = self._auth.compute_auth_events(self, state_ids) + auth_event_ids = self._event_auth_handler.compute_auth_events( + self, state_ids + ) format_version = self.room_version.event_format if format_version == EventFormatVersions.V1: @@ -193,7 +195,7 @@ def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() self.state = hs.get_state_handler() - self.auth = hs.get_auth() + self._event_auth_handler = hs.get_event_auth_handler() def new(self, room_version: str, key_values: dict) -> EventBuilder: """Generate an event builder appropriate for the given room version @@ -229,7 +231,7 @@ def for_room_version( return EventBuilder( store=self.store, state=self.state, - auth=self.auth, + event_auth_handler=self._event_auth_handler, clock=self.clock, hostname=self.hostname, signing_key=self.signing_key, diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index e93b7577fe..b312d0b809 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -108,9 +108,9 @@ class FederationServer(FederationBase): def __init__(self, hs: "HomeServer"): super().__init__(hs) - self.auth = hs.get_auth() self.handler = hs.get_federation_handler() self.state = hs.get_state_handler() + self._event_auth_handler = hs.get_event_auth_handler() self.device_handler = hs.get_device_handler() @@ -420,7 +420,7 @@ async def on_room_state_request( origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) - in_room = await self.auth.check_host_in_room(room_id, origin) + in_room = await self._event_auth_handler.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") @@ -453,7 +453,7 @@ async def on_state_ids_request( origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) - in_room = await self.auth.check_host_in_room(room_id, origin) + in_room = await self._event_auth_handler.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 989996b628..41dbdfd0a1 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import TYPE_CHECKING, Collection, Optional +from typing import TYPE_CHECKING, Collection, List, Optional, Union +from synapse import event_auth from synapse.api.constants import ( EventTypes, JoinRules, @@ -20,9 +21,11 @@ RestrictedJoinRuleTypes, ) from synapse.api.errors import AuthError -from synapse.api.room_versions import RoomVersion +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.events import EventBase +from synapse.events.builder import EventBuilder from synapse.types import StateMap +from synapse.util.metrics import Measure if TYPE_CHECKING: from synapse.server import HomeServer @@ -34,8 +37,63 @@ class EventAuthHandler: """ def __init__(self, hs: "HomeServer"): + self._clock = hs.get_clock() self._store = hs.get_datastore() + async def check_from_context( + self, room_version: str, event, context, do_sig_check=True + ) -> None: + auth_event_ids = event.auth_event_ids() + auth_events_by_id = await self._store.get_events(auth_event_ids) + auth_events = {(e.type, e.state_key): e for e in auth_events_by_id.values()} + + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] + event_auth.check( + room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check + ) + + def compute_auth_events( + self, + event: Union[EventBase, EventBuilder], + current_state_ids: StateMap[str], + for_verification: bool = False, + ) -> List[str]: + """Given an event and current state return the list of event IDs used + to auth an event. + + If `for_verification` is False then only return auth events that + should be added to the event's `auth_events`. + + Returns: + List of event IDs. + """ + + if event.type == EventTypes.Create: + return [] + + # Currently we ignore the `for_verification` flag even though there are + # some situations where we can drop particular auth events when adding + # to the event's `auth_events` (e.g. joins pointing to previous joins + # when room is publicly joinable). Dropping event IDs has the + # advantage that the auth chain for the room grows slower, but we use + # the auth chain in state resolution v2 to order events, which means + # care must be taken if dropping events to ensure that it doesn't + # introduce undesirable "state reset" behaviour. + # + # All of which sounds a bit tricky so we don't bother for now. + + auth_ids = [] + for etype, state_key in event_auth.auth_types_for_event(event): + auth_ev_id = current_state_ids.get((etype, state_key)) + if auth_ev_id: + auth_ids.append(auth_ev_id) + + return auth_ids + + async def check_host_in_room(self, room_id: str, host: str) -> bool: + with Measure(self._clock, "check_host_in_room"): + return await self._store.is_host_joined(room_id, host) + async def check_restricted_join_rules( self, state_ids: StateMap[str], diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d929c65131..991ec9919a 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -250,7 +250,9 @@ async def on_receive_pdu( # # Note that if we were never in the room then we would have already # dropped the event, since we wouldn't know the room version. - is_in_room = await self.auth.check_host_in_room(room_id, self.server_name) + is_in_room = await self._event_auth_handler.check_host_in_room( + room_id, self.server_name + ) if not is_in_room: logger.info( "Ignoring PDU from %s as we're not in the room", @@ -1674,7 +1676,9 @@ async def on_make_join_request( room_version = await self.store.get_room_version_id(room_id) # now check that we are *still* in the room - is_in_room = await self.auth.check_host_in_room(room_id, self.server_name) + is_in_room = await self._event_auth_handler.check_host_in_room( + room_id, self.server_name + ) if not is_in_room: logger.info( "Got /make_join request for room %s we are no longer in", @@ -1705,7 +1709,7 @@ async def on_make_join_request( # The remote hasn't signed it yet, obviously. We'll do the full checks # when we get the event back in `on_send_join_request` - await self.auth.check_from_context( + await self._event_auth_handler.check_from_context( room_version, event, context, do_sig_check=False ) @@ -1877,7 +1881,7 @@ async def on_make_leave_request( try: # The remote hasn't signed it yet, obviously. We'll do the full checks # when we get the event back in `on_send_leave_request` - await self.auth.check_from_context( + await self._event_auth_handler.check_from_context( room_version, event, context, do_sig_check=False ) except AuthError as e: @@ -1939,7 +1943,7 @@ async def on_make_knock_request( try: # The remote hasn't signed it yet, obviously. We'll do the full checks # when we get the event back in `on_send_knock_request` - await self.auth.check_from_context( + await self._event_auth_handler.check_from_context( room_version, event, context, do_sig_check=False ) except AuthError as e: @@ -2111,7 +2115,7 @@ async def get_state_ids_for_pdu(self, room_id: str, event_id: str) -> List[str]: async def on_backfill_request( self, origin: str, room_id: str, pdu_list: List[str], limit: int ) -> List[EventBase]: - in_room = await self.auth.check_host_in_room(room_id, origin) + in_room = await self._event_auth_handler.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") @@ -2146,7 +2150,9 @@ async def get_persisted_pdu( ) if event: - in_room = await self.auth.check_host_in_room(event.room_id, origin) + in_room = await self._event_auth_handler.check_host_in_room( + event.room_id, origin + ) if not in_room: raise AuthError(403, "Host not in room.") @@ -2499,7 +2505,7 @@ async def on_get_missing_events( latest_events: List[str], limit: int, ) -> List[EventBase]: - in_room = await self.auth.check_host_in_room(room_id, origin) + in_room = await self._event_auth_handler.check_host_in_room(room_id, origin) if not in_room: raise AuthError(403, "Host not in room.") @@ -2562,7 +2568,7 @@ async def _check_event_auth( if not auth_events: prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self.auth.compute_auth_events( + auth_events_ids = self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=True ) auth_events_x = await self.store.get_events(auth_events_ids) @@ -2991,7 +2997,7 @@ async def exchange_third_party_invite( "state_key": target_user_id, } - if await self.auth.check_host_in_room(room_id, self.hs.hostname): + if await self._event_auth_handler.check_host_in_room(room_id, self.hs.hostname): room_version = await self.store.get_room_version_id(room_id) builder = self.event_builder_factory.new(room_version, event_dict) @@ -3011,7 +3017,9 @@ async def exchange_third_party_invite( event.internal_metadata.send_on_behalf_of = self.hs.hostname try: - await self.auth.check_from_context(room_version, event, context) + await self._event_auth_handler.check_from_context( + room_version, event, context + ) except AuthError as e: logger.warning("Denying new third party invite %r because %s", event, e) raise e @@ -3054,7 +3062,9 @@ async def on_exchange_third_party_invite_request( ) try: - await self.auth.check_from_context(room_version, event, context) + await self._event_auth_handler.check_from_context( + room_version, event, context + ) except AuthError as e: logger.warning("Denying third party invite %r because %s", event, e) raise e @@ -3142,7 +3152,7 @@ async def _check_signature(self, event: EventBase, context: EventContext) -> Non last_exception = None # type: Optional[Exception] # for each public key in the 3pid invite event - for public_key_object in self.hs.get_auth().get_public_keys(invite_event): + for public_key_object in event_auth.get_public_keys(invite_event): try: # for each sig on the third_party_invite block of the actual invite for server, signature_block in signed["signatures"].items(): diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 364c5cd2d3..66e40a915d 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -385,6 +385,7 @@ class EventCreationHandler: def __init__(self, hs: "HomeServer"): self.hs = hs self.auth = hs.get_auth() + self._event_auth_handler = hs.get_event_auth_handler() self.store = hs.get_datastore() self.storage = hs.get_storage() self.state = hs.get_state_handler() @@ -597,7 +598,7 @@ async def create_event( (e.type, e.state_key): e.event_id for e in auth_events } # Actually strip down and use the necessary auth events - auth_event_ids = self.auth.compute_auth_events( + auth_event_ids = self._event_auth_handler.compute_auth_events( event=temp_event, current_state_ids=auth_event_state_map, for_verification=False, @@ -1056,7 +1057,9 @@ async def handle_new_client_event( assert event.content["membership"] == Membership.LEAVE else: try: - await self.auth.check_from_context(room_version, event, context) + await self._event_auth_handler.check_from_context( + room_version, event, context + ) except AuthError as err: logger.warning("Denying new event %r because %s", event, err) raise err @@ -1381,7 +1384,7 @@ async def persist_and_notify_client_event( raise AuthError(403, "Redacting server ACL events is not permitted") prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self.auth.compute_auth_events( + auth_events_ids = self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=True ) auth_events_map = await self.store.get_events(auth_events_ids) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 835d874cee..579b1b93c5 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -83,6 +83,7 @@ def __init__(self, hs: "HomeServer"): self.spam_checker = hs.get_spam_checker() self.event_creation_handler = hs.get_event_creation_handler() self.room_member_handler = hs.get_room_member_handler() + self._event_auth_handler = hs.get_event_auth_handler() self.config = hs.config # Room state based off defined presets @@ -226,7 +227,7 @@ async def _upgrade_room( }, ) old_room_version = await self.store.get_room_version_id(old_room_id) - await self.auth.check_from_context( + await self._event_auth_handler.check_from_context( old_room_version, tombstone_event, tombstone_context ) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 266f369883..b585057ec3 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -472,7 +472,7 @@ async def _is_room_accessible( # If this is a request over federation, check if the host is in the room or # is in one of the spaces specified via the join rules. elif origin: - if await self._auth.check_host_in_room(room_id, origin): + if await self._event_auth_handler.check_host_in_room(room_id, origin): return True # Alternately, if the host has a user in any of the spaces specified @@ -485,7 +485,9 @@ async def _is_room_accessible( await self._event_auth_handler.get_rooms_that_allow_join(state_ids) ) for space_id in allowed_rooms: - if await self._auth.check_host_in_room(space_id, origin): + if await self._event_auth_handler.check_host_in_room( + space_id, origin + ): return True # otherwise, check if the room is peekable diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 350646f458..669ea462e2 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -104,7 +104,7 @@ class BulkPushRuleEvaluator: def __init__(self, hs: "HomeServer"): self.hs = hs self.store = hs.get_datastore() - self.auth = hs.get_auth() + self._event_auth_handler = hs.get_event_auth_handler() # Used by `RulesForRoom` to ensure only one thing mutates the cache at a # time. Keyed off room_id. @@ -172,7 +172,7 @@ async def _get_power_levels_and_sender_level( # not having a power level event is an extreme edge case auth_events = {POWER_KEY: await self.store.get_event(pl_event_id)} else: - auth_events_ids = self.auth.compute_auth_events( + auth_events_ids = self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=False ) auth_events_dict = await self.store.get_events(auth_events_ids) diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index dfb9b3a0fa..18e92e90d7 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -734,7 +734,7 @@ def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() self.state = hs.get_state_handler() - self.auth = hs.get_auth() + self._event_auth_handler = hs.get_event_auth_handler() # We don't actually check signatures in tests, so lets just create a # random key to use. @@ -846,7 +846,7 @@ def _add_new_user(self, room_id, user_id): builder = EventBuilder( state=self.state, - auth=self.auth, + event_auth_handler=self._event_auth_handler, store=self.store, clock=self.clock, hostname=hostname, From 10671da05bdb72d98aab2a8937da503abfc836fd Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 2 Jul 2021 13:20:43 +0200 Subject: [PATCH 351/619] Fix bad link in modules documentation (#10302) Fix link in modules doc to point at instructions on registering a callback instead of ones on registering a web resource. --- changelog.d/10302.doc | 1 + docs/modules.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10302.doc diff --git a/changelog.d/10302.doc b/changelog.d/10302.doc new file mode 100644 index 0000000000..7386817de7 --- /dev/null +++ b/changelog.d/10302.doc @@ -0,0 +1 @@ +Fix link pointing at the wrong section in the modules documentation page. diff --git a/docs/modules.md b/docs/modules.md index 3a9fab61b8..bec1c06d15 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -194,7 +194,7 @@ In order to port a module that uses Synapse's old module interface, its author n * ensure the module's callbacks are all asynchronous. * register their callbacks using one or more of the `register_[...]_callbacks` methods - from the `ModuleApi` class in the module's `__init__` method (see [this section](#registering-a-web-resource) + from the `ModuleApi` class in the module's `__init__` method (see [this section](#registering-a-callback) for more info). Additionally, if the module is packaged with an additional web resource, the module From 7a5873277ef456e8446a05468ccae2d81e363977 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 5 Jul 2021 16:32:12 +0100 Subject: [PATCH 352/619] Add support for evicting cache entries based on last access time. (#10205) --- changelog.d/10205.feature | 1 + docs/sample_config.yaml | 62 +++++---- mypy.ini | 1 + synapse/app/_base.py | 11 +- synapse/config/_base.pyi | 2 + synapse/config/cache.py | 70 ++++++---- synapse/util/caches/lrucache.py | 237 +++++++++++++++++++++++++++----- synapse/util/linked_list.py | 150 ++++++++++++++++++++ tests/util/test_lrucache.py | 46 ++++++- 9 files changed, 485 insertions(+), 95 deletions(-) create mode 100644 changelog.d/10205.feature create mode 100644 synapse/util/linked_list.py diff --git a/changelog.d/10205.feature b/changelog.d/10205.feature new file mode 100644 index 0000000000..db3fd22587 --- /dev/null +++ b/changelog.d/10205.feature @@ -0,0 +1 @@ +Add support for evicting cache entries based on last access time. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 6fcc022b47..c04aca1f42 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -673,35 +673,41 @@ retention: #event_cache_size: 10K caches: - # Controls the global cache factor, which is the default cache factor - # for all caches if a specific factor for that cache is not otherwise - # set. - # - # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment - # variable. Setting by environment variable takes priority over - # setting through the config file. - # - # Defaults to 0.5, which will half the size of all caches. - # - #global_factor: 1.0 + # Controls the global cache factor, which is the default cache factor + # for all caches if a specific factor for that cache is not otherwise + # set. + # + # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment + # variable. Setting by environment variable takes priority over + # setting through the config file. + # + # Defaults to 0.5, which will half the size of all caches. + # + #global_factor: 1.0 - # A dictionary of cache name to cache factor for that individual - # cache. Overrides the global cache factor for a given cache. - # - # These can also be set through environment variables comprised - # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital - # letters and underscores. Setting by environment variable - # takes priority over setting through the config file. - # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0 - # - # Some caches have '*' and other characters that are not - # alphanumeric or underscores. These caches can be named with or - # without the special characters stripped. For example, to specify - # the cache factor for `*stateGroupCache*` via an environment - # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`. - # - per_cache_factors: - #get_users_who_share_room_with_user: 2.0 + # A dictionary of cache name to cache factor for that individual + # cache. Overrides the global cache factor for a given cache. + # + # These can also be set through environment variables comprised + # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital + # letters and underscores. Setting by environment variable + # takes priority over setting through the config file. + # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0 + # + # Some caches have '*' and other characters that are not + # alphanumeric or underscores. These caches can be named with or + # without the special characters stripped. For example, to specify + # the cache factor for `*stateGroupCache*` via an environment + # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`. + # + per_cache_factors: + #get_users_who_share_room_with_user: 2.0 + + # Controls how long an entry can be in a cache without having been + # accessed before being evicted. Defaults to None, which means + # entries are never evicted based on time. + # + #expiry_time: 30m ## Database ## diff --git a/mypy.ini b/mypy.ini index c4ff0e6618..72ce932d73 100644 --- a/mypy.ini +++ b/mypy.ini @@ -75,6 +75,7 @@ files = synapse/util/daemonize.py, synapse/util/hash.py, synapse/util/iterutils.py, + synapse/util/linked_list.py, synapse/util/metrics.py, synapse/util/macaroons.py, synapse/util/module_loader.py, diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 8879136881..b30571fe49 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -21,7 +21,7 @@ import sys import traceback import warnings -from typing import Awaitable, Callable, Iterable +from typing import TYPE_CHECKING, Awaitable, Callable, Iterable from cryptography.utils import CryptographyDeprecationWarning from typing_extensions import NoReturn @@ -41,10 +41,14 @@ from synapse.logging.context import PreserveLoggingContext from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.metrics.jemalloc import setup_jemalloc_stats +from synapse.util.caches.lrucache import setup_expire_lru_cache_entries from synapse.util.daemonize import daemonize_process from synapse.util.rlimit import change_resource_limit from synapse.util.versionstring import get_version_string +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) # list of tuples of function, args list, kwargs dict @@ -312,7 +316,7 @@ def refresh_certificate(hs): logger.info("Context factories updated.") -async def start(hs: "synapse.server.HomeServer"): +async def start(hs: "HomeServer"): """ Start a Synapse server or worker. @@ -365,6 +369,9 @@ def run_sighup(*args, **kwargs): load_legacy_spam_checkers(hs) + # If we've configured an expiry time for caches, start the background job now. + setup_expire_lru_cache_entries(hs) + # It is now safe to start your Synapse. hs.start_listening() hs.get_datastore().db_pool.start_profiling() diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi index 23ca0c83c1..06fbd1166b 100644 --- a/synapse/config/_base.pyi +++ b/synapse/config/_base.pyi @@ -5,6 +5,7 @@ from synapse.config import ( api, appservice, auth, + cache, captcha, cas, consent, @@ -88,6 +89,7 @@ class RootConfig: tracer: tracer.TracerConfig redis: redis.RedisConfig modules: modules.ModulesConfig + caches: cache.CacheConfig federation: federation.FederationConfig config_classes: List = ... diff --git a/synapse/config/cache.py b/synapse/config/cache.py index 91165ee1ce..7789b40323 100644 --- a/synapse/config/cache.py +++ b/synapse/config/cache.py @@ -116,35 +116,41 @@ def generate_config_section(self, **kwargs): #event_cache_size: 10K caches: - # Controls the global cache factor, which is the default cache factor - # for all caches if a specific factor for that cache is not otherwise - # set. - # - # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment - # variable. Setting by environment variable takes priority over - # setting through the config file. - # - # Defaults to 0.5, which will half the size of all caches. - # - #global_factor: 1.0 - - # A dictionary of cache name to cache factor for that individual - # cache. Overrides the global cache factor for a given cache. - # - # These can also be set through environment variables comprised - # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital - # letters and underscores. Setting by environment variable - # takes priority over setting through the config file. - # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0 - # - # Some caches have '*' and other characters that are not - # alphanumeric or underscores. These caches can be named with or - # without the special characters stripped. For example, to specify - # the cache factor for `*stateGroupCache*` via an environment - # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`. - # - per_cache_factors: - #get_users_who_share_room_with_user: 2.0 + # Controls the global cache factor, which is the default cache factor + # for all caches if a specific factor for that cache is not otherwise + # set. + # + # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment + # variable. Setting by environment variable takes priority over + # setting through the config file. + # + # Defaults to 0.5, which will half the size of all caches. + # + #global_factor: 1.0 + + # A dictionary of cache name to cache factor for that individual + # cache. Overrides the global cache factor for a given cache. + # + # These can also be set through environment variables comprised + # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital + # letters and underscores. Setting by environment variable + # takes priority over setting through the config file. + # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0 + # + # Some caches have '*' and other characters that are not + # alphanumeric or underscores. These caches can be named with or + # without the special characters stripped. For example, to specify + # the cache factor for `*stateGroupCache*` via an environment + # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`. + # + per_cache_factors: + #get_users_who_share_room_with_user: 2.0 + + # Controls how long an entry can be in a cache without having been + # accessed before being evicted. Defaults to None, which means + # entries are never evicted based on time. + # + #expiry_time: 30m """ def read_config(self, config, **kwargs): @@ -200,6 +206,12 @@ def read_config(self, config, **kwargs): e.message # noqa: B306, DependencyException.message is a property ) + expiry_time = cache_config.get("expiry_time") + if expiry_time: + self.expiry_time_msec = self.parse_duration(expiry_time) + else: + self.expiry_time_msec = None + # Resize all caches (if necessary) with the new factors we've loaded self.resize_all_caches() diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index d89e9d9b1d..4b9d0433ff 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import threading +import weakref from functools import wraps from typing import ( + TYPE_CHECKING, Any, Callable, Collection, @@ -31,10 +34,19 @@ from typing_extensions import Literal +from twisted.internet import reactor + from synapse.config import cache as cache_config -from synapse.util import caches +from synapse.metrics.background_process_metrics import wrap_as_background_process +from synapse.util import Clock, caches from synapse.util.caches import CacheMetric, register_cache from synapse.util.caches.treecache import TreeCache, iterate_tree_cache_entry +from synapse.util.linked_list import ListNode + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) try: from pympler.asizeof import Asizer @@ -82,19 +94,126 @@ def enumerate_leaves(node, depth): yield m +P = TypeVar("P") + + +class _TimedListNode(ListNode[P]): + """A `ListNode` that tracks last access time.""" + + __slots__ = ["last_access_ts_secs"] + + def update_last_access(self, clock: Clock): + self.last_access_ts_secs = int(clock.time()) + + +# Whether to insert new cache entries to the global list. We only add to it if +# time based eviction is enabled. +USE_GLOBAL_LIST = False + +# A linked list of all cache entries, allowing efficient time based eviction. +GLOBAL_ROOT = ListNode["_Node"].create_root_node() + + +@wrap_as_background_process("LruCache._expire_old_entries") +async def _expire_old_entries(clock: Clock, expiry_seconds: int): + """Walks the global cache list to find cache entries that haven't been + accessed in the given number of seconds. + """ + + now = int(clock.time()) + node = GLOBAL_ROOT.prev_node + assert node is not None + + i = 0 + + logger.debug("Searching for stale caches") + + while node is not GLOBAL_ROOT: + # Only the root node isn't a `_TimedListNode`. + assert isinstance(node, _TimedListNode) + + if node.last_access_ts_secs > now - expiry_seconds: + break + + cache_entry = node.get_cache_entry() + next_node = node.prev_node + + # The node should always have a reference to a cache entry and a valid + # `prev_node`, as we only drop them when we remove the node from the + # list. + assert next_node is not None + assert cache_entry is not None + cache_entry.drop_from_cache() + + # If we do lots of work at once we yield to allow other stuff to happen. + if (i + 1) % 10000 == 0: + logger.debug("Waiting during drop") + await clock.sleep(0) + logger.debug("Waking during drop") + + node = next_node + + # If we've yielded then our current node may have been evicted, so we + # need to check that its still valid. + if node.prev_node is None: + break + + i += 1 + + logger.info("Dropped %d items from caches", i) + + +def setup_expire_lru_cache_entries(hs: "HomeServer"): + """Start a background job that expires all cache entries if they have not + been accessed for the given number of seconds. + """ + if not hs.config.caches.expiry_time_msec: + return + + logger.info( + "Expiring LRU caches after %d seconds", hs.config.caches.expiry_time_msec / 1000 + ) + + global USE_GLOBAL_LIST + USE_GLOBAL_LIST = True + + clock = hs.get_clock() + clock.looping_call( + _expire_old_entries, 30 * 1000, clock, hs.config.caches.expiry_time_msec / 1000 + ) + + class _Node: - __slots__ = ["prev_node", "next_node", "key", "value", "callbacks", "memory"] + __slots__ = [ + "_list_node", + "_global_list_node", + "_cache", + "key", + "value", + "callbacks", + "memory", + ] def __init__( self, - prev_node, - next_node, + root: "ListNode[_Node]", key, value, + cache: "weakref.ReferenceType[LruCache]", + clock: Clock, callbacks: Collection[Callable[[], None]] = (), ): - self.prev_node = prev_node - self.next_node = next_node + self._list_node = ListNode.insert_after(self, root) + self._global_list_node = None + if USE_GLOBAL_LIST: + self._global_list_node = _TimedListNode.insert_after(self, GLOBAL_ROOT) + self._global_list_node.update_last_access(clock) + + # We store a weak reference to the cache object so that this _Node can + # remove itself from the cache. If the cache is dropped we ensure we + # remove our entries in the lists. + self._cache = cache + self.key = key self.value = value @@ -116,11 +235,16 @@ def __init__( self.memory = ( _get_size_of(key) + _get_size_of(value) + + _get_size_of(self._list_node, recurse=False) + _get_size_of(self.callbacks, recurse=False) + _get_size_of(self, recurse=False) ) self.memory += _get_size_of(self.memory, recurse=False) + if self._global_list_node: + self.memory += _get_size_of(self._global_list_node, recurse=False) + self.memory += _get_size_of(self._global_list_node.last_access_ts_secs) + def add_callbacks(self, callbacks: Collection[Callable[[], None]]) -> None: """Add to stored list of callbacks, removing duplicates.""" @@ -147,6 +271,32 @@ def run_and_clear_callbacks(self) -> None: self.callbacks = None + def drop_from_cache(self) -> None: + """Drop this node from the cache. + + Ensures that the entry gets removed from the cache and that we get + removed from all lists. + """ + cache = self._cache() + if not cache or not cache.pop(self.key, None): + # `cache.pop` should call `drop_from_lists()`, unless this Node had + # already been removed from the cache. + self.drop_from_lists() + + def drop_from_lists(self) -> None: + """Remove this node from the cache lists.""" + self._list_node.remove_from_list() + + if self._global_list_node: + self._global_list_node.remove_from_list() + + def move_to_front(self, clock: Clock, cache_list_root: ListNode) -> None: + """Moves this node to the front of all the lists its in.""" + self._list_node.move_after(cache_list_root) + if self._global_list_node: + self._global_list_node.move_after(GLOBAL_ROOT) + self._global_list_node.update_last_access(clock) + class LruCache(Generic[KT, VT]): """ @@ -163,6 +313,7 @@ def __init__( size_callback: Optional[Callable] = None, metrics_collection_callback: Optional[Callable[[], None]] = None, apply_cache_factor_from_config: bool = True, + clock: Optional[Clock] = None, ): """ Args: @@ -188,6 +339,13 @@ def __init__( apply_cache_factor_from_config (bool): If true, `max_size` will be multiplied by a cache factor derived from the homeserver config """ + # Default `clock` to something sensible. Note that we rename it to + # `real_clock` so that mypy doesn't think its still `Optional`. + if clock is None: + real_clock = Clock(reactor) + else: + real_clock = clock + cache = cache_type() self.cache = cache # Used for introspection. self.apply_cache_factor_from_config = apply_cache_factor_from_config @@ -219,17 +377,31 @@ def __init__( # this is exposed for access from outside this class self.metrics = metrics - list_root = _Node(None, None, None, None) - list_root.next_node = list_root - list_root.prev_node = list_root + # We create a single weakref to self here so that we don't need to keep + # creating more each time we create a `_Node`. + weak_ref_to_self = weakref.ref(self) + + list_root = ListNode[_Node].create_root_node() lock = threading.Lock() def evict(): while cache_len() > self.max_size: + # Get the last node in the list (i.e. the oldest node). todelete = list_root.prev_node - evicted_len = delete_node(todelete) - cache.pop(todelete.key, None) + + # The list root should always have a valid `prev_node` if the + # cache is not empty. + assert todelete is not None + + # The node should always have a reference to a cache entry, as + # we only drop the cache entry when we remove the node from the + # list. + node = todelete.get_cache_entry() + assert node is not None + + evicted_len = delete_node(node) + cache.pop(node.key, None) if metrics: metrics.inc_evictions(evicted_len) @@ -255,11 +427,7 @@ def cache_len(): self.len = synchronized(cache_len) def add_node(key, value, callbacks: Collection[Callable[[], None]] = ()): - prev_node = list_root - next_node = prev_node.next_node - node = _Node(prev_node, next_node, key, value, callbacks) - prev_node.next_node = node - next_node.prev_node = node + node = _Node(list_root, key, value, weak_ref_to_self, real_clock, callbacks) cache[key] = node if size_callback: @@ -268,23 +436,11 @@ def add_node(key, value, callbacks: Collection[Callable[[], None]] = ()): if caches.TRACK_MEMORY_USAGE and metrics: metrics.inc_memory_usage(node.memory) - def move_node_to_front(node): - prev_node = node.prev_node - next_node = node.next_node - prev_node.next_node = next_node - next_node.prev_node = prev_node - prev_node = list_root - next_node = prev_node.next_node - node.prev_node = prev_node - node.next_node = next_node - prev_node.next_node = node - next_node.prev_node = node - - def delete_node(node): - prev_node = node.prev_node - next_node = node.next_node - prev_node.next_node = next_node - next_node.prev_node = prev_node + def move_node_to_front(node: _Node): + node.move_to_front(real_clock, list_root) + + def delete_node(node: _Node) -> int: + node.drop_from_lists() deleted_len = 1 if size_callback: @@ -411,10 +567,13 @@ def cache_del_multi(key: KT) -> None: @synchronized def cache_clear() -> None: - list_root.next_node = list_root - list_root.prev_node = list_root for node in cache.values(): node.run_and_clear_callbacks() + node.drop_from_lists() + + assert list_root.next_node == list_root + assert list_root.prev_node == list_root + cache.clear() if size_callback: cached_cache_len[0] = 0 @@ -484,3 +643,11 @@ def set_cache_factor(self, factor: float) -> bool: self._on_resize() return True return False + + def __del__(self) -> None: + # We're about to be deleted, so we make sure to clear up all the nodes + # and run callbacks, etc. + # + # This happens e.g. in the sync code where we have an expiring cache of + # lru caches. + self.clear() diff --git a/synapse/util/linked_list.py b/synapse/util/linked_list.py new file mode 100644 index 0000000000..a456b136f0 --- /dev/null +++ b/synapse/util/linked_list.py @@ -0,0 +1,150 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A circular doubly linked list implementation. +""" + +import threading +from typing import Generic, Optional, Type, TypeVar + +P = TypeVar("P") +LN = TypeVar("LN", bound="ListNode") + + +class ListNode(Generic[P]): + """A node in a circular doubly linked list, with an (optional) reference to + a cache entry. + + The reference should only be `None` for the root node or if the node has + been removed from the list. + """ + + # A lock to protect mutating the list prev/next pointers. + _LOCK = threading.Lock() + + # We don't use attrs here as in py3.6 you can't have `attr.s(slots=True)` + # and inherit from `Generic` for some reason + __slots__ = [ + "cache_entry", + "prev_node", + "next_node", + ] + + def __init__(self, cache_entry: Optional[P] = None) -> None: + self.cache_entry = cache_entry + self.prev_node: Optional[ListNode[P]] = None + self.next_node: Optional[ListNode[P]] = None + + @classmethod + def create_root_node(cls: Type["ListNode[P]"]) -> "ListNode[P]": + """Create a new linked list by creating a "root" node, which is a node + that has prev_node/next_node pointing to itself and no associated cache + entry. + """ + root = cls() + root.prev_node = root + root.next_node = root + return root + + @classmethod + def insert_after( + cls: Type[LN], + cache_entry: P, + node: "ListNode[P]", + ) -> LN: + """Create a new list node that is placed after the given node. + + Args: + cache_entry: The associated cache entry. + node: The existing node in the list to insert the new entry after. + """ + new_node = cls(cache_entry) + with cls._LOCK: + new_node._refs_insert_after(node) + return new_node + + def remove_from_list(self): + """Remove this node from the list.""" + with self._LOCK: + self._refs_remove_node_from_list() + + # We drop the reference to the cache entry to break the reference cycle + # between the list node and cache entry, allowing the two to be dropped + # immediately rather than at the next GC. + self.cache_entry = None + + def move_after(self, node: "ListNode"): + """Move this node from its current location in the list to after the + given node. + """ + with self._LOCK: + # We assert that both this node and the target node is still "alive". + assert self.prev_node + assert self.next_node + assert node.prev_node + assert node.next_node + + assert self is not node + + # Remove self from the list + self._refs_remove_node_from_list() + + # Insert self back into the list, after target node + self._refs_insert_after(node) + + def _refs_remove_node_from_list(self): + """Internal method to *just* remove the node from the list, without + e.g. clearing out the cache entry. + """ + if self.prev_node is None or self.next_node is None: + # We've already been removed from the list. + return + + prev_node = self.prev_node + next_node = self.next_node + + prev_node.next_node = next_node + next_node.prev_node = prev_node + + # We set these to None so that we don't get circular references, + # allowing us to be dropped without having to go via the GC. + self.prev_node = None + self.next_node = None + + def _refs_insert_after(self, node: "ListNode"): + """Internal method to insert the node after the given node.""" + + # This method should only be called when we're not already in the list. + assert self.prev_node is None + assert self.next_node is None + + # We expect the given node to be in the list and thus have valid + # prev/next refs. + assert node.next_node + assert node.prev_node + + prev_node = node + next_node = node.next_node + + self.prev_node = prev_node + self.next_node = next_node + + prev_node.next_node = self + next_node.prev_node = self + + def get_cache_entry(self) -> Optional[P]: + """Get the cache entry, returns None if this is the root node (i.e. + cache_entry is None) or if the entry has been dropped. + """ + return self.cache_entry diff --git a/tests/util/test_lrucache.py b/tests/util/test_lrucache.py index 377904e72e..6578f3411e 100644 --- a/tests/util/test_lrucache.py +++ b/tests/util/test_lrucache.py @@ -15,7 +15,7 @@ from unittest.mock import Mock -from synapse.util.caches.lrucache import LruCache +from synapse.util.caches.lrucache import LruCache, setup_expire_lru_cache_entries from synapse.util.caches.treecache import TreeCache from tests import unittest @@ -260,3 +260,47 @@ def test_evict(self): self.assertEquals(cache["key3"], [3]) self.assertEquals(cache["key4"], [4]) self.assertEquals(cache["key5"], [5, 6]) + + +class TimeEvictionTestCase(unittest.HomeserverTestCase): + """Test that time based eviction works correctly.""" + + def default_config(self): + config = super().default_config() + + config.setdefault("caches", {})["expiry_time"] = "30m" + + return config + + def test_evict(self): + setup_expire_lru_cache_entries(self.hs) + + cache = LruCache(5, clock=self.hs.get_clock()) + + # Check that we evict entries we haven't accessed for 30 minutes. + cache["key1"] = 1 + cache["key2"] = 2 + + self.reactor.advance(20 * 60) + + self.assertEqual(cache.get("key1"), 1) + + self.reactor.advance(20 * 60) + + # We have only touched `key1` in the last 30m, so we expect that to + # still be in the cache while `key2` should have been evicted. + self.assertEqual(cache.get("key1"), 1) + self.assertEqual(cache.get("key2"), None) + + # Check that re-adding an expired key works correctly. + cache["key2"] = 3 + self.assertEqual(cache.get("key2"), 3) + + self.reactor.advance(20 * 60) + + self.assertEqual(cache.get("key2"), 3) + + self.reactor.advance(20 * 60) + + self.assertEqual(cache.get("key1"), None) + self.assertEqual(cache.get("key2"), 3) From d7a94a7dcc955e08bf6bc62b95e02965b304af7f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 6 Jul 2021 11:00:05 +0100 Subject: [PATCH 353/619] Add upgrade notes about disk space for events migration (#10314) --- CHANGES.md | 4 ++++ changelog.d/10314.bugfix | 1 + docs/upgrade.md | 40 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10314.bugfix diff --git a/CHANGES.md b/CHANGES.md index 0c64d5bda6..a2fc423096 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +Synapse 1.38.0 (**UNRELEASED**) +=============================== +This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade.md#upgrading-to-v1380) for more information. + Synapse 1.37.1 (2021-06-30) =========================== diff --git a/changelog.d/10314.bugfix b/changelog.d/10314.bugfix new file mode 100644 index 0000000000..7ebda7cdc2 --- /dev/null +++ b/changelog.d/10314.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/docs/upgrade.md b/docs/upgrade.md index a44960c2b8..011aadf638 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -84,7 +84,45 @@ process, for example: wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb ``` - + + +# Upgrading to v1.38.0 + +## Re-indexing of `events` table on Postgres databases + +This release includes a database schema update which requires re-indexing one of +the larger tables in the database, `events`. This could result in increased +disk I/O for several hours or days after upgrading while the migration +completes. Furthermore, because we have to keep the old indexes until the new +indexes are ready, it could result in a significant, temporary, increase in +disk space. + +To get a rough idea of the disk space required, check the current size of one +of the indexes. For example, from a `psql` shell, run the following sql: + +```sql +SELECT pg_size_pretty(pg_relation_size('events_order_room')); +``` + +We need to rebuild **four** indexes, so you will need to multiply this result +by four to give an estimate of the disk space required. For example, on one +particular server: + +``` +synapse=# select pg_size_pretty(pg_relation_size('events_order_room')); + pg_size_pretty +---------------- + 288 MB +(1 row) +``` + +On this server, it would be wise to ensure that at least 1152MB are free. + +The additional disk space will be freed once the migration completes. + +SQLite databases are unaffected by this change. + + # Upgrading to v1.37.0 ## Deprecation of the current spam checker interface From c65067d67307de7688fa39246426370421e56452 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jul 2021 13:02:37 +0100 Subject: [PATCH 354/619] Handle old staged inbound events (#10303) We might have events in the staging area if the service was restarted while there were unhandled events in the staging area. Fixes #10295 --- changelog.d/10303.bugfix | 1 + synapse/federation/federation_server.py | 67 ++++++++++++++++--- .../databases/main/event_federation.py | 9 +++ 3 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10303.bugfix diff --git a/changelog.d/10303.bugfix b/changelog.d/10303.bugfix new file mode 100644 index 0000000000..c0577c9f73 --- /dev/null +++ b/changelog.d/10303.bugfix @@ -0,0 +1 @@ +Ensure that inbound events from federation that were being processed when Synapse was restarted get promptly processed on start up. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index b312d0b809..bf67d0f574 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -148,6 +148,41 @@ def __init__(self, hs: "HomeServer"): self._room_prejoin_state_types = hs.config.api.room_prejoin_state + # Whether we have started handling old events in the staging area. + self._started_handling_of_staged_events = False + + @wrap_as_background_process("_handle_old_staged_events") + async def _handle_old_staged_events(self) -> None: + """Handle old staged events by fetching all rooms that have staged + events and start the processing of each of those rooms. + """ + + # Get all the rooms IDs with staged events. + room_ids = await self.store.get_all_rooms_with_staged_incoming_events() + + # We then shuffle them so that if there are multiple instances doing + # this work they're less likely to collide. + random.shuffle(room_ids) + + for room_id in room_ids: + room_version = await self.store.get_room_version(room_id) + + # Try and acquire the processing lock for the room, if we get it start a + # background process for handling the events in the room. + lock = await self.store.try_acquire_lock( + _INBOUND_EVENT_HANDLING_LOCK_NAME, room_id + ) + if lock: + logger.info("Handling old staged inbound events in %s", room_id) + self._process_incoming_pdus_in_room_inner( + room_id, + room_version, + lock, + ) + + # We pause a bit so that we don't start handling all rooms at once. + await self._clock.sleep(random.uniform(0, 0.1)) + async def on_backfill_request( self, origin: str, room_id: str, versions: List[str], limit: int ) -> Tuple[int, Dict[str, Any]]: @@ -166,6 +201,12 @@ async def on_backfill_request( async def on_incoming_transaction( self, origin: str, transaction_data: JsonDict ) -> Tuple[int, Dict[str, Any]]: + # If we receive a transaction we should make sure that kick off handling + # any old events in the staging area. + if not self._started_handling_of_staged_events: + self._started_handling_of_staged_events = True + self._handle_old_staged_events() + # keep this as early as possible to make the calculated origin ts as # accurate as possible. request_time = self._clock.time_msec() @@ -882,25 +923,28 @@ async def _process_incoming_pdus_in_room_inner( room_id: str, room_version: RoomVersion, lock: Lock, - latest_origin: str, - latest_event: EventBase, + latest_origin: Optional[str] = None, + latest_event: Optional[EventBase] = None, ) -> None: """Process events in the staging area for the given room. The latest_origin and latest_event args are the latest origin and event - received. + received (or None to simply pull the next event from the database). """ # The common path is for the event we just received be the only event in # the room, so instead of pulling the event out of the DB and parsing # the event we just pull out the next event ID and check if that matches. - next_origin, next_event_id = await self.store.get_next_staged_event_id_for_room( - room_id - ) - if next_origin == latest_origin and next_event_id == latest_event.event_id: - origin = latest_origin - event = latest_event - else: + if latest_event is not None and latest_origin is not None: + ( + next_origin, + next_event_id, + ) = await self.store.get_next_staged_event_id_for_room(room_id) + if next_origin != latest_origin or next_event_id != latest_event.event_id: + latest_origin = None + latest_event = None + + if latest_origin is None or latest_event is None: next = await self.store.get_next_staged_event_for_room( room_id, room_version ) @@ -908,6 +952,9 @@ async def _process_incoming_pdus_in_room_inner( return origin, event = next + else: + origin = latest_origin + event = latest_event # We loop round until there are no more events in the room in the # staging area, or we fail to get the lock (which means another process diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index 08d75b0d41..c4474df975 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -1207,6 +1207,15 @@ def _get_next_staged_event_for_room_txn(txn): return origin, event + async def get_all_rooms_with_staged_incoming_events(self) -> List[str]: + """Get the room IDs of all events currently staged.""" + return await self.db_pool.simple_select_onecol( + table="federation_inbound_events_staging", + keyvalues={}, + retcol="DISTINCT room_id", + desc="get_all_rooms_with_staged_incoming_events", + ) + @wrap_as_background_process("_get_stats_for_federation_staging") async def _get_stats_for_federation_staging(self): """Update the prometheus metrics for the inbound federation staging area.""" From 6655ea558727138a80ea70fdbd9ee89b041f180f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 6 Jul 2021 13:03:16 +0100 Subject: [PATCH 355/619] Add script for getting info about recently registered users (#10290) --- changelog.d/10290.feature | 1 + debian/changelog | 6 + debian/hash_password.1 | 42 +----- debian/hash_password.ronn | 2 +- debian/manpages | 1 + debian/matrix-synapse-py3.links | 1 + debian/register_new_matrix_user.1 | 37 +---- debian/register_new_matrix_user.ronn | 2 +- debian/synapse_port_db.1 | 59 ++------ debian/synapse_port_db.ronn | 8 +- debian/synapse_review_recent_signups.1 | 26 ++++ debian/synapse_review_recent_signups.ronn | 37 +++++ debian/synctl.1 | 42 ++---- debian/synctl.ronn | 2 +- scripts/synapse_review_recent_signups | 19 +++ synapse/_scripts/review_recent_signups.py | 175 ++++++++++++++++++++++ synapse/storage/database.py | 2 +- 17 files changed, 309 insertions(+), 153 deletions(-) create mode 100644 changelog.d/10290.feature create mode 100644 debian/synapse_review_recent_signups.1 create mode 100644 debian/synapse_review_recent_signups.ronn create mode 100755 scripts/synapse_review_recent_signups create mode 100644 synapse/_scripts/review_recent_signups.py diff --git a/changelog.d/10290.feature b/changelog.d/10290.feature new file mode 100644 index 0000000000..4e4c2e24ef --- /dev/null +++ b/changelog.d/10290.feature @@ -0,0 +1 @@ +Add script to print information about recently registered users. diff --git a/debian/changelog b/debian/changelog index 35a0cddeaf..cafd03c6c1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.37.1ubuntu1) UNRELEASED; urgency=medium + + * Add synapse_review_recent_signups script + + -- Erik Johnston Thu, 01 Jul 2021 15:55:03 +0100 + matrix-synapse-py3 (1.37.1) stable; urgency=medium * New synapse release 1.37.1. diff --git a/debian/hash_password.1 b/debian/hash_password.1 index 383f452991..d64b91e7c8 100644 --- a/debian/hash_password.1 +++ b/debian/hash_password.1 @@ -1,90 +1,58 @@ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "HASH_PASSWORD" "1" "February 2017" "" "" -. +.\" generated with Ronn-NG/v0.8.0 +.\" http://github.com/apjanke/ronn-ng/tree/0.8.0 +.TH "HASH_PASSWORD" "1" "July 2021" "" "" .SH "NAME" \fBhash_password\fR \- Calculate the hash of a new password, so that passwords can be reset -. .SH "SYNOPSIS" \fBhash_password\fR [\fB\-p\fR|\fB\-\-password\fR [password]] [\fB\-c\fR|\fB\-\-config\fR \fIfile\fR] -. .SH "DESCRIPTION" \fBhash_password\fR calculates the hash of a supplied password using bcrypt\. -. .P \fBhash_password\fR takes a password as an parameter either on the command line or the \fBSTDIN\fR if not supplied\. -. .P It accepts an YAML file which can be used to specify parameters like the number of rounds for bcrypt and password_config section having the pepper value used for the hashing\. By default \fBbcrypt_rounds\fR is set to \fB10\fR\. -. .P The hashed password is written on the \fBSTDOUT\fR\. -. .SH "FILES" A sample YAML file accepted by \fBhash_password\fR is described below: -. .P bcrypt_rounds: 17 password_config: pepper: "random hashing pepper" -. .SH "OPTIONS" -. .TP \fB\-p\fR, \fB\-\-password\fR Read the password form the command line if [password] is supplied\. If not, prompt the user and read the password form the \fBSTDIN\fR\. It is not recommended to type the password on the command line directly\. Use the STDIN instead\. -. .TP \fB\-c\fR, \fB\-\-config\fR Read the supplied YAML \fIfile\fR containing the options \fBbcrypt_rounds\fR and the \fBpassword_config\fR section containing the \fBpepper\fR value\. -. .SH "EXAMPLES" Hash from the command line: -. .IP "" 4 -. .nf - $ hash_password \-p "p@ssw0rd" $2b$12$VJNqWQYfsWTEwcELfoSi4Oa8eA17movHqqi8\.X8fWFpum7SxZ9MFe -. .fi -. .IP "" 0 -. .P Hash from the STDIN: -. .IP "" 4 -. .nf - $ hash_password Password: Confirm password: $2b$12$AszlvfmJl2esnyhmn8m/kuR2tdXgROWtWxnX\.rcuAbM8ErLoUhybG -. .fi -. .IP "" 0 -. .P Using a config file: -. .IP "" 4 -. .nf - $ hash_password \-c config\.yml Password: Confirm password: $2b$12$CwI\.wBNr\.w3kmiUlV3T5s\.GT2wH7uebDCovDrCOh18dFedlANK99O -. .fi -. .IP "" 0 -. .SH "COPYRIGHT" -This man page was written by Rahul De <\fIrahulde@swecha\.net\fR> for Debian GNU/Linux distribution\. -. +This man page was written by Rahul De <\fI\%mailto:rahulde@swecha\.net\fR> for Debian GNU/Linux distribution\. .SH "SEE ALSO" -synctl(1), synapse_port_db(1), register_new_matrix_user(1) +synctl(1), synapse_port_db(1), register_new_matrix_user(1), synapse_review_recent_signups(1) diff --git a/debian/hash_password.ronn b/debian/hash_password.ronn index 0b2afa7374..eeb354602d 100644 --- a/debian/hash_password.ronn +++ b/debian/hash_password.ronn @@ -66,4 +66,4 @@ for Debian GNU/Linux distribution. ## SEE ALSO -synctl(1), synapse_port_db(1), register_new_matrix_user(1) +synctl(1), synapse_port_db(1), register_new_matrix_user(1), synapse_review_recent_signups(1) diff --git a/debian/manpages b/debian/manpages index 2c30583530..4b13f52853 100644 --- a/debian/manpages +++ b/debian/manpages @@ -1,4 +1,5 @@ debian/hash_password.1 debian/register_new_matrix_user.1 debian/synapse_port_db.1 +debian/synapse_review_recent_signups.1 debian/synctl.1 diff --git a/debian/matrix-synapse-py3.links b/debian/matrix-synapse-py3.links index bf19efa562..53e2965418 100644 --- a/debian/matrix-synapse-py3.links +++ b/debian/matrix-synapse-py3.links @@ -1,4 +1,5 @@ opt/venvs/matrix-synapse/bin/hash_password usr/bin/hash_password opt/venvs/matrix-synapse/bin/register_new_matrix_user usr/bin/register_new_matrix_user opt/venvs/matrix-synapse/bin/synapse_port_db usr/bin/synapse_port_db +opt/venvs/matrix-synapse/bin/synapse_review_recent_signups usr/bin/synapse_review_recent_signups opt/venvs/matrix-synapse/bin/synctl usr/bin/synctl diff --git a/debian/register_new_matrix_user.1 b/debian/register_new_matrix_user.1 index 99156a7354..57bfc4e024 100644 --- a/debian/register_new_matrix_user.1 +++ b/debian/register_new_matrix_user.1 @@ -1,72 +1,47 @@ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "REGISTER_NEW_MATRIX_USER" "1" "February 2017" "" "" -. +.\" generated with Ronn-NG/v0.8.0 +.\" http://github.com/apjanke/ronn-ng/tree/0.8.0 +.TH "REGISTER_NEW_MATRIX_USER" "1" "July 2021" "" "" .SH "NAME" \fBregister_new_matrix_user\fR \- Used to register new users with a given home server when registration has been disabled -. .SH "SYNOPSIS" -\fBregister_new_matrix_user\fR options\.\.\. -. +\fBregister_new_matrix_user\fR options\|\.\|\.\|\. .SH "DESCRIPTION" \fBregister_new_matrix_user\fR registers new users with a given home server when registration has been disabled\. For this to work, the home server must be configured with the \'registration_shared_secret\' option set\. -. .P This accepts the user credentials like the username, password, is user an admin or not and registers the user onto the homeserver database\. Also, a YAML file containing the shared secret can be provided\. If not, the shared secret can be provided via the command line\. -. .P By default it assumes the home server URL to be \fBhttps://localhost:8448\fR\. This can be changed via the \fBserver_url\fR command line option\. -. .SH "FILES" A sample YAML file accepted by \fBregister_new_matrix_user\fR is described below: -. .IP "" 4 -. .nf - registration_shared_secret: "s3cr3t" -. .fi -. .IP "" 0 -. .SH "OPTIONS" -. .TP \fB\-u\fR, \fB\-\-user\fR Local part of the new user\. Will prompt if omitted\. -. .TP \fB\-p\fR, \fB\-\-password\fR New password for user\. Will prompt if omitted\. Supplying the password on the command line is not recommended\. Use the STDIN instead\. -. .TP \fB\-a\fR, \fB\-\-admin\fR Register new user as an admin\. Will prompt if omitted\. -. .TP \fB\-c\fR, \fB\-\-config\fR Path to server config file containing the shared secret\. -. .TP \fB\-k\fR, \fB\-\-shared\-secret\fR Shared secret as defined in server config file\. This is an optional parameter as it can be also supplied via the YAML file\. -. .TP \fBserver_url\fR URL of the home server\. Defaults to \'https://localhost:8448\'\. -. .SH "EXAMPLES" -. .nf - $ register_new_matrix_user \-u user1 \-p p@ssword \-a \-c config\.yaml -. .fi -. .SH "COPYRIGHT" -This man page was written by Rahul De <\fIrahulde@swecha\.net\fR> for Debian GNU/Linux distribution\. -. +This man page was written by Rahul De <\fI\%mailto:rahulde@swecha\.net\fR> for Debian GNU/Linux distribution\. .SH "SEE ALSO" -synctl(1), synapse_port_db(1), hash_password(1) +synctl(1), synapse_port_db(1), hash_password(1), synapse_review_recent_signups(1) diff --git a/debian/register_new_matrix_user.ronn b/debian/register_new_matrix_user.ronn index 4c22e74dde..0410b1f4cd 100644 --- a/debian/register_new_matrix_user.ronn +++ b/debian/register_new_matrix_user.ronn @@ -58,4 +58,4 @@ for Debian GNU/Linux distribution. ## SEE ALSO -synctl(1), synapse_port_db(1), hash_password(1) +synctl(1), synapse_port_db(1), hash_password(1), synapse_review_recent_signups(1) diff --git a/debian/synapse_port_db.1 b/debian/synapse_port_db.1 index 4e6bc04827..0e7e20001c 100644 --- a/debian/synapse_port_db.1 +++ b/debian/synapse_port_db.1 @@ -1,83 +1,56 @@ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "SYNAPSE_PORT_DB" "1" "February 2017" "" "" -. +.\" generated with Ronn-NG/v0.8.0 +.\" http://github.com/apjanke/ronn-ng/tree/0.8.0 +.TH "SYNAPSE_PORT_DB" "1" "July 2021" "" "" .SH "NAME" \fBsynapse_port_db\fR \- A script to port an existing synapse SQLite database to a new PostgreSQL database\. -. .SH "SYNOPSIS" \fBsynapse_port_db\fR [\-v] \-\-sqlite\-database=\fIdbfile\fR \-\-postgres\-config=\fIyamlconfig\fR [\-\-curses] [\-\-batch\-size=\fIbatch\-size\fR] -. .SH "DESCRIPTION" \fBsynapse_port_db\fR ports an existing synapse SQLite database to a new PostgreSQL database\. -. .P SQLite database is specified with \fB\-\-sqlite\-database\fR option and PostgreSQL configuration required to connect to PostgreSQL database is provided using \fB\-\-postgres\-config\fR configuration\. The configuration is specified in YAML format\. -. .SH "OPTIONS" -. .TP \fB\-v\fR Print log messages in \fBdebug\fR level instead of \fBinfo\fR level\. -. .TP \fB\-\-sqlite\-database\fR The snapshot of the SQLite database file\. This must not be currently used by a running synapse server\. -. .TP \fB\-\-postgres\-config\fR The database config file for the PostgreSQL database\. -. .TP \fB\-\-curses\fR Display a curses based progress UI\. -. .SH "CONFIG FILE" The postgres configuration file must be a valid YAML file with the following options\. -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBdatabase\fR: Database configuration section\. This section header can be ignored and the options below may be specified as top level keys\. -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBname\fR: Connector to use when connecting to the database\. This value must be \fBpsycopg2\fR\. -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBargs\fR: DB API 2\.0 compatible arguments to send to the \fBpsycopg2\fR module\. -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBdbname\fR \- the database name -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBuser\fR \- user name used to authenticate -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBpassword\fR \- password used to authenticate -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBhost\fR \- database host address (defaults to UNIX socket if not provided) -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBport\fR \- connection port number (defaults to 5432 if not provided) -. .IP "" 0 -. -.IP "\(bu" 4 +.IP "\[ci]" 4 \fBsynchronous_commit\fR: Optional\. Default is True\. If the value is \fBFalse\fR, enable asynchronous commit and don\'t wait for the server to call fsync before ending the transaction\. See: https://www\.postgresql\.org/docs/current/static/wal\-async\-commit\.html -. .IP "" 0 -. .IP "" 0 -. .P Following example illustrates the configuration file format\. -. .IP "" 4 -. .nf - database: name: psycopg2 args: @@ -86,13 +59,9 @@ database: password: ORohmi9Eet=ohphi host: localhost synchronous_commit: false -. .fi -. .IP "" 0 -. .SH "COPYRIGHT" -This man page was written by Sunil Mohan Adapa <\fIsunil@medhas\.org\fR> for Debian GNU/Linux distribution\. -. +This man page was written by Sunil Mohan Adapa <\fI\%mailto:sunil@medhas\.org\fR> for Debian GNU/Linux distribution\. .SH "SEE ALSO" -synctl(1), hash_password(1), register_new_matrix_user(1) +synctl(1), hash_password(1), register_new_matrix_user(1), synapse_review_recent_signups(1) diff --git a/debian/synapse_port_db.ronn b/debian/synapse_port_db.ronn index fcb32ebd0d..e167af2ba4 100644 --- a/debian/synapse_port_db.ronn +++ b/debian/synapse_port_db.ronn @@ -47,7 +47,7 @@ following options. * `args`: DB API 2.0 compatible arguments to send to the `psycopg2` module. - * `dbname` - the database name + * `dbname` - the database name * `user` - user name used to authenticate @@ -58,7 +58,7 @@ following options. * `port` - connection port number (defaults to 5432 if not provided) - + * `synchronous_commit`: Optional. Default is True. If the value is `False`, enable @@ -76,7 +76,7 @@ Following example illustrates the configuration file format. password: ORohmi9Eet=ohphi host: localhost synchronous_commit: false - + ## COPYRIGHT This man page was written by Sunil Mohan Adapa <> for @@ -84,4 +84,4 @@ Debian GNU/Linux distribution. ## SEE ALSO -synctl(1), hash_password(1), register_new_matrix_user(1) +synctl(1), hash_password(1), register_new_matrix_user(1), synapse_review_recent_signups(1) diff --git a/debian/synapse_review_recent_signups.1 b/debian/synapse_review_recent_signups.1 new file mode 100644 index 0000000000..2976c085f9 --- /dev/null +++ b/debian/synapse_review_recent_signups.1 @@ -0,0 +1,26 @@ +.\" generated with Ronn-NG/v0.8.0 +.\" http://github.com/apjanke/ronn-ng/tree/0.8.0 +.TH "SYNAPSE_REVIEW_RECENT_SIGNUPS" "1" "July 2021" "" "" +.SH "NAME" +\fBsynapse_review_recent_signups\fR \- Print users that have recently registered on Synapse +.SH "SYNOPSIS" +\fBsynapse_review_recent_signups\fR \fB\-c\fR|\fB\-\-config\fR \fIfile\fR [\fB\-s\fR|\fB\-\-since\fR \fIperiod\fR] [\fB\-e\fR|\fB\-\-exclude\-emails\fR] [\fB\-u\fR|\fB\-\-only\-users\fR] +.SH "DESCRIPTION" +\fBsynapse_review_recent_signups\fR prints out recently registered users on a Synapse server, as well as some basic information about the user\. +.P +\fBsynapse_review_recent_signups\fR must be supplied with the config of the Synapse server, so that it can fetch the database config and connect to the database\. +.SH "OPTIONS" +.TP +\fB\-c\fR, \fB\-\-config\fR +The config file(s) used by the Synapse server\. +.TP +\fB\-s\fR, \fB\-\-since\fR +How far back to search for newly registered users\. Defaults to 7d, i\.e\. up to seven days in the past\. Valid units are \'s\', \'m\', \'h\', \'d\', \'w\', or \'y\'\. +.TP +\fB\-e\fR, \fB\-\-exclude\-emails\fR +Do not print out users that have validated emails associated with their account\. +.TP +\fB\-u\fR, \fB\-\-only\-users\fR +Only print out the user IDs of recently registered users, without any additional information +.SH "SEE ALSO" +synctl(1), synapse_port_db(1), register_new_matrix_user(1), hash_password(1) diff --git a/debian/synapse_review_recent_signups.ronn b/debian/synapse_review_recent_signups.ronn new file mode 100644 index 0000000000..77f2b040b9 --- /dev/null +++ b/debian/synapse_review_recent_signups.ronn @@ -0,0 +1,37 @@ +synapse_review_recent_signups(1) -- Print users that have recently registered on Synapse +======================================================================================== + +## SYNOPSIS + +`synapse_review_recent_signups` `-c`|`--config` [`-s`|`--since` ] [`-e`|`--exclude-emails`] [`-u`|`--only-users`] + +## DESCRIPTION + +**synapse_review_recent_signups** prints out recently registered users on a +Synapse server, as well as some basic information about the user. + +`synapse_review_recent_signups` must be supplied with the config of the Synapse +server, so that it can fetch the database config and connect to the database. + + +## OPTIONS + + * `-c`, `--config`: + The config file(s) used by the Synapse server. + + * `-s`, `--since`: + How far back to search for newly registered users. Defaults to 7d, i.e. up + to seven days in the past. Valid units are 's', 'm', 'h', 'd', 'w', or 'y'. + + * `-e`, `--exclude-emails`: + Do not print out users that have validated emails associated with their + account. + + * `-u`, `--only-users`: + Only print out the user IDs of recently registered users, without any + additional information + + +## SEE ALSO + +synctl(1), synapse_port_db(1), register_new_matrix_user(1), hash_password(1) diff --git a/debian/synctl.1 b/debian/synctl.1 index af58c8d224..2fdd770f09 100644 --- a/debian/synctl.1 +++ b/debian/synctl.1 @@ -1,63 +1,41 @@ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "SYNCTL" "1" "February 2017" "" "" -. +.\" generated with Ronn-NG/v0.8.0 +.\" http://github.com/apjanke/ronn-ng/tree/0.8.0 +.TH "SYNCTL" "1" "July 2021" "" "" .SH "NAME" \fBsynctl\fR \- Synapse server control interface -. .SH "SYNOPSIS" Start, stop or restart synapse server\. -. .P \fBsynctl\fR {start|stop|restart} [configfile] [\-w|\-\-worker=\fIWORKERCONFIG\fR] [\-a|\-\-all\-processes=\fIWORKERCONFIGDIR\fR] -. .SH "DESCRIPTION" \fBsynctl\fR can be used to start, stop or restart Synapse server\. The control operation can be done on all processes or a single worker process\. -. .SH "OPTIONS" -. .TP \fBaction\fR The value of action should be one of \fBstart\fR, \fBstop\fR or \fBrestart\fR\. -. .TP \fBconfigfile\fR Optional path of the configuration file to use\. Default value is \fBhomeserver\.yaml\fR\. The configuration file must exist for the operation to succeed\. -. .TP \fB\-w\fR, \fB\-\-worker\fR: -. -.IP -Perform start, stop or restart operations on a single worker\. Incompatible with \fB\-a\fR|\fB\-\-all\-processes\fR\. Value passed must be a valid worker\'s configuration file\. -. + .TP \fB\-a\fR, \fB\-\-all\-processes\fR: -. -.IP -Perform start, stop or restart operations on all the workers in the given directory and the main synapse process\. Incompatible with \fB\-w\fR|\fB\-\-worker\fR\. Value passed must be a directory containing valid work configuration files\. All files ending with \fB\.yaml\fR extension shall be considered as configuration files and all other files in the directory are ignored\. -. + .SH "CONFIGURATION FILE" Configuration file may be generated as follows: -. .IP "" 4 -. .nf - $ python \-m synapse\.app\.homeserver \-c config\.yaml \-\-generate\-config \-\-server\-name= -. .fi -. .IP "" 0 -. .SH "ENVIRONMENT" -. .TP \fBSYNAPSE_CACHE_FACTOR\fR -Synapse\'s architecture is quite RAM hungry currently \- a lot of recent room data and metadata is deliberately cached in RAM in order to speed up common requests\. This will be improved in future, but for now the easiest way to either reduce the RAM usage (at the risk of slowing things down) is to set the SYNAPSE_CACHE_FACTOR environment variable\. Roughly speaking, a SYNAPSE_CACHE_FACTOR of 1\.0 will max out at around 3\-4GB of resident memory \- this is what we currently run the matrix\.org on\. The default setting is currently 0\.1, which is probably around a ~700MB footprint\. You can dial it down further to 0\.02 if desired, which targets roughly ~512MB\. Conversely you can dial it up if you need performance for lots of users and have a box with a lot of RAM\. -. +Synapse\'s architecture is quite RAM hungry currently \- we deliberately cache a lot of recent room data and metadata in RAM in order to speed up common requests\. We\'ll improve this in the future, but for now the easiest way to either reduce the RAM usage (at the risk of slowing things down) is to set the almost\-undocumented \fBSYNAPSE_CACHE_FACTOR\fR environment variable\. The default is 0\.5, which can be decreased to reduce RAM usage in memory constrained enviroments, or increased if performance starts to degrade\. +.IP +However, degraded performance due to a low cache factor, common on machines with slow disks, often leads to explosions in memory use due backlogged requests\. In this case, reducing the cache factor will make things worse\. Instead, try increasing it drastically\. 2\.0 is a good starting value\. .SH "COPYRIGHT" -This man page was written by Sunil Mohan Adapa <\fIsunil@medhas\.org\fR> for Debian GNU/Linux distribution\. -. +This man page was written by Sunil Mohan Adapa <\fI\%mailto:sunil@medhas\.org\fR> for Debian GNU/Linux distribution\. .SH "SEE ALSO" -synapse_port_db(1), hash_password(1), register_new_matrix_user(1) +synapse_port_db(1), hash_password(1), register_new_matrix_user(1), synapse_review_recent_signups(1) diff --git a/debian/synctl.ronn b/debian/synctl.ronn index 10cbda988f..eca6a16815 100644 --- a/debian/synctl.ronn +++ b/debian/synctl.ronn @@ -68,4 +68,4 @@ Debian GNU/Linux distribution. ## SEE ALSO -synapse_port_db(1), hash_password(1), register_new_matrix_user(1) +synapse_port_db(1), hash_password(1), register_new_matrix_user(1), synapse_review_recent_signups(1) diff --git a/scripts/synapse_review_recent_signups b/scripts/synapse_review_recent_signups new file mode 100755 index 0000000000..a36d46e14c --- /dev/null +++ b/scripts/synapse_review_recent_signups @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse._scripts.review_recent_signups import main + +if __name__ == "__main__": + main() diff --git a/synapse/_scripts/review_recent_signups.py b/synapse/_scripts/review_recent_signups.py new file mode 100644 index 0000000000..01dc0c4237 --- /dev/null +++ b/synapse/_scripts/review_recent_signups.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import sys +import time +from datetime import datetime +from typing import List + +import attr + +from synapse.config._base import RootConfig, find_config_files, read_config_files +from synapse.config.database import DatabaseConfig +from synapse.storage.database import DatabasePool, LoggingTransaction, make_conn +from synapse.storage.engines import create_engine + + +class ReviewConfig(RootConfig): + "A config class that just pulls out the database config" + config_classes = [DatabaseConfig] + + +@attr.s(auto_attribs=True) +class UserInfo: + user_id: str + creation_ts: int + emails: List[str] = attr.Factory(list) + private_rooms: List[str] = attr.Factory(list) + public_rooms: List[str] = attr.Factory(list) + ips: List[str] = attr.Factory(list) + + +def get_recent_users(txn: LoggingTransaction, since_ms: int) -> List[UserInfo]: + """Fetches recently registered users and some info on them.""" + + sql = """ + SELECT name, creation_ts FROM users + WHERE + ? <= creation_ts + AND deactivated = 0 + """ + + txn.execute(sql, (since_ms / 1000,)) + + user_infos = [UserInfo(user_id, creation_ts) for user_id, creation_ts in txn] + + for user_info in user_infos: + user_info.emails = DatabasePool.simple_select_onecol_txn( + txn, + table="user_threepids", + keyvalues={"user_id": user_info.user_id, "medium": "email"}, + retcol="address", + ) + + sql = """ + SELECT room_id, canonical_alias, name, join_rules + FROM local_current_membership + INNER JOIN room_stats_state USING (room_id) + WHERE user_id = ? AND membership = 'join' + """ + + txn.execute(sql, (user_info.user_id,)) + for room_id, canonical_alias, name, join_rules in txn: + if join_rules == "public": + user_info.public_rooms.append(canonical_alias or name or room_id) + else: + user_info.private_rooms.append(canonical_alias or name or room_id) + + user_info.ips = DatabasePool.simple_select_onecol_txn( + txn, + table="user_ips", + keyvalues={"user_id": user_info.user_id}, + retcol="ip", + ) + + return user_infos + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-c", + "--config-path", + action="append", + metavar="CONFIG_FILE", + help="The config files for Synapse.", + required=True, + ) + parser.add_argument( + "-s", + "--since", + metavar="duration", + help="Specify how far back to review user registrations for, defaults to 7d (i.e. 7 days).", + default="7d", + ) + parser.add_argument( + "-e", + "--exclude-emails", + action="store_true", + help="Exclude users that have validated email addresses", + ) + parser.add_argument( + "-u", + "--only-users", + action="store_true", + help="Only print user IDs that match.", + ) + + config = ReviewConfig() + + config_args = parser.parse_args(sys.argv[1:]) + config_files = find_config_files(search_paths=config_args.config_path) + config_dict = read_config_files(config_files) + config.parse_config_dict( + config_dict, + ) + + since_ms = time.time() * 1000 - config.parse_duration(config_args.since) + exclude_users_with_email = config_args.exclude_emails + include_context = not config_args.only_users + + for database_config in config.database.databases: + if "main" in database_config.databases: + break + + engine = create_engine(database_config.config) + + with make_conn(database_config, engine, "review_recent_signups") as db_conn: + user_infos = get_recent_users(db_conn.cursor(), since_ms) + + for user_info in user_infos: + if exclude_users_with_email and user_info.emails: + continue + + if include_context: + print_public_rooms = "" + if user_info.public_rooms: + print_public_rooms = "(" + ", ".join(user_info.public_rooms[:3]) + + if len(user_info.public_rooms) > 3: + print_public_rooms += ", ..." + + print_public_rooms += ")" + + print("# Created:", datetime.fromtimestamp(user_info.creation_ts)) + print("# Email:", ", ".join(user_info.emails) or "None") + print("# IPs:", ", ".join(user_info.ips)) + print( + "# Number joined public rooms:", + len(user_info.public_rooms), + print_public_rooms, + ) + print("# Number joined private rooms:", len(user_info.private_rooms)) + print("#") + + print(user_info.user_id) + + if include_context: + print() + + +if __name__ == "__main__": + main() diff --git a/synapse/storage/database.py b/synapse/storage/database.py index d470cdacde..33c42cf95a 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -111,7 +111,7 @@ def make_conn( db_config: DatabaseConnectionConfig, engine: BaseDatabaseEngine, default_txn_name: str, -) -> Connection: +) -> "LoggingDatabaseConnection": """Make a new connection to the database and return it. Returns: From bcb0962a7250d6c1430ad42f5ed234ffea8f2468 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 6 Jul 2021 14:08:53 +0200 Subject: [PATCH 356/619] Fix deactivate a user if he does not have a profile (#10252) --- changelog.d/10252.bugfix | 1 + synapse/storage/databases/main/profile.py | 8 +-- tests/rest/admin/test_user.py | 86 ++++++++++++++++++----- 3 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 changelog.d/10252.bugfix diff --git a/changelog.d/10252.bugfix b/changelog.d/10252.bugfix new file mode 100644 index 0000000000..c8ddd14528 --- /dev/null +++ b/changelog.d/10252.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.26.0 where only users who have set profile information could be deactivated with erasure enabled. diff --git a/synapse/storage/databases/main/profile.py b/synapse/storage/databases/main/profile.py index 9b4e95e134..ba7075caa5 100644 --- a/synapse/storage/databases/main/profile.py +++ b/synapse/storage/databases/main/profile.py @@ -73,20 +73,20 @@ async def create_profile(self, user_localpart: str) -> None: async def set_profile_displayname( self, user_localpart: str, new_displayname: Optional[str] ) -> None: - await self.db_pool.simple_update_one( + await self.db_pool.simple_upsert( table="profiles", keyvalues={"user_id": user_localpart}, - updatevalues={"displayname": new_displayname}, + values={"displayname": new_displayname}, desc="set_profile_displayname", ) async def set_profile_avatar_url( self, user_localpart: str, new_avatar_url: Optional[str] ) -> None: - await self.db_pool.simple_update_one( + await self.db_pool.simple_upsert( table="profiles", keyvalues={"user_id": user_localpart}, - updatevalues={"avatar_url": new_avatar_url}, + values={"avatar_url": new_avatar_url}, desc="set_profile_avatar_url", ) diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index a34d051734..4fccce34fd 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -939,7 +939,7 @@ def test_no_auth(self): """ channel = self.make_request("POST", self.url, b"{}") - self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(401, channel.code, msg=channel.json_body) self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) def test_requester_is_not_admin(self): @@ -950,7 +950,7 @@ def test_requester_is_not_admin(self): channel = self.make_request("POST", url, access_token=self.other_user_token) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual("You are not a server admin", channel.json_body["error"]) channel = self.make_request( @@ -960,7 +960,7 @@ def test_requester_is_not_admin(self): content=b"{}", ) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual("You are not a server admin", channel.json_body["error"]) def test_user_does_not_exist(self): @@ -990,7 +990,7 @@ def test_erase_is_not_bool(self): access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.BAD_JSON, channel.json_body["errcode"]) def test_user_is_not_local(self): @@ -1006,7 +1006,7 @@ def test_user_is_not_local(self): def test_deactivate_user_erase_true(self): """ - Test deactivating an user and set `erase` to `true` + Test deactivating a user and set `erase` to `true` """ # Get user @@ -1016,24 +1016,22 @@ def test_deactivate_user_erase_true(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual(False, channel.json_body["deactivated"]) self.assertEqual("foo@bar.com", channel.json_body["threepids"][0]["address"]) self.assertEqual("mxc://servername/mediaid", channel.json_body["avatar_url"]) self.assertEqual("User1", channel.json_body["displayname"]) - # Deactivate user - body = json.dumps({"erase": True}) - + # Deactivate and erase user channel = self.make_request( "POST", self.url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content={"erase": True}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # Get user channel = self.make_request( @@ -1042,7 +1040,7 @@ def test_deactivate_user_erase_true(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual(True, channel.json_body["deactivated"]) self.assertEqual(0, len(channel.json_body["threepids"])) @@ -1053,7 +1051,7 @@ def test_deactivate_user_erase_true(self): def test_deactivate_user_erase_false(self): """ - Test deactivating an user and set `erase` to `false` + Test deactivating a user and set `erase` to `false` """ # Get user @@ -1063,7 +1061,7 @@ def test_deactivate_user_erase_false(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual(False, channel.json_body["deactivated"]) self.assertEqual("foo@bar.com", channel.json_body["threepids"][0]["address"]) @@ -1071,13 +1069,11 @@ def test_deactivate_user_erase_false(self): self.assertEqual("User1", channel.json_body["displayname"]) # Deactivate user - body = json.dumps({"erase": False}) - channel = self.make_request( "POST", self.url, access_token=self.admin_user_tok, - content=body.encode(encoding="utf_8"), + content={"erase": False}, ) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) @@ -1089,7 +1085,7 @@ def test_deactivate_user_erase_false(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@user:test", channel.json_body["name"]) self.assertEqual(True, channel.json_body["deactivated"]) self.assertEqual(0, len(channel.json_body["threepids"])) @@ -1098,6 +1094,60 @@ def test_deactivate_user_erase_false(self): self._is_erased("@user:test", False) + def test_deactivate_user_erase_true_no_profile(self): + """ + Test deactivating a user and set `erase` to `true` + if user has no profile information (stored in the database table `profiles`). + """ + + # Users normally have an entry in `profiles`, but occasionally they are created without one. + # To test deactivation for users without a profile, we delete the profile information for our user. + self.get_success( + self.store.db_pool.simple_delete_one( + table="profiles", keyvalues={"user_id": "user"} + ) + ) + + # Get user + channel = self.make_request( + "GET", + self.url_other_user, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(False, channel.json_body["deactivated"]) + self.assertEqual("foo@bar.com", channel.json_body["threepids"][0]["address"]) + self.assertIsNone(channel.json_body["avatar_url"]) + self.assertIsNone(channel.json_body["displayname"]) + + # Deactivate and erase user + channel = self.make_request( + "POST", + self.url, + access_token=self.admin_user_tok, + content={"erase": True}, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + + # Get user + channel = self.make_request( + "GET", + self.url_other_user, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(True, channel.json_body["deactivated"]) + self.assertEqual(0, len(channel.json_body["threepids"])) + self.assertIsNone(channel.json_body["avatar_url"]) + self.assertIsNone(channel.json_body["displayname"]) + + self._is_erased("@user:test", True) + def _is_erased(self, user_id: str, expect: bool) -> None: """Assert that the user is erased or not""" d = self.store.is_user_erased(user_id) From 37da9db082de686fd425058d29a605763b24cdfa Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 6 Jul 2021 13:54:23 +0100 Subject: [PATCH 357/619] 1.38.0rc1 --- CHANGES.md | 49 +++++++++++++++++++++++++++++++++++++++ changelog.d/10114.misc | 1 - changelog.d/10166.doc | 1 - changelog.d/10205.feature | 1 - changelog.d/10213.misc | 1 - changelog.d/10214.feature | 1 - changelog.d/10223.bugfix | 1 - changelog.d/10225.feature | 1 - changelog.d/10237.misc | 1 - changelog.d/10239.misc | 1 - changelog.d/10242.doc | 1 - changelog.d/10243.feature | 1 - changelog.d/10252.bugfix | 1 - changelog.d/10253.misc | 1 - changelog.d/10256.misc | 1 - changelog.d/10258.doc | 1 - changelog.d/10261.feature | 1 - changelog.d/10263.feature | 1 - changelog.d/10264.bugfix | 1 - changelog.d/10267.bugfix | 1 - changelog.d/10268.misc | 1 - changelog.d/10279.bugfix | 1 - changelog.d/10282.bugfix | 1 - changelog.d/10284.feature | 1 - changelog.d/10286.bugfix | 1 - changelog.d/10288.doc | 1 - changelog.d/10290.feature | 1 - changelog.d/10291.bugfix | 1 - changelog.d/10292.misc | 1 - changelog.d/10302.doc | 1 - changelog.d/10303.bugfix | 1 - changelog.d/10314.bugfix | 1 - changelog.d/9450.feature | 1 - synapse/__init__.py | 2 +- 34 files changed, 50 insertions(+), 33 deletions(-) delete mode 100644 changelog.d/10114.misc delete mode 100644 changelog.d/10166.doc delete mode 100644 changelog.d/10205.feature delete mode 100644 changelog.d/10213.misc delete mode 100644 changelog.d/10214.feature delete mode 100644 changelog.d/10223.bugfix delete mode 100644 changelog.d/10225.feature delete mode 100644 changelog.d/10237.misc delete mode 100644 changelog.d/10239.misc delete mode 100644 changelog.d/10242.doc delete mode 100644 changelog.d/10243.feature delete mode 100644 changelog.d/10252.bugfix delete mode 100644 changelog.d/10253.misc delete mode 100644 changelog.d/10256.misc delete mode 100644 changelog.d/10258.doc delete mode 100644 changelog.d/10261.feature delete mode 100644 changelog.d/10263.feature delete mode 100644 changelog.d/10264.bugfix delete mode 100644 changelog.d/10267.bugfix delete mode 100644 changelog.d/10268.misc delete mode 100644 changelog.d/10279.bugfix delete mode 100644 changelog.d/10282.bugfix delete mode 100644 changelog.d/10284.feature delete mode 100644 changelog.d/10286.bugfix delete mode 100644 changelog.d/10288.doc delete mode 100644 changelog.d/10290.feature delete mode 100644 changelog.d/10291.bugfix delete mode 100644 changelog.d/10292.misc delete mode 100644 changelog.d/10302.doc delete mode 100644 changelog.d/10303.bugfix delete mode 100644 changelog.d/10314.bugfix delete mode 100644 changelog.d/9450.feature diff --git a/CHANGES.md b/CHANGES.md index a2fc423096..273d53690a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,52 @@ +Synapse 1.38.0rc1 (2021-07-06) +============================== + +Features +-------- + +- Implement refresh tokens as specified by [MSC2918](https://github.com/matrix-org/matrix-doc/pull/2918). ([\#9450](https://github.com/matrix-org/synapse/issues/9450)) +- Add support for evicting cache entries based on last access time. ([\#10205](https://github.com/matrix-org/synapse/issues/10205)) +- Omit empty fields from the `/sync` response. Contributed by @deepbluev7. ([\#10214](https://github.com/matrix-org/synapse/issues/10214)) +- Improve validation on federation `send_{join,leave,knock}` endpoints. ([\#10225](https://github.com/matrix-org/synapse/issues/10225), [\#10243](https://github.com/matrix-org/synapse/issues/10243)) +- Add SSO `external_ids` to the Query User Account admin API. ([\#10261](https://github.com/matrix-org/synapse/issues/10261)) +- Mark events received over federation which fail a spam check as "soft-failed". ([\#10263](https://github.com/matrix-org/synapse/issues/10263)) +- Add metrics for new inbound federation staging area. ([\#10284](https://github.com/matrix-org/synapse/issues/10284)) +- Add script to print information about recently registered users. ([\#10290](https://github.com/matrix-org/synapse/issues/10290)) + + +Bugfixes +-------- + +- Fix a long-standing bug which meant that invite rejections and knocks were not sent out over federation in a timely manner. ([\#10223](https://github.com/matrix-org/synapse/issues/10223)) +- Fix a bug introduced in v1.26.0 where only users who have set profile information could be deactivated with erasure enabled. ([\#10252](https://github.com/matrix-org/synapse/issues/10252)) +- Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. ([\#10264](https://github.com/matrix-org/synapse/issues/10264), [\#10267](https://github.com/matrix-org/synapse/issues/10267), [\#10282](https://github.com/matrix-org/synapse/issues/10282), [\#10286](https://github.com/matrix-org/synapse/issues/10286), [\#10291](https://github.com/matrix-org/synapse/issues/10291), [\#10314](https://github.com/matrix-org/synapse/issues/10314)) +- Fix the prometheus `synapse_federation_server_pdu_process_time` metric. Broke in v1.37.1. ([\#10279](https://github.com/matrix-org/synapse/issues/10279)) +- Ensure that inbound events from federation that were being processed when Synapse was restarted get promptly processed on start up. ([\#10303](https://github.com/matrix-org/synapse/issues/10303)) + + +Improved Documentation +---------------------- + +- Move the upgrade notes to [docs/upgrade.md](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md) and convert them to markdown. ([\#10166](https://github.com/matrix-org/synapse/issues/10166)) +- Choose Welcome & Overview as the default page for synapse documentation website. ([\#10242](https://github.com/matrix-org/synapse/issues/10242)) +- Adjust the URL in the README.rst file to point to irc.libera.chat. ([\#10258](https://github.com/matrix-org/synapse/issues/10258)) +- Fix homeserver config option name in presence router documentation. ([\#10288](https://github.com/matrix-org/synapse/issues/10288)) +- Fix link pointing at the wrong section in the modules documentation page. ([\#10302](https://github.com/matrix-org/synapse/issues/10302)) + + +Internal Changes +---------------- + +- Drop Origin and Accept from the value of the Access-Control-Allow-Headers response header. ([\#10114](https://github.com/matrix-org/synapse/issues/10114)) +- Add type hints to the federation servlets. ([\#10213](https://github.com/matrix-org/synapse/issues/10213)) +- Improve the reliability of auto-joining remote rooms. ([\#10237](https://github.com/matrix-org/synapse/issues/10237)) +- Update the release script to use the semver terminology and determine the release branch based on the next version. ([\#10239](https://github.com/matrix-org/synapse/issues/10239)) +- Fix type hints for computing auth events. ([\#10253](https://github.com/matrix-org/synapse/issues/10253)) +- Improve the performance of the spaces summary endpoint by only recursing into spaces (and not rooms in general). ([\#10256](https://github.com/matrix-org/synapse/issues/10256)) +- Move event authentication methods from `Auth` to `EventAuthHandler`. ([\#10268](https://github.com/matrix-org/synapse/issues/10268)) +- Reenable a SyTest after it has been fixed. ([\#10292](https://github.com/matrix-org/synapse/issues/10292)) + + Synapse 1.38.0 (**UNRELEASED**) =============================== This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade.md#upgrading-to-v1380) for more information. diff --git a/changelog.d/10114.misc b/changelog.d/10114.misc deleted file mode 100644 index 808548f7c7..0000000000 --- a/changelog.d/10114.misc +++ /dev/null @@ -1 +0,0 @@ -Drop Origin and Accept from the value of the Access-Control-Allow-Headers response header. diff --git a/changelog.d/10166.doc b/changelog.d/10166.doc deleted file mode 100644 index 8d1710c132..0000000000 --- a/changelog.d/10166.doc +++ /dev/null @@ -1 +0,0 @@ -Move the upgrade notes to [docs/upgrade.md](https://github.com/matrix-org/synapse/blob/develop/docs/upgrade.md) and convert them to markdown. diff --git a/changelog.d/10205.feature b/changelog.d/10205.feature deleted file mode 100644 index db3fd22587..0000000000 --- a/changelog.d/10205.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for evicting cache entries based on last access time. diff --git a/changelog.d/10213.misc b/changelog.d/10213.misc deleted file mode 100644 index 9adb0fbd02..0000000000 --- a/changelog.d/10213.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to the federation servlets. diff --git a/changelog.d/10214.feature b/changelog.d/10214.feature deleted file mode 100644 index a3818c9d25..0000000000 --- a/changelog.d/10214.feature +++ /dev/null @@ -1 +0,0 @@ -Omit empty fields from the `/sync` response. Contributed by @deepbluev7. \ No newline at end of file diff --git a/changelog.d/10223.bugfix b/changelog.d/10223.bugfix deleted file mode 100644 index 4e42f6b608..0000000000 --- a/changelog.d/10223.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug which meant that invite rejections and knocks were not sent out over federation in a timely manner. diff --git a/changelog.d/10225.feature b/changelog.d/10225.feature deleted file mode 100644 index d16f66ffe9..0000000000 --- a/changelog.d/10225.feature +++ /dev/null @@ -1 +0,0 @@ -Improve validation on federation `send_{join,leave,knock}` endpoints. diff --git a/changelog.d/10237.misc b/changelog.d/10237.misc deleted file mode 100644 index d76c119a41..0000000000 --- a/changelog.d/10237.misc +++ /dev/null @@ -1 +0,0 @@ -Improve the reliability of auto-joining remote rooms. diff --git a/changelog.d/10239.misc b/changelog.d/10239.misc deleted file mode 100644 index d05f1c4411..0000000000 --- a/changelog.d/10239.misc +++ /dev/null @@ -1 +0,0 @@ -Update the release script to use the semver terminology and determine the release branch based on the next version. diff --git a/changelog.d/10242.doc b/changelog.d/10242.doc deleted file mode 100644 index 2241b28547..0000000000 --- a/changelog.d/10242.doc +++ /dev/null @@ -1 +0,0 @@ -Choose Welcome & Overview as the default page for synapse documentation website. diff --git a/changelog.d/10243.feature b/changelog.d/10243.feature deleted file mode 100644 index d16f66ffe9..0000000000 --- a/changelog.d/10243.feature +++ /dev/null @@ -1 +0,0 @@ -Improve validation on federation `send_{join,leave,knock}` endpoints. diff --git a/changelog.d/10252.bugfix b/changelog.d/10252.bugfix deleted file mode 100644 index c8ddd14528..0000000000 --- a/changelog.d/10252.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.26.0 where only users who have set profile information could be deactivated with erasure enabled. diff --git a/changelog.d/10253.misc b/changelog.d/10253.misc deleted file mode 100644 index 44d9217245..0000000000 --- a/changelog.d/10253.misc +++ /dev/null @@ -1 +0,0 @@ -Fix type hints for computing auth events. diff --git a/changelog.d/10256.misc b/changelog.d/10256.misc deleted file mode 100644 index adef12fcb9..0000000000 --- a/changelog.d/10256.misc +++ /dev/null @@ -1 +0,0 @@ -Improve the performance of the spaces summary endpoint by only recursing into spaces (and not rooms in general). diff --git a/changelog.d/10258.doc b/changelog.d/10258.doc deleted file mode 100644 index 1549786c0c..0000000000 --- a/changelog.d/10258.doc +++ /dev/null @@ -1 +0,0 @@ -Adjust the URL in the README.rst file to point to irc.libera.chat. diff --git a/changelog.d/10261.feature b/changelog.d/10261.feature deleted file mode 100644 index cd55cecbd5..0000000000 --- a/changelog.d/10261.feature +++ /dev/null @@ -1 +0,0 @@ -Add SSO `external_ids` to the Query User Account admin API. diff --git a/changelog.d/10263.feature b/changelog.d/10263.feature deleted file mode 100644 index 7b1d2fe60f..0000000000 --- a/changelog.d/10263.feature +++ /dev/null @@ -1 +0,0 @@ -Mark events received over federation which fail a spam check as "soft-failed". diff --git a/changelog.d/10264.bugfix b/changelog.d/10264.bugfix deleted file mode 100644 index 7ebda7cdc2..0000000000 --- a/changelog.d/10264.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/changelog.d/10267.bugfix b/changelog.d/10267.bugfix deleted file mode 100644 index 7ebda7cdc2..0000000000 --- a/changelog.d/10267.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/changelog.d/10268.misc b/changelog.d/10268.misc deleted file mode 100644 index 9e3f60c72f..0000000000 --- a/changelog.d/10268.misc +++ /dev/null @@ -1 +0,0 @@ -Move event authentication methods from `Auth` to `EventAuthHandler`. diff --git a/changelog.d/10279.bugfix b/changelog.d/10279.bugfix deleted file mode 100644 index ac8b64ead9..0000000000 --- a/changelog.d/10279.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the prometheus `synapse_federation_server_pdu_process_time` metric. Broke in v1.37.1. diff --git a/changelog.d/10282.bugfix b/changelog.d/10282.bugfix deleted file mode 100644 index 7ebda7cdc2..0000000000 --- a/changelog.d/10282.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/changelog.d/10284.feature b/changelog.d/10284.feature deleted file mode 100644 index 379155e8cf..0000000000 --- a/changelog.d/10284.feature +++ /dev/null @@ -1 +0,0 @@ -Add metrics for new inbound federation staging area. diff --git a/changelog.d/10286.bugfix b/changelog.d/10286.bugfix deleted file mode 100644 index 7ebda7cdc2..0000000000 --- a/changelog.d/10286.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/changelog.d/10288.doc b/changelog.d/10288.doc deleted file mode 100644 index 0739687b92..0000000000 --- a/changelog.d/10288.doc +++ /dev/null @@ -1 +0,0 @@ -Fix homeserver config option name in presence router documentation. diff --git a/changelog.d/10290.feature b/changelog.d/10290.feature deleted file mode 100644 index 4e4c2e24ef..0000000000 --- a/changelog.d/10290.feature +++ /dev/null @@ -1 +0,0 @@ -Add script to print information about recently registered users. diff --git a/changelog.d/10291.bugfix b/changelog.d/10291.bugfix deleted file mode 100644 index 7ebda7cdc2..0000000000 --- a/changelog.d/10291.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/changelog.d/10292.misc b/changelog.d/10292.misc deleted file mode 100644 index 9e87d8682c..0000000000 --- a/changelog.d/10292.misc +++ /dev/null @@ -1 +0,0 @@ -Reenable a SyTest after it has been fixed. diff --git a/changelog.d/10302.doc b/changelog.d/10302.doc deleted file mode 100644 index 7386817de7..0000000000 --- a/changelog.d/10302.doc +++ /dev/null @@ -1 +0,0 @@ -Fix link pointing at the wrong section in the modules documentation page. diff --git a/changelog.d/10303.bugfix b/changelog.d/10303.bugfix deleted file mode 100644 index c0577c9f73..0000000000 --- a/changelog.d/10303.bugfix +++ /dev/null @@ -1 +0,0 @@ -Ensure that inbound events from federation that were being processed when Synapse was restarted get promptly processed on start up. diff --git a/changelog.d/10314.bugfix b/changelog.d/10314.bugfix deleted file mode 100644 index 7ebda7cdc2..0000000000 --- a/changelog.d/10314.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/changelog.d/9450.feature b/changelog.d/9450.feature deleted file mode 100644 index 455936a41d..0000000000 --- a/changelog.d/9450.feature +++ /dev/null @@ -1 +0,0 @@ -Implement refresh tokens as specified by [MSC2918](https://github.com/matrix-org/matrix-doc/pull/2918). diff --git a/synapse/__init__.py b/synapse/__init__.py index 1bd03462ac..aa9a3269c0 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.37.1" +__version__ = "1.38.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 994722410a9810cd2e736bb96ae3d04c708c46e7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 6 Jul 2021 14:08:12 +0100 Subject: [PATCH 358/619] Small changelog tweaks --- CHANGES.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 273d53690a..c4551fdd69 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ Synapse 1.38.0rc1 (2021-07-06) ============================== +This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade.md#upgrading-to-v1380) for more information. + Features -------- @@ -37,20 +39,16 @@ Improved Documentation Internal Changes ---------------- -- Drop Origin and Accept from the value of the Access-Control-Allow-Headers response header. ([\#10114](https://github.com/matrix-org/synapse/issues/10114)) +- Drop `Origin` and `Accept` from the value of the `Access-Control-Allow-Headers` response header. ([\#10114](https://github.com/matrix-org/synapse/issues/10114)) - Add type hints to the federation servlets. ([\#10213](https://github.com/matrix-org/synapse/issues/10213)) - Improve the reliability of auto-joining remote rooms. ([\#10237](https://github.com/matrix-org/synapse/issues/10237)) - Update the release script to use the semver terminology and determine the release branch based on the next version. ([\#10239](https://github.com/matrix-org/synapse/issues/10239)) - Fix type hints for computing auth events. ([\#10253](https://github.com/matrix-org/synapse/issues/10253)) - Improve the performance of the spaces summary endpoint by only recursing into spaces (and not rooms in general). ([\#10256](https://github.com/matrix-org/synapse/issues/10256)) - Move event authentication methods from `Auth` to `EventAuthHandler`. ([\#10268](https://github.com/matrix-org/synapse/issues/10268)) -- Reenable a SyTest after it has been fixed. ([\#10292](https://github.com/matrix-org/synapse/issues/10292)) +- Re-enable a SyTest after it has been fixed. ([\#10292](https://github.com/matrix-org/synapse/issues/10292)) -Synapse 1.38.0 (**UNRELEASED**) -=============================== -This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade.md#upgrading-to-v1380) for more information. - Synapse 1.37.1 (2021-06-30) =========================== From 47e28b4031c7c5e2c87824c2b4873492b996d02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dagfinn=20Ilmari=20Manns=C3=A5ker?= Date: Tue, 6 Jul 2021 14:31:13 +0100 Subject: [PATCH 359/619] Ignore EDUs for rooms we're not in (#10317) --- changelog.d/10317.bugfix | 1 + synapse/handlers/receipts.py | 15 ++++++++++++++ synapse/handlers/typing.py | 14 +++++++++++++ tests/handlers/test_typing.py | 37 +++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 changelog.d/10317.bugfix diff --git a/changelog.d/10317.bugfix b/changelog.d/10317.bugfix new file mode 100644 index 0000000000..826c269eff --- /dev/null +++ b/changelog.d/10317.bugfix @@ -0,0 +1 @@ +Fix purging rooms that other homeservers are still sending events for. Contributed by @ilmari. diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index f782d9db32..0059ad0f56 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -30,6 +30,8 @@ def __init__(self, hs: "HomeServer"): self.server_name = hs.config.server_name self.store = hs.get_datastore() + self.event_auth_handler = hs.get_event_auth_handler() + self.hs = hs # We only need to poke the federation sender explicitly if its on the @@ -59,6 +61,19 @@ async def _received_remote_receipt(self, origin: str, content: JsonDict) -> None """Called when we receive an EDU of type m.receipt from a remote HS.""" receipts = [] for room_id, room_values in content.items(): + # If we're not in the room just ditch the event entirely. This is + # probably an old server that has come back and thinks we're still in + # the room (or we've been rejoined to the room by a state reset). + is_in_room = await self.event_auth_handler.check_host_in_room( + room_id, self.server_name + ) + if not is_in_room: + logger.info( + "Ignoring receipt from %s as we're not in the room", + origin, + ) + continue + for receipt_type, users in room_values.items(): for user_id, user_values in users.items(): if get_domain_from_id(user_id) != origin: diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index e22393adc4..c0a8364755 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -208,6 +208,7 @@ def __init__(self, hs: "HomeServer"): self.auth = hs.get_auth() self.notifier = hs.get_notifier() + self.event_auth_handler = hs.get_event_auth_handler() self.hs = hs @@ -326,6 +327,19 @@ async def _recv_edu(self, origin: str, content: JsonDict) -> None: room_id = content["room_id"] user_id = content["user_id"] + # If we're not in the room just ditch the event entirely. This is + # probably an old server that has come back and thinks we're still in + # the room (or we've been rejoined to the room by a state reset). + is_in_room = await self.event_auth_handler.check_host_in_room( + room_id, self.server_name + ) + if not is_in_room: + logger.info( + "Ignoring typing update from %s as we're not in the room", + origin, + ) + return + member = RoomMember(user_id=user_id, room_id=room_id) # Check that the string is a valid user id diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index f58afbc244..fa3cff598e 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -38,6 +38,9 @@ # Test room id ROOM_ID = "a-room" +# Room we're not in +OTHER_ROOM_ID = "another-room" + def _expect_edu_transaction(edu_type, content, origin="test"): return { @@ -115,6 +118,11 @@ async def check_user_in_room(room_id, user_id): hs.get_auth().check_user_in_room = check_user_in_room + async def check_host_in_room(room_id, server_name): + return room_id == ROOM_ID + + hs.get_event_auth_handler().check_host_in_room = check_host_in_room + def get_joined_hosts_for_room(room_id): return {member.domain for member in self.room_members} @@ -244,6 +252,35 @@ def test_started_typing_remote_recv(self): ], ) + def test_started_typing_remote_recv_not_in_room(self): + self.room_members = [U_APPLE, U_ONION] + + self.assertEquals(self.event_source.get_current_key(), 0) + + channel = self.make_request( + "PUT", + "/_matrix/federation/v1/send/1000000", + _make_edu_transaction_json( + "m.typing", + content={ + "room_id": OTHER_ROOM_ID, + "user_id": U_ONION.to_string(), + "typing": True, + }, + ), + federation_auth_origin=b"farm", + ) + self.assertEqual(channel.code, 200) + + self.on_new_event.assert_not_called() + + self.assertEquals(self.event_source.get_current_key(), 0) + events = self.get_success( + self.event_source.get_new_events(room_ids=[OTHER_ROOM_ID], from_key=0) + ) + self.assertEquals(events[0], []) + self.assertEquals(events[1], 0) + @override_config({"send_federation": True}) def test_stopped_typing(self): self.room_members = [U_APPLE, U_BANANA, U_ONION] From 7c823789921ac34f1fee670be7ef7f6c8266832b Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 7 Jul 2021 10:43:54 +0100 Subject: [PATCH 360/619] build the docs for master (#10323) --- .github/workflows/docs.yaml | 59 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 23b8d7f909..22a2d4f6bf 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -7,6 +7,8 @@ on: - develop # For documentation specific to a release - 'release-v*' + # stable docs + - master workflow_dispatch: @@ -25,40 +27,35 @@ jobs: - name: Build the documentation run: mdbook build - # Deploy to the latest documentation directories - - name: Deploy latest documentation - uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - keep_files: true - publish_dir: ./book - destination_dir: ./develop - - - name: Get the current Synapse version + # Figure out the target directory. + # + # The target directory depends on the name of the branch + # + - name: Get the target directory name id: vars - # The $GITHUB_REF value for a branch looks like `refs/heads/release-v1.2`. We do some - # shell magic to remove the "refs/heads/release-v" bit from this, to end up with "1.2", - # our major/minor version number, and set this to a var called `branch-version`. - # - # We then use some python to get Synapse's full version string, which may look - # like "1.2.3rc4". We set this to a var called `synapse-version`. We use this - # to determine if this release is still an RC, and if so block deployment. run: | - echo ::set-output name=branch-version::${GITHUB_REF#refs/heads/release-v} - echo ::set-output name=synapse-version::`python3 -c 'import synapse; print(synapse.__version__)'` - - # Deploy to the version-specific directory - - name: Deploy release-specific documentation - # We only carry out this step if we're running on a release branch, - # and the current Synapse version does not have "rc" in the name. - # - # The result is that only full releases are deployed, but can be - # updated if the release branch gets retroactive fixes. - if: ${{ startsWith( github.ref, 'refs/heads/release-v' ) && !contains( steps.vars.outputs.synapse-version, 'rc') }} - uses: peaceiris/actions-gh-pages@v3 + # first strip the 'refs/heads/' prefix with some shell foo + branch="${GITHUB_REF#refs/heads/}" + + case $branch in + release-*) + # strip 'release-' from the name for release branches. + branch="${branch#release-}" + ;; + master) + # deploy to "latest" for the master branch. + branch="latest" + ;; + esac + + # finally, set the 'branch-version' var. + echo "::set-output name=branch-version::$branch" + + # Deploy to the target directory. + - name: Deploy to gh pages + uses: peaceiris/actions-gh-pages@068dc23d9710f1ba62e86896f84735d869951305 # v3.8.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} keep_files: true publish_dir: ./book - # The resulting documentation will end up in a directory named `vX.Y`. - destination_dir: ./v${{ steps.vars.outputs.branch-version }} + destination_dir: ./${{ steps.vars.outputs.branch-version }} From 9ad84558951dd970dc2a362c923552141a42a5f3 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 7 Jul 2021 11:56:17 +0200 Subject: [PATCH 361/619] ANALYZE new stream ordering column (#10326) Fixes #10325 --- changelog.d/10326.bugfix | 1 + synapse/storage/databases/main/events_bg_updates.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 changelog.d/10326.bugfix diff --git a/changelog.d/10326.bugfix b/changelog.d/10326.bugfix new file mode 100644 index 0000000000..7ebda7cdc2 --- /dev/null +++ b/changelog.d/10326.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index 1c95c66648..29f33bac55 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -1146,6 +1146,16 @@ def process(txn: Cursor) -> None: logger.info("completing stream_ordering migration: %s", sql) txn.execute(sql) + # ANALYZE the new column to build stats on it, to encourage PostgreSQL to use the + # indexes on it. + # We need to pass execute a dummy function to handle the txn's result otherwise + # it tries to call fetchall() on it and fails because there's no result to fetch. + await self.db_pool.execute( + "background_analyze_new_stream_ordering_column", + lambda txn: None, + "ANALYZE events(stream_ordering2)", + ) + await self.db_pool.runInteraction( "_background_replace_stream_ordering_column", process ) From 24796f80ba3aecf449bc9921b259e3d98a049920 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 7 Jul 2021 11:21:58 +0100 Subject: [PATCH 362/619] Merge latest fix into the changelog --- CHANGES.md | 2 +- changelog.d/10326.bugfix | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 changelog.d/10326.bugfix diff --git a/CHANGES.md b/CHANGES.md index c4551fdd69..1d6dffec6e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,7 +21,7 @@ Bugfixes - Fix a long-standing bug which meant that invite rejections and knocks were not sent out over federation in a timely manner. ([\#10223](https://github.com/matrix-org/synapse/issues/10223)) - Fix a bug introduced in v1.26.0 where only users who have set profile information could be deactivated with erasure enabled. ([\#10252](https://github.com/matrix-org/synapse/issues/10252)) -- Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. ([\#10264](https://github.com/matrix-org/synapse/issues/10264), [\#10267](https://github.com/matrix-org/synapse/issues/10267), [\#10282](https://github.com/matrix-org/synapse/issues/10282), [\#10286](https://github.com/matrix-org/synapse/issues/10286), [\#10291](https://github.com/matrix-org/synapse/issues/10291), [\#10314](https://github.com/matrix-org/synapse/issues/10314)) +- Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. ([\#10264](https://github.com/matrix-org/synapse/issues/10264), [\#10267](https://github.com/matrix-org/synapse/issues/10267), [\#10282](https://github.com/matrix-org/synapse/issues/10282), [\#10286](https://github.com/matrix-org/synapse/issues/10286), [\#10291](https://github.com/matrix-org/synapse/issues/10291), [\#10314](https://github.com/matrix-org/synapse/issues/10314), [\#10326](https://github.com/matrix-org/synapse/issues/10326)) - Fix the prometheus `synapse_federation_server_pdu_process_time` metric. Broke in v1.37.1. ([\#10279](https://github.com/matrix-org/synapse/issues/10279)) - Ensure that inbound events from federation that were being processed when Synapse was restarted get promptly processed on start up. ([\#10303](https://github.com/matrix-org/synapse/issues/10303)) diff --git a/changelog.d/10326.bugfix b/changelog.d/10326.bugfix deleted file mode 100644 index 7ebda7cdc2..0000000000 --- a/changelog.d/10326.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would return errors after 231 events were handled by the server. From 7cb51680875170e22c72fc8b0d2fb3e3e09f4c67 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 7 Jul 2021 11:32:20 +0100 Subject: [PATCH 363/619] Fix broken link --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1d6dffec6e..2b0179edc3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Synapse 1.38.0rc1 (2021-07-06) ============================== -This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade.md#upgrading-to-v1380) for more information. +This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade#upgrading-to-v1380) for more information. Features -------- From 2d044667cff1b6aeb1d791c6dede95cf7f5a8f2b Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:18:36 +0200 Subject: [PATCH 364/619] Simplify structure of room admin API docs (#10313) --- changelog.d/10313.doc | 1 + docs/admin_api/rooms.md | 69 ++++++++++++++--------------------------- 2 files changed, 25 insertions(+), 45 deletions(-) create mode 100644 changelog.d/10313.doc diff --git a/changelog.d/10313.doc b/changelog.d/10313.doc new file mode 100644 index 0000000000..44086e3d9d --- /dev/null +++ b/changelog.d/10313.doc @@ -0,0 +1 @@ +Simplify structure of room admin API. \ No newline at end of file diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index bb7828a525..48777dd231 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -1,13 +1,9 @@ # Contents - [List Room API](#list-room-api) - * [Parameters](#parameters) - * [Usage](#usage) - [Room Details API](#room-details-api) - [Room Members API](#room-members-api) - [Room State API](#room-state-api) - [Delete Room API](#delete-room-api) - * [Parameters](#parameters-1) - * [Response](#response) * [Undoing room shutdowns](#undoing-room-shutdowns) - [Make Room Admin API](#make-room-admin-api) - [Forward Extremities Admin API](#forward-extremities-admin-api) @@ -19,7 +15,7 @@ The List Room admin API allows server admins to get a list of rooms on their server. There are various parameters available that allow for filtering and sorting the returned list. This API supports pagination. -## Parameters +**Parameters** The following query parameters are available: @@ -46,6 +42,8 @@ The following query parameters are available: * `search_term` - Filter rooms by their room name. Search term can be contained in any part of the room name. Defaults to no filtering. +**Response** + The following fields are possible in the JSON response body: * `rooms` - An array of objects, each containing information about a room. @@ -79,17 +77,15 @@ The following fields are possible in the JSON response body: Use `prev_batch` for the `from` value in the next request to get the "previous page" of results. -## Usage +The API is: A standard request with no filtering: ``` GET /_synapse/admin/v1/rooms - -{} ``` -Response: +A response body like the following is returned: ```jsonc { @@ -137,11 +133,9 @@ Filtering by room name: ``` GET /_synapse/admin/v1/rooms?search_term=TWIM - -{} ``` -Response: +A response body like the following is returned: ```json { @@ -172,11 +166,9 @@ Paginating through a list of rooms: ``` GET /_synapse/admin/v1/rooms?order_by=size - -{} ``` -Response: +A response body like the following is returned: ```jsonc { @@ -228,11 +220,9 @@ parameter to the value of `next_token`. ``` GET /_synapse/admin/v1/rooms?order_by=size&from=100 - -{} ``` -Response: +A response body like the following is returned: ```jsonc { @@ -304,17 +294,13 @@ The following fields are possible in the JSON response body: * `history_visibility` - Who can see the room history. One of: ["invited", "joined", "shared", "world_readable"]. * `state_events` - Total number of state_events of a room. Complexity of the room. -## Usage - -A standard request: +The API is: ``` GET /_synapse/admin/v1/rooms/ - -{} ``` -Response: +A response body like the following is returned: ```json { @@ -347,17 +333,13 @@ The response includes the following fields: * `members` - A list of all the members that are present in the room, represented by their ids. * `total` - Total number of members in the room. -## Usage - -A standard request: +The API is: ``` GET /_synapse/admin/v1/rooms//members - -{} ``` -Response: +A response body like the following is returned: ```json { @@ -378,17 +360,13 @@ The response includes the following fields: * `state` - The current state of the room at the time of request. -## Usage - -A standard request: +The API is: ``` GET /_synapse/admin/v1/rooms//state - -{} ``` -Response: +A response body like the following is returned: ```json { @@ -432,6 +410,7 @@ DELETE /_synapse/admin/v1/rooms/ ``` with a body of: + ```json { "new_room_user_id": "@someuser:example.com", @@ -461,7 +440,7 @@ A response body like the following is returned: } ``` -## Parameters +**Parameters** The following parameters should be set in the URL: @@ -491,7 +470,7 @@ The following JSON body parameters are available: The JSON body must not be empty. The body must be at least `{}`. -## Response +**Response** The following fields are returned in the JSON response body: @@ -548,10 +527,10 @@ By default the server admin (the caller) is granted power, but another user can optionally be specified, e.g.: ``` - POST /_synapse/admin/v1/rooms//make_room_admin - { - "user_id": "@foo:example.com" - } +POST /_synapse/admin/v1/rooms//make_room_admin +{ + "user_id": "@foo:example.com" +} ``` # Forward Extremities Admin API @@ -565,7 +544,7 @@ extremities accumulate in a room, performance can become degraded. For details, To check the status of forward extremities for a room: ``` - GET /_synapse/admin/v1/rooms//forward_extremities +GET /_synapse/admin/v1/rooms//forward_extremities ``` A response as follows will be returned: @@ -581,7 +560,7 @@ A response as follows will be returned: "received_ts": 1611263016761 } ] -} +} ``` ## Deleting forward extremities @@ -594,7 +573,7 @@ If a room has lots of forward extremities, the extra can be deleted as follows: ``` - DELETE /_synapse/admin/v1/rooms//forward_extremities +DELETE /_synapse/admin/v1/rooms//forward_extremities ``` A response as follows will be returned, indicating the amount of forward extremities From 56fd5fa8e1cacdba89ff1c9a9c18d0d6f0cb0f74 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Wed, 7 Jul 2021 13:35:45 +0200 Subject: [PATCH 365/619] Update links to documentation in sample config (#10287) Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10287.doc | 1 + docs/sample_config.yaml | 44 ++++++++++++----------- docs/sample_log_config.yaml | 2 +- synapse/config/consent.py | 2 +- synapse/config/database.py | 3 +- synapse/config/jwt.py | 2 +- synapse/config/logger.py | 2 +- synapse/config/modules.py | 2 +- synapse/config/oidc.py | 4 +-- synapse/config/password_auth_providers.py | 2 +- synapse/config/repository.py | 2 +- synapse/config/server.py | 23 ++++++------ synapse/config/spam_checker.py | 2 +- synapse/config/stats.py | 2 +- synapse/config/tracer.py | 2 +- synapse/config/user_directory.py | 2 +- 16 files changed, 51 insertions(+), 46 deletions(-) create mode 100644 changelog.d/10287.doc diff --git a/changelog.d/10287.doc b/changelog.d/10287.doc new file mode 100644 index 0000000000..d62afc1e15 --- /dev/null +++ b/changelog.d/10287.doc @@ -0,0 +1 @@ +Update links to documentation in sample config. Contributed by @dklimpel. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index c04aca1f42..71463168e3 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -36,7 +36,7 @@ # Server admins can expand Synapse's functionality with external modules. # -# See https://matrix-org.github.io/synapse/develop/modules.html for more +# See https://matrix-org.github.io/synapse/latest/modules.html for more # documentation on how to configure or create custom modules for Synapse. # modules: @@ -58,7 +58,7 @@ modules: # In most cases you should avoid using a matrix specific subdomain such as # matrix.example.com or synapse.example.com as the server_name for the same # reasons you wouldn't use user@email.example.com as your email address. -# See https://github.com/matrix-org/synapse/blob/master/docs/delegate.md +# See https://matrix-org.github.io/synapse/latest/delegate.html # for information on how to host Synapse on a subdomain while preserving # a clean server_name. # @@ -253,9 +253,9 @@ presence: # 'all local interfaces'. # # type: the type of listener. Normally 'http', but other valid options are: -# 'manhole' (see docs/manhole.md), -# 'metrics' (see docs/metrics-howto.md), -# 'replication' (see docs/workers.md). +# 'manhole' (see https://matrix-org.github.io/synapse/latest/manhole.html), +# 'metrics' (see https://matrix-org.github.io/synapse/latest/metrics-howto.html), +# 'replication' (see https://matrix-org.github.io/synapse/latest/workers.html). # # tls: set to true to enable TLS for this listener. Will use the TLS # key/cert specified in tls_private_key_path / tls_certificate_path. @@ -280,8 +280,8 @@ presence: # 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 -# docs/consent_tracking.md. +# consent: user consent forms (/_matrix/consent). +# See https://matrix-org.github.io/synapse/latest/consent_tracking.html. # # federation: the server-server API (/_matrix/federation). Also implies # 'media', 'keys', 'openid' @@ -290,12 +290,13 @@ presence: # # media: the media API (/_matrix/media). # -# metrics: the metrics interface. See docs/metrics-howto.md. +# metrics: the metrics interface. +# See https://matrix-org.github.io/synapse/latest/metrics-howto.html. # # openid: OpenID authentication. # -# replication: the HTTP replication API (/_synapse/replication). See -# docs/workers.md. +# replication: the HTTP replication API (/_synapse/replication). +# See https://matrix-org.github.io/synapse/latest/workers.html. # # static: static resources under synapse/static (/_matrix/static). (Mostly # useful for 'fallback authentication'.) @@ -319,7 +320,7 @@ listeners: # that unwraps TLS. # # If you plan to use a reverse proxy, please see - # https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.md. + # https://matrix-org.github.io/synapse/latest/reverse_proxy.html. # - port: 8008 tls: false @@ -747,7 +748,8 @@ caches: # cp_min: 5 # cp_max: 10 # -# For more information on using Synapse with Postgres, see `docs/postgres.md`. +# For more information on using Synapse with Postgres, +# see https://matrix-org.github.io/synapse/latest/postgres.html. # database: name: sqlite3 @@ -900,7 +902,7 @@ media_store_path: "DATADIR/media_store" # # If you are using a reverse proxy you may also need to set this value in # your reverse proxy's config. Notably Nginx has a small max body size by default. -# See https://matrix-org.github.io/synapse/develop/reverse_proxy.html. +# See https://matrix-org.github.io/synapse/latest/reverse_proxy.html. # #max_upload_size: 50M @@ -1840,7 +1842,7 @@ saml2_config: # # module: The class name of a custom mapping module. Default is # 'synapse.handlers.oidc.JinjaOidcMappingProvider'. -# See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers +# See https://matrix-org.github.io/synapse/latest/sso_mapping_providers.html#openid-mapping-providers # for information on implementing a custom mapping provider. # # config: Configuration for the mapping provider module. This section will @@ -1891,7 +1893,7 @@ saml2_config: # - attribute: groups # value: "admin" # -# See https://github.com/matrix-org/synapse/blob/master/docs/openid.md +# See https://matrix-org.github.io/synapse/latest/openid.html # for information on how to configure these options. # # For backwards compatibility, it is also possible to configure a single OIDC @@ -2169,7 +2171,7 @@ sso: # Note that this is a non-standard login type and client support is # expected to be non-existent. # -# See https://github.com/matrix-org/synapse/blob/master/docs/jwt.md. +# See https://matrix-org.github.io/synapse/latest/jwt.html. # #jwt_config: # Uncomment the following to enable authorization using JSON web @@ -2469,7 +2471,7 @@ email: # ex. LDAP, external tokens, etc. # # For more information and known implementations, please see -# https://github.com/matrix-org/synapse/blob/master/docs/password_auth_providers.md +# https://matrix-org.github.io/synapse/latest/password_auth_providers.html # # Note: instances wishing to use SAML or CAS authentication should # instead use the `saml2_config` or `cas_config` options, @@ -2571,7 +2573,7 @@ user_directory: # # If you set it true, you'll have to rebuild the user_directory search # indexes, see: - # https://github.com/matrix-org/synapse/blob/master/docs/user_directory.md + # https://matrix-org.github.io/synapse/latest/user_directory.html # # Uncomment to return search results containing all known users, even if that # user does not share a room with the requester. @@ -2591,7 +2593,7 @@ user_directory: # User Consent configuration # # for detailed instructions, see -# https://github.com/matrix-org/synapse/blob/master/docs/consent_tracking.md +# https://matrix-org.github.io/synapse/latest/consent_tracking.html # # Parts of this section are required if enabling the 'consent' resource under # 'listeners', in particular 'template_dir' and 'version'. @@ -2641,7 +2643,7 @@ user_directory: # Settings for local room and user statistics collection. See -# docs/room_and_user_statistics.md. +# https://matrix-org.github.io/synapse/latest/room_and_user_statistics.html. # stats: # Uncomment the following to disable room and user statistics. Note that doing @@ -2768,7 +2770,7 @@ opentracing: #enabled: true # The list of homeservers we wish to send and receive span contexts and span baggage. - # See docs/opentracing.rst. + # See https://matrix-org.github.io/synapse/latest/opentracing.html. # # This is a list of regexes which are matched against the server_name of the # homeserver. diff --git a/docs/sample_log_config.yaml b/docs/sample_log_config.yaml index ff3c747180..669e600081 100644 --- a/docs/sample_log_config.yaml +++ b/docs/sample_log_config.yaml @@ -7,7 +7,7 @@ # be ingested by ELK stacks. See [2] for details. # # [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://github.com/matrix-org/synapse/blob/master/docs/structured_logging.md +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html version: 1 diff --git a/synapse/config/consent.py b/synapse/config/consent.py index 30d07cc219..b05a9bd97f 100644 --- a/synapse/config/consent.py +++ b/synapse/config/consent.py @@ -22,7 +22,7 @@ # User Consent configuration # # for detailed instructions, see -# https://github.com/matrix-org/synapse/blob/master/docs/consent_tracking.md +# https://matrix-org.github.io/synapse/latest/consent_tracking.html # # Parts of this section are required if enabling the 'consent' resource under # 'listeners', in particular 'template_dir' and 'version'. diff --git a/synapse/config/database.py b/synapse/config/database.py index c76ef1e1de..3d7d92f615 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -62,7 +62,8 @@ # cp_min: 5 # cp_max: 10 # -# For more information on using Synapse with Postgres, see `docs/postgres.md`. +# For more information on using Synapse with Postgres, +# see https://matrix-org.github.io/synapse/latest/postgres.html. # database: name: sqlite3 diff --git a/synapse/config/jwt.py b/synapse/config/jwt.py index 9e07e73008..9d295f5856 100644 --- a/synapse/config/jwt.py +++ b/synapse/config/jwt.py @@ -64,7 +64,7 @@ def generate_config_section(self, **kwargs): # Note that this is a non-standard login type and client support is # expected to be non-existent. # - # See https://github.com/matrix-org/synapse/blob/master/docs/jwt.md. + # See https://matrix-org.github.io/synapse/latest/jwt.html. # #jwt_config: # Uncomment the following to enable authorization using JSON web diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 91d9bcf32e..ad4e6e61c3 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -49,7 +49,7 @@ # be ingested by ELK stacks. See [2] for details. # # [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://github.com/matrix-org/synapse/blob/master/docs/structured_logging.md +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html version: 1 diff --git a/synapse/config/modules.py b/synapse/config/modules.py index 3209e1c492..ae0821e5a5 100644 --- a/synapse/config/modules.py +++ b/synapse/config/modules.py @@ -37,7 +37,7 @@ def generate_config_section(self, **kwargs): # Server admins can expand Synapse's functionality with external modules. # - # See https://matrix-org.github.io/synapse/develop/modules.html for more + # See https://matrix-org.github.io/synapse/latest/modules.html for more # documentation on how to configure or create custom modules for Synapse. # modules: diff --git a/synapse/config/oidc.py b/synapse/config/oidc.py index ea0abf5aa2..942e2672a9 100644 --- a/synapse/config/oidc.py +++ b/synapse/config/oidc.py @@ -166,7 +166,7 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # # module: The class name of a custom mapping module. Default is # {mapping_provider!r}. - # See https://github.com/matrix-org/synapse/blob/master/docs/sso_mapping_providers.md#openid-mapping-providers + # See https://matrix-org.github.io/synapse/latest/sso_mapping_providers.html#openid-mapping-providers # for information on implementing a custom mapping provider. # # config: Configuration for the mapping provider module. This section will @@ -217,7 +217,7 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # - attribute: groups # value: "admin" # - # See https://github.com/matrix-org/synapse/blob/master/docs/openid.md + # See https://matrix-org.github.io/synapse/latest/openid.html # for information on how to configure these options. # # For backwards compatibility, it is also possible to configure a single OIDC diff --git a/synapse/config/password_auth_providers.py b/synapse/config/password_auth_providers.py index 1cf69734bb..fd90b79772 100644 --- a/synapse/config/password_auth_providers.py +++ b/synapse/config/password_auth_providers.py @@ -57,7 +57,7 @@ def generate_config_section(self, **kwargs): # ex. LDAP, external tokens, etc. # # For more information and known implementations, please see - # https://github.com/matrix-org/synapse/blob/master/docs/password_auth_providers.md + # https://matrix-org.github.io/synapse/latest/password_auth_providers.html # # Note: instances wishing to use SAML or CAS authentication should # instead use the `saml2_config` or `cas_config` options, diff --git a/synapse/config/repository.py b/synapse/config/repository.py index 2f77d6703d..a7a82742ac 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -250,7 +250,7 @@ def generate_config_section(self, data_dir_path, **kwargs): # # If you are using a reverse proxy you may also need to set this value in # your reverse proxy's config. Notably Nginx has a small max body size by default. - # See https://matrix-org.github.io/synapse/develop/reverse_proxy.html. + # See https://matrix-org.github.io/synapse/latest/reverse_proxy.html. # #max_upload_size: 50M diff --git a/synapse/config/server.py b/synapse/config/server.py index 0833a5f7bc..6bff715230 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -153,7 +153,7 @@ def generate_ip_set( METRICS_PORT_WARNING = """\ The metrics_port configuration option is deprecated in Synapse 0.31 in favour of a listener. Please see -https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.md +https://matrix-org.github.io/synapse/latest/metrics-howto.html on how to configure the new listener. --------------------------------------------------------------------------------""" @@ -811,7 +811,7 @@ def generate_config_section( # In most cases you should avoid using a matrix specific subdomain such as # matrix.example.com or synapse.example.com as the server_name for the same # reasons you wouldn't use user@email.example.com as your email address. - # See https://github.com/matrix-org/synapse/blob/master/docs/delegate.md + # See https://matrix-org.github.io/synapse/latest/delegate.html # for information on how to host Synapse on a subdomain while preserving # a clean server_name. # @@ -988,9 +988,9 @@ def generate_config_section( # 'all local interfaces'. # # type: the type of listener. Normally 'http', but other valid options are: - # 'manhole' (see docs/manhole.md), - # 'metrics' (see docs/metrics-howto.md), - # 'replication' (see docs/workers.md). + # 'manhole' (see https://matrix-org.github.io/synapse/latest/manhole.html), + # 'metrics' (see https://matrix-org.github.io/synapse/latest/metrics-howto.html), + # 'replication' (see https://matrix-org.github.io/synapse/latest/workers.html). # # tls: set to true to enable TLS for this listener. Will use the TLS # key/cert specified in tls_private_key_path / tls_certificate_path. @@ -1015,8 +1015,8 @@ def generate_config_section( # 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 - # docs/consent_tracking.md. + # consent: user consent forms (/_matrix/consent). + # See https://matrix-org.github.io/synapse/latest/consent_tracking.html. # # federation: the server-server API (/_matrix/federation). Also implies # 'media', 'keys', 'openid' @@ -1025,12 +1025,13 @@ def generate_config_section( # # media: the media API (/_matrix/media). # - # metrics: the metrics interface. See docs/metrics-howto.md. + # metrics: the metrics interface. + # See https://matrix-org.github.io/synapse/latest/metrics-howto.html. # # openid: OpenID authentication. # - # replication: the HTTP replication API (/_synapse/replication). See - # docs/workers.md. + # replication: the HTTP replication API (/_synapse/replication). + # See https://matrix-org.github.io/synapse/latest/workers.html. # # static: static resources under synapse/static (/_matrix/static). (Mostly # useful for 'fallback authentication'.) @@ -1050,7 +1051,7 @@ def generate_config_section( # that unwraps TLS. # # If you plan to use a reverse proxy, please see - # https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.md. + # https://matrix-org.github.io/synapse/latest/reverse_proxy.html. # %(unsecure_http_bindings)s diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py index d0311d6468..cb7716c837 100644 --- a/synapse/config/spam_checker.py +++ b/synapse/config/spam_checker.py @@ -26,7 +26,7 @@ This server is using a spam checker module that is implementing the deprecated spam checker interface. Please check with the module's maintainer to see if a new version supporting Synapse's generic modules system is available. -For more information, please see https://matrix-org.github.io/synapse/develop/modules.html +For more information, please see https://matrix-org.github.io/synapse/latest/modules.html ---------------------------------------------------------------------------------------""" diff --git a/synapse/config/stats.py b/synapse/config/stats.py index 3d44b51201..78f61fe9da 100644 --- a/synapse/config/stats.py +++ b/synapse/config/stats.py @@ -51,7 +51,7 @@ def read_config(self, config, **kwargs): def generate_config_section(self, config_dir_path, server_name, **kwargs): return """ # Settings for local room and user statistics collection. See - # docs/room_and_user_statistics.md. + # https://matrix-org.github.io/synapse/latest/room_and_user_statistics.html. # stats: # Uncomment the following to disable room and user statistics. Note that doing diff --git a/synapse/config/tracer.py b/synapse/config/tracer.py index d0ea17261f..21b9a88353 100644 --- a/synapse/config/tracer.py +++ b/synapse/config/tracer.py @@ -81,7 +81,7 @@ def generate_config_section(cls, **kwargs): #enabled: true # The list of homeservers we wish to send and receive span contexts and span baggage. - # See docs/opentracing.rst. + # See https://matrix-org.github.io/synapse/latest/opentracing.html. # # This is a list of regexes which are matched against the server_name of the # homeserver. diff --git a/synapse/config/user_directory.py b/synapse/config/user_directory.py index 4cbf79eeed..b10df8a232 100644 --- a/synapse/config/user_directory.py +++ b/synapse/config/user_directory.py @@ -50,7 +50,7 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # # If you set it true, you'll have to rebuild the user_directory search # indexes, see: - # https://github.com/matrix-org/synapse/blob/master/docs/user_directory.md + # https://matrix-org.github.io/synapse/latest/user_directory.html # # Uncomment to return search results containing all known users, even if that # user does not share a room with the requester. From 189652b2fea038340e4e1420081c6ddd8093da0e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 7 Jul 2021 12:54:57 +0100 Subject: [PATCH 366/619] Fix a broken link in the admin api docs (#10322) * Fix a broken link in the admin api docs * Rename 10321.doc to 10321.docs * Rename 10321.docs to 10322.doc --- changelog.d/10322.doc | 1 + docs/admin_api/media_admin_api.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10322.doc diff --git a/changelog.d/10322.doc b/changelog.d/10322.doc new file mode 100644 index 0000000000..db604cf2aa --- /dev/null +++ b/changelog.d/10322.doc @@ -0,0 +1 @@ +Fix a broken link in the admin api docs. diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index b033fc03ef..61bed1e0d5 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -47,7 +47,7 @@ The API returns a JSON body like the following: ## List all media uploaded by a user Listing all media that has been uploaded by a local user can be achieved through -the use of the [List media of a user](user_admin_api.rst#list-media-of-a-user) +the use of the [List media of a user](user_admin_api.md#list-media-of-a-user) Admin API. # Quarantine media @@ -257,7 +257,7 @@ URL Parameters * `server_name`: string - The name of your local server (e.g `matrix.org`). * `before_ts`: string representing a positive integer - Unix timestamp in ms. Files that were last used before this timestamp will be deleted. It is the timestamp of -last access and not the timestamp creation. +last access and not the timestamp creation. * `size_gt`: Optional - string representing a positive integer - Size of the media in bytes. Files that are larger will be deleted. Defaults to `0`. * `keep_profiles`: Optional - string representing a boolean - Switch to also delete files From 225be7778727682e250a02acf975217f8eca9ed7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 8 Jul 2021 13:00:05 +0200 Subject: [PATCH 367/619] Rebuild event auth when rebuilding an event after a call to a `ThirdPartyEventRules` module (#10316) Because modules might send extra state events when processing an event (e.g. matrix-org/synapse-dinsic#100), and in some cases these extra events might get dropped if we don't recalculate the initial event's auth. --- changelog.d/10316.misc | 1 + synapse/handlers/message.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 changelog.d/10316.misc diff --git a/changelog.d/10316.misc b/changelog.d/10316.misc new file mode 100644 index 0000000000..1fd0810fde --- /dev/null +++ b/changelog.d/10316.misc @@ -0,0 +1 @@ +Rebuild event context and auth when processing specific results from `ThirdPartyEventRules` modules. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 66e40a915d..b960e18c4c 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1594,11 +1594,13 @@ async def _rebuild_event_after_third_party_rules( for k, v in original_event.internal_metadata.get_dict().items(): setattr(builder.internal_metadata, k, v) - # the event type hasn't changed, so there's no point in re-calculating the - # auth events. + # modules can send new state events, so we re-calculate the auth events just in + # case. + prev_event_ids = await self.store.get_prev_events_for_room(builder.room_id) + event = await builder.build( - prev_event_ids=original_event.prev_event_ids(), - auth_event_ids=original_event.auth_event_ids(), + prev_event_ids=prev_event_ids, + auth_event_ids=None, ) # we rebuild the event context, to be on the safe side. If nothing else, From aa7806486960f501d72917f1a90a36cdc8035a05 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Thu, 8 Jul 2021 14:27:12 +0100 Subject: [PATCH 368/619] Minor changes to `user_daily_visits` (#10324) * Use fake time in tests in _get_start_of_day. * Change the inequality of last_seen in user_daily_visits Co-authored-by: Erik Johnston --- changelog.d/10324.misc | 1 + synapse/storage/databases/main/metrics.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10324.misc diff --git a/changelog.d/10324.misc b/changelog.d/10324.misc new file mode 100644 index 0000000000..3c3ee6d6fc --- /dev/null +++ b/changelog.d/10324.misc @@ -0,0 +1 @@ +Minor change to the code that populates `user_daily_visits`. diff --git a/synapse/storage/databases/main/metrics.py b/synapse/storage/databases/main/metrics.py index c3f551d377..e3a544d9b2 100644 --- a/synapse/storage/databases/main/metrics.py +++ b/synapse/storage/databases/main/metrics.py @@ -320,7 +320,7 @@ def _get_start_of_day(self): """ Returns millisecond unixtime for start of UTC day. """ - now = time.gmtime() + now = time.gmtime(self._clock.time()) today_start = calendar.timegm((now.tm_year, now.tm_mon, now.tm_mday, 0, 0, 0)) return today_start * 1000 @@ -352,7 +352,7 @@ def _generate_user_daily_visits(txn): ) udv ON u.user_id = udv.user_id AND u.device_id=udv.device_id INNER JOIN users ON users.name=u.user_id - WHERE last_seen > ? AND last_seen <= ? + WHERE ? <= last_seen AND last_seen < ? AND udv.timestamp IS NULL AND users.is_guest=0 AND users.appservice_id IS NULL GROUP BY u.user_id, u.device_id From 974261cd819b06589b8d3588203c0bcddfddd795 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 8 Jul 2021 16:46:13 +0200 Subject: [PATCH 369/619] Fix broken links in INSTALL.md (#10331) Signed-off-by: Dirk Klimpel dirk@klimpel.org --- CHANGES.md | 17 +- INSTALL.md | 594 +----------------------------- README.rst | 8 +- UPGRADE.rst | 2 +- changelog.d/10331.doc | 1 + contrib/systemd/README.md | 3 +- docker/README.md | 4 +- docs/.sample_config_header.yaml | 3 +- docs/MSC1711_certificates_FAQ.md | 2 +- docs/postgres.md | 4 +- docs/sample_config.yaml | 3 +- docs/setup/installation.md | 603 ++++++++++++++++++++++++++++++- docs/upgrade.md | 2 +- 13 files changed, 629 insertions(+), 617 deletions(-) create mode 100644 changelog.d/10331.doc diff --git a/CHANGES.md b/CHANGES.md index 2b0179edc3..c930b48b25 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1226,7 +1226,10 @@ Crucially, this means __we will not produce .deb packages for Debian 9 (Stretch) The website https://endoflife.date/ has convenient summaries of the support schedules for projects like [Python](https://endoflife.date/python) and [PostgreSQL](https://endoflife.date/postgresql). -If you are unable to upgrade your environment to a supported version of Python or Postgres, we encourage you to consider using the [Synapse Docker images](./INSTALL.md#docker-images-and-ansible-playbooks) instead. +If you are unable to upgrade your environment to a supported version of Python or +Postgres, we encourage you to consider using the +[Synapse Docker images](https://matrix-org.github.io/synapse/latest/setup/installation.html#docker-images-and-ansible-playbooks) +instead. ### Transition Period @@ -1369,11 +1372,11 @@ To upgrade Synapse along with the cryptography package: * Administrators using the [`matrix.org` Docker image](https://hub.docker.com/r/matrixdotorg/synapse/) or the [Debian/Ubuntu packages from - `matrix.org`](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#matrixorg-packages) + `matrix.org`](https://matrix-org.github.io/synapse/latest/setup/installation.html#matrixorg-packages) should ensure that they have version 1.24.0 or 1.23.1 installed: these images include the updated packages. * Administrators who have [installed Synapse from - source](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#installing-from-source) + source](https://matrix-org.github.io/synapse/latest/setup/installation.html#installing-from-source) should upgrade the cryptography package within their virtualenv by running: ```sh /bin/pip install 'cryptography>=3.3' @@ -1415,11 +1418,11 @@ To upgrade Synapse along with the cryptography package: * Administrators using the [`matrix.org` Docker image](https://hub.docker.com/r/matrixdotorg/synapse/) or the [Debian/Ubuntu packages from - `matrix.org`](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#matrixorg-packages) + `matrix.org`](https://matrix-org.github.io/synapse/latest/setup/installation.html#matrixorg-packages) should ensure that they have version 1.24.0 or 1.23.1 installed: these images include the updated packages. * Administrators who have [installed Synapse from - source](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#installing-from-source) + source](https://matrix-org.github.io/synapse/latest/setup/installation.html#installing-from-source) should upgrade the cryptography package within their virtualenv by running: ```sh /bin/pip install 'cryptography>=3.3' @@ -2998,11 +3001,11 @@ installation remains secure. * Administrators using the [`matrix.org` Docker image](https://hub.docker.com/r/matrixdotorg/synapse/) or the [Debian/Ubuntu packages from - `matrix.org`](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#matrixorg-packages) + `matrix.org`](https://matrix-org.github.io/synapse/latest/setup/installation.html#matrixorg-packages) should ensure that they have version 1.12.0 installed: these images include Twisted 20.3.0. * Administrators who have [installed Synapse from - source](https://github.com/matrix-org/synapse/blob/master/INSTALL.md#installing-from-source) + source](https://matrix-org.github.io/synapse/latest/setup/installation.html#installing-from-source) should upgrade Twisted within their virtualenv by running: ```sh /bin/pip install 'Twisted>=20.3.0' diff --git a/INSTALL.md b/INSTALL.md index b0697052c1..f199b233b9 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,593 +1,7 @@ # Installation Instructions -There are 3 steps to follow under **Installation Instructions**. +This document has moved to the +[Synapse documentation website](https://matrix-org.github.io/synapse/latest/setup/installation.html). +Please update your links. -- [Installation Instructions](#installation-instructions) - - [Choosing your server name](#choosing-your-server-name) - - [Installing Synapse](#installing-synapse) - - [Installing from source](#installing-from-source) - - [Platform-specific prerequisites](#platform-specific-prerequisites) - - [Debian/Ubuntu/Raspbian](#debianubunturaspbian) - - [ArchLinux](#archlinux) - - [CentOS/Fedora](#centosfedora) - - [macOS](#macos) - - [OpenSUSE](#opensuse) - - [OpenBSD](#openbsd) - - [Windows](#windows) - - [Prebuilt packages](#prebuilt-packages) - - [Docker images and Ansible playbooks](#docker-images-and-ansible-playbooks) - - [Debian/Ubuntu](#debianubuntu) - - [Matrix.org packages](#matrixorg-packages) - - [Downstream Debian packages](#downstream-debian-packages) - - [Downstream Ubuntu packages](#downstream-ubuntu-packages) - - [Fedora](#fedora) - - [OpenSUSE](#opensuse-1) - - [SUSE Linux Enterprise Server](#suse-linux-enterprise-server) - - [ArchLinux](#archlinux-1) - - [Void Linux](#void-linux) - - [FreeBSD](#freebsd) - - [OpenBSD](#openbsd-1) - - [NixOS](#nixos) - - [Setting up Synapse](#setting-up-synapse) - - [Using PostgreSQL](#using-postgresql) - - [TLS certificates](#tls-certificates) - - [Client Well-Known URI](#client-well-known-uri) - - [Email](#email) - - [Registering a user](#registering-a-user) - - [Setting up a TURN server](#setting-up-a-turn-server) - - [URL previews](#url-previews) - - [Troubleshooting Installation](#troubleshooting-installation) - - -## Choosing your server name - -It is important to choose the name for your server before you install Synapse, -because it cannot be changed later. - -The server name determines the "domain" part of user-ids for users on your -server: these will all be of the format `@user:my.domain.name`. It also -determines how other matrix servers will reach yours for federation. - -For a test configuration, set this to the hostname of your server. For a more -production-ready setup, you will probably want to specify your domain -(`example.com`) rather than a matrix-specific hostname here (in the same way -that your email address is probably `user@example.com` rather than -`user@email.example.com`) - but doing so may require more advanced setup: see -[Setting up Federation](docs/federate.md). - -## Installing Synapse - -### Installing from source - -(Prebuilt packages are available for some platforms - see [Prebuilt packages](#prebuilt-packages).) - -When installing from source please make sure that the [Platform-specific prerequisites](#platform-specific-prerequisites) are already installed. - -System requirements: - -- POSIX-compliant system (tested on Linux & OS X) -- Python 3.5.2 or later, up to Python 3.9. -- At least 1GB of free RAM if you want to join large public rooms like #matrix:matrix.org - - -To install the Synapse homeserver run: - -```sh -mkdir -p ~/synapse -virtualenv -p python3 ~/synapse/env -source ~/synapse/env/bin/activate -pip install --upgrade pip -pip install --upgrade setuptools -pip install matrix-synapse -``` - -This will download Synapse from [PyPI](https://pypi.org/project/matrix-synapse) -and install it, along with the python libraries it uses, into a virtual environment -under `~/synapse/env`. Feel free to pick a different directory if you -prefer. - -This Synapse installation can then be later upgraded by using pip again with the -update flag: - -```sh -source ~/synapse/env/bin/activate -pip install -U matrix-synapse -``` - -Before you can start Synapse, you will need to generate a configuration -file. To do this, run (in your virtualenv, as before): - -```sh -cd ~/synapse -python -m synapse.app.homeserver \ - --server-name my.domain.name \ - --config-path homeserver.yaml \ - --generate-config \ - --report-stats=[yes|no] -``` - -... substituting an appropriate value for `--server-name`. - -This command will generate you a config file that you can then customise, but it will -also generate a set of keys for you. These keys will allow your homeserver to -identify itself to other homeserver, so don't lose or delete them. It would be -wise to back them up somewhere safe. (If, for whatever reason, you do need to -change your homeserver's keys, you may find that other homeserver have the -old key cached. If you update the signing key, you should change the name of the -key in the `.signing.key` file (the second word) to something -different. See the [spec](https://matrix.org/docs/spec/server_server/latest.html#retrieving-server-keys) for more information on key management). - -To actually run your new homeserver, pick a working directory for Synapse to -run (e.g. `~/synapse`), and: - -```sh -cd ~/synapse -source env/bin/activate -synctl start -``` - -#### Platform-specific prerequisites - -Synapse is written in Python but some of the libraries it uses are written in -C. So before we can install Synapse itself we need a working C compiler and the -header files for Python C extensions. - -##### Debian/Ubuntu/Raspbian - -Installing prerequisites on Ubuntu or Debian: - -```sh -sudo apt install build-essential python3-dev libffi-dev \ - python3-pip python3-setuptools sqlite3 \ - libssl-dev virtualenv libjpeg-dev libxslt1-dev -``` - -##### ArchLinux - -Installing prerequisites on ArchLinux: - -```sh -sudo pacman -S base-devel python python-pip \ - python-setuptools python-virtualenv sqlite3 -``` - -##### CentOS/Fedora - -Installing prerequisites on CentOS or Fedora Linux: - -```sh -sudo dnf install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ - libwebp-devel libxml2-devel libxslt-devel libpq-devel \ - python3-virtualenv libffi-devel openssl-devel python3-devel -sudo dnf groupinstall "Development Tools" -``` - -##### macOS - -Installing prerequisites on macOS: - -```sh -xcode-select --install -sudo easy_install pip -sudo pip install virtualenv -brew install pkg-config libffi -``` - -On macOS Catalina (10.15) you may need to explicitly install OpenSSL -via brew and inform `pip` about it so that `psycopg2` builds: - -```sh -brew install openssl@1.1 -export LDFLAGS="-L/usr/local/opt/openssl/lib" -export CPPFLAGS="-I/usr/local/opt/openssl/include" -``` - -##### OpenSUSE - -Installing prerequisites on openSUSE: - -```sh -sudo zypper in -t pattern devel_basis -sudo zypper in python-pip python-setuptools sqlite3 python-virtualenv \ - python-devel libffi-devel libopenssl-devel libjpeg62-devel -``` - -##### OpenBSD - -A port of Synapse is available under `net/synapse`. The filesystem -underlying the homeserver directory (defaults to `/var/synapse`) has to be -mounted with `wxallowed` (cf. `mount(8)`), so creating a separate filesystem -and mounting it to `/var/synapse` should be taken into consideration. - -To be able to build Synapse's dependency on python the `WRKOBJDIR` -(cf. `bsd.port.mk(5)`) for building python, too, needs to be on a filesystem -mounted with `wxallowed` (cf. `mount(8)`). - -Creating a `WRKOBJDIR` for building python under `/usr/local` (which on a -default OpenBSD installation is mounted with `wxallowed`): - -```sh -doas mkdir /usr/local/pobj_wxallowed -``` - -Assuming `PORTS_PRIVSEP=Yes` (cf. `bsd.port.mk(5)`) and `SUDO=doas` are -configured in `/etc/mk.conf`: - -```sh -doas chown _pbuild:_pbuild /usr/local/pobj_wxallowed -``` - -Setting the `WRKOBJDIR` for building python: - -```sh -echo WRKOBJDIR_lang/python/3.7=/usr/local/pobj_wxallowed \\nWRKOBJDIR_lang/python/2.7=/usr/local/pobj_wxallowed >> /etc/mk.conf -``` - -Building Synapse: - -```sh -cd /usr/ports/net/synapse -make install -``` - -##### Windows - -If you wish to run or develop Synapse on Windows, the Windows Subsystem For -Linux provides a Linux environment on Windows 10 which is capable of using the -Debian, Fedora, or source installation methods. More information about WSL can -be found at for -Windows 10 and -for Windows Server. - -### Prebuilt packages - -As an alternative to installing from source, prebuilt packages are available -for a number of platforms. - -#### Docker images and Ansible playbooks - -There is an official synapse image available at - which can be used with -the docker-compose file available at [contrib/docker](contrib/docker). Further -information on this including configuration options is available in the README -on hub.docker.com. - -Alternatively, Andreas Peters (previously Silvio Fricke) has contributed a -Dockerfile to automate a synapse server in a single Docker image, at - - -Slavi Pantaleev has created an Ansible playbook, -which installs the offical Docker image of Matrix Synapse -along with many other Matrix-related services (Postgres database, Element, coturn, -ma1sd, SSL support, etc.). -For more details, see - - -#### Debian/Ubuntu - -##### Matrix.org packages - -Matrix.org provides Debian/Ubuntu packages of the latest stable version of -Synapse via . They are available for Debian -9 (Stretch), Ubuntu 16.04 (Xenial), and later. To use them: - -```sh -sudo apt install -y lsb-release wget apt-transport-https -sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg -echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main" | - sudo tee /etc/apt/sources.list.d/matrix-org.list -sudo apt update -sudo apt install matrix-synapse-py3 -``` - -**Note**: if you followed a previous version of these instructions which -recommended using `apt-key add` to add an old key from -`https://matrix.org/packages/debian/`, you should note that this key has been -revoked. You should remove the old key with `sudo apt-key remove -C35EB17E1EAE708E6603A9B3AD0592FE47F0DF61`, and follow the above instructions to -update your configuration. - -The fingerprint of the repository signing key (as shown by `gpg -/usr/share/keyrings/matrix-org-archive-keyring.gpg`) is -`AAF9AE843A7584B5A3E4CD2BCF45A512DE2DA058`. - -##### Downstream Debian packages - -We do not recommend using the packages from the default Debian `buster` -repository at this time, as they are old and suffer from known security -vulnerabilities. You can install the latest version of Synapse from -[our repository](#matrixorg-packages) or from `buster-backports`. Please -see the [Debian documentation](https://backports.debian.org/Instructions/) -for information on how to use backports. - -If you are using Debian `sid` or testing, Synapse is available in the default -repositories and it should be possible to install it simply with: - -```sh -sudo apt install matrix-synapse -``` - -##### Downstream Ubuntu packages - -We do not recommend using the packages in the default Ubuntu repository -at this time, as they are old and suffer from known security vulnerabilities. -The latest version of Synapse can be installed from [our repository](#matrixorg-packages). - -#### Fedora - -Synapse is in the Fedora repositories as `matrix-synapse`: - -```sh -sudo dnf install matrix-synapse -``` - -Oleg Girko provides Fedora RPMs at - - -#### OpenSUSE - -Synapse is in the OpenSUSE repositories as `matrix-synapse`: - -```sh -sudo zypper install matrix-synapse -``` - -#### SUSE Linux Enterprise Server - -Unofficial package are built for SLES 15 in the openSUSE:Backports:SLE-15 repository at - - -#### ArchLinux - -The quickest way to get up and running with ArchLinux is probably with the community package -, which should pull in most of -the necessary dependencies. - -pip may be outdated (6.0.7-1 and needs to be upgraded to 6.0.8-1 ): - -```sh -sudo pip install --upgrade pip -``` - -If you encounter an error with lib bcrypt causing an Wrong ELF Class: -ELFCLASS32 (x64 Systems), you may need to reinstall py-bcrypt to correctly -compile it under the right architecture. (This should not be needed if -installing under virtualenv): - -```sh -sudo pip uninstall py-bcrypt -sudo pip install py-bcrypt -``` - -#### Void Linux - -Synapse can be found in the void repositories as 'synapse': - -```sh -xbps-install -Su -xbps-install -S synapse -``` - -#### FreeBSD - -Synapse can be installed via FreeBSD Ports or Packages contributed by Brendan Molloy from: - -- Ports: `cd /usr/ports/net-im/py-matrix-synapse && make install clean` -- Packages: `pkg install py37-matrix-synapse` - -#### OpenBSD - -As of OpenBSD 6.7 Synapse is available as a pre-compiled binary. The filesystem -underlying the homeserver directory (defaults to `/var/synapse`) has to be -mounted with `wxallowed` (cf. `mount(8)`), so creating a separate filesystem -and mounting it to `/var/synapse` should be taken into consideration. - -Installing Synapse: - -```sh -doas pkg_add synapse -``` - -#### NixOS - -Robin Lambertz has packaged Synapse for NixOS at: - - -## Setting up Synapse - -Once you have installed synapse as above, you will need to configure it. - -### Using PostgreSQL - -By default Synapse uses an [SQLite](https://sqlite.org/) database and in doing so trades -performance for convenience. Almost all installations should opt to use [PostgreSQL](https://www.postgresql.org) -instead. Advantages include: - -- significant performance improvements due to the superior threading and - caching model, smarter query optimiser -- allowing the DB to be run on separate hardware - -For information on how to install and use PostgreSQL in Synapse, please see -[docs/postgres.md](docs/postgres.md) - -SQLite is only acceptable for testing purposes. SQLite should not be used in -a production server. Synapse will perform poorly when using -SQLite, especially when participating in large rooms. - -### TLS certificates - -The default configuration exposes a single HTTP port on the local -interface: `http://localhost:8008`. It is suitable for local testing, -but for any practical use, you will need Synapse's APIs to be served -over HTTPS. - -The recommended way to do so is to set up a reverse proxy on port -`8448`. You can find documentation on doing so in -[docs/reverse_proxy.md](docs/reverse_proxy.md). - -Alternatively, you can configure Synapse to expose an HTTPS port. To do -so, you will need to edit `homeserver.yaml`, as follows: - -- First, under the `listeners` section, uncomment the configuration for the - TLS-enabled listener. (Remove the hash sign (`#`) at the start of - each line). The relevant lines are like this: - -```yaml - - port: 8448 - type: http - tls: true - resources: - - names: [client, federation] - ``` - -- You will also need to uncomment the `tls_certificate_path` and - `tls_private_key_path` lines under the `TLS` section. You will need to manage - provisioning of these certificates yourself. - - If you are using your own certificate, be sure to use a `.pem` file that - includes the full certificate chain including any intermediate certificates - (for instance, if using certbot, use `fullchain.pem` as your certificate, not - `cert.pem`). - -For a more detailed guide to configuring your server for federation, see -[federate.md](docs/federate.md). - -### Client Well-Known URI - -Setting up the client Well-Known URI is optional but if you set it up, it will -allow users to enter their full username (e.g. `@user:`) into clients -which support well-known lookup to automatically configure the homeserver and -identity server URLs. This is useful so that users don't have to memorize or think -about the actual homeserver URL you are using. - -The URL `https:///.well-known/matrix/client` should return JSON in -the following format. - -```json -{ - "m.homeserver": { - "base_url": "https://" - } -} -``` - -It can optionally contain identity server information as well. - -```json -{ - "m.homeserver": { - "base_url": "https://" - }, - "m.identity_server": { - "base_url": "https://" - } -} -``` - -To work in browser based clients, the file must be served with the appropriate -Cross-Origin Resource Sharing (CORS) headers. A recommended value would be -`Access-Control-Allow-Origin: *` which would allow all browser based clients to -view it. - -In nginx this would be something like: - -```nginx -location /.well-known/matrix/client { - return 200 '{"m.homeserver": {"base_url": "https://"}}'; - default_type application/json; - add_header Access-Control-Allow-Origin *; -} -``` - -You should also ensure the `public_baseurl` option in `homeserver.yaml` is set -correctly. `public_baseurl` should be set to the URL that clients will use to -connect to your server. This is the same URL you put for the `m.homeserver` -`base_url` above. - -```yaml -public_baseurl: "https://" -``` - -### Email - -It is desirable for Synapse to have the capability to send email. This allows -Synapse to send password reset emails, send verifications when an email address -is added to a user's account, and send email notifications to users when they -receive new messages. - -To configure an SMTP server for Synapse, modify the configuration section -headed `email`, and be sure to have at least the `smtp_host`, `smtp_port` -and `notif_from` fields filled out. You may also need to set `smtp_user`, -`smtp_pass`, and `require_transport_security`. - -If email is not configured, password reset, registration and notifications via -email will be disabled. - -### Registering a user - -The easiest way to create a new user is to do so from a client like [Element](https://element.io/). - -Alternatively, you can do so from the command line. This can be done as follows: - - 1. If synapse was installed via pip, activate the virtualenv as follows (if Synapse was - installed via a prebuilt package, `register_new_matrix_user` should already be - on the search path): - ```sh - cd ~/synapse - source env/bin/activate - synctl start # if not already running - ``` - 2. Run the following command: - ```sh - register_new_matrix_user -c homeserver.yaml http://localhost:8008 - ``` - -This will prompt you to add details for the new user, and will then connect to -the running Synapse to create the new user. For example: -``` -New user localpart: erikj -Password: -Confirm password: -Make admin [no]: -Success! -``` - -This process uses a setting `registration_shared_secret` in -`homeserver.yaml`, which is shared between Synapse itself and the -`register_new_matrix_user` script. It doesn't matter what it is (a random -value is generated by `--generate-config`), but it should be kept secret, as -anyone with knowledge of it can register users, including admin accounts, -on your server even if `enable_registration` is `false`. - -### Setting up a TURN server - -For reliable VoIP calls to be routed via this homeserver, you MUST configure -a TURN server. See [docs/turn-howto.md](docs/turn-howto.md) for details. - -### URL previews - -Synapse includes support for previewing URLs, which is disabled by default. To -turn it on you must enable the `url_preview_enabled: True` config parameter -and explicitly specify the IP ranges that Synapse is not allowed to spider for -previewing in the `url_preview_ip_range_blacklist` configuration parameter. -This is critical from a security perspective to stop arbitrary Matrix users -spidering 'internal' URLs on your network. At the very least we recommend that -your loopback and RFC1918 IP addresses are blacklisted. - -This also requires the optional `lxml` python dependency to be installed. This -in turn requires the `libxml2` library to be available - on Debian/Ubuntu this -means `apt-get install libxml2-dev`, or equivalent for your OS. - -### Troubleshooting Installation - -`pip` seems to leak *lots* of memory during installation. For instance, a Linux -host with 512MB of RAM may run out of memory whilst installing Twisted. If this -happens, you will have to individually install the dependencies which are -failing, e.g.: - -```sh -pip install twisted -``` - -If you have any other problems, feel free to ask in -[#synapse:matrix.org](https://matrix.to/#/#synapse:matrix.org). +The markdown source is available in [docs/setup/installation.md](docs/setup/installation.md). diff --git a/README.rst b/README.rst index 6d3cf6c1a5..e5332d62a9 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,8 @@ Synapse Installation .. _federation: -* For details on how to install synapse, see ``_. +* For details on how to install synapse, see + `Installation Instructions `_. * For specific details on how to configure Synapse for federation see `docs/federate.md `_ @@ -106,7 +107,8 @@ from a web client. Unless you are running a test instance of Synapse on your local machine, in general, you will need to enable TLS support before you can successfully -connect from a client: see ``_. +connect from a client: see +`TLS certificates `_. An easy way to get started is to login or register via Element at https://app.element.io/#/login or https://app.element.io/#/register respectively. @@ -265,7 +267,7 @@ Join our developer community on Matrix: `#synapse-dev:matrix.org `_. +`Installing from source `_. To check out a synapse for development, clone the git repo into a working directory of your choice:: diff --git a/UPGRADE.rst b/UPGRADE.rst index 82548ac850..17ecd935fd 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -1,7 +1,7 @@ Upgrading Synapse ================= -This document has moved to the `Synapse documentation website `_. +This document has moved to the `Synapse documentation website `_. Please update your links. The markdown source is available in `docs/upgrade.md `_. diff --git a/changelog.d/10331.doc b/changelog.d/10331.doc new file mode 100644 index 0000000000..9b9acd9007 --- /dev/null +++ b/changelog.d/10331.doc @@ -0,0 +1 @@ +Fix broken links in INSTALL.md. Contributed by @dklimpel. diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md index 5d42b3464f..2844cbc8e0 100644 --- a/contrib/systemd/README.md +++ b/contrib/systemd/README.md @@ -2,7 +2,8 @@ This is a setup for managing synapse with a user contributed systemd unit file. It provides a `matrix-synapse` systemd unit file that should be tailored to accommodate your installation in accordance with the installation -instructions provided in [installation instructions](../../INSTALL.md). +instructions provided in +[installation instructions](https://matrix-org.github.io/synapse/latest/setup/installation.html). ## Setup 1. Under the service section, ensure the `User` variable matches which user diff --git a/docker/README.md b/docker/README.md index 3f28cdada3..edf917bb11 100644 --- a/docker/README.md +++ b/docker/README.md @@ -45,7 +45,7 @@ docker run -it --rm \ ``` For information on picking a suitable server name, see -https://github.com/matrix-org/synapse/blob/master/INSTALL.md. +https://matrix-org.github.io/synapse/latest/setup/installation.html. The above command will generate a `homeserver.yaml` in (typically) `/var/lib/docker/volumes/synapse-data/_data`. You should check this file, and @@ -139,7 +139,7 @@ For documentation on using a reverse proxy, see https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.md. For more information on enabling TLS support in synapse itself, see -https://github.com/matrix-org/synapse/blob/master/INSTALL.md#tls-certificates. Of +https://matrix-org.github.io/synapse/latest/setup/installation.html#tls-certificates. Of course, you will need to expose the TLS port from the container with a `-p` argument to `docker run`. diff --git a/docs/.sample_config_header.yaml b/docs/.sample_config_header.yaml index 8c9b31acdb..09e86ca0ca 100644 --- a/docs/.sample_config_header.yaml +++ b/docs/.sample_config_header.yaml @@ -8,7 +8,8 @@ # # It is *not* intended to be copied and used as the basis for a real # homeserver.yaml. Instead, if you are starting from scratch, please generate -# a fresh config using Synapse by following the instructions in INSTALL.md. +# a fresh config using Synapse by following the instructions in +# https://matrix-org.github.io/synapse/latest/setup/installation.html. # Configuration options that take a time period can be set using a number # followed by a letter. Letters have the following meanings: diff --git a/docs/MSC1711_certificates_FAQ.md b/docs/MSC1711_certificates_FAQ.md index ce8189d4ed..283f288aaf 100644 --- a/docs/MSC1711_certificates_FAQ.md +++ b/docs/MSC1711_certificates_FAQ.md @@ -14,7 +14,7 @@ upgraded, however it may be of use to those with old installs returning to the project. If you are setting up a server from scratch you almost certainly should look at -the [installation guide](../INSTALL.md) instead. +the [installation guide](setup/installation.md) instead. ## Introduction The goal of Synapse 0.99.0 is to act as a stepping stone to Synapse 1.0.0. It diff --git a/docs/postgres.md b/docs/postgres.md index f83155e52a..2c0a5b803a 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -8,14 +8,14 @@ Synapse will require the python postgres client library in order to connect to a postgres database. - If you are using the [matrix.org debian/ubuntu - packages](../INSTALL.md#matrixorg-packages), the necessary python + packages](setup/installation.md#matrixorg-packages), the necessary python library will already be installed, but you will need to ensure the low-level postgres library is installed, which you can do with `apt install libpq5`. - For other pre-built packages, please consult the documentation from the relevant package. - If you installed synapse [in a - virtualenv](../INSTALL.md#installing-from-source), you can install + virtualenv](setup/installation.md#installing-from-source), you can install the library with: ~/synapse/env/bin/pip install "matrix-synapse[postgres]" diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 71463168e3..054770f71f 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -8,7 +8,8 @@ # # It is *not* intended to be copied and used as the basis for a real # homeserver.yaml. Instead, if you are starting from scratch, please generate -# a fresh config using Synapse by following the instructions in INSTALL.md. +# a fresh config using Synapse by following the instructions in +# https://matrix-org.github.io/synapse/latest/setup/installation.html. # Configuration options that take a time period can be set using a number # followed by a letter. Letters have the following meanings: diff --git a/docs/setup/installation.md b/docs/setup/installation.md index 8bb1cffd3d..d041d08333 100644 --- a/docs/setup/installation.md +++ b/docs/setup/installation.md @@ -1,7 +1,596 @@ - -{{#include ../../INSTALL.md}} \ No newline at end of file +# Installation Instructions + +There are 3 steps to follow under **Installation Instructions**. + +- [Installation Instructions](#installation-instructions) + - [Choosing your server name](#choosing-your-server-name) + - [Installing Synapse](#installing-synapse) + - [Installing from source](#installing-from-source) + - [Platform-specific prerequisites](#platform-specific-prerequisites) + - [Debian/Ubuntu/Raspbian](#debianubunturaspbian) + - [ArchLinux](#archlinux) + - [CentOS/Fedora](#centosfedora) + - [macOS](#macos) + - [OpenSUSE](#opensuse) + - [OpenBSD](#openbsd) + - [Windows](#windows) + - [Prebuilt packages](#prebuilt-packages) + - [Docker images and Ansible playbooks](#docker-images-and-ansible-playbooks) + - [Debian/Ubuntu](#debianubuntu) + - [Matrix.org packages](#matrixorg-packages) + - [Downstream Debian packages](#downstream-debian-packages) + - [Downstream Ubuntu packages](#downstream-ubuntu-packages) + - [Fedora](#fedora) + - [OpenSUSE](#opensuse-1) + - [SUSE Linux Enterprise Server](#suse-linux-enterprise-server) + - [ArchLinux](#archlinux-1) + - [Void Linux](#void-linux) + - [FreeBSD](#freebsd) + - [OpenBSD](#openbsd-1) + - [NixOS](#nixos) + - [Setting up Synapse](#setting-up-synapse) + - [Using PostgreSQL](#using-postgresql) + - [TLS certificates](#tls-certificates) + - [Client Well-Known URI](#client-well-known-uri) + - [Email](#email) + - [Registering a user](#registering-a-user) + - [Setting up a TURN server](#setting-up-a-turn-server) + - [URL previews](#url-previews) + - [Troubleshooting Installation](#troubleshooting-installation) + + +## Choosing your server name + +It is important to choose the name for your server before you install Synapse, +because it cannot be changed later. + +The server name determines the "domain" part of user-ids for users on your +server: these will all be of the format `@user:my.domain.name`. It also +determines how other matrix servers will reach yours for federation. + +For a test configuration, set this to the hostname of your server. For a more +production-ready setup, you will probably want to specify your domain +(`example.com`) rather than a matrix-specific hostname here (in the same way +that your email address is probably `user@example.com` rather than +`user@email.example.com`) - but doing so may require more advanced setup: see +[Setting up Federation](../federate.md). + +## Installing Synapse + +### Installing from source + +(Prebuilt packages are available for some platforms - see [Prebuilt packages](#prebuilt-packages).) + +When installing from source please make sure that the [Platform-specific prerequisites](#platform-specific-prerequisites) are already installed. + +System requirements: + +- POSIX-compliant system (tested on Linux & OS X) +- Python 3.5.2 or later, up to Python 3.9. +- At least 1GB of free RAM if you want to join large public rooms like #matrix:matrix.org + + +To install the Synapse homeserver run: + +```sh +mkdir -p ~/synapse +virtualenv -p python3 ~/synapse/env +source ~/synapse/env/bin/activate +pip install --upgrade pip +pip install --upgrade setuptools +pip install matrix-synapse +``` + +This will download Synapse from [PyPI](https://pypi.org/project/matrix-synapse) +and install it, along with the python libraries it uses, into a virtual environment +under `~/synapse/env`. Feel free to pick a different directory if you +prefer. + +This Synapse installation can then be later upgraded by using pip again with the +update flag: + +```sh +source ~/synapse/env/bin/activate +pip install -U matrix-synapse +``` + +Before you can start Synapse, you will need to generate a configuration +file. To do this, run (in your virtualenv, as before): + +```sh +cd ~/synapse +python -m synapse.app.homeserver \ + --server-name my.domain.name \ + --config-path homeserver.yaml \ + --generate-config \ + --report-stats=[yes|no] +``` + +... substituting an appropriate value for `--server-name`. + +This command will generate you a config file that you can then customise, but it will +also generate a set of keys for you. These keys will allow your homeserver to +identify itself to other homeserver, so don't lose or delete them. It would be +wise to back them up somewhere safe. (If, for whatever reason, you do need to +change your homeserver's keys, you may find that other homeserver have the +old key cached. If you update the signing key, you should change the name of the +key in the `.signing.key` file (the second word) to something +different. See the [spec](https://matrix.org/docs/spec/server_server/latest.html#retrieving-server-keys) for more information on key management). + +To actually run your new homeserver, pick a working directory for Synapse to +run (e.g. `~/synapse`), and: + +```sh +cd ~/synapse +source env/bin/activate +synctl start +``` + +#### Platform-specific prerequisites + +Synapse is written in Python but some of the libraries it uses are written in +C. So before we can install Synapse itself we need a working C compiler and the +header files for Python C extensions. + +##### Debian/Ubuntu/Raspbian + +Installing prerequisites on Ubuntu or Debian: + +```sh +sudo apt install build-essential python3-dev libffi-dev \ + python3-pip python3-setuptools sqlite3 \ + libssl-dev virtualenv libjpeg-dev libxslt1-dev +``` + +##### ArchLinux + +Installing prerequisites on ArchLinux: + +```sh +sudo pacman -S base-devel python python-pip \ + python-setuptools python-virtualenv sqlite3 +``` + +##### CentOS/Fedora + +Installing prerequisites on CentOS or Fedora Linux: + +```sh +sudo dnf install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ + libwebp-devel libxml2-devel libxslt-devel libpq-devel \ + python3-virtualenv libffi-devel openssl-devel python3-devel +sudo dnf groupinstall "Development Tools" +``` + +##### macOS + +Installing prerequisites on macOS: + +```sh +xcode-select --install +sudo easy_install pip +sudo pip install virtualenv +brew install pkg-config libffi +``` + +On macOS Catalina (10.15) you may need to explicitly install OpenSSL +via brew and inform `pip` about it so that `psycopg2` builds: + +```sh +brew install openssl@1.1 +export LDFLAGS="-L/usr/local/opt/openssl/lib" +export CPPFLAGS="-I/usr/local/opt/openssl/include" +``` + +##### OpenSUSE + +Installing prerequisites on openSUSE: + +```sh +sudo zypper in -t pattern devel_basis +sudo zypper in python-pip python-setuptools sqlite3 python-virtualenv \ + python-devel libffi-devel libopenssl-devel libjpeg62-devel +``` + +##### OpenBSD + +A port of Synapse is available under `net/synapse`. The filesystem +underlying the homeserver directory (defaults to `/var/synapse`) has to be +mounted with `wxallowed` (cf. `mount(8)`), so creating a separate filesystem +and mounting it to `/var/synapse` should be taken into consideration. + +To be able to build Synapse's dependency on python the `WRKOBJDIR` +(cf. `bsd.port.mk(5)`) for building python, too, needs to be on a filesystem +mounted with `wxallowed` (cf. `mount(8)`). + +Creating a `WRKOBJDIR` for building python under `/usr/local` (which on a +default OpenBSD installation is mounted with `wxallowed`): + +```sh +doas mkdir /usr/local/pobj_wxallowed +``` + +Assuming `PORTS_PRIVSEP=Yes` (cf. `bsd.port.mk(5)`) and `SUDO=doas` are +configured in `/etc/mk.conf`: + +```sh +doas chown _pbuild:_pbuild /usr/local/pobj_wxallowed +``` + +Setting the `WRKOBJDIR` for building python: + +```sh +echo WRKOBJDIR_lang/python/3.7=/usr/local/pobj_wxallowed \\nWRKOBJDIR_lang/python/2.7=/usr/local/pobj_wxallowed >> /etc/mk.conf +``` + +Building Synapse: + +```sh +cd /usr/ports/net/synapse +make install +``` + +##### Windows + +If you wish to run or develop Synapse on Windows, the Windows Subsystem For +Linux provides a Linux environment on Windows 10 which is capable of using the +Debian, Fedora, or source installation methods. More information about WSL can +be found at for +Windows 10 and +for Windows Server. + +### Prebuilt packages + +As an alternative to installing from source, prebuilt packages are available +for a number of platforms. + +#### Docker images and Ansible playbooks + +There is an official synapse image available at + which can be used with +the docker-compose file available at +[contrib/docker](https://github.com/matrix-org/synapse/tree/develop/contrib/docker). +Further information on this including configuration options is available in the README +on hub.docker.com. + +Alternatively, Andreas Peters (previously Silvio Fricke) has contributed a +Dockerfile to automate a synapse server in a single Docker image, at + + +Slavi Pantaleev has created an Ansible playbook, +which installs the offical Docker image of Matrix Synapse +along with many other Matrix-related services (Postgres database, Element, coturn, +ma1sd, SSL support, etc.). +For more details, see + + +#### Debian/Ubuntu + +##### Matrix.org packages + +Matrix.org provides Debian/Ubuntu packages of the latest stable version of +Synapse via . They are available for Debian +9 (Stretch), Ubuntu 16.04 (Xenial), and later. To use them: + +```sh +sudo apt install -y lsb-release wget apt-transport-https +sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg +echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main" | + sudo tee /etc/apt/sources.list.d/matrix-org.list +sudo apt update +sudo apt install matrix-synapse-py3 +``` + +**Note**: if you followed a previous version of these instructions which +recommended using `apt-key add` to add an old key from +`https://matrix.org/packages/debian/`, you should note that this key has been +revoked. You should remove the old key with `sudo apt-key remove +C35EB17E1EAE708E6603A9B3AD0592FE47F0DF61`, and follow the above instructions to +update your configuration. + +The fingerprint of the repository signing key (as shown by `gpg +/usr/share/keyrings/matrix-org-archive-keyring.gpg`) is +`AAF9AE843A7584B5A3E4CD2BCF45A512DE2DA058`. + +##### Downstream Debian packages + +We do not recommend using the packages from the default Debian `buster` +repository at this time, as they are old and suffer from known security +vulnerabilities. You can install the latest version of Synapse from +[our repository](#matrixorg-packages) or from `buster-backports`. Please +see the [Debian documentation](https://backports.debian.org/Instructions/) +for information on how to use backports. + +If you are using Debian `sid` or testing, Synapse is available in the default +repositories and it should be possible to install it simply with: + +```sh +sudo apt install matrix-synapse +``` + +##### Downstream Ubuntu packages + +We do not recommend using the packages in the default Ubuntu repository +at this time, as they are old and suffer from known security vulnerabilities. +The latest version of Synapse can be installed from [our repository](#matrixorg-packages). + +#### Fedora + +Synapse is in the Fedora repositories as `matrix-synapse`: + +```sh +sudo dnf install matrix-synapse +``` + +Oleg Girko provides Fedora RPMs at + + +#### OpenSUSE + +Synapse is in the OpenSUSE repositories as `matrix-synapse`: + +```sh +sudo zypper install matrix-synapse +``` + +#### SUSE Linux Enterprise Server + +Unofficial package are built for SLES 15 in the openSUSE:Backports:SLE-15 repository at + + +#### ArchLinux + +The quickest way to get up and running with ArchLinux is probably with the community package +, which should pull in most of +the necessary dependencies. + +pip may be outdated (6.0.7-1 and needs to be upgraded to 6.0.8-1 ): + +```sh +sudo pip install --upgrade pip +``` + +If you encounter an error with lib bcrypt causing an Wrong ELF Class: +ELFCLASS32 (x64 Systems), you may need to reinstall py-bcrypt to correctly +compile it under the right architecture. (This should not be needed if +installing under virtualenv): + +```sh +sudo pip uninstall py-bcrypt +sudo pip install py-bcrypt +``` + +#### Void Linux + +Synapse can be found in the void repositories as 'synapse': + +```sh +xbps-install -Su +xbps-install -S synapse +``` + +#### FreeBSD + +Synapse can be installed via FreeBSD Ports or Packages contributed by Brendan Molloy from: + +- Ports: `cd /usr/ports/net-im/py-matrix-synapse && make install clean` +- Packages: `pkg install py37-matrix-synapse` + +#### OpenBSD + +As of OpenBSD 6.7 Synapse is available as a pre-compiled binary. The filesystem +underlying the homeserver directory (defaults to `/var/synapse`) has to be +mounted with `wxallowed` (cf. `mount(8)`), so creating a separate filesystem +and mounting it to `/var/synapse` should be taken into consideration. + +Installing Synapse: + +```sh +doas pkg_add synapse +``` + +#### NixOS + +Robin Lambertz has packaged Synapse for NixOS at: + + +## Setting up Synapse + +Once you have installed synapse as above, you will need to configure it. + +### Using PostgreSQL + +By default Synapse uses an [SQLite](https://sqlite.org/) database and in doing so trades +performance for convenience. Almost all installations should opt to use [PostgreSQL](https://www.postgresql.org) +instead. Advantages include: + +- significant performance improvements due to the superior threading and + caching model, smarter query optimiser +- allowing the DB to be run on separate hardware + +For information on how to install and use PostgreSQL in Synapse, please see +[docs/postgres.md](../postgres.md) + +SQLite is only acceptable for testing purposes. SQLite should not be used in +a production server. Synapse will perform poorly when using +SQLite, especially when participating in large rooms. + +### TLS certificates + +The default configuration exposes a single HTTP port on the local +interface: `http://localhost:8008`. It is suitable for local testing, +but for any practical use, you will need Synapse's APIs to be served +over HTTPS. + +The recommended way to do so is to set up a reverse proxy on port +`8448`. You can find documentation on doing so in +[docs/reverse_proxy.md](../reverse_proxy.md). + +Alternatively, you can configure Synapse to expose an HTTPS port. To do +so, you will need to edit `homeserver.yaml`, as follows: + +- First, under the `listeners` section, uncomment the configuration for the + TLS-enabled listener. (Remove the hash sign (`#`) at the start of + each line). The relevant lines are like this: + +```yaml + - port: 8448 + type: http + tls: true + resources: + - names: [client, federation] + ``` + +- You will also need to uncomment the `tls_certificate_path` and + `tls_private_key_path` lines under the `TLS` section. You will need to manage + provisioning of these certificates yourself. + + If you are using your own certificate, be sure to use a `.pem` file that + includes the full certificate chain including any intermediate certificates + (for instance, if using certbot, use `fullchain.pem` as your certificate, not + `cert.pem`). + +For a more detailed guide to configuring your server for federation, see +[federate.md](../federate.md). + +### Client Well-Known URI + +Setting up the client Well-Known URI is optional but if you set it up, it will +allow users to enter their full username (e.g. `@user:`) into clients +which support well-known lookup to automatically configure the homeserver and +identity server URLs. This is useful so that users don't have to memorize or think +about the actual homeserver URL you are using. + +The URL `https:///.well-known/matrix/client` should return JSON in +the following format. + +```json +{ + "m.homeserver": { + "base_url": "https://" + } +} +``` + +It can optionally contain identity server information as well. + +```json +{ + "m.homeserver": { + "base_url": "https://" + }, + "m.identity_server": { + "base_url": "https://" + } +} +``` + +To work in browser based clients, the file must be served with the appropriate +Cross-Origin Resource Sharing (CORS) headers. A recommended value would be +`Access-Control-Allow-Origin: *` which would allow all browser based clients to +view it. + +In nginx this would be something like: + +```nginx +location /.well-known/matrix/client { + return 200 '{"m.homeserver": {"base_url": "https://"}}'; + default_type application/json; + add_header Access-Control-Allow-Origin *; +} +``` + +You should also ensure the `public_baseurl` option in `homeserver.yaml` is set +correctly. `public_baseurl` should be set to the URL that clients will use to +connect to your server. This is the same URL you put for the `m.homeserver` +`base_url` above. + +```yaml +public_baseurl: "https://" +``` + +### Email + +It is desirable for Synapse to have the capability to send email. This allows +Synapse to send password reset emails, send verifications when an email address +is added to a user's account, and send email notifications to users when they +receive new messages. + +To configure an SMTP server for Synapse, modify the configuration section +headed `email`, and be sure to have at least the `smtp_host`, `smtp_port` +and `notif_from` fields filled out. You may also need to set `smtp_user`, +`smtp_pass`, and `require_transport_security`. + +If email is not configured, password reset, registration and notifications via +email will be disabled. + +### Registering a user + +The easiest way to create a new user is to do so from a client like [Element](https://element.io/). + +Alternatively, you can do so from the command line. This can be done as follows: + + 1. If synapse was installed via pip, activate the virtualenv as follows (if Synapse was + installed via a prebuilt package, `register_new_matrix_user` should already be + on the search path): + ```sh + cd ~/synapse + source env/bin/activate + synctl start # if not already running + ``` + 2. Run the following command: + ```sh + register_new_matrix_user -c homeserver.yaml http://localhost:8008 + ``` + +This will prompt you to add details for the new user, and will then connect to +the running Synapse to create the new user. For example: +``` +New user localpart: erikj +Password: +Confirm password: +Make admin [no]: +Success! +``` + +This process uses a setting `registration_shared_secret` in +`homeserver.yaml`, which is shared between Synapse itself and the +`register_new_matrix_user` script. It doesn't matter what it is (a random +value is generated by `--generate-config`), but it should be kept secret, as +anyone with knowledge of it can register users, including admin accounts, +on your server even if `enable_registration` is `false`. + +### Setting up a TURN server + +For reliable VoIP calls to be routed via this homeserver, you MUST configure +a TURN server. See +[docs/turn-howto.md](../turn-howto.md) +for details. + +### URL previews + +Synapse includes support for previewing URLs, which is disabled by default. To +turn it on you must enable the `url_preview_enabled: True` config parameter +and explicitly specify the IP ranges that Synapse is not allowed to spider for +previewing in the `url_preview_ip_range_blacklist` configuration parameter. +This is critical from a security perspective to stop arbitrary Matrix users +spidering 'internal' URLs on your network. At the very least we recommend that +your loopback and RFC1918 IP addresses are blacklisted. + +This also requires the optional `lxml` python dependency to be installed. This +in turn requires the `libxml2` library to be available - on Debian/Ubuntu this +means `apt-get install libxml2-dev`, or equivalent for your OS. + +### Troubleshooting Installation + +`pip` seems to leak *lots* of memory during installation. For instance, a Linux +host with 512MB of RAM may run out of memory whilst installing Twisted. If this +happens, you will have to individually install the dependencies which are +failing, e.g.: + +```sh +pip install twisted +``` + +If you have any other problems, feel free to ask in +[#synapse:matrix.org](https://matrix.to/#/#synapse:matrix.org). diff --git a/docs/upgrade.md b/docs/upgrade.md index 011aadf638..db0450f563 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -16,7 +16,7 @@ this document. summaries. - If Synapse was installed using [prebuilt - packages](../setup/INSTALL.md#prebuilt-packages), you will need to follow the + packages](setup/installation.md#prebuilt-packages), you will need to follow the normal process for upgrading those packages. - If Synapse was installed from source, then: From f6767abc054f3461cd9a70ba096fcf9a8e640edb Mon Sep 17 00:00:00 2001 From: Cristina Date: Thu, 8 Jul 2021 10:57:13 -0500 Subject: [PATCH 370/619] Remove functionality associated with unused historical stats tables (#9721) Fixes #9602 --- changelog.d/9721.removal | 1 + docs/room_and_user_statistics.md | 50 +-- docs/sample_config.yaml | 5 - synapse/config/stats.py | 9 - synapse/handlers/stats.py | 27 -- .../storage/databases/main/purge_events.py | 1 - synapse/storage/databases/main/stats.py | 291 +----------------- synapse/storage/schema/__init__.py | 6 +- tests/handlers/test_stats.py | 203 +----------- tests/rest/admin/test_room.py | 1 - 10 files changed, 22 insertions(+), 572 deletions(-) create mode 100644 changelog.d/9721.removal diff --git a/changelog.d/9721.removal b/changelog.d/9721.removal new file mode 100644 index 0000000000..da2ba48c84 --- /dev/null +++ b/changelog.d/9721.removal @@ -0,0 +1 @@ +Remove functionality associated with the unused `room_stats_historical` and `user_stats_historical` tables. Contributed by @xmunoz. diff --git a/docs/room_and_user_statistics.md b/docs/room_and_user_statistics.md index e1facb38d4..cc38c890bb 100644 --- a/docs/room_and_user_statistics.md +++ b/docs/room_and_user_statistics.md @@ -1,9 +1,9 @@ Room and User Statistics ======================== -Synapse maintains room and user statistics (as well as a cache of room state), -in various tables. These can be used for administrative purposes but are also -used when generating the public room directory. +Synapse maintains room and user statistics in various tables. These can be used +for administrative purposes but are also used when generating the public room +directory. # Synapse Developer Documentation @@ -15,48 +15,8 @@ used when generating the public room directory. * **subject**: Something we are tracking stats about – currently a room or user. * **current row**: An entry for a subject in the appropriate current statistics table. Each subject can have only one. -* **historical row**: An entry for a subject in the appropriate historical - statistics table. Each subject can have any number of these. ### Overview -Stats are maintained as time series. There are two kinds of column: - -* absolute columns – where the value is correct for the time given by `end_ts` - in the stats row. (Imagine a line graph for these values) - * They can also be thought of as 'gauges' in Prometheus, if you are familiar. -* per-slice columns – where the value corresponds to how many of the occurrences - occurred within the time slice given by `(end_ts − bucket_size)…end_ts` - or `start_ts…end_ts`. (Imagine a histogram for these values) - -Stats are maintained in two tables (for each type): current and historical. - -Current stats correspond to the present values. Each subject can only have one -entry. - -Historical stats correspond to values in the past. Subjects may have multiple -entries. - -## Concepts around the management of stats - -### Current rows - -Current rows contain the most up-to-date statistics for a room. -They only contain absolute columns - -### Historical rows - -Historical rows can always be considered to be valid for the time slice and -end time specified. - -* historical rows will not exist for every time slice – they will be omitted - if there were no changes. In this case, the following assumptions can be - made to interpolate/recreate missing rows: - - absolute fields have the same values as in the preceding row - - per-slice fields are zero (`0`) -* historical rows will not be retained forever – rows older than a configurable - time will be purged. - -#### Purge - -The purging of historical rows is not yet implemented. +Stats correspond to the present values. Current rows contain the most up-to-date +statistics for a room. Each subject can only have one entry. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 71463168e3..cbbe7d58d9 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -2652,11 +2652,6 @@ stats: # #enabled: false - # The size of each timeslice in the room_stats_historical and - # user_stats_historical tables, as a time period. Defaults to "1d". - # - #bucket_size: 1h - # Server Notices room configuration # diff --git a/synapse/config/stats.py b/synapse/config/stats.py index 78f61fe9da..6f253e00c0 100644 --- a/synapse/config/stats.py +++ b/synapse/config/stats.py @@ -38,13 +38,9 @@ class StatsConfig(Config): def read_config(self, config, **kwargs): self.stats_enabled = True - self.stats_bucket_size = 86400 * 1000 stats_config = config.get("stats", None) if stats_config: self.stats_enabled = stats_config.get("enabled", self.stats_enabled) - self.stats_bucket_size = self.parse_duration( - stats_config.get("bucket_size", "1d") - ) if not self.stats_enabled: logger.warning(ROOM_STATS_DISABLED_WARN) @@ -59,9 +55,4 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # correctly. # #enabled: false - - # The size of each timeslice in the room_stats_historical and - # user_stats_historical tables, as a time period. Defaults to "1d". - # - #bucket_size: 1h """ diff --git a/synapse/handlers/stats.py b/synapse/handlers/stats.py index 4e45d1da57..814d08efcb 100644 --- a/synapse/handlers/stats.py +++ b/synapse/handlers/stats.py @@ -45,7 +45,6 @@ def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.notifier = hs.get_notifier() self.is_mine_id = hs.is_mine_id - self.stats_bucket_size = hs.config.stats_bucket_size self.stats_enabled = hs.config.stats_enabled @@ -106,20 +105,6 @@ async def _unsafe_process(self) -> None: room_deltas = {} user_deltas = {} - # Then count deltas for total_events and total_event_bytes. - ( - room_count, - user_count, - ) = await self.store.get_changes_room_total_events_and_bytes( - self.pos, max_pos - ) - - for room_id, fields in room_count.items(): - room_deltas.setdefault(room_id, Counter()).update(fields) - - for user_id, fields in user_count.items(): - user_deltas.setdefault(user_id, Counter()).update(fields) - logger.debug("room_deltas: %s", room_deltas) logger.debug("user_deltas: %s", user_deltas) @@ -181,12 +166,10 @@ async def _handle_deltas( event_content = {} # type: JsonDict - sender = None if event_id is not None: event = await self.store.get_event(event_id, allow_none=True) if event: event_content = event.content or {} - sender = event.sender # All the values in this dict are deltas (RELATIVE changes) room_stats_delta = room_to_stats_deltas.setdefault(room_id, Counter()) @@ -244,12 +227,6 @@ async def _handle_deltas( room_stats_delta["joined_members"] += 1 elif membership == Membership.INVITE: room_stats_delta["invited_members"] += 1 - - if sender and self.is_mine_id(sender): - user_to_stats_deltas.setdefault(sender, Counter())[ - "invites_sent" - ] += 1 - elif membership == Membership.LEAVE: room_stats_delta["left_members"] += 1 elif membership == Membership.BAN: @@ -279,10 +256,6 @@ async def _handle_deltas( room_state["is_federatable"] = ( event_content.get("m.federate", True) is True ) - if sender and self.is_mine_id(sender): - user_to_stats_deltas.setdefault(sender, Counter())[ - "rooms_created" - ] += 1 elif typ == EventTypes.JoinRules: room_state["join_rules"] = event_content.get("join_rule") elif typ == EventTypes.RoomHistoryVisibility: diff --git a/synapse/storage/databases/main/purge_events.py b/synapse/storage/databases/main/purge_events.py index 7fb7780d0f..ec6b1eb5d4 100644 --- a/synapse/storage/databases/main/purge_events.py +++ b/synapse/storage/databases/main/purge_events.py @@ -392,7 +392,6 @@ def _purge_room_txn(self, txn, room_id: str) -> List[int]: "room_memberships", "room_stats_state", "room_stats_current", - "room_stats_historical", "room_stats_earliest_token", "rooms", "stream_ordering_to_exterm", diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index 82a1833509..b10bee6daf 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -26,7 +26,6 @@ from synapse.api.errors import StoreError from synapse.storage.database import DatabasePool from synapse.storage.databases.main.state_deltas import StateDeltasStore -from synapse.storage.engines import PostgresEngine from synapse.types import JsonDict from synapse.util.caches.descriptors import cached @@ -49,14 +48,6 @@ "user": ("joined_rooms",), } -# these fields are per-timeslice and so should be reset to 0 upon a new slice -# You can draw these stats on a histogram. -# Example: number of events sent locally during a time slice -PER_SLICE_FIELDS = { - "room": ("total_events", "total_event_bytes"), - "user": ("invites_sent", "rooms_created", "total_events", "total_event_bytes"), -} - TYPE_TO_TABLE = {"room": ("room_stats", "room_id"), "user": ("user_stats", "user_id")} # these are the tables (& ID columns) which contain our actual subjects @@ -106,7 +97,6 @@ def __init__(self, database: DatabasePool, db_conn, hs): self.server_name = hs.hostname self.clock = self.hs.get_clock() self.stats_enabled = hs.config.stats_enabled - self.stats_bucket_size = hs.config.stats_bucket_size self.stats_delta_processing_lock = DeferredLock() @@ -122,22 +112,6 @@ def __init__(self, database: DatabasePool, db_conn, hs): self.db_pool.updates.register_noop_background_update("populate_stats_cleanup") self.db_pool.updates.register_noop_background_update("populate_stats_prepare") - def quantise_stats_time(self, ts): - """ - Quantises a timestamp to be a multiple of the bucket size. - - Args: - ts (int): the timestamp to quantise, in milliseconds since the Unix - Epoch - - Returns: - int: a timestamp which - - is divisible by the bucket size; - - is no later than `ts`; and - - is the largest such timestamp. - """ - return (ts // self.stats_bucket_size) * self.stats_bucket_size - async def _populate_stats_process_users(self, progress, batch_size): """ This is a background update which regenerates statistics for users. @@ -288,56 +262,6 @@ async def update_room_state(self, room_id: str, fields: Dict[str, Any]) -> None: desc="update_room_state", ) - async def get_statistics_for_subject( - self, stats_type: str, stats_id: str, start: str, size: int = 100 - ) -> List[dict]: - """ - Get statistics for a given subject. - - Args: - stats_type: The type of subject - stats_id: The ID of the subject (e.g. room_id or user_id) - start: Pagination start. Number of entries, not timestamp. - size: How many entries to return. - - Returns: - A list of dicts, where the dict has the keys of - ABSOLUTE_STATS_FIELDS[stats_type], and "bucket_size" and "end_ts". - """ - return await self.db_pool.runInteraction( - "get_statistics_for_subject", - self._get_statistics_for_subject_txn, - stats_type, - stats_id, - start, - size, - ) - - def _get_statistics_for_subject_txn( - self, txn, stats_type, stats_id, start, size=100 - ): - """ - Transaction-bound version of L{get_statistics_for_subject}. - """ - - table, id_col = TYPE_TO_TABLE[stats_type] - selected_columns = list( - ABSOLUTE_STATS_FIELDS[stats_type] + PER_SLICE_FIELDS[stats_type] - ) - - slice_list = self.db_pool.simple_select_list_paginate_txn( - txn, - table + "_historical", - "end_ts", - start, - size, - retcols=selected_columns + ["bucket_size", "end_ts"], - keyvalues={id_col: stats_id}, - order_direction="DESC", - ) - - return slice_list - @cached() async def get_earliest_token_for_stats( self, stats_type: str, id: str @@ -451,14 +375,10 @@ def _update_stats_delta_txn( table, id_col = TYPE_TO_TABLE[stats_type] - quantised_ts = self.quantise_stats_time(int(ts)) - end_ts = quantised_ts + self.stats_bucket_size - # Lets be paranoid and check that all the given field names are known abs_field_names = ABSOLUTE_STATS_FIELDS[stats_type] - slice_field_names = PER_SLICE_FIELDS[stats_type] for field in chain(fields.keys(), absolute_field_overrides.keys()): - if field not in abs_field_names and field not in slice_field_names: + if field not in abs_field_names: # guard against potential SQL injection dodginess raise ValueError( "%s is not a recognised field" @@ -491,20 +411,6 @@ def _update_stats_delta_txn( additive_relatives=deltas_of_absolute_fields, ) - per_slice_additive_relatives = { - key: fields.get(key, 0) for key in slice_field_names - } - self._upsert_copy_from_table_with_additive_relatives_txn( - txn=txn, - into_table=table + "_historical", - keyvalues={id_col: stats_id}, - extra_dst_insvalues={"bucket_size": self.stats_bucket_size}, - extra_dst_keyvalues={"end_ts": end_ts}, - additive_relatives=per_slice_additive_relatives, - src_table=table + "_current", - copy_columns=abs_field_names, - ) - def _upsert_with_additive_relatives_txn( self, txn, table, keyvalues, absolutes, additive_relatives ): @@ -572,201 +478,6 @@ def _upsert_with_additive_relatives_txn( current_row.update(absolutes) self.db_pool.simple_update_one_txn(txn, table, keyvalues, current_row) - def _upsert_copy_from_table_with_additive_relatives_txn( - self, - txn, - into_table, - keyvalues, - extra_dst_keyvalues, - extra_dst_insvalues, - additive_relatives, - src_table, - copy_columns, - ): - """Updates the historic stats table with latest updates. - - This involves copying "absolute" fields from the `_current` table, and - adding relative fields to any existing values. - - Args: - txn: Transaction - into_table (str): The destination table to UPSERT the row into - keyvalues (dict[str, any]): Row-identifying key values - extra_dst_keyvalues (dict[str, any]): Additional keyvalues - for `into_table`. - extra_dst_insvalues (dict[str, any]): Additional values to insert - on new row creation for `into_table`. - additive_relatives (dict[str, any]): Fields that will be added onto - if existing row present. (Must be disjoint from copy_columns.) - src_table (str): The source table to copy from - copy_columns (iterable[str]): The list of columns to copy - """ - if self.database_engine.can_native_upsert: - ins_columns = chain( - keyvalues, - copy_columns, - additive_relatives, - extra_dst_keyvalues, - extra_dst_insvalues, - ) - sel_exprs = chain( - keyvalues, - copy_columns, - ( - "?" - for _ in chain( - additive_relatives, extra_dst_keyvalues, extra_dst_insvalues - ) - ), - ) - keyvalues_where = ("%s = ?" % f for f in keyvalues) - - sets_cc = ("%s = EXCLUDED.%s" % (f, f) for f in copy_columns) - sets_ar = ( - "%s = EXCLUDED.%s + %s.%s" % (f, f, into_table, f) - for f in additive_relatives - ) - - sql = """ - INSERT INTO %(into_table)s (%(ins_columns)s) - SELECT %(sel_exprs)s - FROM %(src_table)s - WHERE %(keyvalues_where)s - ON CONFLICT (%(keyvalues)s) - DO UPDATE SET %(sets)s - """ % { - "into_table": into_table, - "ins_columns": ", ".join(ins_columns), - "sel_exprs": ", ".join(sel_exprs), - "keyvalues_where": " AND ".join(keyvalues_where), - "src_table": src_table, - "keyvalues": ", ".join( - chain(keyvalues.keys(), extra_dst_keyvalues.keys()) - ), - "sets": ", ".join(chain(sets_cc, sets_ar)), - } - - qargs = list( - chain( - additive_relatives.values(), - extra_dst_keyvalues.values(), - extra_dst_insvalues.values(), - keyvalues.values(), - ) - ) - txn.execute(sql, qargs) - else: - self.database_engine.lock_table(txn, into_table) - src_row = self.db_pool.simple_select_one_txn( - txn, src_table, keyvalues, copy_columns - ) - all_dest_keyvalues = {**keyvalues, **extra_dst_keyvalues} - dest_current_row = self.db_pool.simple_select_one_txn( - txn, - into_table, - keyvalues=all_dest_keyvalues, - retcols=list(chain(additive_relatives.keys(), copy_columns)), - allow_none=True, - ) - - if dest_current_row is None: - merged_dict = { - **keyvalues, - **extra_dst_keyvalues, - **extra_dst_insvalues, - **src_row, - **additive_relatives, - } - self.db_pool.simple_insert_txn(txn, into_table, merged_dict) - else: - for (key, val) in additive_relatives.items(): - src_row[key] = dest_current_row[key] + val - self.db_pool.simple_update_txn( - txn, into_table, all_dest_keyvalues, src_row - ) - - async def get_changes_room_total_events_and_bytes( - self, min_pos: int, max_pos: int - ) -> Tuple[Dict[str, Dict[str, int]], Dict[str, Dict[str, int]]]: - """Fetches the counts of events in the given range of stream IDs. - - Args: - min_pos - max_pos - - Returns: - Mapping of room ID to field changes. - """ - - return await self.db_pool.runInteraction( - "stats_incremental_total_events_and_bytes", - self.get_changes_room_total_events_and_bytes_txn, - min_pos, - max_pos, - ) - - def get_changes_room_total_events_and_bytes_txn( - self, txn, low_pos: int, high_pos: int - ) -> Tuple[Dict[str, Dict[str, int]], Dict[str, Dict[str, int]]]: - """Gets the total_events and total_event_bytes counts for rooms and - senders, in a range of stream_orderings (including backfilled events). - - Args: - txn - low_pos: Low stream ordering - high_pos: High stream ordering - - Returns: - The room and user deltas for total_events/total_event_bytes in the - format of `stats_id` -> fields - """ - - if low_pos >= high_pos: - # nothing to do here. - return {}, {} - - if isinstance(self.database_engine, PostgresEngine): - new_bytes_expression = "OCTET_LENGTH(json)" - else: - new_bytes_expression = "LENGTH(CAST(json AS BLOB))" - - sql = """ - SELECT events.room_id, COUNT(*) AS new_events, SUM(%s) AS new_bytes - FROM events INNER JOIN event_json USING (event_id) - WHERE (? < stream_ordering AND stream_ordering <= ?) - OR (? <= stream_ordering AND stream_ordering <= ?) - GROUP BY events.room_id - """ % ( - new_bytes_expression, - ) - - txn.execute(sql, (low_pos, high_pos, -high_pos, -low_pos)) - - room_deltas = { - room_id: {"total_events": new_events, "total_event_bytes": new_bytes} - for room_id, new_events, new_bytes in txn - } - - sql = """ - SELECT events.sender, COUNT(*) AS new_events, SUM(%s) AS new_bytes - FROM events INNER JOIN event_json USING (event_id) - WHERE (? < stream_ordering AND stream_ordering <= ?) - OR (? <= stream_ordering AND stream_ordering <= ?) - GROUP BY events.sender - """ % ( - new_bytes_expression, - ) - - txn.execute(sql, (low_pos, high_pos, -high_pos, -low_pos)) - - user_deltas = { - user_id: {"total_events": new_events, "total_event_bytes": new_bytes} - for user_id, new_events, new_bytes in txn - if self.hs.is_mine_id(user_id) - } - - return room_deltas, user_deltas - async def _calculate_and_set_initial_state_for_room( self, room_id: str ) -> Tuple[dict, dict, int]: diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index 0a53b73ccc..36340a652a 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -SCHEMA_VERSION = 60 +SCHEMA_VERSION = 61 """Represents the expectations made by the codebase about the database schema This should be incremented whenever the codebase changes its requirements on the @@ -21,6 +21,10 @@ See `README.md `_ for more information on how this works. + +Changes in SCHEMA_VERSION = 61: + - The `user_stats_historical` and `room_stats_historical` tables are not written and + are not read (previously, they were written but not read). """ diff --git a/tests/handlers/test_stats.py b/tests/handlers/test_stats.py index c9d4fd9336..e4059acda3 100644 --- a/tests/handlers/test_stats.py +++ b/tests/handlers/test_stats.py @@ -88,16 +88,12 @@ async def get_all_room_state(self): def _get_current_stats(self, stats_type, stat_id): table, id_col = stats.TYPE_TO_TABLE[stats_type] - cols = list(stats.ABSOLUTE_STATS_FIELDS[stats_type]) + list( - stats.PER_SLICE_FIELDS[stats_type] - ) - - end_ts = self.store.quantise_stats_time(self.reactor.seconds() * 1000) + cols = list(stats.ABSOLUTE_STATS_FIELDS[stats_type]) return self.get_success( self.store.db_pool.simple_select_one( - table + "_historical", - {id_col: stat_id, end_ts: end_ts}, + table + "_current", + {id_col: stat_id}, cols, allow_none=True, ) @@ -156,115 +152,6 @@ def test_initial_room(self): self.assertEqual(len(r), 1) self.assertEqual(r[0]["topic"], "foo") - def test_initial_earliest_token(self): - """ - Ingestion via notify_new_event will ignore tokens that the background - update have already processed. - """ - - self.reactor.advance(86401) - - self.hs.config.stats_enabled = False - self.handler.stats_enabled = False - - u1 = self.register_user("u1", "pass") - u1_token = self.login("u1", "pass") - - u2 = self.register_user("u2", "pass") - u2_token = self.login("u2", "pass") - - u3 = self.register_user("u3", "pass") - u3_token = self.login("u3", "pass") - - room_1 = self.helper.create_room_as(u1, tok=u1_token) - self.helper.send_state( - room_1, event_type="m.room.topic", body={"topic": "foo"}, tok=u1_token - ) - - # Begin the ingestion by creating the temp tables. This will also store - # the position that the deltas should begin at, once they take over. - self.hs.config.stats_enabled = True - self.handler.stats_enabled = True - self.store.db_pool.updates._all_done = False - self.get_success( - self.store.db_pool.simple_update_one( - table="stats_incremental_position", - keyvalues={}, - updatevalues={"stream_id": 0}, - ) - ) - - self.get_success( - self.store.db_pool.simple_insert( - "background_updates", - {"update_name": "populate_stats_prepare", "progress_json": "{}"}, - ) - ) - - while not self.get_success( - self.store.db_pool.updates.has_completed_background_updates() - ): - self.get_success( - self.store.db_pool.updates.do_next_background_update(100), by=0.1 - ) - - # Now, before the table is actually ingested, add some more events. - self.helper.invite(room=room_1, src=u1, targ=u2, tok=u1_token) - self.helper.join(room=room_1, user=u2, tok=u2_token) - - # orig_delta_processor = self.store. - - # Now do the initial ingestion. - self.get_success( - self.store.db_pool.simple_insert( - "background_updates", - {"update_name": "populate_stats_process_rooms", "progress_json": "{}"}, - ) - ) - self.get_success( - self.store.db_pool.simple_insert( - "background_updates", - { - "update_name": "populate_stats_cleanup", - "progress_json": "{}", - "depends_on": "populate_stats_process_rooms", - }, - ) - ) - - self.store.db_pool.updates._all_done = False - while not self.get_success( - self.store.db_pool.updates.has_completed_background_updates() - ): - self.get_success( - self.store.db_pool.updates.do_next_background_update(100), by=0.1 - ) - - self.reactor.advance(86401) - - # Now add some more events, triggering ingestion. Because of the stream - # position being set to before the events sent in the middle, a simpler - # implementation would reprocess those events, and say there were four - # users, not three. - self.helper.invite(room=room_1, src=u1, targ=u3, tok=u1_token) - self.helper.join(room=room_1, user=u3, tok=u3_token) - - # self.handler.notify_new_event() - - # We need to let the delta processor advance… - self.reactor.advance(10 * 60) - - # Get the slices! There should be two -- day 1, and day 2. - r = self.get_success(self.store.get_statistics_for_subject("room", room_1, 0)) - - self.assertEqual(len(r), 2) - - # The oldest has 2 joined members - self.assertEqual(r[-1]["joined_members"], 2) - - # The newest has 3 - self.assertEqual(r[0]["joined_members"], 3) - def test_create_user(self): """ When we create a user, it should have statistics already ready. @@ -296,22 +183,6 @@ def test_create_room(self): self.assertIsNotNone(r1stats) self.assertIsNotNone(r2stats) - # contains the default things you'd expect in a fresh room - self.assertEqual( - r1stats["total_events"], - EXPT_NUM_STATE_EVTS_IN_FRESH_PUBLIC_ROOM, - "Wrong number of total_events in new room's stats!" - " You may need to update this if more state events are added to" - " the room creation process.", - ) - self.assertEqual( - r2stats["total_events"], - EXPT_NUM_STATE_EVTS_IN_FRESH_PRIVATE_ROOM, - "Wrong number of total_events in new room's stats!" - " You may need to update this if more state events are added to" - " the room creation process.", - ) - self.assertEqual( r1stats["current_state_events"], EXPT_NUM_STATE_EVTS_IN_FRESH_PUBLIC_ROOM ) @@ -327,24 +198,6 @@ def test_create_room(self): self.assertEqual(r2stats["invited_members"], 0) self.assertEqual(r2stats["banned_members"], 0) - def test_send_message_increments_total_events(self): - """ - When we send a message, it increments total_events. - """ - - self._perform_background_initial_update() - - u1 = self.register_user("u1", "pass") - u1token = self.login("u1", "pass") - r1 = self.helper.create_room_as(u1, tok=u1token) - r1stats_ante = self._get_current_stats("room", r1) - - self.helper.send(r1, "hiss", tok=u1token) - - r1stats_post = self._get_current_stats("room", r1) - - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) - def test_updating_profile_information_does_not_increase_joined_members_count(self): """ Check that the joined_members count does not increase when a user changes their @@ -378,7 +231,7 @@ def test_updating_profile_information_does_not_increase_joined_members_count(sel def test_send_state_event_nonoverwriting(self): """ - When we send a non-overwriting state event, it increments total_events AND current_state_events + When we send a non-overwriting state event, it increments current_state_events """ self._perform_background_initial_update() @@ -399,44 +252,14 @@ def test_send_state_event_nonoverwriting(self): r1stats_post = self._get_current_stats("room", r1) - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) self.assertEqual( r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], 1, ) - def test_send_state_event_overwriting(self): - """ - When we send an overwriting state event, it increments total_events ONLY - """ - - self._perform_background_initial_update() - - u1 = self.register_user("u1", "pass") - u1token = self.login("u1", "pass") - r1 = self.helper.create_room_as(u1, tok=u1token) - - self.helper.send_state( - r1, "cat.hissing", {"value": True}, tok=u1token, state_key="tabby" - ) - - r1stats_ante = self._get_current_stats("room", r1) - - self.helper.send_state( - r1, "cat.hissing", {"value": False}, tok=u1token, state_key="tabby" - ) - - r1stats_post = self._get_current_stats("room", r1) - - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) - self.assertEqual( - r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], - 0, - ) - def test_join_first_time(self): """ - When a user joins a room for the first time, total_events, current_state_events and + When a user joins a room for the first time, current_state_events and joined_members should increase by exactly 1. """ @@ -455,7 +278,6 @@ def test_join_first_time(self): r1stats_post = self._get_current_stats("room", r1) - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) self.assertEqual( r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], 1, @@ -466,7 +288,7 @@ def test_join_first_time(self): def test_join_after_leave(self): """ - When a user joins a room after being previously left, total_events and + When a user joins a room after being previously left, joined_members should increase by exactly 1. current_state_events should not increase. left_members should decrease by exactly 1. @@ -490,7 +312,6 @@ def test_join_after_leave(self): r1stats_post = self._get_current_stats("room", r1) - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) self.assertEqual( r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], 0, @@ -504,7 +325,7 @@ def test_join_after_leave(self): def test_invited(self): """ - When a user invites another user, current_state_events, total_events and + When a user invites another user, current_state_events and invited_members should increase by exactly 1. """ @@ -522,7 +343,6 @@ def test_invited(self): r1stats_post = self._get_current_stats("room", r1) - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) self.assertEqual( r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], 1, @@ -533,7 +353,7 @@ def test_invited(self): def test_join_after_invite(self): """ - When a user joins a room after being invited, total_events and + When a user joins a room after being invited and joined_members should increase by exactly 1. current_state_events should not increase. invited_members should decrease by exactly 1. @@ -556,7 +376,6 @@ def test_join_after_invite(self): r1stats_post = self._get_current_stats("room", r1) - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) self.assertEqual( r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], 0, @@ -570,7 +389,7 @@ def test_join_after_invite(self): def test_left(self): """ - When a user leaves a room after joining, total_events and + When a user leaves a room after joining and left_members should increase by exactly 1. current_state_events should not increase. joined_members should decrease by exactly 1. @@ -593,7 +412,6 @@ def test_left(self): r1stats_post = self._get_current_stats("room", r1) - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) self.assertEqual( r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], 0, @@ -607,7 +425,7 @@ def test_left(self): def test_banned(self): """ - When a user is banned from a room after joining, total_events and + When a user is banned from a room after joining and left_members should increase by exactly 1. current_state_events should not increase. banned_members should decrease by exactly 1. @@ -630,7 +448,6 @@ def test_banned(self): r1stats_post = self._get_current_stats("room", r1) - self.assertEqual(r1stats_post["total_events"] - r1stats_ante["total_events"], 1) self.assertEqual( r1stats_post["current_state_events"] - r1stats_ante["current_state_events"], 0, diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index ee071c2477..959d3cea77 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -1753,7 +1753,6 @@ def test_not_enough_power(self): "room_memberships", "room_stats_state", "room_stats_current", - "room_stats_historical", "room_stats_earliest_token", "rooms", "stream_ordering_to_exterm", From 33ae301fee3aac6fec492b8238899cac22e3908d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 8 Jul 2021 18:16:30 +0200 Subject: [PATCH 371/619] Fix formatting in the logcontext doc (#10337) --- changelog.d/10337.doc | 1 + docs/log_contexts.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10337.doc diff --git a/changelog.d/10337.doc b/changelog.d/10337.doc new file mode 100644 index 0000000000..f305bdb3ba --- /dev/null +++ b/changelog.d/10337.doc @@ -0,0 +1 @@ +Fix formatting in the logcontext documentation. diff --git a/docs/log_contexts.md b/docs/log_contexts.md index fe30ca2791..9a43d46091 100644 --- a/docs/log_contexts.md +++ b/docs/log_contexts.md @@ -17,7 +17,7 @@ class). Deferreds make the whole thing complicated, so this document describes how it all works, and how to write code which follows the rules. -##Logcontexts without Deferreds +## Logcontexts without Deferreds In the absence of any Deferred voodoo, things are simple enough. As with any code of this nature, the rule is that our function should leave From d26094e92cace20525552e5a0c8b21ff9ce53f11 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 8 Jul 2021 20:25:59 -0500 Subject: [PATCH 372/619] Add base starting insertion event when no chunk ID is provided (MSC2716) (#10250) * Add base starting insertion point when no chunk ID is provided This is so we can have the marker event point to this initial insertion event and be able to traverse the events in the first chunk. --- changelog.d/10250.bugfix | 1 + synapse/handlers/message.py | 8 +++ synapse/rest/client/v1/room.py | 112 ++++++++++++++++++++++++++------- 3 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 changelog.d/10250.bugfix diff --git a/changelog.d/10250.bugfix b/changelog.d/10250.bugfix new file mode 100644 index 0000000000..a8107dafb2 --- /dev/null +++ b/changelog.d/10250.bugfix @@ -0,0 +1 @@ +Add base starting insertion event when no chunk ID is specified in the historical batch send API. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index b960e18c4c..e06655f3d4 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -518,6 +518,9 @@ async def create_event( outlier: Indicates whether the event is an `outlier`, i.e. if it's from an arbitrary point and floating in the DAG as opposed to being inline with the current DAG. + historical: Indicates whether the message is being inserted + back in time around some existing events. This is used to skip + a few checks and mark the event as backfilled. depth: Override the depth used to order the event in the DAG. Should normally be set to None, which will cause the depth to be calculated based on the prev_events. @@ -772,6 +775,7 @@ async def create_and_send_nonmember_event( txn_id: Optional[str] = None, ignore_shadow_ban: bool = False, outlier: bool = False, + historical: bool = False, depth: Optional[int] = None, ) -> Tuple[EventBase, int]: """ @@ -799,6 +803,9 @@ async def create_and_send_nonmember_event( outlier: Indicates whether the event is an `outlier`, i.e. if it's from an arbitrary point and floating in the DAG as opposed to being inline with the current DAG. + historical: Indicates whether the message is being inserted + back in time around some existing events. This is used to skip + a few checks and mark the event as backfilled. depth: Override the depth used to order the event in the DAG. Should normally be set to None, which will cause the depth to be calculated based on the prev_events. @@ -847,6 +854,7 @@ async def create_and_send_nonmember_event( prev_event_ids=prev_event_ids, auth_event_ids=auth_event_ids, outlier=outlier, + historical=historical, depth=depth, ) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 92ebe838fd..9c58e3689e 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -349,6 +349,35 @@ async def inherit_depth_from_prev_ids(self, prev_event_ids) -> int: return depth + def _create_insertion_event_dict( + self, sender: str, room_id: str, origin_server_ts: int + ): + """Creates an event dict for an "insertion" event with the proper fields + and a random chunk ID. + + Args: + sender: The event author MXID + room_id: The room ID that the event belongs to + origin_server_ts: Timestamp when the event was sent + + Returns: + Tuple of event ID and stream ordering position + """ + + next_chunk_id = random_string(8) + insertion_event = { + "type": EventTypes.MSC2716_INSERTION, + "sender": sender, + "room_id": room_id, + "content": { + EventContentFields.MSC2716_NEXT_CHUNK_ID: next_chunk_id, + EventContentFields.MSC2716_HISTORICAL: True, + }, + "origin_server_ts": origin_server_ts, + } + + return insertion_event + async def on_POST(self, request, room_id): requester = await self.auth.get_user_by_req(request, allow_guest=False) @@ -449,37 +478,68 @@ async def on_POST(self, request, room_id): events_to_create = body["events"] - # If provided, connect the chunk to the last insertion point - # The chunk ID passed in comes from the chunk_id in the - # "insertion" event from the previous chunk. + prev_event_ids = prev_events_from_query + inherited_depth = await self.inherit_depth_from_prev_ids(prev_events_from_query) + + # Figure out which chunk to connect to. If they passed in + # chunk_id_from_query let's use it. The chunk ID passed in comes + # from the chunk_id in the "insertion" event from the previous chunk. + last_event_in_chunk = events_to_create[-1] + chunk_id_to_connect_to = chunk_id_from_query + base_insertion_event = None if chunk_id_from_query: - last_event_in_chunk = events_to_create[-1] - last_event_in_chunk["content"][ - EventContentFields.MSC2716_CHUNK_ID - ] = chunk_id_from_query + # TODO: Verify the chunk_id_from_query corresponds to an insertion event + pass + # Otherwise, create an insertion event to act as a starting point. + # + # We don't always have an insertion event to start hanging more history + # off of (ideally there would be one in the main DAG, but that's not the + # case if we're wanting to add history to e.g. existing rooms without + # an insertion event), in which case we just create a new insertion event + # that can then get pointed to by a "marker" event later. + else: + base_insertion_event_dict = self._create_insertion_event_dict( + sender=requester.user.to_string(), + room_id=room_id, + origin_server_ts=last_event_in_chunk["origin_server_ts"], + ) + base_insertion_event_dict["prev_events"] = prev_event_ids.copy() - # Add an "insertion" event to the start of each chunk (next to the oldest + ( + base_insertion_event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + requester, + base_insertion_event_dict, + prev_event_ids=base_insertion_event_dict.get("prev_events"), + auth_event_ids=auth_event_ids, + historical=True, + depth=inherited_depth, + ) + + chunk_id_to_connect_to = base_insertion_event["content"][ + EventContentFields.MSC2716_NEXT_CHUNK_ID + ] + + # Connect this current chunk to the insertion event from the previous chunk + last_event_in_chunk["content"][ + EventContentFields.MSC2716_CHUNK_ID + ] = chunk_id_to_connect_to + + # Add an "insertion" event to the start of each chunk (next to the oldest-in-time # event in the chunk) so the next chunk can be connected to this one. - next_chunk_id = random_string(64) - insertion_event = { - "type": EventTypes.MSC2716_INSERTION, - "sender": requester.user.to_string(), - "content": { - EventContentFields.MSC2716_NEXT_CHUNK_ID: next_chunk_id, - EventContentFields.MSC2716_HISTORICAL: True, - }, + insertion_event = self._create_insertion_event_dict( + sender=requester.user.to_string(), + room_id=room_id, # Since the insertion event is put at the start of the chunk, - # where the oldest event is, copy the origin_server_ts from + # where the oldest-in-time event is, copy the origin_server_ts from # the first event we're inserting - "origin_server_ts": events_to_create[0]["origin_server_ts"], - } + origin_server_ts=events_to_create[0]["origin_server_ts"], + ) # Prepend the insertion event to the start of the chunk events_to_create = [insertion_event] + events_to_create - inherited_depth = await self.inherit_depth_from_prev_ids(prev_events_from_query) - event_ids = [] - prev_event_ids = prev_events_from_query events_to_persist = [] for ev in events_to_create: assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"]) @@ -533,10 +593,16 @@ async def on_POST(self, request, room_id): context=context, ) + # Add the base_insertion_event to the bottom of the list we return + if base_insertion_event is not None: + event_ids.append(base_insertion_event.event_id) + return 200, { "state_events": auth_event_ids, "events": event_ids, - "next_chunk_id": next_chunk_id, + "next_chunk_id": insertion_event["content"][ + EventContentFields.MSC2716_NEXT_CHUNK_ID + ], } def on_GET(self, request, room_id): From 1579fdd54a9aab6b65ddb8de4e83b61c3384e2fe Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 10:16:54 +0100 Subject: [PATCH 373/619] Ensure we always drop the federation inbound lock (#10336) --- changelog.d/10336.bugfix | 1 + synapse/federation/federation_server.py | 1 + synapse/storage/databases/main/lock.py | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10336.bugfix diff --git a/changelog.d/10336.bugfix b/changelog.d/10336.bugfix new file mode 100644 index 0000000000..5e75ed3335 --- /dev/null +++ b/changelog.d/10336.bugfix @@ -0,0 +1 @@ +Fix bug where inbound federation in a room could be delayed due to not correctly dropping a lock. Introduced in v1.37.1. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index bf67d0f574..ac0f2ccfb3 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -949,6 +949,7 @@ async def _process_incoming_pdus_in_room_inner( room_id, room_version ) if not next: + await lock.release() return origin, event = next diff --git a/synapse/storage/databases/main/lock.py b/synapse/storage/databases/main/lock.py index e76188328c..774861074c 100644 --- a/synapse/storage/databases/main/lock.py +++ b/synapse/storage/databases/main/lock.py @@ -310,14 +310,25 @@ async def __aexit__( _excinst: Optional[BaseException], _exctb: Optional[TracebackType], ) -> bool: + await self.release() + + return False + + async def release(self) -> None: + """Release the lock. + + This is automatically called when using the lock as a context manager. + """ + + if self._dropped: + return + if self._looping_call.running: self._looping_call.stop() await self._store._drop_lock(self._lock_name, self._lock_key, self._token) self._dropped = True - return False - def __del__(self) -> None: if not self._dropped: # We should not be dropped without the lock being released (unless From 717a07b73fec1a63a716c3ad33f3dbd2de05b06d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 10:59:28 +0100 Subject: [PATCH 374/619] 1.38.0rc2 --- CHANGES.md | 16 ++++++++++++++++ changelog.d/10287.doc | 1 - changelog.d/10331.doc | 1 - changelog.d/10336.bugfix | 1 - synapse/__init__.py | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/10287.doc delete mode 100644 changelog.d/10331.doc delete mode 100644 changelog.d/10336.bugfix diff --git a/CHANGES.md b/CHANGES.md index c930b48b25..cd26ca3c4c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +Synapse 1.38.0rc2 (2021-07-09) +============================== + +Bugfixes +-------- + +- Fix bug where inbound federation in a room could be delayed due to not correctly dropping a lock. Introduced in v1.37.1. ([\#10336](https://github.com/matrix-org/synapse/issues/10336)) + + +Improved Documentation +---------------------- + +- Update links to documentation in sample config. Contributed by @dklimpel. ([\#10287](https://github.com/matrix-org/synapse/issues/10287)) +- Fix broken links in INSTALL.md. Contributed by @dklimpel. ([\#10331](https://github.com/matrix-org/synapse/issues/10331)) + + Synapse 1.38.0rc1 (2021-07-06) ============================== diff --git a/changelog.d/10287.doc b/changelog.d/10287.doc deleted file mode 100644 index d62afc1e15..0000000000 --- a/changelog.d/10287.doc +++ /dev/null @@ -1 +0,0 @@ -Update links to documentation in sample config. Contributed by @dklimpel. diff --git a/changelog.d/10331.doc b/changelog.d/10331.doc deleted file mode 100644 index 9b9acd9007..0000000000 --- a/changelog.d/10331.doc +++ /dev/null @@ -1 +0,0 @@ -Fix broken links in INSTALL.md. Contributed by @dklimpel. diff --git a/changelog.d/10336.bugfix b/changelog.d/10336.bugfix deleted file mode 100644 index 5e75ed3335..0000000000 --- a/changelog.d/10336.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug where inbound federation in a room could be delayed due to not correctly dropping a lock. Introduced in v1.37.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index aa9a3269c0..119afa9ebe 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.38.0rc1" +__version__ = "1.38.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 5aba3ff033106e8fba03a7ac87f1142ddb8bb64d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 11:00:20 +0100 Subject: [PATCH 375/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index cd26ca3c4c..abc27af2da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ Improved Documentation ---------------------- - Update links to documentation in sample config. Contributed by @dklimpel. ([\#10287](https://github.com/matrix-org/synapse/issues/10287)) -- Fix broken links in INSTALL.md. Contributed by @dklimpel. ([\#10331](https://github.com/matrix-org/synapse/issues/10331)) +- Fix broken links in [INSTALL.md](INSTALL.md). Contributed by @dklimpel. ([\#10331](https://github.com/matrix-org/synapse/issues/10331)) Synapse 1.38.0rc1 (2021-07-06) From e3e73e181b2f399f3acc9fd3138d1857f0492fa9 Mon Sep 17 00:00:00 2001 From: Andreas Rammhold Date: Fri, 9 Jul 2021 12:03:02 +0200 Subject: [PATCH 376/619] Upsert redactions in case they already exists (#10343) * Upsert redactions in case they already exists Occasionally, in combination with retention, redactions aren't deleted from the database whenever they are due for deletion. The server will eventually try to backfill the deleted events and trip over the already existing redaction events. Switching to an UPSERT for those events allows us to recover from there situations. The retention code still needs fixing but that is outside of my current comfort zone on this code base. This is related to #8707 where the error was discussed already. Signed-off-by: Andreas Rammhold * Also purge redactions when purging events Previously redacints where left behind leading to backfilling issues when the server stumbled across the already existing yet to be backfilled redactions. This issues has been discussed in #8707. Signed-off-by: Andreas Rammhold --- changelog.d/10343.bugfix | 1 + synapse/storage/databases/main/events.py | 4 ++-- synapse/storage/databases/main/purge_events.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10343.bugfix diff --git a/changelog.d/10343.bugfix b/changelog.d/10343.bugfix new file mode 100644 index 0000000000..53ccf79a81 --- /dev/null +++ b/changelog.d/10343.bugfix @@ -0,0 +1 @@ +Fix errors during backfill caused by previously purged redaction events. Contributed by Andreas Rammhold (@andir). diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index 897fa06639..08c580b0dc 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -1580,11 +1580,11 @@ def _store_redaction(self, txn, event): # invalidate the cache for the redacted event txn.call_after(self.store._invalidate_get_event_cache, event.redacts) - self.db_pool.simple_insert_txn( + self.db_pool.simple_upsert_txn( txn, table="redactions", + keyvalues={"event_id": event.event_id}, values={ - "event_id": event.event_id, "redacts": event.redacts, "received_ts": self._clock.time_msec(), }, diff --git a/synapse/storage/databases/main/purge_events.py b/synapse/storage/databases/main/purge_events.py index ec6b1eb5d4..eb4841830d 100644 --- a/synapse/storage/databases/main/purge_events.py +++ b/synapse/storage/databases/main/purge_events.py @@ -215,6 +215,7 @@ def _purge_history_txn( "event_relations", "event_search", "rejections", + "redactions", ): logger.info("[purge] removing events from %s", table) From 42389555c47ef56402b6abda2336074c5f78637d Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 11:07:13 +0100 Subject: [PATCH 377/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index abc27af2da..a1419d6495 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ Bugfixes Improved Documentation ---------------------- -- Update links to documentation in sample config. Contributed by @dklimpel. ([\#10287](https://github.com/matrix-org/synapse/issues/10287)) +- Update links to documentation in the sample config. Contributed by @dklimpel. ([\#10287](https://github.com/matrix-org/synapse/issues/10287)) - Fix broken links in [INSTALL.md](INSTALL.md). Contributed by @dklimpel. ([\#10331](https://github.com/matrix-org/synapse/issues/10331)) From 100686a0691054c486e747e3df37861887c6e8cb Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 11:16:50 +0100 Subject: [PATCH 378/619] Fix README rst --- README.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index e5332d62a9..2cf540b957 100644 --- a/README.rst +++ b/README.rst @@ -94,8 +94,7 @@ Synapse Installation .. _federation: -* For details on how to install synapse, see - `Installation Instructions `_. +* For details on how to install synapse, see `Installation Instructions`_. * For specific details on how to configure Synapse for federation see `docs/federate.md `_ @@ -335,8 +334,8 @@ access the API as a Matrix client would. It is able to run Synapse directly from the source tree, so installation of the server is not required. Testing with SyTest is recommended for verifying that changes related to the -Client-Server API are functioning correctly. See the `installation instructions -`_ for details. +Client-Server API are functioning correctly. See the `installation instructions`_ +for details. Platform dependencies @@ -456,3 +455,5 @@ This is normally caused by a misconfiguration in your reverse-proxy. See .. |python| image:: https://img.shields.io/pypi/pyversions/matrix-synapse :alt: (supported python versions) :target: https://pypi.org/project/matrix-synapse + +.. _installation instructions From b5d42377bf61ab306debb532d7ec62a58087351a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 11:21:41 +0100 Subject: [PATCH 379/619] Fix README rst --- README.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 2cf540b957..0ae05616e7 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,8 @@ Synapse Installation .. _federation: -* For details on how to install synapse, see `Installation Instructions`_. +* For details on how to install synapse, see + `Installation Instructions `_. * For specific details on how to configure Synapse for federation see `docs/federate.md `_ @@ -334,8 +335,8 @@ access the API as a Matrix client would. It is able to run Synapse directly from the source tree, so installation of the server is not required. Testing with SyTest is recommended for verifying that changes related to the -Client-Server API are functioning correctly. See the `installation instructions`_ -for details. +Client-Server API are functioning correctly. See the `SyTest installation +instructions `_ for details. Platform dependencies @@ -455,5 +456,3 @@ This is normally caused by a misconfiguration in your reverse-proxy. See .. |python| image:: https://img.shields.io/pypi/pyversions/matrix-synapse :alt: (supported python versions) :target: https://pypi.org/project/matrix-synapse - -.. _installation instructions From 751372fa61e28f06715d086fe5cc58d174ca1a17 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 9 Jul 2021 13:01:11 +0100 Subject: [PATCH 380/619] Switch `application_services_txns.txn_id` to BIGINT (#10349) --- changelog.d/10349.misc | 1 + .../61/01change_appservices_txns.sql.postgres | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 changelog.d/10349.misc create mode 100644 synapse/storage/schema/main/delta/61/01change_appservices_txns.sql.postgres diff --git a/changelog.d/10349.misc b/changelog.d/10349.misc new file mode 100644 index 0000000000..5b014e7416 --- /dev/null +++ b/changelog.d/10349.misc @@ -0,0 +1 @@ +Switch `application_services_txns.txn_id` database column to `BIGINT`. diff --git a/synapse/storage/schema/main/delta/61/01change_appservices_txns.sql.postgres b/synapse/storage/schema/main/delta/61/01change_appservices_txns.sql.postgres new file mode 100644 index 0000000000..c8aec78e60 --- /dev/null +++ b/synapse/storage/schema/main/delta/61/01change_appservices_txns.sql.postgres @@ -0,0 +1,23 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- we use bigint elsewhere in the database for appservice txn ids (notably, +-- application_services_state.last_txn), and generally we use bigints everywhere else +-- we have monotonic counters, so let's bring this one in line. +-- +-- assuming there aren't thousands of rows for decommisioned/non-functional ASes, this +-- table should be pretty small, so safe to do a synchronous ALTER TABLE. + +ALTER TABLE application_services_txns ALTER COLUMN txn_id SET DATA TYPE BIGINT; From ca9dface8c63ee164979fbed68693a2511c455f7 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:12:47 +0100 Subject: [PATCH 381/619] Fix the user directory becoming broken (and noisy errors being logged) when knocking and room statistics are in use. (#10344) Signed-off-by: Olivier Wilkinson (reivilibre) --- changelog.d/10344.bugfix | 1 + synapse/storage/databases/main/stats.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10344.bugfix diff --git a/changelog.d/10344.bugfix b/changelog.d/10344.bugfix new file mode 100644 index 0000000000..ab6eb4999f --- /dev/null +++ b/changelog.d/10344.bugfix @@ -0,0 +1 @@ +Fix the user directory becoming broken (and noisy errors being logged) when knocking and room statistics are in use. diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index b10bee6daf..59d67c255b 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -434,7 +434,7 @@ def _upsert_with_additive_relatives_txn( ] relative_updates = [ - "%(field)s = EXCLUDED.%(field)s + %(table)s.%(field)s" + "%(field)s = EXCLUDED.%(field)s + COALESCE(%(table)s.%(field)s, 0)" % {"table": table, "field": field} for field in additive_relatives.keys() ] @@ -474,7 +474,10 @@ def _upsert_with_additive_relatives_txn( self.db_pool.simple_insert_txn(txn, table, merged_dict) else: for (key, val) in additive_relatives.items(): - current_row[key] += val + if current_row[key] is None: + current_row[key] = val + else: + current_row[key] += val current_row.update(absolutes) self.db_pool.simple_update_one_txn(txn, table, keyvalues, current_row) @@ -604,6 +607,7 @@ def _fetch_current_state_stats(txn): "invited_members": membership_counts.get(Membership.INVITE, 0), "left_members": membership_counts.get(Membership.LEAVE, 0), "banned_members": membership_counts.get(Membership.BAN, 0), + "knocked_members": membership_counts.get(Membership.KNOCK, 0), "local_users_in_room": len(local_users_in_room), }, ) From 944428d1163d1521ef96db88040852520ad6cbff Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 14:51:37 +0100 Subject: [PATCH 382/619] Newsfile --- changelog.d/10355.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10355.bugfix diff --git a/changelog.d/10355.bugfix b/changelog.d/10355.bugfix new file mode 100644 index 0000000000..92df612011 --- /dev/null +++ b/changelog.d/10355.bugfix @@ -0,0 +1 @@ +Fix newly added `synapse_federation_server_oldest_inbound_pdu_in_staging` prometheus metric to measure age rather than timestamp. From ac036e26c6d84075870b80facc7d5f12565c6743 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 14:52:00 +0100 Subject: [PATCH 383/619] Revert "Newsfile" This reverts commit 944428d1163d1521ef96db88040852520ad6cbff. --- changelog.d/10355.bugfix | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/10355.bugfix diff --git a/changelog.d/10355.bugfix b/changelog.d/10355.bugfix deleted file mode 100644 index 92df612011..0000000000 --- a/changelog.d/10355.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix newly added `synapse_federation_server_oldest_inbound_pdu_in_staging` prometheus metric to measure age rather than timestamp. From 0f7ed3fc08d1e3302663b9407387cdf750e3804a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 9 Jul 2021 17:13:11 +0100 Subject: [PATCH 384/619] Re-enable room v6 sytest (#10345) ... now that it has been fixed in https://github.com/matrix-org/sytest/pull/1061. --- changelog.d/10345.misc | 1 + sytest-blacklist | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/10345.misc diff --git a/changelog.d/10345.misc b/changelog.d/10345.misc new file mode 100644 index 0000000000..7bfa1c4af2 --- /dev/null +++ b/changelog.d/10345.misc @@ -0,0 +1 @@ +Re-enable a Sytest that was disabled for the 1.37.1 release. diff --git a/sytest-blacklist b/sytest-blacklist index 566ef96711..73c4aa76a2 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -44,5 +44,4 @@ Peeked rooms only turn up in the sync for the device who peeked them # Blacklisted due to changes made in #10272 -Outbound federation will ignore a missing event with bad JSON for room version 6 Federation rejects inbound events where the prev_events cannot be found From 8eddbde0e23b8cb596cd20282779d9cc58f9357c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 9 Jul 2021 17:51:15 +0100 Subject: [PATCH 385/619] Unblacklist fixed tests (#10357) --- changelog.d/10345.misc | 2 +- changelog.d/10357.misc | 1 + sytest-blacklist | 4 ---- 3 files changed, 2 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10357.misc diff --git a/changelog.d/10345.misc b/changelog.d/10345.misc index 7bfa1c4af2..7424486e87 100644 --- a/changelog.d/10345.misc +++ b/changelog.d/10345.misc @@ -1 +1 @@ -Re-enable a Sytest that was disabled for the 1.37.1 release. +Re-enable Sytests that were disabled for the 1.37.1 release. diff --git a/changelog.d/10357.misc b/changelog.d/10357.misc new file mode 100644 index 0000000000..7424486e87 --- /dev/null +++ b/changelog.d/10357.misc @@ -0,0 +1 @@ +Re-enable Sytests that were disabled for the 1.37.1 release. diff --git a/sytest-blacklist b/sytest-blacklist index 73c4aa76a2..de9986357b 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -41,7 +41,3 @@ We can't peek into rooms with invited history_visibility We can't peek into rooms with joined history_visibility Local users can peek by room alias Peeked rooms only turn up in the sync for the device who peeked them - - -# Blacklisted due to changes made in #10272 -Federation rejects inbound events where the prev_events cannot be found From 19d0401c56a8f31441c65e62ffd688f615536d76 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 12 Jul 2021 11:21:04 -0400 Subject: [PATCH 386/619] Additional unit tests for spaces summary. (#10305) --- changelog.d/10305.misc | 1 + tests/handlers/test_space_summary.py | 204 ++++++++++++++++++++++++++- 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10305.misc diff --git a/changelog.d/10305.misc b/changelog.d/10305.misc new file mode 100644 index 0000000000..8488d47f6f --- /dev/null +++ b/changelog.d/10305.misc @@ -0,0 +1 @@ +Additional unit tests for the spaces summary API. diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 9771d3fb3b..faed1f1a18 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -14,7 +14,7 @@ from typing import Any, Iterable, Optional, Tuple from unittest import mock -from synapse.api.constants import EventContentFields, RoomTypes +from synapse.api.constants import EventContentFields, JoinRules, RoomTypes from synapse.api.errors import AuthError from synapse.handlers.space_summary import _child_events_comparison_key from synapse.rest import admin @@ -178,3 +178,205 @@ def test_world_readable(self): result = self.get_success(self.handler.get_space_summary(user2, self.space)) self._assert_rooms(result, [self.space]) self._assert_events(result, [(self.space, self.room)]) + + def test_complex_space(self): + """ + Create a "complex" space to see how it handles things like loops and subspaces. + """ + # Create an inaccessible room. + user2 = self.register_user("user2", "pass") + token2 = self.login("user2", "pass") + room2 = self.helper.create_room_as(user2, tok=token2) + # This is a bit odd as "user" is adding a room they don't know about, but + # it works for the tests. + self._add_child(self.space, room2, self.token) + + # Create a subspace under the space with an additional room in it. + subspace = self.helper.create_room_as( + self.user, + tok=self.token, + extra_content={ + "creation_content": {EventContentFields.ROOM_TYPE: RoomTypes.SPACE} + }, + ) + subroom = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(self.space, subspace, token=self.token) + self._add_child(subspace, subroom, token=self.token) + # Also add the two rooms from the space into this subspace (causing loops). + self._add_child(subspace, self.room, token=self.token) + self._add_child(subspace, room2, self.token) + + result = self.get_success(self.handler.get_space_summary(self.user, self.space)) + + # The result should include each room a single time and each link. + self._assert_rooms(result, [self.space, self.room, subspace, subroom]) + self._assert_events( + result, + [ + (self.space, self.room), + (self.space, room2), + (self.space, subspace), + (subspace, subroom), + (subspace, self.room), + (subspace, room2), + ], + ) + + def test_fed_complex(self): + """ + Return data over federation and ensure that it is handled properly. + """ + fed_hostname = self.hs.hostname + "2" + subspace = "#subspace:" + fed_hostname + subroom = "#subroom:" + fed_hostname + + async def summarize_remote_room( + _self, room, suggested_only, max_children, exclude_rooms + ): + # Return some good data, and some bad data: + # + # * Event *back* to the root room. + # * Unrelated events / rooms + # * Multiple levels of events (in a not-useful order, e.g. grandchild + # events before child events). + + # Note that these entries are brief, but should contain enough info. + rooms = [ + { + "room_id": subspace, + "world_readable": True, + "room_type": RoomTypes.SPACE, + }, + { + "room_id": subroom, + "world_readable": True, + }, + ] + event_content = {"via": [fed_hostname]} + events = [ + { + "room_id": subspace, + "state_key": subroom, + "content": event_content, + }, + ] + return rooms, events + + # Add a room to the space which is on another server. + self._add_child(self.space, subspace, self.token) + + with mock.patch( + "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room", + new=summarize_remote_room, + ): + result = self.get_success( + self.handler.get_space_summary(self.user, self.space) + ) + + self._assert_rooms(result, [self.space, self.room, subspace, subroom]) + self._assert_events( + result, + [ + (self.space, self.room), + (self.space, subspace), + (subspace, subroom), + ], + ) + + def test_fed_filtering(self): + """ + Rooms returned over federation should be properly filtered to only include + rooms the user has access to. + """ + fed_hostname = self.hs.hostname + "2" + subspace = "#subspace:" + fed_hostname + + # Create a few rooms which will have different properties. + restricted_room = "#restricted:" + fed_hostname + restricted_accessible_room = "#restricted_accessible:" + fed_hostname + world_readable_room = "#world_readable:" + fed_hostname + joined_room = self.helper.create_room_as(self.user, tok=self.token) + + async def summarize_remote_room( + _self, room, suggested_only, max_children, exclude_rooms + ): + # Note that these entries are brief, but should contain enough info. + rooms = [ + { + "room_id": restricted_room, + "world_readable": False, + "join_rules": JoinRules.MSC3083_RESTRICTED, + "allowed_spaces": [], + }, + { + "room_id": restricted_accessible_room, + "world_readable": False, + "join_rules": JoinRules.MSC3083_RESTRICTED, + "allowed_spaces": [self.room], + }, + { + "room_id": world_readable_room, + "world_readable": True, + "join_rules": JoinRules.INVITE, + }, + { + "room_id": joined_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ] + + # Place each room in the sub-space. + event_content = {"via": [fed_hostname]} + events = [ + { + "room_id": subspace, + "state_key": room["room_id"], + "content": event_content, + } + for room in rooms + ] + + # Also include the subspace. + rooms.insert( + 0, + { + "room_id": subspace, + "world_readable": True, + }, + ) + return rooms, events + + # Add a room to the space which is on another server. + self._add_child(self.space, subspace, self.token) + + with mock.patch( + "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room", + new=summarize_remote_room, + ): + result = self.get_success( + self.handler.get_space_summary(self.user, self.space) + ) + + self._assert_rooms( + result, + [ + self.space, + self.room, + subspace, + restricted_accessible_room, + world_readable_room, + joined_room, + ], + ) + self._assert_events( + result, + [ + (self.space, self.room), + (self.space, subspace), + (subspace, restricted_room), + (subspace, restricted_accessible_room), + (subspace, world_readable_room), + (subspace, joined_room), + ], + ) From c2c364f27f61bece85dc7fd17cdedc4b60b9f7af Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 12 Jul 2021 17:22:54 +0100 Subject: [PATCH 387/619] Replace `room_depth.min_depth` with a BIGINT (#10289) while I'm dealing with INTEGERs and BIGINTs, let's replace room_depth.min_depth with a BIGINT. --- changelog.d/10289.misc | 1 + synapse/storage/databases/main/room.py | 104 ++++++++++++++++-- .../61/02drop_redundant_room_depth_index.sql | 18 +++ .../main/delta/61/03recreate_min_depth.py | 70 ++++++++++++ 4 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 changelog.d/10289.misc create mode 100644 synapse/storage/schema/main/delta/61/02drop_redundant_room_depth_index.sql create mode 100644 synapse/storage/schema/main/delta/61/03recreate_min_depth.py diff --git a/changelog.d/10289.misc b/changelog.d/10289.misc new file mode 100644 index 0000000000..2df30e7a7a --- /dev/null +++ b/changelog.d/10289.misc @@ -0,0 +1 @@ +Convert `room_depth.min_depth` column to a `BIGINT`. diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 9f0d64a325..6ddafe5434 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -25,6 +25,7 @@ from synapse.storage._base import SQLBaseStore, db_to_json from synapse.storage.database import DatabasePool, LoggingTransaction from synapse.storage.databases.main.search import SearchStore +from synapse.storage.types import Cursor from synapse.types import JsonDict, ThirdPartyInstanceID from synapse.util import json_encoder from synapse.util.caches.descriptors import cached @@ -1022,10 +1023,22 @@ def get_rooms_for_retention_period_in_range_txn(txn): ) -class RoomBackgroundUpdateStore(SQLBaseStore): +class _BackgroundUpdates: REMOVE_TOMESTONED_ROOMS_BG_UPDATE = "remove_tombstoned_rooms_from_directory" ADD_ROOMS_ROOM_VERSION_COLUMN = "add_rooms_room_version_column" + POPULATE_ROOM_DEPTH_MIN_DEPTH2 = "populate_room_depth_min_depth2" + REPLACE_ROOM_DEPTH_MIN_DEPTH = "replace_room_depth_min_depth" + + +_REPLACE_ROOM_DEPTH_SQL_COMMANDS = ( + "DROP TRIGGER populate_min_depth2_trigger ON room_depth", + "DROP FUNCTION populate_min_depth2()", + "ALTER TABLE room_depth DROP COLUMN min_depth", + "ALTER TABLE room_depth RENAME COLUMN min_depth2 TO min_depth", +) + +class RoomBackgroundUpdateStore(SQLBaseStore): def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) @@ -1037,15 +1050,25 @@ def __init__(self, database: DatabasePool, db_conn, hs): ) self.db_pool.updates.register_background_update_handler( - self.REMOVE_TOMESTONED_ROOMS_BG_UPDATE, + _BackgroundUpdates.REMOVE_TOMESTONED_ROOMS_BG_UPDATE, self._remove_tombstoned_rooms_from_directory, ) self.db_pool.updates.register_background_update_handler( - self.ADD_ROOMS_ROOM_VERSION_COLUMN, + _BackgroundUpdates.ADD_ROOMS_ROOM_VERSION_COLUMN, self._background_add_rooms_room_version_column, ) + # BG updates to change the type of room_depth.min_depth + self.db_pool.updates.register_background_update_handler( + _BackgroundUpdates.POPULATE_ROOM_DEPTH_MIN_DEPTH2, + self._background_populate_room_depth_min_depth2, + ) + self.db_pool.updates.register_background_update_handler( + _BackgroundUpdates.REPLACE_ROOM_DEPTH_MIN_DEPTH, + self._background_replace_room_depth_min_depth, + ) + async def _background_insert_retention(self, progress, batch_size): """Retrieves a list of all rooms within a range and inserts an entry for each of them into the room_retention table. @@ -1164,7 +1187,9 @@ def _background_add_rooms_room_version_column_txn(txn: LoggingTransaction): new_last_room_id = room_id self.db_pool.updates._background_update_progress_txn( - txn, self.ADD_ROOMS_ROOM_VERSION_COLUMN, {"room_id": new_last_room_id} + txn, + _BackgroundUpdates.ADD_ROOMS_ROOM_VERSION_COLUMN, + {"room_id": new_last_room_id}, ) return False @@ -1176,7 +1201,7 @@ def _background_add_rooms_room_version_column_txn(txn: LoggingTransaction): if end: await self.db_pool.updates._end_background_update( - self.ADD_ROOMS_ROOM_VERSION_COLUMN + _BackgroundUpdates.ADD_ROOMS_ROOM_VERSION_COLUMN ) return batch_size @@ -1215,7 +1240,7 @@ def _get_rooms(txn): if not rooms: await self.db_pool.updates._end_background_update( - self.REMOVE_TOMESTONED_ROOMS_BG_UPDATE + _BackgroundUpdates.REMOVE_TOMESTONED_ROOMS_BG_UPDATE ) return 0 @@ -1224,7 +1249,7 @@ def _get_rooms(txn): await self.set_room_is_public(room_id, False) await self.db_pool.updates._background_update_progress( - self.REMOVE_TOMESTONED_ROOMS_BG_UPDATE, {"room_id": rooms[-1]} + _BackgroundUpdates.REMOVE_TOMESTONED_ROOMS_BG_UPDATE, {"room_id": rooms[-1]} ) return len(rooms) @@ -1268,6 +1293,71 @@ async def has_auth_chain_index(self, room_id: str) -> bool: return max_ordering is None + async def _background_populate_room_depth_min_depth2( + self, progress: JsonDict, batch_size: int + ) -> int: + """Populate room_depth.min_depth2 + + This is to deal with the fact that min_depth was initially created as a + 32-bit integer field. + """ + + def process(txn: Cursor) -> int: + last_room = progress.get("last_room", "") + txn.execute( + """ + UPDATE room_depth SET min_depth2=min_depth + WHERE room_id IN ( + SELECT room_id FROM room_depth WHERE room_id > ? + ORDER BY room_id LIMIT ? + ) + RETURNING room_id; + """, + (last_room, batch_size), + ) + row_count = txn.rowcount + if row_count == 0: + return 0 + last_room = max(row[0] for row in txn) + logger.info("populated room_depth up to %s", last_room) + + self.db_pool.updates._background_update_progress_txn( + txn, + _BackgroundUpdates.POPULATE_ROOM_DEPTH_MIN_DEPTH2, + {"last_room": last_room}, + ) + return row_count + + result = await self.db_pool.runInteraction( + "_background_populate_min_depth2", process + ) + + if result != 0: + return result + + await self.db_pool.updates._end_background_update( + _BackgroundUpdates.POPULATE_ROOM_DEPTH_MIN_DEPTH2 + ) + return 0 + + async def _background_replace_room_depth_min_depth( + self, progress: JsonDict, batch_size: int + ) -> int: + """Drop the old 'min_depth' column and rename 'min_depth2' into its place.""" + + def process(txn: Cursor) -> None: + for sql in _REPLACE_ROOM_DEPTH_SQL_COMMANDS: + logger.info("completing room_depth migration: %s", sql) + txn.execute(sql) + + await self.db_pool.runInteraction("_background_replace_room_depth", process) + + await self.db_pool.updates._end_background_update( + _BackgroundUpdates.REPLACE_ROOM_DEPTH_MIN_DEPTH, + ) + + return 0 + class RoomStore(RoomBackgroundUpdateStore, RoomWorkerStore, SearchStore): def __init__(self, database: DatabasePool, db_conn, hs): diff --git a/synapse/storage/schema/main/delta/61/02drop_redundant_room_depth_index.sql b/synapse/storage/schema/main/delta/61/02drop_redundant_room_depth_index.sql new file mode 100644 index 0000000000..35ca7a40c0 --- /dev/null +++ b/synapse/storage/schema/main/delta/61/02drop_redundant_room_depth_index.sql @@ -0,0 +1,18 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- this index is redundant; there is another UNIQUE index on this table. +DROP INDEX IF EXISTS room_depth_room; + diff --git a/synapse/storage/schema/main/delta/61/03recreate_min_depth.py b/synapse/storage/schema/main/delta/61/03recreate_min_depth.py new file mode 100644 index 0000000000..f8d7db9f2e --- /dev/null +++ b/synapse/storage/schema/main/delta/61/03recreate_min_depth.py @@ -0,0 +1,70 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This migration handles the process of changing the type of `room_depth.min_depth` to +a BIGINT. +""" +from synapse.storage.engines import BaseDatabaseEngine, PostgresEngine +from synapse.storage.types import Cursor + + +def run_create(cur: Cursor, database_engine: BaseDatabaseEngine, *args, **kwargs): + if not isinstance(database_engine, PostgresEngine): + # this only applies to postgres - sqlite does not distinguish between big and + # little ints. + return + + # First add a new column to contain the bigger min_depth + cur.execute("ALTER TABLE room_depth ADD COLUMN min_depth2 BIGINT") + + # Create a trigger which will keep it populated. + cur.execute( + """ + CREATE OR REPLACE FUNCTION populate_min_depth2() RETURNS trigger AS $BODY$ + BEGIN + new.min_depth2 := new.min_depth; + RETURN NEW; + END; + $BODY$ LANGUAGE plpgsql + """ + ) + + cur.execute( + """ + CREATE TRIGGER populate_min_depth2_trigger BEFORE INSERT OR UPDATE ON room_depth + FOR EACH ROW + EXECUTE PROCEDURE populate_min_depth2() + """ + ) + + # Start a bg process to populate it for old rooms + cur.execute( + """ + INSERT INTO background_updates (ordering, update_name, progress_json) VALUES + (6103, 'populate_room_depth_min_depth2', '{}') + """ + ) + + # and another to switch them over once it completes. + cur.execute( + """ + INSERT INTO background_updates (ordering, update_name, progress_json, depends_on) VALUES + (6103, 'replace_room_depth_min_depth', '{}', 'populate_room_depth2') + """ + ) + + +def run_upgrade(cur: Cursor, database_engine: BaseDatabaseEngine, *args, **kwargs): + pass From 5f2848f379ebefd100fc0a1ac5a034f447137f60 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 12 Jul 2021 19:03:14 +0100 Subject: [PATCH 388/619] build debs in GHA (#10247) GHA workflow to build the debs --- .github/workflows/debs.yml | 44 +++++++++++++++++++++++++++++++ changelog.d/10247.misc | 1 + scripts-dev/build_debian_packages | 17 +++++++++--- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/debs.yml create mode 100644 changelog.d/10247.misc diff --git a/.github/workflows/debs.yml b/.github/workflows/debs.yml new file mode 100644 index 0000000000..e03a419426 --- /dev/null +++ b/.github/workflows/debs.yml @@ -0,0 +1,44 @@ +# GitHub actions workflow which builds the debian packages. + +name: Debs + +on: + push: + branches: ["develop", "release-*"] + +permissions: + contents: read + +jobs: + # first get the list of distros to build for. + get-distros: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - id: set-distros + run: | + echo "::set-output name=distros::$(scripts-dev/build_debian_packages --show-dists-json)" + # map the step outputs to job outputs + outputs: + distros: ${{ steps.set-distros.outputs.distros }} + + # now build the packages with a matrix build. + build-debs: + needs: get-distros + name: "Build .deb packages" + runs-on: ubuntu-latest + strategy: + matrix: + distro: ${{ fromJson(needs.get-distros.outputs.distros) }} + + steps: + - uses: actions/checkout@v2 + with: + path: src + - uses: actions/setup-python@v2 + - run: ./src/scripts-dev/build_debian_packages "${{ matrix.distro }}" + - uses: actions/upload-artifact@v2 + with: + name: packages + path: debs/* diff --git a/changelog.d/10247.misc b/changelog.d/10247.misc new file mode 100644 index 0000000000..5824907bca --- /dev/null +++ b/changelog.d/10247.misc @@ -0,0 +1 @@ +Build the Debian packages in CI. diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index 546724f89f..e25c5bb265 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -10,6 +10,7 @@ # can be passed on the commandline for debugging. import argparse +import json import os import signal import subprocess @@ -34,6 +35,8 @@ By default, builds for all known distributions, but a list of distributions can be passed on the commandline for debugging. """ +projdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + class Builder(object): def __init__(self, redirect_stdout=False): @@ -57,9 +60,6 @@ class Builder(object): raise def _inner_build(self, dist, skip_tests=False): - projdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - os.chdir(projdir) - tag = dist.split(":", 1)[1] # Make the dir where the debs will live. @@ -93,6 +93,7 @@ class Builder(object): ], stdout=stdout, stderr=subprocess.STDOUT, + cwd=projdir, ) container_name = "synapse_build_" + tag @@ -179,6 +180,11 @@ if __name__ == "__main__": action="store_true", help="skip running tests after building", ) + parser.add_argument( + "--show-dists-json", + action="store_true", + help="instead of building the packages, just list the dists to build for, as a json array", + ) parser.add_argument( "dist", nargs="*", @@ -186,4 +192,7 @@ if __name__ == "__main__": help="a list of distributions to build for. Default: %(default)s", ) args = parser.parse_args() - run_builds(dists=args.dist, jobs=args.jobs, skip_tests=args.no_check) + if args.show_dists_json: + print(json.dumps(DISTS)) + else: + run_builds(dists=args.dist, jobs=args.jobs, skip_tests=args.no_check) From ae81ec428d4fc0600b5cc06df2c2b8cb696d43c9 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 13 Jul 2021 00:20:11 +0100 Subject: [PATCH 389/619] Build the python release artifacts in GHA too --- .../{debs.yml => release-artifacts.yml} | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) rename .github/workflows/{debs.yml => release-artifacts.yml} (59%) diff --git a/.github/workflows/debs.yml b/.github/workflows/release-artifacts.yml similarity index 59% rename from .github/workflows/debs.yml rename to .github/workflows/release-artifacts.yml index e03a419426..9d1fb89834 100644 --- a/.github/workflows/debs.yml +++ b/.github/workflows/release-artifacts.yml @@ -1,11 +1,17 @@ -# GitHub actions workflow which builds the debian packages. +# GitHub actions workflow which builds the release artifacts. -name: Debs +name: Build release artifacts on: push: + # we build on develop and release branches to (hopefully) get early warning + # of things breaking branches: ["develop", "release-*"] + # we also rebuild on tags, so that we can be sure of picking the artifacts + # from the right tag. + tags: ["v*"] + permissions: contents: read @@ -40,5 +46,19 @@ jobs: - run: ./src/scripts-dev/build_debian_packages "${{ matrix.distro }}" - uses: actions/upload-artifact@v2 with: - name: packages + name: debs path: debs/* + + build-sdist: + name: "Build pypi distribution files" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - run: pip install wheel + - run: | + python setup.py sdist bdist_wheel + - uses: actions/upload-artifact@v2 + with: + name: python-dist + path: dist/* From 879d8c1ee1703a0f612b7f442409d2fcded587d6 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 13 Jul 2021 11:33:15 +0100 Subject: [PATCH 390/619] Fix federation inbound age metric. (#10355) We should be reporting the age rather than absolute timestamp. --- changelog.d/10355.bugfix | 1 + synapse/storage/databases/main/event_federation.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10355.bugfix diff --git a/changelog.d/10355.bugfix b/changelog.d/10355.bugfix new file mode 100644 index 0000000000..92df612011 --- /dev/null +++ b/changelog.d/10355.bugfix @@ -0,0 +1 @@ +Fix newly added `synapse_federation_server_oldest_inbound_pdu_in_staging` prometheus metric to measure age rather than timestamp. diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index c4474df975..4e06938849 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -1230,7 +1230,9 @@ def _get_stats_for_federation_staging_txn(txn): "SELECT coalesce(min(received_ts), 0) FROM federation_inbound_events_staging" ) - (age,) = txn.fetchone() + (received_ts,) = txn.fetchone() + + age = self._clock.time_msec() - received_ts return count, age From 89cfc3dd9849b0580146151098ad039a7680c63f Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 13 Jul 2021 12:43:15 +0200 Subject: [PATCH 391/619] [pyupgrade] `tests/` (#10347) --- changelog.d/10347.misc | 1 + tests/config/test_load.py | 4 ++-- tests/handlers/test_profile.py | 2 +- .../test_matrix_federation_agent.py | 2 +- tests/http/test_fedclient.py | 8 +++----- tests/replication/_base.py | 6 +++--- tests/replication/test_multi_media_repo.py | 4 ++-- .../test_sharded_event_persister.py | 6 +++--- tests/rest/admin/test_admin.py | 6 ++---- tests/rest/admin/test_room.py | 20 +++++++++---------- tests/rest/client/v1/test_rooms.py | 14 ++++++------- tests/rest/client/v2_alpha/test_relations.py | 2 +- .../rest/client/v2_alpha/test_report_event.py | 2 +- tests/rest/media/v1/test_media_storage.py | 2 +- tests/storage/test_directory.py | 2 +- tests/storage/test_profile.py | 12 ++--------- tests/storage/test_purge.py | 2 +- tests/storage/test_room.py | 2 +- tests/test_types.py | 4 +--- tests/unittest.py | 2 +- 20 files changed, 45 insertions(+), 58 deletions(-) create mode 100644 changelog.d/10347.misc diff --git a/changelog.d/10347.misc b/changelog.d/10347.misc new file mode 100644 index 0000000000..b2275a1350 --- /dev/null +++ b/changelog.d/10347.misc @@ -0,0 +1 @@ +Run `pyupgrade` on the codebase. \ No newline at end of file diff --git a/tests/config/test_load.py b/tests/config/test_load.py index ebe2c05165..903c69127d 100644 --- a/tests/config/test_load.py +++ b/tests/config/test_load.py @@ -43,7 +43,7 @@ def test_load_fails_if_server_name_missing(self): def test_generates_and_loads_macaroon_secret_key(self): self.generate_config() - with open(self.file, "r") as f: + with open(self.file) as f: raw = yaml.safe_load(f) self.assertIn("macaroon_secret_key", raw) @@ -120,7 +120,7 @@ def generate_config(self): def generate_config_and_remove_lines_containing(self, needle): self.generate_config() - with open(self.file, "r") as f: + with open(self.file) as f: contents = f.readlines() contents = [line for line in contents if needle not in line] with open(self.file, "w") as f: diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index cdb41101b3..2928c4f48c 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -103,7 +103,7 @@ def test_set_my_name(self): ) self.assertIsNone( - (self.get_success(self.store.get_profile_displayname(self.frank.localpart))) + self.get_success(self.store.get_profile_displayname(self.frank.localpart)) ) def test_set_my_name_if_disabled(self): diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index e45980316b..a37bce08c3 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -273,7 +273,7 @@ def test_get(self): self.assertEqual(response.code, 200) # Send the body - request.write('{ "a": 1 }'.encode("ascii")) + request.write(b'{ "a": 1 }') request.finish() self.reactor.pump((0.1,)) diff --git a/tests/http/test_fedclient.py b/tests/http/test_fedclient.py index ed9a884d76..d9a8b077d3 100644 --- a/tests/http/test_fedclient.py +++ b/tests/http/test_fedclient.py @@ -102,7 +102,7 @@ def do_request(): self.assertNoResult(test_d) # Send it the HTTP response - res_json = '{ "a": 1 }'.encode("ascii") + res_json = b'{ "a": 1 }' protocol.dataReceived( b"HTTP/1.1 200 OK\r\n" b"Server: Fake\r\n" @@ -339,10 +339,8 @@ def test_timeout_reading_body(self, method_name: str): # Send it the HTTP response client.dataReceived( - ( - b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" - b"Server: Fake\r\n\r\n" - ) + b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n" + b"Server: Fake\r\n\r\n" ) # Push by enough to time it out diff --git a/tests/replication/_base.py b/tests/replication/_base.py index 624bd1b927..386ea70a25 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -550,12 +550,12 @@ def encode(self, obj): if obj is None: return "$-1\r\n" if isinstance(obj, str): - return "${len}\r\n{str}\r\n".format(len=len(obj), str=obj) + return f"${len(obj)}\r\n{obj}\r\n" if isinstance(obj, int): - return ":{val}\r\n".format(val=obj) + return f":{obj}\r\n" if isinstance(obj, (list, tuple)): items = "".join(self.encode(a) for a in obj) - return "*{len}\r\n{items}".format(len=len(obj), items=items) + return f"*{len(obj)}\r\n{items}" raise Exception("Unrecognized type for encoding redis: %r: %r", type(obj), obj) diff --git a/tests/replication/test_multi_media_repo.py b/tests/replication/test_multi_media_repo.py index 76e6644353..b42f1288eb 100644 --- a/tests/replication/test_multi_media_repo.py +++ b/tests/replication/test_multi_media_repo.py @@ -70,7 +70,7 @@ def _get_media_req( self.reactor, FakeSite(resource), "GET", - "/{}/{}".format(target, media_id), + f"/{target}/{media_id}", shorthand=False, access_token=self.access_token, await_result=False, @@ -113,7 +113,7 @@ def _get_media_req( self.assertEqual(request.method, b"GET") self.assertEqual( request.path, - "/_matrix/media/r0/download/{}/{}".format(target, media_id).encode("utf-8"), + f"/_matrix/media/r0/download/{target}/{media_id}".encode("utf-8"), ) self.assertEqual( request.requestHeaders.getRawHeaders(b"host"), [target.encode("utf-8")] diff --git a/tests/replication/test_sharded_event_persister.py b/tests/replication/test_sharded_event_persister.py index 5eca5c165d..f3615af97e 100644 --- a/tests/replication/test_sharded_event_persister.py +++ b/tests/replication/test_sharded_event_persister.py @@ -211,7 +211,7 @@ def test_vector_clock_token(self): self.reactor, sync_hs_site, "GET", - "/sync?since={}".format(next_batch), + f"/sync?since={next_batch}", access_token=access_token, ) @@ -241,7 +241,7 @@ def test_vector_clock_token(self): self.reactor, sync_hs_site, "GET", - "/sync?since={}".format(vector_clock_token), + f"/sync?since={vector_clock_token}", access_token=access_token, ) @@ -266,7 +266,7 @@ def test_vector_clock_token(self): self.reactor, sync_hs_site, "GET", - "/sync?since={}".format(next_batch), + f"/sync?since={next_batch}", access_token=access_token, ) diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index 2f7090e554..a7c6e595b9 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -66,7 +66,7 @@ def test_delete_group(self): # Create a new group channel = self.make_request( "POST", - "/create_group".encode("ascii"), + b"/create_group", access_token=self.admin_user_tok, content={"localpart": "test"}, ) @@ -129,9 +129,7 @@ def _check_group(self, group_id, expect_code): def _get_groups_user_is_in(self, access_token): """Returns the list of groups the user is in (given their access token)""" - channel = self.make_request( - "GET", "/joined_groups".encode("ascii"), access_token=access_token - ) + channel = self.make_request("GET", b"/joined_groups", access_token=access_token) self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 959d3cea77..17ec8bfd3b 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -535,7 +535,7 @@ def _is_purged(self, room_id): ) ) - self.assertEqual(count, 0, msg="Rows not purged in {}".format(table)) + self.assertEqual(count, 0, msg=f"Rows not purged in {table}") def _assert_peek(self, room_id, expect_code): """Assert that the admin user can (or cannot) peek into the room.""" @@ -599,7 +599,7 @@ def test_purge_room(self): ) ) - self.assertEqual(count, 0, msg="Rows not purged in {}".format(table)) + self.assertEqual(count, 0, msg=f"Rows not purged in {table}") class RoomTestCase(unittest.HomeserverTestCase): @@ -1280,7 +1280,7 @@ def prepare(self, reactor, clock, homeserver): self.public_room_id = self.helper.create_room_as( self.creator, tok=self.creator_tok, is_public=True ) - self.url = "/_synapse/admin/v1/join/{}".format(self.public_room_id) + self.url = f"/_synapse/admin/v1/join/{self.public_room_id}" def test_requester_is_no_admin(self): """ @@ -1420,7 +1420,7 @@ def test_join_private_room_if_not_member(self): private_room_id = self.helper.create_room_as( self.creator, tok=self.creator_tok, is_public=False ) - url = "/_synapse/admin/v1/join/{}".format(private_room_id) + url = f"/_synapse/admin/v1/join/{private_room_id}" body = json.dumps({"user_id": self.second_user_id}) channel = self.make_request( @@ -1463,7 +1463,7 @@ def test_join_private_room_if_member(self): # Join user to room. - url = "/_synapse/admin/v1/join/{}".format(private_room_id) + url = f"/_synapse/admin/v1/join/{private_room_id}" body = json.dumps({"user_id": self.second_user_id}) channel = self.make_request( @@ -1493,7 +1493,7 @@ def test_join_private_room_if_owner(self): private_room_id = self.helper.create_room_as( self.admin_user, tok=self.admin_user_tok, is_public=False ) - url = "/_synapse/admin/v1/join/{}".format(private_room_id) + url = f"/_synapse/admin/v1/join/{private_room_id}" body = json.dumps({"user_id": self.second_user_id}) channel = self.make_request( @@ -1633,7 +1633,7 @@ def test_public_room(self): channel = self.make_request( "POST", - "/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id), + f"/_synapse/admin/v1/rooms/{room_id}/make_room_admin", content={}, access_token=self.admin_user_tok, ) @@ -1660,7 +1660,7 @@ def test_private_room(self): channel = self.make_request( "POST", - "/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id), + f"/_synapse/admin/v1/rooms/{room_id}/make_room_admin", content={}, access_token=self.admin_user_tok, ) @@ -1686,7 +1686,7 @@ def test_other_user(self): channel = self.make_request( "POST", - "/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id), + f"/_synapse/admin/v1/rooms/{room_id}/make_room_admin", content={"user_id": self.second_user_id}, access_token=self.admin_user_tok, ) @@ -1720,7 +1720,7 @@ def test_not_enough_power(self): channel = self.make_request( "POST", - "/_synapse/admin/v1/rooms/{}/make_room_admin".format(room_id), + f"/_synapse/admin/v1/rooms/{room_id}/make_room_admin", content={}, access_token=self.admin_user_tok, ) diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index e94566ffd7..3df070c936 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -1206,7 +1206,7 @@ def test_join_reason(self): reason = "hello" channel = self.make_request( "POST", - "/_matrix/client/r0/rooms/{}/join".format(self.room_id), + f"/_matrix/client/r0/rooms/{self.room_id}/join", content={"reason": reason}, access_token=self.second_tok, ) @@ -1220,7 +1220,7 @@ def test_leave_reason(self): reason = "hello" channel = self.make_request( "POST", - "/_matrix/client/r0/rooms/{}/leave".format(self.room_id), + f"/_matrix/client/r0/rooms/{self.room_id}/leave", content={"reason": reason}, access_token=self.second_tok, ) @@ -1234,7 +1234,7 @@ def test_kick_reason(self): reason = "hello" channel = self.make_request( "POST", - "/_matrix/client/r0/rooms/{}/kick".format(self.room_id), + f"/_matrix/client/r0/rooms/{self.room_id}/kick", content={"reason": reason, "user_id": self.second_user_id}, access_token=self.second_tok, ) @@ -1248,7 +1248,7 @@ def test_ban_reason(self): reason = "hello" channel = self.make_request( "POST", - "/_matrix/client/r0/rooms/{}/ban".format(self.room_id), + f"/_matrix/client/r0/rooms/{self.room_id}/ban", content={"reason": reason, "user_id": self.second_user_id}, access_token=self.creator_tok, ) @@ -1260,7 +1260,7 @@ def test_unban_reason(self): reason = "hello" channel = self.make_request( "POST", - "/_matrix/client/r0/rooms/{}/unban".format(self.room_id), + f"/_matrix/client/r0/rooms/{self.room_id}/unban", content={"reason": reason, "user_id": self.second_user_id}, access_token=self.creator_tok, ) @@ -1272,7 +1272,7 @@ def test_invite_reason(self): reason = "hello" channel = self.make_request( "POST", - "/_matrix/client/r0/rooms/{}/invite".format(self.room_id), + f"/_matrix/client/r0/rooms/{self.room_id}/invite", content={"reason": reason, "user_id": self.second_user_id}, access_token=self.creator_tok, ) @@ -1291,7 +1291,7 @@ def test_reject_invite_reason(self): reason = "hello" channel = self.make_request( "POST", - "/_matrix/client/r0/rooms/{}/leave".format(self.room_id), + f"/_matrix/client/r0/rooms/{self.room_id}/leave", content={"reason": reason}, access_token=self.second_tok, ) diff --git a/tests/rest/client/v2_alpha/test_relations.py b/tests/rest/client/v2_alpha/test_relations.py index 856aa8682f..2e2f94742e 100644 --- a/tests/rest/client/v2_alpha/test_relations.py +++ b/tests/rest/client/v2_alpha/test_relations.py @@ -273,7 +273,7 @@ def test_aggregation_pagination_within_group(self): prev_token = None found_event_ids = [] - encoded_key = urllib.parse.quote_plus("👍".encode("utf-8")) + encoded_key = urllib.parse.quote_plus("👍".encode()) for _ in range(20): from_token = "" if prev_token: diff --git a/tests/rest/client/v2_alpha/test_report_event.py b/tests/rest/client/v2_alpha/test_report_event.py index 1ec6b05e5b..a76a6fef1e 100644 --- a/tests/rest/client/v2_alpha/test_report_event.py +++ b/tests/rest/client/v2_alpha/test_report_event.py @@ -41,7 +41,7 @@ def prepare(self, reactor, clock, hs): self.helper.join(self.room_id, user=self.admin_user, tok=self.admin_user_tok) resp = self.helper.send(self.room_id, tok=self.admin_user_tok) self.event_id = resp["event_id"] - self.report_path = "rooms/{}/report/{}".format(self.room_id, self.event_id) + self.report_path = f"rooms/{self.room_id}/report/{self.event_id}" def test_reason_str_and_score_int(self): data = {"reason": "this makes me sad", "score": -100} diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index 95e7075841..2d6b49692e 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -310,7 +310,7 @@ def test_disposition_filenamestar_utf8escaped(self): correctly decode it as the UTF-8 string, and use filename* in the response. """ - filename = parse.quote("\u2603".encode("utf8")).encode("ascii") + filename = parse.quote("\u2603".encode()).encode("ascii") channel = self._req( b"inline; filename*=utf-8''" + filename + self.test_image.extension ) diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py index 41bef62ca8..43628ce44f 100644 --- a/tests/storage/test_directory.py +++ b/tests/storage/test_directory.py @@ -59,5 +59,5 @@ def test_delete_alias(self): self.assertEqual(self.room.to_string(), room_id) self.assertIsNone( - (self.get_success(self.store.get_association_from_room_alias(self.alias))) + self.get_success(self.store.get_association_from_room_alias(self.alias)) ) diff --git a/tests/storage/test_profile.py b/tests/storage/test_profile.py index 8a446da848..a1ba99ff14 100644 --- a/tests/storage/test_profile.py +++ b/tests/storage/test_profile.py @@ -45,11 +45,7 @@ def test_displayname(self): ) self.assertIsNone( - ( - self.get_success( - self.store.get_profile_displayname(self.u_frank.localpart) - ) - ) + self.get_success(self.store.get_profile_displayname(self.u_frank.localpart)) ) def test_avatar_url(self): @@ -76,9 +72,5 @@ def test_avatar_url(self): ) self.assertIsNone( - ( - self.get_success( - self.store.get_profile_avatar_url(self.u_frank.localpart) - ) - ) + self.get_success(self.store.get_profile_avatar_url(self.u_frank.localpart)) ) diff --git a/tests/storage/test_purge.py b/tests/storage/test_purge.py index 54c5b470c7..e5574063f1 100644 --- a/tests/storage/test_purge.py +++ b/tests/storage/test_purge.py @@ -75,7 +75,7 @@ def test_purge_history_wont_delete_extrems(self): token = self.get_success( self.store.get_topological_token_for_event(last["event_id"]) ) - event = "t{}-{}".format(token.topological + 1, token.stream + 1) + event = f"t{token.topological + 1}-{token.stream + 1}" # Purge everything before this topological token f = self.get_failure( diff --git a/tests/storage/test_room.py b/tests/storage/test_room.py index 70257bf210..31ce7f6252 100644 --- a/tests/storage/test_room.py +++ b/tests/storage/test_room.py @@ -49,7 +49,7 @@ def test_get_room(self): ) def test_get_room_unknown_room(self): - self.assertIsNone((self.get_success(self.store.get_room("!uknown:test")))) + self.assertIsNone(self.get_success(self.store.get_room("!uknown:test"))) def test_get_room_with_stats(self): self.assertDictContainsSubset( diff --git a/tests/test_types.py b/tests/test_types.py index d7881021d3..0d0c00d97a 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -103,6 +103,4 @@ def testLeadingUnderscore(self): def testNonAscii(self): # this should work with either a unicode or a bytes self.assertEqual(map_username_to_mxid_localpart("têst"), "t=c3=aast") - self.assertEqual( - map_username_to_mxid_localpart("têst".encode("utf-8")), "t=c3=aast" - ) + self.assertEqual(map_username_to_mxid_localpart("têst".encode()), "t=c3=aast") diff --git a/tests/unittest.py b/tests/unittest.py index 74db7c08f1..907b94b10a 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -140,7 +140,7 @@ def assertObjectHasAttributes(self, attrs, obj): try: self.assertEquals(attrs[key], getattr(obj, key)) except AssertionError as e: - raise (type(e))("Assert error for '.{}':".format(key)) from e + raise (type(e))(f"Assert error for '.{key}':") from e def assert_dict(self, required, actual): """Does a partial assert of a dict. From 2d8b60e0f23ac43546d666520a3d43d867a57526 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 13 Jul 2021 11:50:14 +0100 Subject: [PATCH 392/619] Github Actions workflow to attach release artifacts to release (#10379) --- .github/workflows/release-artifacts.yml | 28 ++++++++++++++++++++++++- changelog.d/10379.misc | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10379.misc diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 9d1fb89834..f292d703ed 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -13,7 +13,7 @@ on: tags: ["v*"] permissions: - contents: read + contents: write jobs: # first get the list of distros to build for. @@ -62,3 +62,29 @@ jobs: with: name: python-dist path: dist/* + + # if it's a tag, create a release and attach the artifacts to it + attach-assets: + name: "Attach assets to release" + if: startsWith(github.ref, 'refs/tags/') + needs: + - build-debs + - build-sdist + runs-on: ubuntu-latest + steps: + - name: Download all workflow run artifacts + uses: actions/download-artifact@v2 + - name: Build a tarball for the debs + run: tar -cvJf debs.tar.xz debs + - name: Attach to release + uses: softprops/action-gh-release@a929a66f232c1b11af63782948aa2210f981808a # PR#109 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: | + python-dist/* + debs.tar.xz + # if it's not already published, keep the release as a draft. + draft: true + # mark it as a prerelease if the tag contains 'rc'. + prerelease: ${{ contains(github.ref, 'rc') }} diff --git a/changelog.d/10379.misc b/changelog.d/10379.misc new file mode 100644 index 0000000000..00bf178bb8 --- /dev/null +++ b/changelog.d/10379.misc @@ -0,0 +1 @@ +Add Github Actions workflow to attach release artifacts to release. From 93729719b8451493e1df9930feb9f02f14ea5cef Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 13 Jul 2021 12:52:58 +0200 Subject: [PATCH 393/619] Use inline type hints in `tests/` (#10350) This PR is tantamount to running: python3.8 -m com2ann -v 6 tests/ (com2ann requires python 3.8 to run) --- changelog.d/10350.misc | 1 + tests/events/test_presence_router.py | 6 +++--- tests/module_api/test_api.py | 16 ++++++++-------- tests/replication/_base.py | 12 ++++++------ tests/replication/tcp/streams/test_events.py | 14 +++++++------- tests/replication/tcp/streams/test_receipts.py | 4 ++-- tests/replication/tcp/streams/test_typing.py | 4 ++-- tests/replication/test_multi_media_repo.py | 2 +- tests/rest/client/test_third_party_rules.py | 4 ++-- tests/rest/client/v1/test_login.py | 14 ++++++-------- tests/server.py | 8 +++++--- tests/storage/test_background_update.py | 4 +--- tests/storage/test_id_generators.py | 6 +++--- tests/test_state.py | 2 +- tests/test_utils/html_parsers.py | 6 +++--- tests/unittest.py | 2 +- tests/util/caches/test_descriptors.py | 2 +- tests/util/test_itertools.py | 18 +++++++++--------- 18 files changed, 62 insertions(+), 63 deletions(-) create mode 100644 changelog.d/10350.misc diff --git a/changelog.d/10350.misc b/changelog.d/10350.misc new file mode 100644 index 0000000000..eed2d8552a --- /dev/null +++ b/changelog.d/10350.misc @@ -0,0 +1 @@ +Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/tests/events/test_presence_router.py b/tests/events/test_presence_router.py index 875b0d0a11..c4ad33194d 100644 --- a/tests/events/test_presence_router.py +++ b/tests/events/test_presence_router.py @@ -152,7 +152,7 @@ def test_receiving_all_presence(self): ) self.assertEqual(len(presence_updates), 1) - presence_update = presence_updates[0] # type: UserPresenceState + presence_update: UserPresenceState = presence_updates[0] self.assertEqual(presence_update.user_id, self.other_user_one_id) self.assertEqual(presence_update.state, "online") self.assertEqual(presence_update.status_msg, "boop") @@ -274,7 +274,7 @@ def test_send_local_online_presence_to_with_module(self): presence_updates, _ = sync_presence(self, self.other_user_id) self.assertEqual(len(presence_updates), 1) - presence_update = presence_updates[0] # type: UserPresenceState + presence_update: UserPresenceState = presence_updates[0] self.assertEqual(presence_update.user_id, self.other_user_id) self.assertEqual(presence_update.state, "online") self.assertEqual(presence_update.status_msg, "I'm online!") @@ -320,7 +320,7 @@ def test_send_local_online_presence_to_with_module(self): ) for call in calls: call_args = call[0] - federation_transaction = call_args[0] # type: Transaction + federation_transaction: Transaction = call_args[0] # Get the sent EDUs in this transaction edus = federation_transaction.get_dict()["edus"] diff --git a/tests/module_api/test_api.py b/tests/module_api/test_api.py index 2c68b9a13c..81d9e2f484 100644 --- a/tests/module_api/test_api.py +++ b/tests/module_api/test_api.py @@ -100,9 +100,9 @@ def test_sending_events_into_room(self): "content": content, "sender": user_id, } - event = self.get_success( + event: EventBase = self.get_success( self.module_api.create_and_send_event_into_room(event_dict) - ) # type: EventBase + ) self.assertEqual(event.sender, user_id) self.assertEqual(event.type, "m.room.message") self.assertEqual(event.room_id, room_id) @@ -136,9 +136,9 @@ def test_sending_events_into_room(self): "sender": user_id, "state_key": "", } - event = self.get_success( + event: EventBase = self.get_success( self.module_api.create_and_send_event_into_room(event_dict) - ) # type: EventBase + ) self.assertEqual(event.sender, user_id) self.assertEqual(event.type, "m.room.power_levels") self.assertEqual(event.room_id, room_id) @@ -281,7 +281,7 @@ def test_send_local_online_presence_to_federation(self): ) for call in calls: call_args = call[0] - federation_transaction = call_args[0] # type: Transaction + federation_transaction: Transaction = call_args[0] # Get the sent EDUs in this transaction edus = federation_transaction.get_dict()["edus"] @@ -390,7 +390,7 @@ def _test_sending_local_online_presence_to_local_user( ) test_case.assertEqual(len(presence_updates), 1) - presence_update = presence_updates[0] # type: UserPresenceState + presence_update: UserPresenceState = presence_updates[0] test_case.assertEqual(presence_update.user_id, test_case.presence_sender_id) test_case.assertEqual(presence_update.state, "online") @@ -443,7 +443,7 @@ def _test_sending_local_online_presence_to_local_user( ) test_case.assertEqual(len(presence_updates), 1) - presence_update = presence_updates[0] # type: UserPresenceState + presence_update: UserPresenceState = presence_updates[0] test_case.assertEqual(presence_update.user_id, test_case.presence_sender_id) test_case.assertEqual(presence_update.state, "online") @@ -454,7 +454,7 @@ def _test_sending_local_online_presence_to_local_user( ) test_case.assertEqual(len(presence_updates), 1) - presence_update = presence_updates[0] # type: UserPresenceState + presence_update: UserPresenceState = presence_updates[0] test_case.assertEqual(presence_update.user_id, test_case.presence_sender_id) test_case.assertEqual(presence_update.state, "online") diff --git a/tests/replication/_base.py b/tests/replication/_base.py index 386ea70a25..e9fd991718 100644 --- a/tests/replication/_base.py +++ b/tests/replication/_base.py @@ -53,9 +53,9 @@ def prepare(self, reactor, clock, hs): # build a replication server server_factory = ReplicationStreamProtocolFactory(hs) self.streamer = hs.get_replication_streamer() - self.server = server_factory.buildProtocol( + self.server: ServerReplicationStreamProtocol = server_factory.buildProtocol( None - ) # type: ServerReplicationStreamProtocol + ) # Make a new HomeServer object for the worker self.reactor.lookups["testserv"] = "1.2.3.4" @@ -195,7 +195,7 @@ def assert_request_is_get_repl_stream_updates( fetching updates for given stream. """ - path = request.path # type: bytes # type: ignore + path: bytes = request.path # type: ignore self.assertRegex( path, br"^/_synapse/replication/get_repl_stream_updates/%s/[^/]+$" @@ -212,7 +212,7 @@ class BaseMultiWorkerStreamTestCase(unittest.HomeserverTestCase): unlike `BaseStreamTestCase`. """ - servlets = [] # type: List[Callable[[HomeServer, JsonResource], None]] + servlets: List[Callable[[HomeServer, JsonResource], None]] = [] def setUp(self): super().setUp() @@ -448,7 +448,7 @@ def __init__(self, hs: HomeServer): super().__init__(hs) # list of received (stream_name, token, row) tuples - self.received_rdata_rows = [] # type: List[Tuple[str, int, Any]] + self.received_rdata_rows: List[Tuple[str, int, Any]] = [] async def on_rdata(self, stream_name, instance_name, token, rows): await super().on_rdata(stream_name, instance_name, token, rows) @@ -484,7 +484,7 @@ def buildProtocol(self, addr): class FakeRedisPubSubProtocol(Protocol): """A connection from a client talking to the fake Redis server.""" - transport = None # type: Optional[FakeTransport] + transport: Optional[FakeTransport] = None def __init__(self, server: FakeRedisPubSubServer): self._server = server diff --git a/tests/replication/tcp/streams/test_events.py b/tests/replication/tcp/streams/test_events.py index f51fa0a79e..666008425a 100644 --- a/tests/replication/tcp/streams/test_events.py +++ b/tests/replication/tcp/streams/test_events.py @@ -135,9 +135,9 @@ def test_update_function_huge_state_change(self): ) # this is the point in the DAG where we make a fork - fork_point = self.get_success( + fork_point: List[str] = self.get_success( self.hs.get_datastore().get_latest_event_ids_in_room(self.room_id) - ) # type: List[str] + ) events = [ self._inject_state_event(sender=OTHER_USER) @@ -238,7 +238,7 @@ def test_update_function_huge_state_change(self): self.assertEqual(row.data.event_id, pl_event.event_id) # the state rows are unsorted - state_rows = [] # type: List[EventsStreamCurrentStateRow] + state_rows: List[EventsStreamCurrentStateRow] = [] for stream_name, _, row in received_rows: self.assertEqual("events", stream_name) self.assertIsInstance(row, EventsStreamRow) @@ -290,11 +290,11 @@ def test_update_function_state_row_limit(self): ) # this is the point in the DAG where we make a fork - fork_point = self.get_success( + fork_point: List[str] = self.get_success( self.hs.get_datastore().get_latest_event_ids_in_room(self.room_id) - ) # type: List[str] + ) - events = [] # type: List[EventBase] + events: List[EventBase] = [] for user in user_ids: events.extend( self._inject_state_event(sender=user) for _ in range(STATES_PER_USER) @@ -355,7 +355,7 @@ def test_update_function_state_row_limit(self): self.assertEqual(row.data.event_id, pl_events[i].event_id) # the state rows are unsorted - state_rows = [] # type: List[EventsStreamCurrentStateRow] + state_rows: List[EventsStreamCurrentStateRow] = [] for _ in range(STATES_PER_USER + 1): stream_name, token, row = received_rows.pop(0) self.assertEqual("events", stream_name) diff --git a/tests/replication/tcp/streams/test_receipts.py b/tests/replication/tcp/streams/test_receipts.py index 7f5d932f0b..38e292c1ab 100644 --- a/tests/replication/tcp/streams/test_receipts.py +++ b/tests/replication/tcp/streams/test_receipts.py @@ -43,7 +43,7 @@ def test_receipt(self): stream_name, _, token, rdata_rows = self.test_handler.on_rdata.call_args[0] self.assertEqual(stream_name, "receipts") self.assertEqual(1, len(rdata_rows)) - row = rdata_rows[0] # type: ReceiptsStream.ReceiptsStreamRow + row: ReceiptsStream.ReceiptsStreamRow = rdata_rows[0] self.assertEqual("!room:blue", row.room_id) self.assertEqual("m.read", row.receipt_type) self.assertEqual(USER_ID, row.user_id) @@ -75,7 +75,7 @@ def test_receipt(self): self.assertEqual(token, 3) self.assertEqual(1, len(rdata_rows)) - row = rdata_rows[0] # type: ReceiptsStream.ReceiptsStreamRow + row: ReceiptsStream.ReceiptsStreamRow = rdata_rows[0] self.assertEqual("!room2:blue", row.room_id) self.assertEqual("m.read", row.receipt_type) self.assertEqual(USER_ID, row.user_id) diff --git a/tests/replication/tcp/streams/test_typing.py b/tests/replication/tcp/streams/test_typing.py index ecd360c2d0..3ff5afc6e5 100644 --- a/tests/replication/tcp/streams/test_typing.py +++ b/tests/replication/tcp/streams/test_typing.py @@ -47,7 +47,7 @@ def test_typing(self): stream_name, _, token, rdata_rows = self.test_handler.on_rdata.call_args[0] self.assertEqual(stream_name, "typing") self.assertEqual(1, len(rdata_rows)) - row = rdata_rows[0] # type: TypingStream.TypingStreamRow + row: TypingStream.TypingStreamRow = rdata_rows[0] self.assertEqual(ROOM_ID, row.room_id) self.assertEqual([USER_ID], row.user_ids) @@ -102,7 +102,7 @@ def test_reset(self): stream_name, _, token, rdata_rows = self.test_handler.on_rdata.call_args[0] self.assertEqual(stream_name, "typing") self.assertEqual(1, len(rdata_rows)) - row = rdata_rows[0] # type: TypingStream.TypingStreamRow + row: TypingStream.TypingStreamRow = rdata_rows[0] self.assertEqual(ROOM_ID, row.room_id) self.assertEqual([USER_ID], row.user_ids) diff --git a/tests/replication/test_multi_media_repo.py b/tests/replication/test_multi_media_repo.py index b42f1288eb..ffa425328f 100644 --- a/tests/replication/test_multi_media_repo.py +++ b/tests/replication/test_multi_media_repo.py @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) -test_server_connection_factory = None # type: Optional[TestServerTLSConnectionFactory] +test_server_connection_factory: Optional[TestServerTLSConnectionFactory] = None class MediaRepoShardTestCase(BaseMultiWorkerStreamTestCase): diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index e1fe72fc5d..c5e1c5458b 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -233,11 +233,11 @@ def test_send_event(self): "content": content, "sender": self.user_id, } - event = self.get_success( + event: EventBase = self.get_success( current_rules_module().module_api.create_and_send_event_into_room( event_dict ) - ) # type: EventBase + ) self.assertEquals(event.sender, self.user_id) self.assertEquals(event.room_id, self.room_id) diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index 605b952316..7eba69642a 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -453,7 +453,7 @@ def test_get_msc2858_login_flows(self): self.assertEqual(channel.code, 200, channel.result) # stick the flows results in a dict by type - flow_results = {} # type: Dict[str, Any] + flow_results: Dict[str, Any] = {} for f in channel.json_body["flows"]: flow_type = f["type"] self.assertNotIn( @@ -501,7 +501,7 @@ def test_multi_sso_redirect(self): p.close() # there should be a link for each href - returned_idps = [] # type: List[str] + returned_idps: List[str] = [] for link in p.links: path, query = link.split("?", 1) self.assertEqual(path, "pick_idp") @@ -582,7 +582,7 @@ def test_login_via_oidc(self): # ... and should have set a cookie including the redirect url cookie_headers = channel.headers.getRawHeaders("Set-Cookie") assert cookie_headers - cookies = {} # type: Dict[str, str] + cookies: Dict[str, str] = {} for h in cookie_headers: key, value = h.split(";")[0].split("=", maxsplit=1) cookies[key] = value @@ -874,9 +874,7 @@ def make_homeserver(self, reactor, clock): def jwt_encode(self, payload: Dict[str, Any], secret: str = jwt_secret) -> str: # PyJWT 2.0.0 changed the return type of jwt.encode from bytes to str. - result = jwt.encode( - payload, secret, self.jwt_algorithm - ) # type: Union[str, bytes] + result: Union[str, bytes] = jwt.encode(payload, secret, self.jwt_algorithm) if isinstance(result, bytes): return result.decode("ascii") return result @@ -1084,7 +1082,7 @@ def make_homeserver(self, reactor, clock): def jwt_encode(self, payload: Dict[str, Any], secret: str = jwt_privatekey) -> str: # PyJWT 2.0.0 changed the return type of jwt.encode from bytes to str. - result = jwt.encode(payload, secret, "RS256") # type: Union[bytes,str] + result: Union[bytes, str] = jwt.encode(payload, secret, "RS256") if isinstance(result, bytes): return result.decode("ascii") return result @@ -1272,7 +1270,7 @@ def test_username_picker(self): self.assertEqual(picker_url, "/_synapse/client/pick_username/account_details") # ... with a username_mapping_session cookie - cookies = {} # type: Dict[str,str] + cookies: Dict[str, str] = {} channel.extract_cookies(cookies) self.assertIn("username_mapping_session", cookies) session_id = cookies["username_mapping_session"] diff --git a/tests/server.py b/tests/server.py index f32d8dc375..6fddd3b305 100644 --- a/tests/server.py +++ b/tests/server.py @@ -52,7 +52,7 @@ class FakeChannel: _reactor = attr.ib() result = attr.ib(type=dict, default=attr.Factory(dict)) _ip = attr.ib(type=str, default="127.0.0.1") - _producer = None # type: Optional[Union[IPullProducer, IPushProducer]] + _producer: Optional[Union[IPullProducer, IPushProducer]] = None @property def json_body(self): @@ -316,8 +316,10 @@ def __init__(self): self._tcp_callbacks = {} self._udp = [] - lookups = self.lookups = {} # type: Dict[str, str] - self._thread_callbacks = deque() # type: Deque[Callable[[], None]] + self.lookups: Dict[str, str] = {} + self._thread_callbacks: Deque[Callable[[], None]] = deque() + + lookups = self.lookups @implementer(IResolverSimple) class FakeResolver: diff --git a/tests/storage/test_background_update.py b/tests/storage/test_background_update.py index 069db0edc4..0da42b5ac5 100644 --- a/tests/storage/test_background_update.py +++ b/tests/storage/test_background_update.py @@ -7,9 +7,7 @@ class BackgroundUpdateTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, homeserver): - self.updates = ( - self.hs.get_datastore().db_pool.updates - ) # type: BackgroundUpdater + self.updates: BackgroundUpdater = self.hs.get_datastore().db_pool.updates # the base test class should have run the real bg updates for us self.assertTrue( self.get_success(self.updates.has_completed_background_updates()) diff --git a/tests/storage/test_id_generators.py b/tests/storage/test_id_generators.py index 792b1c44c1..7486078284 100644 --- a/tests/storage/test_id_generators.py +++ b/tests/storage/test_id_generators.py @@ -27,7 +27,7 @@ class MultiWriterIdGeneratorTestCase(HomeserverTestCase): def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - self.db_pool = self.store.db_pool # type: DatabasePool + self.db_pool: DatabasePool = self.store.db_pool self.get_success(self.db_pool.runInteraction("_setup_db", self._setup_db)) @@ -460,7 +460,7 @@ class BackwardsMultiWriterIdGeneratorTestCase(HomeserverTestCase): def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - self.db_pool = self.store.db_pool # type: DatabasePool + self.db_pool: DatabasePool = self.store.db_pool self.get_success(self.db_pool.runInteraction("_setup_db", self._setup_db)) @@ -586,7 +586,7 @@ class MultiTableMultiWriterIdGeneratorTestCase(HomeserverTestCase): def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() - self.db_pool = self.store.db_pool # type: DatabasePool + self.db_pool: DatabasePool = self.store.db_pool self.get_success(self.db_pool.runInteraction("_setup_db", self._setup_db)) diff --git a/tests/test_state.py b/tests/test_state.py index 62f7095873..780eba823c 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -199,7 +199,7 @@ def test_branch_no_conflict(self): self.store.register_events(graph.walk()) - context_store = {} # type: dict[str, EventContext] + context_store: dict[str, EventContext] = {} for event in graph.walk(): context = yield defer.ensureDeferred( diff --git a/tests/test_utils/html_parsers.py b/tests/test_utils/html_parsers.py index 1fbb38f4be..e878af5f12 100644 --- a/tests/test_utils/html_parsers.py +++ b/tests/test_utils/html_parsers.py @@ -23,13 +23,13 @@ def __init__(self): super().__init__() # a list of links found in the doc - self.links = [] # type: List[str] + self.links: List[str] = [] # the values of any hidden s: map from name to value - self.hiddens = {} # type: Dict[str, Optional[str]] + self.hiddens: Dict[str, Optional[str]] = {} # the values of any radio buttons: map from name to list of values - self.radios = {} # type: Dict[str, List[Optional[str]]] + self.radios: Dict[str, List[Optional[str]]] = {} def handle_starttag( self, tag: str, attrs: Iterable[Tuple[str, Optional[str]]] diff --git a/tests/unittest.py b/tests/unittest.py index 907b94b10a..c6d9064423 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -520,7 +520,7 @@ def get_success_or_raise(self, d, by=0.0): if not isinstance(deferred, Deferred): return d - results = [] # type: list + results: list = [] deferred.addBoth(results.append) self.pump(by=by) diff --git a/tests/util/caches/test_descriptors.py b/tests/util/caches/test_descriptors.py index 0277998cbe..39947a166b 100644 --- a/tests/util/caches/test_descriptors.py +++ b/tests/util/caches/test_descriptors.py @@ -174,7 +174,7 @@ def fn(self, arg1): return self.result obj = Cls() - callbacks = set() # type: Set[str] + callbacks: Set[str] = set() # set off an asynchronous request obj.result = origin_d = defer.Deferred() diff --git a/tests/util/test_itertools.py b/tests/util/test_itertools.py index e712eb42ea..3c0ddd4f18 100644 --- a/tests/util/test_itertools.py +++ b/tests/util/test_itertools.py @@ -44,7 +44,7 @@ def test_uneven_parts(self): ) def test_empty_input(self): - parts = chunk_seq([], 5) # type: Iterable[Sequence] + parts: Iterable[Sequence] = chunk_seq([], 5) self.assertEqual( list(parts), @@ -56,13 +56,13 @@ class SortTopologically(TestCase): def test_empty(self): "Test that an empty graph works correctly" - graph = {} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {} self.assertEqual(list(sorted_topologically([], graph)), []) def test_handle_empty_graph(self): "Test that a graph where a node doesn't have an entry is treated as empty" - graph = {} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {} # For disconnected nodes the output is simply sorted. self.assertEqual(list(sorted_topologically([1, 2], graph)), [1, 2]) @@ -70,7 +70,7 @@ def test_handle_empty_graph(self): def test_disconnected(self): "Test that a graph with no edges work" - graph = {1: [], 2: []} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {1: [], 2: []} # For disconnected nodes the output is simply sorted. self.assertEqual(list(sorted_topologically([1, 2], graph)), [1, 2]) @@ -78,19 +78,19 @@ def test_disconnected(self): def test_linear(self): "Test that a simple `4 -> 3 -> 2 -> 1` graph works" - graph = {1: [], 2: [1], 3: [2], 4: [3]} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {1: [], 2: [1], 3: [2], 4: [3]} self.assertEqual(list(sorted_topologically([4, 3, 2, 1], graph)), [1, 2, 3, 4]) def test_subset(self): "Test that only sorting a subset of the graph works" - graph = {1: [], 2: [1], 3: [2], 4: [3]} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {1: [], 2: [1], 3: [2], 4: [3]} self.assertEqual(list(sorted_topologically([4, 3], graph)), [3, 4]) def test_fork(self): "Test that a forked graph works" - graph = {1: [], 2: [1], 3: [1], 4: [2, 3]} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {1: [], 2: [1], 3: [1], 4: [2, 3]} # Valid orderings are `[1, 3, 2, 4]` or `[1, 2, 3, 4]`, but we should # always get the same one. @@ -98,12 +98,12 @@ def test_fork(self): def test_duplicates(self): "Test that a graph with duplicate edges work" - graph = {1: [], 2: [1, 1], 3: [2, 2], 4: [3]} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {1: [], 2: [1, 1], 3: [2, 2], 4: [3]} self.assertEqual(list(sorted_topologically([4, 3, 2, 1], graph)), [1, 2, 3, 4]) def test_multiple_paths(self): "Test that a graph with multiple paths between two nodes work" - graph = {1: [], 2: [1], 3: [2], 4: [3, 2, 1]} # type: Dict[int, List[int]] + graph: Dict[int, List[int]] = {1: [], 2: [1], 3: [2], 4: [3, 2, 1]} self.assertEqual(list(sorted_topologically([4, 3, 2, 1], graph)), [1, 2, 3, 4]) From d9b3637e446ca639b64be05e8a27d1c2ea23c589 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 13 Jul 2021 12:53:45 +0200 Subject: [PATCH 394/619] Bugfix `make_room_admin` fails for users that have left a private room (#10367) Fixes: #10338 --- changelog.d/10367.bugfix | 1 + synapse/rest/admin/rooms.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10367.bugfix diff --git a/changelog.d/10367.bugfix b/changelog.d/10367.bugfix new file mode 100644 index 0000000000..b445556084 --- /dev/null +++ b/changelog.d/10367.bugfix @@ -0,0 +1 @@ +Bugfix `make_room_admin` fails for users that have left a private room. \ No newline at end of file diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index f0cddd2d2c..3c51a742bf 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -462,6 +462,7 @@ def __init__(self, hs: "HomeServer"): super().__init__(hs) self.hs = hs self.auth = hs.get_auth() + self.store = hs.get_datastore() self.event_creation_handler = hs.get_event_creation_handler() self.state_handler = hs.get_state_handler() self.is_mine_id = hs.is_mine_id @@ -500,7 +501,13 @@ async def on_POST( admin_user_id = None for admin_user in reversed(admin_users): - if room_state.get((EventTypes.Member, admin_user)): + ( + current_membership_type, + _, + ) = await self.store.get_local_current_membership_for_user_in_room( + admin_user, room_id + ) + if current_membership_type == "join": admin_user_id = admin_user break From e938f69697aac0723a03605831403a815e8a1b45 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 13 Jul 2021 12:55:48 +0200 Subject: [PATCH 395/619] Fix some links in `docs` and `contrib` (#10370) --- changelog.d/10370.doc | 1 + contrib/docker/docker-compose.yml | 2 +- contrib/grafana/README.md | 4 ++-- contrib/prometheus/README.md | 2 +- contrib/purge_api/README.md | 10 ++++++---- contrib/purge_api/purge_history.sh | 2 +- contrib/systemd-with-workers/README.md | 3 ++- docs/systemd-with-workers/README.md | 14 ++++++++------ docs/usage/configuration/logging_sample_config.md | 2 +- 9 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 changelog.d/10370.doc diff --git a/changelog.d/10370.doc b/changelog.d/10370.doc new file mode 100644 index 0000000000..8c59d98ee8 --- /dev/null +++ b/changelog.d/10370.doc @@ -0,0 +1 @@ +Fix some links in `docs` and `contrib`. \ No newline at end of file diff --git a/contrib/docker/docker-compose.yml b/contrib/docker/docker-compose.yml index d1ecd453db..26d640c448 100644 --- a/contrib/docker/docker-compose.yml +++ b/contrib/docker/docker-compose.yml @@ -56,7 +56,7 @@ services: - POSTGRES_USER=synapse - POSTGRES_PASSWORD=changeme # ensure the database gets created correctly - # https://github.com/matrix-org/synapse/blob/master/docs/postgres.md#set-up-database + # https://matrix-org.github.io/synapse/latest/postgres.html#set-up-database - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C volumes: # You may store the database tables in a local folder.. diff --git a/contrib/grafana/README.md b/contrib/grafana/README.md index 4608793394..0d4e1b59b2 100644 --- a/contrib/grafana/README.md +++ b/contrib/grafana/README.md @@ -1,6 +1,6 @@ # Using the Synapse Grafana dashboard 0. Set up Prometheus and Grafana. Out of scope for this readme. Useful documentation about using Grafana with Prometheus: http://docs.grafana.org/features/datasources/prometheus/ -1. Have your Prometheus scrape your Synapse. https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.md +1. Have your Prometheus scrape your Synapse. https://matrix-org.github.io/synapse/latest/metrics-howto.html 2. Import dashboard into Grafana. Download `synapse.json`. Import it to Grafana and select the correct Prometheus datasource. http://docs.grafana.org/reference/export_import/ -3. Set up required recording rules. https://github.com/matrix-org/synapse/tree/master/contrib/prometheus +3. Set up required recording rules. [contrib/prometheus](../prometheus) diff --git a/contrib/prometheus/README.md b/contrib/prometheus/README.md index b3f23bcc80..4dbf648df8 100644 --- a/contrib/prometheus/README.md +++ b/contrib/prometheus/README.md @@ -34,7 +34,7 @@ Add a new job to the main prometheus.yml file: ``` An example of a Prometheus configuration with workers can be found in -[metrics-howto.md](https://github.com/matrix-org/synapse/blob/master/docs/metrics-howto.md). +[metrics-howto.md](https://matrix-org.github.io/synapse/latest/metrics-howto.html). To use `synapse.rules` add diff --git a/contrib/purge_api/README.md b/contrib/purge_api/README.md index 06b4cdb9f7..2f2e5c58cd 100644 --- a/contrib/purge_api/README.md +++ b/contrib/purge_api/README.md @@ -3,8 +3,9 @@ Purge history API examples # `purge_history.sh` -A bash file, that uses the [purge history API](/docs/admin_api/purge_history_api.rst) to -purge all messages in a list of rooms up to a certain event. You can select a +A bash file, that uses the +[purge history API](https://matrix-org.github.io/synapse/latest/admin_api/purge_history_api.html) +to purge all messages in a list of rooms up to a certain event. You can select a timeframe or a number of messages that you want to keep in the room. Just configure the variables DOMAIN, ADMIN, ROOMS_ARRAY and TIME at the top of @@ -12,5 +13,6 @@ the script. # `purge_remote_media.sh` -A bash file, that uses the [purge history API](/docs/admin_api/purge_history_api.rst) to -purge all old cached remote media. +A bash file, that uses the +[purge history API](https://matrix-org.github.io/synapse/latest/admin_api/purge_history_api.html) +to purge all old cached remote media. diff --git a/contrib/purge_api/purge_history.sh b/contrib/purge_api/purge_history.sh index c45136ff53..9d5324ea1c 100644 --- a/contrib/purge_api/purge_history.sh +++ b/contrib/purge_api/purge_history.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # this script will use the api: -# https://github.com/matrix-org/synapse/blob/master/docs/admin_api/purge_history_api.rst +# https://matrix-org.github.io/synapse/latest/admin_api/purge_history_api.html # # It will purge all messages in a list of rooms up to a cetrain event diff --git a/contrib/systemd-with-workers/README.md b/contrib/systemd-with-workers/README.md index 8d21d532bd..9b19b042e9 100644 --- a/contrib/systemd-with-workers/README.md +++ b/contrib/systemd-with-workers/README.md @@ -1,2 +1,3 @@ The documentation for using systemd to manage synapse workers is now part of -the main synapse distribution. See [docs/systemd-with-workers](../../docs/systemd-with-workers). +the main synapse distribution. See +[docs/systemd-with-workers](https://matrix-org.github.io/synapse/latest/systemd-with-workers/index.html). diff --git a/docs/systemd-with-workers/README.md b/docs/systemd-with-workers/README.md index a7de2de88a..3237ba4e93 100644 --- a/docs/systemd-with-workers/README.md +++ b/docs/systemd-with-workers/README.md @@ -15,9 +15,11 @@ contains an example configuration for the `federation_reader` worker. ## Synapse configuration files See [workers.md](../workers.md) for information on how to set up the -configuration files and reverse-proxy correctly. You can find an example worker -config in the [workers](https://github.com/matrix-org/synapse/tree/develop/docs/systemd-with-workers/workers/) -folder. +configuration files and reverse-proxy correctly. +Below is a sample `federation_reader` worker configuration file. +```yaml +{{#include workers/federation_reader.yaml}} +``` Systemd manages daemonization itself, so ensure that none of the configuration files set either `daemonize` or `worker_daemonize`. @@ -72,12 +74,12 @@ systemctl restart matrix-synapse.target **Optional:** If further hardening is desired, the file `override-hardened.conf` may be copied from -`contrib/systemd/override-hardened.conf` in this repository to the location +[contrib/systemd/override-hardened.conf](https://github.com/matrix-org/synapse/tree/develop/contrib/systemd/) +in this repository to the location `/etc/systemd/system/matrix-synapse.service.d/override-hardened.conf` (the directory may have to be created). It enables certain sandboxing features in systemd to further secure the synapse service. You may read the comments to -understand what the override file is doing. The same file will need to be copied -to +understand what the override file is doing. The same file will need to be copied to `/etc/systemd/system/matrix-synapse-worker@.service.d/override-hardened-worker.conf` (this directory may also have to be created) in order to apply the same hardening options to any worker processes. diff --git a/docs/usage/configuration/logging_sample_config.md b/docs/usage/configuration/logging_sample_config.md index 4c4bc6fc16..a673d487b8 100644 --- a/docs/usage/configuration/logging_sample_config.md +++ b/docs/usage/configuration/logging_sample_config.md @@ -11,4 +11,4 @@ a fresh config using Synapse by following the instructions in ```yaml {{#include ../../sample_log_config.yaml}} -``__` \ No newline at end of file +``` \ No newline at end of file From f7bfa694aec6cb933b2b6e2ff971cda98dba3c6c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 13 Jul 2021 11:57:55 +0100 Subject: [PATCH 396/619] 1.38.0rc3 --- CHANGES.md | 9 +++++++++ changelog.d/10247.misc | 1 - changelog.d/10379.misc | 1 - debian/changelog | 8 ++++++-- synapse/__init__.py | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) delete mode 100644 changelog.d/10247.misc delete mode 100644 changelog.d/10379.misc diff --git a/CHANGES.md b/CHANGES.md index a1419d6495..dbdf0a1185 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.38.0rc3 (2021-07-13) +============================== + +Internal Changes +---------------- + +- Build the Debian packages in CI. ([\#10247](https://github.com/matrix-org/synapse/issues/10247), [\#10379](https://github.com/matrix-org/synapse/issues/10379)) + + Synapse 1.38.0rc2 (2021-07-09) ============================== diff --git a/changelog.d/10247.misc b/changelog.d/10247.misc deleted file mode 100644 index 5824907bca..0000000000 --- a/changelog.d/10247.misc +++ /dev/null @@ -1 +0,0 @@ -Build the Debian packages in CI. diff --git a/changelog.d/10379.misc b/changelog.d/10379.misc deleted file mode 100644 index 00bf178bb8..0000000000 --- a/changelog.d/10379.misc +++ /dev/null @@ -1 +0,0 @@ -Add Github Actions workflow to attach release artifacts to release. diff --git a/debian/changelog b/debian/changelog index cafd03c6c1..efc84718a4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,12 @@ -matrix-synapse-py3 (1.37.1ubuntu1) UNRELEASED; urgency=medium +matrix-synapse-py3 (1.38.0rc3) prerelease; urgency=medium + [ Erik Johnston ] * Add synapse_review_recent_signups script - -- Erik Johnston Thu, 01 Jul 2021 15:55:03 +0100 + [ Synapse Packaging team ] + * New synapse release 1.38.0rc3. + + -- Synapse Packaging team Tue, 13 Jul 2021 11:53:56 +0100 matrix-synapse-py3 (1.37.1) stable; urgency=medium diff --git a/synapse/__init__.py b/synapse/__init__.py index 119afa9ebe..bbd691fba2 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.38.0rc2" +__version__ = "1.38.0rc3" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From c647c2a9ac7badd96cf15c35c86cc035db1b3fc5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 13 Jul 2021 13:19:06 +0100 Subject: [PATCH 397/619] 1.38.0 --- CHANGES.md | 6 ++++++ debian/changelog | 2 +- synapse/__init__.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dbdf0a1185..745c565740 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.38.0 (2021-07-13) +=========================== + +No significant changes. + + Synapse 1.38.0rc3 (2021-07-13) ============================== diff --git a/debian/changelog b/debian/changelog index efc84718a4..573a6fefd0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -6,7 +6,7 @@ matrix-synapse-py3 (1.38.0rc3) prerelease; urgency=medium [ Synapse Packaging team ] * New synapse release 1.38.0rc3. - -- Synapse Packaging team Tue, 13 Jul 2021 11:53:56 +0100 + -- Synapse Packaging team Tue, 13 Jul 2021 13:15:40 +0100 matrix-synapse-py3 (1.37.1) stable; urgency=medium diff --git a/synapse/__init__.py b/synapse/__init__.py index bbd691fba2..5ecce24eee 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.38.0rc3" +__version__ = "1.38.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 08a8297c0d7ae84c9ef3c1335168dd1759204da1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 13 Jul 2021 13:22:12 +0100 Subject: [PATCH 398/619] fix debian changelog --- debian/changelog | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 573a6fefd0..43d26fc133 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.38.0) stable; urgency=medium + + * New synapse release 1.38.0. + + -- Synapse Packaging team Tue, 13 Jul 2021 13:20:56 +0100 + matrix-synapse-py3 (1.38.0rc3) prerelease; urgency=medium [ Erik Johnston ] @@ -6,7 +12,7 @@ matrix-synapse-py3 (1.38.0rc3) prerelease; urgency=medium [ Synapse Packaging team ] * New synapse release 1.38.0rc3. - -- Synapse Packaging team Tue, 13 Jul 2021 13:15:40 +0100 + -- Synapse Packaging team Tue, 13 Jul 2021 11:53:56 +0100 matrix-synapse-py3 (1.37.1) stable; urgency=medium From f7309622e02c94fd55fabb59e43ebec237fc17f5 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 13 Jul 2021 13:23:07 +0100 Subject: [PATCH 399/619] Update CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 745c565740..5e23ee2d88 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Synapse 1.38.0 (2021-07-13) =========================== -No significant changes. +No significant changes since 1.38.0rc3. Synapse 1.38.0rc3 (2021-07-13) From 519ec8271ff6a1d59043ac318c34dca898e079dc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 13 Jul 2021 13:25:46 +0100 Subject: [PATCH 400/619] Move upgrade blurb --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5e23ee2d88..82baaa2d1f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ Synapse 1.38.0 (2021-07-13) =========================== +This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade#upgrading-to-v1380) for more information. + No significant changes since 1.38.0rc3. @@ -32,8 +34,6 @@ Improved Documentation Synapse 1.38.0rc1 (2021-07-06) ============================== -This release includes a database schema update which could result in elevated disk usage. See the [upgrade notes](https://matrix-org.github.io/synapse/develop/upgrade#upgrading-to-v1380) for more information. - Features -------- From 2d16e69b4bf09b5274a8fa15c8ca4719db8366c1 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 08:59:27 -0400 Subject: [PATCH 401/619] Show all joinable rooms in the spaces summary. (#10298) Previously only world-readable rooms were shown. This means that rooms which are public, knockable, or invite-only with a pending invitation, are included in a space summary. It also applies the same logic to the experimental room version from MSC3083 -- if a user has access to the proper allowed rooms then it is shown in the spaces summary. This change is made per MSC3173 allowing stripped state of a room to be shown to any potential room joiner. --- changelog.d/10298.feature | 1 + changelog.d/10305.feature | 1 + changelog.d/10305.misc | 1 - synapse/handlers/space_summary.py | 68 +++++-- synapse/storage/databases/main/roommember.py | 13 +- tests/handlers/test_space_summary.py | 191 +++++++++++++++++-- 6 files changed, 237 insertions(+), 38 deletions(-) create mode 100644 changelog.d/10298.feature create mode 100644 changelog.d/10305.feature delete mode 100644 changelog.d/10305.misc diff --git a/changelog.d/10298.feature b/changelog.d/10298.feature new file mode 100644 index 0000000000..7059db5075 --- /dev/null +++ b/changelog.d/10298.feature @@ -0,0 +1 @@ +The spaces summary API now returns any joinable rooms, not only rooms which are world-readable. diff --git a/changelog.d/10305.feature b/changelog.d/10305.feature new file mode 100644 index 0000000000..7059db5075 --- /dev/null +++ b/changelog.d/10305.feature @@ -0,0 +1 @@ +The spaces summary API now returns any joinable rooms, not only rooms which are world-readable. diff --git a/changelog.d/10305.misc b/changelog.d/10305.misc deleted file mode 100644 index 8488d47f6f..0000000000 --- a/changelog.d/10305.misc +++ /dev/null @@ -1 +0,0 @@ -Additional unit tests for the spaces summary API. diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index b585057ec3..366e6211e5 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -24,6 +24,7 @@ EventContentFields, EventTypes, HistoryVisibility, + JoinRules, Membership, RoomTypes, ) @@ -150,14 +151,21 @@ async def get_space_summary( # The room should only be included in the summary if: # a. the user is in the room; # b. the room is world readable; or - # c. the user is in a space that has been granted access to - # the room. + # c. the user could join the room, e.g. the join rules + # are set to public or the user is in a space that + # has been granted access to the room. # # Note that we know the user is not in the root room (which is # why the remote call was made in the first place), but the user # could be in one of the children rooms and we just didn't know # about the link. - include_room = room.get("world_readable") is True + + # The API doesn't return the room version so assume that a + # join rule of knock is valid. + include_room = ( + room.get("join_rules") in (JoinRules.PUBLIC, JoinRules.KNOCK) + or room.get("world_readable") is True + ) # Check if the user is a member of any of the allowed spaces # from the response. @@ -420,9 +428,8 @@ async def _is_room_accessible( It should be included if: - * The requester is joined or invited to the room. - * The requester can join without an invite (per MSC3083). - * The origin server has any user that is joined or invited to the room. + * The requester is joined or can join the room (per MSC3173). + * The origin server has any user that is joined or can join the room. * The history visibility is set to world readable. Args: @@ -441,13 +448,39 @@ async def _is_room_accessible( # If there's no state for the room, it isn't known. if not state_ids: + # The user might have a pending invite for the room. + if requester and await self._store.get_invite_for_local_user_in_room( + requester, room_id + ): + return True + logger.info("room %s is unknown, omitting from summary", room_id) return False room_version = await self._store.get_room_version(room_id) - # if we have an authenticated requesting user, first check if they are able to view - # stripped state in the room. + # Include the room if it has join rules of public or knock. + join_rules_event_id = state_ids.get((EventTypes.JoinRules, "")) + if join_rules_event_id: + join_rules_event = await self._store.get_event(join_rules_event_id) + join_rule = join_rules_event.content.get("join_rule") + if join_rule == JoinRules.PUBLIC or ( + room_version.msc2403_knocking and join_rule == JoinRules.KNOCK + ): + return True + + # Include the room if it is peekable. + hist_vis_event_id = state_ids.get((EventTypes.RoomHistoryVisibility, "")) + if hist_vis_event_id: + hist_vis_ev = await self._store.get_event(hist_vis_event_id) + hist_vis = hist_vis_ev.content.get("history_visibility") + if hist_vis == HistoryVisibility.WORLD_READABLE: + return True + + # Otherwise we need to check information specific to the user or server. + + # If we have an authenticated requesting user, check if they are a member + # of the room (or can join the room). if requester: member_event_id = state_ids.get((EventTypes.Member, requester), None) @@ -470,9 +503,11 @@ async def _is_room_accessible( return True # If this is a request over federation, check if the host is in the room or - # is in one of the spaces specified via the join rules. + # has a user who could join the room. elif origin: - if await self._event_auth_handler.check_host_in_room(room_id, origin): + if await self._event_auth_handler.check_host_in_room( + room_id, origin + ) or await self._store.is_host_invited(room_id, origin): return True # Alternately, if the host has a user in any of the spaces specified @@ -490,18 +525,10 @@ async def _is_room_accessible( ): return True - # otherwise, check if the room is peekable - hist_vis_event_id = state_ids.get((EventTypes.RoomHistoryVisibility, ""), None) - if hist_vis_event_id: - hist_vis_ev = await self._store.get_event(hist_vis_event_id) - hist_vis = hist_vis_ev.content.get("history_visibility") - if hist_vis == HistoryVisibility.WORLD_READABLE: - return True - logger.info( - "room %s is unpeekable and user %s is not a member / not allowed to join, omitting from summary", + "room %s is unpeekable and requester %s is not a member / not allowed to join, omitting from summary", room_id, - requester, + requester or origin, ) return False @@ -535,6 +562,7 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: "canonical_alias": stats["canonical_alias"], "num_joined_members": stats["joined_members"], "avatar_url": stats["avatar"], + "join_rules": stats["join_rules"], "world_readable": ( stats["history_visibility"] == HistoryVisibility.WORLD_READABLE ), diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 2796354a1f..4d82c4c26d 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -703,13 +703,22 @@ async def _get_joined_profiles_from_event_ids(self, event_ids: Iterable[str]): @cached(max_entries=10000) async def is_host_joined(self, room_id: str, host: str) -> bool: + return await self._check_host_room_membership(room_id, host, Membership.JOIN) + + @cached(max_entries=10000) + async def is_host_invited(self, room_id: str, host: str) -> bool: + return await self._check_host_room_membership(room_id, host, Membership.INVITE) + + async def _check_host_room_membership( + self, room_id: str, host: str, membership: str + ) -> bool: if "%" in host or "_" in host: raise Exception("Invalid host name") sql = """ SELECT state_key FROM current_state_events AS c INNER JOIN room_memberships AS m USING (event_id) - WHERE m.membership = 'join' + WHERE m.membership = ? AND type = 'm.room.member' AND c.room_id = ? AND state_key LIKE ? @@ -722,7 +731,7 @@ async def is_host_joined(self, room_id: str, host: str) -> bool: like_clause = "%:" + host rows = await self.db_pool.execute( - "is_host_joined", None, sql, room_id, like_clause + "is_host_joined", None, sql, membership, room_id, like_clause ) if not rows: diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index faed1f1a18..3f73ad7f94 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -14,8 +14,18 @@ from typing import Any, Iterable, Optional, Tuple from unittest import mock -from synapse.api.constants import EventContentFields, JoinRules, RoomTypes +from synapse.api.constants import ( + EventContentFields, + EventTypes, + HistoryVisibility, + JoinRules, + Membership, + RestrictedJoinRuleTypes, + RoomTypes, +) from synapse.api.errors import AuthError +from synapse.api.room_versions import RoomVersions +from synapse.events import make_event_from_dict from synapse.handlers.space_summary import _child_events_comparison_key from synapse.rest import admin from synapse.rest.client.v1 import login, room @@ -117,7 +127,7 @@ def _add_child(self, space_id: str, room_id: str, token: str) -> None: """Add a child room to a space.""" self.helper.send_state( space_id, - event_type="m.space.child", + event_type=EventTypes.SpaceChild, body={"via": [self.hs.hostname]}, tok=token, state_key=room_id, @@ -155,29 +165,129 @@ def test_visibility(self): # The user cannot see the space. self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError) - # Joining the room causes it to be visible. - self.helper.join(self.space, user2, tok=token2) + # If the space is made world-readable it should return a result. + self.helper.send_state( + self.space, + event_type=EventTypes.RoomHistoryVisibility, + body={"history_visibility": HistoryVisibility.WORLD_READABLE}, + tok=self.token, + ) result = self.get_success(self.handler.get_space_summary(user2, self.space)) - - # The result should only have the space, but includes the link to the room. - self._assert_rooms(result, [self.space]) + self._assert_rooms(result, [self.space, self.room]) self._assert_events(result, [(self.space, self.room)]) - def test_world_readable(self): - """A world-readable room is visible to everyone.""" + # Make it not world-readable again and confirm it results in an error. self.helper.send_state( self.space, - event_type="m.room.history_visibility", - body={"history_visibility": "world_readable"}, + event_type=EventTypes.RoomHistoryVisibility, + body={"history_visibility": HistoryVisibility.JOINED}, tok=self.token, ) + self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError) + + # Join the space and results should be returned. + self.helper.join(self.space, user2, tok=token2) + result = self.get_success(self.handler.get_space_summary(user2, self.space)) + self._assert_rooms(result, [self.space, self.room]) + self._assert_events(result, [(self.space, self.room)]) + def _create_room_with_join_rule( + self, join_rule: str, room_version: Optional[str] = None, **extra_content + ) -> str: + """Create a room with the given join rule and add it to the space.""" + room_id = self.helper.create_room_as( + self.user, + room_version=room_version, + tok=self.token, + extra_content={ + "initial_state": [ + { + "type": EventTypes.JoinRules, + "state_key": "", + "content": { + "join_rule": join_rule, + **extra_content, + }, + } + ] + }, + ) + self._add_child(self.space, room_id, self.token) + return room_id + + def test_filtering(self): + """ + Rooms should be properly filtered to only include rooms the user has access to. + """ user2 = self.register_user("user2", "pass") + token2 = self.login("user2", "pass") - # The space should be visible, as well as the link to the room. + # Create a few rooms which will have different properties. + public_room = self._create_room_with_join_rule(JoinRules.PUBLIC) + knock_room = self._create_room_with_join_rule( + JoinRules.KNOCK, room_version=RoomVersions.V7.identifier + ) + not_invited_room = self._create_room_with_join_rule(JoinRules.INVITE) + invited_room = self._create_room_with_join_rule(JoinRules.INVITE) + self.helper.invite(invited_room, targ=user2, tok=self.token) + restricted_room = self._create_room_with_join_rule( + JoinRules.MSC3083_RESTRICTED, + room_version=RoomVersions.MSC3083.identifier, + allow=[], + ) + restricted_accessible_room = self._create_room_with_join_rule( + JoinRules.MSC3083_RESTRICTED, + room_version=RoomVersions.MSC3083.identifier, + allow=[ + { + "type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP, + "room_id": self.space, + "via": [self.hs.hostname], + } + ], + ) + world_readable_room = self._create_room_with_join_rule(JoinRules.INVITE) + self.helper.send_state( + world_readable_room, + event_type=EventTypes.RoomHistoryVisibility, + body={"history_visibility": HistoryVisibility.WORLD_READABLE}, + tok=self.token, + ) + joined_room = self._create_room_with_join_rule(JoinRules.INVITE) + self.helper.invite(joined_room, targ=user2, tok=self.token) + self.helper.join(joined_room, user2, tok=token2) + + # Join the space. + self.helper.join(self.space, user2, tok=token2) result = self.get_success(self.handler.get_space_summary(user2, self.space)) - self._assert_rooms(result, [self.space]) - self._assert_events(result, [(self.space, self.room)]) + + self._assert_rooms( + result, + [ + self.space, + self.room, + public_room, + knock_room, + invited_room, + restricted_accessible_room, + world_readable_room, + joined_room, + ], + ) + self._assert_events( + result, + [ + (self.space, self.room), + (self.space, public_room), + (self.space, knock_room), + (self.space, not_invited_room), + (self.space, invited_room), + (self.space, restricted_room), + (self.space, restricted_accessible_room), + (self.space, world_readable_room), + (self.space, joined_room), + ], + ) def test_complex_space(self): """ @@ -186,7 +296,7 @@ def test_complex_space(self): # Create an inaccessible room. user2 = self.register_user("user2", "pass") token2 = self.login("user2", "pass") - room2 = self.helper.create_room_as(user2, tok=token2) + room2 = self.helper.create_room_as(user2, is_public=False, tok=token2) # This is a bit odd as "user" is adding a room they don't know about, but # it works for the tests. self._add_child(self.space, room2, self.token) @@ -292,16 +402,60 @@ def test_fed_filtering(self): subspace = "#subspace:" + fed_hostname # Create a few rooms which will have different properties. + public_room = "#public:" + fed_hostname + knock_room = "#knock:" + fed_hostname + not_invited_room = "#not_invited:" + fed_hostname + invited_room = "#invited:" + fed_hostname restricted_room = "#restricted:" + fed_hostname restricted_accessible_room = "#restricted_accessible:" + fed_hostname world_readable_room = "#world_readable:" + fed_hostname joined_room = self.helper.create_room_as(self.user, tok=self.token) + # Poke an invite over federation into the database. + fed_handler = self.hs.get_federation_handler() + event = make_event_from_dict( + { + "room_id": invited_room, + "event_id": "!abcd:" + fed_hostname, + "type": EventTypes.Member, + "sender": "@remote:" + fed_hostname, + "state_key": self.user, + "content": {"membership": Membership.INVITE}, + "prev_events": [], + "auth_events": [], + "depth": 1, + "origin_server_ts": 1234, + } + ) + self.get_success( + fed_handler.on_invite_request(fed_hostname, event, RoomVersions.V6) + ) + async def summarize_remote_room( _self, room, suggested_only, max_children, exclude_rooms ): # Note that these entries are brief, but should contain enough info. rooms = [ + { + "room_id": public_room, + "world_readable": False, + "join_rules": JoinRules.PUBLIC, + }, + { + "room_id": knock_room, + "world_readable": False, + "join_rules": JoinRules.KNOCK, + }, + { + "room_id": not_invited_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + { + "room_id": invited_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, { "room_id": restricted_room, "world_readable": False, @@ -364,6 +518,9 @@ async def summarize_remote_room( self.space, self.room, subspace, + public_room, + knock_room, + invited_room, restricted_accessible_room, world_readable_room, joined_room, @@ -374,6 +531,10 @@ async def summarize_remote_room( [ (self.space, self.room), (self.space, subspace), + (subspace, public_room), + (subspace, knock_room), + (subspace, not_invited_room), + (subspace, invited_room), (subspace, restricted_room), (subspace, restricted_accessible_room), (subspace, world_readable_room), From 30b56f69258068d5f9ae7dcde27ac54f75a1a56c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 12:08:47 -0400 Subject: [PATCH 402/619] Add type hints to get_domain_from_id and get_localpart_from_id. (#10385) --- changelog.d/10385.misc | 1 + synapse/federation/transport/server.py | 96 +++++++++++++++++++------- synapse/types.py | 4 +- 3 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 changelog.d/10385.misc diff --git a/changelog.d/10385.misc b/changelog.d/10385.misc new file mode 100644 index 0000000000..e515ac09fd --- /dev/null +++ b/changelog.d/10385.misc @@ -0,0 +1 @@ +Add type hints to `get_{domain,localpart}_from_id`. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index d37d9565fc..0b21b375ee 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1095,7 +1095,9 @@ async def on_GET( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1110,7 +1112,9 @@ async def on_POST( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1131,7 +1135,9 @@ async def on_GET( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1152,7 +1158,9 @@ async def on_GET( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1174,7 +1182,9 @@ async def on_POST( group_id: str, room_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1192,7 +1202,9 @@ async def on_DELETE( group_id: str, room_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1220,7 +1232,9 @@ async def on_POST( room_id: str, config_key: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1243,7 +1257,9 @@ async def on_GET( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1264,7 +1280,9 @@ async def on_GET( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1288,7 +1306,9 @@ async def on_POST( group_id: str, user_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1354,7 +1374,9 @@ async def on_POST( group_id: str, user_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1487,7 +1509,9 @@ async def on_POST( category_id: str, room_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1523,7 +1547,9 @@ async def on_DELETE( category_id: str, room_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1549,7 +1575,9 @@ async def on_GET( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1571,7 +1599,9 @@ async def on_GET( group_id: str, category_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1589,7 +1619,9 @@ async def on_POST( group_id: str, category_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1618,7 +1650,9 @@ async def on_DELETE( group_id: str, category_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1644,7 +1678,9 @@ async def on_GET( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1666,7 +1702,9 @@ async def on_GET( group_id: str, role_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1682,7 +1720,9 @@ async def on_POST( group_id: str, role_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1713,7 +1753,9 @@ async def on_DELETE( group_id: str, role_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1750,7 +1792,9 @@ async def on_POST( role_id: str, user_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1784,7 +1828,9 @@ async def on_DELETE( role_id: str, user_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") @@ -1825,7 +1871,9 @@ async def on_PUT( query: Dict[bytes, List[bytes]], group_id: str, ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args(query, "requester_user_id") + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) if get_domain_from_id(requester_user_id) != origin: raise SynapseError(403, "requester_user_id doesn't match origin") diff --git a/synapse/types.py b/synapse/types.py index 8d2fa00f71..64c442bd0f 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -182,14 +182,14 @@ def create_requester( ) -def get_domain_from_id(string): +def get_domain_from_id(string: str) -> str: idx = string.find(":") if idx == -1: raise SynapseError(400, "Invalid ID: %r" % (string,)) return string[idx + 1 :] -def get_localpart_from_id(string): +def get_localpart_from_id(string: str) -> str: idx = string.find(":") if idx == -1: raise SynapseError(400, "Invalid ID: %r" % (string,)) From 0d5b08ac7ac88ae14cf81f0927084edc2c63a15f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 13 Jul 2021 14:12:33 -0500 Subject: [PATCH 403/619] Fix messages from multiple senders in historical chunk (MSC2716) (#10276) Fix messages from multiple senders in historical chunk. This also means that an app service does not need to define `?user_id` when using this endpoint. Follow-up to https://github.com/matrix-org/synapse/pull/9247 Part of MSC2716: https://github.com/matrix-org/matrix-doc/pull/2716 --- changelog.d/10276.bugfix | 1 + synapse/api/auth.py | 37 ++++++++++++++++++++++--- synapse/rest/client/v1/room.py | 49 +++++++++++++++++++++++++++++----- 3 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 changelog.d/10276.bugfix diff --git a/changelog.d/10276.bugfix b/changelog.d/10276.bugfix new file mode 100644 index 0000000000..42adc57ad1 --- /dev/null +++ b/changelog.d/10276.bugfix @@ -0,0 +1 @@ +Fix historical batch send endpoint (MSC2716) rejecting batches with messages from multiple senders. diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 307f5f9a94..42476a18e5 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -240,6 +240,37 @@ async def get_user_by_req( except KeyError: raise MissingClientTokenError() + async def validate_appservice_can_control_user_id( + self, app_service: ApplicationService, user_id: str + ): + """Validates that the app service is allowed to control + the given user. + + Args: + app_service: The app service that controls the user + user_id: The author MXID that the app service is controlling + + Raises: + AuthError: If the application service is not allowed to control the user + (user namespace regex does not match, wrong homeserver, etc) + or if the user has not been registered yet. + """ + + # It's ok if the app service is trying to use the sender from their registration + if app_service.sender == user_id: + pass + # Check to make sure the app service is allowed to control the user + elif not app_service.is_interested_in_user(user_id): + raise AuthError( + 403, + "Application service cannot masquerade as this user (%s)." % user_id, + ) + # Check to make sure the user is already registered on the homeserver + elif not (await self.store.get_user_by_id(user_id)): + raise AuthError( + 403, "Application service has not registered this user (%s)" % user_id + ) + async def _get_appservice_user_id( self, request: Request ) -> Tuple[Optional[str], Optional[ApplicationService]]: @@ -261,13 +292,11 @@ async def _get_appservice_user_id( return app_service.sender, app_service user_id = request.args[b"user_id"][0].decode("utf8") + await self.validate_appservice_can_control_user_id(app_service, user_id) + if app_service.sender == user_id: return app_service.sender, app_service - if not app_service.is_interested_in_user(user_id): - raise AuthError(403, "Application service cannot masquerade as this user.") - if not (await self.store.get_user_by_id(user_id)): - raise AuthError(403, "Application service has not registered this user") return user_id, app_service async def get_user_by_access_token( diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 9c58e3689e..ebf4e32230 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -29,6 +29,7 @@ SynapseError, ) from synapse.api.filtering import Filter +from synapse.appservice import ApplicationService from synapse.events.utils import format_event_for_client_v2 from synapse.http.servlet import ( RestServlet, @@ -47,11 +48,13 @@ from synapse.streams.config import PaginationConfig from synapse.types import ( JsonDict, + Requester, RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID, + create_requester, ) from synapse.util import json_decoder from synapse.util.stringutils import parse_and_validate_server_name, random_string @@ -309,7 +312,7 @@ def __init__(self, hs): self.room_member_handler = hs.get_room_member_handler() self.auth = hs.get_auth() - async def inherit_depth_from_prev_ids(self, prev_event_ids) -> int: + async def _inherit_depth_from_prev_ids(self, prev_event_ids) -> int: ( most_recent_prev_event_id, most_recent_prev_event_depth, @@ -378,6 +381,25 @@ def _create_insertion_event_dict( return insertion_event + async def _create_requester_for_user_id_from_app_service( + self, user_id: str, app_service: ApplicationService + ) -> Requester: + """Creates a new requester for the given user_id + and validates that the app service is allowed to control + the given user. + + Args: + user_id: The author MXID that the app service is controlling + app_service: The app service that controls the user + + Returns: + Requester object + """ + + await self.auth.validate_appservice_can_control_user_id(app_service, user_id) + + return create_requester(user_id, app_service=app_service) + async def on_POST(self, request, room_id): requester = await self.auth.get_user_by_req(request, allow_guest=False) @@ -443,7 +465,9 @@ async def on_POST(self, request, room_id): if event_dict["type"] == EventTypes.Member: membership = event_dict["content"].get("membership", None) event_id, _ = await self.room_member_handler.update_membership( - requester, + await self._create_requester_for_user_id_from_app_service( + state_event["sender"], requester.app_service + ), target=UserID.from_string(event_dict["state_key"]), room_id=room_id, action=membership, @@ -463,7 +487,9 @@ async def on_POST(self, request, room_id): event, _, ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, + await self._create_requester_for_user_id_from_app_service( + state_event["sender"], requester.app_service + ), event_dict, outlier=True, prev_event_ids=[fake_prev_event_id], @@ -479,7 +505,9 @@ async def on_POST(self, request, room_id): events_to_create = body["events"] prev_event_ids = prev_events_from_query - inherited_depth = await self.inherit_depth_from_prev_ids(prev_events_from_query) + inherited_depth = await self._inherit_depth_from_prev_ids( + prev_events_from_query + ) # Figure out which chunk to connect to. If they passed in # chunk_id_from_query let's use it. The chunk ID passed in comes @@ -509,7 +537,10 @@ async def on_POST(self, request, room_id): base_insertion_event, _, ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, + await self._create_requester_for_user_id_from_app_service( + base_insertion_event_dict["sender"], + requester.app_service, + ), base_insertion_event_dict, prev_event_ids=base_insertion_event_dict.get("prev_events"), auth_event_ids=auth_event_ids, @@ -558,7 +589,9 @@ async def on_POST(self, request, room_id): } event, context = await self.event_creation_handler.create_event( - requester, + await self._create_requester_for_user_id_from_app_service( + ev["sender"], requester.app_service + ), event_dict, prev_event_ids=event_dict.get("prev_events"), auth_event_ids=auth_event_ids, @@ -588,7 +621,9 @@ async def on_POST(self, request, room_id): # where topological_ordering is just depth. for (event, context) in reversed(events_to_persist): ev = await self.event_creation_handler.handle_new_client_event( - requester=requester, + await self._create_requester_for_user_id_from_app_service( + event["sender"], requester.app_service + ), event=event, context=context, ) From eb3beb8f12a5ee93e19eacf0f03c6bcde18999fe Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 14 Jul 2021 09:13:40 -0400 Subject: [PATCH 404/619] Add type hints and comments to event auth code. (#10393) --- changelog.d/10393.misc | 1 + mypy.ini | 1 + synapse/event_auth.py | 3 +++ tests/test_event_auth.py | 23 +++++++++++++---------- 4 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10393.misc diff --git a/changelog.d/10393.misc b/changelog.d/10393.misc new file mode 100644 index 0000000000..e80f16d607 --- /dev/null +++ b/changelog.d/10393.misc @@ -0,0 +1 @@ +Add type hints and comments to event auth code. diff --git a/mypy.ini b/mypy.ini index 72ce932d73..8717ae738e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -83,6 +83,7 @@ files = synapse/util/stringutils.py, synapse/visibility.py, tests/replication, + tests/test_event_auth.py, tests/test_utils, tests/handlers/test_password_providers.py, tests/rest/client/v1/test_login.py, diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 89bcf81515..a3df6cfcc1 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -48,6 +48,9 @@ def check( room_version_obj: the version of the room event: the event being checked. auth_events: the existing room state. + do_sig_check: True if it should be verified that the sending server + signed the event. + do_size_check: True if the size of the event fields should be verified. Raises: AuthError if the checks fail diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index 88888319cc..f73306ecc4 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -13,12 +13,13 @@ # limitations under the License. import unittest +from typing import Optional from synapse import event_auth from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersions -from synapse.events import make_event_from_dict -from synapse.types import get_domain_from_id +from synapse.events import EventBase, make_event_from_dict +from synapse.types import JsonDict, get_domain_from_id class EventAuthTestCase(unittest.TestCase): @@ -432,7 +433,7 @@ def test_join_rules_msc3083_restricted(self): TEST_ROOM_ID = "!test:room" -def _create_event(user_id): +def _create_event(user_id: str) -> EventBase: return make_event_from_dict( { "room_id": TEST_ROOM_ID, @@ -444,7 +445,9 @@ def _create_event(user_id): ) -def _member_event(user_id, membership, sender=None): +def _member_event( + user_id: str, membership: str, sender: Optional[str] = None +) -> EventBase: return make_event_from_dict( { "room_id": TEST_ROOM_ID, @@ -458,11 +461,11 @@ def _member_event(user_id, membership, sender=None): ) -def _join_event(user_id): +def _join_event(user_id: str) -> EventBase: return _member_event(user_id, "join") -def _power_levels_event(sender, content): +def _power_levels_event(sender: str, content: JsonDict) -> EventBase: return make_event_from_dict( { "room_id": TEST_ROOM_ID, @@ -475,7 +478,7 @@ def _power_levels_event(sender, content): ) -def _alias_event(sender, **kwargs): +def _alias_event(sender: str, **kwargs) -> EventBase: data = { "room_id": TEST_ROOM_ID, "event_id": _get_event_id(), @@ -488,7 +491,7 @@ def _alias_event(sender, **kwargs): return make_event_from_dict(data) -def _random_state_event(sender): +def _random_state_event(sender: str) -> EventBase: return make_event_from_dict( { "room_id": TEST_ROOM_ID, @@ -501,7 +504,7 @@ def _random_state_event(sender): ) -def _join_rules_event(sender, join_rule): +def _join_rules_event(sender: str, join_rule: str) -> EventBase: return make_event_from_dict( { "room_id": TEST_ROOM_ID, @@ -519,7 +522,7 @@ def _join_rules_event(sender, join_rule): event_count = 0 -def _get_event_id(): +def _get_event_id() -> str: global event_count c = event_count event_count += 1 From 07e0992a76b33de80616570582c17edd2768150f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 14 Jul 2021 14:41:23 +0100 Subject: [PATCH 405/619] Make GHA config more efficient (#10383) A few things here: * Build the debs for single distro for each PR, so that we can see if it breaks. Do the same for develop. Building all the debs ties up the GHA workers for ages. * Stop building the debs for release branches. Again, it takes ages, and I don't think anyone is actually going to stop and look at them. We'll know they are working when we make an RC. * Change the configs so that if we manually cancel a workflow, it actually does something. --- .github/workflows/release-artifacts.yml | 21 +++++++++++++-------- .github/workflows/tests.yml | 14 +++++++------- changelog.d/10383.misc | 1 + 3 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 changelog.d/10383.misc diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index f292d703ed..325c1f7d39 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -3,28 +3,33 @@ name: Build release artifacts on: + # we build on PRs and develop to (hopefully) get early warning + # of things breaking (but only build one set of debs) + pull_request: push: - # we build on develop and release branches to (hopefully) get early warning - # of things breaking - branches: ["develop", "release-*"] + branches: ["develop"] - # we also rebuild on tags, so that we can be sure of picking the artifacts - # from the right tag. + # we do the full build on tags. tags: ["v*"] permissions: contents: write jobs: - # first get the list of distros to build for. get-distros: + name: "Calculate list of debian distros" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - id: set-distros run: | - echo "::set-output name=distros::$(scripts-dev/build_debian_packages --show-dists-json)" + # if we're running from a tag, get the full list of distros; otherwise just use debian:sid + dists='["debian:sid"]' + if [[ $GITHUB_REF == refs/tags/* ]]; then + dists=$(scripts-dev/build_debian_packages --show-dists-json) + fi + echo "::set-output name=distros::$dists" # map the step outputs to job outputs outputs: distros: ${{ steps.set-distros.outputs.distros }} @@ -66,7 +71,7 @@ jobs: # if it's a tag, create a release and attach the artifacts to it attach-assets: name: "Attach assets to release" - if: startsWith(github.ref, 'refs/tags/') + if: ${{ !failure() && !cancelled() && startsWith(github.ref, 'refs/tags/') }} needs: - build-debs - build-sdist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bf36ee1cdf..505bac1308 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,14 +65,14 @@ jobs: # Dummy step to gate other tests on without repeating the whole list linting-done: - if: ${{ always() }} # Run this even if prior jobs were skipped + if: ${{ !cancelled() }} # Run this even if prior jobs were skipped needs: [lint, lint-crlf, lint-newsfile, lint-sdist] runs-on: ubuntu-latest steps: - run: "true" trial: - if: ${{ !failure() }} # Allow previous steps to be skipped, but not fail + if: ${{ !cancelled() && !failure() }} # Allow previous steps to be skipped, but not fail needs: linting-done runs-on: ubuntu-latest strategy: @@ -131,7 +131,7 @@ jobs: || true trial-olddeps: - if: ${{ !failure() }} # Allow previous steps to be skipped, but not fail + if: ${{ !cancelled() && !failure() }} # Allow previous steps to be skipped, but not fail needs: linting-done runs-on: ubuntu-latest steps: @@ -156,7 +156,7 @@ jobs: trial-pypy: # Very slow; only run if the branch name includes 'pypy' - if: ${{ contains(github.ref, 'pypy') && !failure() }} + if: ${{ contains(github.ref, 'pypy') && !failure() && !cancelled() }} needs: linting-done runs-on: ubuntu-latest strategy: @@ -185,7 +185,7 @@ jobs: || true sytest: - if: ${{ !failure() }} + if: ${{ !failure() && !cancelled() }} needs: linting-done runs-on: ubuntu-latest container: @@ -245,7 +245,7 @@ jobs: /logs/**/*.log* portdb: - if: ${{ !failure() }} # Allow previous steps to be skipped, but not fail + if: ${{ !failure() && !cancelled() }} # Allow previous steps to be skipped, but not fail needs: linting-done runs-on: ubuntu-latest strategy: @@ -286,7 +286,7 @@ jobs: - run: .buildkite/scripts/test_synapse_port_db.sh complement: - if: ${{ !failure() }} + if: ${{ !failure() && !cancelled() }} needs: linting-done runs-on: ubuntu-latest container: diff --git a/changelog.d/10383.misc b/changelog.d/10383.misc new file mode 100644 index 0000000000..952c1e77a8 --- /dev/null +++ b/changelog.d/10383.misc @@ -0,0 +1 @@ +Make the Github Actions workflow configuration more efficient. From c82eb02d6423a51852115bbda647fe12e86af673 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 14 Jul 2021 14:41:40 +0100 Subject: [PATCH 406/619] Set section for prerelease debs (#10391) This is part of fixing #6116: we want to put RC debs into a different place than release debs, so reprepro has to be able to tell them apart. --- changelog.d/10391.misc | 1 + docker/build_debian.sh | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 changelog.d/10391.misc diff --git a/changelog.d/10391.misc b/changelog.d/10391.misc new file mode 100644 index 0000000000..3f191b520a --- /dev/null +++ b/changelog.d/10391.misc @@ -0,0 +1 @@ +When building Debian packages for prerelease versions, set the Section accordingly. diff --git a/docker/build_debian.sh b/docker/build_debian.sh index f426d2b77b..f572ed9aa0 100644 --- a/docker/build_debian.sh +++ b/docker/build_debian.sh @@ -15,6 +15,20 @@ cd /synapse/build dch -M -l "+$DIST" "build for $DIST" dch -M -r "" --force-distribution --distribution "$DIST" +# if this is a prerelease, set the Section accordingly. +# +# When the package is later added to the package repo, reprepro will use the +# Section to determine which "component" it should go into (see +# https://manpages.debian.org/stretch/reprepro/reprepro.1.en.html#GUESSING) + +DEB_VERSION=`dpkg-parsechangelog -SVersion` +case $DEB_VERSION in + *rc*|*a*|*b*|*c*) + sed -ie '/^Section:/c\Section: prerelease' debian/control + ;; +esac + + dpkg-buildpackage -us -uc ls -l .. From 28ffff73c1f69be92155749275408b14ec7318d0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 14 Jul 2021 17:12:01 +0100 Subject: [PATCH 407/619] Instructions on installing RC debs (#10396) --- changelog.d/10396.doc | 1 + docs/setup/installation.md | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10396.doc diff --git a/changelog.d/10396.doc b/changelog.d/10396.doc new file mode 100644 index 0000000000..b521ad9cbf --- /dev/null +++ b/changelog.d/10396.doc @@ -0,0 +1 @@ +Add instructructions on installing Debian packages for release candidates. diff --git a/docs/setup/installation.md b/docs/setup/installation.md index d041d08333..afa57a825d 100644 --- a/docs/setup/installation.md +++ b/docs/setup/installation.md @@ -268,9 +268,8 @@ For more details, see ##### Matrix.org packages -Matrix.org provides Debian/Ubuntu packages of the latest stable version of -Synapse via . They are available for Debian -9 (Stretch), Ubuntu 16.04 (Xenial), and later. To use them: +Matrix.org provides Debian/Ubuntu packages of Synapse via +. To install the latest release: ```sh sudo apt install -y lsb-release wget apt-transport-https @@ -281,12 +280,16 @@ sudo apt update sudo apt install matrix-synapse-py3 ``` -**Note**: if you followed a previous version of these instructions which -recommended using `apt-key add` to add an old key from -`https://matrix.org/packages/debian/`, you should note that this key has been -revoked. You should remove the old key with `sudo apt-key remove -C35EB17E1EAE708E6603A9B3AD0592FE47F0DF61`, and follow the above instructions to -update your configuration. +Packages are also published for release candidates. To enable the prerelease +channel, add `prerelease` to the `sources.list` line. For example: + +```sh +sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg +echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main prerelease" | + sudo tee /etc/apt/sources.list.d/matrix-org.list +sudo apt update +sudo apt install matrix-synapse-py3 +``` The fingerprint of the repository signing key (as shown by `gpg /usr/share/keyrings/matrix-org-archive-keyring.gpg`) is From 0ae95b38474a4d64a4d5057499e645a3b81e3736 Mon Sep 17 00:00:00 2001 From: Moritz Dietz Date: Wed, 14 Jul 2021 18:50:30 +0200 Subject: [PATCH 408/619] doc: Add delegation example to the caddy reverse proxy section (#10368) --- changelog.d/10368.doc | 1 + docs/reverse_proxy.md | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 changelog.d/10368.doc diff --git a/changelog.d/10368.doc b/changelog.d/10368.doc new file mode 100644 index 0000000000..10297aa424 --- /dev/null +++ b/changelog.d/10368.doc @@ -0,0 +1 @@ +Add delegation example for caddy in the reverse proxy documentation. Contributed by @moritzdietz. diff --git a/docs/reverse_proxy.md b/docs/reverse_proxy.md index 01db466f96..0f3fbbed8b 100644 --- a/docs/reverse_proxy.md +++ b/docs/reverse_proxy.md @@ -98,6 +98,33 @@ example.com:8448 { reverse_proxy http://localhost:8008 } ``` +[Delegation](delegate.md) example: +``` +(matrix-well-known-header) { + # Headers + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" + header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization" + header Content-Type "application/json" +} + +example.com { + handle /.well-known/matrix/server { + import matrix-well-known-header + respond `{"m.server":"matrix.example.com:443"}` + } + + handle /.well-known/matrix/client { + import matrix-well-known-header + respond `{"m.homeserver":{"base_url":"https://matrix.example.com"},"m.identity_server":{"base_url":"https://identity.example.com"}}` + } +} + +matrix.example.com { + reverse_proxy /_matrix/* http://localhost:8008 + reverse_proxy /_synapse/client/* http://localhost:8008 +} +``` ### Apache From 7695ca06187bb6742ed74c5ae060c48a08af99ce Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 15 Jul 2021 10:35:46 +0100 Subject: [PATCH 409/619] Fix a number of logged errors caused by remote servers being down. (#10400) --- changelog.d/10400.bugfix | 1 + synapse/handlers/directory.py | 5 ++++- synapse/handlers/federation.py | 25 ++++++++++++++--------- synapse/handlers/room_list.py | 26 +++++++++++++++--------- synapse/http/matrixfederationclient.py | 28 ++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 changelog.d/10400.bugfix diff --git a/changelog.d/10400.bugfix b/changelog.d/10400.bugfix new file mode 100644 index 0000000000..bfebed8d29 --- /dev/null +++ b/changelog.d/10400.bugfix @@ -0,0 +1 @@ +Fix a number of logged errors caused by remote servers being down. diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 4064a2b859..06d7012bac 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -22,6 +22,7 @@ CodeMessageException, Codes, NotFoundError, + RequestSendFailed, ShadowBanError, StoreError, SynapseError, @@ -252,12 +253,14 @@ async def get_association(self, room_alias: RoomAlias) -> JsonDict: retry_on_dns_fail=False, ignore_backoff=True, ) + except RequestSendFailed: + raise SynapseError(502, "Failed to fetch alias") except CodeMessageException as e: logging.warning("Error retrieving alias") if e.code == 404: fed_result = None else: - raise + raise SynapseError(502, "Failed to fetch alias") if fed_result and "room_id" in fed_result and "servers" in fed_result: room_id = fed_result["room_id"] diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 991ec9919a..0209aee186 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1414,12 +1414,15 @@ async def send_invite(self, target_host: str, event: EventBase) -> EventBase: Invites must be signed by the invitee's server before distribution. """ - pdu = await self.federation_client.send_invite( - destination=target_host, - room_id=event.room_id, - event_id=event.event_id, - pdu=event, - ) + try: + pdu = await self.federation_client.send_invite( + destination=target_host, + room_id=event.room_id, + event_id=event.event_id, + pdu=event, + ) + except RequestSendFailed: + raise SynapseError(502, f"Can't connect to server {target_host}") return pdu @@ -3031,9 +3034,13 @@ async def exchange_third_party_invite( await member_handler.send_membership_event(None, event, context) else: destinations = {x.split(":", 1)[-1] for x in (sender_user_id, room_id)} - await self.federation_client.forward_third_party_invite( - destinations, room_id, event_dict - ) + + try: + await self.federation_client.forward_third_party_invite( + destinations, room_id, event_dict + ) + except (RequestSendFailed, HttpResponseException): + raise SynapseError(502, "Failed to forward third party invite") async def on_exchange_third_party_invite_request( self, event_dict: JsonDict diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index 5e3ef7ce3a..c6bfa5451f 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -20,7 +20,12 @@ from unpaddedbase64 import decode_base64, encode_base64 from synapse.api.constants import EventTypes, HistoryVisibility, JoinRules -from synapse.api.errors import Codes, HttpResponseException +from synapse.api.errors import ( + Codes, + HttpResponseException, + RequestSendFailed, + SynapseError, +) from synapse.types import JsonDict, ThirdPartyInstanceID from synapse.util.caches.descriptors import cached from synapse.util.caches.response_cache import ResponseCache @@ -417,14 +422,17 @@ async def _get_remote_list_cached( repl_layer = self.hs.get_federation_client() if search_filter: # We can't cache when asking for search - return await repl_layer.get_public_rooms( - server_name, - limit=limit, - since_token=since_token, - search_filter=search_filter, - include_all_networks=include_all_networks, - third_party_instance_id=third_party_instance_id, - ) + try: + return await repl_layer.get_public_rooms( + server_name, + limit=limit, + since_token=since_token, + search_filter=search_filter, + include_all_networks=include_all_networks, + third_party_instance_id=third_party_instance_id, + ) + except (RequestSendFailed, HttpResponseException): + raise SynapseError(502, "Failed to fetch room list") key = ( server_name, diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index b8849c0150..3bace2c965 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -43,6 +43,7 @@ from twisted.internet.error import DNSLookupError from twisted.internet.interfaces import IReactorTime from twisted.internet.task import _EPSILON, Cooperator +from twisted.web.client import ResponseFailed from twisted.web.http_headers import Headers from twisted.web.iweb import IBodyProducer, IResponse @@ -262,6 +263,15 @@ async def _handle_response( request.uri.decode("ascii"), ) raise RequestSendFailed(e, can_retry=True) from e + except ResponseFailed as e: + logger.warning( + "{%s} [%s] Failed to read response - %s %s", + request.txn_id, + request.destination, + request.method, + request.uri.decode("ascii"), + ) + raise RequestSendFailed(e, can_retry=True) from e except Exception as e: logger.warning( "{%s} [%s] Error reading response %s %s: %s", @@ -1137,6 +1147,24 @@ async def get_file( msg, ) raise SynapseError(502, msg, Codes.TOO_LARGE) + except defer.TimeoutError as e: + logger.warning( + "{%s} [%s] Timed out reading response - %s %s", + request.txn_id, + request.destination, + request.method, + request.uri.decode("ascii"), + ) + raise RequestSendFailed(e, can_retry=True) from e + except ResponseFailed as e: + logger.warning( + "{%s} [%s] Failed to read response - %s %s", + request.txn_id, + request.destination, + request.method, + request.uri.decode("ascii"), + ) + raise RequestSendFailed(e, can_retry=True) from e except Exception as e: logger.warning( "{%s} [%s] Error reading response: %s", From c7603af1d06d65932c420ae76002b6ed94dbf23c Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 15 Jul 2021 11:37:08 +0200 Subject: [PATCH 410/619] Allow providing credentials to `http_proxy` (#10360) --- changelog.d/10360.feature | 1 + synapse/http/proxyagent.py | 12 ++++++- tests/http/test_proxyagent.py | 65 ++++++++++++++++++++++++++++------- 3 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 changelog.d/10360.feature diff --git a/changelog.d/10360.feature b/changelog.d/10360.feature new file mode 100644 index 0000000000..904221cb6d --- /dev/null +++ b/changelog.d/10360.feature @@ -0,0 +1 @@ +Allow providing credentials to `http_proxy`. \ No newline at end of file diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py index 7dfae8b786..7a6a1717de 100644 --- a/synapse/http/proxyagent.py +++ b/synapse/http/proxyagent.py @@ -117,7 +117,8 @@ def __init__( https_proxy = proxies["https"].encode() if "https" in proxies else None no_proxy = proxies["no"] if "no" in proxies else None - # Parse credentials from https proxy connection string if present + # Parse credentials from http and https proxy connection string if present + self.http_proxy_creds, http_proxy = parse_username_password(http_proxy) self.https_proxy_creds, https_proxy = parse_username_password(https_proxy) self.http_proxy_endpoint = _http_proxy_endpoint( @@ -189,6 +190,15 @@ def request(self, method, uri, headers=None, bodyProducer=None): and self.http_proxy_endpoint and not should_skip_proxy ): + # Determine whether we need to set Proxy-Authorization headers + if self.http_proxy_creds: + # Set a Proxy-Authorization header + if headers is None: + headers = Headers() + headers.addRawHeader( + b"Proxy-Authorization", + self.http_proxy_creds.as_proxy_authorization_value(), + ) # Cache *all* connections under the same key, since we are only # connecting to a single destination, the proxy: pool_key = ("http-proxy", self.http_proxy_endpoint) diff --git a/tests/http/test_proxyagent.py b/tests/http/test_proxyagent.py index fefc8099c9..437113929a 100644 --- a/tests/http/test_proxyagent.py +++ b/tests/http/test_proxyagent.py @@ -205,6 +205,41 @@ def test_https_request_via_no_proxy_star(self): @patch.dict(os.environ, {"http_proxy": "proxy.com:8888", "no_proxy": "unused.com"}) def test_http_request_via_proxy(self): + """ + Tests that requests can be made through a proxy. + """ + self._do_http_request_via_proxy(auth_credentials=None) + + @patch.dict( + os.environ, + {"http_proxy": "bob:pinkponies@proxy.com:8888", "no_proxy": "unused.com"}, + ) + def test_http_request_via_proxy_with_auth(self): + """ + Tests that authenticated requests can be made through a proxy. + """ + self._do_http_request_via_proxy(auth_credentials="bob:pinkponies") + + @patch.dict(os.environ, {"https_proxy": "proxy.com", "no_proxy": "unused.com"}) + def test_https_request_via_proxy(self): + """Tests that TLS-encrypted requests can be made through a proxy""" + self._do_https_request_via_proxy(auth_credentials=None) + + @patch.dict( + os.environ, + {"https_proxy": "bob:pinkponies@proxy.com", "no_proxy": "unused.com"}, + ) + def test_https_request_via_proxy_with_auth(self): + """Tests that authenticated, TLS-encrypted requests can be made through a proxy""" + self._do_https_request_via_proxy(auth_credentials="bob:pinkponies") + + def _do_http_request_via_proxy( + self, + auth_credentials: Optional[str] = None, + ): + """ + Tests that requests can be made through a proxy. + """ agent = ProxyAgent(self.reactor, use_proxy=True) self.reactor.lookups["proxy.com"] = "1.2.3.5" @@ -229,6 +264,23 @@ def test_http_request_via_proxy(self): self.assertEqual(len(http_server.requests), 1) request = http_server.requests[0] + + # Check whether auth credentials have been supplied to the proxy + proxy_auth_header_values = request.requestHeaders.getRawHeaders( + b"Proxy-Authorization" + ) + + if auth_credentials is not None: + # Compute the correct header value for Proxy-Authorization + encoded_credentials = base64.b64encode(b"bob:pinkponies") + expected_header_value = b"Basic " + encoded_credentials + + # Validate the header's value + self.assertIn(expected_header_value, proxy_auth_header_values) + else: + # Check that the Proxy-Authorization header has not been supplied to the proxy + self.assertIsNone(proxy_auth_header_values) + self.assertEqual(request.method, b"GET") self.assertEqual(request.path, b"http://test.com") self.assertEqual(request.requestHeaders.getRawHeaders(b"host"), [b"test.com"]) @@ -241,19 +293,6 @@ def test_http_request_via_proxy(self): body = self.successResultOf(treq.content(resp)) self.assertEqual(body, b"result") - @patch.dict(os.environ, {"https_proxy": "proxy.com", "no_proxy": "unused.com"}) - def test_https_request_via_proxy(self): - """Tests that TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(auth_credentials=None) - - @patch.dict( - os.environ, - {"https_proxy": "bob:pinkponies@proxy.com", "no_proxy": "unused.com"}, - ) - def test_https_request_via_proxy_with_auth(self): - """Tests that authenticated, TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(auth_credentials="bob:pinkponies") - def _do_https_request_via_proxy( self, auth_credentials: Optional[str] = None, From bf72d10dbf506f5ea486d67094b6003947d38fb7 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 15 Jul 2021 12:02:43 +0200 Subject: [PATCH 411/619] Use inline type hints in various other places (in `synapse/`) (#10380) --- changelog.d/10380.misc | 1 + synapse/api/auth.py | 4 +-- synapse/api/errors.py | 4 +-- synapse/api/filtering.py | 2 +- synapse/api/ratelimiting.py | 4 +-- synapse/api/room_versions.py | 4 +-- synapse/app/generic_worker.py | 2 +- synapse/appservice/api.py | 4 +-- synapse/config/appservice.py | 4 +-- synapse/config/cache.py | 4 +-- synapse/config/emailconfig.py | 4 +-- synapse/config/experimental.py | 6 ++-- synapse/config/federation.py | 2 +- synapse/config/oidc.py | 2 +- synapse/config/password_auth_providers.py | 2 +- synapse/config/repository.py | 4 +-- synapse/config/server.py | 16 ++++----- synapse/config/spam_checker.py | 2 +- synapse/config/sso.py | 2 +- synapse/config/tls.py | 6 ++-- synapse/crypto/keyring.py | 20 ++++++----- synapse/event_auth.py | 8 ++--- synapse/events/__init__.py | 26 +++++++------- synapse/events/builder.py | 16 ++++----- synapse/events/spamcheck.py | 4 +-- synapse/federation/federation_client.py | 10 +++--- synapse/federation/federation_server.py | 34 ++++++++----------- synapse/federation/send_queue.py | 26 +++++++------- synapse/federation/sender/__init__.py | 14 ++++---- .../sender/per_destination_queue.py | 18 +++++----- synapse/federation/transport/client.py | 8 ++--- synapse/federation/transport/server.py | 24 ++++++------- synapse/groups/groups_server.py | 12 +++---- synapse/http/__init__.py | 2 +- synapse/http/client.py | 18 +++++----- synapse/http/matrixfederationclient.py | 12 +++---- synapse/http/server.py | 8 ++--- synapse/http/servlet.py | 2 +- synapse/http/site.py | 14 ++++---- synapse/logging/_remote.py | 14 ++++---- synapse/logging/_structured.py | 2 +- synapse/logging/context.py | 16 ++++----- synapse/logging/opentracing.py | 10 +++--- synapse/metrics/__init__.py | 6 ++-- synapse/metrics/_exposition.py | 2 +- synapse/metrics/background_process_metrics.py | 4 +-- synapse/module_api/__init__.py | 2 +- synapse/notifier.py | 18 +++++----- synapse/push/bulk_push_rule_evaluator.py | 4 +-- synapse/push/clientformat.py | 4 +-- synapse/push/emailpusher.py | 6 ++-- synapse/push/httppusher.py | 2 +- synapse/push/mailer.py | 12 +++---- synapse/push/presentable_names.py | 2 +- synapse/push/push_rule_evaluator.py | 4 +-- synapse/push/pusher.py | 6 ++-- synapse/push/pusherpool.py | 2 +- synapse/python_dependencies.py | 4 +-- synapse/replication/http/_base.py | 10 +++--- synapse/replication/slave/storage/_base.py | 6 ++-- .../replication/slave/storage/client_ips.py | 4 +-- synapse/replication/tcp/client.py | 10 +++--- synapse/replication/tcp/commands.py | 6 ++-- synapse/replication/tcp/handler.py | 16 ++++----- synapse/replication/tcp/protocol.py | 14 ++++---- synapse/replication/tcp/redis.py | 8 ++--- synapse/replication/tcp/streams/_base.py | 14 ++++---- synapse/replication/tcp/streams/events.py | 28 +++++++-------- synapse/replication/tcp/streams/federation.py | 6 ++-- synapse/server.py | 6 ++-- .../server_notices/consent_server_notices.py | 2 +- .../resource_limits_server_notices.py | 2 +- .../server_notices/server_notices_sender.py | 6 ++-- synapse/state/__init__.py | 20 ++++++----- synapse/state/v1.py | 2 +- synapse/state/v2.py | 18 +++++----- synapse/streams/events.py | 4 +-- synapse/types.py | 6 ++-- synapse/visibility.py | 2 +- 79 files changed, 329 insertions(+), 336 deletions(-) create mode 100644 changelog.d/10380.misc diff --git a/changelog.d/10380.misc b/changelog.d/10380.misc new file mode 100644 index 0000000000..eed2d8552a --- /dev/null +++ b/changelog.d/10380.misc @@ -0,0 +1 @@ +Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 42476a18e5..8916e6fa2f 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -63,9 +63,9 @@ def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() self.state = hs.get_state_handler() - self.token_cache = LruCache( + self.token_cache: LruCache[str, Tuple[str, bool]] = LruCache( 10000, "token_cache" - ) # type: LruCache[str, Tuple[str, bool]] + ) self._auth_blocking = AuthBlocking(self.hs) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 4cb8bbaf70..054ab14ab6 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -118,7 +118,7 @@ def __init__(self, location: bytes, http_code: int = http.FOUND): super().__init__(code=http_code, msg=msg) self.location = location - self.cookies = [] # type: List[bytes] + self.cookies: List[bytes] = [] class SynapseError(CodeMessageException): @@ -160,7 +160,7 @@ def __init__( ): super().__init__(code, msg, errcode) if additional_fields is None: - self._additional_fields = {} # type: Dict + self._additional_fields: Dict = {} else: self._additional_fields = dict(additional_fields) diff --git a/synapse/api/filtering.py b/synapse/api/filtering.py index ce49a0ad58..ad1ff6a9df 100644 --- a/synapse/api/filtering.py +++ b/synapse/api/filtering.py @@ -289,7 +289,7 @@ def check(self, event): room_id = None ev_type = "m.presence" contains_url = False - labels = [] # type: List[str] + labels: List[str] = [] else: sender = event.get("sender", None) if not sender: diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py index b9a10283f4..3e3d09bbd2 100644 --- a/synapse/api/ratelimiting.py +++ b/synapse/api/ratelimiting.py @@ -46,9 +46,7 @@ def __init__( # * How many times an action has occurred since a point in time # * The point in time # * The rate_hz of this particular entry. This can vary per request - self.actions = ( - OrderedDict() - ) # type: OrderedDict[Hashable, Tuple[float, int, float]] + self.actions: OrderedDict[Hashable, Tuple[float, int, float]] = OrderedDict() async def can_do_action( self, diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index f6c1c97b40..a20abc5a65 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -195,7 +195,7 @@ class RoomVersions: ) -KNOWN_ROOM_VERSIONS = { +KNOWN_ROOM_VERSIONS: Dict[str, RoomVersion] = { v.identifier: v for v in ( RoomVersions.V1, @@ -209,4 +209,4 @@ class RoomVersions: RoomVersions.V7, ) # Note that we do not include MSC2043 here unless it is enabled in the config. -} # type: Dict[str, RoomVersion] +} diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 5b041fcaad..b43d858f59 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -270,7 +270,7 @@ def _listen_http(self, listener_config: ListenerConfig): site_tag = port # We always include a health resource. - resources = {"/health": HealthResource()} # type: Dict[str, IResource] + resources: Dict[str, IResource] = {"/health": HealthResource()} for res in listener_config.http_options.resources: for name in res.names: diff --git a/synapse/appservice/api.py b/synapse/appservice/api.py index 61152b2c46..935f24263c 100644 --- a/synapse/appservice/api.py +++ b/synapse/appservice/api.py @@ -88,9 +88,9 @@ def __init__(self, hs): super().__init__(hs) self.clock = hs.get_clock() - self.protocol_meta_cache = ResponseCache( + self.protocol_meta_cache: ResponseCache[Tuple[str, str]] = ResponseCache( hs.get_clock(), "as_protocol_meta", timeout_ms=HOUR_IN_MS - ) # type: ResponseCache[Tuple[str, str]] + ) async def query_user(self, service, user_id): if service.url is None: diff --git a/synapse/config/appservice.py b/synapse/config/appservice.py index 746fc3cc02..a39d457c56 100644 --- a/synapse/config/appservice.py +++ b/synapse/config/appservice.py @@ -57,8 +57,8 @@ def load_appservices(hostname, config_files): return [] # Dicts of value -> filename - seen_as_tokens = {} # type: Dict[str, str] - seen_ids = {} # type: Dict[str, str] + seen_as_tokens: Dict[str, str] = {} + seen_ids: Dict[str, str] = {} appservices = [] diff --git a/synapse/config/cache.py b/synapse/config/cache.py index 7789b40323..8d5f38b5d9 100644 --- a/synapse/config/cache.py +++ b/synapse/config/cache.py @@ -25,7 +25,7 @@ _CACHE_PREFIX = "SYNAPSE_CACHE_FACTOR" # Map from canonicalised cache name to cache. -_CACHES = {} # type: Dict[str, Callable[[float], None]] +_CACHES: Dict[str, Callable[[float], None]] = {} # a lock on the contents of _CACHES _CACHES_LOCK = threading.Lock() @@ -157,7 +157,7 @@ def read_config(self, config, **kwargs): self.event_cache_size = self.parse_size( config.get("event_cache_size", _DEFAULT_EVENT_CACHE_SIZE) ) - self.cache_factors = {} # type: Dict[str, float] + self.cache_factors: Dict[str, float] = {} cache_config = config.get("caches") or {} self.global_factor = cache_config.get( diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 5564d7d097..bcecbfec03 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -134,9 +134,9 @@ def read_config(self, config, **kwargs): # trusted_third_party_id_servers does not contain a scheme whereas # account_threepid_delegate_email is expected to. Presume https - self.account_threepid_delegate_email = ( + self.account_threepid_delegate_email: Optional[str] = ( "https://" + first_trusted_identity_server - ) # type: Optional[str] + ) self.using_identity_server_from_trusted_list = True else: raise ConfigError( diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 7fb1f7021f..e25ccba9ac 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -25,10 +25,10 @@ def read_config(self, config: JsonDict, **kwargs): experimental = config.get("experimental_features") or {} # MSC2858 (multiple SSO identity providers) - self.msc2858_enabled = experimental.get("msc2858_enabled", False) # type: bool + self.msc2858_enabled: bool = experimental.get("msc2858_enabled", False) # MSC3026 (busy presence state) - self.msc3026_enabled = experimental.get("msc3026_enabled", False) # type: bool + self.msc3026_enabled: bool = experimental.get("msc3026_enabled", False) # MSC2716 (backfill existing history) - self.msc2716_enabled = experimental.get("msc2716_enabled", False) # type: bool + self.msc2716_enabled: bool = experimental.get("msc2716_enabled", False) diff --git a/synapse/config/federation.py b/synapse/config/federation.py index cdd7a1ef05..7d64993e22 100644 --- a/synapse/config/federation.py +++ b/synapse/config/federation.py @@ -22,7 +22,7 @@ class FederationConfig(Config): def read_config(self, config, **kwargs): # FIXME: federation_domain_whitelist needs sytests - self.federation_domain_whitelist = None # type: Optional[dict] + self.federation_domain_whitelist: Optional[dict] = None federation_domain_whitelist = config.get("federation_domain_whitelist", None) if federation_domain_whitelist is not None: diff --git a/synapse/config/oidc.py b/synapse/config/oidc.py index 942e2672a9..ba89d11cf0 100644 --- a/synapse/config/oidc.py +++ b/synapse/config/oidc.py @@ -460,7 +460,7 @@ def _parse_oidc_config_dict( ) from e client_secret_jwt_key_config = oidc_config.get("client_secret_jwt_key") - client_secret_jwt_key = None # type: Optional[OidcProviderClientSecretJwtKey] + client_secret_jwt_key: Optional[OidcProviderClientSecretJwtKey] = None if client_secret_jwt_key_config is not None: keyfile = client_secret_jwt_key_config.get("key_file") if keyfile: diff --git a/synapse/config/password_auth_providers.py b/synapse/config/password_auth_providers.py index fd90b79772..0f5b2b3977 100644 --- a/synapse/config/password_auth_providers.py +++ b/synapse/config/password_auth_providers.py @@ -25,7 +25,7 @@ class PasswordAuthProviderConfig(Config): section = "authproviders" def read_config(self, config, **kwargs): - self.password_providers = [] # type: List[Any] + self.password_providers: List[Any] = [] providers = [] # We want to be backwards compatible with the old `ldap_config` diff --git a/synapse/config/repository.py b/synapse/config/repository.py index a7a82742ac..0dfb3a227a 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -62,7 +62,7 @@ def parse_thumbnail_requirements(thumbnail_sizes): Dictionary mapping from media type string to list of ThumbnailRequirement tuples. """ - requirements = {} # type: Dict[str, List] + requirements: Dict[str, List] = {} for size in thumbnail_sizes: width = size["width"] height = size["height"] @@ -141,7 +141,7 @@ def read_config(self, config, **kwargs): # # We don't create the storage providers here as not all workers need # them to be started. - self.media_storage_providers = [] # type: List[tuple] + self.media_storage_providers: List[tuple] = [] for i, provider_config in enumerate(storage_providers): # We special case the module "file_system" so as not to need to diff --git a/synapse/config/server.py b/synapse/config/server.py index 6bff715230..b9e0c0b300 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -505,7 +505,7 @@ def read_config(self, config, **kwargs): " greater than 'allowed_lifetime_max'" ) - self.retention_purge_jobs = [] # type: List[Dict[str, Optional[int]]] + self.retention_purge_jobs: List[Dict[str, Optional[int]]] = [] for purge_job_config in retention_config.get("purge_jobs", []): interval_config = purge_job_config.get("interval") @@ -688,23 +688,21 @@ class LimitRemoteRoomsConfig: # not included in the sample configuration file on purpose as it's a temporary # hack, so that some users can trial the new defaults without impacting every # user on the homeserver. - users_new_default_push_rules = ( + users_new_default_push_rules: list = ( config.get("users_new_default_push_rules") or [] - ) # type: list + ) if not isinstance(users_new_default_push_rules, list): raise ConfigError("'users_new_default_push_rules' must be a list") # Turn the list into a set to improve lookup speed. - self.users_new_default_push_rules = set( - users_new_default_push_rules - ) # type: set + self.users_new_default_push_rules: set = set(users_new_default_push_rules) # Whitelist of domain names that given next_link parameters must have - next_link_domain_whitelist = config.get( + next_link_domain_whitelist: Optional[List[str]] = config.get( "next_link_domain_whitelist" - ) # type: Optional[List[str]] + ) - self.next_link_domain_whitelist = None # type: Optional[Set[str]] + self.next_link_domain_whitelist: Optional[Set[str]] = None if next_link_domain_whitelist is not None: if not isinstance(next_link_domain_whitelist, list): raise ConfigError("'next_link_domain_whitelist' must be a list") diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py index cb7716c837..a233a9ce03 100644 --- a/synapse/config/spam_checker.py +++ b/synapse/config/spam_checker.py @@ -34,7 +34,7 @@ class SpamCheckerConfig(Config): section = "spamchecker" def read_config(self, config, **kwargs): - self.spam_checkers = [] # type: List[Tuple[Any, Dict]] + self.spam_checkers: List[Tuple[Any, Dict]] = [] spam_checkers = config.get("spam_checker") or [] if isinstance(spam_checkers, dict): diff --git a/synapse/config/sso.py b/synapse/config/sso.py index e4346e02aa..d0f04cf8e6 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -39,7 +39,7 @@ class SSOConfig(Config): section = "sso" def read_config(self, config, **kwargs): - sso_config = config.get("sso") or {} # type: Dict[str, Any] + sso_config: Dict[str, Any] = config.get("sso") or {} # The sso-specific template_dir self.sso_template_dir = sso_config.get("template_dir") diff --git a/synapse/config/tls.py b/synapse/config/tls.py index 9a16a8fbae..fed05ac7be 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -80,7 +80,7 @@ def read_config(self, config: dict, config_dir_path: str, **kwargs): fed_whitelist_entries = [] # Support globs (*) in whitelist values - self.federation_certificate_verification_whitelist = [] # type: List[Pattern] + self.federation_certificate_verification_whitelist: List[Pattern] = [] for entry in fed_whitelist_entries: try: entry_regex = glob_to_regex(entry.encode("ascii").decode("ascii")) @@ -132,8 +132,8 @@ def read_config(self, config: dict, config_dir_path: str, **kwargs): "use_insecure_ssl_client_just_for_testing_do_not_use" ) - self.tls_certificate = None # type: Optional[crypto.X509] - self.tls_private_key = None # type: Optional[crypto.PKey] + self.tls_certificate: Optional[crypto.X509] = None + self.tls_private_key: Optional[crypto.PKey] = None def is_disk_cert_valid(self, allow_self_signed=True): """ diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py index e5a4685ed4..9e9b1c1c86 100644 --- a/synapse/crypto/keyring.py +++ b/synapse/crypto/keyring.py @@ -170,11 +170,13 @@ def __init__( ) self._key_fetchers = key_fetchers - self._server_queue = BatchingQueue( + self._server_queue: BatchingQueue[ + _FetchKeyRequest, Dict[str, Dict[str, FetchKeyResult]] + ] = BatchingQueue( "keyring_server", clock=hs.get_clock(), process_batch_callback=self._inner_fetch_key_requests, - ) # type: BatchingQueue[_FetchKeyRequest, Dict[str, Dict[str, FetchKeyResult]]] + ) async def verify_json_for_server( self, @@ -330,7 +332,7 @@ async def _inner_fetch_key_requests( # First we need to deduplicate requests for the same key. We do this by # taking the *maximum* requested `minimum_valid_until_ts` for each pair # of server name/key ID. - server_to_key_to_ts = {} # type: Dict[str, Dict[str, int]] + server_to_key_to_ts: Dict[str, Dict[str, int]] = {} for request in requests: by_server = server_to_key_to_ts.setdefault(request.server_name, {}) for key_id in request.key_ids: @@ -355,7 +357,7 @@ async def _inner_fetch_key_requests( # We now convert the returned list of results into a map from server # name to key ID to FetchKeyResult, to return. - to_return = {} # type: Dict[str, Dict[str, FetchKeyResult]] + to_return: Dict[str, Dict[str, FetchKeyResult]] = {} for (request, results) in zip(deduped_requests, results_per_request): to_return_by_server = to_return.setdefault(request.server_name, {}) for key_id, key_result in results.items(): @@ -455,7 +457,7 @@ async def _fetch_keys(self, keys_to_fetch: List[_FetchKeyRequest]): ) res = await self.store.get_server_verify_keys(key_ids_to_fetch) - keys = {} # type: Dict[str, Dict[str, FetchKeyResult]] + keys: Dict[str, Dict[str, FetchKeyResult]] = {} for (server_name, key_id), key in res.items(): keys.setdefault(server_name, {})[key_id] = key return keys @@ -603,7 +605,7 @@ async def get_key(key_server: TrustedKeyServer) -> Dict: ).addErrback(unwrapFirstError) ) - union_of_keys = {} # type: Dict[str, Dict[str, FetchKeyResult]] + union_of_keys: Dict[str, Dict[str, FetchKeyResult]] = {} for result in results: for server_name, keys in result.items(): union_of_keys.setdefault(server_name, {}).update(keys) @@ -656,8 +658,8 @@ async def get_server_verify_key_v2_indirect( except HttpResponseException as e: raise KeyLookupError("Remote server returned an error: %s" % (e,)) - keys = {} # type: Dict[str, Dict[str, FetchKeyResult]] - added_keys = [] # type: List[Tuple[str, str, FetchKeyResult]] + keys: Dict[str, Dict[str, FetchKeyResult]] = {} + added_keys: List[Tuple[str, str, FetchKeyResult]] = [] time_now_ms = self.clock.time_msec() @@ -805,7 +807,7 @@ async def get_server_verify_key_v2_direct( Raises: KeyLookupError if there was a problem making the lookup """ - keys = {} # type: Dict[str, FetchKeyResult] + keys: Dict[str, FetchKeyResult] = {} for requested_key_id in key_ids: # we may have found this key as a side-effect of asking for another. diff --git a/synapse/event_auth.py b/synapse/event_auth.py index a3df6cfcc1..137dff2513 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -531,7 +531,7 @@ def _check_power_levels( user_level = get_user_power_level(event.user_id, auth_events) # Check other levels: - levels_to_check = [ + levels_to_check: List[Tuple[str, Optional[str]]] = [ ("users_default", None), ("events_default", None), ("state_default", None), @@ -539,7 +539,7 @@ def _check_power_levels( ("redact", None), ("kick", None), ("invite", None), - ] # type: List[Tuple[str, Optional[str]]] + ] old_list = current_state.content.get("users", {}) for user in set(list(old_list) + list(user_list)): @@ -569,12 +569,12 @@ def _check_power_levels( new_loc = new_loc.get(dir, {}) if level_to_check in old_loc: - old_level = int(old_loc[level_to_check]) # type: Optional[int] + old_level: Optional[int] = int(old_loc[level_to_check]) else: old_level = None if level_to_check in new_loc: - new_level = int(new_loc[level_to_check]) # type: Optional[int] + new_level: Optional[int] = int(new_loc[level_to_check]) else: new_level = None diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 6286ad999a..65dc7a4ed0 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -105,28 +105,28 @@ def __init__(self, internal_metadata_dict: JsonDict): self._dict = dict(internal_metadata_dict) # the stream ordering of this event. None, until it has been persisted. - self.stream_ordering = None # type: Optional[int] + self.stream_ordering: Optional[int] = None # whether this event is an outlier (ie, whether we have the state at that point # in the DAG) self.outlier = False - out_of_band_membership = DictProperty("out_of_band_membership") # type: bool - send_on_behalf_of = DictProperty("send_on_behalf_of") # type: str - recheck_redaction = DictProperty("recheck_redaction") # type: bool - soft_failed = DictProperty("soft_failed") # type: bool - proactively_send = DictProperty("proactively_send") # type: bool - redacted = DictProperty("redacted") # type: bool - txn_id = DictProperty("txn_id") # type: str - token_id = DictProperty("token_id") # type: int - historical = DictProperty("historical") # type: bool + out_of_band_membership: bool = DictProperty("out_of_band_membership") + send_on_behalf_of: str = DictProperty("send_on_behalf_of") + recheck_redaction: bool = DictProperty("recheck_redaction") + soft_failed: bool = DictProperty("soft_failed") + proactively_send: bool = DictProperty("proactively_send") + redacted: bool = DictProperty("redacted") + txn_id: str = DictProperty("txn_id") + token_id: int = DictProperty("token_id") + historical: bool = DictProperty("historical") # XXX: These are set by StreamWorkerStore._set_before_and_after. # I'm pretty sure that these are never persisted to the database, so shouldn't # be here - before = DictProperty("before") # type: RoomStreamToken - after = DictProperty("after") # type: RoomStreamToken - order = DictProperty("order") # type: Tuple[int, int] + before: RoomStreamToken = DictProperty("before") + after: RoomStreamToken = DictProperty("after") + order: Tuple[int, int] = DictProperty("order") def get_dict(self) -> JsonDict: return dict(self._dict) diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 26e3950859..87e2bb123b 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -132,12 +132,12 @@ async def build( format_version = self.room_version.event_format if format_version == EventFormatVersions.V1: # The types of auth/prev events changes between event versions. - auth_events = await self._store.add_event_hashes( - auth_event_ids - ) # type: Union[List[str], List[Tuple[str, Dict[str, str]]]] - prev_events = await self._store.add_event_hashes( - prev_event_ids - ) # type: Union[List[str], List[Tuple[str, Dict[str, str]]]] + auth_events: Union[ + List[str], List[Tuple[str, Dict[str, str]]] + ] = await self._store.add_event_hashes(auth_event_ids) + prev_events: Union[ + List[str], List[Tuple[str, Dict[str, str]]] + ] = await self._store.add_event_hashes(prev_event_ids) else: auth_events = auth_event_ids prev_events = prev_event_ids @@ -156,7 +156,7 @@ async def build( # the db) depth = min(depth, MAX_DEPTH) - event_dict = { + event_dict: Dict[str, Any] = { "auth_events": auth_events, "prev_events": prev_events, "type": self.type, @@ -166,7 +166,7 @@ async def build( "unsigned": self.unsigned, "depth": depth, "prev_state": [], - } # type: Dict[str, Any] + } if self.is_state(): event_dict["state_key"] = self._state_key diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index efec16c226..57f1d53fa8 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -76,7 +76,7 @@ def load_legacy_spam_checkers(hs: "synapse.server.HomeServer"): """Wrapper that loads spam checkers configured using the old configuration, and registers the spam checker hooks they implement. """ - spam_checkers = [] # type: List[Any] + spam_checkers: List[Any] = [] api = hs.get_module_api() for module, config in hs.config.spam_checkers: # Older spam checkers don't accept the `api` argument, so we @@ -239,7 +239,7 @@ async def check_event_for_spam( will be used as the error message returned to the user. """ for callback in self._check_event_for_spam_callbacks: - res = await callback(event) # type: Union[bool, str] + res: Union[bool, str] = await callback(event) if res: return res diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index ed09c6af1f..c767d30627 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -86,7 +86,7 @@ class FederationClient(FederationBase): def __init__(self, hs: "HomeServer"): super().__init__(hs) - self.pdu_destination_tried = {} # type: Dict[str, Dict[str, int]] + self.pdu_destination_tried: Dict[str, Dict[str, int]] = {} self._clock.looping_call(self._clear_tried_cache, 60 * 1000) self.state = hs.get_state_handler() self.transport_layer = hs.get_federation_transport_client() @@ -94,13 +94,13 @@ def __init__(self, hs: "HomeServer"): self.hostname = hs.hostname self.signing_key = hs.signing_key - self._get_pdu_cache = ExpiringCache( + self._get_pdu_cache: ExpiringCache[str, EventBase] = ExpiringCache( cache_name="get_pdu_cache", clock=self._clock, max_len=1000, expiry_ms=120 * 1000, reset_expiry_on_get=False, - ) # type: ExpiringCache[str, EventBase] + ) def _clear_tried_cache(self): """Clear pdu_destination_tried cache""" @@ -293,10 +293,10 @@ async def get_pdu( transaction_data, ) - pdu_list = [ + pdu_list: List[EventBase] = [ event_from_pdu_json(p, room_version, outlier=outlier) for p in transaction_data["pdus"] - ] # type: List[EventBase] + ] if pdu_list and pdu_list[0]: pdu = pdu_list[0] diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index ac0f2ccfb3..d91f0ff32f 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -122,12 +122,12 @@ def __init__(self, hs: "HomeServer"): # origins that we are currently processing a transaction from. # a dict from origin to txn id. - self._active_transactions = {} # type: Dict[str, str] + self._active_transactions: Dict[str, str] = {} # We cache results for transaction with the same ID - self._transaction_resp_cache = ResponseCache( + self._transaction_resp_cache: ResponseCache[Tuple[str, str]] = ResponseCache( hs.get_clock(), "fed_txn_handler", timeout_ms=30000 - ) # type: ResponseCache[Tuple[str, str]] + ) self.transaction_actions = TransactionActions(self.store) @@ -135,12 +135,12 @@ def __init__(self, hs: "HomeServer"): # We cache responses to state queries, as they take a while and often # come in waves. - self._state_resp_cache = ResponseCache( - hs.get_clock(), "state_resp", timeout_ms=30000 - ) # type: ResponseCache[Tuple[str, Optional[str]]] - self._state_ids_resp_cache = ResponseCache( + self._state_resp_cache: ResponseCache[ + Tuple[str, Optional[str]] + ] = ResponseCache(hs.get_clock(), "state_resp", timeout_ms=30000) + self._state_ids_resp_cache: ResponseCache[Tuple[str, str]] = ResponseCache( hs.get_clock(), "state_ids_resp", timeout_ms=30000 - ) # type: ResponseCache[Tuple[str, str]] + ) self._federation_metrics_domains = ( hs.config.federation.federation_metrics_domains @@ -337,7 +337,7 @@ async def _handle_pdus_in_txn( origin_host, _ = parse_server_name(origin) - pdus_by_room = {} # type: Dict[str, List[EventBase]] + pdus_by_room: Dict[str, List[EventBase]] = {} newest_pdu_ts = 0 @@ -516,9 +516,9 @@ async def _on_context_state_request_compute( self, room_id: str, event_id: Optional[str] ) -> Dict[str, list]: if event_id: - pdus = await self.handler.get_state_for_pdu( + pdus: Iterable[EventBase] = await self.handler.get_state_for_pdu( room_id, event_id - ) # type: Iterable[EventBase] + ) else: pdus = (await self.state.get_current_state(room_id)).values() @@ -791,7 +791,7 @@ async def on_claim_client_keys( log_kv({"message": "Claiming one time keys.", "user, device pairs": query}) results = await self.store.claim_e2e_one_time_keys(query) - json_result = {} # type: Dict[str, Dict[str, dict]] + json_result: Dict[str, Dict[str, dict]] = {} for user_id, device_keys in results.items(): for device_id, keys in device_keys.items(): for key_id, json_str in keys.items(): @@ -1119,17 +1119,13 @@ def __init__(self, hs: "HomeServer"): self._get_query_client = ReplicationGetQueryRestServlet.make_client(hs) self._send_edu = ReplicationFederationSendEduRestServlet.make_client(hs) - self.edu_handlers = ( - {} - ) # type: Dict[str, Callable[[str, dict], Awaitable[None]]] - self.query_handlers = ( - {} - ) # type: Dict[str, Callable[[dict], Awaitable[JsonDict]]] + self.edu_handlers: Dict[str, Callable[[str, dict], Awaitable[None]]] = {} + self.query_handlers: Dict[str, Callable[[dict], Awaitable[JsonDict]]] = {} # Map from type to instance names that we should route EDU handling to. # We randomly choose one instance from the list to route to for each new # EDU received. - self._edu_type_to_instance = {} # type: Dict[str, List[str]] + self._edu_type_to_instance: Dict[str, List[str]] = {} def register_edu_handler( self, edu_type: str, handler: Callable[[str, JsonDict], Awaitable[None]] diff --git a/synapse/federation/send_queue.py b/synapse/federation/send_queue.py index 65d76ea974..1fbf325fdc 100644 --- a/synapse/federation/send_queue.py +++ b/synapse/federation/send_queue.py @@ -71,34 +71,32 @@ def __init__(self, hs: "HomeServer"): # We may have multiple federation sender instances, so we need to track # their positions separately. self._sender_instances = hs.config.worker.federation_shard_config.instances - self._sender_positions = {} # type: Dict[str, int] + self._sender_positions: Dict[str, int] = {} # Pending presence map user_id -> UserPresenceState - self.presence_map = {} # type: Dict[str, UserPresenceState] + self.presence_map: Dict[str, UserPresenceState] = {} # Stores the destinations we need to explicitly send presence to about a # given user. # Stream position -> (user_id, destinations) - self.presence_destinations = ( - SortedDict() - ) # type: SortedDict[int, Tuple[str, Iterable[str]]] + self.presence_destinations: SortedDict[ + int, Tuple[str, Iterable[str]] + ] = SortedDict() # (destination, key) -> EDU - self.keyed_edu = {} # type: Dict[Tuple[str, tuple], Edu] + self.keyed_edu: Dict[Tuple[str, tuple], Edu] = {} # stream position -> (destination, key) - self.keyed_edu_changed = ( - SortedDict() - ) # type: SortedDict[int, Tuple[str, tuple]] + self.keyed_edu_changed: SortedDict[int, Tuple[str, tuple]] = SortedDict() - self.edus = SortedDict() # type: SortedDict[int, Edu] + self.edus: SortedDict[int, Edu] = SortedDict() # stream ID for the next entry into keyed_edu_changed/edus. self.pos = 1 # map from stream ID to the time that stream entry was generated, so that we # can clear out entries after a while - self.pos_time = SortedDict() # type: SortedDict[int, int] + self.pos_time: SortedDict[int, int] = SortedDict() # EVERYTHING IS SAD. In particular, python only makes new scopes when # we make a new function, so we need to make a new function so the inner @@ -291,7 +289,7 @@ async def get_replication_rows( # list of tuple(int, BaseFederationRow), where the first is the position # of the federation stream. - rows = [] # type: List[Tuple[int, BaseFederationRow]] + rows: List[Tuple[int, BaseFederationRow]] = [] # Fetch presence to send to destinations i = self.presence_destinations.bisect_right(from_token) @@ -445,11 +443,11 @@ def add_to_buffer(self, buff): buff.edus.setdefault(self.edu.destination, []).append(self.edu) -_rowtypes = ( +_rowtypes: Tuple[Type[BaseFederationRow], ...] = ( PresenceDestinationsRow, KeyedEduRow, EduRow, -) # type: Tuple[Type[BaseFederationRow], ...] +) TypeToRow = {Row.TypeId: Row for Row in _rowtypes} diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index deb40f4610..0960f033bc 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -148,14 +148,14 @@ def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.is_mine_id = hs.is_mine_id - self._presence_router = None # type: Optional[PresenceRouter] + self._presence_router: Optional["PresenceRouter"] = None self._transaction_manager = TransactionManager(hs) self._instance_name = hs.get_instance_name() self._federation_shard_config = hs.config.worker.federation_shard_config # map from destination to PerDestinationQueue - self._per_destination_queues = {} # type: Dict[str, PerDestinationQueue] + self._per_destination_queues: Dict[str, PerDestinationQueue] = {} LaterGauge( "synapse_federation_transaction_queue_pending_destinations", @@ -192,9 +192,7 @@ def __init__(self, hs: "HomeServer"): # awaiting a call to flush_read_receipts_for_room. The presence of an entry # here for a given room means that we are rate-limiting RR flushes to that room, # and that there is a pending call to _flush_rrs_for_room in the system. - self._queues_awaiting_rr_flush_by_room = ( - {} - ) # type: Dict[str, Set[PerDestinationQueue]] + self._queues_awaiting_rr_flush_by_room: Dict[str, Set[PerDestinationQueue]] = {} self._rr_txn_interval_per_room_ms = ( 1000.0 / hs.config.federation_rr_transactions_per_room_per_second @@ -265,7 +263,7 @@ async def handle_event(event: EventBase) -> None: if not event.internal_metadata.should_proactively_send(): return - destinations = None # type: Optional[Set[str]] + destinations: Optional[Set[str]] = None if not event.prev_event_ids(): # If there are no prev event IDs then the state is empty # and so no remote servers in the room @@ -331,7 +329,7 @@ async def handle_room_events(events: Iterable[EventBase]) -> None: for event in events: await handle_event(event) - events_by_room = {} # type: Dict[str, List[EventBase]] + events_by_room: Dict[str, List[EventBase]] = {} for event in events: events_by_room.setdefault(event.room_id, []).append(event) @@ -628,7 +626,7 @@ async def _wake_destinations_needing_catchup(self) -> None: In order to reduce load spikes, adds a delay between each destination. """ - last_processed = None # type: Optional[str] + last_processed: Optional[str] = None while True: destinations_to_wake = ( diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index 3a2efd56ee..d06a3aff19 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -105,34 +105,34 @@ def __init__( # catch-up at startup. # New events will only be sent once this is finished, at which point # _catching_up is flipped to False. - self._catching_up = True # type: bool + self._catching_up: bool = True # The stream_ordering of the most recent PDU that was discarded due to # being in catch-up mode. - self._catchup_last_skipped = 0 # type: int + self._catchup_last_skipped: int = 0 # Cache of the last successfully-transmitted stream ordering for this # destination (we are the only updater so this is safe) - self._last_successful_stream_ordering = None # type: Optional[int] + self._last_successful_stream_ordering: Optional[int] = None # a queue of pending PDUs - self._pending_pdus = [] # type: List[EventBase] + self._pending_pdus: List[EventBase] = [] # XXX this is never actually used: see # https://github.com/matrix-org/synapse/issues/7549 - self._pending_edus = [] # type: List[Edu] + self._pending_edus: List[Edu] = [] # Pending EDUs by their "key". Keyed EDUs are EDUs that get clobbered # based on their key (e.g. typing events by room_id) # Map of (edu_type, key) -> Edu - self._pending_edus_keyed = {} # type: Dict[Tuple[str, Hashable], Edu] + self._pending_edus_keyed: Dict[Tuple[str, Hashable], Edu] = {} # Map of user_id -> UserPresenceState of pending presence to be sent to this # destination - self._pending_presence = {} # type: Dict[str, UserPresenceState] + self._pending_presence: Dict[str, UserPresenceState] = {} # room_id -> receipt_type -> user_id -> receipt_dict - self._pending_rrs = {} # type: Dict[str, Dict[str, Dict[str, dict]]] + self._pending_rrs: Dict[str, Dict[str, Dict[str, dict]]] = {} self._rrs_pending_flush = False # stream_id of last successfully sent to-device message. @@ -243,7 +243,7 @@ def attempt_new_transaction(self) -> None: ) async def _transaction_transmission_loop(self) -> None: - pending_pdus = [] # type: List[EventBase] + pending_pdus: List[EventBase] = [] try: self.transmission_loop_running = True diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index c9e7c57461..98b1bf77fd 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -395,9 +395,9 @@ async def get_public_rooms( # this uses MSC2197 (Search Filtering over Federation) path = _create_v1_path("/publicRooms") - data = { + data: Dict[str, Any] = { "include_all_networks": "true" if include_all_networks else "false" - } # type: Dict[str, Any] + } if third_party_instance_id: data["third_party_instance_id"] = third_party_instance_id if limit: @@ -423,9 +423,9 @@ async def get_public_rooms( else: path = _create_v1_path("/publicRooms") - args = { + args: Dict[str, Any] = { "include_all_networks": "true" if include_all_networks else "false" - } # type: Dict[str, Any] + } if third_party_instance_id: args["third_party_instance_id"] = (third_party_instance_id,) if limit: diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 0b21b375ee..2974d4d0cc 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1013,7 +1013,7 @@ async def on_POST( if not self.allow_access: raise FederationDeniedError(origin) - limit = int(content.get("limit", 100)) # type: Optional[int] + limit: Optional[int] = int(content.get("limit", 100)) since_token = content.get("since", None) search_filter = content.get("filter", None) @@ -1991,7 +1991,7 @@ async def on_GET( return 200, complexity -FEDERATION_SERVLET_CLASSES = ( +FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( FederationSendServlet, FederationEventServlet, FederationStateV1Servlet, @@ -2019,15 +2019,13 @@ async def on_GET( FederationSpaceSummaryServlet, FederationV1SendKnockServlet, FederationMakeKnockServlet, -) # type: Tuple[Type[BaseFederationServlet], ...] +) -OPENID_SERVLET_CLASSES = ( - OpenIdUserInfo, -) # type: Tuple[Type[BaseFederationServlet], ...] +OPENID_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = (OpenIdUserInfo,) -ROOM_LIST_CLASSES = (PublicRoomList,) # type: Tuple[Type[PublicRoomList], ...] +ROOM_LIST_CLASSES: Tuple[Type[PublicRoomList], ...] = (PublicRoomList,) -GROUP_SERVER_SERVLET_CLASSES = ( +GROUP_SERVER_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( FederationGroupsProfileServlet, FederationGroupsSummaryServlet, FederationGroupsRoomsServlet, @@ -2046,19 +2044,19 @@ async def on_GET( FederationGroupsAddRoomsServlet, FederationGroupsAddRoomsConfigServlet, FederationGroupsSettingJoinPolicyServlet, -) # type: Tuple[Type[BaseFederationServlet], ...] +) -GROUP_LOCAL_SERVLET_CLASSES = ( +GROUP_LOCAL_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( FederationGroupsLocalInviteServlet, FederationGroupsRemoveLocalUserServlet, FederationGroupsBulkPublicisedServlet, -) # type: Tuple[Type[BaseFederationServlet], ...] +) -GROUP_ATTESTATION_SERVLET_CLASSES = ( +GROUP_ATTESTATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( FederationGroupsRenewAttestaionServlet, -) # type: Tuple[Type[BaseFederationServlet], ...] +) DEFAULT_SERVLET_GROUPS = ( diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py index a06d060ebf..3dc55ab861 100644 --- a/synapse/groups/groups_server.py +++ b/synapse/groups/groups_server.py @@ -707,9 +707,9 @@ async def _add_user( See accept_invite, join_group. """ if not self.hs.is_mine_id(user_id): - local_attestation = self.attestations.create_attestation( - group_id, user_id - ) # type: Optional[JsonDict] + local_attestation: Optional[ + JsonDict + ] = self.attestations.create_attestation(group_id, user_id) remote_attestation = content["attestation"] @@ -868,9 +868,9 @@ async def create_group( remote_attestation, user_id=requester_user_id, group_id=group_id ) - local_attestation = self.attestations.create_attestation( - group_id, requester_user_id - ) # type: Optional[JsonDict] + local_attestation: Optional[ + JsonDict + ] = self.attestations.create_attestation(group_id, requester_user_id) else: local_attestation = None remote_attestation = None diff --git a/synapse/http/__init__.py b/synapse/http/__init__.py index ed4671b7de..578fc48ef4 100644 --- a/synapse/http/__init__.py +++ b/synapse/http/__init__.py @@ -69,7 +69,7 @@ def _get_requested_host(request: IRequest) -> bytes: return hostname # no Host header, use the address/port that the request arrived on - host = request.getHost() # type: Union[address.IPv4Address, address.IPv6Address] + host: Union[address.IPv4Address, address.IPv6Address] = request.getHost() hostname = host.host.encode("ascii") diff --git a/synapse/http/client.py b/synapse/http/client.py index 1ca6624fd5..2ac76b15c2 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -160,7 +160,7 @@ def __init__( def resolveHostName( self, recv: IResolutionReceiver, hostname: str, portNumber: int = 0 ) -> IResolutionReceiver: - addresses = [] # type: List[IAddress] + addresses: List[IAddress] = [] def _callback() -> None: has_bad_ip = False @@ -333,9 +333,9 @@ def __init__( if self._ip_blacklist: # If we have an IP blacklist, we need to use a DNS resolver which # filters out blacklisted IP addresses, to prevent DNS rebinding. - self.reactor = BlacklistingReactorWrapper( + self.reactor: ISynapseReactor = BlacklistingReactorWrapper( hs.get_reactor(), self._ip_whitelist, self._ip_blacklist - ) # type: ISynapseReactor + ) else: self.reactor = hs.get_reactor() @@ -349,14 +349,14 @@ def __init__( pool.maxPersistentPerHost = max((100 * hs.config.caches.global_factor, 5)) pool.cachedConnectionTimeout = 2 * 60 - self.agent = ProxyAgent( + self.agent: IAgent = ProxyAgent( self.reactor, hs.get_reactor(), connectTimeout=15, contextFactory=self.hs.get_http_client_context_factory(), pool=pool, use_proxy=use_proxy, - ) # type: IAgent + ) if self._ip_blacklist: # If we have an IP blacklist, we then install the blacklisting Agent @@ -411,7 +411,7 @@ async def request( cooperator=self._cooperator, ) - request_deferred = treq.request( + request_deferred: defer.Deferred = treq.request( method, uri, agent=self.agent, @@ -421,7 +421,7 @@ async def request( # response bodies. unbuffered=True, **self._extra_treq_args, - ) # type: defer.Deferred + ) # we use our own timeout mechanism rather than treq's as a workaround # for https://twistedmatrix.com/trac/ticket/9534. @@ -772,7 +772,7 @@ class BodyExceededMaxSize(Exception): class _DiscardBodyWithMaxSizeProtocol(protocol.Protocol): """A protocol which immediately errors upon receiving data.""" - transport = None # type: Optional[ITCPTransport] + transport: Optional[ITCPTransport] = None def __init__(self, deferred: defer.Deferred): self.deferred = deferred @@ -798,7 +798,7 @@ def connectionLost(self, reason: Failure = connectionDone) -> None: class _ReadBodyWithMaxSizeProtocol(protocol.Protocol): """A protocol which reads body to a stream, erroring if the body exceeds a maximum size.""" - transport = None # type: Optional[ITCPTransport] + transport: Optional[ITCPTransport] = None def __init__( self, stream: ByteWriteable, deferred: defer.Deferred, max_size: Optional[int] diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 3bace2c965..2efa15bf04 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -106,7 +106,7 @@ class ByteParser(ByteWriteable, Generic[T], abc.ABC): the parsed data. """ - CONTENT_TYPE = abc.abstractproperty() # type: str # type: ignore + CONTENT_TYPE: str = abc.abstractproperty() # type: ignore """The expected content type of the response, e.g. `application/json`. If the content type doesn't match we fail the request. """ @@ -327,11 +327,11 @@ def __init__(self, hs, tls_client_options_factory): # We need to use a DNS resolver which filters out blacklisted IP # addresses, to prevent DNS rebinding. - self.reactor = BlacklistingReactorWrapper( + self.reactor: ISynapseReactor = BlacklistingReactorWrapper( hs.get_reactor(), hs.config.federation_ip_range_whitelist, hs.config.federation_ip_range_blacklist, - ) # type: ISynapseReactor + ) user_agent = hs.version_string if hs.config.user_agent_suffix: @@ -504,7 +504,7 @@ async def _send_request( ) # Inject the span into the headers - headers_dict = {} # type: Dict[bytes, List[bytes]] + headers_dict: Dict[bytes, List[bytes]] = {} opentracing.inject_header_dict(headers_dict, request.destination) headers_dict[b"User-Agent"] = [self.version_string_bytes] @@ -533,9 +533,9 @@ async def _send_request( destination_bytes, method_bytes, url_to_sign_bytes, json ) data = encode_canonical_json(json) - producer = QuieterFileBodyProducer( + producer: Optional[IBodyProducer] = QuieterFileBodyProducer( BytesIO(data), cooperator=self._cooperator - ) # type: Optional[IBodyProducer] + ) else: producer = None auth_headers = self.build_auth_headers( diff --git a/synapse/http/server.py b/synapse/http/server.py index efbc6d5b25..b79fa722e9 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -81,7 +81,7 @@ def return_json_error(f: failure.Failure, request: SynapseRequest) -> None: if f.check(SynapseError): # mypy doesn't understand that f.check asserts the type. - exc = f.value # type: SynapseError # type: ignore + exc: SynapseError = f.value # type: ignore error_code = exc.code error_dict = exc.error_dict() @@ -132,7 +132,7 @@ def return_html_error( """ if f.check(CodeMessageException): # mypy doesn't understand that f.check asserts the type. - cme = f.value # type: CodeMessageException # type: ignore + cme: CodeMessageException = f.value # type: ignore code = cme.code msg = cme.msg @@ -404,7 +404,7 @@ def _get_handler_for_request( key word arguments to pass to the callback """ # At this point the path must be bytes. - request_path_bytes = request.path # type: bytes # type: ignore + request_path_bytes: bytes = request.path # type: ignore request_path = request_path_bytes.decode("ascii") # Treat HEAD requests as GET requests. request_method = request.method @@ -557,7 +557,7 @@ def __init__( request: Request, iterator: Iterator[bytes], ): - self._request = request # type: Optional[Request] + self._request: Optional[Request] = request self._iterator = iterator self._paused = False diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 6ba2ce1e53..04560fb589 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -205,7 +205,7 @@ def parse_string( parameter is present, must be one of a list of allowed values and is not one of those allowed values. """ - args = request.args # type: Dict[bytes, List[bytes]] # type: ignore + args: Dict[bytes, List[bytes]] = request.args # type: ignore return parse_string_from_args( args, name, diff --git a/synapse/http/site.py b/synapse/http/site.py index 40754b7bea..3b0a38124e 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -64,16 +64,16 @@ class SynapseRequest(Request): def __init__(self, channel, *args, max_request_body_size=1024, **kw): Request.__init__(self, channel, *args, **kw) self._max_request_body_size = max_request_body_size - self.site = channel.site # type: SynapseSite + self.site: SynapseSite = channel.site self._channel = channel # this is used by the tests self.start_time = 0.0 # The requester, if authenticated. For federation requests this is the # server name, for client requests this is the Requester object. - self._requester = None # type: Optional[Union[Requester, str]] + self._requester: Optional[Union[Requester, str]] = None # we can't yet create the logcontext, as we don't know the method. - self.logcontext = None # type: Optional[LoggingContext] + self.logcontext: Optional[LoggingContext] = None global _next_request_seq self.request_seq = _next_request_seq @@ -152,7 +152,7 @@ def get_redacted_uri(self) -> str: Returns: The redacted URI as a string. """ - uri = self.uri # type: Union[bytes, str] + uri: Union[bytes, str] = self.uri if isinstance(uri, bytes): uri = uri.decode("ascii", errors="replace") return redact_uri(uri) @@ -167,7 +167,7 @@ def get_method(self) -> str: Returns: The request method as a string. """ - method = self.method # type: Union[bytes, str] + method: Union[bytes, str] = self.method if isinstance(method, bytes): return self.method.decode("ascii") return method @@ -434,8 +434,8 @@ class XForwardedForRequest(SynapseRequest): """ # the client IP and ssl flag, as extracted from the headers. - _forwarded_for = None # type: Optional[_XForwardedForAddress] - _forwarded_https = False # type: bool + _forwarded_for: "Optional[_XForwardedForAddress]" = None + _forwarded_https: bool = False def requestReceived(self, command, path, version): # this method is called by the Channel once the full request has been diff --git a/synapse/logging/_remote.py b/synapse/logging/_remote.py index c515690b38..8202d0494d 100644 --- a/synapse/logging/_remote.py +++ b/synapse/logging/_remote.py @@ -110,9 +110,9 @@ def __init__( self.port = port self.maximum_buffer = maximum_buffer - self._buffer = deque() # type: Deque[logging.LogRecord] - self._connection_waiter = None # type: Optional[Deferred] - self._producer = None # type: Optional[LogProducer] + self._buffer: Deque[logging.LogRecord] = deque() + self._connection_waiter: Optional[Deferred] = None + self._producer: Optional[LogProducer] = None # Connect without DNS lookups if it's a direct IP. if _reactor is None: @@ -123,9 +123,9 @@ def __init__( try: ip = ip_address(self.host) if isinstance(ip, IPv4Address): - endpoint = TCP4ClientEndpoint( + endpoint: IStreamClientEndpoint = TCP4ClientEndpoint( _reactor, self.host, self.port - ) # type: IStreamClientEndpoint + ) elif isinstance(ip, IPv6Address): endpoint = TCP6ClientEndpoint(_reactor, self.host, self.port) else: @@ -165,7 +165,7 @@ def fail(failure: Failure) -> None: def writer(result: Protocol) -> None: # Force recognising transport as a Connection and not the more # generic ITransport. - transport = result.transport # type: Connection # type: ignore + transport: Connection = result.transport # type: ignore # We have a connection. If we already have a producer, and its # transport is the same, just trigger a resumeProducing. @@ -188,7 +188,7 @@ def writer(result: Protocol) -> None: self._producer.resumeProducing() self._connection_waiter = None - deferred = self._service.whenConnected(failAfterFailures=1) # type: Deferred + deferred: Deferred = self._service.whenConnected(failAfterFailures=1) deferred.addCallbacks(writer, fail) self._connection_waiter = deferred diff --git a/synapse/logging/_structured.py b/synapse/logging/_structured.py index c7a971a9d6..b9933a1528 100644 --- a/synapse/logging/_structured.py +++ b/synapse/logging/_structured.py @@ -63,7 +63,7 @@ def parse_drain_configs( DrainType.CONSOLE_JSON, DrainType.FILE_JSON, ): - formatter = "json" # type: Optional[str] + formatter: Optional[str] = "json" elif logging_type in ( DrainType.CONSOLE_JSON_TERSE, DrainType.NETWORK_JSON_TERSE, diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 7fc11a9ac2..18ac507802 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -113,13 +113,13 @@ def __init__(self, copy_from: "Optional[ContextResourceUsage]" = None) -> None: self.reset() else: # FIXME: mypy can't infer the types set via reset() above, so specify explicitly for now - self.ru_utime = copy_from.ru_utime # type: float - self.ru_stime = copy_from.ru_stime # type: float - self.db_txn_count = copy_from.db_txn_count # type: int + self.ru_utime: float = copy_from.ru_utime + self.ru_stime: float = copy_from.ru_stime + self.db_txn_count: int = copy_from.db_txn_count - self.db_txn_duration_sec = copy_from.db_txn_duration_sec # type: float - self.db_sched_duration_sec = copy_from.db_sched_duration_sec # type: float - self.evt_db_fetch_count = copy_from.evt_db_fetch_count # type: int + self.db_txn_duration_sec: float = copy_from.db_txn_duration_sec + self.db_sched_duration_sec: float = copy_from.db_sched_duration_sec + self.evt_db_fetch_count: int = copy_from.evt_db_fetch_count def copy(self) -> "ContextResourceUsage": return ContextResourceUsage(copy_from=self) @@ -289,12 +289,12 @@ def __init__( # The thread resource usage when the logcontext became active. None # if the context is not currently active. - self.usage_start = None # type: Optional[resource._RUsage] + self.usage_start: Optional[resource._RUsage] = None self.main_thread = get_thread_id() self.request = None self.tag = "" - self.scope = None # type: Optional[_LogContextScope] + self.scope: Optional["_LogContextScope"] = None # keep track of whether we have hit the __exit__ block for this context # (suggesting that the the thing that created the context thinks it should diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index 140ed711e3..185844f188 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -251,7 +251,7 @@ def report_span(self, span): except Exception: logger.exception("Failed to report span") - RustReporter = _WrappedRustReporter # type: Optional[Type[_WrappedRustReporter]] + RustReporter: Optional[Type[_WrappedRustReporter]] = _WrappedRustReporter except ImportError: RustReporter = None @@ -286,7 +286,7 @@ class SynapseBaggage: # Block everything by default # A regex which matches the server_names to expose traces for. # None means 'block everything'. -_homeserver_whitelist = None # type: Optional[Pattern[str]] +_homeserver_whitelist: Optional[Pattern[str]] = None # Util methods @@ -662,7 +662,7 @@ def inject_header_dict( span = opentracing.tracer.active_span - carrier = {} # type: Dict[str, str] + carrier: Dict[str, str] = {} opentracing.tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, carrier) for key, value in carrier.items(): @@ -704,7 +704,7 @@ def get_active_span_text_map(destination=None): if destination and not whitelisted_homeserver(destination): return {} - carrier = {} # type: Dict[str, str] + carrier: Dict[str, str] = {} opentracing.tracer.inject( opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier ) @@ -718,7 +718,7 @@ def active_span_context_as_string(): Returns: The active span context encoded as a string. """ - carrier = {} # type: Dict[str, str] + carrier: Dict[str, str] = {} if opentracing: opentracing.tracer.inject( opentracing.tracer.active_span.context, opentracing.Format.TEXT_MAP, carrier diff --git a/synapse/metrics/__init__.py b/synapse/metrics/__init__.py index fef2846669..f237b8a236 100644 --- a/synapse/metrics/__init__.py +++ b/synapse/metrics/__init__.py @@ -46,7 +46,7 @@ METRICS_PREFIX = "/_synapse/metrics" running_on_pypy = platform.python_implementation() == "PyPy" -all_gauges = {} # type: Dict[str, Union[LaterGauge, InFlightGauge]] +all_gauges: "Dict[str, Union[LaterGauge, InFlightGauge]]" = {} HAVE_PROC_SELF_STAT = os.path.exists("/proc/self/stat") @@ -130,7 +130,7 @@ def __init__(self, name, desc, labels, sub_metrics): ) # Counts number of in flight blocks for a given set of label values - self._registrations = {} # type: Dict + self._registrations: Dict = {} # Protects access to _registrations self._lock = threading.Lock() @@ -248,7 +248,7 @@ def __init__( # We initially set this to None. We won't report metrics until # this has been initialised after a successful data update - self._metric = None # type: Optional[GaugeHistogramMetricFamily] + self._metric: Optional[GaugeHistogramMetricFamily] = None registry.register(self) diff --git a/synapse/metrics/_exposition.py b/synapse/metrics/_exposition.py index 8002be56e0..7e49d0d02c 100644 --- a/synapse/metrics/_exposition.py +++ b/synapse/metrics/_exposition.py @@ -125,7 +125,7 @@ def generate_latest(registry, emit_help=False): ) output.append("# TYPE {0} {1}\n".format(mname, mtype)) - om_samples = {} # type: Dict[str, List[str]] + om_samples: Dict[str, List[str]] = {} for s in metric.samples: for suffix in ["_created", "_gsum", "_gcount"]: if s.name == metric.name + suffix: diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index de96ca0821..4455fa71a8 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -93,7 +93,7 @@ # map from description to a counter, so that we can name our logcontexts # incrementally. (It actually duplicates _background_process_start_count, but # it's much simpler to do so than to try to combine them.) -_background_process_counts = {} # type: Dict[str, int] +_background_process_counts: Dict[str, int] = {} # Set of all running background processes that became active active since the # last time metrics were scraped (i.e. background processes that performed some @@ -103,7 +103,7 @@ # background processes stacking up behind a lock or linearizer, where we then # only need to iterate over and update metrics for the process that have # actually been active and can ignore the idle ones. -_background_processes_active_since_last_scrape = set() # type: Set[_BackgroundProcess] +_background_processes_active_since_last_scrape: "Set[_BackgroundProcess]" = set() # A lock that covers the above set and dict _bg_metrics_lock = threading.Lock() diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 721c45abac..308f045700 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -54,7 +54,7 @@ def __init__(self, hs: "HomeServer", auth_handler): self._state = hs.get_state_handler() # We expose these as properties below in order to attach a helpful docstring. - self._http_client = hs.get_simple_http_client() # type: SimpleHttpClient + self._http_client: SimpleHttpClient = hs.get_simple_http_client() self._public_room_list_manager = PublicRoomListManager(hs) self._spam_checker = hs.get_spam_checker() diff --git a/synapse/notifier.py b/synapse/notifier.py index 3c3cc47631..c5fbebc17d 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -203,21 +203,21 @@ class Notifier: UNUSED_STREAM_EXPIRY_MS = 10 * 60 * 1000 def __init__(self, hs: "synapse.server.HomeServer"): - self.user_to_user_stream = {} # type: Dict[str, _NotifierUserStream] - self.room_to_user_streams = {} # type: Dict[str, Set[_NotifierUserStream]] + self.user_to_user_stream: Dict[str, _NotifierUserStream] = {} + self.room_to_user_streams: Dict[str, Set[_NotifierUserStream]] = {} self.hs = hs self.storage = hs.get_storage() self.event_sources = hs.get_event_sources() self.store = hs.get_datastore() - self.pending_new_room_events = [] # type: List[_PendingRoomEventEntry] + self.pending_new_room_events: List[_PendingRoomEventEntry] = [] # Called when there are new things to stream over replication - self.replication_callbacks = [] # type: List[Callable[[], None]] + self.replication_callbacks: List[Callable[[], None]] = [] # Called when remote servers have come back online after having been # down. - self.remote_server_up_callbacks = [] # type: List[Callable[[str], None]] + self.remote_server_up_callbacks: List[Callable[[str], None]] = [] self.clock = hs.get_clock() self.appservice_handler = hs.get_application_service_handler() @@ -237,7 +237,7 @@ def __init__(self, hs: "synapse.server.HomeServer"): # when rendering the metrics page, which is likely once per minute at # most when scraping it. def count_listeners(): - all_user_streams = set() # type: Set[_NotifierUserStream] + all_user_streams: Set[_NotifierUserStream] = set() for streams in list(self.room_to_user_streams.values()): all_user_streams |= streams @@ -329,8 +329,8 @@ def _notify_pending_new_room_events(self, max_room_stream_token: RoomStreamToken pending = self.pending_new_room_events self.pending_new_room_events = [] - users = set() # type: Set[UserID] - rooms = set() # type: Set[str] + users: Set[UserID] = set() + rooms: Set[str] = set() for entry in pending: if entry.event_pos.persisted_after(max_room_stream_token): @@ -580,7 +580,7 @@ async def check_for_updates( if after_token == before_token: return EventStreamResult([], (from_token, from_token)) - events = [] # type: List[EventBase] + events: List[EventBase] = [] end_token = from_token for name, source in self.event_sources.sources.items(): diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 669ea462e2..c337e530d3 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -194,7 +194,7 @@ async def action_for_event_by_user( count_as_unread = _should_count_as_unread(event, context) rules_by_user = await self._get_rules_for_event(event, context) - actions_by_user = {} # type: Dict[str, List[Union[dict, str]]] + actions_by_user: Dict[str, List[Union[dict, str]]] = {} room_members = await self.store.get_joined_users_from_context(event, context) @@ -207,7 +207,7 @@ async def action_for_event_by_user( event, len(room_members), sender_power_level, power_levels ) - condition_cache = {} # type: Dict[str, bool] + 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(): diff --git a/synapse/push/clientformat.py b/synapse/push/clientformat.py index 2ee0ccd58a..1fc9716a34 100644 --- a/synapse/push/clientformat.py +++ b/synapse/push/clientformat.py @@ -26,10 +26,10 @@ def format_push_rules_for_user(user: UserID, ruleslist) -> Dict[str, Dict[str, l # We're going to be mutating this a lot, so do a deep copy ruleslist = copy.deepcopy(ruleslist) - rules = { + rules: Dict[str, Dict[str, List[Dict[str, Any]]]] = { "global": {}, "device": {}, - } # type: Dict[str, Dict[str, List[Dict[str, Any]]]] + } rules["global"] = _add_empty_priority_class_arrays(rules["global"]) diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 99a18874d1..e08e125cb8 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -66,8 +66,8 @@ def __init__(self, hs: "HomeServer", pusher_config: PusherConfig, mailer: Mailer self.store = self.hs.get_datastore() self.email = pusher_config.pushkey - self.timed_call = None # type: Optional[IDelayedCall] - self.throttle_params = {} # type: Dict[str, ThrottleParams] + self.timed_call: Optional[IDelayedCall] = None + self.throttle_params: Dict[str, ThrottleParams] = {} self._inited = False self._is_processing = False @@ -168,7 +168,7 @@ async def _unsafe_process(self) -> None: ) ) - soonest_due_at = None # type: Optional[int] + soonest_due_at: Optional[int] = None if not unprocessed: await self.save_last_stream_ordering_and_success(self.max_stream_ordering) diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 06bf5f8ada..36aabd8422 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -71,7 +71,7 @@ def __init__(self, hs: "HomeServer", pusher_config: PusherConfig): self.data = pusher_config.data self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC self.failing_since = pusher_config.failing_since - self.timed_call = None # type: Optional[IDelayedCall] + self.timed_call: Optional[IDelayedCall] = None self._is_processing = False self._group_unread_count_by_room = hs.config.push_group_unread_count_by_room self._pusherpool = hs.get_pusherpool() diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 5f9ea5003a..7be5fe1e9b 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -110,7 +110,7 @@ def __init__( self.state_handler = self.hs.get_state_handler() self.storage = hs.get_storage() self.app_name = app_name - self.email_subjects = hs.config.email_subjects # type: EmailSubjectConfig + self.email_subjects: EmailSubjectConfig = hs.config.email_subjects logger.info("Created Mailer for app_name %s" % app_name) @@ -230,7 +230,7 @@ async def send_notification_mail( [pa["event_id"] for pa in push_actions] ) - notifs_by_room = {} # type: Dict[str, List[Dict[str, Any]]] + notifs_by_room: Dict[str, List[Dict[str, Any]]] = {} for pa in push_actions: notifs_by_room.setdefault(pa["room_id"], []).append(pa) @@ -356,13 +356,13 @@ async def _get_room_vars( room_name = await calculate_room_name(self.store, room_state_ids, user_id) - room_vars = { + room_vars: Dict[str, Any] = { "title": room_name, "hash": string_ordinal_total(room_id), # See sender avatar hash "notifs": [], "invite": is_invite, "link": self._make_room_link(room_id), - } # type: Dict[str, Any] + } if not is_invite: for n in notifs: @@ -460,9 +460,9 @@ async def _get_message_vars( type_state_key = ("m.room.member", event.sender) sender_state_event_id = room_state_ids.get(type_state_key) if sender_state_event_id: - sender_state_event = await self.store.get_event( + sender_state_event: Optional[EventBase] = await self.store.get_event( sender_state_event_id - ) # type: Optional[EventBase] + ) else: # Attempt to check the historical state for the room. historical_state = await self.state_store.get_state_for_event( diff --git a/synapse/push/presentable_names.py b/synapse/push/presentable_names.py index 412941393f..0510c1cbd5 100644 --- a/synapse/push/presentable_names.py +++ b/synapse/push/presentable_names.py @@ -199,7 +199,7 @@ def name_from_member_event(member_event: EventBase) -> str: def _state_as_two_level_dict(state: StateMap[str]) -> Dict[str, Dict[str, str]]: - ret = {} # type: Dict[str, Dict[str, str]] + ret: Dict[str, Dict[str, str]] = {} for k, v in state.items(): ret.setdefault(k[0], {})[k[1]] = v return ret diff --git a/synapse/push/push_rule_evaluator.py b/synapse/push/push_rule_evaluator.py index 98b90a4f51..7a8dc63976 100644 --- a/synapse/push/push_rule_evaluator.py +++ b/synapse/push/push_rule_evaluator.py @@ -195,9 +195,9 @@ def _get_value(self, dotted_key: str) -> Optional[str]: # Caches (string, is_glob, word_boundary) -> regex for push. See _glob_matches -regex_cache = LruCache( +regex_cache: LruCache[Tuple[str, bool, bool], Pattern] = LruCache( 50000, "regex_push_cache" -) # type: LruCache[Tuple[str, bool, bool], Pattern] +) def _glob_matches(glob: str, value: str, word_boundary: bool = False) -> bool: diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index c51938b8cf..021275437c 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -31,13 +31,13 @@ def __init__(self, hs: "HomeServer"): self.hs = hs self.config = hs.config - self.pusher_types = { + self.pusher_types: Dict[str, Callable[[HomeServer, PusherConfig], Pusher]] = { "http": HttpPusher - } # type: Dict[str, Callable[[HomeServer, PusherConfig], Pusher]] + } logger.info("email enable notifs: %r", hs.config.email_enable_notifs) if hs.config.email_enable_notifs: - self.mailers = {} # type: Dict[str, Mailer] + self.mailers: Dict[str, Mailer] = {} self._notif_template_html = hs.config.email_notif_template_html self._notif_template_text = hs.config.email_notif_template_text diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 579fcdf472..2519ad76db 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -87,7 +87,7 @@ def __init__(self, hs: "HomeServer"): self._last_room_stream_id_seen = self.store.get_room_max_stream_ordering() # map from user id to app_id:pushkey to pusher - self.pushers = {} # type: Dict[str, Dict[str, Pusher]] + self.pushers: Dict[str, Dict[str, Pusher]] = {} def start(self) -> None: """Starts the pushers off in a background process.""" diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 271c17c226..cdcbdd772b 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -115,7 +115,7 @@ "cache_memory": ["pympler"], } -ALL_OPTIONAL_REQUIREMENTS = set() # type: Set[str] +ALL_OPTIONAL_REQUIREMENTS: Set[str] = set() for name, optional_deps in CONDITIONAL_REQUIREMENTS.items(): # Exclude systemd as it's a system-based requirement. @@ -193,7 +193,7 @@ def check_requirements(for_feature=None): if not for_feature: # Check the optional dependencies are up to date. We allow them to not be # installed. - OPTS = sum(CONDITIONAL_REQUIREMENTS.values(), []) # type: List[str] + OPTS: List[str] = sum(CONDITIONAL_REQUIREMENTS.values(), []) for dependency in OPTS: try: diff --git a/synapse/replication/http/_base.py b/synapse/replication/http/_base.py index f13a7c23b4..25589b0042 100644 --- a/synapse/replication/http/_base.py +++ b/synapse/replication/http/_base.py @@ -85,17 +85,17 @@ class ReplicationEndpoint(metaclass=abc.ABCMeta): is received. """ - NAME = abc.abstractproperty() # type: str # type: ignore - PATH_ARGS = abc.abstractproperty() # type: Tuple[str, ...] # type: ignore + NAME: str = abc.abstractproperty() # type: ignore + PATH_ARGS: Tuple[str, ...] = abc.abstractproperty() # type: ignore METHOD = "POST" CACHE = True RETRY_ON_TIMEOUT = True def __init__(self, hs: "HomeServer"): if self.CACHE: - self.response_cache = ResponseCache( + self.response_cache: ResponseCache[str] = ResponseCache( hs.get_clock(), "repl." + self.NAME, timeout_ms=30 * 60 * 1000 - ) # type: ResponseCache[str] + ) # We reserve `instance_name` as a parameter to sending requests, so we # assert here that sub classes don't try and use the name. @@ -232,7 +232,7 @@ async def send_request(*, instance_name="master", **kwargs): # have a good idea that the request has either succeeded or failed on # the master, and so whether we should clean up or not. while True: - headers = {} # type: Dict[bytes, List[bytes]] + headers: Dict[bytes, List[bytes]] = {} # Add an authorization header, if configured. if replication_secret: headers[b"Authorization"] = [b"Bearer " + replication_secret] diff --git a/synapse/replication/slave/storage/_base.py b/synapse/replication/slave/storage/_base.py index faa99387a7..e460dd85cd 100644 --- a/synapse/replication/slave/storage/_base.py +++ b/synapse/replication/slave/storage/_base.py @@ -27,7 +27,9 @@ class BaseSlavedStore(CacheInvalidationWorkerStore): def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) if isinstance(self.database_engine, PostgresEngine): - self._cache_id_gen = MultiWriterIdGenerator( + self._cache_id_gen: Optional[ + MultiWriterIdGenerator + ] = MultiWriterIdGenerator( db_conn, database, stream_name="caches", @@ -41,7 +43,7 @@ def __init__(self, database: DatabasePool, db_conn, hs): ], sequence_name="cache_invalidation_stream_seq", writers=[], - ) # type: Optional[MultiWriterIdGenerator] + ) else: self._cache_id_gen = None diff --git a/synapse/replication/slave/storage/client_ips.py b/synapse/replication/slave/storage/client_ips.py index 13ed87adc4..436d39c320 100644 --- a/synapse/replication/slave/storage/client_ips.py +++ b/synapse/replication/slave/storage/client_ips.py @@ -23,9 +23,9 @@ class SlavedClientIpStore(BaseSlavedStore): def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) - self.client_ip_last_seen = LruCache( + self.client_ip_last_seen: LruCache[tuple, int] = LruCache( cache_name="client_ip_last_seen", max_size=50000 - ) # type: LruCache[tuple, int] + ) async def insert_client_ip(self, user_id, access_token, ip, user_agent, device_id): now = int(self._clock.time_msec()) diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 62d7809175..9d4859798b 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -121,13 +121,13 @@ def __init__(self, hs: "HomeServer"): self._pusher_pool = hs.get_pusherpool() self._presence_handler = hs.get_presence_handler() - self.send_handler = None # type: Optional[FederationSenderHandler] + self.send_handler: Optional[FederationSenderHandler] = None if hs.should_send_federation(): self.send_handler = FederationSenderHandler(hs) # Map from stream to list of deferreds waiting for the stream to # arrive at a particular position. The lists are sorted by stream position. - self._streams_to_waiters = {} # type: Dict[str, List[Tuple[int, Deferred]]] + self._streams_to_waiters: Dict[str, List[Tuple[int, Deferred]]] = {} async def on_rdata( self, stream_name: str, instance_name: str, token: int, rows: list @@ -173,7 +173,7 @@ async def on_rdata( if entities: self.notifier.on_new_event("to_device_key", token, users=entities) elif stream_name == DeviceListsStream.NAME: - all_room_ids = set() # type: Set[str] + 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) @@ -201,7 +201,7 @@ async def on_rdata( if row.data.rejected: continue - extra_users = () # type: Tuple[UserID, ...] + extra_users: Tuple[UserID, ...] = () if row.data.type == EventTypes.Member and row.data.state_key: extra_users = (UserID.from_string(row.data.state_key),) @@ -348,7 +348,7 @@ def __init__(self, hs: "HomeServer"): # Stores the latest position in the federation stream we've gotten up # to. This is always set before we use it. - self.federation_position = None # type: Optional[int] + self.federation_position: Optional[int] = None self._fed_position_linearizer = Linearizer(name="_fed_position_linearizer") diff --git a/synapse/replication/tcp/commands.py b/synapse/replication/tcp/commands.py index 505d450e19..1311b013da 100644 --- a/synapse/replication/tcp/commands.py +++ b/synapse/replication/tcp/commands.py @@ -34,7 +34,7 @@ class Command(metaclass=abc.ABCMeta): A full command line on the wire is constructed from `NAME + " " + to_line()` """ - NAME = None # type: str + NAME: str @classmethod @abc.abstractmethod @@ -380,7 +380,7 @@ class RemoteServerUpCommand(_SimpleCommand): NAME = "REMOTE_SERVER_UP" -_COMMANDS = ( +_COMMANDS: Tuple[Type[Command], ...] = ( ServerCommand, RdataCommand, PositionCommand, @@ -393,7 +393,7 @@ class RemoteServerUpCommand(_SimpleCommand): UserIpCommand, RemoteServerUpCommand, ClearUserSyncsCommand, -) # type: Tuple[Type[Command], ...] +) # Map of command name to command type. COMMAND_MAP = {cmd.NAME: cmd for cmd in _COMMANDS} diff --git a/synapse/replication/tcp/handler.py b/synapse/replication/tcp/handler.py index 2ad7a200bb..eae4515363 100644 --- a/synapse/replication/tcp/handler.py +++ b/synapse/replication/tcp/handler.py @@ -105,12 +105,12 @@ def __init__(self, hs: "HomeServer"): hs.get_instance_name() in hs.config.worker.writers.presence ) - self._streams = { + self._streams: Dict[str, Stream] = { stream.NAME: stream(hs) for stream in STREAMS_MAP.values() - } # type: Dict[str, Stream] + } # List of streams that this instance is the source of - self._streams_to_replicate = [] # type: List[Stream] + self._streams_to_replicate: List[Stream] = [] for stream in self._streams.values(): if hs.config.redis.redis_enabled and stream.NAME == CachesStream.NAME: @@ -180,14 +180,14 @@ def __init__(self, hs: "HomeServer"): # Map of stream name to batched updates. See RdataCommand for info on # how batching works. - self._pending_batches = {} # type: Dict[str, List[Any]] + self._pending_batches: Dict[str, List[Any]] = {} # The factory used to create connections. - self._factory = None # type: Optional[ReconnectingClientFactory] + self._factory: Optional[ReconnectingClientFactory] = None # The currently connected connections. (The list of places we need to send # outgoing replication commands to.) - self._connections = [] # type: List[IReplicationConnection] + self._connections: List[IReplicationConnection] = [] LaterGauge( "synapse_replication_tcp_resource_total_connections", @@ -200,7 +200,7 @@ def __init__(self, hs: "HomeServer"): # them in order in a separate background process. # the streams which are currently being processed by _unsafe_process_queue - self._processing_streams = set() # type: Set[str] + self._processing_streams: Set[str] = set() # for each stream, a queue of commands that are awaiting processing, and the # connection that they arrived on. @@ -210,7 +210,7 @@ def __init__(self, hs: "HomeServer"): # For each connection, the incoming stream names that have received a POSITION # from that connection. - self._streams_by_connection = {} # type: Dict[IReplicationConnection, Set[str]] + self._streams_by_connection: Dict[IReplicationConnection, Set[str]] = {} LaterGauge( "synapse_replication_tcp_command_queue", diff --git a/synapse/replication/tcp/protocol.py b/synapse/replication/tcp/protocol.py index 6e3705364f..8c80153ab6 100644 --- a/synapse/replication/tcp/protocol.py +++ b/synapse/replication/tcp/protocol.py @@ -102,7 +102,7 @@ # A list of all connected protocols. This allows us to send metrics about the # connections. -connected_connections = [] # type: List[BaseReplicationStreamProtocol] +connected_connections: "List[BaseReplicationStreamProtocol]" = [] logger = logging.getLogger(__name__) @@ -146,15 +146,15 @@ class BaseReplicationStreamProtocol(LineOnlyReceiver): # The transport is going to be an ITCPTransport, but that doesn't have the # (un)registerProducer methods, those are only on the implementation. - transport = None # type: Connection + transport: Connection delimiter = b"\n" # Valid commands we expect to receive - VALID_INBOUND_COMMANDS = [] # type: Collection[str] + VALID_INBOUND_COMMANDS: Collection[str] = [] # Valid commands we can send - VALID_OUTBOUND_COMMANDS = [] # type: Collection[str] + VALID_OUTBOUND_COMMANDS: Collection[str] = [] max_line_buffer = 10000 @@ -165,7 +165,7 @@ def __init__(self, clock: Clock, handler: "ReplicationCommandHandler"): self.last_received_command = self.clock.time_msec() self.last_sent_command = 0 # When we requested the connection be closed - self.time_we_closed = None # type: Optional[int] + self.time_we_closed: Optional[int] = None self.received_ping = False # Have we received a ping from the other side @@ -175,10 +175,10 @@ def __init__(self, clock: Clock, handler: "ReplicationCommandHandler"): self.conn_id = random_string(5) # To dedupe in case of name clashes. # List of pending commands to send once we've established the connection - self.pending_commands = [] # type: List[Command] + self.pending_commands: List[Command] = [] # The LoopingCall for sending pings. - self._send_ping_loop = None # type: Optional[task.LoopingCall] + self._send_ping_loop: Optional[task.LoopingCall] = None # a logcontext which we use for processing incoming commands. We declare it as a # background process so that the CPU stats get reported to prometheus. diff --git a/synapse/replication/tcp/redis.py b/synapse/replication/tcp/redis.py index 6a2c2655e4..8c0df627c8 100644 --- a/synapse/replication/tcp/redis.py +++ b/synapse/replication/tcp/redis.py @@ -57,7 +57,7 @@ class ConstantProperty(Generic[T, V]): it. """ - constant = attr.ib() # type: V + constant: V = attr.ib() def __get__(self, obj: Optional[T], objtype: Optional[Type[T]] = None) -> V: return self.constant @@ -91,9 +91,9 @@ class RedisSubscriber(txredisapi.SubscriberProtocol): commands. """ - synapse_handler = None # type: ReplicationCommandHandler - synapse_stream_name = None # type: str - synapse_outbound_redis_connection = None # type: txredisapi.RedisProtocol + synapse_handler: "ReplicationCommandHandler" + synapse_stream_name: str + synapse_outbound_redis_connection: txredisapi.RedisProtocol def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index b03824925a..3716c41bea 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -85,9 +85,9 @@ class Stream: time it was called. """ - NAME = None # type: str # The name of the stream + NAME: str # The name of the stream # The type of the row. Used by the default impl of parse_row. - ROW_TYPE = None # type: Any + ROW_TYPE: Any = None @classmethod def parse_row(cls, row: StreamRow): @@ -283,9 +283,7 @@ def __init__(self, hs: "HomeServer"): assert isinstance(presence_handler, PresenceHandler) - update_function = ( - presence_handler.get_all_presence_updates - ) # type: UpdateFunction + update_function: UpdateFunction = presence_handler.get_all_presence_updates else: # Query presence writer process update_function = make_http_update_function(hs, self.NAME) @@ -334,9 +332,9 @@ def __init__(self, hs: "HomeServer"): if writer_instance == hs.get_instance_name(): # On the writer, query the typing handler typing_writer_handler = hs.get_typing_writer_handler() - update_function = ( - typing_writer_handler.get_all_typing_updates - ) # type: Callable[[str, int, int, int], Awaitable[Tuple[List[Tuple[int, Any]], int, bool]]] + update_function: Callable[ + [str, int, int, int], Awaitable[Tuple[List[Tuple[int, Any]], int, bool]] + ] = typing_writer_handler.get_all_typing_updates current_token_function = typing_writer_handler.get_current_token else: # Query the typing writer process diff --git a/synapse/replication/tcp/streams/events.py b/synapse/replication/tcp/streams/events.py index e7e87bac92..a030e9299e 100644 --- a/synapse/replication/tcp/streams/events.py +++ b/synapse/replication/tcp/streams/events.py @@ -65,7 +65,7 @@ class BaseEventsStreamRow: """ # Unique string that ids the type. Must be overridden in sub classes. - TypeId = None # type: str + TypeId: str @classmethod def from_data(cls, data): @@ -103,10 +103,10 @@ class EventsStreamCurrentStateRow(BaseEventsStreamRow): event_id = attr.ib() # str, optional -_EventRows = ( +_EventRows: Tuple[Type[BaseEventsStreamRow], ...] = ( EventsStreamEventRow, EventsStreamCurrentStateRow, -) # type: Tuple[Type[BaseEventsStreamRow], ...] +) TypeToRow = {Row.TypeId: Row for Row in _EventRows} @@ -157,9 +157,9 @@ async def _update_function( # now we fetch up to that many rows from the events table - event_rows = await self._store.get_all_new_forward_event_rows( + event_rows: List[Tuple] = await self._store.get_all_new_forward_event_rows( instance_name, from_token, current_token, target_row_count - ) # type: List[Tuple] + ) # we rely on get_all_new_forward_event_rows strictly honouring the limit, so # that we know it is safe to just take upper_limit = event_rows[-1][0]. @@ -172,7 +172,7 @@ async def _update_function( if len(event_rows) == target_row_count: limited = True - upper_limit = event_rows[-1][0] # type: int + upper_limit: int = event_rows[-1][0] else: limited = False upper_limit = current_token @@ -191,30 +191,30 @@ async def _update_function( # finally, fetch the ex-outliers rows. We assume there are few enough of these # not to bother with the limit. - ex_outliers_rows = await self._store.get_ex_outlier_stream_rows( + ex_outliers_rows: List[Tuple] = await self._store.get_ex_outlier_stream_rows( instance_name, from_token, upper_limit - ) # type: List[Tuple] + ) # we now need to turn the raw database rows returned into tuples suitable # for the replication protocol (basically, we add an identifier to # distinguish the row type). At the same time, we can limit the event_rows # to the max stream_id from state_rows. - event_updates = ( + event_updates: Iterable[Tuple[int, Tuple]] = ( (stream_id, (EventsStreamEventRow.TypeId, rest)) for (stream_id, *rest) in event_rows if stream_id <= upper_limit - ) # type: Iterable[Tuple[int, Tuple]] + ) - state_updates = ( + state_updates: Iterable[Tuple[int, Tuple]] = ( (stream_id, (EventsStreamCurrentStateRow.TypeId, rest)) for (stream_id, *rest) in state_rows - ) # type: Iterable[Tuple[int, Tuple]] + ) - ex_outliers_updates = ( + ex_outliers_updates: Iterable[Tuple[int, Tuple]] = ( (stream_id, (EventsStreamEventRow.TypeId, rest)) for (stream_id, *rest) in ex_outliers_rows - ) # type: Iterable[Tuple[int, Tuple]] + ) # we need to return a sorted list, so merge them together. updates = list(heapq.merge(event_updates, state_updates, ex_outliers_updates)) diff --git a/synapse/replication/tcp/streams/federation.py b/synapse/replication/tcp/streams/federation.py index 096a85d363..c445af9bd9 100644 --- a/synapse/replication/tcp/streams/federation.py +++ b/synapse/replication/tcp/streams/federation.py @@ -51,9 +51,9 @@ def __init__(self, hs: "HomeServer"): current_token = current_token_without_instance( federation_sender.get_current_token ) - update_function = ( - federation_sender.get_replication_rows - ) # type: Callable[[str, int, int, int], Awaitable[Tuple[List[Tuple[int, Any]], int, bool]]] + update_function: Callable[ + [str, int, int, int], Awaitable[Tuple[List[Tuple[int, Any]], int, bool]] + ] = federation_sender.get_replication_rows elif hs.should_send_federation(): # federation sender: Query master process diff --git a/synapse/server.py b/synapse/server.py index 2c27d2a7e8..095dba9ad0 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -247,15 +247,15 @@ def __init__( # the key we use to sign events and requests self.signing_key = config.key.signing_key[0] self.config = config - self._listening_services = [] # type: List[twisted.internet.tcp.Port] - self.start_time = None # type: Optional[int] + self._listening_services: List[twisted.internet.tcp.Port] = [] + self.start_time: Optional[int] = None self._instance_id = random_string(5) self._instance_name = config.worker.instance_name self.version_string = version_string - self.datastores = None # type: Optional[Databases] + self.datastores: Optional[Databases] = None self._module_web_resources: Dict[str, IResource] = {} self._module_web_resources_consumed = False diff --git a/synapse/server_notices/consent_server_notices.py b/synapse/server_notices/consent_server_notices.py index e65f6f88fe..4e0f814035 100644 --- a/synapse/server_notices/consent_server_notices.py +++ b/synapse/server_notices/consent_server_notices.py @@ -34,7 +34,7 @@ def __init__(self, hs: "HomeServer"): self._server_notices_manager = hs.get_server_notices_manager() self._store = hs.get_datastore() - self._users_in_progress = set() # type: Set[str] + self._users_in_progress: Set[str] = set() self._current_consent_version = hs.config.user_consent_version self._server_notice_content = hs.config.user_consent_server_notice_content diff --git a/synapse/server_notices/resource_limits_server_notices.py b/synapse/server_notices/resource_limits_server_notices.py index e4b0bc5c72..073b0d754f 100644 --- a/synapse/server_notices/resource_limits_server_notices.py +++ b/synapse/server_notices/resource_limits_server_notices.py @@ -205,7 +205,7 @@ async def _is_room_currently_blocked(self, room_id: str) -> Tuple[bool, List[str # The user has yet to join the server notices room pass - referenced_events = [] # type: List[str] + referenced_events: List[str] = [] if pinned_state_event is not None: referenced_events = list(pinned_state_event.content.get("pinned", [])) diff --git a/synapse/server_notices/server_notices_sender.py b/synapse/server_notices/server_notices_sender.py index c875b15b32..cdf0973d05 100644 --- a/synapse/server_notices/server_notices_sender.py +++ b/synapse/server_notices/server_notices_sender.py @@ -32,10 +32,12 @@ class ServerNoticesSender(WorkerServerNoticesSender): def __init__(self, hs: "HomeServer"): super().__init__(hs) - self._server_notices = ( + self._server_notices: Iterable[ + Union[ConsentServerNotices, ResourceLimitsServerNotices] + ] = ( ConsentServerNotices(hs), ResourceLimitsServerNotices(hs), - ) # type: Iterable[Union[ConsentServerNotices, ResourceLimitsServerNotices]] + ) async def on_user_syncing(self, user_id: str) -> None: """Called when the user performs a sync operation. diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index a1770f620e..6223daf522 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -309,9 +309,9 @@ async def compute_event_context( if old_state: # if we're given the state before the event, then we use that - state_ids_before_event = { + state_ids_before_event: StateMap[str] = { (s.type, s.state_key): s.event_id for s in old_state - } # type: StateMap[str] + } state_group_before_event = None state_group_before_event_prev_group = None deltas_to_state_group_before_event = None @@ -513,23 +513,25 @@ def __init__(self, hs): self.resolve_linearizer = Linearizer(name="state_resolve_lock") # dict of set of event_ids -> _StateCacheEntry. - self._state_cache = ExpiringCache( + self._state_cache: ExpiringCache[ + FrozenSet[int], _StateCacheEntry + ] = ExpiringCache( cache_name="state_cache", clock=self.clock, max_len=100000, expiry_ms=EVICTION_TIMEOUT_SECONDS * 1000, iterable=True, reset_expiry_on_get=True, - ) # type: ExpiringCache[FrozenSet[int], _StateCacheEntry] + ) # # stuff for tracking time spent on state-res by room # # tracks the amount of work done on state res per room - self._state_res_metrics = defaultdict( + self._state_res_metrics: DefaultDict[str, _StateResMetrics] = defaultdict( _StateResMetrics - ) # type: DefaultDict[str, _StateResMetrics] + ) self.clock.looping_call(self._report_metrics, 120 * 1000) @@ -700,9 +702,9 @@ def _report_biggest( items = self._state_res_metrics.items() # log the N biggest rooms - biggest = heapq.nlargest( + biggest: List[Tuple[str, _StateResMetrics]] = heapq.nlargest( n_to_log, items, key=lambda i: extract_key(i[1]) - ) # type: List[Tuple[str, _StateResMetrics]] + ) metrics_logger.debug( "%i biggest rooms for state-res by %s: %s", len(biggest), @@ -754,7 +756,7 @@ def _make_state_cache_entry( # failing that, look for the closest match. prev_group = None - delta_ids = None # type: Optional[StateMap[str]] + delta_ids: Optional[StateMap[str]] = None for old_group, old_state in state_groups_ids.items(): n_delta_ids = {k: v for k, v in new_state.items() if old_state.get(k) != v} diff --git a/synapse/state/v1.py b/synapse/state/v1.py index 318e998813..267193cedf 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -159,7 +159,7 @@ def _seperate( """ state_set_iterator = iter(state_sets) unconflicted_state = dict(next(state_set_iterator)) - conflicted_state = {} # type: MutableStateMap[Set[str]] + conflicted_state: MutableStateMap[Set[str]] = {} for state_set in state_set_iterator: for key, value in state_set.items(): diff --git a/synapse/state/v2.py b/synapse/state/v2.py index 008644cd98..e66e6571c8 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -276,7 +276,7 @@ async def _get_auth_chain_difference( # event IDs if they appear in the `event_map`. This is the intersection of # the event's auth chain with the events in the `event_map` *plus* their # auth event IDs. - events_to_auth_chain = {} # type: Dict[str, Set[str]] + events_to_auth_chain: Dict[str, Set[str]] = {} for event in event_map.values(): chain = {event.event_id} events_to_auth_chain[event.event_id] = chain @@ -301,17 +301,17 @@ async def _get_auth_chain_difference( # ((type, state_key)->event_id) mappings; and (b) we have stripped out # unpersisted events and replaced them with the persisted events in # their auth chain. - state_sets_ids = [] # type: List[Set[str]] + state_sets_ids: List[Set[str]] = [] # For each state set, the unpersisted event IDs reachable (by their auth # chain) from the events in that set. - unpersisted_set_ids = [] # type: List[Set[str]] + unpersisted_set_ids: List[Set[str]] = [] for state_set in state_sets: - set_ids = set() # type: Set[str] + set_ids: Set[str] = set() state_sets_ids.append(set_ids) - unpersisted_ids = set() # type: Set[str] + unpersisted_ids: Set[str] = set() unpersisted_set_ids.append(unpersisted_ids) for event_id in state_set.values(): @@ -334,7 +334,7 @@ async def _get_auth_chain_difference( union = unpersisted_set_ids[0].union(*unpersisted_set_ids[1:]) intersection = unpersisted_set_ids[0].intersection(*unpersisted_set_ids[1:]) - difference_from_event_map = union - intersection # type: Collection[str] + difference_from_event_map: Collection[str] = union - intersection else: difference_from_event_map = () state_sets_ids = [set(state_set.values()) for state_set in state_sets] @@ -458,7 +458,7 @@ async def _reverse_topological_power_sort( The sorted list """ - graph = {} # type: Dict[str, Set[str]] + graph: Dict[str, Set[str]] = {} for idx, event_id in enumerate(event_ids, start=1): await _add_event_and_auth_chain_to_graph( graph, room_id, event_id, event_map, state_res_store, auth_diff @@ -657,7 +657,7 @@ async def _get_mainline_depth_for_event( """ room_id = event.room_id - tmp_event = event # type: Optional[EventBase] + tmp_event: Optional[EventBase] = event # We do an iterative search, replacing `event with the power level in its # auth events (if any) @@ -767,7 +767,7 @@ def lexicographical_topological_sort( # outgoing edges, c.f. # https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm outdegree_map = graph - reverse_graph = {} # type: Dict[str, Set[str]] + reverse_graph: Dict[str, Set[str]] = {} # Lists of nodes with zero out degree. Is actually a tuple of # `(key(node), node)` so that sorting does the right thing diff --git a/synapse/streams/events.py b/synapse/streams/events.py index 20fceaa935..99b0aac2fb 100644 --- a/synapse/streams/events.py +++ b/synapse/streams/events.py @@ -32,9 +32,9 @@ class EventSources: } def __init__(self, hs): - self.sources = { + self.sources: Dict[str, Any] = { name: cls(hs) for name, cls in EventSources.SOURCE_TYPES.items() - } # type: Dict[str, Any] + } self.store = hs.get_datastore() def get_current_token(self) -> StreamToken: diff --git a/synapse/types.py b/synapse/types.py index 64c442bd0f..fad23c8700 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -210,7 +210,7 @@ class DomainSpecificString(metaclass=abc.ABCMeta): 'domain' : The domain part of the name """ - SIGIL = abc.abstractproperty() # type: str # type: ignore + SIGIL: str = abc.abstractproperty() # type: ignore localpart = attr.ib(type=str) domain = attr.ib(type=str) @@ -304,7 +304,7 @@ class GroupID(DomainSpecificString): @classmethod def from_string(cls: Type[DS], s: str) -> DS: - group_id = super().from_string(s) # type: DS # type: ignore + group_id: DS = super().from_string(s) # type: ignore if not group_id.localpart: raise SynapseError(400, "Group ID cannot be empty", Codes.INVALID_PARAM) @@ -600,7 +600,7 @@ class StreamToken: groups_key = attr.ib(type=int) _SEPARATOR = "_" - START = None # type: StreamToken + START: "StreamToken" @classmethod async def from_string(cls, store: "DataStore", string: str) -> "StreamToken": diff --git a/synapse/visibility.py b/synapse/visibility.py index 490fb26e81..1dc6b90275 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -90,7 +90,7 @@ async def filter_events_for_client( AccountDataTypes.IGNORED_USER_LIST, user_id ) - ignore_list = frozenset() # type: FrozenSet[str] + ignore_list: FrozenSet[str] = frozenset() if ignore_dict_content: ignored_users_dict = ignore_dict_content.get("ignored_users", {}) if isinstance(ignored_users_dict, dict): From 5ecad4e7a57610baa55f64f1389b92d483716155 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 15 Jul 2021 12:38:05 +0200 Subject: [PATCH 412/619] Update the logcontext doc (#10353) By referring to awaitables instead of deferreds. --- changelog.d/10353.doc | 1 + docs/log_contexts.md | 331 +++++++++++++----------------------------- 2 files changed, 102 insertions(+), 230 deletions(-) create mode 100644 changelog.d/10353.doc diff --git a/changelog.d/10353.doc b/changelog.d/10353.doc new file mode 100644 index 0000000000..274ac83549 --- /dev/null +++ b/changelog.d/10353.doc @@ -0,0 +1 @@ +Refresh the logcontext dev documentation. diff --git a/docs/log_contexts.md b/docs/log_contexts.md index 9a43d46091..d49dce8830 100644 --- a/docs/log_contexts.md +++ b/docs/log_contexts.md @@ -14,12 +14,16 @@ The `synapse.logging.context` module provides a facilities for managing the current log context (as well as providing the `LoggingContextFilter` class). -Deferreds make the whole thing complicated, so this document describes +Asynchronous functions make the whole thing complicated, so this document describes how it all works, and how to write code which follows the rules. -## Logcontexts without Deferreds +In this document, "awaitable" refers to any object which can be `await`ed. In the context of +Synapse, that normally means either a coroutine or a Twisted +[`Deferred`](https://twistedmatrix.com/documents/current/api/twisted.internet.defer.Deferred.html). -In the absence of any Deferred voodoo, things are simple enough. As with +## Logcontexts without asynchronous code + +In the absence of any asynchronous voodoo, things are simple enough. As with any code of this nature, the rule is that our function should leave things as it found them: @@ -55,126 +59,109 @@ def do_request_handling(): logger.debug("phew") ``` -## Using logcontexts with Deferreds +## Using logcontexts with awaitables -Deferreds --- and in particular, `defer.inlineCallbacks` --- break the -linear flow of code so that there is no longer a single entry point -where we should set the logcontext and a single exit point where we -should remove it. +Awaitables break the linear flow of code so that there is no longer a single entry point +where we should set the logcontext and a single exit point where we should remove it. Consider the example above, where `do_request_handling` needs to do some -blocking operation, and returns a deferred: +blocking operation, and returns an awaitable: ```python -@defer.inlineCallbacks -def handle_request(request_id): +async def handle_request(request_id): with context.LoggingContext() as request_context: request_context.request = request_id - yield do_request_handling() + await do_request_handling() logger.debug("finished") ``` In the above flow: - The logcontext is set -- `do_request_handling` is called, and returns a deferred -- `handle_request` yields the deferred -- The `inlineCallbacks` wrapper of `handle_request` returns a deferred +- `do_request_handling` is called, and returns an awaitable +- `handle_request` awaits the awaitable +- Execution of `handle_request` is suspended So we have stopped processing the request (and will probably go on to start processing the next), without clearing the logcontext. To circumvent this problem, synapse code assumes that, wherever you have -a deferred, you will want to yield on it. To that end, whereever -functions return a deferred, we adopt the following conventions: +an awaitable, you will want to `await` it. To that end, whereever +functions return awaitables, we adopt the following conventions: -**Rules for functions returning deferreds:** +**Rules for functions returning awaitables:** -> - If the deferred is already complete, the function returns with the +> - If the awaitable is already complete, the function returns with the > same logcontext it started with. -> - If the deferred is incomplete, the function clears the logcontext -> before returning; when the deferred completes, it restores the +> - If the awaitable is incomplete, the function clears the logcontext +> before returning; when the awaitable completes, it restores the > logcontext before running any callbacks. That sounds complicated, but actually it means a lot of code (including the example above) "just works". There are two cases: -- If `do_request_handling` returns a completed deferred, then the +- If `do_request_handling` returns a completed awaitable, then the logcontext will still be in place. In this case, execution will - continue immediately after the `yield`; the "finished" line will + continue immediately after the `await`; the "finished" line will be logged against the right context, and the `with` block restores the original context before we return to the caller. -- If the returned deferred is incomplete, `do_request_handling` clears +- If the returned awaitable is incomplete, `do_request_handling` clears the logcontext before returning. The logcontext is therefore clear - when `handle_request` yields the deferred. At that point, the - `inlineCallbacks` wrapper adds a callback to the deferred, and - returns another (incomplete) deferred to the caller, and it is safe - to begin processing the next request. - - Once `do_request_handling`'s deferred completes, it will reinstate - the logcontext, before running the callback added by the - `inlineCallbacks` wrapper. That callback runs the second half of - `handle_request`, so again the "finished" line will be logged - against the right context, and the `with` block restores the - original context. + when `handle_request` `await`s the awaitable. + + Once `do_request_handling`'s awaitable completes, it will reinstate + the logcontext, before running the second half of `handle_request`, + so again the "finished" line will be logged against the right context, + and the `with` block restores the original context. As an aside, it's worth noting that `handle_request` follows our rules --though that only matters if the caller has its own logcontext which it +- though that only matters if the caller has its own logcontext which it cares about. The following sections describe pitfalls and helpful patterns when implementing these rules. -Always yield your deferreds ---------------------------- +Always await your awaitables +---------------------------- -Whenever you get a deferred back from a function, you should `yield` on -it as soon as possible. (Returning it directly to your caller is ok too, -if you're not doing `inlineCallbacks`.) Do not pass go; do not do any -logging; do not call any other functions. +Whenever you get an awaitable back from a function, you should `await` on +it as soon as possible. Do not pass go; do not do any logging; do not +call any other functions. ```python -@defer.inlineCallbacks -def fun(): +async def fun(): logger.debug("starting") - yield do_some_stuff() # just like this + await do_some_stuff() # just like this - d = more_stuff() - result = yield d # also fine, of course + coro = more_stuff() + result = await coro # also fine, of course return result - -def nonInlineCallbacksFun(): - logger.debug("just a wrapper really") - return do_some_stuff() # this is ok too - the caller will yield on - # it anyway. ``` Provided this pattern is followed all the way back up to the callchain to where the logcontext was set, this will make things work out ok: provided `do_some_stuff` and `more_stuff` follow the rules above, then -so will `fun` (as wrapped by `inlineCallbacks`) and -`nonInlineCallbacksFun`. +so will `fun`. -It's all too easy to forget to `yield`: for instance if we forgot that -`do_some_stuff` returned a deferred, we might plough on regardless. This +It's all too easy to forget to `await`: for instance if we forgot that +`do_some_stuff` returned an awaitable, we might plough on regardless. This leads to a mess; it will probably work itself out eventually, but not before a load of stuff has been logged against the wrong context. (Normally, other things will break, more obviously, if you forget to -`yield`, so this tends not to be a major problem in practice.) +`await`, so this tends not to be a major problem in practice.) Of course sometimes you need to do something a bit fancier with your -Deferreds - not all code follows the linear A-then-B-then-C pattern. +awaitable - not all code follows the linear A-then-B-then-C pattern. Notes on implementing more complex patterns are in later sections. -## Where you create a new Deferred, make it follow the rules +## Where you create a new awaitable, make it follow the rules -Most of the time, a Deferred comes from another synapse function. -Sometimes, though, we need to make up a new Deferred, or we get a -Deferred back from external code. We need to make it follow our rules. +Most of the time, an awaitable comes from another synapse function. +Sometimes, though, we need to make up a new awaitable, or we get an awaitable +back from external code. We need to make it follow our rules. -The easy way to do it is with a combination of `defer.inlineCallbacks`, -and `context.PreserveLoggingContext`. Suppose we want to implement +The easy way to do it is by using `context.make_deferred_yieldable`. Suppose we want to implement `sleep`, which returns a deferred which will run its callbacks after a given number of seconds. That might look like: @@ -186,25 +173,12 @@ def get_sleep_deferred(seconds): return d ``` -That doesn't follow the rules, but we can fix it by wrapping it with -`PreserveLoggingContext` and `yield` ing on it: +That doesn't follow the rules, but we can fix it by calling it through +`context.make_deferred_yieldable`: ```python -@defer.inlineCallbacks -def sleep(seconds): - with PreserveLoggingContext(): - yield get_sleep_deferred(seconds) -``` - -This technique works equally for external functions which return -deferreds, or deferreds we have made ourselves. - -You can also use `context.make_deferred_yieldable`, which just does the -boilerplate for you, so the above could be written: - -```python -def sleep(seconds): - return context.make_deferred_yieldable(get_sleep_deferred(seconds)) +async def sleep(seconds): + return await context.make_deferred_yieldable(get_sleep_deferred(seconds)) ``` ## Fire-and-forget @@ -213,20 +187,18 @@ Sometimes you want to fire off a chain of execution, but not wait for its result. That might look a bit like this: ```python -@defer.inlineCallbacks -def do_request_handling(): - yield foreground_operation() +async def do_request_handling(): + await foreground_operation() # *don't* do this background_operation() logger.debug("Request handling complete") -@defer.inlineCallbacks -def background_operation(): - yield first_background_step() +async def background_operation(): + await first_background_step() logger.debug("Completed first step") - yield second_background_step() + await second_background_step() logger.debug("Completed second step") ``` @@ -235,13 +207,13 @@ The above code does a couple of steps in the background after against the `request_context` logcontext, which may or may not be desirable. There are two big problems with the above, however. The first problem is that, if `background_operation` returns an incomplete -Deferred, it will expect its caller to `yield` immediately, so will have +awaitable, it will expect its caller to `await` immediately, so will have cleared the logcontext. In this example, that means that 'Request handling complete' will be logged without any context. The second problem, which is potentially even worse, is that when the -Deferred returned by `background_operation` completes, it will restore -the original logcontext. There is nothing waiting on that Deferred, so +awaitable returned by `background_operation` completes, it will restore +the original logcontext. There is nothing waiting on that awaitable, so the logcontext will leak into the reactor and possibly get attached to some arbitrary future operation. @@ -254,9 +226,8 @@ deferred completes will be the empty logcontext), and will restore the current logcontext before continuing the foreground process: ```python -@defer.inlineCallbacks -def do_request_handling(): - yield foreground_operation() +async def do_request_handling(): + await foreground_operation() # start background_operation off in the empty logcontext, to # avoid leaking the current context into the reactor. @@ -274,16 +245,15 @@ Obviously that option means that the operations done in The second option is to use `context.run_in_background`, which wraps a function so that it doesn't reset the logcontext even when it returns -an incomplete deferred, and adds a callback to the returned deferred to +an incomplete awaitable, and adds a callback to the returned awaitable to reset the logcontext. In other words, it turns a function that follows -the Synapse rules about logcontexts and Deferreds into one which behaves +the Synapse rules about logcontexts and awaitables into one which behaves more like an external function --- the opposite operation to that described in the previous section. It can be used like this: ```python -@defer.inlineCallbacks -def do_request_handling(): - yield foreground_operation() +async def do_request_handling(): + await foreground_operation() context.run_in_background(background_operation) @@ -294,152 +264,53 @@ def do_request_handling(): ## Passing synapse deferreds into third-party functions A typical example of this is where we want to collect together two or -more deferred via `defer.gatherResults`: +more awaitables via `defer.gatherResults`: ```python -d1 = operation1() -d2 = operation2() -d3 = defer.gatherResults([d1, d2]) +a1 = operation1() +a2 = operation2() +a3 = defer.gatherResults([a1, a2]) ``` This is really a variation of the fire-and-forget problem above, in that -we are firing off `d1` and `d2` without yielding on them. The difference +we are firing off `a1` and `a2` without awaiting on them. The difference is that we now have third-party code attached to their callbacks. Anyway either technique given in the [Fire-and-forget](#fire-and-forget) section will work. -Of course, the new Deferred returned by `gatherResults` needs to be +Of course, the new awaitable returned by `gather` needs to be wrapped in order to make it follow the logcontext rules before we can -yield it, as described in [Where you create a new Deferred, make it +yield it, as described in [Where you create a new awaitable, make it follow the -rules](#where-you-create-a-new-deferred-make-it-follow-the-rules). +rules](#where-you-create-a-new-awaitable-make-it-follow-the-rules). So, option one: reset the logcontext before starting the operations to be gathered: ```python -@defer.inlineCallbacks -def do_request_handling(): +async def do_request_handling(): with PreserveLoggingContext(): - d1 = operation1() - d2 = operation2() - result = yield defer.gatherResults([d1, d2]) + a1 = operation1() + a2 = operation2() + result = await defer.gatherResults([a1, a2]) ``` In this case particularly, though, option two, of using -`context.preserve_fn` almost certainly makes more sense, so that +`context.run_in_background` almost certainly makes more sense, so that `operation1` and `operation2` are both logged against the original logcontext. This looks like: ```python -@defer.inlineCallbacks -def do_request_handling(): - d1 = context.preserve_fn(operation1)() - d2 = context.preserve_fn(operation2)() +async def do_request_handling(): + a1 = context.run_in_background(operation1) + a2 = context.run_in_background(operation2) - with PreserveLoggingContext(): - result = yield defer.gatherResults([d1, d2]) + result = await make_deferred_yieldable(defer.gatherResults([a1, a2])) ``` -## Was all this really necessary? - -The conventions used work fine for a linear flow where everything -happens in series via `defer.inlineCallbacks` and `yield`, but are -certainly tricky to follow for any more exotic flows. It's hard not to -wonder if we could have done something else. - -We're not going to rewrite Synapse now, so the following is entirely of -academic interest, but I'd like to record some thoughts on an -alternative approach. - -I briefly prototyped some code following an alternative set of rules. I -think it would work, but I certainly didn't get as far as thinking how -it would interact with concepts as complicated as the cache descriptors. - -My alternative rules were: - -- functions always preserve the logcontext of their caller, whether or - not they are returning a Deferred. -- Deferreds returned by synapse functions run their callbacks in the - same context as the function was orignally called in. - -The main point of this scheme is that everywhere that sets the -logcontext is responsible for clearing it before returning control to -the reactor. - -So, for example, if you were the function which started a -`with LoggingContext` block, you wouldn't `yield` within it --- instead -you'd start off the background process, and then leave the `with` block -to wait for it: - -```python -def handle_request(request_id): - with context.LoggingContext() as request_context: - request_context.request = request_id - d = do_request_handling() - - def cb(r): - logger.debug("finished") - - d.addCallback(cb) - return d -``` - -(in general, mixing `with LoggingContext` blocks and -`defer.inlineCallbacks` in the same function leads to slighly -counter-intuitive code, under this scheme). - -Because we leave the original `with` block as soon as the Deferred is -returned (as opposed to waiting for it to be resolved, as we do today), -the logcontext is cleared before control passes back to the reactor; so -if there is some code within `do_request_handling` which needs to wait -for a Deferred to complete, there is no need for it to worry about -clearing the logcontext before doing so: - -```python -def handle_request(): - r = do_some_stuff() - r.addCallback(do_some_more_stuff) - return r -``` - ---- and provided `do_some_stuff` follows the rules of returning a -Deferred which runs its callbacks in the original logcontext, all is -happy. - -The business of a Deferred which runs its callbacks in the original -logcontext isn't hard to achieve --- we have it today, in the shape of -`context._PreservingContextDeferred`: - -```python -def do_some_stuff(): - deferred = do_some_io() - pcd = _PreservingContextDeferred(LoggingContext.current_context()) - deferred.chainDeferred(pcd) - return pcd -``` - -It turns out that, thanks to the way that Deferreds chain together, we -automatically get the property of a context-preserving deferred with -`defer.inlineCallbacks`, provided the final Defered the function -`yields` on has that property. So we can just write: - -```python -@defer.inlineCallbacks -def handle_request(): - yield do_some_stuff() - yield do_some_more_stuff() -``` - -To conclude: I think this scheme would have worked equally well, with -less danger of messing it up, and probably made some more esoteric code -easier to write. But again --- changing the conventions of the entire -Synapse codebase is not a sensible option for the marginal improvement -offered. - -## A note on garbage-collection of Deferred chains +## A note on garbage-collection of awaitable chains -It turns out that our logcontext rules do not play nicely with Deferred +It turns out that our logcontext rules do not play nicely with awaitable chains which get orphaned and garbage-collected. Imagine we have some code that looks like this: @@ -451,13 +322,12 @@ def on_something_interesting(): for d in listener_queue: d.callback("foo") -@defer.inlineCallbacks -def await_something_interesting(): - new_deferred = defer.Deferred() - listener_queue.append(new_deferred) +async def await_something_interesting(): + new_awaitable = defer.Deferred() + listener_queue.append(new_awaitable) with PreserveLoggingContext(): - yield new_deferred + await new_awaitable ``` Obviously, the idea here is that we have a bunch of things which are @@ -476,18 +346,19 @@ def reset_listener_queue(): listener_queue.clear() ``` -So, both ends of the deferred chain have now dropped their references, -and the deferred chain is now orphaned, and will be garbage-collected at -some point. Note that `await_something_interesting` is a generator -function, and when Python garbage-collects generator functions, it gives -them a chance to clean up by making the `yield` raise a `GeneratorExit` +So, both ends of the awaitable chain have now dropped their references, +and the awaitable chain is now orphaned, and will be garbage-collected at +some point. Note that `await_something_interesting` is a coroutine, +which Python implements as a generator function. When Python +garbage-collects generator functions, it gives them a chance to +clean up by making the `async` (or `yield`) raise a `GeneratorExit` exception. In our case, that means that the `__exit__` handler of `PreserveLoggingContext` will carefully restore the request context, but there is now nothing waiting for its return, so the request context is never cleared. -To reiterate, this problem only arises when *both* ends of a deferred -chain are dropped. Dropping the the reference to a deferred you're -supposed to be calling is probably bad practice, so this doesn't +To reiterate, this problem only arises when *both* ends of a awaitable +chain are dropped. Dropping the the reference to an awaitable you're +supposed to be awaiting is bad practice, so this doesn't actually happen too much. Unfortunately, when it does happen, it will lead to leaked logcontexts which are incredibly hard to track down. From ac5c221208ceb499cf8e9305b03efe1765ba48f6 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 15 Jul 2021 11:52:56 +0100 Subject: [PATCH 413/619] Stagger send presence to remotes (#10398) This is to help with performance, where trying to connect to thousands of hosts at once can consume a lot of CPU (due to TLS etc). Co-authored-by: Brendan Abolivier --- changelog.d/10398.misc | 1 + synapse/federation/sender/__init__.py | 96 ++++++++++++++++++- .../sender/per_destination_queue.py | 16 +++- tests/events/test_presence_router.py | 8 ++ 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10398.misc diff --git a/changelog.d/10398.misc b/changelog.d/10398.misc new file mode 100644 index 0000000000..326e54655a --- /dev/null +++ b/changelog.d/10398.misc @@ -0,0 +1 @@ +Stagger sending of presence update to remote servers, reducing CPU spikes caused by starting many connections to remote servers at once. diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index 0960f033bc..d980e0d986 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -14,9 +14,12 @@ import abc import logging +from collections import OrderedDict from typing import TYPE_CHECKING, Dict, Hashable, Iterable, List, Optional, Set, Tuple +import attr from prometheus_client import Counter +from typing_extensions import Literal from twisted.internet import defer @@ -33,8 +36,12 @@ event_processing_loop_room_count, events_processed_counter, ) -from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.metrics.background_process_metrics import ( + run_as_background_process, + wrap_as_background_process, +) from synapse.types import JsonDict, ReadReceipt, RoomStreamToken +from synapse.util import Clock from synapse.util.metrics import Measure if TYPE_CHECKING: @@ -137,6 +144,84 @@ async def get_replication_rows( raise NotImplementedError() +@attr.s +class _PresenceQueue: + """A queue of destinations that need to be woken up due to new presence + updates. + + Staggers waking up of per destination queues to ensure that we don't attempt + to start TLS connections with many hosts all at once, leading to pinned CPU. + """ + + # The maximum duration in seconds between queuing up a destination and it + # being woken up. + _MAX_TIME_IN_QUEUE = 30.0 + + # The maximum duration in seconds between waking up consecutive destination + # queues. + _MAX_DELAY = 0.1 + + sender: "FederationSender" = attr.ib() + clock: Clock = attr.ib() + queue: "OrderedDict[str, Literal[None]]" = attr.ib(factory=OrderedDict) + processing: bool = attr.ib(default=False) + + def add_to_queue(self, destination: str) -> None: + """Add a destination to the queue to be woken up.""" + + self.queue[destination] = None + + if not self.processing: + self._handle() + + @wrap_as_background_process("_PresenceQueue.handle") + async def _handle(self) -> None: + """Background process to drain the queue.""" + + if not self.queue: + return + + assert not self.processing + self.processing = True + + try: + # We start with a delay that should drain the queue quickly enough that + # we process all destinations in the queue in _MAX_TIME_IN_QUEUE + # seconds. + # + # We also add an upper bound to the delay, to gracefully handle the + # case where the queue only has a few entries in it. + current_sleep_seconds = min( + self._MAX_DELAY, self._MAX_TIME_IN_QUEUE / len(self.queue) + ) + + while self.queue: + destination, _ = self.queue.popitem(last=False) + + queue = self.sender._get_per_destination_queue(destination) + + if not queue._new_data_to_send: + # The per destination queue has already been woken up. + continue + + queue.attempt_new_transaction() + + await self.clock.sleep(current_sleep_seconds) + + if not self.queue: + break + + # More destinations may have been added to the queue, so we may + # need to reduce the delay to ensure everything gets processed + # within _MAX_TIME_IN_QUEUE seconds. + current_sleep_seconds = min( + current_sleep_seconds, self._MAX_TIME_IN_QUEUE / len(self.queue) + ) + + finally: + self.processing = False + + class FederationSender(AbstractFederationSender): def __init__(self, hs: "HomeServer"): self.hs = hs @@ -208,6 +293,8 @@ def __init__(self, hs: "HomeServer"): self._external_cache = hs.get_external_cache() + self._presence_queue = _PresenceQueue(self, self.clock) + def _get_per_destination_queue(self, destination: str) -> PerDestinationQueue: """Get or create a PerDestinationQueue for the given destination @@ -517,7 +604,12 @@ def send_presence_to_destinations( self._instance_name, destination ): continue - self._get_per_destination_queue(destination).send_presence(states) + + self._get_per_destination_queue(destination).send_presence( + states, start_loop=False + ) + + self._presence_queue.add_to_queue(destination) def build_and_send_edu( self, diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py index d06a3aff19..c11d1f6d31 100644 --- a/synapse/federation/sender/per_destination_queue.py +++ b/synapse/federation/sender/per_destination_queue.py @@ -171,14 +171,24 @@ def send_pdu(self, pdu: EventBase) -> None: self.attempt_new_transaction() - def send_presence(self, states: Iterable[UserPresenceState]) -> None: - """Add presence updates to the queue. Start the transmission loop if necessary. + def send_presence( + self, states: Iterable[UserPresenceState], start_loop: bool = True + ) -> None: + """Add presence updates to the queue. + + Args: + states: Presence updates to send + start_loop: Whether to start the transmission loop if not already + running. Args: states: presence to send """ self._pending_presence.update({state.user_id: state for state in states}) - self.attempt_new_transaction() + self._new_data_to_send = True + + if start_loop: + self.attempt_new_transaction() def queue_read_receipt(self, receipt: ReadReceipt) -> None: """Add a RR to the list to be sent. Doesn't start the transmission loop yet diff --git a/tests/events/test_presence_router.py b/tests/events/test_presence_router.py index c4ad33194d..3f41e99950 100644 --- a/tests/events/test_presence_router.py +++ b/tests/events/test_presence_router.py @@ -285,6 +285,10 @@ def test_send_local_online_presence_to_with_module(self): presence_updates, _ = sync_presence(self, self.presence_receiving_user_two_id) self.assertEqual(len(presence_updates), 3) + # We stagger sending of presence, so we need to wait a bit for them to + # get sent out. + self.reactor.advance(60) + # Test that sending to a remote user works remote_user_id = "@far_away_person:island" @@ -301,6 +305,10 @@ def test_send_local_online_presence_to_with_module(self): self.module_api.send_local_online_presence_to([remote_user_id]) ) + # We stagger sending of presence, so we need to wait a bit for them to + # get sent out. + self.reactor.advance(60) + # Check that the expected presence updates were sent # We explicitly compare using sets as we expect that calling # module_api.send_local_online_presence_to will create a presence From c1414550490355aa9c4e2bf80fa4d13bd06e28d1 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 15 Jul 2021 12:47:55 +0100 Subject: [PATCH 414/619] Docs: Use something other than the document name to describe a page (#10399) Our documentation has a history of using a document's name as a way to link to it, such as "See [workers.md]() for details". This makes sense when you're traversing a directory of files, but less sense when the files are abstracted away - as they are on the documentation website. This PR changes the links to various documentation pages to something that fits better into the surrounding sentence, as you would when making any hyperlink on the web. --- changelog.d/10399.doc | 1 + docs/MSC1711_certificates_FAQ.md | 4 ++-- docs/admin_api/server_notices.md | 2 +- docs/consent_tracking.md | 2 +- docs/delegate.md | 2 +- docs/federate.md | 8 ++++---- docs/replication.md | 2 +- docs/reverse_proxy.md | 2 +- docs/server_notices.md | 4 ++-- docs/setup/installation.md | 10 ++++------ docs/systemd-with-workers/README.md | 2 +- docs/workers.md | 6 +++--- 12 files changed, 22 insertions(+), 23 deletions(-) create mode 100644 changelog.d/10399.doc diff --git a/changelog.d/10399.doc b/changelog.d/10399.doc new file mode 100644 index 0000000000..b596ac5627 --- /dev/null +++ b/changelog.d/10399.doc @@ -0,0 +1 @@ +Rewrite the text of links to be clearer in the documentation. diff --git a/docs/MSC1711_certificates_FAQ.md b/docs/MSC1711_certificates_FAQ.md index 283f288aaf..7d71c190ab 100644 --- a/docs/MSC1711_certificates_FAQ.md +++ b/docs/MSC1711_certificates_FAQ.md @@ -132,7 +132,7 @@ your domain, you can simply route all traffic through the reverse proxy by updating the SRV record appropriately (or removing it, if the proxy listens on 8448). -See [reverse_proxy.md](reverse_proxy.md) for information on setting up a +See [the reverse proxy documentation](reverse_proxy.md) for information on setting up a reverse proxy. #### Option 3: add a .well-known file to delegate your matrix traffic @@ -303,7 +303,7 @@ We no longer actively recommend against using a reverse proxy. Many admins will find it easier to direct federation traffic to a reverse proxy and manage their own TLS certificates, and this is a supported configuration. -See [reverse_proxy.md](reverse_proxy.md) for information on setting up a +See [the reverse proxy documentation](reverse_proxy.md) for information on setting up a reverse proxy. ### Do I still need to give my TLS certificates to Synapse if I am using a reverse proxy? diff --git a/docs/admin_api/server_notices.md b/docs/admin_api/server_notices.md index 858b052b84..323138491a 100644 --- a/docs/admin_api/server_notices.md +++ b/docs/admin_api/server_notices.md @@ -45,4 +45,4 @@ Once the notice has been sent, the API will return the following response: ``` Note that server notices must be enabled in `homeserver.yaml` before this API -can be used. See [server_notices.md](../server_notices.md) for more information. +can be used. See [the server notices documentation](../server_notices.md) for more information. diff --git a/docs/consent_tracking.md b/docs/consent_tracking.md index 3f997e5903..911a1f95db 100644 --- a/docs/consent_tracking.md +++ b/docs/consent_tracking.md @@ -152,7 +152,7 @@ version of the policy. To do so: * ensure that the consent resource is configured, as in the previous section - * ensure that server notices are configured, as in [server_notices.md](server_notices.md). + * ensure that server notices are configured, as in [the server notice documentation](server_notices.md). * Add `server_notice_content` under `user_consent` in `homeserver.yaml`. For example: diff --git a/docs/delegate.md b/docs/delegate.md index 208ddb6277..05cb635047 100644 --- a/docs/delegate.md +++ b/docs/delegate.md @@ -74,7 +74,7 @@ We no longer actively recommend against using a reverse proxy. Many admins will find it easier to direct federation traffic to a reverse proxy and manage their own TLS certificates, and this is a supported configuration. -See [reverse_proxy.md](reverse_proxy.md) for information on setting up a +See [the reverse proxy documentation](reverse_proxy.md) for information on setting up a reverse proxy. ### Do I still need to give my TLS certificates to Synapse if I am using a reverse proxy? diff --git a/docs/federate.md b/docs/federate.md index 89c2b19638..5107f995be 100644 --- a/docs/federate.md +++ b/docs/federate.md @@ -14,7 +14,7 @@ you set the `server_name` to match your machine's public DNS hostname. For this default configuration to work, you will need to listen for TLS connections on port 8448. The preferred way to do that is by using a -reverse proxy: see [reverse_proxy.md](reverse_proxy.md) for instructions +reverse proxy: see [the reverse proxy documentation](reverse_proxy.md) for instructions on how to correctly set one up. In some cases you might not want to run Synapse on the machine that has @@ -23,7 +23,7 @@ traffic to use a different port than 8448. For example, you might want to have your user names look like `@user:example.com`, but you want to run Synapse on `synapse.example.com` on port 443. This can be done using delegation, which allows an admin to control where federation traffic should -be sent. See [delegate.md](delegate.md) for instructions on how to set this up. +be sent. See [the delegation documentation](delegate.md) for instructions on how to set this up. Once federation has been configured, you should be able to join a room over federation. A good place to start is `#synapse:matrix.org` - a room for @@ -44,8 +44,8 @@ a complicated dance which requires connections in both directions). Another common problem is that people on other servers can't join rooms that you invite them to. This can be caused by an incorrectly-configured reverse -proxy: see [reverse_proxy.md](reverse_proxy.md) for instructions on how to correctly -configure a reverse proxy. +proxy: see [the reverse proxy documentation](reverse_proxy.md) for instructions on how +to correctly configure a reverse proxy. ### Known issues diff --git a/docs/replication.md b/docs/replication.md index ed88233157..e82df0de8a 100644 --- a/docs/replication.md +++ b/docs/replication.md @@ -28,7 +28,7 @@ minimal. ### The Replication Protocol -See [tcp_replication.md](tcp_replication.md) +See [the TCP replication documentation](tcp_replication.md). ### The Slaved DataStore diff --git a/docs/reverse_proxy.md b/docs/reverse_proxy.md index 0f3fbbed8b..76bb45aff2 100644 --- a/docs/reverse_proxy.md +++ b/docs/reverse_proxy.md @@ -21,7 +21,7 @@ port 8448. Where these are different, we refer to the 'client port' and the 'federation port'. See [the Matrix specification](https://matrix.org/docs/spec/server_server/latest#resolving-server-names) for more details of the algorithm used for federation connections, and -[delegate.md](delegate.md) for instructions on setting up delegation. +[Delegation](delegate.md) for instructions on setting up delegation. **NOTE**: Your reverse proxy must not `canonicalise` or `normalise` the requested URI in any way (for example, by decoding `%xx` escapes). diff --git a/docs/server_notices.md b/docs/server_notices.md index 950a6608e9..339d10a0ab 100644 --- a/docs/server_notices.md +++ b/docs/server_notices.md @@ -3,8 +3,8 @@ 'Server Notices' are a new feature introduced in Synapse 0.30. They provide a channel whereby server administrators can send messages to users on the server. -They are used as part of communication of the server polices(see -[consent_tracking.md](consent_tracking.md)), however the intention is that +They are used as part of communication of the server polices (see +[Consent Tracking](consent_tracking.md)), however the intention is that they may also find a use for features such as "Message of the day". This is a feature specific to Synapse, but it uses standard Matrix diff --git a/docs/setup/installation.md b/docs/setup/installation.md index afa57a825d..f18f804c23 100644 --- a/docs/setup/installation.md +++ b/docs/setup/installation.md @@ -412,7 +412,7 @@ instead. Advantages include: - allowing the DB to be run on separate hardware For information on how to install and use PostgreSQL in Synapse, please see -[docs/postgres.md](../postgres.md) +[Using Postgres](../postgres.md) SQLite is only acceptable for testing purposes. SQLite should not be used in a production server. Synapse will perform poorly when using @@ -427,7 +427,7 @@ over HTTPS. The recommended way to do so is to set up a reverse proxy on port `8448`. You can find documentation on doing so in -[docs/reverse_proxy.md](../reverse_proxy.md). +[the reverse proxy documentation](../reverse_proxy.md). Alternatively, you can configure Synapse to expose an HTTPS port. To do so, you will need to edit `homeserver.yaml`, as follows: @@ -454,7 +454,7 @@ so, you will need to edit `homeserver.yaml`, as follows: `cert.pem`). For a more detailed guide to configuring your server for federation, see -[federate.md](../federate.md). +[Federation](../federate.md). ### Client Well-Known URI @@ -566,9 +566,7 @@ on your server even if `enable_registration` is `false`. ### Setting up a TURN server For reliable VoIP calls to be routed via this homeserver, you MUST configure -a TURN server. See -[docs/turn-howto.md](../turn-howto.md) -for details. +a TURN server. See [TURN setup](../turn-howto.md) for details. ### URL previews diff --git a/docs/systemd-with-workers/README.md b/docs/systemd-with-workers/README.md index 3237ba4e93..b160d93528 100644 --- a/docs/systemd-with-workers/README.md +++ b/docs/systemd-with-workers/README.md @@ -14,7 +14,7 @@ contains an example configuration for the `federation_reader` worker. ## Synapse configuration files -See [workers.md](../workers.md) for information on how to set up the +See [the worker documentation](../workers.md) for information on how to set up the configuration files and reverse-proxy correctly. Below is a sample `federation_reader` worker configuration file. ```yaml diff --git a/docs/workers.md b/docs/workers.md index 797758ee84..d8672324c3 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -73,7 +73,7 @@ https://hub.docker.com/r/matrixdotorg/synapse/. To make effective use of the workers, you will need to configure an HTTP reverse-proxy such as nginx or haproxy, which will direct incoming requests to the correct worker, or to the main synapse instance. See -[reverse_proxy.md](reverse_proxy.md) for information on setting up a reverse +[the reverse proxy documentation](reverse_proxy.md) for information on setting up a reverse proxy. When using workers, each worker process has its own configuration file which @@ -170,8 +170,8 @@ Finally, you need to start your worker processes. This can be done with either `synctl` or your distribution's preferred service manager such as `systemd`. We recommend the use of `systemd` where available: for information on setting up `systemd` to start synapse workers, see -[systemd-with-workers](systemd-with-workers). To use `synctl`, see -[synctl_workers.md](synctl_workers.md). +[Systemd with Workers](systemd-with-workers). To use `synctl`, see +[Using synctl with Workers](synctl_workers.md). ## Available worker applications From 23a90a6a5c3bde22482b1910bb8b6f54c1c581cc Mon Sep 17 00:00:00 2001 From: Luke Walsh Date: Thu, 15 Jul 2021 20:18:58 +0800 Subject: [PATCH 415/619] Updating install prerequisites for newer macOS & ARM Macs. (#9971) --- changelog.d/9971.doc | 1 + docs/setup/installation.md | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 changelog.d/9971.doc diff --git a/changelog.d/9971.doc b/changelog.d/9971.doc new file mode 100644 index 0000000000..ada68f70ca --- /dev/null +++ b/changelog.d/9971.doc @@ -0,0 +1 @@ +Updated installation dependencies for newer macOS versions and ARM Macs. Contributed by Luke Walsh. diff --git a/docs/setup/installation.md b/docs/setup/installation.md index f18f804c23..8540a7b0c1 100644 --- a/docs/setup/installation.md +++ b/docs/setup/installation.md @@ -166,13 +166,16 @@ sudo dnf groupinstall "Development Tools" Installing prerequisites on macOS: +You may need to install the latest Xcode developer tools: ```sh xcode-select --install -sudo easy_install pip -sudo pip install virtualenv -brew install pkg-config libffi ``` +On ARM-based Macs you may need to explicitly install libjpeg which is a pillow dependency. You can use Homebrew (https://brew.sh): +```sh + brew install jpeg + ``` + On macOS Catalina (10.15) you may need to explicitly install OpenSSL via brew and inform `pip` about it so that `psycopg2` builds: From 6a6006825067827b533b9c2b35c5a1d6a796e27c Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Thu, 15 Jul 2021 13:51:27 +0100 Subject: [PATCH 416/619] Add tests to characterise the current behaviour of R30 phone-home metrics (#10315) Signed-off-by: Olivier Wilkinson (reivilibre) --- changelog.d/10315.misc | 1 + tests/app/test_phone_stats_home.py | 153 +++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 changelog.d/10315.misc create mode 100644 tests/app/test_phone_stats_home.py diff --git a/changelog.d/10315.misc b/changelog.d/10315.misc new file mode 100644 index 0000000000..2c78644e20 --- /dev/null +++ b/changelog.d/10315.misc @@ -0,0 +1 @@ +Add tests to characterise the current behaviour of R30 phone-home metrics. diff --git a/tests/app/test_phone_stats_home.py b/tests/app/test_phone_stats_home.py new file mode 100644 index 0000000000..2da6ba4dde --- /dev/null +++ b/tests/app/test_phone_stats_home.py @@ -0,0 +1,153 @@ +import synapse +from synapse.rest.client.v1 import login, room + +from tests import unittest +from tests.unittest import HomeserverTestCase + +ONE_DAY_IN_SECONDS = 86400 + + +class PhoneHomeTestCase(HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + ] + + # Override the retention time for the user_ips table because otherwise it + # gets pruned too aggressively for our R30 test. + @unittest.override_config({"user_ips_max_age": "365d"}) + def test_r30_minimum_usage(self): + """ + Tests the minimum amount of interaction necessary for the R30 metric + to consider a user 'retained'. + """ + + # Register a user, log it in, create a room and send a message + user_id = self.register_user("u1", "secret!") + access_token = self.login("u1", "secret!") + room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token) + self.helper.send(room_id, "message", tok=access_token) + + # Check the R30 results do not count that user. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + # Advance 30 days (+ 1 second, because strict inequality causes issues if we are + # bang on 30 days later). + self.reactor.advance(30 * ONE_DAY_IN_SECONDS + 1) + + # (Make sure the user isn't somehow counted by this point.) + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + # Send a message (this counts as activity) + self.helper.send(room_id, "message2", tok=access_token) + + # We have to wait some time for _update_client_ips_batch to get + # called and update the user_ips table. + self.reactor.advance(2 * 60 * 60) + + # *Now* the user is counted. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 1, "unknown": 1}) + + # Advance 29 days. The user has now not posted for 29 days. + self.reactor.advance(29 * ONE_DAY_IN_SECONDS) + + # The user is still counted. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 1, "unknown": 1}) + + # Advance another day. The user has now not posted for 30 days. + self.reactor.advance(ONE_DAY_IN_SECONDS) + + # The user is now no longer counted in R30. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + def test_r30_minimum_usage_using_default_config(self): + """ + Tests the minimum amount of interaction necessary for the R30 metric + to consider a user 'retained'. + + N.B. This test does not override the `user_ips_max_age` config setting, + which defaults to 28 days. + """ + + # Register a user, log it in, create a room and send a message + user_id = self.register_user("u1", "secret!") + access_token = self.login("u1", "secret!") + room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token) + self.helper.send(room_id, "message", tok=access_token) + + # Check the R30 results do not count that user. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + # Advance 30 days (+ 1 second, because strict inequality causes issues if we are + # bang on 30 days later). + self.reactor.advance(30 * ONE_DAY_IN_SECONDS + 1) + + # (Make sure the user isn't somehow counted by this point.) + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + # Send a message (this counts as activity) + self.helper.send(room_id, "message2", tok=access_token) + + # We have to wait some time for _update_client_ips_batch to get + # called and update the user_ips table. + self.reactor.advance(2 * 60 * 60) + + # *Now* the user is counted. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 1, "unknown": 1}) + + # Advance 27 days. The user has now not posted for 27 days. + self.reactor.advance(27 * ONE_DAY_IN_SECONDS) + + # The user is still counted. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 1, "unknown": 1}) + + # Advance another day. The user has now not posted for 28 days. + self.reactor.advance(ONE_DAY_IN_SECONDS) + + # The user is now no longer counted in R30. + # (This is because the user_ips table has been pruned, which by default + # only preserves the last 28 days of entries.) + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + def test_r30_user_must_be_retained_for_at_least_a_month(self): + """ + Tests that a newly-registered user must be retained for a whole month + before appearing in the R30 statistic, even if they post every day + during that time! + """ + # Register a user and send a message + user_id = self.register_user("u1", "secret!") + access_token = self.login("u1", "secret!") + room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token) + self.helper.send(room_id, "message", tok=access_token) + + # Check the user does not contribute to R30 yet. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + for _ in range(30): + # This loop posts a message every day for 30 days + self.reactor.advance(ONE_DAY_IN_SECONDS) + self.helper.send(room_id, "I'm still here", tok=access_token) + + # Notice that the user *still* does not contribute to R30! + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 0}) + + self.reactor.advance(ONE_DAY_IN_SECONDS) + self.helper.send(room_id, "Still here!", tok=access_token) + + # *Now* the user appears in R30. + r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) + self.assertEqual(r30_results, {"all": 1, "unknown": 1}) From 3fffb71254d052c54d7a6eabae8534480f021adc Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Thu, 15 Jul 2021 15:54:22 +0200 Subject: [PATCH 417/619] Make deprecation notice of the spam checker doc more obvious (#10395) --- changelog.d/10395.doc | 1 + docs/spam_checker.md | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10395.doc diff --git a/changelog.d/10395.doc b/changelog.d/10395.doc new file mode 100644 index 0000000000..4bdaea76c5 --- /dev/null +++ b/changelog.d/10395.doc @@ -0,0 +1 @@ +Make deprecation notice of the spam checker doc more obvious. diff --git a/docs/spam_checker.md b/docs/spam_checker.md index c16914e61d..1b6d814937 100644 --- a/docs/spam_checker.md +++ b/docs/spam_checker.md @@ -1,6 +1,8 @@ -**Note: this page of the Synapse documentation is now deprecated. For up to date +

+This page of the Synapse documentation is now deprecated. For up to date documentation on setting up or writing a spam checker module, please see -[this page](https://matrix-org.github.io/synapse/develop/modules.html).** +this page. +

# Handling spam in Synapse From 3acf85c85f62655077f8c4b466389de4a4183604 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 15 Jul 2021 16:02:12 +0100 Subject: [PATCH 418/619] Reduce likelihood of Postgres table scanning `state_groups_state`. (#10359) The postgres statistics collector sometimes massively underestimates the number of distinct state groups are in the `state_groups_state`, which can cause postgres to use table scans for queries for multiple state groups. We fix this by manually setting `n_distinct` on the column. --- changelog.d/10359.bugfix | 1 + ...state_groups_state_n_distinct.sql.postgres | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 changelog.d/10359.bugfix create mode 100644 synapse/storage/schema/state/delta/61/02state_groups_state_n_distinct.sql.postgres diff --git a/changelog.d/10359.bugfix b/changelog.d/10359.bugfix new file mode 100644 index 0000000000..d318f8fa08 --- /dev/null +++ b/changelog.d/10359.bugfix @@ -0,0 +1 @@ +Fix PostgreSQL sometimes using table scans for queries against `state_groups_state` table, taking a long time and a large amount of IO. diff --git a/synapse/storage/schema/state/delta/61/02state_groups_state_n_distinct.sql.postgres b/synapse/storage/schema/state/delta/61/02state_groups_state_n_distinct.sql.postgres new file mode 100644 index 0000000000..35a153da7b --- /dev/null +++ b/synapse/storage/schema/state/delta/61/02state_groups_state_n_distinct.sql.postgres @@ -0,0 +1,34 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +-- By default the postgres statistics collector massively underestimates the +-- number of distinct state groups are in the `state_groups_state`, which can +-- cause postgres to use table scans for queries for multiple state groups. +-- +-- To work around this we can manually tell postgres the number of distinct state +-- groups there are by setting `n_distinct` (a negative value here is the number +-- of distinct values divided by the number of rows, so -0.02 means on average +-- there are 50 rows per distinct value). We don't need a particularly +-- accurate number here, as a) we just want it to always use index scans and b) +-- our estimate is going to be better than the one made by the statistics +-- collector. + +ALTER TABLE state_groups_state ALTER COLUMN state_group SET (n_distinct = -0.02); + +-- Ideally we'd do an `ANALYZE state_groups_state (state_group)` here so that +-- the above gets picked up immediately, but that can take a bit of time so we +-- rely on the autovacuum eventually getting run and doing that in the +-- background for us. From bdfde6dca11a9468372b3c9b327ad3327cbdbe4a Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 15 Jul 2021 18:46:54 +0200 Subject: [PATCH 419/619] Use inline type hints in `http/federation/`, `storage/` and `util/` (#10381) --- changelog.d/10381.misc | 1 + .../http/federation/well_known_resolver.py | 13 +++---- synapse/storage/background_updates.py | 16 ++++---- synapse/storage/database.py | 14 +++---- synapse/storage/databases/main/appservice.py | 4 +- .../storage/databases/main/end_to_end_keys.py | 2 +- .../databases/main/event_federation.py | 26 ++++++------- .../databases/main/event_push_actions.py | 2 +- synapse/storage/databases/main/events.py | 38 +++++++++---------- .../databases/main/events_bg_updates.py | 8 ++-- .../storage/databases/main/events_worker.py | 6 +-- .../storage/databases/main/purge_events.py | 2 +- synapse/storage/databases/main/push_rule.py | 6 +-- .../storage/databases/main/registration.py | 2 +- synapse/storage/databases/main/stream.py | 6 +-- synapse/storage/databases/main/tags.py | 2 +- synapse/storage/databases/main/ui_auth.py | 4 +- synapse/storage/persist_events.py | 16 ++++---- synapse/storage/prepare_database.py | 6 +-- synapse/storage/state.py | 4 +- synapse/storage/util/id_generators.py | 12 +++--- synapse/storage/util/sequence.py | 6 +-- synapse/util/async_helpers.py | 8 ++-- synapse/util/batching_queue.py | 8 ++-- synapse/util/caches/__init__.py | 4 +- synapse/util/caches/cached_call.py | 6 +-- synapse/util/caches/deferred_cache.py | 12 +++--- synapse/util/caches/descriptors.py | 36 +++++++++--------- synapse/util/caches/dictionary_cache.py | 6 +-- synapse/util/caches/expiringcache.py | 4 +- synapse/util/caches/lrucache.py | 8 ++-- synapse/util/caches/response_cache.py | 2 +- synapse/util/caches/stream_change_cache.py | 6 +-- synapse/util/caches/ttlcache.py | 6 +-- synapse/util/iterutils.py | 2 +- synapse/util/macaroons.py | 2 +- synapse/util/metrics.py | 2 +- synapse/util/patch_inline_callbacks.py | 4 +- 38 files changed, 150 insertions(+), 162 deletions(-) create mode 100644 changelog.d/10381.misc diff --git a/changelog.d/10381.misc b/changelog.d/10381.misc new file mode 100644 index 0000000000..eed2d8552a --- /dev/null +++ b/changelog.d/10381.misc @@ -0,0 +1 @@ +Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/synapse/http/federation/well_known_resolver.py b/synapse/http/federation/well_known_resolver.py index 20d39a4ea6..43f2140429 100644 --- a/synapse/http/federation/well_known_resolver.py +++ b/synapse/http/federation/well_known_resolver.py @@ -70,10 +70,8 @@ logger = logging.getLogger(__name__) -_well_known_cache = TTLCache("well-known") # type: TTLCache[bytes, Optional[bytes]] -_had_valid_well_known_cache = TTLCache( - "had-valid-well-known" -) # type: TTLCache[bytes, bool] +_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) @@ -130,9 +128,10 @@ async def get_well_known(self, server_name: bytes) -> WellKnownLookupResult: # requests for the same server in parallel? try: with Measure(self._clock, "get_well_known"): - result, cache_period = await self._fetch_well_known( - server_name - ) # type: Optional[bytes], float + result: Optional[bytes] + cache_period: float + + result, cache_period = await self._fetch_well_known(server_name) except _FetchWellKnownFailure as e: if prev_result and e.temporary: diff --git a/synapse/storage/background_updates.py b/synapse/storage/background_updates.py index 142787fdfd..82b31d24f1 100644 --- a/synapse/storage/background_updates.py +++ b/synapse/storage/background_updates.py @@ -92,14 +92,12 @@ def __init__(self, hs: "HomeServer", database: "DatabasePool"): self.db_pool = database # if a background update is currently running, its name. - self._current_background_update = None # type: Optional[str] - - self._background_update_performance = ( - {} - ) # type: Dict[str, BackgroundUpdatePerformance] - self._background_update_handlers = ( - {} - ) # type: Dict[str, Callable[[JsonDict, int], Awaitable[int]]] + self._current_background_update: Optional[str] = None + + self._background_update_performance: Dict[str, BackgroundUpdatePerformance] = {} + self._background_update_handlers: Dict[ + str, Callable[[JsonDict, int], Awaitable[int]] + ] = {} self._all_done = False def start_doing_background_updates(self) -> None: @@ -411,7 +409,7 @@ def create_index_sqlite(conn: Connection) -> None: c.execute(sql) if isinstance(self.db_pool.engine, engines.PostgresEngine): - runner = create_index_psql # type: Optional[Callable[[Connection], None]] + runner: Optional[Callable[[Connection], None]] = create_index_psql elif psql_only: runner = None else: diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 33c42cf95a..f80d822c12 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -670,8 +670,8 @@ async def runInteraction( Returns: The result of func """ - after_callbacks = [] # type: List[_CallbackListEntry] - exception_callbacks = [] # type: List[_CallbackListEntry] + after_callbacks: List[_CallbackListEntry] = [] + exception_callbacks: List[_CallbackListEntry] = [] if not current_context(): logger.warning("Starting db txn '%s' from sentinel context", desc) @@ -1090,7 +1090,7 @@ def _getwhere(key): return False # We didn't find any existing rows, so insert a new one - allvalues = {} # type: Dict[str, Any] + allvalues: Dict[str, Any] = {} allvalues.update(keyvalues) allvalues.update(values) allvalues.update(insertion_values) @@ -1121,7 +1121,7 @@ def simple_upsert_txn_native_upsert( values: The nonunique columns and their new values insertion_values: additional key/values to use only when inserting """ - allvalues = {} # type: Dict[str, Any] + allvalues: Dict[str, Any] = {} allvalues.update(keyvalues) allvalues.update(insertion_values or {}) @@ -1257,7 +1257,7 @@ def simple_upsert_many_txn_native_upsert( value_values: A list of each row's value column values. Ignored if value_names is empty. """ - allnames = [] # type: List[str] + allnames: List[str] = [] allnames.extend(key_names) allnames.extend(value_names) @@ -1566,7 +1566,7 @@ async def simple_select_many_batch( """ keyvalues = keyvalues or {} - results = [] # type: List[Dict[str, Any]] + results: List[Dict[str, Any]] = [] if not iterable: return results @@ -1978,7 +1978,7 @@ def simple_select_list_paginate_txn( raise ValueError("order_direction must be one of 'ASC' or 'DESC'.") where_clause = "WHERE " if filters or keyvalues or exclude_keyvalues else "" - arg_list = [] # type: List[Any] + arg_list: List[Any] = [] if filters: where_clause += " AND ".join("%s LIKE ?" % (k,) for k in filters) arg_list += list(filters.values()) diff --git a/synapse/storage/databases/main/appservice.py b/synapse/storage/databases/main/appservice.py index 9f182c2a89..e2d1b758bd 100644 --- a/synapse/storage/databases/main/appservice.py +++ b/synapse/storage/databases/main/appservice.py @@ -48,9 +48,7 @@ def _make_exclusive_regex( ] if exclusive_user_regexes: exclusive_user_regex = "|".join("(" + r + ")" for r in exclusive_user_regexes) - exclusive_user_pattern = re.compile( - exclusive_user_regex - ) # type: Optional[Pattern] + exclusive_user_pattern: Optional[Pattern] = re.compile(exclusive_user_regex) else: # We handle this case specially otherwise the constructed regex # will always match diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index 0e3dd4e9ca..78ae68ec68 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -247,7 +247,7 @@ def _get_e2e_device_keys_txn( txn.execute(sql, query_params) - result = {} # type: Dict[str, Dict[str, Optional[DeviceKeyLookupResult]]] + result: Dict[str, Dict[str, Optional[DeviceKeyLookupResult]]] = {} for (user_id, device_id, display_name, key_json) in txn: if include_deleted_devices: deleted_devices.remove((user_id, device_id)) diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index 4e06938849..d39368c20e 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -62,9 +62,9 @@ def __init__(self, database: DatabasePool, db_conn, hs): ) # Cache of event ID to list of auth event IDs and their depths. - self._event_auth_cache = LruCache( + self._event_auth_cache: LruCache[str, List[Tuple[str, int]]] = LruCache( 500000, "_event_auth_cache", size_callback=len - ) # type: LruCache[str, List[Tuple[str, int]]] + ) self._clock.looping_call(self._get_stats_for_federation_staging, 30 * 1000) @@ -137,10 +137,10 @@ def _get_auth_chain_ids_using_cover_index_txn( initial_events = set(event_ids) # All the events that we've found that are reachable from the events. - seen_events = set() # type: Set[str] + seen_events: Set[str] = set() # A map from chain ID to max sequence number of the given events. - event_chains = {} # type: Dict[int, int] + event_chains: Dict[int, int] = {} sql = """ SELECT event_id, chain_id, sequence_number @@ -182,7 +182,7 @@ def _get_auth_chain_ids_using_cover_index_txn( """ # A map from chain ID to max sequence number *reachable* from any event ID. - chains = {} # type: Dict[int, int] + chains: Dict[int, int] = {} # Add all linked chains reachable from initial set of chains. for batch in batch_iter(event_chains, 1000): @@ -353,14 +353,14 @@ def _get_auth_chain_difference_using_cover_index_txn( initial_events = set(state_sets[0]).union(*state_sets[1:]) # Map from event_id -> (chain ID, seq no) - chain_info = {} # type: Dict[str, Tuple[int, int]] + chain_info: Dict[str, Tuple[int, int]] = {} # Map from chain ID -> seq no -> event Id - chain_to_event = {} # type: Dict[int, Dict[int, str]] + chain_to_event: Dict[int, Dict[int, str]] = {} # All the chains that we've found that are reachable from the state # sets. - seen_chains = set() # type: Set[int] + seen_chains: Set[int] = set() sql = """ SELECT event_id, chain_id, sequence_number @@ -392,9 +392,9 @@ def _get_auth_chain_difference_using_cover_index_txn( # Corresponds to `state_sets`, except as a map from chain ID to max # sequence number reachable from the state set. - set_to_chain = [] # type: List[Dict[int, int]] + set_to_chain: List[Dict[int, int]] = [] for state_set in state_sets: - chains = {} # type: Dict[int, int] + chains: Dict[int, int] = {} set_to_chain.append(chains) for event_id in state_set: @@ -446,7 +446,7 @@ def _get_auth_chain_difference_using_cover_index_txn( # Mapping from chain ID to the range of sequence numbers that should be # pulled from the database. - chain_to_gap = {} # type: Dict[int, Tuple[int, int]] + chain_to_gap: Dict[int, Tuple[int, int]] = {} for chain_id in seen_chains: min_seq_no = min(chains.get(chain_id, 0) for chains in set_to_chain) @@ -555,7 +555,7 @@ def _get_auth_chain_difference_txn( } # The sorted list of events whose auth chains we should walk. - search = [] # type: List[Tuple[int, str]] + search: List[Tuple[int, str]] = [] # We need to get the depth of the initial events for sorting purposes. sql = """ @@ -578,7 +578,7 @@ def _get_auth_chain_difference_txn( search.sort() # Map from event to its auth events - event_to_auth_events = {} # type: Dict[str, Set[str]] + event_to_auth_events: Dict[str, Set[str]] = {} base_sql = """ SELECT a.event_id, auth_id, depth diff --git a/synapse/storage/databases/main/event_push_actions.py b/synapse/storage/databases/main/event_push_actions.py index d1237c65cc..55caa6bbe7 100644 --- a/synapse/storage/databases/main/event_push_actions.py +++ b/synapse/storage/databases/main/event_push_actions.py @@ -759,7 +759,7 @@ def _rotate_notifs_before_txn(self, txn, rotate_to_stream_ordering): # object because we might not have the same amount of rows in each of them. To do # this, we use a dict indexed on the user ID and room ID to make it easier to # populate. - summaries = {} # type: Dict[Tuple[str, str], _EventPushSummary] + summaries: Dict[Tuple[str, str], _EventPushSummary] = {} for row in txn: summaries[(row[0], row[1])] = _EventPushSummary( unread_count=row[2], diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index 08c580b0dc..ec8579b9ad 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -109,10 +109,8 @@ def __init__( # Ideally we'd move these ID gens here, unfortunately some other ID # generators are chained off them so doing so is a bit of a PITA. - self._backfill_id_gen = ( - self.store._backfill_id_gen - ) # type: MultiWriterIdGenerator - self._stream_id_gen = self.store._stream_id_gen # type: MultiWriterIdGenerator + self._backfill_id_gen: MultiWriterIdGenerator = self.store._backfill_id_gen + self._stream_id_gen: MultiWriterIdGenerator = self.store._stream_id_gen # This should only exist on instances that are configured to write assert ( @@ -221,7 +219,7 @@ async def _get_events_which_are_prevs(self, event_ids: Iterable[str]) -> List[st Returns: Filtered event ids """ - results = [] # type: List[str] + results: List[str] = [] def _get_events_which_are_prevs_txn(txn, batch): sql = """ @@ -508,7 +506,7 @@ def _add_chain_cover_index( """ # Map from event ID to chain ID/sequence number. - chain_map = {} # type: Dict[str, Tuple[int, int]] + chain_map: Dict[str, Tuple[int, int]] = {} # Set of event IDs to calculate chain ID/seq numbers for. events_to_calc_chain_id_for = set(event_to_room_id) @@ -817,8 +815,8 @@ def _allocate_chain_ids( # new chain if the sequence number has already been allocated. # - existing_chains = set() # type: Set[int] - tree = [] # type: List[Tuple[str, Optional[str]]] + existing_chains: Set[int] = set() + tree: List[Tuple[str, Optional[str]]] = [] # We need to do this in a topologically sorted order as we want to # generate chain IDs/sequence numbers of an event's auth events before @@ -848,7 +846,7 @@ def _allocate_chain_ids( ) txn.execute(sql % (clause,), args) - chain_to_max_seq_no = {row[0]: row[1] for row in txn} # type: Dict[Any, int] + chain_to_max_seq_no: Dict[Any, int] = {row[0]: row[1] for row in txn} # Allocate the new events chain ID/sequence numbers. # @@ -858,8 +856,8 @@ def _allocate_chain_ids( # number of new chain IDs in one call, replacing all temporary # objects with real allocated chain IDs. - unallocated_chain_ids = set() # type: Set[object] - new_chain_tuples = {} # type: Dict[str, Tuple[Any, int]] + unallocated_chain_ids: Set[object] = set() + new_chain_tuples: Dict[str, Tuple[Any, int]] = {} for event_id, auth_event_id in tree: # If we reference an auth_event_id we fetch the allocated chain ID, # either from the existing `chain_map` or the newly generated @@ -870,7 +868,7 @@ def _allocate_chain_ids( if not existing_chain_id: existing_chain_id = chain_map[auth_event_id] - new_chain_tuple = None # type: Optional[Tuple[Any, int]] + new_chain_tuple: Optional[Tuple[Any, int]] = None if existing_chain_id: # We found a chain ID/sequence number candidate, check its # not already taken. @@ -897,9 +895,9 @@ def _allocate_chain_ids( ) # Map from potentially temporary chain ID to real chain ID - chain_id_to_allocated_map = dict( + chain_id_to_allocated_map: Dict[Any, int] = dict( zip(unallocated_chain_ids, newly_allocated_chain_ids) - ) # type: Dict[Any, int] + ) chain_id_to_allocated_map.update((c, c) for c in existing_chains) return { @@ -1175,9 +1173,9 @@ def _filter_events_and_contexts_for_duplicates( Returns: list[(EventBase, EventContext)]: filtered list """ - new_events_and_contexts = ( - OrderedDict() - ) # type: OrderedDict[str, Tuple[EventBase, EventContext]] + new_events_and_contexts: OrderedDict[ + str, Tuple[EventBase, EventContext] + ] = OrderedDict() for event, context in events_and_contexts: prev_event_context = new_events_and_contexts.get(event.event_id) if prev_event_context: @@ -1205,7 +1203,7 @@ def _update_room_depths_txn( we are persisting backfilled (bool): True if the events were backfilled """ - depth_updates = {} # type: Dict[str, int] + depth_updates: Dict[str, int] = {} for event, context in events_and_contexts: # Remove the any existing cache entries for the event_ids txn.call_after(self.store._invalidate_get_event_cache, event.event_id) @@ -1885,7 +1883,7 @@ def _set_push_actions_for_event_and_users_txn( ), ) - room_to_event_ids = {} # type: Dict[str, List[str]] + room_to_event_ids: Dict[str, List[str]] = {} for e, _ in events_and_contexts: room_to_event_ids.setdefault(e.room_id, []).append(e.event_id) @@ -2012,7 +2010,7 @@ def _update_backward_extremeties(self, txn, events): Forward extremities are handled when we first start persisting the events. """ - events_by_room = {} # type: Dict[str, List[EventBase]] + events_by_room: Dict[str, List[EventBase]] = {} for ev in events: events_by_room.setdefault(ev.room_id, []).append(ev) diff --git a/synapse/storage/databases/main/events_bg_updates.py b/synapse/storage/databases/main/events_bg_updates.py index 29f33bac55..6fcb2b8353 100644 --- a/synapse/storage/databases/main/events_bg_updates.py +++ b/synapse/storage/databases/main/events_bg_updates.py @@ -960,9 +960,9 @@ def _calculate_chain_cover_txn( event_to_types = {row[0]: (row[1], row[2]) for row in rows} # Calculate the new last position we've processed up to. - new_last_depth = rows[-1][3] if rows else last_depth # type: int - new_last_stream = rows[-1][4] if rows else last_stream # type: int - new_last_room_id = rows[-1][5] if rows else "" # type: str + new_last_depth: int = rows[-1][3] if rows else last_depth + new_last_stream: int = rows[-1][4] if rows else last_stream + new_last_room_id: str = rows[-1][5] if rows else "" # Map from room_id to last depth/stream_ordering processed for the room, # excluding the last room (which we're likely still processing). We also @@ -989,7 +989,7 @@ def _calculate_chain_cover_txn( retcols=("event_id", "auth_id"), ) - event_to_auth_chain = {} # type: Dict[str, List[str]] + event_to_auth_chain: Dict[str, List[str]] = {} for row in auth_events: event_to_auth_chain.setdefault(row["event_id"], []).append(row["auth_id"]) diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index 403a5ddaba..3c86adab56 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -1365,10 +1365,10 @@ def get_deltas_for_stream_id_txn(txn, stream_id): # we need to make sure that, for every stream id in the results, we get *all* # the rows with that stream id. - rows = await self.db_pool.runInteraction( + rows: List[Tuple] = await self.db_pool.runInteraction( "get_all_updated_current_state_deltas", get_all_updated_current_state_deltas_txn, - ) # type: List[Tuple] + ) # if we've got fewer rows than the limit, we're good if len(rows) < target_row_count: @@ -1469,7 +1469,7 @@ async def get_already_persisted_events( """ mapping = {} - txn_id_to_event = {} # type: Dict[Tuple[str, int, str], str] + txn_id_to_event: Dict[Tuple[str, int, str], str] = {} for event in events: token_id = getattr(event.internal_metadata, "token_id", None) diff --git a/synapse/storage/databases/main/purge_events.py b/synapse/storage/databases/main/purge_events.py index eb4841830d..664c65dac5 100644 --- a/synapse/storage/databases/main/purge_events.py +++ b/synapse/storage/databases/main/purge_events.py @@ -115,7 +115,7 @@ def _purge_history_txn( logger.info("[purge] looking for events to delete") should_delete_expr = "state_key IS NULL" - should_delete_params = () # type: Tuple[Any, ...] + should_delete_params: Tuple[Any, ...] = () if not delete_local_events: should_delete_expr += " AND event_id NOT LIKE ?" diff --git a/synapse/storage/databases/main/push_rule.py b/synapse/storage/databases/main/push_rule.py index db52176337..a7fb8cd848 100644 --- a/synapse/storage/databases/main/push_rule.py +++ b/synapse/storage/databases/main/push_rule.py @@ -79,9 +79,9 @@ def __init__(self, database: DatabasePool, db_conn, hs): super().__init__(database, db_conn, hs) if hs.config.worker.worker_app is None: - self._push_rules_stream_id_gen = StreamIdGenerator( - db_conn, "push_rules_stream", "stream_id" - ) # type: Union[StreamIdGenerator, SlavedIdTracker] + self._push_rules_stream_id_gen: Union[ + StreamIdGenerator, SlavedIdTracker + ] = StreamIdGenerator(db_conn, "push_rules_stream", "stream_id") else: self._push_rules_stream_id_gen = SlavedIdTracker( db_conn, "push_rules_stream", "stream_id" diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index e31c5864ac..6ad1a0cf7f 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -1744,7 +1744,7 @@ def f(txn): items = keyvalues.items() where_clause = " AND ".join(k + " = ?" for k, _ in items) - values = [v for _, v in items] # type: List[Union[str, int]] + values: List[Union[str, int]] = [v for _, v in items] # Conveniently, refresh_tokens and access_tokens both use the user_id and device_id fields. Only caveat # is the `except_token_id` param that is tricky to get right, so for now we're just using the same where # clause and values before we handle that. This seems to be only used in the "set password" handler. diff --git a/synapse/storage/databases/main/stream.py b/synapse/storage/databases/main/stream.py index 7581c7d3ff..959f13de47 100644 --- a/synapse/storage/databases/main/stream.py +++ b/synapse/storage/databases/main/stream.py @@ -1085,9 +1085,7 @@ def _paginate_room_events_txn( # stream token (as returned by `RoomStreamToken.get_max_stream_pos`) and # then filtering the results. if from_token.topological is not None: - from_bound = ( - from_token.as_historical_tuple() - ) # type: Tuple[Optional[int], int] + from_bound: Tuple[Optional[int], int] = from_token.as_historical_tuple() elif direction == "b": from_bound = ( None, @@ -1099,7 +1097,7 @@ def _paginate_room_events_txn( from_token.stream, ) - to_bound = None # type: Optional[Tuple[Optional[int], int]] + to_bound: Optional[Tuple[Optional[int], int]] = None if to_token: if to_token.topological is not None: to_bound = to_token.as_historical_tuple() diff --git a/synapse/storage/databases/main/tags.py b/synapse/storage/databases/main/tags.py index 1d62c6140f..f93ff0a545 100644 --- a/synapse/storage/databases/main/tags.py +++ b/synapse/storage/databases/main/tags.py @@ -42,7 +42,7 @@ async def get_tags_for_user(self, user_id: str) -> Dict[str, Dict[str, JsonDict] "room_tags", {"user_id": user_id}, ["room_id", "tag", "content"] ) - tags_by_room = {} # type: Dict[str, Dict[str, JsonDict]] + tags_by_room: Dict[str, Dict[str, JsonDict]] = {} for row in rows: room_tags = tags_by_room.setdefault(row["room_id"], {}) room_tags[row["tag"]] = db_to_json(row["content"]) diff --git a/synapse/storage/databases/main/ui_auth.py b/synapse/storage/databases/main/ui_auth.py index 22c05cdde7..38bfdf5dad 100644 --- a/synapse/storage/databases/main/ui_auth.py +++ b/synapse/storage/databases/main/ui_auth.py @@ -224,12 +224,12 @@ def _set_ui_auth_session_data_txn( self, txn: LoggingTransaction, session_id: str, key: str, value: Any ): # Get the current value. - result = self.db_pool.simple_select_one_txn( + result: Dict[str, Any] = self.db_pool.simple_select_one_txn( # type: ignore txn, table="ui_auth_sessions", keyvalues={"session_id": session_id}, retcols=("serverdict",), - ) # type: Dict[str, Any] # type: ignore + ) # Update it and add it back to the database. serverdict = db_to_json(result["serverdict"]) diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index 051095fea9..a39877f0d5 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -307,7 +307,7 @@ async def persist_events( matched the transcation ID; the existing event is returned in such a case. """ - partitioned = {} # type: Dict[str, List[Tuple[EventBase, EventContext]]] + partitioned: Dict[str, List[Tuple[EventBase, EventContext]]] = {} for event, ctx in events_and_contexts: partitioned.setdefault(event.room_id, []).append((event, ctx)) @@ -384,7 +384,7 @@ async def _persist_event_batch( A dictionary of event ID to event ID we didn't persist as we already had another event persisted with the same TXN ID. """ - replaced_events = {} # type: Dict[str, str] + replaced_events: Dict[str, str] = {} if not events_and_contexts: return replaced_events @@ -440,16 +440,14 @@ async def _persist_event_batch( # Set of remote users which were in rooms the server has left. We # should check if we still share any rooms and if not we mark their # device lists as stale. - potentially_left_users = set() # type: Set[str] + potentially_left_users: Set[str] = set() if not backfilled: with Measure(self._clock, "_calculate_state_and_extrem"): # Work out the new "current state" for each room. # We do this by working out what the new extremities are and then # calculating the state from that. - events_by_room = ( - {} - ) # type: Dict[str, List[Tuple[EventBase, EventContext]]] + events_by_room: Dict[str, List[Tuple[EventBase, EventContext]]] = {} for event, context in chunk: events_by_room.setdefault(event.room_id, []).append( (event, context) @@ -622,9 +620,9 @@ async def _calculate_new_extremities( ) # Remove any events which are prev_events of any existing events. - existing_prevs = await self.persist_events_store._get_events_which_are_prevs( - result - ) # type: Collection[str] + existing_prevs: Collection[ + str + ] = await self.persist_events_store._get_events_which_are_prevs(result) result.difference_update(existing_prevs) # Finally handle the case where the new events have soft-failed prev diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 683e5e3b90..82a7686df0 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -256,7 +256,7 @@ def _setup_new_database( for database in databases ) - directory_entries = [] # type: List[_DirectoryListing] + directory_entries: List[_DirectoryListing] = [] for directory in directories: directory_entries.extend( _DirectoryListing(file_name, os.path.join(directory, file_name)) @@ -424,10 +424,10 @@ def _upgrade_existing_database( directories.append(os.path.join(schema_path, database, "delta", str(v))) # Used to check if we have any duplicate file names - file_name_counter = Counter() # type: CounterType[str] + file_name_counter: CounterType[str] = Counter() # Now find which directories have anything of interest. - directory_entries = [] # type: List[_DirectoryListing] + directory_entries: List[_DirectoryListing] = [] for directory in directories: logger.debug("Looking for schema deltas in %s", directory) try: diff --git a/synapse/storage/state.py b/synapse/storage/state.py index c9dce726cb..f8fbba9d38 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -91,7 +91,7 @@ def from_types(types: Iterable[Tuple[str, Optional[str]]]) -> "StateFilter": Returns: The new state filter. """ - type_dict = {} # type: Dict[str, Optional[Set[str]]] + type_dict: Dict[str, Optional[Set[str]]] = {} for typ, s in types: if typ in type_dict: if type_dict[typ] is None: @@ -194,7 +194,7 @@ def make_sql_filter_clause(self) -> Tuple[str, List[str]]: """ where_clause = "" - where_args = [] # type: List[str] + where_args: List[str] = [] if self.is_full(): return where_clause, where_args diff --git a/synapse/storage/util/id_generators.py b/synapse/storage/util/id_generators.py index f1e62f9e85..c768fdea56 100644 --- a/synapse/storage/util/id_generators.py +++ b/synapse/storage/util/id_generators.py @@ -112,7 +112,7 @@ def __init__( # insertion ordering will ensure its in the correct ordering. # # The key and values are the same, but we never look at the values. - self._unfinished_ids = OrderedDict() # type: OrderedDict[int, int] + self._unfinished_ids: OrderedDict[int, int] = OrderedDict() def get_next(self): """ @@ -236,15 +236,15 @@ def __init__( # Note: If we are a negative stream then we still store all the IDs as # positive to make life easier for us, and simply negate the IDs when we # return them. - self._current_positions = {} # type: Dict[str, int] + self._current_positions: Dict[str, int] = {} # Set of local IDs that we're still processing. The current position # should be less than the minimum of this set (if not empty). - self._unfinished_ids = set() # type: Set[int] + self._unfinished_ids: Set[int] = set() # Set of local IDs that we've processed that are larger than the current # position, due to there being smaller unpersisted IDs. - self._finished_ids = set() # type: Set[int] + self._finished_ids: Set[int] = set() # We track the max position where we know everything before has been # persisted. This is done by a) looking at the min across all instances @@ -265,7 +265,7 @@ def __init__( self._persisted_upto_position = ( min(self._current_positions.values()) if self._current_positions else 1 ) - self._known_persisted_positions = [] # type: List[int] + self._known_persisted_positions: List[int] = [] self._sequence_gen = PostgresSequenceGenerator(sequence_name) @@ -465,7 +465,7 @@ def _mark_id_as_finished(self, next_id: int): self._unfinished_ids.discard(next_id) self._finished_ids.add(next_id) - new_cur = None # type: Optional[int] + new_cur: Optional[int] = None if self._unfinished_ids: # If there are unfinished IDs then the new position will be the diff --git a/synapse/storage/util/sequence.py b/synapse/storage/util/sequence.py index 30b6b8e0ca..bb33e04fb1 100644 --- a/synapse/storage/util/sequence.py +++ b/synapse/storage/util/sequence.py @@ -208,10 +208,10 @@ def __init__(self, get_first_callback: GetFirstCallbackType): get_next_id_txn; should return the curreent maximum id """ # the callback. this is cleared after it is called, so that it can be GCed. - self._callback = get_first_callback # type: Optional[GetFirstCallbackType] + self._callback: Optional[GetFirstCallbackType] = get_first_callback # The current max value, or None if we haven't looked in the DB yet. - self._current_max_id = None # type: Optional[int] + self._current_max_id: Optional[int] = None self._lock = threading.Lock() def get_next_id_txn(self, txn: Cursor) -> int: @@ -274,7 +274,7 @@ def build_sequence_generator( `check_consistency` details. """ if isinstance(database_engine, PostgresEngine): - seq = PostgresSequenceGenerator(sequence_name) # type: SequenceGenerator + seq: SequenceGenerator = PostgresSequenceGenerator(sequence_name) else: seq = LocalSequenceGenerator(get_first_callback) diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 061102c3c8..014db1355b 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -257,7 +257,7 @@ def __init__( max_count: The maximum number of concurrent accesses """ if name is None: - self.name = id(self) # type: Union[str, int] + self.name: Union[str, int] = id(self) else: self.name = name @@ -269,7 +269,7 @@ def __init__( self.max_count = max_count # key_to_defer is a map from the key to a _LinearizerEntry. - self.key_to_defer = {} # type: Dict[Hashable, _LinearizerEntry] + self.key_to_defer: Dict[Hashable, _LinearizerEntry] = {} def is_queued(self, key: Hashable) -> bool: """Checks whether there is a process queued up waiting""" @@ -409,10 +409,10 @@ class ReadWriteLock: def __init__(self): # Latest readers queued - self.key_to_current_readers = {} # type: Dict[str, Set[defer.Deferred]] + self.key_to_current_readers: Dict[str, Set[defer.Deferred]] = {} # Latest writer queued - self.key_to_current_writer = {} # type: Dict[str, defer.Deferred] + self.key_to_current_writer: Dict[str, defer.Deferred] = {} async def read(self, key: str) -> ContextManager: new_defer = defer.Deferred() diff --git a/synapse/util/batching_queue.py b/synapse/util/batching_queue.py index 8fd5bfb69b..274cea7eb7 100644 --- a/synapse/util/batching_queue.py +++ b/synapse/util/batching_queue.py @@ -93,11 +93,11 @@ def __init__( self._clock = clock # The set of keys currently being processed. - self._processing_keys = set() # type: Set[Hashable] + self._processing_keys: Set[Hashable] = set() # The currently pending batch of values by key, with a Deferred to call # with the result of the corresponding `_process_batch_callback` call. - self._next_values = {} # type: Dict[Hashable, List[Tuple[V, defer.Deferred]]] + self._next_values: Dict[Hashable, List[Tuple[V, defer.Deferred]]] = {} # The function to call with batches of values. self._process_batch_callback = process_batch_callback @@ -108,9 +108,7 @@ def __init__( number_of_keys.labels(self._name).set_function(lambda: len(self._next_values)) - self._number_in_flight_metric = number_in_flight.labels( - self._name - ) # type: Gauge + self._number_in_flight_metric: Gauge = number_in_flight.labels(self._name) async def add_to_queue(self, value: V, key: Hashable = ()) -> R: """Adds the value to the queue with the given key, returning the result diff --git a/synapse/util/caches/__init__.py b/synapse/util/caches/__init__.py index ca36f07c20..9012034b7a 100644 --- a/synapse/util/caches/__init__.py +++ b/synapse/util/caches/__init__.py @@ -29,8 +29,8 @@ TRACK_MEMORY_USAGE = False -caches_by_name = {} # type: Dict[str, Sized] -collectors_by_name = {} # type: Dict[str, CacheMetric] +caches_by_name: Dict[str, Sized] = {} +collectors_by_name: Dict[str, "CacheMetric"] = {} cache_size = Gauge("synapse_util_caches_cache:size", "", ["name"]) cache_hits = Gauge("synapse_util_caches_cache:hits", "", ["name"]) diff --git a/synapse/util/caches/cached_call.py b/synapse/util/caches/cached_call.py index a301c9e89b..891bee0b33 100644 --- a/synapse/util/caches/cached_call.py +++ b/synapse/util/caches/cached_call.py @@ -63,9 +63,9 @@ def __init__(self, f: Callable[[], Awaitable[TV]]): f: The underlying function. Only one call to this function will be alive at once (per instance of CachedCall) """ - self._callable = f # type: Optional[Callable[[], Awaitable[TV]]] - self._deferred = None # type: Optional[Deferred] - self._result = None # type: Union[None, Failure, TV] + self._callable: Optional[Callable[[], Awaitable[TV]]] = f + self._deferred: Optional[Deferred] = None + self._result: Union[None, Failure, TV] = None async def get(self) -> TV: """Kick off the call if necessary, and return the result""" diff --git a/synapse/util/caches/deferred_cache.py b/synapse/util/caches/deferred_cache.py index 1044139119..8c6fafc677 100644 --- a/synapse/util/caches/deferred_cache.py +++ b/synapse/util/caches/deferred_cache.py @@ -80,25 +80,25 @@ def __init__( cache_type = TreeCache if tree else dict # _pending_deferred_cache maps from the key value to a `CacheEntry` object. - self._pending_deferred_cache = ( - cache_type() - ) # type: Union[TreeCache, MutableMapping[KT, CacheEntry]] + self._pending_deferred_cache: Union[ + TreeCache, "MutableMapping[KT, CacheEntry]" + ] = cache_type() def metrics_cb(): cache_pending_metric.labels(name).set(len(self._pending_deferred_cache)) # cache is used for completed results and maps to the result itself, rather than # a Deferred. - self.cache = LruCache( + self.cache: LruCache[KT, VT] = LruCache( max_size=max_entries, cache_name=name, cache_type=cache_type, size_callback=(lambda d: len(d) or 1) if iterable else None, metrics_collection_callback=metrics_cb, apply_cache_factor_from_config=apply_cache_factor_from_config, - ) # type: LruCache[KT, VT] + ) - self.thread = None # type: Optional[threading.Thread] + self.thread: Optional[threading.Thread] = None @property def max_entries(self): diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index d77e8edeea..1e8e6b1d01 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -46,17 +46,17 @@ class _CachedFunction(Generic[F]): - invalidate = None # type: Any - invalidate_all = None # type: Any - prefill = None # type: Any - cache = None # type: Any - num_args = None # type: Any + invalidate: Any = None + invalidate_all: Any = None + prefill: Any = None + cache: Any = None + num_args: Any = None - __name__ = None # type: str + __name__: str # Note: This function signature is actually fiddled with by the synapse mypy # plugin to a) make it a bound method, and b) remove any `cache_context` arg. - __call__ = None # type: F + __call__: F class _CacheDescriptorBase: @@ -115,8 +115,8 @@ def __init__(self, orig: Callable[..., Any], num_args, cache_context=False): class _LruCachedFunction(Generic[F]): - cache = None # type: LruCache[CacheKey, Any] - __call__ = None # type: F + cache: LruCache[CacheKey, Any] + __call__: F def lru_cache( @@ -180,10 +180,10 @@ def __init__( self.max_entries = max_entries def __get__(self, obj, owner): - cache = LruCache( + cache: LruCache[CacheKey, Any] = LruCache( cache_name=self.orig.__name__, max_size=self.max_entries, - ) # type: LruCache[CacheKey, Any] + ) get_cache_key = self.cache_key_builder sentinel = LruCacheDescriptor._Sentinel.sentinel @@ -271,12 +271,12 @@ def __init__( self.iterable = iterable def __get__(self, obj, owner): - cache = DeferredCache( + cache: DeferredCache[CacheKey, Any] = DeferredCache( name=self.orig.__name__, max_entries=self.max_entries, tree=self.tree, iterable=self.iterable, - ) # type: DeferredCache[CacheKey, Any] + ) get_cache_key = self.cache_key_builder @@ -359,7 +359,7 @@ def __init__(self, orig, cached_method_name, list_name, num_args=None): def __get__(self, obj, objtype=None): cached_method = getattr(obj, self.cached_method_name) - cache = cached_method.cache # type: DeferredCache[CacheKey, Any] + cache: DeferredCache[CacheKey, Any] = cached_method.cache num_args = cached_method.num_args @functools.wraps(self.orig) @@ -472,15 +472,15 @@ class _CacheContext: Cache = Union[DeferredCache, LruCache] - _cache_context_objects = ( - WeakValueDictionary() - ) # type: WeakValueDictionary[Tuple[_CacheContext.Cache, CacheKey], _CacheContext] + _cache_context_objects: """WeakValueDictionary[ + Tuple["_CacheContext.Cache", CacheKey], "_CacheContext" + ]""" = WeakValueDictionary() def __init__(self, cache: "_CacheContext.Cache", cache_key: CacheKey) -> None: self._cache = cache self._cache_key = cache_key - def invalidate(self): # type: () -> None + def invalidate(self) -> None: """Invalidates the cache entry referred to by the context.""" self._cache.invalidate(self._cache_key) diff --git a/synapse/util/caches/dictionary_cache.py b/synapse/util/caches/dictionary_cache.py index 56d94d96ce..3f852edd7f 100644 --- a/synapse/util/caches/dictionary_cache.py +++ b/synapse/util/caches/dictionary_cache.py @@ -62,13 +62,13 @@ class DictionaryCache(Generic[KT, DKT]): """ def __init__(self, name: str, max_entries: int = 1000): - self.cache = LruCache( + self.cache: LruCache[KT, DictionaryEntry] = LruCache( max_size=max_entries, cache_name=name, size_callback=len - ) # type: LruCache[KT, DictionaryEntry] + ) self.name = name self.sequence = 0 - self.thread = None # type: Optional[threading.Thread] + self.thread: Optional[threading.Thread] = None def check_thread(self) -> None: expected_thread = self.thread diff --git a/synapse/util/caches/expiringcache.py b/synapse/util/caches/expiringcache.py index ac47a31cd7..bde16b8577 100644 --- a/synapse/util/caches/expiringcache.py +++ b/synapse/util/caches/expiringcache.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) -SENTINEL = object() # type: Any +SENTINEL: Any = object() T = TypeVar("T") @@ -71,7 +71,7 @@ def __init__( self._expiry_ms = expiry_ms self._reset_expiry_on_get = reset_expiry_on_get - self._cache = OrderedDict() # type: OrderedDict[KT, _CacheEntry] + self._cache: OrderedDict[KT, _CacheEntry] = OrderedDict() self.iterable = iterable diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index 4b9d0433ff..efeba0cb96 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -226,7 +226,7 @@ def __init__( # footprint down. Storing `None` is free as its a singleton, while empty # lists are 56 bytes (and empty sets are 216 bytes, if we did the naive # thing and used sets). - self.callbacks = None # type: Optional[List[Callable[[], None]]] + self.callbacks: Optional[List[Callable[[], None]]] = None self.add_callbacks(callbacks) @@ -362,15 +362,15 @@ def __init__( # register_cache might call our "set_cache_factor" callback; there's nothing to # do yet when we get resized. - self._on_resize = None # type: Optional[Callable[[],None]] + self._on_resize: Optional[Callable[[], None]] = None if cache_name is not None: - metrics = register_cache( + metrics: Optional[CacheMetric] = register_cache( "lru_cache", cache_name, self, collect_callback=metrics_collection_callback, - ) # type: Optional[CacheMetric] + ) else: metrics = None diff --git a/synapse/util/caches/response_cache.py b/synapse/util/caches/response_cache.py index 34c662c4db..ed7204336f 100644 --- a/synapse/util/caches/response_cache.py +++ b/synapse/util/caches/response_cache.py @@ -66,7 +66,7 @@ def __init__(self, clock: Clock, name: str, timeout_ms: float = 0): # This is poorly-named: it includes both complete and incomplete results. # We keep complete results rather than switching to absolute values because # that makes it easier to cache Failure results. - self.pending_result_cache = {} # type: Dict[KV, ObservableDeferred] + self.pending_result_cache: Dict[KV, ObservableDeferred] = {} self.clock = clock self.timeout_sec = timeout_ms / 1000.0 diff --git a/synapse/util/caches/stream_change_cache.py b/synapse/util/caches/stream_change_cache.py index e81e468899..3a41a8baa6 100644 --- a/synapse/util/caches/stream_change_cache.py +++ b/synapse/util/caches/stream_change_cache.py @@ -45,10 +45,10 @@ def __init__( ): self._original_max_size = max_size self._max_size = math.floor(max_size) - self._entity_to_key = {} # type: Dict[EntityType, int] + self._entity_to_key: Dict[EntityType, int] = {} # map from stream id to the a set of entities which changed at that stream id. - self._cache = SortedDict() # type: SortedDict[int, Set[EntityType]] + self._cache: SortedDict[int, Set[EntityType]] = SortedDict() # the earliest stream_pos for which we can reliably answer # get_all_entities_changed. In other words, one less than the earliest @@ -155,7 +155,7 @@ def get_all_entities_changed(self, stream_pos: int) -> Optional[List[EntityType] if stream_pos < self._earliest_known_stream_pos: return None - changed_entities = [] # type: List[EntityType] + changed_entities: List[EntityType] = [] for k in self._cache.islice(start=self._cache.bisect_right(stream_pos)): changed_entities.extend(self._cache[k]) diff --git a/synapse/util/caches/ttlcache.py b/synapse/util/caches/ttlcache.py index c276107d56..46afe3f934 100644 --- a/synapse/util/caches/ttlcache.py +++ b/synapse/util/caches/ttlcache.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) -SENTINEL = object() # type: Any +SENTINEL: Any = object() T = TypeVar("T") KT = TypeVar("KT") @@ -35,10 +35,10 @@ class TTLCache(Generic[KT, VT]): def __init__(self, cache_name: str, timer: Callable[[], float] = time.time): # map from key to _CacheEntry - self._data = {} # type: Dict[KT, _CacheEntry] + self._data: Dict[KT, _CacheEntry] = {} # the _CacheEntries, sorted by expiry time - self._expiry_list = SortedList() # type: SortedList[_CacheEntry] + self._expiry_list: SortedList[_CacheEntry] = SortedList() self._timer = timer diff --git a/synapse/util/iterutils.py b/synapse/util/iterutils.py index 886afa9d19..8ac3eab2f5 100644 --- a/synapse/util/iterutils.py +++ b/synapse/util/iterutils.py @@ -68,7 +68,7 @@ def sorted_topologically( # This is implemented by Kahn's algorithm. degree_map = {node: 0 for node in nodes} - reverse_graph = {} # type: Dict[T, Set[T]] + reverse_graph: Dict[T, Set[T]] = {} for node, edges in graph.items(): if node not in degree_map: diff --git a/synapse/util/macaroons.py b/synapse/util/macaroons.py index f6ebfd7e7d..d1f76e3dc5 100644 --- a/synapse/util/macaroons.py +++ b/synapse/util/macaroons.py @@ -39,7 +39,7 @@ def get_value_from_macaroon(macaroon: pymacaroons.Macaroon, key: str) -> str: caveat in the macaroon, or if the caveat was not found in the macaroon. """ prefix = key + " = " - result = None # type: Optional[str] + result: Optional[str] = None for caveat in macaroon.caveats: if not caveat.caveat_id.startswith(prefix): continue diff --git a/synapse/util/metrics.py b/synapse/util/metrics.py index 45353d41c5..1b82dca81b 100644 --- a/synapse/util/metrics.py +++ b/synapse/util/metrics.py @@ -124,7 +124,7 @@ def __init__(self, clock, name: str): assert isinstance(curr_context, LoggingContext) parent_context = curr_context self._logging_context = LoggingContext(str(curr_context), parent_context) - self.start = None # type: Optional[int] + self.start: Optional[int] = None def __enter__(self) -> "Measure": if self.start is not None: diff --git a/synapse/util/patch_inline_callbacks.py b/synapse/util/patch_inline_callbacks.py index eed0291cae..99f01e325c 100644 --- a/synapse/util/patch_inline_callbacks.py +++ b/synapse/util/patch_inline_callbacks.py @@ -41,7 +41,7 @@ def new_inline_callbacks(f): @functools.wraps(f) def wrapped(*args, **kwargs): start_context = current_context() - changes = [] # type: List[str] + changes: List[str] = [] orig = orig_inline_callbacks(_check_yield_points(f, changes)) try: @@ -131,7 +131,7 @@ def check_yield_points_inner(*args, **kwargs): gen = f(*args, **kwargs) last_yield_line_no = gen.gi_frame.f_lineno - result = None # type: Any + result: Any = None while True: expected_context = current_context() From d427f64724569d606add3c1e6f3008bdd82c092d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 16 Jul 2021 10:36:38 -0400 Subject: [PATCH 420/619] Do not include signatures/hashes in make_{join,leave,knock} responses. (#10404) These signatures would end up invalid since the joining/leaving/knocking server would modify the response before calling send_{join,leave,knock}. --- changelog.d/10404.bugfix | 1 + synapse/events/__init__.py | 14 ++++++++++++++ synapse/federation/federation_server.py | 9 +++------ 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10404.bugfix diff --git a/changelog.d/10404.bugfix b/changelog.d/10404.bugfix new file mode 100644 index 0000000000..2e095b6402 --- /dev/null +++ b/changelog.d/10404.bugfix @@ -0,0 +1 @@ +Responses from `/make_{join,leave,knock}` no longer include signatures, which will turn out to be invalid after events are returned to `/send_{join,leave,knock}`. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 65dc7a4ed0..0298af4c02 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -291,6 +291,20 @@ def get_pdu_json(self, time_now=None) -> JsonDict: return pdu_json + def get_templated_pdu_json(self) -> JsonDict: + """ + Return a JSON object suitable for a templated event, as used in the + make_{join,leave,knock} workflow. + """ + # By using _dict directly we don't pull in signatures/unsigned. + template_json = dict(self._dict) + # The hashes (similar to the signature) need to be recalculated by the + # joining/leaving/knocking server after (potentially) modifying the + # event. + template_json.pop("hashes") + + return template_json + def __set__(self, instance, value): raise AttributeError("Unrecognized attribute %s" % (instance,)) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d91f0ff32f..29619aeeb8 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -562,8 +562,7 @@ async def on_make_join_request( raise IncompatibleRoomVersionError(room_version=room_version) pdu = await self.handler.on_make_join_request(origin, room_id, user_id) - time_now = self._clock.time_msec() - return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} + return {"event": pdu.get_templated_pdu_json(), "room_version": room_version} async def on_invite_request( self, origin: str, content: JsonDict, room_version_id: str @@ -611,8 +610,7 @@ async def on_make_leave_request( room_version = await self.store.get_room_version_id(room_id) - time_now = self._clock.time_msec() - return {"event": pdu.get_pdu_json(time_now), "room_version": room_version} + return {"event": pdu.get_templated_pdu_json(), "room_version": room_version} async def on_send_leave_request( self, origin: str, content: JsonDict, room_id: str @@ -659,9 +657,8 @@ async def on_make_knock_request( ) pdu = await self.handler.on_make_knock_request(origin, room_id, user_id) - time_now = self._clock.time_msec() return { - "event": pdu.get_pdu_json(time_now), + "event": pdu.get_templated_pdu_json(), "room_version": room_version.identifier, } From 36dc15412de9fc1bb2ba955c8b6f2da20d2ca20f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 16 Jul 2021 18:11:53 +0200 Subject: [PATCH 421/619] Add a module type for account validity (#9884) This adds an API for third-party plugin modules to implement account validity, so they can provide this feature instead of Synapse. The module implementing the current behaviour for this feature can be found at https://github.com/matrix-org/synapse-email-account-validity. To allow for a smooth transition between the current feature and the new module, hooks have been added to the existing account validity endpoints to allow their behaviours to be overridden by a module. --- changelog.d/9884.feature | 1 + docs/modules.md | 47 +++- docs/sample_config.yaml | 85 ------- synapse/api/auth.py | 17 +- synapse/config/account_validity.py | 102 ++------ synapse/handlers/account_validity.py | 128 +++++++++- synapse/handlers/register.py | 5 + synapse/module_api/__init__.py | 219 +++++++++++++++++- synapse/module_api/errors.py | 6 +- synapse/push/pusherpool.py | 24 +- synapse/rest/admin/users.py | 24 +- .../rest/client/v2_alpha/account_validity.py | 7 +- tests/test_state.py | 1 + 13 files changed, 438 insertions(+), 228 deletions(-) create mode 100644 changelog.d/9884.feature diff --git a/changelog.d/9884.feature b/changelog.d/9884.feature new file mode 100644 index 0000000000..525fd2f93c --- /dev/null +++ b/changelog.d/9884.feature @@ -0,0 +1 @@ +Add a module type for the account validity feature. diff --git a/docs/modules.md b/docs/modules.md index bec1c06d15..c4cb7018f7 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -63,7 +63,7 @@ Modules can register web resources onto Synapse's web server using the following API method: ```python -def ModuleApi.register_web_resource(path: str, resource: IResource) +def ModuleApi.register_web_resource(path: str, resource: IResource) -> None ``` The path is the full absolute path to register the resource at. For example, if you @@ -91,12 +91,17 @@ are split in categories. A single module may implement callbacks from multiple c and is under no obligation to implement all callbacks from the categories it registers callbacks for. +Modules can register callbacks using one of the module API's `register_[...]_callbacks` +methods. The callback functions are passed to these methods as keyword arguments, with +the callback name as the argument name and the function as its value. This is demonstrated +in the example below. A `register_[...]_callbacks` method exists for each module type +documented in this section. + #### Spam checker callbacks -To register one of the callbacks described in this section, a module needs to use the -module API's `register_spam_checker_callbacks` method. The callback functions are passed -to `register_spam_checker_callbacks` as keyword arguments, with the callback name as the -argument name and the function as its value. This is demonstrated in the example below. +Spam checker callbacks allow module developers to implement spam mitigation actions for +Synapse instances. Spam checker callbacks can be registered using the module API's +`register_spam_checker_callbacks` method. The available spam checker callbacks are: @@ -115,7 +120,7 @@ async def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool Called when processing an invitation. The module must return a `bool` indicating whether the inviter can invite the invitee to the given room. Both inviter and invitee are -represented by their Matrix user ID (i.e. `@alice:example.com`). +represented by their Matrix user ID (e.g. `@alice:example.com`). ```python async def user_may_create_room(user: str) -> bool @@ -188,6 +193,36 @@ async def check_media_file_for_spam( Called when storing a local or remote file. The module must return a boolean indicating whether the given file can be stored in the homeserver's media store. +#### Account validity callbacks + +Account validity callbacks allow module developers to add extra steps to verify the +validity on an account, i.e. see if a user can be granted access to their account on the +Synapse instance. Account validity callbacks can be registered using the module API's +`register_account_validity_callbacks` method. + +The available account validity callbacks are: + +```python +async def is_user_expired(user: str) -> Optional[bool] +``` + +Called when processing any authenticated request (except for logout requests). The module +can return a `bool` to indicate whether the user has expired and should be locked out of +their account, or `None` if the module wasn't able to figure it out. The user is +represented by their Matrix user ID (e.g. `@alice:example.com`). + +If the module returns `True`, the current request will be denied with the error code +`ORG_MATRIX_EXPIRED_ACCOUNT` and the HTTP status code 403. Note that this doesn't +invalidate the user's access token. + +```python +async def on_user_registration(user: str) -> None +``` + +Called after successfully registering a user, in case the module needs to perform extra +operations to keep track of them. (e.g. add them to a database table). The user is +represented by their Matrix user ID. + ### Porting an existing module that uses the old interface In order to port a module that uses Synapse's old module interface, its author needs to: diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index a45732a246..f4845a5841 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1310,91 +1310,6 @@ account_threepid_delegates: #auto_join_rooms_for_guests: false -## Account Validity ## - -# Optional account validity configuration. This allows for accounts to be denied -# any request after a given period. -# -# Once this feature is enabled, Synapse will look for registered users without an -# expiration date at startup and will add one to every account it found using the -# current settings at that time. -# This means that, if a validity period is set, and Synapse is restarted (it will -# then derive an expiration date from the current validity period), and some time -# after that the validity period changes and Synapse is restarted, the users' -# expiration dates won't be updated unless their account is manually renewed. This -# date will be randomly selected within a range [now + period - d ; now + period], -# where d is equal to 10% of the validity period. -# -account_validity: - # The account validity feature is disabled by default. Uncomment the - # following line to enable it. - # - #enabled: true - - # The period after which an account is valid after its registration. When - # renewing the account, its validity period will be extended by this amount - # of time. This parameter is required when using the account validity - # feature. - # - #period: 6w - - # The amount of time before an account's expiry date at which Synapse will - # send an email to the account's email address with a renewal link. By - # default, no such emails are sent. - # - # If you enable this setting, you will also need to fill out the 'email' and - # 'public_baseurl' configuration sections. - # - #renew_at: 1w - - # The subject of the email sent out with the renewal link. '%(app)s' can be - # used as a placeholder for the 'app_name' parameter from the 'email' - # section. - # - # Note that the placeholder must be written '%(app)s', including the - # trailing 's'. - # - # If this is not set, a default value is used. - # - #renew_email_subject: "Renew your %(app)s account" - - # Directory in which Synapse will try to find templates for the HTML files to - # serve to the user when trying to renew an account. If not set, default - # templates from within the Synapse package will be used. - # - # The currently available templates are: - # - # * account_renewed.html: Displayed to the user after they have successfully - # renewed their account. - # - # * account_previously_renewed.html: Displayed to the user if they attempt to - # renew their account with a token that is valid, but that has already - # been used. In this case the account is not renewed again. - # - # * invalid_token.html: Displayed to the user when they try to renew an account - # with an unknown or invalid renewal token. - # - # See https://github.com/matrix-org/synapse/tree/master/synapse/res/templates for - # default template contents. - # - # The file name of some of these templates can be configured below for legacy - # reasons. - # - #template_dir: "res/templates" - - # A custom file name for the 'account_renewed.html' template. - # - # If not set, the file is assumed to be named "account_renewed.html". - # - #account_renewed_html_path: "account_renewed.html" - - # A custom file name for the 'invalid_token.html' template. - # - # If not set, the file is assumed to be named "invalid_token.html". - # - #invalid_token_html_path: "invalid_token.html" - - ## Metrics ### # Enable collection and rendering of performance metrics diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 8916e6fa2f..05699714ee 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -62,6 +62,7 @@ def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.store = hs.get_datastore() self.state = hs.get_state_handler() + self._account_validity_handler = hs.get_account_validity_handler() self.token_cache: LruCache[str, Tuple[str, bool]] = LruCache( 10000, "token_cache" @@ -69,9 +70,6 @@ def __init__(self, hs: "HomeServer"): self._auth_blocking = AuthBlocking(self.hs) - self._account_validity_enabled = ( - hs.config.account_validity.account_validity_enabled - ) self._track_appservice_user_ips = hs.config.track_appservice_user_ips self._macaroon_secret_key = hs.config.macaroon_secret_key self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users @@ -187,12 +185,17 @@ async def get_user_by_req( shadow_banned = user_info.shadow_banned # Deny the request if the user account has expired. - if self._account_validity_enabled and not allow_expired: - if await self.store.is_account_expired( - user_info.user_id, self.clock.time_msec() + if not allow_expired: + if await self._account_validity_handler.is_user_expired( + user_info.user_id ): + # Raise the error if either an account validity module has determined + # the account has expired, or the legacy account validity + # implementation is enabled and determined the account has expired raise AuthError( - 403, "User account has expired", errcode=Codes.EXPIRED_ACCOUNT + 403, + "User account has expired", + errcode=Codes.EXPIRED_ACCOUNT, ) device_id = user_info.device_id diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index 957de7f3a6..6be4eafe55 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -18,6 +18,21 @@ class AccountValidityConfig(Config): section = "account_validity" def read_config(self, config, **kwargs): + """Parses the old account validity config. The config format looks like this: + + account_validity: + enabled: true + period: 6w + renew_at: 1w + renew_email_subject: "Renew your %(app)s account" + template_dir: "res/templates" + account_renewed_html_path: "account_renewed.html" + invalid_token_html_path: "invalid_token.html" + + We expect admins to use modules for this feature (which is why it doesn't appear + in the sample config file), but we want to keep support for it around for a bit + for backwards compatibility. + """ account_validity_config = config.get("account_validity") or {} self.account_validity_enabled = account_validity_config.get("enabled", False) self.account_validity_renew_by_email_enabled = ( @@ -75,90 +90,3 @@ def read_config(self, config, **kwargs): ], account_validity_template_dir, ) - - def generate_config_section(self, **kwargs): - return """\ - ## Account Validity ## - - # Optional account validity configuration. This allows for accounts to be denied - # any request after a given period. - # - # Once this feature is enabled, Synapse will look for registered users without an - # expiration date at startup and will add one to every account it found using the - # current settings at that time. - # This means that, if a validity period is set, and Synapse is restarted (it will - # then derive an expiration date from the current validity period), and some time - # after that the validity period changes and Synapse is restarted, the users' - # expiration dates won't be updated unless their account is manually renewed. This - # date will be randomly selected within a range [now + period - d ; now + period], - # where d is equal to 10% of the validity period. - # - account_validity: - # The account validity feature is disabled by default. Uncomment the - # following line to enable it. - # - #enabled: true - - # The period after which an account is valid after its registration. When - # renewing the account, its validity period will be extended by this amount - # of time. This parameter is required when using the account validity - # feature. - # - #period: 6w - - # The amount of time before an account's expiry date at which Synapse will - # send an email to the account's email address with a renewal link. By - # default, no such emails are sent. - # - # If you enable this setting, you will also need to fill out the 'email' and - # 'public_baseurl' configuration sections. - # - #renew_at: 1w - - # The subject of the email sent out with the renewal link. '%(app)s' can be - # used as a placeholder for the 'app_name' parameter from the 'email' - # section. - # - # Note that the placeholder must be written '%(app)s', including the - # trailing 's'. - # - # If this is not set, a default value is used. - # - #renew_email_subject: "Renew your %(app)s account" - - # Directory in which Synapse will try to find templates for the HTML files to - # serve to the user when trying to renew an account. If not set, default - # templates from within the Synapse package will be used. - # - # The currently available templates are: - # - # * account_renewed.html: Displayed to the user after they have successfully - # renewed their account. - # - # * account_previously_renewed.html: Displayed to the user if they attempt to - # renew their account with a token that is valid, but that has already - # been used. In this case the account is not renewed again. - # - # * invalid_token.html: Displayed to the user when they try to renew an account - # with an unknown or invalid renewal token. - # - # See https://github.com/matrix-org/synapse/tree/master/synapse/res/templates for - # default template contents. - # - # The file name of some of these templates can be configured below for legacy - # reasons. - # - #template_dir: "res/templates" - - # A custom file name for the 'account_renewed.html' template. - # - # If not set, the file is assumed to be named "account_renewed.html". - # - #account_renewed_html_path: "account_renewed.html" - - # A custom file name for the 'invalid_token.html' template. - # - # If not set, the file is assumed to be named "invalid_token.html". - # - #invalid_token_html_path: "invalid_token.html" - """ diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index d752cf34f0..078accd634 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -15,9 +15,11 @@ import email.mime.multipart import email.utils import logging -from typing import TYPE_CHECKING, List, Optional, Tuple +from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Tuple -from synapse.api.errors import StoreError, SynapseError +from twisted.web.http import Request + +from synapse.api.errors import AuthError, StoreError, SynapseError from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.types import UserID from synapse.util import stringutils @@ -27,6 +29,15 @@ logger = logging.getLogger(__name__) +# Types for callbacks to be registered via the module api +IS_USER_EXPIRED_CALLBACK = Callable[[str], Awaitable[Optional[bool]]] +ON_USER_REGISTRATION_CALLBACK = Callable[[str], Awaitable] +# Temporary hooks to allow for a transition from `/_matrix/client` endpoints +# to `/_synapse/client/account_validity`. See `register_account_validity_callbacks`. +ON_LEGACY_SEND_MAIL_CALLBACK = Callable[[str], Awaitable] +ON_LEGACY_RENEW_CALLBACK = Callable[[str], Awaitable[Tuple[bool, bool, int]]] +ON_LEGACY_ADMIN_REQUEST = Callable[[Request], Awaitable] + class AccountValidityHandler: def __init__(self, hs: "HomeServer"): @@ -70,6 +81,99 @@ def __init__(self, hs: "HomeServer"): if hs.config.run_background_tasks: self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000) + self._is_user_expired_callbacks: List[IS_USER_EXPIRED_CALLBACK] = [] + self._on_user_registration_callbacks: List[ON_USER_REGISTRATION_CALLBACK] = [] + self._on_legacy_send_mail_callback: Optional[ + ON_LEGACY_SEND_MAIL_CALLBACK + ] = None + self._on_legacy_renew_callback: Optional[ON_LEGACY_RENEW_CALLBACK] = None + + # The legacy admin requests callback isn't a protected attribute because we need + # to access it from the admin servlet, which is outside of this handler. + self.on_legacy_admin_request_callback: Optional[ON_LEGACY_ADMIN_REQUEST] = None + + def register_account_validity_callbacks( + self, + is_user_expired: Optional[IS_USER_EXPIRED_CALLBACK] = None, + on_user_registration: Optional[ON_USER_REGISTRATION_CALLBACK] = None, + on_legacy_send_mail: Optional[ON_LEGACY_SEND_MAIL_CALLBACK] = None, + on_legacy_renew: Optional[ON_LEGACY_RENEW_CALLBACK] = None, + on_legacy_admin_request: Optional[ON_LEGACY_ADMIN_REQUEST] = None, + ): + """Register callbacks from module for each hook.""" + if is_user_expired is not None: + self._is_user_expired_callbacks.append(is_user_expired) + + if on_user_registration is not None: + self._on_user_registration_callbacks.append(on_user_registration) + + # The builtin account validity feature exposes 3 endpoints (send_mail, renew, and + # an admin one). As part of moving the feature into a module, we need to change + # the path from /_matrix/client/unstable/account_validity/... to + # /_synapse/client/account_validity, because: + # + # * the feature isn't part of the Matrix spec thus shouldn't live under /_matrix + # * the way we register servlets means that modules can't register resources + # under /_matrix/client + # + # We need to allow for a transition period between the old and new endpoints + # in order to allow for clients to update (and for emails to be processed). + # + # Once the email-account-validity module is loaded, it will take control of account + # validity by moving the rows from our `account_validity` table into its own table. + # + # Therefore, we need to allow modules (in practice just the one implementing the + # email-based account validity) to temporarily hook into the legacy endpoints so we + # can route the traffic coming into the old endpoints into the module, which is + # why we have the following three temporary hooks. + if on_legacy_send_mail is not None: + if self._on_legacy_send_mail_callback is not None: + raise RuntimeError("Tried to register on_legacy_send_mail twice") + + self._on_legacy_send_mail_callback = on_legacy_send_mail + + if on_legacy_renew is not None: + if self._on_legacy_renew_callback is not None: + raise RuntimeError("Tried to register on_legacy_renew twice") + + self._on_legacy_renew_callback = on_legacy_renew + + if on_legacy_admin_request is not None: + if self.on_legacy_admin_request_callback is not None: + raise RuntimeError("Tried to register on_legacy_admin_request twice") + + self.on_legacy_admin_request_callback = on_legacy_admin_request + + async def is_user_expired(self, user_id: str) -> bool: + """Checks if a user has expired against third-party modules. + + Args: + user_id: The user to check the expiry of. + + Returns: + Whether the user has expired. + """ + for callback in self._is_user_expired_callbacks: + expired = await callback(user_id) + if expired is not None: + return expired + + if self._account_validity_enabled: + # If no module could determine whether the user has expired and the legacy + # configuration is enabled, fall back to it. + return await self.store.is_account_expired(user_id, self.clock.time_msec()) + + return False + + async def on_user_registration(self, user_id: str): + """Tell third-party modules about a user's registration. + + Args: + user_id: The ID of the newly registered user. + """ + for callback in self._on_user_registration_callbacks: + await callback(user_id) + @wrap_as_background_process("send_renewals") async def _send_renewal_emails(self) -> None: """Gets the list of users whose account is expiring in the amount of time @@ -95,6 +199,17 @@ async def send_renewal_email_to_user(self, user_id: str) -> None: Raises: SynapseError if the user is not set to renew. """ + # If a module supports sending a renewal email from here, do that, otherwise do + # the legacy dance. + if self._on_legacy_send_mail_callback is not None: + await self._on_legacy_send_mail_callback(user_id) + return + + if not self._account_validity_renew_by_email_enabled: + raise AuthError( + 403, "Account renewal via email is disabled on this server." + ) + expiration_ts = await self.store.get_expiration_ts_for_user(user_id) # If this user isn't set to be expired, raise an error. @@ -209,6 +324,10 @@ async def renew_account(self, renewal_token: str) -> Tuple[bool, bool, int]: token is considered stale. A token is stale if the 'token_used_ts_ms' db column is non-null. + This method exists to support handling the legacy account validity /renew + endpoint. If a module implements the on_legacy_renew callback, then this process + is delegated to the module instead. + Args: renewal_token: Token sent with the renewal request. Returns: @@ -218,6 +337,11 @@ async def renew_account(self, renewal_token: str) -> Tuple[bool, bool, int]: * An int representing the user's expiry timestamp as milliseconds since the epoch, or 0 if the token was invalid. """ + # If a module supports triggering a renew from here, do that, otherwise do the + # legacy dance. + if self._on_legacy_renew_callback is not None: + return await self._on_legacy_renew_callback(renewal_token) + try: ( user_id, diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 26ef016179..056fe5e89f 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -77,6 +77,7 @@ def __init__(self, hs: "HomeServer"): self.identity_handler = self.hs.get_identity_handler() self.ratelimiter = hs.get_registration_ratelimiter() self.macaroon_gen = hs.get_macaroon_generator() + self._account_validity_handler = hs.get_account_validity_handler() self._server_notices_mxid = hs.config.server_notices_mxid self._server_name = hs.hostname @@ -700,6 +701,10 @@ async def register_with_store( shadow_banned=shadow_banned, ) + # Only call the account validity module(s) on the main process, to avoid + # repeating e.g. database writes on all of the workers. + await self._account_validity_handler.on_user_registration(user_id) + async def register_device( self, user_id: str, diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 308f045700..f3c78089b7 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -12,18 +12,42 @@ # 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 email.utils import logging -from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, Tuple +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Generator, + Iterable, + List, + Optional, + Tuple, +) + +import jinja2 from twisted.internet import defer from twisted.web.resource import IResource from synapse.events import EventBase from synapse.http.client import SimpleHttpClient +from synapse.http.server import ( + DirectServeHtmlResource, + DirectServeJsonResource, + respond_with_html, +) +from synapse.http.servlet import parse_json_object_from_request from synapse.http.site import SynapseRequest from synapse.logging.context import make_deferred_yieldable, run_in_background +from synapse.metrics.background_process_metrics import run_as_background_process +from synapse.storage.database import DatabasePool, LoggingTransaction +from synapse.storage.databases.main.roommember import ProfileInfo from synapse.storage.state import StateFilter -from synapse.types import JsonDict, UserID, create_requester +from synapse.types import JsonDict, Requester, UserID, create_requester +from synapse.util import Clock +from synapse.util.caches.descriptors import cached if TYPE_CHECKING: from synapse.server import HomeServer @@ -33,7 +57,20 @@ are loaded into Synapse. """ -__all__ = ["errors", "make_deferred_yieldable", "run_in_background", "ModuleApi"] +__all__ = [ + "errors", + "make_deferred_yieldable", + "parse_json_object_from_request", + "respond_with_html", + "run_in_background", + "cached", + "UserID", + "DatabasePool", + "LoggingTransaction", + "DirectServeHtmlResource", + "DirectServeJsonResource", + "ModuleApi", +] logger = logging.getLogger(__name__) @@ -52,12 +89,27 @@ def __init__(self, hs: "HomeServer", auth_handler): self._server_name = hs.hostname self._presence_stream = hs.get_event_sources().sources["presence"] self._state = hs.get_state_handler() + self._clock = hs.get_clock() # type: Clock + self._send_email_handler = hs.get_send_email_handler() + + try: + app_name = self._hs.config.email_app_name + + self._from_string = self._hs.config.email_notif_from % {"app": app_name} + except (KeyError, TypeError): + # If substitution failed (which can happen if the string contains + # placeholders other than just "app", or if the type of the placeholder is + # not a string), fall back to the bare strings. + self._from_string = self._hs.config.email_notif_from + + self._raw_from = email.utils.parseaddr(self._from_string)[1] # We expose these as properties below in order to attach a helpful docstring. self._http_client: SimpleHttpClient = hs.get_simple_http_client() self._public_room_list_manager = PublicRoomListManager(hs) self._spam_checker = hs.get_spam_checker() + self._account_validity_handler = hs.get_account_validity_handler() ################################################################################# # The following methods should only be called during the module's initialisation. @@ -67,6 +119,11 @@ def register_spam_checker_callbacks(self): """Registers callbacks for spam checking capabilities.""" return self._spam_checker.register_callbacks + @property + def register_account_validity_callbacks(self): + """Registers callbacks for account validity capabilities.""" + return self._account_validity_handler.register_account_validity_callbacks + def register_web_resource(self, path: str, resource: IResource): """Registers a web resource to be served at the given path. @@ -101,22 +158,56 @@ def public_room_list_manager(self): """ return self._public_room_list_manager - def get_user_by_req(self, req, allow_guest=False): + @property + def public_baseurl(self) -> str: + """The configured public base URL for this homeserver.""" + return self._hs.config.public_baseurl + + @property + def email_app_name(self) -> str: + """The application name configured in the homeserver's configuration.""" + return self._hs.config.email.email_app_name + + async def get_user_by_req( + self, + req: SynapseRequest, + allow_guest: bool = False, + allow_expired: bool = False, + ) -> Requester: """Check the access_token provided for a request Args: - req (twisted.web.server.Request): Incoming HTTP request - allow_guest (bool): True if guest users should be allowed. If this + req: Incoming HTTP request + allow_guest: True if guest users should be allowed. If this is False, and the access token is for a guest user, an AuthError will be thrown + allow_expired: True if expired users should be allowed. If this + is False, and the access token is for an expired user, an + AuthError will be thrown + Returns: - twisted.internet.defer.Deferred[synapse.types.Requester]: - the requester for this request + The requester for this request + Raises: - synapse.api.errors.AuthError: if no user by that token exists, + InvalidClientCredentialsError: if no user by that token exists, or the token is invalid. """ - return self._auth.get_user_by_req(req, allow_guest) + return await self._auth.get_user_by_req( + req, + allow_guest, + allow_expired=allow_expired, + ) + + async def is_user_admin(self, user_id: str) -> bool: + """Checks if a user is a server admin. + + Args: + user_id: The Matrix ID of the user to check. + + Returns: + True if the user is a server admin, False otherwise. + """ + return await self._store.is_server_admin(UserID.from_string(user_id)) def get_qualified_user_id(self, username): """Qualify a user id, if necessary @@ -134,6 +225,32 @@ def get_qualified_user_id(self, username): return username return UserID(username, self._hs.hostname).to_string() + async def get_profile_for_user(self, localpart: str) -> ProfileInfo: + """Look up the profile info for the user with the given localpart. + + Args: + localpart: The localpart to look up profile information for. + + Returns: + The profile information (i.e. display name and avatar URL). + """ + return await self._store.get_profileinfo(localpart) + + async def get_threepids_for_user(self, user_id: str) -> List[Dict[str, str]]: + """Look up the threepids (email addresses and phone numbers) associated with the + given Matrix user ID. + + Args: + user_id: The Matrix user ID to look up threepids for. + + Returns: + A list of threepids, each threepid being represented by a dictionary + containing a "medium" key which value is "email" for email addresses and + "msisdn" for phone numbers, and an "address" key which value is the + threepid's address. + """ + return await self._store.user_get_threepids(user_id) + def check_user_exists(self, user_id): """Check if user exists. @@ -464,6 +581,88 @@ async def send_local_online_presence_to(self, users: Iterable[str]) -> None: presence_events, destination ) + def looping_background_call( + self, + f: Callable, + msec: float, + *args, + desc: Optional[str] = None, + **kwargs, + ): + """Wraps a function as a background process and calls it repeatedly. + + Waits `msec` initially before calling `f` for the first time. + + Args: + f: The function to call repeatedly. f can be either synchronous or + asynchronous, and must follow Synapse's logcontext rules. + More info about logcontexts is available at + https://matrix-org.github.io/synapse/latest/log_contexts.html + msec: How long to wait between calls in milliseconds. + *args: Positional arguments to pass to function. + desc: The background task's description. Default to the function's name. + **kwargs: Key arguments to pass to function. + """ + if desc is None: + desc = f.__name__ + + if self._hs.config.run_background_tasks: + self._clock.looping_call( + run_as_background_process, + msec, + desc, + f, + *args, + **kwargs, + ) + else: + logger.warning( + "Not running looping call %s as the configuration forbids it", + f, + ) + + async def send_mail( + self, + recipient: str, + subject: str, + html: str, + text: str, + ): + """Send an email on behalf of the homeserver. + + Args: + recipient: The email address for the recipient. + subject: The email's subject. + html: The email's HTML content. + text: The email's text content. + """ + await self._send_email_handler.send_email( + email_address=recipient, + subject=subject, + app_name=self.email_app_name, + html=html, + text=text, + ) + + def read_templates( + self, + filenames: List[str], + custom_template_directory: Optional[str] = None, + ) -> List[jinja2.Template]: + """Read and load the content of the template files at the given location. + By default, Synapse will look for these templates in its configured template + directory, but another directory to search in can be provided. + + Args: + filenames: The name of the template files to look for. + custom_template_directory: An additional directory to look for the files in. + + Returns: + A list containing the loaded templates, with the orders matching the one of + the filenames parameter. + """ + return self._hs.config.read_templates(filenames, custom_template_directory) + class PublicRoomListManager: """Contains methods for adding to, removing from and querying whether a room diff --git a/synapse/module_api/errors.py b/synapse/module_api/errors.py index 02bbb0be39..98ea911a81 100644 --- a/synapse/module_api/errors.py +++ b/synapse/module_api/errors.py @@ -14,5 +14,9 @@ """Exception types which are exposed as part of the stable module API""" -from synapse.api.errors import RedirectException, SynapseError # noqa: F401 +from synapse.api.errors import ( # noqa: F401 + InvalidClientCredentialsError, + RedirectException, + SynapseError, +) from synapse.config._base import ConfigError # noqa: F401 diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 2519ad76db..85621f33ef 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -62,10 +62,6 @@ def __init__(self, hs: "HomeServer"): self.store = self.hs.get_datastore() self.clock = self.hs.get_clock() - self._account_validity_enabled = ( - hs.config.account_validity.account_validity_enabled - ) - # We shard the handling of push notifications by user ID. self._pusher_shard_config = hs.config.push.pusher_shard_config self._instance_name = hs.get_instance_name() @@ -89,6 +85,8 @@ def __init__(self, hs: "HomeServer"): # map from user id to app_id:pushkey to pusher self.pushers: Dict[str, Dict[str, Pusher]] = {} + self._account_validity_handler = hs.get_account_validity_handler() + def start(self) -> None: """Starts the pushers off in a background process.""" if not self._should_start_pushers: @@ -238,12 +236,9 @@ async def _on_new_notifications(self, max_token: RoomStreamToken) -> None: for u in users_affected: # Don't push if the user account has expired - if self._account_validity_enabled: - expired = await self.store.is_account_expired( - u, self.clock.time_msec() - ) - if expired: - continue + expired = await self._account_validity_handler.is_user_expired(u) + if expired: + continue if u in self.pushers: for p in self.pushers[u].values(): @@ -268,12 +263,9 @@ async def on_new_receipts( for u in users_affected: # Don't push if the user account has expired - if self._account_validity_enabled: - expired = await self.store.is_account_expired( - u, self.clock.time_msec() - ) - if expired: - continue + expired = await self._account_validity_handler.is_user_expired(u) + if expired: + continue if u in self.pushers: for p in self.pushers[u].values(): diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 7d75564758..06e6ccee42 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -560,16 +560,24 @@ def __init__(self, hs: "HomeServer"): async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: await assert_requester_is_admin(self.auth, request) - body = parse_json_object_from_request(request) + if self.account_activity_handler.on_legacy_admin_request_callback: + expiration_ts = await ( + self.account_activity_handler.on_legacy_admin_request_callback(request) + ) + else: + body = parse_json_object_from_request(request) - if "user_id" not in body: - raise SynapseError(400, "Missing property 'user_id' in the request body") + if "user_id" not in body: + raise SynapseError( + 400, + "Missing property 'user_id' in the request body", + ) - expiration_ts = await self.account_activity_handler.renew_account_for_user( - body["user_id"], - body.get("expiration_ts"), - not body.get("enable_renewal_emails", True), - ) + expiration_ts = await self.account_activity_handler.renew_account_for_user( + body["user_id"], + body.get("expiration_ts"), + not body.get("enable_renewal_emails", True), + ) res = {"expiration_ts": expiration_ts} return 200, res diff --git a/synapse/rest/client/v2_alpha/account_validity.py b/synapse/rest/client/v2_alpha/account_validity.py index 2d1ad3d3fb..3ebe401861 100644 --- a/synapse/rest/client/v2_alpha/account_validity.py +++ b/synapse/rest/client/v2_alpha/account_validity.py @@ -14,7 +14,7 @@ import logging -from synapse.api.errors import AuthError, SynapseError +from synapse.api.errors import SynapseError from synapse.http.server import respond_with_html from synapse.http.servlet import RestServlet @@ -92,11 +92,6 @@ def __init__(self, hs): ) async def on_POST(self, request): - if not self.account_validity_renew_by_email_enabled: - raise AuthError( - 403, "Account renewal via email is disabled on this server." - ) - requester = await self.auth.get_user_by_req(request, allow_expired=True) user_id = requester.user.to_string() await self.account_activity_handler.send_renewal_email_to_user(user_id) diff --git a/tests/test_state.py b/tests/test_state.py index 780eba823c..e5488df1ac 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -168,6 +168,7 @@ def setUp(self): "get_state_handler", "get_clock", "get_state_resolution_handler", + "get_account_validity_handler", "hostname", ] ) From 98aec1cc9da2bd6b8e34ffb282c85abf9b8b42ca Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 16 Jul 2021 19:22:36 +0200 Subject: [PATCH 422/619] Use inline type hints in `handlers/` and `rest/`. (#10382) --- changelog.d/10382.misc | 1 + synapse/handlers/_base.py | 8 ++-- synapse/handlers/admin.py | 4 +- synapse/handlers/appservice.py | 6 +-- synapse/handlers/auth.py | 16 ++++---- synapse/handlers/cas.py | 2 +- synapse/handlers/device.py | 14 +++---- synapse/handlers/devicemessage.py | 2 +- synapse/handlers/directory.py | 6 +-- synapse/handlers/e2e_keys.py | 40 +++++++++---------- synapse/handlers/events.py | 6 +-- synapse/handlers/federation.py | 22 +++++----- synapse/handlers/groups_local.py | 4 +- synapse/handlers/initial_sync.py | 14 +++++-- synapse/handlers/message.py | 18 ++++----- synapse/handlers/oidc.py | 18 ++++----- synapse/handlers/pagination.py | 4 +- synapse/handlers/presence.py | 28 ++++++------- synapse/handlers/profile.py | 4 +- synapse/handlers/receipts.py | 4 +- synapse/handlers/room.py | 16 ++++---- synapse/handlers/room_list.py | 18 ++++----- synapse/handlers/saml.py | 6 +-- synapse/handlers/search.py | 8 ++-- synapse/handlers/space_summary.py | 16 ++++---- synapse/handlers/sso.py | 12 +++--- synapse/handlers/stats.py | 10 ++--- synapse/handlers/sync.py | 32 ++++++++------- synapse/handlers/typing.py | 14 +++---- synapse/handlers/user_directory.py | 2 +- synapse/rest/admin/rooms.py | 8 ++-- synapse/rest/admin/users.py | 2 +- synapse/rest/client/v1/login.py | 8 ++-- synapse/rest/client/v1/room.py | 10 ++--- synapse/rest/client/v2_alpha/sendtodevice.py | 2 +- synapse/rest/consent/consent_resource.py | 4 +- synapse/rest/key/v2/remote_key_resource.py | 4 +- synapse/rest/media/v1/_base.py | 2 +- synapse/rest/media/v1/media_repository.py | 10 ++--- synapse/rest/media/v1/media_storage.py | 4 +- synapse/rest/media/v1/preview_url_resource.py | 8 ++-- synapse/rest/media/v1/upload_resource.py | 6 +-- synapse/rest/synapse/client/pick_username.py | 4 +- 43 files changed, 212 insertions(+), 215 deletions(-) create mode 100644 changelog.d/10382.misc diff --git a/changelog.d/10382.misc b/changelog.d/10382.misc new file mode 100644 index 0000000000..eed2d8552a --- /dev/null +++ b/changelog.d/10382.misc @@ -0,0 +1 @@ +Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index d800e16912..525f3d39b1 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -38,10 +38,10 @@ class BaseHandler: """ def __init__(self, hs: "HomeServer"): - self.store = hs.get_datastore() # type: synapse.storage.DataStore + self.store = hs.get_datastore() self.auth = hs.get_auth() self.notifier = hs.get_notifier() - self.state_handler = hs.get_state_handler() # type: synapse.state.StateHandler + self.state_handler = hs.get_state_handler() self.distributor = hs.get_distributor() self.clock = hs.get_clock() self.hs = hs @@ -55,12 +55,12 @@ def __init__(self, hs: "HomeServer"): # Check whether ratelimiting room admin message redaction is enabled # by the presence of rate limits in the config if self.hs.config.rc_admin_redaction: - self.admin_redaction_ratelimiter = Ratelimiter( + self.admin_redaction_ratelimiter: Optional[Ratelimiter] = Ratelimiter( store=self.store, clock=self.clock, rate_hz=self.hs.config.rc_admin_redaction.per_second, burst_count=self.hs.config.rc_admin_redaction.burst_count, - ) # type: Optional[Ratelimiter] + ) else: self.admin_redaction_ratelimiter = None diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py index d75a8b15c3..bfa7f2c545 100644 --- a/synapse/handlers/admin.py +++ b/synapse/handlers/admin.py @@ -139,7 +139,7 @@ async def export_user_data(self, user_id: str, writer: "ExfiltrationWriter") -> to_key = RoomStreamToken(None, stream_ordering) # Events that we've processed in this room - written_events = set() # type: Set[str] + written_events: Set[str] = set() # We need to track gaps in the events stream so that we can then # write out the state at those events. We do this by keeping track @@ -152,7 +152,7 @@ async def export_user_data(self, user_id: str, writer: "ExfiltrationWriter") -> # The reverse mapping to above, i.e. map from unseen event to events # that have the unseen event in their prev_events, i.e. the unseen # events "children". - unseen_to_child_events = {} # type: Dict[str, Set[str]] + unseen_to_child_events: Dict[str, Set[str]] = {} # We fetch events in the room the user could see by fetching *all* # events that we have and then filtering, this isn't the most diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py index 862638cc4f..21a17cd2e8 100644 --- a/synapse/handlers/appservice.py +++ b/synapse/handlers/appservice.py @@ -96,7 +96,7 @@ async def _notify_interested_services(self, max_token: RoomStreamToken): self.current_max, limit ) - events_by_room = {} # type: Dict[str, List[EventBase]] + events_by_room: Dict[str, List[EventBase]] = {} for event in events: events_by_room.setdefault(event.room_id, []).append(event) @@ -275,7 +275,7 @@ async def _handle_receipts(self, service: ApplicationService) -> List[JsonDict]: async def _handle_presence( self, service: ApplicationService, users: Collection[Union[str, UserID]] ) -> List[JsonDict]: - events = [] # type: List[JsonDict] + events: List[JsonDict] = [] presence_source = self.event_sources.sources["presence"] from_key = await self.store.get_type_stream_id_for_appservice( service, "presence" @@ -375,7 +375,7 @@ async def get_3pe_protocols( self, only_protocol: Optional[str] = None ) -> Dict[str, JsonDict]: services = self.store.get_app_services() - protocols = {} # type: Dict[str, List[JsonDict]] + protocols: Dict[str, List[JsonDict]] = {} # Collect up all the individual protocol responses out of the ASes for s in services: diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index e2ac595a62..22a8552241 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -191,7 +191,7 @@ class AuthHandler(BaseHandler): def __init__(self, hs: "HomeServer"): super().__init__(hs) - self.checkers = {} # type: Dict[str, UserInteractiveAuthChecker] + self.checkers: Dict[str, UserInteractiveAuthChecker] = {} for auth_checker_class in INTERACTIVE_AUTH_CHECKERS: inst = auth_checker_class(hs) if inst.is_enabled(): @@ -296,7 +296,7 @@ def __init__(self, hs: "HomeServer"): # A mapping of user ID to extra attributes to include in the login # response. - self._extra_attributes = {} # type: Dict[str, SsoLoginExtraAttributes] + self._extra_attributes: Dict[str, SsoLoginExtraAttributes] = {} async def validate_user_via_ui_auth( self, @@ -500,7 +500,7 @@ async def check_ui_auth( all the stages in any of the permitted flows. """ - sid = None # type: Optional[str] + sid: Optional[str] = None authdict = clientdict.pop("auth", {}) if "session" in authdict: sid = authdict["session"] @@ -588,9 +588,9 @@ async def check_ui_auth( ) # check auth type currently being presented - errordict = {} # type: Dict[str, Any] + errordict: Dict[str, Any] = {} if "type" in authdict: - login_type = authdict["type"] # type: str + login_type: str = authdict["type"] try: result = await self._check_auth_dict(authdict, clientip) if result: @@ -766,7 +766,7 @@ def _auth_dict_for_flows( LoginType.TERMS: self._get_params_terms, } - params = {} # type: Dict[str, Any] + params: Dict[str, Any] = {} for f in public_flows: for stage in f: @@ -1530,9 +1530,9 @@ async def start_sso_ui_auth(self, request: SynapseRequest, session_id: str) -> s except StoreError: raise SynapseError(400, "Unknown session ID: %s" % (session_id,)) - user_id_to_verify = await self.get_session_data( + user_id_to_verify: str = await self.get_session_data( session_id, UIAuthSessionDataConstants.REQUEST_USER_ID - ) # type: str + ) idps = await self.hs.get_sso_handler().get_identity_providers_for_user( user_id_to_verify diff --git a/synapse/handlers/cas.py b/synapse/handlers/cas.py index 7346ccfe93..b681d208bc 100644 --- a/synapse/handlers/cas.py +++ b/synapse/handlers/cas.py @@ -171,7 +171,7 @@ def _parse_cas_response(self, cas_response_body: bytes) -> CasResponse: # Iterate through the nodes and pull out the user and any extra attributes. user = None - attributes = {} # type: Dict[str, List[Optional[str]]] + attributes: Dict[str, List[Optional[str]]] = {} for child in root[0]: if child.tag.endswith("user"): user = child.text diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py index 95bdc5902a..46ee834407 100644 --- a/synapse/handlers/device.py +++ b/synapse/handlers/device.py @@ -452,7 +452,7 @@ async def notify_device_update( user_id ) - hosts = set() # type: Set[str] + hosts: Set[str] = set() if self.hs.is_mine_id(user_id): hosts.update(get_domain_from_id(u) for u in users_who_share_room) hosts.discard(self.server_name) @@ -613,20 +613,20 @@ def __init__(self, hs: "HomeServer", device_handler: DeviceHandler): self._remote_edu_linearizer = Linearizer(name="remote_device_list") # user_id -> list of updates waiting to be handled. - self._pending_updates = ( - {} - ) # type: Dict[str, List[Tuple[str, str, Iterable[str], JsonDict]]] + self._pending_updates: Dict[ + str, List[Tuple[str, str, Iterable[str], JsonDict]] + ] = {} # Recently seen stream ids. We don't bother keeping these in the DB, # but they're useful to have them about to reduce the number of spurious # resyncs. - self._seen_updates = ExpiringCache( + self._seen_updates: ExpiringCache[str, Set[str]] = ExpiringCache( cache_name="device_update_edu", clock=self.clock, max_len=10000, expiry_ms=30 * 60 * 1000, iterable=True, - ) # type: ExpiringCache[str, Set[str]] + ) # Attempt to resync out of sync device lists every 30s. self._resync_retry_in_progress = False @@ -755,7 +755,7 @@ async def _need_to_do_resync( """Given a list of updates for a user figure out if we need to do a full resync, or whether we have enough data that we can just apply the delta. """ - seen_updates = self._seen_updates.get(user_id, set()) # type: Set[str] + seen_updates: Set[str] = self._seen_updates.get(user_id, set()) extremity = await self.store.get_device_list_last_stream_id_for_remote(user_id) diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py index 580b941595..679b47f081 100644 --- a/synapse/handlers/devicemessage.py +++ b/synapse/handlers/devicemessage.py @@ -203,7 +203,7 @@ async def send_device_message( log_kv({"number_of_to_device_messages": len(messages)}) set_tag("sender", sender_user_id) local_messages = {} - remote_messages = {} # type: Dict[str, Dict[str, Dict[str, JsonDict]]] + remote_messages: Dict[str, Dict[str, Dict[str, JsonDict]]] = {} for user_id, by_device in messages.items(): # Ratelimit local cross-user key requests by the sending device. if ( diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 06d7012bac..d487fee627 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -237,9 +237,9 @@ async def _delete_association(self, room_alias: RoomAlias) -> str: async def get_association(self, room_alias: RoomAlias) -> JsonDict: room_id = None if self.hs.is_mine(room_alias): - result = await self.get_association_from_room_alias( - room_alias - ) # type: Optional[RoomAliasMapping] + result: Optional[ + RoomAliasMapping + ] = await self.get_association_from_room_alias(room_alias) if result: room_id = result.room_id diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py index 3972849d4d..d92370859f 100644 --- a/synapse/handlers/e2e_keys.py +++ b/synapse/handlers/e2e_keys.py @@ -115,9 +115,9 @@ async def query_devices( the number of in-flight queries at a time. """ with await self._query_devices_linearizer.queue((from_user_id, from_device_id)): - device_keys_query = query_body.get( + device_keys_query: Dict[str, Iterable[str]] = query_body.get( "device_keys", {} - ) # type: Dict[str, Iterable[str]] + ) # separate users by domain. # make a map from domain to user_id to device_ids @@ -136,7 +136,7 @@ async def query_devices( # First get local devices. # A map of destination -> failure response. - failures = {} # type: Dict[str, JsonDict] + failures: Dict[str, JsonDict] = {} results = {} if local_query: local_result = await self.query_local_devices(local_query) @@ -151,11 +151,9 @@ async def query_devices( # Now attempt to get any remote devices from our local cache. # A map of destination -> user ID -> device IDs. - remote_queries_not_in_cache = ( - {} - ) # type: Dict[str, Dict[str, Iterable[str]]] + remote_queries_not_in_cache: Dict[str, Dict[str, Iterable[str]]] = {} if remote_queries: - query_list = [] # type: List[Tuple[str, Optional[str]]] + query_list: List[Tuple[str, Optional[str]]] = [] for user_id, device_ids in remote_queries.items(): if device_ids: query_list.extend( @@ -362,9 +360,9 @@ async def query_local_devices( A map from user_id -> device_id -> device details """ set_tag("local_query", query) - local_query = [] # type: List[Tuple[str, Optional[str]]] + local_query: List[Tuple[str, Optional[str]]] = [] - result_dict = {} # type: Dict[str, Dict[str, dict]] + result_dict: Dict[str, Dict[str, dict]] = {} for user_id, device_ids in query.items(): # we use UserID.from_string to catch invalid user ids if not self.is_mine(UserID.from_string(user_id)): @@ -402,9 +400,9 @@ async def on_federation_query_client_keys( self, query_body: Dict[str, Dict[str, Optional[List[str]]]] ) -> JsonDict: """Handle a device key query from a federated server""" - device_keys_query = query_body.get( + device_keys_query: Dict[str, Optional[List[str]]] = query_body.get( "device_keys", {} - ) # type: Dict[str, Optional[List[str]]] + ) res = await self.query_local_devices(device_keys_query) ret = {"device_keys": res} @@ -421,8 +419,8 @@ async def on_federation_query_client_keys( async def claim_one_time_keys( self, query: Dict[str, Dict[str, Dict[str, str]]], timeout: int ) -> JsonDict: - local_query = [] # type: List[Tuple[str, str, str]] - remote_queries = {} # type: Dict[str, Dict[str, Dict[str, str]]] + local_query: List[Tuple[str, str, str]] = [] + remote_queries: Dict[str, Dict[str, Dict[str, str]]] = {} for user_id, one_time_keys in query.get("one_time_keys", {}).items(): # we use UserID.from_string to catch invalid user ids @@ -439,8 +437,8 @@ async def claim_one_time_keys( results = await self.store.claim_e2e_one_time_keys(local_query) # A map of user ID -> device ID -> key ID -> key. - json_result = {} # type: Dict[str, Dict[str, Dict[str, JsonDict]]] - failures = {} # type: Dict[str, JsonDict] + json_result: Dict[str, Dict[str, Dict[str, JsonDict]]] = {} + failures: Dict[str, JsonDict] = {} for user_id, device_keys in results.items(): for device_id, keys in device_keys.items(): for key_id, json_str in keys.items(): @@ -768,8 +766,8 @@ async def _process_self_signatures( Raises: SynapseError: if the input is malformed """ - signature_list = [] # type: List[SignatureListItem] - failures = {} # type: Dict[str, Dict[str, JsonDict]] + signature_list: List["SignatureListItem"] = [] + failures: Dict[str, Dict[str, JsonDict]] = {} if not signatures: return signature_list, failures @@ -930,8 +928,8 @@ async def _process_other_signatures( Raises: SynapseError: if the input is malformed """ - signature_list = [] # type: List[SignatureListItem] - failures = {} # type: Dict[str, Dict[str, JsonDict]] + signature_list: List["SignatureListItem"] = [] + failures: Dict[str, Dict[str, JsonDict]] = {} if not signatures: return signature_list, failures @@ -1300,7 +1298,7 @@ def __init__(self, hs: "HomeServer", e2e_keys_handler: E2eKeysHandler): self._remote_edu_linearizer = Linearizer(name="remote_signing_key") # user_id -> list of updates waiting to be handled. - self._pending_updates = {} # type: Dict[str, List[Tuple[JsonDict, JsonDict]]] + self._pending_updates: Dict[str, List[Tuple[JsonDict, JsonDict]]] = {} async def incoming_signing_key_update( self, origin: str, edu_content: JsonDict @@ -1349,7 +1347,7 @@ async def _handle_signing_key_updates(self, user_id: str) -> None: # This can happen since we batch updates return - device_ids = [] # type: List[str] + device_ids: List[str] = [] logger.info("pending updates: %r", pending_updates) diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index f134f1e234..4b3f037072 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -93,7 +93,7 @@ async def get_stream( # When the user joins a new room, or another user joins a currently # joined room, we need to send down presence for those users. - to_add = [] # type: List[JsonDict] + to_add: List[JsonDict] = [] for event in events: if not isinstance(event, EventBase): continue @@ -103,9 +103,9 @@ async def get_stream( # Send down presence. if event.state_key == auth_user_id: # Send down presence for everyone in the room. - users = await self.store.get_users_in_room( + users: Iterable[str] = await self.store.get_users_in_room( event.room_id - ) # type: Iterable[str] + ) else: users = [event.state_key] diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 0209aee186..5c4463583e 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -181,7 +181,7 @@ def __init__(self, hs: "HomeServer"): # When joining a room we need to queue any events for that room up. # For each room, a list of (pdu, origin) tuples. - self.room_queues = {} # type: Dict[str, List[Tuple[EventBase, str]]] + self.room_queues: Dict[str, List[Tuple[EventBase, str]]] = {} self._room_pdu_linearizer = Linearizer("fed_room_pdu") self._room_backfill = Linearizer("room_backfill") @@ -368,7 +368,7 @@ async def on_receive_pdu( ours = await self.state_store.get_state_groups_ids(room_id, seen) # state_maps is a list of mappings from (type, state_key) to event_id - state_maps = list(ours.values()) # type: List[StateMap[str]] + state_maps: List[StateMap[str]] = list(ours.values()) # we don't need this any more, let's delete it. del ours @@ -845,7 +845,7 @@ async def _process_received_pdu( # exact key to expect. Otherwise check it matches any key we # have for that device. - current_keys = [] # type: Container[str] + current_keys: Container[str] = [] if device: keys = device.get("keys", {}).get("keys", {}) @@ -1185,7 +1185,7 @@ def get_domains_from_state(state: StateMap[EventBase]) -> List[Tuple[str, int]]: if e_type == EventTypes.Member and event.membership == Membership.JOIN ] - joined_domains = {} # type: Dict[str, int] + joined_domains: Dict[str, int] = {} for u, d in joined_users: try: dom = get_domain_from_id(u) @@ -1314,7 +1314,7 @@ async def _get_events_and_persist( room_version = await self.store.get_room_version(room_id) - event_map = {} # type: Dict[str, EventBase] + event_map: Dict[str, EventBase] = {} async def get_event(event_id: str): with nested_logging_context(event_id): @@ -1596,7 +1596,7 @@ async def do_knock( # Ask the remote server to create a valid knock event for us. Once received, # we sign the event - params = {"ver": supported_room_versions} # type: Dict[str, Iterable[str]] + params: Dict[str, Iterable[str]] = {"ver": supported_room_versions} origin, event, event_format_version = await self._make_and_verify_event( target_hosts, room_id, knockee, Membership.KNOCK, content, params=params ) @@ -2453,14 +2453,14 @@ async def _check_for_soft_fail( state_sets_d = await self.state_store.get_state_groups( event.room_id, extrem_ids ) - state_sets = list(state_sets_d.values()) # type: List[Iterable[EventBase]] + state_sets: List[Iterable[EventBase]] = list(state_sets_d.values()) state_sets.append(state) current_states = await self.state_handler.resolve_events( room_version, state_sets, event ) - current_state_ids = { + current_state_ids: StateMap[str] = { k: e.event_id for k, e in current_states.items() - } # type: StateMap[str] + } else: current_state_ids = await self.state_handler.get_current_state_ids( event.room_id, latest_event_ids=extrem_ids @@ -2817,7 +2817,7 @@ async def _update_context_for_auth_events( """ # exclude the state key of the new event from the current_state in the context. if event.is_state(): - event_key = (event.type, event.state_key) # type: Optional[Tuple[str, str]] + event_key: Optional[Tuple[str, str]] = (event.type, event.state_key) else: event_key = None state_updates = { @@ -3156,7 +3156,7 @@ async def _check_signature(self, event: EventBase, context: EventContext) -> Non logger.debug("Checking auth on event %r", event.content) - last_exception = None # type: Optional[Exception] + last_exception: Optional[Exception] = None # for each public key in the 3pid invite event for public_key_object in event_auth.get_public_keys(invite_event): diff --git a/synapse/handlers/groups_local.py b/synapse/handlers/groups_local.py index 157f2ff218..1a6c5c64a2 100644 --- a/synapse/handlers/groups_local.py +++ b/synapse/handlers/groups_local.py @@ -214,7 +214,7 @@ async def get_publicised_groups_for_user(self, user_id: str) -> JsonDict: async def bulk_get_publicised_groups( self, user_ids: Iterable[str], proxy: bool = True ) -> JsonDict: - destinations = {} # type: Dict[str, Set[str]] + destinations: Dict[str, Set[str]] = {} local_users = set() for user_id in user_ids: @@ -227,7 +227,7 @@ async def bulk_get_publicised_groups( raise SynapseError(400, "Some user_ids are not local") results = {} - failed_results = [] # type: List[str] + failed_results: List[str] = [] for destination, dest_user_ids in destinations.items(): try: r = await self.transport_client.bulk_get_publicised_groups( diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 76242865ae..5d49640760 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -46,9 +46,17 @@ def __init__(self, hs: "HomeServer"): self.state = hs.get_state_handler() self.clock = hs.get_clock() self.validator = EventValidator() - self.snapshot_cache = ResponseCache( - hs.get_clock(), "initial_sync_cache" - ) # type: ResponseCache[Tuple[str, Optional[StreamToken], Optional[StreamToken], str, Optional[int], bool, bool]] + self.snapshot_cache: ResponseCache[ + Tuple[ + str, + Optional[StreamToken], + Optional[StreamToken], + str, + Optional[int], + bool, + bool, + ] + ] = ResponseCache(hs.get_clock(), "initial_sync_cache") self._event_serializer = hs.get_event_client_serializer() self.storage = hs.get_storage() self.state_store = self.storage.state diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index e06655f3d4..c7fe4ff89e 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -81,7 +81,7 @@ def __init__(self, hs: "HomeServer"): # The scheduled call to self._expire_event. None if no call is currently # scheduled. - self._scheduled_expiry = None # type: Optional[IDelayedCall] + self._scheduled_expiry: Optional[IDelayedCall] = None if not hs.config.worker_app: run_as_background_process( @@ -196,9 +196,7 @@ async def get_state_events( room_state_events = await self.state_store.get_state_for_events( [event.event_id], state_filter=state_filter ) - room_state = room_state_events[ - event.event_id - ] # type: Mapping[Any, EventBase] + room_state: Mapping[Any, EventBase] = room_state_events[event.event_id] else: raise AuthError( 403, @@ -421,9 +419,9 @@ def __init__(self, hs: "HomeServer"): self.action_generator = hs.get_action_generator() self.spam_checker = hs.get_spam_checker() - self.third_party_event_rules = ( + self.third_party_event_rules: "ThirdPartyEventRules" = ( self.hs.get_third_party_event_rules() - ) # type: ThirdPartyEventRules + ) self._block_events_without_consent_error = ( self.config.block_events_without_consent_error @@ -440,7 +438,7 @@ def __init__(self, hs: "HomeServer"): # # map from room id to time-of-last-attempt. # - self._rooms_to_exclude_from_dummy_event_insertion = {} # type: Dict[str, int] + self._rooms_to_exclude_from_dummy_event_insertion: Dict[str, int] = {} # The number of forward extremeities before a dummy event is sent. self._dummy_events_threshold = hs.config.dummy_events_threshold @@ -465,9 +463,7 @@ def __init__(self, hs: "HomeServer"): # Stores the state groups we've recently added to the joined hosts # external cache. Note that the timeout must be significantly less than # the TTL on the external cache. - self._external_cache_joined_hosts_updates = ( - None - ) # type: Optional[ExpiringCache] + self._external_cache_joined_hosts_updates: Optional[ExpiringCache] = None if self._external_cache.is_enabled(): self._external_cache_joined_hosts_updates = ExpiringCache( "_external_cache_joined_hosts_updates", @@ -1299,7 +1295,7 @@ async def persist_and_notify_client_event( # Validate a newly added alias or newly added alt_aliases. original_alias = None - original_alt_aliases = [] # type: List[str] + original_alt_aliases: List[str] = [] original_event_id = event.unsigned.get("replaces_state") if original_event_id: diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py index ee6e41c0e4..a330c48fa7 100644 --- a/synapse/handlers/oidc.py +++ b/synapse/handlers/oidc.py @@ -105,9 +105,9 @@ def __init__(self, hs: "HomeServer"): assert provider_confs self._token_generator = OidcSessionTokenGenerator(hs) - self._providers = { + self._providers: Dict[str, "OidcProvider"] = { p.idp_id: OidcProvider(hs, self._token_generator, p) for p in provider_confs - } # type: Dict[str, OidcProvider] + } async def load_metadata(self) -> None: """Validate the config and load the metadata from the remote endpoint. @@ -178,7 +178,7 @@ async def handle_oidc_callback(self, request: SynapseRequest) -> None: # are two. for cookie_name, _ in _SESSION_COOKIES: - session = request.getCookie(cookie_name) # type: Optional[bytes] + session: Optional[bytes] = request.getCookie(cookie_name) if session is not None: break else: @@ -277,7 +277,7 @@ def __init__( self._token_generator = token_generator self._config = provider - self._callback_url = hs.config.oidc_callback_url # type: str + self._callback_url: str = hs.config.oidc_callback_url # Calculate the prefix for OIDC callback paths based on the public_baseurl. # We'll insert this into the Path= parameter of any session cookies we set. @@ -290,7 +290,7 @@ def __init__( self._scopes = provider.scopes self._user_profile_method = provider.user_profile_method - client_secret = None # type: Union[None, str, JwtClientSecret] + client_secret: Optional[Union[str, JwtClientSecret]] = None if provider.client_secret: client_secret = provider.client_secret elif provider.client_secret_jwt_key: @@ -305,7 +305,7 @@ def __init__( provider.client_id, client_secret, provider.client_auth_method, - ) # type: ClientAuth + ) self._client_auth_method = provider.client_auth_method # cache of metadata for the identity provider (endpoint uris, mostly). This is @@ -324,7 +324,7 @@ def __init__( self._allow_existing_users = provider.allow_existing_users self._http_client = hs.get_proxied_http_client() - self._server_name = hs.config.server_name # type: str + self._server_name: str = hs.config.server_name # identifier for the external_ids table self.idp_id = provider.idp_id @@ -1381,7 +1381,7 @@ def render_template_field(template: Optional[Template]) -> Optional[str]: if display_name == "": display_name = None - emails = [] # type: List[str] + emails: List[str] = [] email = render_template_field(self._config.email_template) if email: emails.append(email) @@ -1391,7 +1391,7 @@ def render_template_field(template: Optional[Template]) -> Optional[str]: ) async def get_extra_attributes(self, userinfo: UserInfo, token: Token) -> JsonDict: - extras = {} # type: Dict[str, str] + extras: Dict[str, str] = {} for key, template in self._config.extra_attributes.items(): try: extras[key] = template.render(user=userinfo).strip() diff --git a/synapse/handlers/pagination.py b/synapse/handlers/pagination.py index 1e1186c29e..1dbafd253d 100644 --- a/synapse/handlers/pagination.py +++ b/synapse/handlers/pagination.py @@ -81,9 +81,9 @@ def __init__(self, hs: "HomeServer"): self._server_name = hs.hostname self.pagination_lock = ReadWriteLock() - self._purges_in_progress_by_room = set() # type: Set[str] + self._purges_in_progress_by_room: Set[str] = set() # map from purge id to PurgeStatus - self._purges_by_id = {} # type: Dict[str, PurgeStatus] + self._purges_by_id: Dict[str, PurgeStatus] = {} self._event_serializer = hs.get_event_client_serializer() self._retention_default_max_lifetime = hs.config.retention_default_max_lifetime diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 44ed7a0712..016c5df2ca 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -378,14 +378,14 @@ def __init__(self, hs: "HomeServer"): # The number of ongoing syncs on this process, by user id. # Empty if _presence_enabled is false. - self._user_to_num_current_syncs = {} # type: Dict[str, int] + self._user_to_num_current_syncs: Dict[str, int] = {} self.notifier = hs.get_notifier() self.instance_id = hs.get_instance_id() # user_id -> last_sync_ms. Lists the users that have stopped syncing but # we haven't notified the presence writer of that yet - self.users_going_offline = {} # type: Dict[str, int] + self.users_going_offline: Dict[str, int] = {} self._bump_active_client = ReplicationBumpPresenceActiveTime.make_client(hs) self._set_state_client = ReplicationPresenceSetState.make_client(hs) @@ -650,7 +650,7 @@ def __init__(self, hs: "HomeServer"): # Set of users who have presence in the `user_to_current_state` that # have not yet been persisted - self.unpersisted_users_changes = set() # type: Set[str] + self.unpersisted_users_changes: Set[str] = set() hs.get_reactor().addSystemEventTrigger( "before", @@ -664,7 +664,7 @@ def __init__(self, hs: "HomeServer"): # Keeps track of the number of *ongoing* syncs on this process. While # this is non zero a user will never go offline. - self.user_to_num_current_syncs = {} # type: Dict[str, int] + self.user_to_num_current_syncs: Dict[str, int] = {} # Keeps track of the number of *ongoing* syncs on other processes. # While any sync is ongoing on another process the user will never @@ -674,8 +674,8 @@ def __init__(self, hs: "HomeServer"): # we assume that all the sync requests on that process have stopped. # Stored as a dict from process_id to set of user_id, and a dict of # process_id to millisecond timestamp last updated. - self.external_process_to_current_syncs = {} # type: Dict[str, Set[str]] - self.external_process_last_updated_ms = {} # type: Dict[str, int] + self.external_process_to_current_syncs: Dict[str, Set[str]] = {} + self.external_process_last_updated_ms: Dict[str, int] = {} self.external_sync_linearizer = Linearizer(name="external_sync_linearizer") @@ -1581,9 +1581,7 @@ async def get_new_events( # The set of users that we're interested in and that have had a presence update. # We'll actually pull the presence updates for these users at the end. - interested_and_updated_users = ( - set() - ) # type: Union[Set[str], FrozenSet[str]] + interested_and_updated_users: Union[Set[str], FrozenSet[str]] = set() if from_key: # First get all users that have had a presence update @@ -1950,8 +1948,8 @@ async def get_interested_parties( A 2-tuple of `(room_ids_to_states, users_to_states)`, with each item being a dict of `entity_name` -> `[UserPresenceState]` """ - room_ids_to_states = {} # type: Dict[str, List[UserPresenceState]] - users_to_states = {} # type: Dict[str, List[UserPresenceState]] + room_ids_to_states: Dict[str, List[UserPresenceState]] = {} + users_to_states: Dict[str, List[UserPresenceState]] = {} for state in states: room_ids = await store.get_rooms_for_user(state.user_id) for room_id in room_ids: @@ -2063,12 +2061,12 @@ def __init__(self, hs: "HomeServer", presence_handler: BasePresenceHandler): # stream_id, destinations, user_ids)`. We don't store the full states # for efficiency, and remote workers will already have the full states # cached. - self._queue = [] # type: List[Tuple[int, int, Collection[str], Set[str]]] + self._queue: List[Tuple[int, int, Collection[str], Set[str]]] = [] self._next_id = 1 # Map from instance name to current token - self._current_tokens = {} # type: Dict[str, int] + self._current_tokens: Dict[str, int] = {} if self._queue_presence_updates: self._clock.looping_call(self._clear_queue, self._CLEAR_ITEMS_EVERY_MS) @@ -2168,7 +2166,7 @@ async def get_replication_rows( # handle the case where `from_token` stream ID has already been dropped. start_idx = max(from_token + 1 - self._next_id, -len(self._queue)) - to_send = [] # type: List[Tuple[int, Tuple[str, str]]] + to_send: List[Tuple[int, Tuple[str, str]]] = [] limited = False new_id = upto_token for _, stream_id, destinations, user_ids in self._queue[start_idx:]: @@ -2216,7 +2214,7 @@ async def process_replication_rows( if not self._federation: return - hosts_to_users = {} # type: Dict[str, Set[str]] + hosts_to_users: Dict[str, Set[str]] = {} for row in rows: hosts_to_users.setdefault(row.destination, set()).add(row.user_id) diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py index 05b4a97b59..20a033d0ba 100644 --- a/synapse/handlers/profile.py +++ b/synapse/handlers/profile.py @@ -197,7 +197,7 @@ async def set_displayname( 400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN,) ) - displayname_to_set = new_displayname # type: Optional[str] + displayname_to_set: Optional[str] = new_displayname if new_displayname == "": displayname_to_set = None @@ -286,7 +286,7 @@ async def set_avatar_url( 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) ) - avatar_url_to_set = new_avatar_url # type: Optional[str] + avatar_url_to_set: Optional[str] = new_avatar_url if new_avatar_url == "": avatar_url_to_set = None diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 0059ad0f56..283483fc2c 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -98,8 +98,8 @@ async def _received_remote_receipt(self, origin: str, content: JsonDict) -> None async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool: """Takes a list of receipts, stores them and informs the notifier.""" - min_batch_id = None # type: Optional[int] - max_batch_id = None # type: Optional[int] + min_batch_id: Optional[int] = None + max_batch_id: Optional[int] = None for receipt in receipts: res = await self.store.insert_receipt( diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 579b1b93c5..64656fda22 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -87,7 +87,7 @@ def __init__(self, hs: "HomeServer"): self.config = hs.config # Room state based off defined presets - self._presets_dict = { + self._presets_dict: Dict[str, Dict[str, Any]] = { RoomCreationPreset.PRIVATE_CHAT: { "join_rules": JoinRules.INVITE, "history_visibility": HistoryVisibility.SHARED, @@ -109,7 +109,7 @@ def __init__(self, hs: "HomeServer"): "guest_can_join": False, "power_level_content_override": {}, }, - } # type: Dict[str, Dict[str, Any]] + } # Modify presets to selectively enable encryption by default per homeserver config for preset_name, preset_config in self._presets_dict.items(): @@ -127,9 +127,9 @@ def __init__(self, hs: "HomeServer"): # If a user tries to update the same room multiple times in quick # succession, only process the first attempt and return its result to # subsequent requests - self._upgrade_response_cache = ResponseCache( + self._upgrade_response_cache: ResponseCache[Tuple[str, str]] = ResponseCache( hs.get_clock(), "room_upgrade", timeout_ms=FIVE_MINUTES_IN_MS - ) # type: ResponseCache[Tuple[str, str]] + ) self._server_notices_mxid = hs.config.server_notices_mxid self.third_party_event_rules = hs.get_third_party_event_rules() @@ -377,10 +377,10 @@ async def clone_existing_room( if not await self.spam_checker.user_may_create_room(user_id): raise SynapseError(403, "You are not permitted to create rooms") - creation_content = { + creation_content: JsonDict = { "room_version": new_room_version.identifier, "predecessor": {"room_id": old_room_id, "event_id": tombstone_event_id}, - } # type: JsonDict + } # Check if old room was non-federatable @@ -936,7 +936,7 @@ async def send(etype: str, content: JsonDict, **kwargs) -> int: etype=EventTypes.PowerLevels, content=pl_content ) else: - power_level_content = { + power_level_content: JsonDict = { "users": {creator_id: 100}, "users_default": 0, "events": { @@ -955,7 +955,7 @@ async def send(etype: str, content: JsonDict, **kwargs) -> int: "kick": 50, "redact": 50, "invite": 50, - } # type: JsonDict + } if config["original_invitees_have_ops"]: for invitee in invite_list: diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index c6bfa5451f..6284bcdfbc 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -47,12 +47,12 @@ class RoomListHandler(BaseHandler): def __init__(self, hs: "HomeServer"): super().__init__(hs) self.enable_room_list_search = hs.config.enable_room_list_search - self.response_cache = ResponseCache( - hs.get_clock(), "room_list" - ) # type: ResponseCache[Tuple[Optional[int], Optional[str], Optional[ThirdPartyInstanceID]]] - self.remote_response_cache = ResponseCache( - hs.get_clock(), "remote_room_list", timeout_ms=30 * 1000 - ) # type: ResponseCache[Tuple[str, Optional[int], Optional[str], bool, Optional[str]]] + self.response_cache: ResponseCache[ + Tuple[Optional[int], Optional[str], Optional[ThirdPartyInstanceID]] + ] = ResponseCache(hs.get_clock(), "room_list") + self.remote_response_cache: ResponseCache[ + Tuple[str, Optional[int], Optional[str], bool, Optional[str]] + ] = ResponseCache(hs.get_clock(), "remote_room_list", timeout_ms=30 * 1000) async def get_local_public_room_list( self, @@ -139,10 +139,10 @@ async def _get_public_room_list( if since_token: batch_token = RoomListNextBatch.from_token(since_token) - bounds = ( + bounds: Optional[Tuple[int, str]] = ( batch_token.last_joined_members, batch_token.last_room_id, - ) # type: Optional[Tuple[int, str]] + ) forwards = batch_token.direction_is_forward has_batch_token = True else: @@ -182,7 +182,7 @@ def build_room_entry(room): results = [build_room_entry(r) for r in results] - response = {} # type: JsonDict + response: JsonDict = {} num_results = len(results) if limit is not None: more_to_come = num_results == probing_limit diff --git a/synapse/handlers/saml.py b/synapse/handlers/saml.py index 80ba65b9e0..72f54c9403 100644 --- a/synapse/handlers/saml.py +++ b/synapse/handlers/saml.py @@ -83,7 +83,7 @@ def __init__(self, hs: "HomeServer"): self.unstable_idp_brand = None # a map from saml session id to Saml2SessionData object - self._outstanding_requests_dict = {} # type: Dict[str, Saml2SessionData] + self._outstanding_requests_dict: Dict[str, Saml2SessionData] = {} self._sso_handler = hs.get_sso_handler() self._sso_handler.register_identity_provider(self) @@ -386,10 +386,10 @@ def dot_replace_for_mxid(username: str) -> str: return username -MXID_MAPPER_MAP = { +MXID_MAPPER_MAP: Dict[str, Callable[[str], str]] = { "hexencode": map_username_to_mxid_localpart, "dotreplace": dot_replace_for_mxid, -} # type: Dict[str, Callable[[str], str]] +} @attr.s diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index 4e718d3f63..8226d6f5a1 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -192,7 +192,7 @@ async def search( # If doing a subset of all rooms seearch, check if any of the rooms # are from an upgraded room, and search their contents as well if search_filter.rooms: - historical_room_ids = [] # type: List[str] + historical_room_ids: List[str] = [] for room_id in search_filter.rooms: # Add any previous rooms to the search if they exist ids = await self.get_old_rooms_from_upgraded_room(room_id) @@ -216,9 +216,9 @@ async def search( rank_map = {} # event_id -> rank of event allowed_events = [] # Holds result of grouping by room, if applicable - room_groups = {} # type: Dict[str, JsonDict] + room_groups: Dict[str, JsonDict] = {} # Holds result of grouping by sender, if applicable - sender_group = {} # type: Dict[str, JsonDict] + sender_group: Dict[str, JsonDict] = {} # Holds the next_batch for the entire result set if one of those exists global_next_batch = None @@ -262,7 +262,7 @@ async def search( s["results"].append(e.event_id) elif order_by == "recent": - room_events = [] # type: List[EventBase] + room_events: List[EventBase] = [] i = 0 pagination_token = batch_token diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 366e6211e5..5f7d4602bd 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -90,14 +90,14 @@ async def get_space_summary( room_queue = deque((_RoomQueueEntry(room_id, ()),)) # rooms we have already processed - processed_rooms = set() # type: Set[str] + processed_rooms: Set[str] = set() # events we have already processed. We don't necessarily have their event ids, # so instead we key on (room id, state key) - processed_events = set() # type: Set[Tuple[str, str]] + processed_events: Set[Tuple[str, str]] = set() - rooms_result = [] # type: List[JsonDict] - events_result = [] # type: List[JsonDict] + rooms_result: List[JsonDict] = [] + events_result: List[JsonDict] = [] while room_queue and len(rooms_result) < MAX_ROOMS: queue_entry = room_queue.popleft() @@ -272,10 +272,10 @@ async def federation_space_summary( # the set of rooms that we should not walk further. Initialise it with the # excluded-rooms list; we will add other rooms as we process them so that # we do not loop. - processed_rooms = set(exclude_rooms) # type: Set[str] + processed_rooms: Set[str] = set(exclude_rooms) - rooms_result = [] # type: List[JsonDict] - events_result = [] # type: List[JsonDict] + rooms_result: List[JsonDict] = [] + events_result: List[JsonDict] = [] while room_queue and len(rooms_result) < MAX_ROOMS: room_id = room_queue.popleft() @@ -353,7 +353,7 @@ async def _summarize_local_room( max_children = MAX_ROOMS_PER_SPACE now = self._clock.time_msec() - events_result = [] # type: List[JsonDict] + events_result: List[JsonDict] = [] for edge_event in itertools.islice(child_events, max_children): events_result.append( await self._event_serializer.serialize_event( diff --git a/synapse/handlers/sso.py b/synapse/handlers/sso.py index 0b297e54c4..1b855a685c 100644 --- a/synapse/handlers/sso.py +++ b/synapse/handlers/sso.py @@ -202,10 +202,10 @@ def __init__(self, hs: "HomeServer"): self._mapping_lock = Linearizer(name="sso_user_mapping", clock=hs.get_clock()) # a map from session id to session data - self._username_mapping_sessions = {} # type: Dict[str, UsernameMappingSession] + self._username_mapping_sessions: Dict[str, UsernameMappingSession] = {} # map from idp_id to SsoIdentityProvider - self._identity_providers = {} # type: Dict[str, SsoIdentityProvider] + self._identity_providers: Dict[str, SsoIdentityProvider] = {} self._consent_at_registration = hs.config.consent.user_consent_at_registration @@ -296,7 +296,7 @@ async def handle_redirect_request( ) # if the client chose an IdP, use that - idp = None # type: Optional[SsoIdentityProvider] + idp: Optional[SsoIdentityProvider] = None if idp_id: idp = self._identity_providers.get(idp_id) if not idp: @@ -669,9 +669,9 @@ async def complete_sso_ui_auth_request( remote_user_id, ) - user_id_to_verify = await self._auth_handler.get_session_data( + user_id_to_verify: str = await self._auth_handler.get_session_data( ui_auth_session_id, UIAuthSessionDataConstants.REQUEST_USER_ID - ) # type: str + ) if not user_id: logger.warning( @@ -793,7 +793,7 @@ async def handle_submit_username_request( session.use_display_name = use_display_name emails_from_idp = set(session.emails) - filtered_emails = set() # type: Set[str] + filtered_emails: Set[str] = set() # we iterate through the list rather than just building a set conjunction, so # that we can log attempts to use unknown addresses diff --git a/synapse/handlers/stats.py b/synapse/handlers/stats.py index 814d08efcb..3fd89af2a4 100644 --- a/synapse/handlers/stats.py +++ b/synapse/handlers/stats.py @@ -49,7 +49,7 @@ def __init__(self, hs: "HomeServer"): self.stats_enabled = hs.config.stats_enabled # The current position in the current_state_delta stream - self.pos = None # type: Optional[int] + self.pos: Optional[int] = None # Guard to ensure we only process deltas one at a time self._is_processing = False @@ -131,10 +131,10 @@ async def _handle_deltas( mapping from room/user ID to changes in the various fields. """ - room_to_stats_deltas = {} # type: Dict[str, CounterType[str]] - user_to_stats_deltas = {} # type: Dict[str, CounterType[str]] + room_to_stats_deltas: Dict[str, CounterType[str]] = {} + user_to_stats_deltas: Dict[str, CounterType[str]] = {} - room_to_state_updates = {} # type: Dict[str, Dict[str, Any]] + room_to_state_updates: Dict[str, Dict[str, Any]] = {} for delta in deltas: typ = delta["type"] @@ -164,7 +164,7 @@ async def _handle_deltas( ) continue - event_content = {} # type: JsonDict + event_content: JsonDict = {} if event_id is not None: event = await self.store.get_event(event_id, allow_none=True) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index b9a0361059..722c4ae670 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -278,12 +278,14 @@ def __init__(self, hs: "HomeServer"): self.state_store = self.storage.state # ExpiringCache((User, Device)) -> LruCache(user_id => event_id) - self.lazy_loaded_members_cache = ExpiringCache( + self.lazy_loaded_members_cache: ExpiringCache[ + Tuple[str, Optional[str]], LruCache[str, str] + ] = ExpiringCache( "lazy_loaded_members_cache", self.clock, max_len=0, expiry_ms=LAZY_LOADED_MEMBERS_CACHE_MAX_AGE, - ) # type: ExpiringCache[Tuple[str, Optional[str]], LruCache[str, str]] + ) async def wait_for_sync_for_user( self, @@ -440,7 +442,7 @@ async def ephemeral_by_room( ) now_token = now_token.copy_and_replace("typing_key", typing_key) - ephemeral_by_room = {} # type: JsonDict + ephemeral_by_room: JsonDict = {} for event in typing: # we want to exclude the room_id from the event, but modifying the @@ -502,7 +504,7 @@ async def _load_filtered_recents( # We check if there are any state events, if there are then we pass # all current state events to the filter_events function. This is to # ensure that we always include current state in the timeline - current_state_ids = frozenset() # type: FrozenSet[str] + current_state_ids: FrozenSet[str] = frozenset() if any(e.is_state() for e in recents): current_state_ids_map = await self.store.get_current_state_ids( room_id @@ -783,9 +785,9 @@ async def compute_summary( def get_lazy_loaded_members_cache( self, cache_key: Tuple[str, Optional[str]] ) -> LruCache[str, str]: - cache = self.lazy_loaded_members_cache.get( + cache: Optional[LruCache[str, str]] = self.lazy_loaded_members_cache.get( cache_key - ) # type: Optional[LruCache[str, str]] + ) if cache is None: logger.debug("creating LruCache for %r", cache_key) cache = LruCache(LAZY_LOADED_MEMBERS_CACHE_MAX_SIZE) @@ -984,7 +986,7 @@ async def compute_state_delta( if t[0] == EventTypes.Member: cache.set(t[1], event_id) - state = {} # type: Dict[str, EventBase] + state: Dict[str, EventBase] = {} if state_ids: state = await self.store.get_events(list(state_ids.values())) @@ -1088,8 +1090,8 @@ async def generate_sync_result( logger.debug("Fetching OTK data") device_id = sync_config.device_id - one_time_key_counts = {} # type: JsonDict - unused_fallback_key_types = [] # type: List[str] + one_time_key_counts: JsonDict = {} + unused_fallback_key_types: List[str] = [] if device_id: one_time_key_counts = await self.store.count_e2e_one_time_keys( user_id, device_id @@ -1437,7 +1439,7 @@ async def _generate_sync_entry_for_rooms( ) if block_all_room_ephemeral: - ephemeral_by_room = {} # type: Dict[str, List[JsonDict]] + ephemeral_by_room: Dict[str, List[JsonDict]] = {} else: now_token, ephemeral_by_room = await self.ephemeral_by_room( sync_result_builder, @@ -1468,7 +1470,7 @@ async def _generate_sync_entry_for_rooms( # If there is ignored users account data and it matches the proper type, # then use it. - ignored_users = frozenset() # type: FrozenSet[str] + ignored_users: FrozenSet[str] = frozenset() if ignored_account_data: ignored_users_data = ignored_account_data.get("ignored_users", {}) if isinstance(ignored_users_data, dict): @@ -1586,7 +1588,7 @@ async def _get_rooms_changed( user_id, since_token.room_key, now_token.room_key ) - mem_change_events_by_room_id = {} # type: Dict[str, List[EventBase]] + mem_change_events_by_room_id: Dict[str, List[EventBase]] = {} for event in rooms_changed: mem_change_events_by_room_id.setdefault(event.room_id, []).append(event) @@ -1722,7 +1724,7 @@ async def _get_rooms_changed( # This is all screaming out for a refactor, as the logic here is # subtle and the moving parts numerous. if leave_event.internal_metadata.is_out_of_band_membership(): - batch_events = [leave_event] # type: Optional[List[EventBase]] + batch_events: Optional[List[EventBase]] = [leave_event] else: batch_events = None @@ -1971,7 +1973,7 @@ async def _generate_room_entry( room_id, batch, sync_config, since_token, now_token, full_state=full_state ) - summary = {} # type: Optional[JsonDict] + summary: Optional[JsonDict] = {} # we include a summary in room responses when we're lazy loading # members (as the client otherwise doesn't have enough info to form @@ -1995,7 +1997,7 @@ async def _generate_room_entry( ) if room_builder.rtype == "joined": - unread_notifications = {} # type: Dict[str, int] + unread_notifications: Dict[str, int] = {} room_sync = JoinedSyncResult( room_id=room_id, timeline=batch, diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index c0a8364755..0cb651a400 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -68,11 +68,11 @@ def __init__(self, hs: "HomeServer"): ) # map room IDs to serial numbers - self._room_serials = {} # type: Dict[str, int] + self._room_serials: Dict[str, int] = {} # map room IDs to sets of users currently typing - self._room_typing = {} # type: Dict[str, Set[str]] + self._room_typing: Dict[str, Set[str]] = {} - self._member_last_federation_poke = {} # type: Dict[RoomMember, int] + self._member_last_federation_poke: Dict[RoomMember, int] = {} self.wheel_timer = WheelTimer(bucket_size=5000) self._latest_room_serial = 0 @@ -217,7 +217,7 @@ def __init__(self, hs: "HomeServer"): hs.get_distributor().observe("user_left_room", self.user_left_room) # clock time we expect to stop - self._member_typing_until = {} # type: Dict[RoomMember, int] + self._member_typing_until: Dict[RoomMember, int] = {} # caches which room_ids changed at which serials self._typing_stream_change_cache = StreamChangeCache( @@ -405,9 +405,9 @@ async def get_all_typing_updates( if last_id == current_id: return [], current_id, False - changed_rooms = self._typing_stream_change_cache.get_all_entities_changed( - last_id - ) # type: Optional[Iterable[str]] + changed_rooms: Optional[ + Iterable[str] + ] = self._typing_stream_change_cache.get_all_entities_changed(last_id) if changed_rooms is None: changed_rooms = self._room_serials diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py index dacc4f3076..6edb1da50a 100644 --- a/synapse/handlers/user_directory.py +++ b/synapse/handlers/user_directory.py @@ -52,7 +52,7 @@ def __init__(self, hs: "HomeServer"): self.search_all_users = hs.config.user_directory_search_all_users self.spam_checker = hs.get_spam_checker() # The current position in the current_state_delta stream - self.pos = None # type: Optional[int] + self.pos: Optional[int] = None # Guard to ensure we only process deltas one at a time self._is_processing = False diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 3c51a742bf..40ee33646c 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -402,9 +402,9 @@ async def on_POST( # Get the room ID from the identifier. try: - remote_room_hosts = [ + remote_room_hosts: Optional[List[str]] = [ x.decode("ascii") for x in request.args[b"server_name"] - ] # type: Optional[List[str]] + ] except Exception: remote_room_hosts = None room_id, remote_room_hosts = await self.resolve_room_id( @@ -659,9 +659,7 @@ async def on_GET( filter_str = parse_string(request, "filter", encoding="utf-8") if filter_str: filter_json = urlparse.unquote(filter_str) - event_filter = Filter( - json_decoder.decode(filter_json) - ) # type: Optional[Filter] + event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) else: event_filter = None diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 06e6ccee42..589e47fa47 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -357,7 +357,7 @@ class UserRegisterServlet(RestServlet): def __init__(self, hs: "HomeServer"): self.auth_handler = hs.get_auth_handler() self.reactor = hs.get_reactor() - self.nonces = {} # type: Dict[str, int] + self.nonces: Dict[str, int] = {} self.hs = hs def _clear_old_nonces(self): diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index cbcb60fe31..99d02cb355 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -121,7 +121,7 @@ def on_GET(self, request: SynapseRequest): flows.append({"type": LoginRestServlet.CAS_TYPE}) if self.cas_enabled or self.saml2_enabled or self.oidc_enabled: - sso_flow = { + sso_flow: JsonDict = { "type": LoginRestServlet.SSO_TYPE, "identity_providers": [ _get_auth_flow_dict_for_idp( @@ -129,7 +129,7 @@ def on_GET(self, request: SynapseRequest): ) for idp in self._sso_handler.get_identity_providers().values() ], - } # type: JsonDict + } if self._msc2858_enabled: # backwards-compatibility support for clients which don't @@ -447,7 +447,7 @@ def _get_auth_flow_dict_for_idp( use_unstable_brands: whether we should use brand identifiers suitable for the unstable API """ - e = {"id": idp.idp_id, "name": idp.idp_name} # type: JsonDict + e: JsonDict = {"id": idp.idp_id, "name": idp.idp_name} if idp.idp_icon: e["icon"] = idp.idp_icon if idp.idp_brand: @@ -561,7 +561,7 @@ async def on_GET( finish_request(request) return - args = request.args # type: Dict[bytes, List[bytes]] # type: ignore + args: Dict[bytes, List[bytes]] = request.args # type: ignore client_redirect_url = parse_bytes_from_args(args, "redirectUrl", required=True) sso_url = await self._sso_handler.handle_redirect_request( request, diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index ebf4e32230..31a1193cd3 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -783,7 +783,7 @@ async def on_POST(self, request): server = parse_string(request, "server", default=None) content = parse_json_object_from_request(request) - limit = int(content.get("limit", 100)) # type: Optional[int] + limit: Optional[int] = int(content.get("limit", 100)) since_token = content.get("since", None) search_filter = content.get("filter", None) @@ -929,9 +929,7 @@ async def on_GET(self, request, room_id): filter_str = parse_string(request, "filter", encoding="utf-8") if filter_str: filter_json = urlparse.unquote(filter_str) - event_filter = Filter( - json_decoder.decode(filter_json) - ) # type: Optional[Filter] + event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) if ( event_filter and event_filter.filter_json.get("event_format", "client") @@ -1044,9 +1042,7 @@ async def on_GET(self, request, room_id, event_id): filter_str = parse_string(request, "filter", encoding="utf-8") if filter_str: filter_json = urlparse.unquote(filter_str) - event_filter = Filter( - json_decoder.decode(filter_json) - ) # type: Optional[Filter] + event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json)) else: event_filter = None diff --git a/synapse/rest/client/v2_alpha/sendtodevice.py b/synapse/rest/client/v2_alpha/sendtodevice.py index f8dcee603c..d537d811d8 100644 --- a/synapse/rest/client/v2_alpha/sendtodevice.py +++ b/synapse/rest/client/v2_alpha/sendtodevice.py @@ -59,7 +59,7 @@ async def _put(self, request, message_type, txn_id): requester, message_type, content["messages"] ) - response = (200, {}) # type: Tuple[int, dict] + response: Tuple[int, dict] = (200, {}) return response diff --git a/synapse/rest/consent/consent_resource.py b/synapse/rest/consent/consent_resource.py index e52570cd8e..4282e2b228 100644 --- a/synapse/rest/consent/consent_resource.py +++ b/synapse/rest/consent/consent_resource.py @@ -117,7 +117,7 @@ async def _async_render_GET(self, request): has_consented = False public_version = username == "" if not public_version: - args = request.args # type: Dict[bytes, List[bytes]] + args: Dict[bytes, List[bytes]] = request.args userhmac_bytes = parse_bytes_from_args(args, "h", required=True) self._check_hash(username, userhmac_bytes) @@ -154,7 +154,7 @@ async def _async_render_POST(self, request): """ version = parse_string(request, "v", required=True) username = parse_string(request, "u", required=True) - args = request.args # type: Dict[bytes, List[bytes]] + args: Dict[bytes, List[bytes]] = request.args userhmac = parse_bytes_from_args(args, "h", required=True) self._check_hash(username, userhmac) diff --git a/synapse/rest/key/v2/remote_key_resource.py b/synapse/rest/key/v2/remote_key_resource.py index d56a1ae482..63a40b1852 100644 --- a/synapse/rest/key/v2/remote_key_resource.py +++ b/synapse/rest/key/v2/remote_key_resource.py @@ -97,7 +97,7 @@ def __init__(self, hs): async def _async_render_GET(self, request): if len(request.postpath) == 1: (server,) = request.postpath - query = {server.decode("ascii"): {}} # type: dict + query: dict = {server.decode("ascii"): {}} elif len(request.postpath) == 2: server, key_id = request.postpath minimum_valid_until_ts = parse_integer(request, "minimum_valid_until_ts") @@ -141,7 +141,7 @@ async def query_keys(self, request, query, query_remote_on_cache_miss=False): time_now_ms = self.clock.time_msec() # Note that the value is unused. - cache_misses = {} # type: Dict[str, Dict[str, int]] + cache_misses: Dict[str, Dict[str, int]] = {} for (server_name, key_id, _), results in cached.items(): results = [(result["ts_added_ms"], result) for result in results] diff --git a/synapse/rest/media/v1/_base.py b/synapse/rest/media/v1/_base.py index 0fb4cd81f1..90364ebcf7 100644 --- a/synapse/rest/media/v1/_base.py +++ b/synapse/rest/media/v1/_base.py @@ -49,7 +49,7 @@ def parse_media_id(request: Request) -> Tuple[str, str, Optional[str]]: try: # The type on postpath seems incorrect in Twisted 21.2.0. - postpath = request.postpath # type: List[bytes] # type: ignore + postpath: List[bytes] = request.postpath # type: ignore assert postpath # This allows users to append e.g. /test.png to the URL. Useful for diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index 21c43c340c..4f702f890c 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -78,16 +78,16 @@ def __init__(self, hs: "HomeServer"): Thumbnailer.set_limits(self.max_image_pixels) - self.primary_base_path = hs.config.media_store_path # type: str - self.filepaths = MediaFilePaths(self.primary_base_path) # type: MediaFilePaths + self.primary_base_path: str = hs.config.media_store_path + self.filepaths: MediaFilePaths = MediaFilePaths(self.primary_base_path) self.dynamic_thumbnails = hs.config.dynamic_thumbnails self.thumbnail_requirements = hs.config.thumbnail_requirements self.remote_media_linearizer = Linearizer(name="media_remote") - self.recently_accessed_remotes = set() # type: Set[Tuple[str, str]] - self.recently_accessed_locals = set() # type: Set[str] + self.recently_accessed_remotes: Set[Tuple[str, str]] = set() + self.recently_accessed_locals: Set[str] = set() self.federation_domain_whitelist = hs.config.federation_domain_whitelist @@ -711,7 +711,7 @@ async def _generate_thumbnails( # We deduplicate the thumbnail sizes by ignoring the cropped versions if # they have the same dimensions of a scaled one. - thumbnails = {} # type: Dict[Tuple[int, int, str], str] + thumbnails: Dict[Tuple[int, int, str], str] = {} for r_width, r_height, r_method, r_type in requirements: if r_method == "crop": thumbnails.setdefault((r_width, r_height, r_type), r_method) diff --git a/synapse/rest/media/v1/media_storage.py b/synapse/rest/media/v1/media_storage.py index c7fd97c46c..56cdc1b4ed 100644 --- a/synapse/rest/media/v1/media_storage.py +++ b/synapse/rest/media/v1/media_storage.py @@ -191,7 +191,7 @@ async def fetch_media(self, file_info: FileInfo) -> Optional[Responder]: for provider in self.storage_providers: for path in paths: - res = await provider.fetch(path, file_info) # type: Any + res: Any = await provider.fetch(path, file_info) if res: logger.debug("Streaming %s from %s", path, provider) return res @@ -233,7 +233,7 @@ async def ensure_media_is_in_local_cache(self, file_info: FileInfo) -> str: os.makedirs(dirname) for provider in self.storage_providers: - res = await provider.fetch(path, file_info) # type: Any + res: Any = await provider.fetch(path, file_info) if res: with res: consumer = BackgroundFileConsumer( diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 0adfb1a70f..8e7fead3a2 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -169,12 +169,12 @@ def __init__( # memory cache mapping urls to an ObservableDeferred returning # JSON-encoded OG metadata - self._cache = ExpiringCache( + self._cache: ExpiringCache[str, ObservableDeferred] = ExpiringCache( cache_name="url_previews", clock=self.clock, # don't spider URLs more often than once an hour expiry_ms=ONE_HOUR, - ) # type: ExpiringCache[str, ObservableDeferred] + ) if self._worker_run_media_background_jobs: self._cleaner_loop = self.clock.looping_call( @@ -460,7 +460,7 @@ async def _download_url(self, url: str, user: str) -> Dict[str, Any]: file_info = FileInfo(server_name=None, file_id=file_id, url_cache=True) # If this URL can be accessed via oEmbed, use that instead. - url_to_download = url # type: Optional[str] + url_to_download: Optional[str] = url oembed_url = self._get_oembed_url(url) if oembed_url: # The result might be a new URL to download, or it might be HTML content. @@ -788,7 +788,7 @@ def _calc_og(tree: "etree.Element", media_uri: str) -> Dict[str, Optional[str]]: # "og:video:height" : "720", # "og:video:secure_url": "https://www.youtube.com/v/LXDBoHyjmtw?version=3", - og = {} # type: Dict[str, Optional[str]] + og: Dict[str, Optional[str]] = {} for tag in tree.xpath("//*/meta[starts-with(@property, 'og:')]"): if "content" in tag.attrib: # if we've got more than 50 tags, someone is taking the piss diff --git a/synapse/rest/media/v1/upload_resource.py b/synapse/rest/media/v1/upload_resource.py index 62dc4aae2d..146adca8f1 100644 --- a/synapse/rest/media/v1/upload_resource.py +++ b/synapse/rest/media/v1/upload_resource.py @@ -61,11 +61,11 @@ async def _async_render_POST(self, request: SynapseRequest) -> None: errcode=Codes.TOO_LARGE, ) - args = request.args # type: Dict[bytes, List[bytes]] # type: ignore + args: Dict[bytes, List[bytes]] = request.args # type: ignore upload_name_bytes = parse_bytes_from_args(args, "filename") if upload_name_bytes: try: - upload_name = upload_name_bytes.decode("utf8") # type: Optional[str] + upload_name: Optional[str] = upload_name_bytes.decode("utf8") except UnicodeDecodeError: raise SynapseError( msg="Invalid UTF-8 filename parameter: %r" % (upload_name), code=400 @@ -89,7 +89,7 @@ async def _async_render_POST(self, request: SynapseRequest) -> None: # TODO(markjh): parse content-dispostion try: - content = request.content # type: IO # type: ignore + content: IO = request.content # type: ignore content_uri = await self.media_repo.create_content( media_type, upload_name, content, content_length, requester.user ) diff --git a/synapse/rest/synapse/client/pick_username.py b/synapse/rest/synapse/client/pick_username.py index 9b002cc15e..ab24ec0a8e 100644 --- a/synapse/rest/synapse/client/pick_username.py +++ b/synapse/rest/synapse/client/pick_username.py @@ -118,9 +118,9 @@ async def _async_render_POST(self, request: SynapseRequest): use_display_name = parse_boolean(request, "use_display_name", default=False) try: - emails_to_use = [ + emails_to_use: List[str] = [ val.decode("utf-8") for val in request.args.get(b"use_email", []) - ] # type: List[str] + ] except ValueError: raise SynapseError(400, "Query parameter use_email must be utf-8") except SynapseError as e: From 323452944e13114264b0c645db875dc6950315c5 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 16 Jul 2021 21:12:56 +0200 Subject: [PATCH 423/619] One last inline type hint (for the whole repo) (#10418) --- changelog.d/10418.misc | 1 + synapse/module_api/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10418.misc diff --git a/changelog.d/10418.misc b/changelog.d/10418.misc new file mode 100644 index 0000000000..eed2d8552a --- /dev/null +++ b/changelog.d/10418.misc @@ -0,0 +1 @@ +Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index f3c78089b7..5df9349134 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -89,7 +89,7 @@ def __init__(self, hs: "HomeServer", auth_handler): self._server_name = hs.hostname self._presence_stream = hs.get_event_sources().sources["presence"] self._state = hs.get_state_handler() - self._clock = hs.get_clock() # type: Clock + self._clock: Clock = hs.get_clock() self._send_email_handler = hs.get_send_email_handler() try: From 7387d6f6249277482964e588462619c8b23a9d82 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 19 Jul 2021 04:16:46 -0500 Subject: [PATCH 424/619] Remove unused `events_by_room` (#10421) It looks like it was first used and introduced in https://github.com/matrix-org/synapse/commit/5130d80d79fe1f95ce03b8f1cfd4fbf0a32f5ac8#diff-8a4a36a7728107b2ccaff2cb405dbab229a1100fe50653a63d1aa9ac10ae45e8R305 but the But the usage was removed in https://github.com/matrix-org/synapse/commit/4c6a31cd6efa25be4c9f1b357e8f92065fac63eb#diff-8a4a36a7728107b2ccaff2cb405dbab229a1100fe50653a63d1aa9ac10ae45e8 --- changelog.d/10421.misc | 1 + synapse/storage/databases/main/events.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 changelog.d/10421.misc diff --git a/changelog.d/10421.misc b/changelog.d/10421.misc new file mode 100644 index 0000000000..385cbe07af --- /dev/null +++ b/changelog.d/10421.misc @@ -0,0 +1 @@ +Remove unused `events_by_room` code (tech debt). diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index ec8579b9ad..a396a201d4 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -2010,10 +2010,6 @@ def _update_backward_extremeties(self, txn, events): Forward extremities are handled when we first start persisting the events. """ - events_by_room: Dict[str, List[EventBase]] = {} - for ev in events: - events_by_room.setdefault(ev.room_id, []).append(ev) - query = ( "INSERT INTO event_backward_extremities (event_id, room_id)" " SELECT ?, ? WHERE NOT EXISTS (" From 95e47b2e782b5e7afa5fd2afd1d0ea7745eaac36 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 19 Jul 2021 16:28:05 +0200 Subject: [PATCH 425/619] [pyupgrade] `synapse/` (#10348) This PR is tantamount to running ``` pyupgrade --py36-plus --keep-percent-format `find synapse/ -type f -name "*.py"` ``` Part of #9744 --- changelog.d/10348.misc | 1 + synapse/app/generic_worker.py | 6 +-- synapse/app/homeserver.py | 6 +-- synapse/config/appservice.py | 2 +- synapse/config/tls.py | 6 +-- synapse/handlers/cas.py | 2 +- synapse/handlers/federation.py | 2 +- synapse/handlers/identity.py | 4 +- synapse/handlers/oidc.py | 38 ++++++++++--------- synapse/handlers/register.py | 15 +++----- synapse/handlers/saml.py | 2 +- synapse/handlers/sync.py | 2 +- synapse/http/proxyagent.py | 2 +- synapse/http/site.py | 2 +- synapse/logging/opentracing.py | 2 +- synapse/metrics/_exposition.py | 26 ++++++------- synapse/metrics/background_process_metrics.py | 3 +- synapse/rest/client/v1/login.py | 25 +++++------- synapse/rest/media/v1/__init__.py | 4 +- synapse/storage/database.py | 2 +- synapse/storage/databases/main/deviceinbox.py | 4 +- .../storage/databases/main/group_server.py | 6 ++- synapse/storage/databases/main/roommember.py | 2 +- synapse/storage/prepare_database.py | 2 +- synapse/types.py | 4 +- synapse/util/caches/lrucache.py | 3 +- synapse/util/caches/treecache.py | 3 +- synapse/util/daemonize.py | 8 ++-- synapse/visibility.py | 4 +- 29 files changed, 86 insertions(+), 102 deletions(-) create mode 100644 changelog.d/10348.misc diff --git a/changelog.d/10348.misc b/changelog.d/10348.misc new file mode 100644 index 0000000000..b2275a1350 --- /dev/null +++ b/changelog.d/10348.misc @@ -0,0 +1 @@ +Run `pyupgrade` on the codebase. \ No newline at end of file diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index b43d858f59..c3d4992518 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -395,10 +395,8 @@ def start_listening(self): elif listener.type == "metrics": if not self.config.enable_metrics: logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) + "Metrics listener configured, but " + "enable_metrics is not True!" ) else: _base.listen_metrics(listener.bind_addresses, listener.port) diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 7af56ac136..920b34d97b 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -305,10 +305,8 @@ def start_listening(self): elif listener.type == "metrics": if not self.config.enable_metrics: logger.warning( - ( - "Metrics listener configured, but " - "enable_metrics is not True!" - ) + "Metrics listener configured, but " + "enable_metrics is not True!" ) else: _base.listen_metrics(listener.bind_addresses, listener.port) diff --git a/synapse/config/appservice.py b/synapse/config/appservice.py index a39d457c56..1ebea88db2 100644 --- a/synapse/config/appservice.py +++ b/synapse/config/appservice.py @@ -64,7 +64,7 @@ def load_appservices(hostname, config_files): for config_file in config_files: try: - with open(config_file, "r") as f: + with open(config_file) as f: appservice = _load_appservice(hostname, yaml.safe_load(f), config_file) if appservice.id in seen_ids: raise ConfigError( diff --git a/synapse/config/tls.py b/synapse/config/tls.py index fed05ac7be..5679f05e42 100644 --- a/synapse/config/tls.py +++ b/synapse/config/tls.py @@ -66,10 +66,8 @@ def read_config(self, config: dict, config_dir_path: str, **kwargs): if self.federation_client_minimum_tls_version == "1.3": if getattr(SSL, "OP_NO_TLSv1_3", None) is None: raise ConfigError( - ( - "federation_client_minimum_tls_version cannot be 1.3, " - "your OpenSSL does not support it" - ) + "federation_client_minimum_tls_version cannot be 1.3, " + "your OpenSSL does not support it" ) # Whitelist of domains to not verify certificates for diff --git a/synapse/handlers/cas.py b/synapse/handlers/cas.py index b681d208bc..0325f86e20 100644 --- a/synapse/handlers/cas.py +++ b/synapse/handlers/cas.py @@ -40,7 +40,7 @@ def __init__(self, error, error_description=None): def __str__(self): if self.error_description: - return "{}: {}".format(self.error, self.error_description) + return f"{self.error}: {self.error_description}" return self.error diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 5c4463583e..cf389be3e4 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -735,7 +735,7 @@ async def _get_state_after_missing_prev_event( # we need to make sure we re-load from the database to get the rejected # state correct. fetched_events.update( - (await self.store.get_events(missing_desired_events, allow_rejected=True)) + await self.store.get_events(missing_desired_events, allow_rejected=True) ) # check for events which were in the wrong room. diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 33d16fbf9c..0961dec5ab 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -302,7 +302,7 @@ async def try_unbind_threepid_with_id_server( ) url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,) - url_bytes = "/_matrix/identity/api/v1/3pid/unbind".encode("ascii") + url_bytes = b"/_matrix/identity/api/v1/3pid/unbind" content = { "mxid": mxid, @@ -695,7 +695,7 @@ async def _lookup_3pid_v1( return data["mxid"] except RequestTimedOutError: raise SynapseError(500, "Timed out contacting identity server") - except IOError as e: + except OSError as e: logger.warning("Error from v1 identity server lookup: %s" % (e,)) return None diff --git a/synapse/handlers/oidc.py b/synapse/handlers/oidc.py index a330c48fa7..eca8f16040 100644 --- a/synapse/handlers/oidc.py +++ b/synapse/handlers/oidc.py @@ -72,26 +72,26 @@ (b"oidc_session_no_samesite", b"HttpOnly"), ] + #: A token exchanged from the token endpoint, as per RFC6749 sec 5.1. and #: OpenID.Core sec 3.1.3.3. -Token = TypedDict( - "Token", - { - "access_token": str, - "token_type": str, - "id_token": Optional[str], - "refresh_token": Optional[str], - "expires_in": int, - "scope": Optional[str], - }, -) +class Token(TypedDict): + access_token: str + token_type: str + id_token: Optional[str] + refresh_token: Optional[str] + expires_in: int + scope: Optional[str] + #: A JWK, as per RFC7517 sec 4. The type could be more precise than that, but #: there is no real point of doing this in our case. JWK = Dict[str, str] + #: A JWK Set, as per RFC7517 sec 5. -JWKS = TypedDict("JWKS", {"keys": List[JWK]}) +class JWKS(TypedDict): + keys: List[JWK] class OidcHandler: @@ -255,7 +255,7 @@ def __init__(self, error, error_description=None): def __str__(self): if self.error_description: - return "{}: {}".format(self.error, self.error_description) + return f"{self.error}: {self.error_description}" return self.error @@ -639,7 +639,7 @@ async def _exchange_code(self, code: str) -> Token: ) logger.warning(description) # Body was still valid JSON. Might be useful to log it for debugging. - logger.warning("Code exchange response: {resp!r}".format(resp=resp)) + logger.warning("Code exchange response: %r", resp) raise OidcError("server_error", description) return resp @@ -1217,10 +1217,12 @@ class OidcSessionData: ui_auth_session_id = attr.ib(type=str) -UserAttributeDict = TypedDict( - "UserAttributeDict", - {"localpart": Optional[str], "display_name": Optional[str], "emails": List[str]}, -) +class UserAttributeDict(TypedDict): + localpart: Optional[str] + display_name: Optional[str] + emails: List[str] + + C = TypeVar("C") diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 056fe5e89f..8cf614136e 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -55,15 +55,12 @@ ["guest", "auth_provider"], ) -LoginDict = TypedDict( - "LoginDict", - { - "device_id": str, - "access_token": str, - "valid_until_ms": Optional[int], - "refresh_token": Optional[str], - }, -) + +class LoginDict(TypedDict): + device_id: str + access_token: str + valid_until_ms: Optional[int] + refresh_token: Optional[str] class RegistrationHandler(BaseHandler): diff --git a/synapse/handlers/saml.py b/synapse/handlers/saml.py index 72f54c9403..e6e71e9729 100644 --- a/synapse/handlers/saml.py +++ b/synapse/handlers/saml.py @@ -372,7 +372,7 @@ def expire_sessions(self): DOT_REPLACE_PATTERN = re.compile( - ("[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),)) + "[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),) ) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 722c4ae670..150a4f291e 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1601,7 +1601,7 @@ async def _get_rooms_changed( logger.debug( "Membership changes in %s: [%s]", room_id, - ", ".join(("%s (%s)" % (e.event_id, e.membership) for e in events)), + ", ".join("%s (%s)" % (e.event_id, e.membership) for e in events), ) non_joins = [e for e in events if e.membership != Membership.JOIN] diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py index 7a6a1717de..f7193e60bd 100644 --- a/synapse/http/proxyagent.py +++ b/synapse/http/proxyagent.py @@ -172,7 +172,7 @@ def request(self, method, uri, headers=None, bodyProducer=None): """ uri = uri.strip() if not _VALID_URI.match(uri): - raise ValueError("Invalid URI {!r}".format(uri)) + raise ValueError(f"Invalid URI {uri!r}") parsed_uri = URI.fromBytes(uri) pool_key = (parsed_uri.scheme, parsed_uri.host, parsed_uri.port) diff --git a/synapse/http/site.py b/synapse/http/site.py index 3b0a38124e..190084e8aa 100644 --- a/synapse/http/site.py +++ b/synapse/http/site.py @@ -384,7 +384,7 @@ def _finished_processing(self): # authenticated (e.g. and admin is puppetting a user) then we log both. requester, authenticated_entity = self.get_authenticated_entity() if authenticated_entity: - requester = "{}.{}".format(authenticated_entity, requester) + requester = f"{authenticated_entity}.{requester}" self.site.access_logger.log( log_level, diff --git a/synapse/logging/opentracing.py b/synapse/logging/opentracing.py index 185844f188..ecd51f1b4a 100644 --- a/synapse/logging/opentracing.py +++ b/synapse/logging/opentracing.py @@ -374,7 +374,7 @@ def init_tracer(hs: "HomeServer"): config = JaegerConfig( config=hs.config.jaeger_config, - service_name="{} {}".format(hs.config.server_name, hs.get_instance_name()), + service_name=f"{hs.config.server_name} {hs.get_instance_name()}", scope_manager=LogContextScopeManager(hs.config), metrics_factory=PrometheusMetricsFactory(), ) diff --git a/synapse/metrics/_exposition.py b/synapse/metrics/_exposition.py index 7e49d0d02c..bb9bcb5592 100644 --- a/synapse/metrics/_exposition.py +++ b/synapse/metrics/_exposition.py @@ -34,7 +34,7 @@ from synapse.util import caches -CONTENT_TYPE_LATEST = str("text/plain; version=0.0.4; charset=utf-8") +CONTENT_TYPE_LATEST = "text/plain; version=0.0.4; charset=utf-8" INF = float("inf") @@ -55,8 +55,8 @@ def floatToGoString(d): # Go switches to exponents sooner than Python. # We only need to care about positive values for le/quantile. if d > 0 and dot > 6: - mantissa = "{0}.{1}{2}".format(s[0], s[1:dot], s[dot + 1 :]).rstrip("0.") - return "{0}e+0{1}".format(mantissa, dot - 1) + mantissa = f"{s[0]}.{s[1:dot]}{s[dot + 1 :]}".rstrip("0.") + return f"{mantissa}e+0{dot - 1}" return s @@ -65,7 +65,7 @@ def sample_line(line, name): labelstr = "{{{0}}}".format( ",".join( [ - '{0}="{1}"'.format( + '{}="{}"'.format( k, v.replace("\\", r"\\").replace("\n", r"\n").replace('"', r"\""), ) @@ -78,10 +78,8 @@ def sample_line(line, name): timestamp = "" if line.timestamp is not None: # Convert to milliseconds. - timestamp = " {0:d}".format(int(float(line.timestamp) * 1000)) - return "{0}{1} {2}{3}\n".format( - name, labelstr, floatToGoString(line.value), timestamp - ) + timestamp = f" {int(float(line.timestamp) * 1000):d}" + return "{}{} {}{}\n".format(name, labelstr, floatToGoString(line.value), timestamp) def generate_latest(registry, emit_help=False): @@ -118,12 +116,12 @@ def generate_latest(registry, emit_help=False): # Output in the old format for compatibility. if emit_help: output.append( - "# HELP {0} {1}\n".format( + "# HELP {} {}\n".format( mname, metric.documentation.replace("\\", r"\\").replace("\n", r"\n"), ) ) - output.append("# TYPE {0} {1}\n".format(mname, mtype)) + output.append(f"# TYPE {mname} {mtype}\n") om_samples: Dict[str, List[str]] = {} for s in metric.samples: @@ -143,13 +141,13 @@ def generate_latest(registry, emit_help=False): for suffix, lines in sorted(om_samples.items()): if emit_help: output.append( - "# HELP {0}{1} {2}\n".format( + "# HELP {}{} {}\n".format( metric.name, suffix, metric.documentation.replace("\\", r"\\").replace("\n", r"\n"), ) ) - output.append("# TYPE {0}{1} gauge\n".format(metric.name, suffix)) + output.append(f"# TYPE {metric.name}{suffix} gauge\n") output.extend(lines) # Get rid of the weird colon things while we're at it @@ -163,12 +161,12 @@ def generate_latest(registry, emit_help=False): # Also output in the new format, if it's different. if emit_help: output.append( - "# HELP {0} {1}\n".format( + "# HELP {} {}\n".format( mnewname, metric.documentation.replace("\\", r"\\").replace("\n", r"\n"), ) ) - output.append("# TYPE {0} {1}\n".format(mnewname, mtype)) + output.append(f"# TYPE {mnewname} {mtype}\n") for s in metric.samples: # Get rid of the OpenMetrics specific samples (we should already have diff --git a/synapse/metrics/background_process_metrics.py b/synapse/metrics/background_process_metrics.py index 4455fa71a8..3a14260752 100644 --- a/synapse/metrics/background_process_metrics.py +++ b/synapse/metrics/background_process_metrics.py @@ -137,8 +137,7 @@ def collect(self): _background_process_db_txn_duration, _background_process_db_sched_duration, ): - for r in m.collect(): - yield r + yield from m.collect() REGISTRY.register(_Collector()) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 99d02cb355..11567bf32c 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -44,19 +44,14 @@ logger = logging.getLogger(__name__) -LoginResponse = TypedDict( - "LoginResponse", - { - "user_id": str, - "access_token": str, - "home_server": str, - "expires_in_ms": Optional[int], - "refresh_token": Optional[str], - "device_id": str, - "well_known": Optional[Dict[str, Any]], - }, - total=False, -) +class LoginResponse(TypedDict, total=False): + user_id: str + access_token: str + home_server: str + expires_in_ms: Optional[int] + refresh_token: Optional[str] + device_id: str + well_known: Optional[Dict[str, Any]] class LoginRestServlet(RestServlet): @@ -150,9 +145,7 @@ def on_GET(self, request: SynapseRequest): # login flow types returned. flows.append({"type": LoginRestServlet.TOKEN_TYPE}) - flows.extend( - ({"type": t} for t in self.auth_handler.get_supported_login_types()) - ) + flows.extend({"type": t} for t in self.auth_handler.get_supported_login_types()) flows.append({"type": LoginRestServlet.APPSERVICE_TYPE}) diff --git a/synapse/rest/media/v1/__init__.py b/synapse/rest/media/v1/__init__.py index d20186bbd0..3dd16d4bb5 100644 --- a/synapse/rest/media/v1/__init__.py +++ b/synapse/rest/media/v1/__init__.py @@ -17,7 +17,7 @@ # check for JPEG support. try: PIL.Image._getdecoder("rgb", "jpeg", None) -except IOError as e: +except OSError as e: if str(e).startswith("decoder jpeg not available"): raise Exception( "FATAL: jpeg codec not supported. Install pillow correctly! " @@ -32,7 +32,7 @@ # check for PNG support. try: PIL.Image._getdecoder("rgb", "zip", None) -except IOError as e: +except OSError as e: if str(e).startswith("decoder zip not available"): raise Exception( "FATAL: zip codec not supported. Install pillow correctly! " diff --git a/synapse/storage/database.py b/synapse/storage/database.py index f80d822c12..ccf9ac51ef 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -907,7 +907,7 @@ def simple_insert_many_txn( # The sort is to ensure that we don't rely on dictionary iteration # order. keys, vals = zip( - *[zip(*(sorted(i.items(), key=lambda kv: kv[0]))) for i in values if i] + *(zip(*(sorted(i.items(), key=lambda kv: kv[0]))) for i in values if i) ) for k in keys: diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py index 50e7ddd735..c55508867d 100644 --- a/synapse/storage/databases/main/deviceinbox.py +++ b/synapse/storage/databases/main/deviceinbox.py @@ -203,9 +203,7 @@ def delete_messages_for_device_txn(txn): "delete_messages_for_device", delete_messages_for_device_txn ) - log_kv( - {"message": "deleted {} messages for device".format(count), "count": count} - ) + log_kv({"message": f"deleted {count} messages for device", "count": count}) # Update the cache, ensuring that we only ever increase the value last_deleted_stream_id = self._last_device_delete_cache.get( diff --git a/synapse/storage/databases/main/group_server.py b/synapse/storage/databases/main/group_server.py index 66ad363bfb..e70d3649ff 100644 --- a/synapse/storage/databases/main/group_server.py +++ b/synapse/storage/databases/main/group_server.py @@ -27,8 +27,11 @@ _DEFAULT_CATEGORY_ID = "" _DEFAULT_ROLE_ID = "" + # A room in a group. -_RoomInGroup = TypedDict("_RoomInGroup", {"room_id": str, "is_public": bool}) +class _RoomInGroup(TypedDict): + room_id: str + is_public: bool class GroupServerWorkerStore(SQLBaseStore): @@ -92,6 +95,7 @@ async def get_rooms_in_group( "is_public": False # Whether this is a public room or not } """ + # TODO: Pagination def _get_rooms_in_group_txn(txn): diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 4d82c4c26d..68f1b40ea6 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -649,7 +649,7 @@ async def _get_joined_users_from_context( event_to_memberships = await self._get_joined_profiles_from_event_ids( missing_member_event_ids ) - users_in_room.update((row for row in event_to_memberships.values() if row)) + users_in_room.update(row for row in event_to_memberships.values() if row) if event is not None and event.type == EventTypes.Member: if event.membership == Membership.JOIN: diff --git a/synapse/storage/prepare_database.py b/synapse/storage/prepare_database.py index 82a7686df0..61392b9639 100644 --- a/synapse/storage/prepare_database.py +++ b/synapse/storage/prepare_database.py @@ -639,7 +639,7 @@ def get_statements(f: Iterable[str]) -> Generator[str, None, None]: def executescript(txn: Cursor, schema_path: str) -> None: - with open(schema_path, "r") as f: + with open(schema_path) as f: execute_statements_from_stream(txn, f) diff --git a/synapse/types.py b/synapse/types.py index fad23c8700..429bb013d2 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -577,10 +577,10 @@ async def to_string(self, store: "DataStore") -> str: entries = [] for name, pos in self.instance_map.items(): instance_id = await store.get_id_for_instance(name) - entries.append("{}.{}".format(instance_id, pos)) + entries.append(f"{instance_id}.{pos}") encoded_map = "~".join(entries) - return "m{}~{}".format(self.stream, encoded_map) + return f"m{self.stream}~{encoded_map}" else: return "s%d" % (self.stream,) diff --git a/synapse/util/caches/lrucache.py b/synapse/util/caches/lrucache.py index efeba0cb96..5c65d187b6 100644 --- a/synapse/util/caches/lrucache.py +++ b/synapse/util/caches/lrucache.py @@ -90,8 +90,7 @@ def enumerate_leaves(node, depth): yield node else: for n in node.values(): - for m in enumerate_leaves(n, depth - 1): - yield m + yield from enumerate_leaves(n, depth - 1) P = TypeVar("P") diff --git a/synapse/util/caches/treecache.py b/synapse/util/caches/treecache.py index a6df81ebff..4138931e7b 100644 --- a/synapse/util/caches/treecache.py +++ b/synapse/util/caches/treecache.py @@ -138,7 +138,6 @@ def iterate_tree_cache_entry(d): """ if isinstance(d, TreeCacheNode): for value_d in d.values(): - for value in iterate_tree_cache_entry(value_d): - yield value + yield from iterate_tree_cache_entry(value_d) else: yield d diff --git a/synapse/util/daemonize.py b/synapse/util/daemonize.py index 31b24dd188..d8532411c2 100644 --- a/synapse/util/daemonize.py +++ b/synapse/util/daemonize.py @@ -31,13 +31,13 @@ def daemonize_process(pid_file: str, logger: logging.Logger, chdir: str = "/") - # If pidfile already exists, we should read pid from there; to overwrite it, if # locking will fail, because locking attempt somehow purges the file contents. if os.path.isfile(pid_file): - with open(pid_file, "r") as pid_fh: + with open(pid_file) as pid_fh: old_pid = pid_fh.read() # Create a lockfile so that only one instance of this daemon is running at any time. try: lock_fh = open(pid_file, "w") - except IOError: + except OSError: print("Unable to create the pidfile.") sys.exit(1) @@ -45,7 +45,7 @@ def daemonize_process(pid_file: str, logger: logging.Logger, chdir: str = "/") - # Try to get an exclusive lock on the file. This will fail if another process # has the file locked. fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: + except OSError: print("Unable to lock on the pidfile.") # We need to overwrite the pidfile if we got here. # @@ -113,7 +113,7 @@ def excepthook(type_, value, traceback): try: lock_fh.write("%s" % (os.getpid())) lock_fh.flush() - except IOError: + except OSError: logger.error("Unable to write pid to the pidfile.") print("Unable to write pid to the pidfile.") sys.exit(1) diff --git a/synapse/visibility.py b/synapse/visibility.py index 1dc6b90275..17532059e9 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -96,7 +96,7 @@ async def filter_events_for_client( if isinstance(ignored_users_dict, dict): ignore_list = frozenset(ignored_users_dict.keys()) - erased_senders = await storage.main.are_users_erased((e.sender for e in events)) + erased_senders = await storage.main.are_users_erased(e.sender for e in events) if filter_send_to_client: room_ids = {e.room_id for e in events} @@ -353,7 +353,7 @@ def check_event_is_visible(event: EventBase, state: StateMap[EventBase]) -> bool ) if not check_history_visibility_only: - erased_senders = await storage.main.are_users_erased((e.sender for e in events)) + erased_senders = await storage.main.are_users_erased(e.sender for e in events) else: # We don't want to check whether users are erased, which is equivalent # to no users having been erased. From 4e340412c020f685cb402a735b983f6e332e206b Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Mon, 19 Jul 2021 16:11:34 +0100 Subject: [PATCH 426/619] Add a new version of the R30 phone-home metric, which removes a false impression of retention given by the old R30 metric (#10332) Signed-off-by: Olivier Wilkinson (reivilibre) --- changelog.d/10332.feature | 1 + synapse/app/phone_stats_home.py | 4 + synapse/storage/databases/main/metrics.py | 129 ++++++++++++ tests/app/test_phone_stats_home.py | 242 ++++++++++++++++++++++ tests/rest/client/v1/utils.py | 30 ++- tests/unittest.py | 15 +- 6 files changed, 416 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10332.feature diff --git a/changelog.d/10332.feature b/changelog.d/10332.feature new file mode 100644 index 0000000000..091947ff22 --- /dev/null +++ b/changelog.d/10332.feature @@ -0,0 +1 @@ +Add a new version of the R30 phone-home metric, which removes a false impression of retention given by the old R30 metric. diff --git a/synapse/app/phone_stats_home.py b/synapse/app/phone_stats_home.py index 8f86cecb76..7904c246df 100644 --- a/synapse/app/phone_stats_home.py +++ b/synapse/app/phone_stats_home.py @@ -107,6 +107,10 @@ async def phone_stats_home(hs, stats, stats_process=_stats_process): for name, count in r30_results.items(): stats["r30_users_" + name] = count + r30v2_results = await hs.get_datastore().count_r30_users() + for name, count in r30v2_results.items(): + stats["r30v2_users_" + name] = count + stats["cache_factor"] = hs.config.caches.global_factor stats["event_cache_size"] = hs.config.caches.event_cache_size diff --git a/synapse/storage/databases/main/metrics.py b/synapse/storage/databases/main/metrics.py index e3a544d9b2..dc0bbc56ac 100644 --- a/synapse/storage/databases/main/metrics.py +++ b/synapse/storage/databases/main/metrics.py @@ -316,6 +316,135 @@ def _count_r30_users(txn): return await self.db_pool.runInteraction("count_r30_users", _count_r30_users) + async def count_r30v2_users(self) -> Dict[str, int]: + """ + Counts the number of 30 day retained users, defined as users that: + - Appear more than once in the past 60 days + - Have more than 30 days between the most and least recent appearances that + occurred in the past 60 days. + + (This is the second version of this metric, hence R30'v2') + + Returns: + A mapping from client type to the number of 30-day retained users for that client. + + The dict keys are: + - "all" (a combined number of users across any and all clients) + - "android" (Element Android) + - "ios" (Element iOS) + - "electron" (Element Desktop) + - "web" (any web application -- it's not possible to distinguish Element Web here) + """ + + def _count_r30v2_users(txn): + thirty_days_in_secs = 86400 * 30 + now = int(self._clock.time()) + sixty_days_ago_in_secs = now - 2 * thirty_days_in_secs + one_day_from_now_in_secs = now + 86400 + + # This is the 'per-platform' count. + sql = """ + SELECT + client_type, + count(client_type) + FROM + ( + SELECT + user_id, + CASE + WHEN + LOWER(user_agent) LIKE '%%riot%%' OR + LOWER(user_agent) LIKE '%%element%%' + THEN CASE + WHEN + LOWER(user_agent) LIKE '%%electron%%' + THEN 'electron' + WHEN + LOWER(user_agent) LIKE '%%android%%' + THEN 'android' + WHEN + LOWER(user_agent) LIKE '%%ios%%' + THEN 'ios' + ELSE 'unknown' + END + WHEN + LOWER(user_agent) LIKE '%%mozilla%%' OR + LOWER(user_agent) LIKE '%%gecko%%' + THEN 'web' + ELSE 'unknown' + END as client_type + FROM + user_daily_visits + WHERE + timestamp > ? + AND + timestamp < ? + GROUP BY + user_id, + client_type + HAVING + max(timestamp) - min(timestamp) > ? + ) AS temp + GROUP BY + client_type + ; + """ + + # We initialise all the client types to zero, so we get an explicit + # zero if they don't appear in the query results + results = {"ios": 0, "android": 0, "web": 0, "electron": 0} + txn.execute( + sql, + ( + sixty_days_ago_in_secs * 1000, + one_day_from_now_in_secs * 1000, + thirty_days_in_secs * 1000, + ), + ) + + for row in txn: + if row[0] == "unknown": + continue + results[row[0]] = row[1] + + # This is the 'all users' count. + sql = """ + SELECT COUNT(*) FROM ( + SELECT + 1 + FROM + user_daily_visits + WHERE + timestamp > ? + AND + timestamp < ? + GROUP BY + user_id + HAVING + max(timestamp) - min(timestamp) > ? + ) AS r30_users + """ + + txn.execute( + sql, + ( + sixty_days_ago_in_secs * 1000, + one_day_from_now_in_secs * 1000, + thirty_days_in_secs * 1000, + ), + ) + row = txn.fetchone() + if row is None: + results["all"] = 0 + else: + results["all"] = row[0] + + return results + + return await self.db_pool.runInteraction( + "count_r30v2_users", _count_r30v2_users + ) + def _get_start_of_day(self): """ Returns millisecond unixtime for start of UTC day. diff --git a/tests/app/test_phone_stats_home.py b/tests/app/test_phone_stats_home.py index 2da6ba4dde..5527e278db 100644 --- a/tests/app/test_phone_stats_home.py +++ b/tests/app/test_phone_stats_home.py @@ -1,9 +1,11 @@ import synapse +from synapse.app.phone_stats_home import start_phone_stats_home from synapse.rest.client.v1 import login, room from tests import unittest from tests.unittest import HomeserverTestCase +FIVE_MINUTES_IN_SECONDS = 300 ONE_DAY_IN_SECONDS = 86400 @@ -151,3 +153,243 @@ def test_r30_user_must_be_retained_for_at_least_a_month(self): # *Now* the user appears in R30. r30_results = self.get_success(self.hs.get_datastore().count_r30_users()) self.assertEqual(r30_results, {"all": 1, "unknown": 1}) + + +class PhoneHomeR30V2TestCase(HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + ] + + def _advance_to(self, desired_time_secs: float): + now = self.hs.get_clock().time() + assert now < desired_time_secs + self.reactor.advance(desired_time_secs - now) + + def make_homeserver(self, reactor, clock): + hs = super(PhoneHomeR30V2TestCase, self).make_homeserver(reactor, clock) + + # We don't want our tests to actually report statistics, so check + # that it's not enabled + assert not hs.config.report_stats + + # This starts the needed data collection that we rely on to calculate + # R30v2 metrics. + start_phone_stats_home(hs) + return hs + + def test_r30v2_minimum_usage(self): + """ + Tests the minimum amount of interaction necessary for the R30v2 metric + to consider a user 'retained'. + """ + + # Register a user, log it in, create a room and send a message + user_id = self.register_user("u1", "secret!") + access_token = self.login("u1", "secret!") + room_id = self.helper.create_room_as(room_creator=user_id, tok=access_token) + self.helper.send(room_id, "message", tok=access_token) + first_post_at = self.hs.get_clock().time() + + # Give time for user_daily_visits table to be updated. + # (user_daily_visits is updated every 5 minutes using a looping call.) + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + store = self.hs.get_datastore() + + # Check the R30 results do not count that user. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + # Advance 31 days. + # (R30v2 includes users with **more** than 30 days between the two visits, + # and user_daily_visits records the timestamp as the start of the day.) + self.reactor.advance(31 * ONE_DAY_IN_SECONDS) + # Also advance 5 minutes to let another user_daily_visits update occur + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + # (Make sure the user isn't somehow counted by this point.) + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + # Send a message (this counts as activity) + self.helper.send(room_id, "message2", tok=access_token) + + # We have to wait a few minutes for the user_daily_visits table to + # be updated by a background process. + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + # *Now* the user is counted. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 1, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + # Advance to JUST under 60 days after the user's first post + self._advance_to(first_post_at + 60 * ONE_DAY_IN_SECONDS - 5) + + # Check the user is still counted. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 1, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + # Advance into the next day. The user's first activity is now more than 60 days old. + self._advance_to(first_post_at + 60 * ONE_DAY_IN_SECONDS + 5) + + # Check the user is now no longer counted in R30. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + def test_r30v2_user_must_be_retained_for_at_least_a_month(self): + """ + Tests that a newly-registered user must be retained for a whole month + before appearing in the R30v2 statistic, even if they post every day + during that time! + """ + + # set a custom user-agent to impersonate Element/Android. + headers = ( + ( + "User-Agent", + "Element/1.1 (Linux; U; Android 9; MatrixAndroidSDK_X 0.0.1)", + ), + ) + + # Register a user and send a message + user_id = self.register_user("u1", "secret!") + access_token = self.login("u1", "secret!", custom_headers=headers) + room_id = self.helper.create_room_as( + room_creator=user_id, tok=access_token, custom_headers=headers + ) + self.helper.send(room_id, "message", tok=access_token, custom_headers=headers) + + # Give time for user_daily_visits table to be updated. + # (user_daily_visits is updated every 5 minutes using a looping call.) + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + store = self.hs.get_datastore() + + # Check the user does not contribute to R30 yet. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + for _ in range(30): + # This loop posts a message every day for 30 days + self.reactor.advance(ONE_DAY_IN_SECONDS - FIVE_MINUTES_IN_SECONDS) + self.helper.send( + room_id, "I'm still here", tok=access_token, custom_headers=headers + ) + + # give time for user_daily_visits to update + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + # Notice that the user *still* does not contribute to R30! + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + # advance yet another day with more activity + self.reactor.advance(ONE_DAY_IN_SECONDS) + self.helper.send( + room_id, "Still here!", tok=access_token, custom_headers=headers + ) + + # give time for user_daily_visits to update + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + # *Now* the user appears in R30. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 1, "android": 1, "electron": 0, "ios": 0, "web": 0} + ) + + def test_r30v2_returning_dormant_users_not_counted(self): + """ + Tests that dormant users (users inactive for a long time) do not + contribute to R30v2 when they return for just a single day. + This is a key difference between R30 and R30v2. + """ + + # set a custom user-agent to impersonate Element/iOS. + headers = ( + ( + "User-Agent", + "Riot/1.4 (iPhone; iOS 13; Scale/4.00)", + ), + ) + + # Register a user and send a message + user_id = self.register_user("u1", "secret!") + access_token = self.login("u1", "secret!", custom_headers=headers) + room_id = self.helper.create_room_as( + room_creator=user_id, tok=access_token, custom_headers=headers + ) + self.helper.send(room_id, "message", tok=access_token, custom_headers=headers) + + # the user goes inactive for 2 months + self.reactor.advance(60 * ONE_DAY_IN_SECONDS) + + # the user returns for one day, perhaps just to check out a new feature + self.helper.send(room_id, "message", tok=access_token, custom_headers=headers) + + # Give time for user_daily_visits table to be updated. + # (user_daily_visits is updated every 5 minutes using a looping call.) + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + store = self.hs.get_datastore() + + # Check that the user does not contribute to R30v2, even though it's been + # more than 30 days since registration. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) + + # Check that this is a situation where old R30 differs: + # old R30 DOES count this as 'retained'. + r30_results = self.get_success(store.count_r30_users()) + self.assertEqual(r30_results, {"all": 1, "ios": 1}) + + # Now we want to check that the user will still be able to appear in + # R30v2 as long as the user performs some other activity between + # 30 and 60 days later. + self.reactor.advance(32 * ONE_DAY_IN_SECONDS) + self.helper.send(room_id, "message", tok=access_token, custom_headers=headers) + + # (give time for tables to update) + self.reactor.advance(FIVE_MINUTES_IN_SECONDS) + + # Check the user now satisfies the requirements to appear in R30v2. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 1, "ios": 1, "android": 0, "electron": 0, "web": 0} + ) + + # Advance to 59.5 days after the user's first R30v2-eligible activity. + self.reactor.advance(27.5 * ONE_DAY_IN_SECONDS) + + # Check the user still appears in R30v2. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 1, "ios": 1, "android": 0, "electron": 0, "web": 0} + ) + + # Advance to 60.5 days after the user's first R30v2-eligible activity. + self.reactor.advance(ONE_DAY_IN_SECONDS) + + # Check the user no longer appears in R30v2. + r30_results = self.get_success(store.count_r30v2_users()) + self.assertEqual( + r30_results, {"all": 0, "android": 0, "electron": 0, "ios": 0, "web": 0} + ) diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index 69798e95c3..fc2d35596e 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -19,7 +19,7 @@ import re import time import urllib.parse -from typing import Any, Dict, Mapping, MutableMapping, Optional +from typing import Any, Dict, Iterable, Mapping, MutableMapping, Optional, Tuple, Union from unittest.mock import patch import attr @@ -53,6 +53,9 @@ def create_room_as( tok: str = None, expect_code: int = 200, extra_content: Optional[Dict] = None, + custom_headers: Optional[ + Iterable[Tuple[Union[bytes, str], Union[bytes, str]]] + ] = None, ) -> str: """ Create a room. @@ -87,6 +90,7 @@ def create_room_as( "POST", path, json.dumps(content).encode("utf8"), + custom_headers=custom_headers, ) assert channel.result["code"] == b"%d" % expect_code, channel.result @@ -175,14 +179,30 @@ def change_membership( self.auth_user_id = temp_id - def send(self, room_id, body=None, txn_id=None, tok=None, expect_code=200): + def send( + self, + room_id, + body=None, + txn_id=None, + tok=None, + expect_code=200, + custom_headers: Optional[ + Iterable[Tuple[Union[bytes, str], Union[bytes, str]]] + ] = None, + ): if body is None: body = "body_text_here" content = {"msgtype": "m.text", "body": body} return self.send_event( - room_id, "m.room.message", content, txn_id, tok, expect_code + room_id, + "m.room.message", + content, + txn_id, + tok, + expect_code, + custom_headers=custom_headers, ) def send_event( @@ -193,6 +213,9 @@ def send_event( txn_id=None, tok=None, expect_code=200, + custom_headers: Optional[ + Iterable[Tuple[Union[bytes, str], Union[bytes, str]]] + ] = None, ): if txn_id is None: txn_id = "m%s" % (str(time.time())) @@ -207,6 +230,7 @@ def send_event( "PUT", path, json.dumps(content or {}).encode("utf8"), + custom_headers=custom_headers, ) assert ( diff --git a/tests/unittest.py b/tests/unittest.py index c6d9064423..3eec9c4d5b 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -594,7 +594,15 @@ def register_user( user_id = channel.json_body["user_id"] return user_id - def login(self, username, password, device_id=None): + def login( + self, + username, + password, + device_id=None, + custom_headers: Optional[ + Iterable[Tuple[Union[bytes, str], Union[bytes, str]]] + ] = None, + ): """ Log in a user, and get an access token. Requires the Login API be registered. @@ -605,7 +613,10 @@ def login(self, username, password, device_id=None): body["device_id"] = device_id channel = self.make_request( - "POST", "/_matrix/client/r0/login", json.dumps(body).encode("utf8") + "POST", + "/_matrix/client/r0/login", + json.dumps(body).encode("utf8"), + custom_headers=custom_headers, ) self.assertEqual(channel.code, 200, channel.result) From eebfd024e9f523572189418735c3f9e324bb8f2b Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Mon, 19 Jul 2021 19:31:17 +0100 Subject: [PATCH 427/619] Factorise `get_datastore` calls in phone_stats_home. (#10427) Follow-up to #10332. --- changelog.d/10427.feature | 1 + synapse/app/phone_stats_home.py | 34 +++++++++++++++++---------------- 2 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 changelog.d/10427.feature diff --git a/changelog.d/10427.feature b/changelog.d/10427.feature new file mode 100644 index 0000000000..091947ff22 --- /dev/null +++ b/changelog.d/10427.feature @@ -0,0 +1 @@ +Add a new version of the R30 phone-home metric, which removes a false impression of retention given by the old R30 metric. diff --git a/synapse/app/phone_stats_home.py b/synapse/app/phone_stats_home.py index 7904c246df..96defac1d2 100644 --- a/synapse/app/phone_stats_home.py +++ b/synapse/app/phone_stats_home.py @@ -71,6 +71,8 @@ async def phone_stats_home(hs, stats, stats_process=_stats_process): # General statistics # + store = hs.get_datastore() + stats["homeserver"] = hs.config.server_name stats["server_context"] = hs.config.server_context stats["timestamp"] = now @@ -79,35 +81,35 @@ async def phone_stats_home(hs, stats, stats_process=_stats_process): stats["python_version"] = "{}.{}.{}".format( version.major, version.minor, version.micro ) - stats["total_users"] = await hs.get_datastore().count_all_users() + stats["total_users"] = await store.count_all_users() - total_nonbridged_users = await hs.get_datastore().count_nonbridged_users() + total_nonbridged_users = await store.count_nonbridged_users() stats["total_nonbridged_users"] = total_nonbridged_users - daily_user_type_results = await hs.get_datastore().count_daily_user_type() + daily_user_type_results = await store.count_daily_user_type() for name, count in daily_user_type_results.items(): stats["daily_user_type_" + name] = count - room_count = await hs.get_datastore().get_room_count() + room_count = await store.get_room_count() stats["total_room_count"] = room_count - stats["daily_active_users"] = await hs.get_datastore().count_daily_users() - stats["monthly_active_users"] = await hs.get_datastore().count_monthly_users() - daily_active_e2ee_rooms = await hs.get_datastore().count_daily_active_e2ee_rooms() + stats["daily_active_users"] = await store.count_daily_users() + stats["monthly_active_users"] = await store.count_monthly_users() + daily_active_e2ee_rooms = await store.count_daily_active_e2ee_rooms() stats["daily_active_e2ee_rooms"] = daily_active_e2ee_rooms - stats["daily_e2ee_messages"] = await hs.get_datastore().count_daily_e2ee_messages() - daily_sent_e2ee_messages = await hs.get_datastore().count_daily_sent_e2ee_messages() + stats["daily_e2ee_messages"] = await store.count_daily_e2ee_messages() + daily_sent_e2ee_messages = await store.count_daily_sent_e2ee_messages() stats["daily_sent_e2ee_messages"] = daily_sent_e2ee_messages - stats["daily_active_rooms"] = await hs.get_datastore().count_daily_active_rooms() - stats["daily_messages"] = await hs.get_datastore().count_daily_messages() - daily_sent_messages = await hs.get_datastore().count_daily_sent_messages() + stats["daily_active_rooms"] = await store.count_daily_active_rooms() + stats["daily_messages"] = await store.count_daily_messages() + daily_sent_messages = await store.count_daily_sent_messages() stats["daily_sent_messages"] = daily_sent_messages - r30_results = await hs.get_datastore().count_r30_users() + r30_results = await store.count_r30_users() for name, count in r30_results.items(): stats["r30_users_" + name] = count - r30v2_results = await hs.get_datastore().count_r30_users() + r30v2_results = await store.count_r30_users() for name, count in r30v2_results.items(): stats["r30v2_users_" + name] = count @@ -119,8 +121,8 @@ async def phone_stats_home(hs, stats, stats_process=_stats_process): # # This only reports info about the *main* database. - stats["database_engine"] = hs.get_datastore().db_pool.engine.module.__name__ - stats["database_server_version"] = hs.get_datastore().db_pool.engine.server_version + stats["database_engine"] = store.db_pool.engine.module.__name__ + stats["database_server_version"] = store.db_pool.engine.server_version # # Logging configuration From f3ac9c6750524ebd142610bc499546955c22fd35 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 20 Jul 2021 11:35:23 +0100 Subject: [PATCH 428/619] Fix exception when failing to get remote room list (#10414) --- changelog.d/10414.bugfix | 1 + synapse/handlers/room_list.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10414.bugfix diff --git a/changelog.d/10414.bugfix b/changelog.d/10414.bugfix new file mode 100644 index 0000000000..bfebed8d29 --- /dev/null +++ b/changelog.d/10414.bugfix @@ -0,0 +1 @@ +Fix a number of logged errors caused by remote servers being down. diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index 6284bcdfbc..fae2c098e3 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -383,7 +383,11 @@ async def get_remote_public_room_list( ): logger.debug("Falling back to locally-filtered /publicRooms") else: - raise # Not an error that should trigger a fallback. + # Not an error that should trigger a fallback. + raise SynapseError(502, "Failed to fetch room list") + except RequestSendFailed: + # Not an error that should trigger a fallback. + raise SynapseError(502, "Failed to fetch room list") # if we reach this point, then we fall back to the situation where # we currently don't support searching across federation, so we have From a743bf46949e851c9a10d8e01a138659f3af2484 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 20 Jul 2021 12:39:46 +0200 Subject: [PATCH 429/619] Port the ThirdPartyEventRules module interface to the new generic interface (#10386) Port the third-party event rules interface to the generic module interface introduced in v1.37.0 --- changelog.d/10386.removal | 1 + docs/modules.md | 62 ++++- docs/sample_config.yaml | 13 -- docs/upgrade.md | 13 ++ synapse/app/_base.py | 2 + synapse/config/third_party_event_rules.py | 15 -- synapse/events/third_party_rules.py | 245 ++++++++++++++++---- synapse/handlers/federation.py | 4 +- synapse/handlers/message.py | 8 +- synapse/handlers/room.py | 10 +- synapse/module_api/__init__.py | 6 + tests/rest/client/test_third_party_rules.py | 132 +++++++++-- 12 files changed, 403 insertions(+), 108 deletions(-) create mode 100644 changelog.d/10386.removal diff --git a/changelog.d/10386.removal b/changelog.d/10386.removal new file mode 100644 index 0000000000..800a6143d7 --- /dev/null +++ b/changelog.d/10386.removal @@ -0,0 +1 @@ +The third-party event rules module interface is deprecated in favour of the generic module interface introduced in Synapse v1.37.0. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. diff --git a/docs/modules.md b/docs/modules.md index c4cb7018f7..9a430390a4 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -186,7 +186,7 @@ The arguments passed to this callback are: ```python async def check_media_file_for_spam( file_wrapper: "synapse.rest.media.v1.media_storage.ReadableFileWrapper", - file_info: "synapse.rest.media.v1._base.FileInfo" + file_info: "synapse.rest.media.v1._base.FileInfo", ) -> bool ``` @@ -223,6 +223,66 @@ Called after successfully registering a user, in case the module needs to perfor operations to keep track of them. (e.g. add them to a database table). The user is represented by their Matrix user ID. +#### Third party rules callbacks + +Third party rules callbacks allow module developers to add extra checks to verify the +validity of incoming events. Third party event rules callbacks can be registered using +the module API's `register_third_party_rules_callbacks` method. + +The available third party rules callbacks are: + +```python +async def check_event_allowed( + event: "synapse.events.EventBase", + state_events: "synapse.types.StateMap", +) -> Tuple[bool, Optional[dict]] +``` + +** +This callback is very experimental and can and will break without notice. Module developers +are encouraged to implement `check_event_for_spam` from the spam checker category instead. +** + +Called when processing any incoming event, with the event and a `StateMap` +representing the current state of the room the event is being sent into. A `StateMap` is +a dictionary that maps tuples containing an event type and a state key to the +corresponding state event. For example retrieving the room's `m.room.create` event from +the `state_events` argument would look like this: `state_events.get(("m.room.create", ""))`. +The module must return a boolean indicating whether the event can be allowed. + +Note that this callback function processes incoming events coming via federation +traffic (on top of client traffic). This means denying an event might cause the local +copy of the room's history to diverge from that of remote servers. This may cause +federation issues in the room. It is strongly recommended to only deny events using this +callback function if the sender is a local user, or in a private federation in which all +servers are using the same module, with the same configuration. + +If the boolean returned by the module is `True`, it may also tell Synapse to replace the +event with new data by returning the new event's data as a dictionary. In order to do +that, it is recommended the module calls `event.get_dict()` to get the current event as a +dictionary, and modify the returned dictionary accordingly. + +Note that replacing the event only works for events sent by local users, not for events +received over federation. + +```python +async def on_create_room( + requester: "synapse.types.Requester", + request_content: dict, + is_requester_admin: bool, +) -> None +``` + +Called when processing a room creation request, with the `Requester` object for the user +performing the request, a dictionary representing the room creation request's JSON body +(see [the spec](https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-createroom) +for a list of possible parameters), and a boolean indicating whether the user performing +the request is a server admin. + +Modules can modify the `request_content` (by e.g. adding events to its `initial_state`), +or deny the room's creation by raising a `module_api.errors.SynapseError`. + + ### Porting an existing module that uses the old interface In order to port a module that uses Synapse's old module interface, its author needs to: diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index f4845a5841..853c2f6899 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -2654,19 +2654,6 @@ stats: # action: allow -# Server admins can define a Python module that implements extra rules for -# allowing or denying incoming events. In order to work, this module needs to -# override the methods defined in synapse/events/third_party_rules.py. -# -# This feature is designed to be used in closed federations only, where each -# participating server enforces the same rules. -# -#third_party_event_rules: -# module: "my_custom_project.SuperRulesSet" -# config: -# example_option: 'things' - - ## Opentracing ## # These settings enable opentracing, which implements distributed tracing. diff --git a/docs/upgrade.md b/docs/upgrade.md index db0450f563..c8f4a2c171 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -86,6 +86,19 @@ process, for example: ``` +# Upgrading to v1.39.0 + +## Deprecation of the current third-party rules module interface + +The current third-party rules module interface is deprecated in favour of the new generic +modules system introduced in Synapse v1.37.0. Authors of third-party rules modules can refer +to [this documentation](modules.md#porting-an-existing-module-that-uses-the-old-interface) +to update their modules. Synapse administrators can refer to [this documentation](modules.md#using-modules) +to update their configuration once the modules they are using have been updated. + +We plan to remove support for the current third-party rules interface in September 2021. + + # Upgrading to v1.38.0 ## Re-indexing of `events` table on Postgres databases diff --git a/synapse/app/_base.py b/synapse/app/_base.py index b30571fe49..50a02f51f5 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -38,6 +38,7 @@ from synapse.config.homeserver import HomeServerConfig from synapse.crypto import context_factory from synapse.events.spamcheck import load_legacy_spam_checkers +from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.logging.context import PreserveLoggingContext from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.metrics.jemalloc import setup_jemalloc_stats @@ -368,6 +369,7 @@ def run_sighup(*args, **kwargs): module(config=config, api=module_api) load_legacy_spam_checkers(hs) + load_legacy_third_party_event_rules(hs) # If we've configured an expiry time for caches, start the background job now. setup_expire_lru_cache_entries(hs) diff --git a/synapse/config/third_party_event_rules.py b/synapse/config/third_party_event_rules.py index f502ff539e..a3fae02420 100644 --- a/synapse/config/third_party_event_rules.py +++ b/synapse/config/third_party_event_rules.py @@ -28,18 +28,3 @@ def read_config(self, config, **kwargs): self.third_party_event_rules = load_module( provider, ("third_party_event_rules",) ) - - def generate_config_section(self, **kwargs): - return """\ - # Server admins can define a Python module that implements extra rules for - # allowing or denying incoming events. In order to work, this module needs to - # override the methods defined in synapse/events/third_party_rules.py. - # - # This feature is designed to be used in closed federations only, where each - # participating server enforces the same rules. - # - #third_party_event_rules: - # module: "my_custom_project.SuperRulesSet" - # config: - # example_option: 'things' - """ diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index f7944fd834..7a6eb3e516 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -11,16 +11,124 @@ # 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, Awaitable, Callable, List, Optional, Tuple -from typing import TYPE_CHECKING, Union - +from synapse.api.errors import SynapseError from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import Requester, StateMap +from synapse.util.async_helpers import maybe_awaitable if TYPE_CHECKING: from synapse.server import HomeServer +logger = logging.getLogger(__name__) + + +CHECK_EVENT_ALLOWED_CALLBACK = Callable[ + [EventBase, StateMap[EventBase]], Awaitable[Tuple[bool, Optional[dict]]] +] +ON_CREATE_ROOM_CALLBACK = Callable[[Requester, dict, bool], Awaitable] +CHECK_THREEPID_CAN_BE_INVITED_CALLBACK = Callable[ + [str, str, StateMap[EventBase]], Awaitable[bool] +] +CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK = Callable[ + [str, StateMap[EventBase], str], Awaitable[bool] +] + + +def load_legacy_third_party_event_rules(hs: "HomeServer"): + """Wrapper that loads a third party event rules module configured using the old + configuration, and registers the hooks they implement. + """ + if hs.config.third_party_event_rules is None: + return + + module, config = hs.config.third_party_event_rules + + api = hs.get_module_api() + third_party_rules = module(config=config, module_api=api) + + # The known hooks. If a module implements a method which name appears in this set, + # we'll want to register it. + third_party_event_rules_methods = { + "check_event_allowed", + "on_create_room", + "check_threepid_can_be_invited", + "check_visibility_can_be_modified", + } + + def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]: + # f might be None if the callback isn't implemented by the module. In this + # case we don't want to register a callback at all so we return None. + if f is None: + return None + + # We return a separate wrapper for these methods because, in order to wrap them + # correctly, we need to await its result. Therefore it doesn't make a lot of + # sense to make it go through the run() wrapper. + if f.__name__ == "check_event_allowed": + + # We need to wrap check_event_allowed because its old form would return either + # a boolean or a dict, but now we want to return the dict separately from the + # boolean. + async def wrap_check_event_allowed( + event: EventBase, + state_events: StateMap[EventBase], + ) -> Tuple[bool, Optional[dict]]: + # We've already made sure f is not None above, but mypy doesn't do well + # across function boundaries so we need to tell it f is definitely not + # None. + assert f is not None + + res = await f(event, state_events) + if isinstance(res, dict): + return True, res + else: + return res, None + + return wrap_check_event_allowed + + if f.__name__ == "on_create_room": + + # We need to wrap on_create_room because its old form would return a boolean + # if the room creation is denied, but now we just want it to raise an + # exception. + async def wrap_on_create_room( + requester: Requester, config: dict, is_requester_admin: bool + ) -> None: + # We've already made sure f is not None above, but mypy doesn't do well + # across function boundaries so we need to tell it f is definitely not + # None. + assert f is not None + + res = await f(requester, config, is_requester_admin) + if res is False: + raise SynapseError( + 403, + "Room creation forbidden with these parameters", + ) + + return wrap_on_create_room + + def run(*args, **kwargs): + # mypy doesn't do well across function boundaries so we need to tell it + # f is definitely not None. + assert f is not None + + return maybe_awaitable(f(*args, **kwargs)) + + return run + + # Register the hooks through the module API. + hooks = { + hook: async_wrapper(getattr(third_party_rules, hook, None)) + for hook in third_party_event_rules_methods + } + + api.register_third_party_rules_callbacks(**hooks) + class ThirdPartyEventRules: """Allows server admins to provide a Python module implementing an extra @@ -35,36 +143,65 @@ def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() - module = None - config = None - if hs.config.third_party_event_rules: - module, config = hs.config.third_party_event_rules + self._check_event_allowed_callbacks: List[CHECK_EVENT_ALLOWED_CALLBACK] = [] + self._on_create_room_callbacks: List[ON_CREATE_ROOM_CALLBACK] = [] + self._check_threepid_can_be_invited_callbacks: List[ + CHECK_THREEPID_CAN_BE_INVITED_CALLBACK + ] = [] + self._check_visibility_can_be_modified_callbacks: List[ + CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK + ] = [] + + def register_third_party_rules_callbacks( + self, + check_event_allowed: Optional[CHECK_EVENT_ALLOWED_CALLBACK] = None, + on_create_room: Optional[ON_CREATE_ROOM_CALLBACK] = None, + check_threepid_can_be_invited: Optional[ + CHECK_THREEPID_CAN_BE_INVITED_CALLBACK + ] = None, + check_visibility_can_be_modified: Optional[ + CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK + ] = None, + ): + """Register callbacks from modules for each hook.""" + if check_event_allowed is not None: + self._check_event_allowed_callbacks.append(check_event_allowed) + + if on_create_room is not None: + self._on_create_room_callbacks.append(on_create_room) + + if check_threepid_can_be_invited is not None: + self._check_threepid_can_be_invited_callbacks.append( + check_threepid_can_be_invited, + ) - if module is not None: - self.third_party_rules = module( - config=config, - module_api=hs.get_module_api(), + if check_visibility_can_be_modified is not None: + self._check_visibility_can_be_modified_callbacks.append( + check_visibility_can_be_modified, ) async def check_event_allowed( self, event: EventBase, context: EventContext - ) -> Union[bool, dict]: + ) -> Tuple[bool, Optional[dict]]: """Check if a provided event should be allowed in the given context. The module can return: * True: the event is allowed. * False: the event is not allowed, and should be rejected with M_FORBIDDEN. - * a dict: replacement event data. + + If the event is allowed, the module can also return a dictionary to use as a + replacement for the event. Args: event: The event to be checked. context: The context of the event. Returns: - The result from the ThirdPartyRules module, as above + The result from the ThirdPartyRules module, as above. """ - if self.third_party_rules is None: - return True + # Bail out early without hitting the store if we don't have any callbacks to run. + if len(self._check_event_allowed_callbacks) == 0: + return True, None prev_state_ids = await context.get_prev_state_ids() @@ -77,29 +214,46 @@ async def check_event_allowed( # the hashes and signatures. event.freeze() - return await self.third_party_rules.check_event_allowed(event, state_events) + for callback in self._check_event_allowed_callbacks: + try: + res, replacement_data = await callback(event, state_events) + except Exception as e: + logger.warning("Failed to run module API callback %s: %s", callback, e) + continue + + # Return if the event shouldn't be allowed or if the module came up with a + # replacement dict for the event. + if res is False: + return res, None + elif isinstance(replacement_data, dict): + return True, replacement_data + + return True, None async def on_create_room( self, requester: Requester, config: dict, is_requester_admin: bool - ) -> bool: - """Intercept requests to create room to allow, deny or update the - request config. + ) -> None: + """Intercept requests to create room to maybe deny it (via an exception) or + update the request config. Args: requester config: The creation config from the client. is_requester_admin: If the requester is an admin - - Returns: - Whether room creation is allowed or denied. """ - - if self.third_party_rules is None: - return True - - return await self.third_party_rules.on_create_room( - requester, config, is_requester_admin - ) + for callback in self._on_create_room_callbacks: + try: + await callback(requester, config, is_requester_admin) + except Exception as e: + # Don't silence the errors raised by this callback since we expect it to + # raise an exception to deny the creation of the room; instead make sure + # it's a SynapseError we can send to clients. + if not isinstance(e, SynapseError): + e = SynapseError( + 403, "Room creation forbidden with these parameters" + ) + + raise e async def check_threepid_can_be_invited( self, medium: str, address: str, room_id: str @@ -114,15 +268,20 @@ async def check_threepid_can_be_invited( Returns: True if the 3PID can be invited, False if not. """ - - if self.third_party_rules is None: + # Bail out early without hitting the store if we don't have any callbacks to run. + if len(self._check_threepid_can_be_invited_callbacks) == 0: return True state_events = await self._get_state_map_for_room(room_id) - return await self.third_party_rules.check_threepid_can_be_invited( - medium, address, state_events - ) + for callback in self._check_threepid_can_be_invited_callbacks: + try: + if await callback(medium, address, state_events) is False: + return False + except Exception as e: + logger.warning("Failed to run module API callback %s: %s", callback, e) + + return True async def check_visibility_can_be_modified( self, room_id: str, new_visibility: str @@ -137,18 +296,20 @@ async def check_visibility_can_be_modified( Returns: True if the room's visibility can be modified, False if not. """ - if self.third_party_rules is None: - return True - - check_func = getattr( - self.third_party_rules, "check_visibility_can_be_modified", None - ) - if not check_func or not callable(check_func): + # Bail out early without hitting the store if we don't have any callback + if len(self._check_visibility_can_be_modified_callbacks) == 0: return True state_events = await self._get_state_map_for_room(room_id) - return await check_func(room_id, state_events, new_visibility) + for callback in self._check_visibility_can_be_modified_callbacks: + try: + if await callback(room_id, state_events, new_visibility) is False: + return False + except Exception as e: + logger.warning("Failed to run module API callback %s: %s", callback, e) + + return True async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]: """Given a room ID, return the state events of that room. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index cf389be3e4..5728719909 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1934,7 +1934,7 @@ async def on_make_knock_request( builder=builder ) - event_allowed = await self.third_party_event_rules.check_event_allowed( + event_allowed, _ = await self.third_party_event_rules.check_event_allowed( event, context ) if not event_allowed: @@ -2026,7 +2026,7 @@ async def on_send_membership_event( # for knock events, we run the third-party event rules. It's not entirely clear # why we don't do this for other sorts of membership events. if event.membership == Membership.KNOCK: - event_allowed = await self.third_party_event_rules.check_event_allowed( + event_allowed, _ = await self.third_party_event_rules.check_event_allowed( event, context ) if not event_allowed: diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index c7fe4ff89e..8a0024ce84 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -949,10 +949,10 @@ async def create_new_client_event( if requester: context.app_service = requester.app_service - third_party_result = await self.third_party_event_rules.check_event_allowed( + res, new_content = await self.third_party_event_rules.check_event_allowed( event, context ) - if not third_party_result: + if res is False: logger.info( "Event %s forbidden by third-party rules", event, @@ -960,11 +960,11 @@ async def create_new_client_event( raise SynapseError( 403, "This event is not allowed in this context", Codes.FORBIDDEN ) - elif isinstance(third_party_result, dict): + elif new_content is not None: # the third-party rules want to replace the event. We'll need to build a new # event. event, context = await self._rebuild_event_after_third_party_rules( - third_party_result, event + new_content, event ) self.validator.validate_new(event, self.config) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 64656fda22..370561e549 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -618,15 +618,11 @@ async def create_room( else: is_requester_admin = await self.auth.is_server_admin(requester.user) - # Check whether the third party rules allows/changes the room create - # request. - event_allowed = await self.third_party_event_rules.on_create_room( + # Let the third party rules modify the room creation config if needed, or abort + # the room creation entirely with an exception. + await self.third_party_event_rules.on_create_room( requester, config, is_requester_admin=is_requester_admin ) - if not event_allowed: - raise SynapseError( - 403, "You are not permitted to create rooms", Codes.FORBIDDEN - ) if not is_requester_admin and not await self.spam_checker.user_may_create_room( user_id diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 5df9349134..1259fc2d90 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -110,6 +110,7 @@ def __init__(self, hs: "HomeServer", auth_handler): self._spam_checker = hs.get_spam_checker() self._account_validity_handler = hs.get_account_validity_handler() + self._third_party_event_rules = hs.get_third_party_event_rules() ################################################################################# # The following methods should only be called during the module's initialisation. @@ -124,6 +125,11 @@ def register_account_validity_callbacks(self): """Registers callbacks for account validity capabilities.""" return self._account_validity_handler.register_account_validity_callbacks + @property + def register_third_party_rules_callbacks(self): + """Registers callbacks for third party event rules capabilities.""" + return self._third_party_event_rules.register_third_party_rules_callbacks + def register_web_resource(self, path: str, resource: IResource): """Registers a web resource to be served at the given path. diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index c5e1c5458b..28dd47a28b 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -16,17 +16,19 @@ from unittest.mock import Mock from synapse.events import EventBase +from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.module_api import ModuleApi from synapse.rest import admin from synapse.rest.client.v1 import login, room from synapse.types import Requester, StateMap +from synapse.util.frozenutils import unfreeze from tests import unittest thread_local = threading.local() -class ThirdPartyRulesTestModule: +class LegacyThirdPartyRulesTestModule: def __init__(self, config: Dict, module_api: ModuleApi): # keep a record of the "current" rules module, so that the test can patch # it if desired. @@ -46,8 +48,26 @@ def parse_config(config): return config -def current_rules_module() -> ThirdPartyRulesTestModule: - return thread_local.rules_module +class LegacyDenyNewRooms(LegacyThirdPartyRulesTestModule): + def __init__(self, config: Dict, module_api: ModuleApi): + super().__init__(config, module_api) + + def on_create_room( + self, requester: Requester, config: dict, is_requester_admin: bool + ): + return False + + +class LegacyChangeEvents(LegacyThirdPartyRulesTestModule): + def __init__(self, config: Dict, module_api: ModuleApi): + super().__init__(config, module_api) + + async def check_event_allowed(self, event: EventBase, state: StateMap[EventBase]): + d = event.get_dict() + content = unfreeze(event.content) + content["foo"] = "bar" + d["content"] = content + return d class ThirdPartyRulesTestCase(unittest.HomeserverTestCase): @@ -57,20 +77,23 @@ class ThirdPartyRulesTestCase(unittest.HomeserverTestCase): room.register_servlets, ] - def default_config(self): - config = super().default_config() - config["third_party_event_rules"] = { - "module": __name__ + ".ThirdPartyRulesTestModule", - "config": {}, - } - return config + def make_homeserver(self, reactor, clock): + hs = self.setup_test_homeserver() + + load_legacy_third_party_event_rules(hs) + + return hs def prepare(self, reactor, clock, homeserver): # Create a user and room to play with during the tests self.user_id = self.register_user("kermit", "monkey") self.tok = self.login("kermit", "monkey") - self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + # Some tests might prevent room creation on purpose. + try: + self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + except Exception: + pass def test_third_party_rules(self): """Tests that a forbidden event is forbidden from being sent, but an allowed one @@ -79,10 +102,12 @@ def test_third_party_rules(self): # patch the rules module with a Mock which will return False for some event # types async def check(ev, state): - return ev.type != "foo.bar.forbidden" + return ev.type != "foo.bar.forbidden", None callback = Mock(spec=[], side_effect=check) - current_rules_module().check_event_allowed = callback + self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [ + callback + ] channel = self.make_request( "PUT", @@ -116,9 +141,9 @@ def test_cannot_modify_event(self): # first patch the event checker so that it will try to modify the event async def check(ev: EventBase, state): ev.content = {"x": "y"} - return True + return True, None - current_rules_module().check_event_allowed = check + self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [check] # now send the event channel = self.make_request( @@ -127,7 +152,19 @@ async def check(ev: EventBase, state): {"x": "x"}, access_token=self.tok, ) - self.assertEqual(channel.result["code"], b"500", channel.result) + # check_event_allowed has some error handling, so it shouldn't 500 just because a + # module did something bad. + self.assertEqual(channel.code, 200, channel.result) + event_id = channel.json_body["event_id"] + + channel = self.make_request( + "GET", + "/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, event_id), + access_token=self.tok, + ) + self.assertEqual(channel.code, 200, channel.result) + ev = channel.json_body + self.assertEqual(ev["content"]["x"], "x") def test_modify_event(self): """The module can return a modified version of the event""" @@ -135,9 +172,9 @@ def test_modify_event(self): async def check(ev: EventBase, state): d = ev.get_dict() d["content"] = {"x": "y"} - return d + return True, d - current_rules_module().check_event_allowed = check + self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [check] # now send the event channel = self.make_request( @@ -168,9 +205,9 @@ async def check(ev: EventBase, state): "msgtype": "m.text", "body": d["content"]["body"].upper(), } - return d + return True, d - current_rules_module().check_event_allowed = check + self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [check] # Send an event, then edit it. channel = self.make_request( @@ -222,7 +259,7 @@ async def check(ev: EventBase, state): self.assertEqual(ev["content"]["body"], "EDITED BODY") def test_send_event(self): - """Tests that the module can send an event into a room via the module api""" + """Tests that a module can send an event into a room via the module api""" content = { "msgtype": "m.text", "body": "Hello!", @@ -234,12 +271,59 @@ def test_send_event(self): "sender": self.user_id, } event: EventBase = self.get_success( - current_rules_module().module_api.create_and_send_event_into_room( - event_dict - ) + self.hs.get_module_api().create_and_send_event_into_room(event_dict) ) self.assertEquals(event.sender, self.user_id) self.assertEquals(event.room_id, self.room_id) self.assertEquals(event.type, "m.room.message") self.assertEquals(event.content, content) + + @unittest.override_config( + { + "third_party_event_rules": { + "module": __name__ + ".LegacyChangeEvents", + "config": {}, + } + } + ) + def test_legacy_check_event_allowed(self): + """Tests that the wrapper for legacy check_event_allowed callbacks works + correctly. + """ + channel = self.make_request( + "PUT", + "/_matrix/client/r0/rooms/%s/send/m.room.message/1" % self.room_id, + { + "msgtype": "m.text", + "body": "Original body", + }, + access_token=self.tok, + ) + self.assertEqual(channel.result["code"], b"200", channel.result) + + event_id = channel.json_body["event_id"] + + channel = self.make_request( + "GET", + "/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, event_id), + access_token=self.tok, + ) + self.assertEqual(channel.result["code"], b"200", channel.result) + + self.assertIn("foo", channel.json_body["content"].keys()) + self.assertEqual(channel.json_body["content"]["foo"], "bar") + + @unittest.override_config( + { + "third_party_event_rules": { + "module": __name__ + ".LegacyDenyNewRooms", + "config": {}, + } + } + ) + def test_legacy_on_create_room(self): + """Tests that the wrapper for legacy on_create_room callbacks works + correctly. + """ + self.helper.create_room_as(self.user_id, tok=self.tok, expect_code=403) From 97c8ae90f7996c3d6039ce137905e87987c1be98 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 20 Jul 2021 11:41:19 +0100 Subject: [PATCH 430/619] Add a github actions job recording success of other jobs. (#10430) --- .github/workflows/tests.yml | 12 ++++++++++++ changelog.d/10430.misc | 1 + 2 files changed, 13 insertions(+) create mode 100644 changelog.d/10430.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 505bac1308..cef4439477 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -344,3 +344,15 @@ jobs: env: COMPLEMENT_BASE_IMAGE: complement-synapse:latest working-directory: complement + + # a job which marks all the other jobs as complete, thus allowing PRs to be merged. + tests-done: + needs: + - trial + - trial-olddeps + - sytest + - portdb + - complement + runs-on: ubuntu-latest + steps: + - run: "true" \ No newline at end of file diff --git a/changelog.d/10430.misc b/changelog.d/10430.misc new file mode 100644 index 0000000000..a017cf4ac9 --- /dev/null +++ b/changelog.d/10430.misc @@ -0,0 +1 @@ +Add a github actions job recording success of other jobs. From 83f1ccfcaba76785ab4bd91e3177724e2dbb85ed Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 20 Jul 2021 12:28:00 +0100 Subject: [PATCH 431/619] Fix dropping locks on shut down --- synapse/storage/databases/main/lock.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/synapse/storage/databases/main/lock.py b/synapse/storage/databases/main/lock.py index 774861074c..3d1dff660b 100644 --- a/synapse/storage/databases/main/lock.py +++ b/synapse/storage/databases/main/lock.py @@ -78,7 +78,11 @@ async def _on_shutdown(self) -> None: """Called when the server is shutting down""" logger.info("Dropping held locks due to shutdown") - for (lock_name, lock_key), token in self._live_tokens.items(): + # We need to take a copy of the tokens dict as dropping the locks will + # cause the dictionary to change. + tokens = dict(self._live_tokens) + + for (lock_name, lock_key), token in tokens.items(): await self._drop_lock(lock_name, lock_key, token) logger.info("Dropped locks due to shutdown") From 794371b1bf800353a7a4496dc6aeefb30c50831e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 20 Jul 2021 12:28:40 +0100 Subject: [PATCH 432/619] Revert "Fix dropping locks on shut down" This reverts commit 83f1ccfcaba76785ab4bd91e3177724e2dbb85ed. --- synapse/storage/databases/main/lock.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/synapse/storage/databases/main/lock.py b/synapse/storage/databases/main/lock.py index 3d1dff660b..774861074c 100644 --- a/synapse/storage/databases/main/lock.py +++ b/synapse/storage/databases/main/lock.py @@ -78,11 +78,7 @@ async def _on_shutdown(self) -> None: """Called when the server is shutting down""" logger.info("Dropping held locks due to shutdown") - # We need to take a copy of the tokens dict as dropping the locks will - # cause the dictionary to change. - tokens = dict(self._live_tokens) - - for (lock_name, lock_key), token in tokens.items(): + for (lock_name, lock_key), token in self._live_tokens.items(): await self._drop_lock(lock_name, lock_key, token) logger.info("Dropped locks due to shutdown") From 12623cf38c48bf14a24610467b15924141ce9966 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Jul 2021 12:31:51 +0100 Subject: [PATCH 433/619] 1.39.0rc1 --- CHANGES.md | 71 +++++++++++++++++++++++++++++++++++++++ changelog.d/10250.bugfix | 1 - changelog.d/10276.bugfix | 1 - changelog.d/10289.misc | 1 - changelog.d/10298.feature | 1 - changelog.d/10305.feature | 1 - changelog.d/10313.doc | 1 - changelog.d/10315.misc | 1 - changelog.d/10316.misc | 1 - changelog.d/10317.bugfix | 1 - changelog.d/10322.doc | 1 - changelog.d/10324.misc | 1 - changelog.d/10332.feature | 1 - changelog.d/10337.doc | 1 - changelog.d/10343.bugfix | 1 - changelog.d/10344.bugfix | 1 - changelog.d/10345.misc | 1 - changelog.d/10347.misc | 1 - changelog.d/10348.misc | 1 - changelog.d/10349.misc | 1 - changelog.d/10350.misc | 1 - changelog.d/10353.doc | 1 - changelog.d/10355.bugfix | 1 - changelog.d/10357.misc | 1 - changelog.d/10359.bugfix | 1 - changelog.d/10360.feature | 1 - changelog.d/10367.bugfix | 1 - changelog.d/10368.doc | 1 - changelog.d/10370.doc | 1 - changelog.d/10380.misc | 1 - changelog.d/10381.misc | 1 - changelog.d/10382.misc | 1 - changelog.d/10383.misc | 1 - changelog.d/10385.misc | 1 - changelog.d/10386.removal | 1 - changelog.d/10391.misc | 1 - changelog.d/10393.misc | 1 - changelog.d/10395.doc | 1 - changelog.d/10396.doc | 1 - changelog.d/10398.misc | 1 - changelog.d/10399.doc | 1 - changelog.d/10400.bugfix | 1 - changelog.d/10404.bugfix | 1 - changelog.d/10414.bugfix | 1 - changelog.d/10418.misc | 1 - changelog.d/10421.misc | 1 - changelog.d/10427.feature | 1 - changelog.d/10430.misc | 1 - changelog.d/9721.removal | 1 - changelog.d/9884.feature | 1 - changelog.d/9971.doc | 1 - synapse/__init__.py | 2 +- 52 files changed, 72 insertions(+), 51 deletions(-) delete mode 100644 changelog.d/10250.bugfix delete mode 100644 changelog.d/10276.bugfix delete mode 100644 changelog.d/10289.misc delete mode 100644 changelog.d/10298.feature delete mode 100644 changelog.d/10305.feature delete mode 100644 changelog.d/10313.doc delete mode 100644 changelog.d/10315.misc delete mode 100644 changelog.d/10316.misc delete mode 100644 changelog.d/10317.bugfix delete mode 100644 changelog.d/10322.doc delete mode 100644 changelog.d/10324.misc delete mode 100644 changelog.d/10332.feature delete mode 100644 changelog.d/10337.doc delete mode 100644 changelog.d/10343.bugfix delete mode 100644 changelog.d/10344.bugfix delete mode 100644 changelog.d/10345.misc delete mode 100644 changelog.d/10347.misc delete mode 100644 changelog.d/10348.misc delete mode 100644 changelog.d/10349.misc delete mode 100644 changelog.d/10350.misc delete mode 100644 changelog.d/10353.doc delete mode 100644 changelog.d/10355.bugfix delete mode 100644 changelog.d/10357.misc delete mode 100644 changelog.d/10359.bugfix delete mode 100644 changelog.d/10360.feature delete mode 100644 changelog.d/10367.bugfix delete mode 100644 changelog.d/10368.doc delete mode 100644 changelog.d/10370.doc delete mode 100644 changelog.d/10380.misc delete mode 100644 changelog.d/10381.misc delete mode 100644 changelog.d/10382.misc delete mode 100644 changelog.d/10383.misc delete mode 100644 changelog.d/10385.misc delete mode 100644 changelog.d/10386.removal delete mode 100644 changelog.d/10391.misc delete mode 100644 changelog.d/10393.misc delete mode 100644 changelog.d/10395.doc delete mode 100644 changelog.d/10396.doc delete mode 100644 changelog.d/10398.misc delete mode 100644 changelog.d/10399.doc delete mode 100644 changelog.d/10400.bugfix delete mode 100644 changelog.d/10404.bugfix delete mode 100644 changelog.d/10414.bugfix delete mode 100644 changelog.d/10418.misc delete mode 100644 changelog.d/10421.misc delete mode 100644 changelog.d/10427.feature delete mode 100644 changelog.d/10430.misc delete mode 100644 changelog.d/9721.removal delete mode 100644 changelog.d/9884.feature delete mode 100644 changelog.d/9971.doc diff --git a/CHANGES.md b/CHANGES.md index 82baaa2d1f..3179c22dfc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,74 @@ +Synapse 1.39.0rc1 (2021-07-20) +============================== + +Note that Third-Party Event Rules module interface is deprecated in favour of the generic module interface introduced in Synapse v1.37.0. Support for the old interface is planned to be rmeoved in September 2021. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. + +Features +-------- + +- Add a module type for the account validity feature. ([\#9884](https://github.com/matrix-org/synapse/issues/9884)) +- The spaces summary API now returns any joinable rooms, not only rooms which are world-readable. ([\#10298](https://github.com/matrix-org/synapse/issues/10298), [\#10305](https://github.com/matrix-org/synapse/issues/10305)) +- Add a new version of the R30 phone-home metric, which removes a false impression of retention given by the old R30 metric. ([\#10332](https://github.com/matrix-org/synapse/issues/10332), [\#10427](https://github.com/matrix-org/synapse/issues/10427)) +- Allow providing credentials to `http_proxy`. ([\#10360](https://github.com/matrix-org/synapse/issues/10360)) + + +Bugfixes +-------- + +- Add base starting insertion event when no chunk ID is specified in the historical batch send API. ([\#10250](https://github.com/matrix-org/synapse/issues/10250)) +- Fix historical batch send endpoint (MSC2716) rejecting batches with messages from multiple senders. ([\#10276](https://github.com/matrix-org/synapse/issues/10276)) +- Fix purging rooms that other homeservers are still sending events for. Contributed by @ilmari. ([\#10317](https://github.com/matrix-org/synapse/issues/10317)) +- Fix errors during backfill caused by previously purged redaction events. Contributed by Andreas Rammhold (@andir). ([\#10343](https://github.com/matrix-org/synapse/issues/10343)) +- Fix the user directory becoming broken (and noisy errors being logged) when knocking and room statistics are in use. ([\#10344](https://github.com/matrix-org/synapse/issues/10344)) +- Fix newly added `synapse_federation_server_oldest_inbound_pdu_in_staging` prometheus metric to measure age rather than timestamp. ([\#10355](https://github.com/matrix-org/synapse/issues/10355)) +- Fix PostgreSQL sometimes using table scans for queries against `state_groups_state` table, taking a long time and a large amount of IO. ([\#10359](https://github.com/matrix-org/synapse/issues/10359)) +- Fix `make_room_admin` failing for users that have left a private room. ([\#10367](https://github.com/matrix-org/synapse/issues/10367)) +- Fix a number of logged errors caused by remote servers being down. ([\#10400](https://github.com/matrix-org/synapse/issues/10400), [\#10414](https://github.com/matrix-org/synapse/issues/10414)) +- Responses from `/make_{join,leave,knock}` no longer include signatures, which will turn out to be invalid after events are returned to `/send_{join,leave,knock}`. ([\#10404](https://github.com/matrix-org/synapse/issues/10404)) + + +Improved Documentation +---------------------- + +- Updated installation dependencies for newer macOS versions and ARM Macs. Contributed by Luke Walsh. ([\#9971](https://github.com/matrix-org/synapse/issues/9971)) +- Simplify structure of room admin API. ([\#10313](https://github.com/matrix-org/synapse/issues/10313)) +- Fix a broken link in the admin api docs. ([\#10322](https://github.com/matrix-org/synapse/issues/10322)) +- Fix formatting in the logcontext documentation. ([\#10337](https://github.com/matrix-org/synapse/issues/10337)) +- Refresh the logcontext dev documentation. ([\#10353](https://github.com/matrix-org/synapse/issues/10353)) +- Add delegation example for caddy in the reverse proxy documentation. Contributed by @moritzdietz. ([\#10368](https://github.com/matrix-org/synapse/issues/10368)) +- Fix some links in `docs` and `contrib`. ([\#10370](https://github.com/matrix-org/synapse/issues/10370)) +- Make deprecation notice of the spam checker doc more obvious. ([\#10395](https://github.com/matrix-org/synapse/issues/10395)) +- Add instructructions on installing Debian packages for release candidates. ([\#10396](https://github.com/matrix-org/synapse/issues/10396)) +- Rewrite the text of links to be clearer in the documentation. ([\#10399](https://github.com/matrix-org/synapse/issues/10399)) + + +Deprecations and Removals +------------------------- + +- Remove functionality associated with the unused `room_stats_historical` and `user_stats_historical` tables. Contributed by @xmunoz. ([\#9721](https://github.com/matrix-org/synapse/issues/9721)) +- The third-party event rules module interface is deprecated in favour of the generic module interface introduced in Synapse v1.37.0. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. ([\#10386](https://github.com/matrix-org/synapse/issues/10386)) + + +Internal Changes +---------------- + +- Convert `room_depth.min_depth` column to a `BIGINT`. ([\#10289](https://github.com/matrix-org/synapse/issues/10289)) +- Add tests to characterise the current behaviour of R30 phone-home metrics. ([\#10315](https://github.com/matrix-org/synapse/issues/10315)) +- Rebuild event context and auth when processing specific results from `ThirdPartyEventRules` modules. ([\#10316](https://github.com/matrix-org/synapse/issues/10316)) +- Minor change to the code that populates `user_daily_visits`. ([\#10324](https://github.com/matrix-org/synapse/issues/10324)) +- Re-enable Sytests that were disabled for the 1.37.1 release. ([\#10345](https://github.com/matrix-org/synapse/issues/10345), [\#10357](https://github.com/matrix-org/synapse/issues/10357)) +- Run `pyupgrade` on the codebase. ([\#10347](https://github.com/matrix-org/synapse/issues/10347), [\#10348](https://github.com/matrix-org/synapse/issues/10348)) +- Switch `application_services_txns.txn_id` database column to `BIGINT`. ([\#10349](https://github.com/matrix-org/synapse/issues/10349)) +- Convert internal type variable syntax to reflect wider ecosystem use. ([\#10350](https://github.com/matrix-org/synapse/issues/10350), [\#10380](https://github.com/matrix-org/synapse/issues/10380), [\#10381](https://github.com/matrix-org/synapse/issues/10381), [\#10382](https://github.com/matrix-org/synapse/issues/10382), [\#10418](https://github.com/matrix-org/synapse/issues/10418)) +- Make the Github Actions workflow configuration more efficient. ([\#10383](https://github.com/matrix-org/synapse/issues/10383)) +- Add type hints to `get_{domain,localpart}_from_id`. ([\#10385](https://github.com/matrix-org/synapse/issues/10385)) +- When building Debian packages for prerelease versions, set the Section accordingly. ([\#10391](https://github.com/matrix-org/synapse/issues/10391)) +- Add type hints and comments to event auth code. ([\#10393](https://github.com/matrix-org/synapse/issues/10393)) +- Stagger sending of presence update to remote servers, reducing CPU spikes caused by starting many connections to remote servers at once. ([\#10398](https://github.com/matrix-org/synapse/issues/10398)) +- Remove unused `events_by_room` code (tech debt). ([\#10421](https://github.com/matrix-org/synapse/issues/10421)) +- Add a github actions job which records success of other jobs. ([\#10430](https://github.com/matrix-org/synapse/issues/10430)) + + Synapse 1.38.0 (2021-07-13) =========================== diff --git a/changelog.d/10250.bugfix b/changelog.d/10250.bugfix deleted file mode 100644 index a8107dafb2..0000000000 --- a/changelog.d/10250.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add base starting insertion event when no chunk ID is specified in the historical batch send API. diff --git a/changelog.d/10276.bugfix b/changelog.d/10276.bugfix deleted file mode 100644 index 42adc57ad1..0000000000 --- a/changelog.d/10276.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix historical batch send endpoint (MSC2716) rejecting batches with messages from multiple senders. diff --git a/changelog.d/10289.misc b/changelog.d/10289.misc deleted file mode 100644 index 2df30e7a7a..0000000000 --- a/changelog.d/10289.misc +++ /dev/null @@ -1 +0,0 @@ -Convert `room_depth.min_depth` column to a `BIGINT`. diff --git a/changelog.d/10298.feature b/changelog.d/10298.feature deleted file mode 100644 index 7059db5075..0000000000 --- a/changelog.d/10298.feature +++ /dev/null @@ -1 +0,0 @@ -The spaces summary API now returns any joinable rooms, not only rooms which are world-readable. diff --git a/changelog.d/10305.feature b/changelog.d/10305.feature deleted file mode 100644 index 7059db5075..0000000000 --- a/changelog.d/10305.feature +++ /dev/null @@ -1 +0,0 @@ -The spaces summary API now returns any joinable rooms, not only rooms which are world-readable. diff --git a/changelog.d/10313.doc b/changelog.d/10313.doc deleted file mode 100644 index 44086e3d9d..0000000000 --- a/changelog.d/10313.doc +++ /dev/null @@ -1 +0,0 @@ -Simplify structure of room admin API. \ No newline at end of file diff --git a/changelog.d/10315.misc b/changelog.d/10315.misc deleted file mode 100644 index 2c78644e20..0000000000 --- a/changelog.d/10315.misc +++ /dev/null @@ -1 +0,0 @@ -Add tests to characterise the current behaviour of R30 phone-home metrics. diff --git a/changelog.d/10316.misc b/changelog.d/10316.misc deleted file mode 100644 index 1fd0810fde..0000000000 --- a/changelog.d/10316.misc +++ /dev/null @@ -1 +0,0 @@ -Rebuild event context and auth when processing specific results from `ThirdPartyEventRules` modules. diff --git a/changelog.d/10317.bugfix b/changelog.d/10317.bugfix deleted file mode 100644 index 826c269eff..0000000000 --- a/changelog.d/10317.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix purging rooms that other homeservers are still sending events for. Contributed by @ilmari. diff --git a/changelog.d/10322.doc b/changelog.d/10322.doc deleted file mode 100644 index db604cf2aa..0000000000 --- a/changelog.d/10322.doc +++ /dev/null @@ -1 +0,0 @@ -Fix a broken link in the admin api docs. diff --git a/changelog.d/10324.misc b/changelog.d/10324.misc deleted file mode 100644 index 3c3ee6d6fc..0000000000 --- a/changelog.d/10324.misc +++ /dev/null @@ -1 +0,0 @@ -Minor change to the code that populates `user_daily_visits`. diff --git a/changelog.d/10332.feature b/changelog.d/10332.feature deleted file mode 100644 index 091947ff22..0000000000 --- a/changelog.d/10332.feature +++ /dev/null @@ -1 +0,0 @@ -Add a new version of the R30 phone-home metric, which removes a false impression of retention given by the old R30 metric. diff --git a/changelog.d/10337.doc b/changelog.d/10337.doc deleted file mode 100644 index f305bdb3ba..0000000000 --- a/changelog.d/10337.doc +++ /dev/null @@ -1 +0,0 @@ -Fix formatting in the logcontext documentation. diff --git a/changelog.d/10343.bugfix b/changelog.d/10343.bugfix deleted file mode 100644 index 53ccf79a81..0000000000 --- a/changelog.d/10343.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix errors during backfill caused by previously purged redaction events. Contributed by Andreas Rammhold (@andir). diff --git a/changelog.d/10344.bugfix b/changelog.d/10344.bugfix deleted file mode 100644 index ab6eb4999f..0000000000 --- a/changelog.d/10344.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the user directory becoming broken (and noisy errors being logged) when knocking and room statistics are in use. diff --git a/changelog.d/10345.misc b/changelog.d/10345.misc deleted file mode 100644 index 7424486e87..0000000000 --- a/changelog.d/10345.misc +++ /dev/null @@ -1 +0,0 @@ -Re-enable Sytests that were disabled for the 1.37.1 release. diff --git a/changelog.d/10347.misc b/changelog.d/10347.misc deleted file mode 100644 index b2275a1350..0000000000 --- a/changelog.d/10347.misc +++ /dev/null @@ -1 +0,0 @@ -Run `pyupgrade` on the codebase. \ No newline at end of file diff --git a/changelog.d/10348.misc b/changelog.d/10348.misc deleted file mode 100644 index b2275a1350..0000000000 --- a/changelog.d/10348.misc +++ /dev/null @@ -1 +0,0 @@ -Run `pyupgrade` on the codebase. \ No newline at end of file diff --git a/changelog.d/10349.misc b/changelog.d/10349.misc deleted file mode 100644 index 5b014e7416..0000000000 --- a/changelog.d/10349.misc +++ /dev/null @@ -1 +0,0 @@ -Switch `application_services_txns.txn_id` database column to `BIGINT`. diff --git a/changelog.d/10350.misc b/changelog.d/10350.misc deleted file mode 100644 index eed2d8552a..0000000000 --- a/changelog.d/10350.misc +++ /dev/null @@ -1 +0,0 @@ -Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/changelog.d/10353.doc b/changelog.d/10353.doc deleted file mode 100644 index 274ac83549..0000000000 --- a/changelog.d/10353.doc +++ /dev/null @@ -1 +0,0 @@ -Refresh the logcontext dev documentation. diff --git a/changelog.d/10355.bugfix b/changelog.d/10355.bugfix deleted file mode 100644 index 92df612011..0000000000 --- a/changelog.d/10355.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix newly added `synapse_federation_server_oldest_inbound_pdu_in_staging` prometheus metric to measure age rather than timestamp. diff --git a/changelog.d/10357.misc b/changelog.d/10357.misc deleted file mode 100644 index 7424486e87..0000000000 --- a/changelog.d/10357.misc +++ /dev/null @@ -1 +0,0 @@ -Re-enable Sytests that were disabled for the 1.37.1 release. diff --git a/changelog.d/10359.bugfix b/changelog.d/10359.bugfix deleted file mode 100644 index d318f8fa08..0000000000 --- a/changelog.d/10359.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix PostgreSQL sometimes using table scans for queries against `state_groups_state` table, taking a long time and a large amount of IO. diff --git a/changelog.d/10360.feature b/changelog.d/10360.feature deleted file mode 100644 index 904221cb6d..0000000000 --- a/changelog.d/10360.feature +++ /dev/null @@ -1 +0,0 @@ -Allow providing credentials to `http_proxy`. \ No newline at end of file diff --git a/changelog.d/10367.bugfix b/changelog.d/10367.bugfix deleted file mode 100644 index b445556084..0000000000 --- a/changelog.d/10367.bugfix +++ /dev/null @@ -1 +0,0 @@ -Bugfix `make_room_admin` fails for users that have left a private room. \ No newline at end of file diff --git a/changelog.d/10368.doc b/changelog.d/10368.doc deleted file mode 100644 index 10297aa424..0000000000 --- a/changelog.d/10368.doc +++ /dev/null @@ -1 +0,0 @@ -Add delegation example for caddy in the reverse proxy documentation. Contributed by @moritzdietz. diff --git a/changelog.d/10370.doc b/changelog.d/10370.doc deleted file mode 100644 index 8c59d98ee8..0000000000 --- a/changelog.d/10370.doc +++ /dev/null @@ -1 +0,0 @@ -Fix some links in `docs` and `contrib`. \ No newline at end of file diff --git a/changelog.d/10380.misc b/changelog.d/10380.misc deleted file mode 100644 index eed2d8552a..0000000000 --- a/changelog.d/10380.misc +++ /dev/null @@ -1 +0,0 @@ -Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/changelog.d/10381.misc b/changelog.d/10381.misc deleted file mode 100644 index eed2d8552a..0000000000 --- a/changelog.d/10381.misc +++ /dev/null @@ -1 +0,0 @@ -Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/changelog.d/10382.misc b/changelog.d/10382.misc deleted file mode 100644 index eed2d8552a..0000000000 --- a/changelog.d/10382.misc +++ /dev/null @@ -1 +0,0 @@ -Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/changelog.d/10383.misc b/changelog.d/10383.misc deleted file mode 100644 index 952c1e77a8..0000000000 --- a/changelog.d/10383.misc +++ /dev/null @@ -1 +0,0 @@ -Make the Github Actions workflow configuration more efficient. diff --git a/changelog.d/10385.misc b/changelog.d/10385.misc deleted file mode 100644 index e515ac09fd..0000000000 --- a/changelog.d/10385.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to `get_{domain,localpart}_from_id`. diff --git a/changelog.d/10386.removal b/changelog.d/10386.removal deleted file mode 100644 index 800a6143d7..0000000000 --- a/changelog.d/10386.removal +++ /dev/null @@ -1 +0,0 @@ -The third-party event rules module interface is deprecated in favour of the generic module interface introduced in Synapse v1.37.0. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. diff --git a/changelog.d/10391.misc b/changelog.d/10391.misc deleted file mode 100644 index 3f191b520a..0000000000 --- a/changelog.d/10391.misc +++ /dev/null @@ -1 +0,0 @@ -When building Debian packages for prerelease versions, set the Section accordingly. diff --git a/changelog.d/10393.misc b/changelog.d/10393.misc deleted file mode 100644 index e80f16d607..0000000000 --- a/changelog.d/10393.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints and comments to event auth code. diff --git a/changelog.d/10395.doc b/changelog.d/10395.doc deleted file mode 100644 index 4bdaea76c5..0000000000 --- a/changelog.d/10395.doc +++ /dev/null @@ -1 +0,0 @@ -Make deprecation notice of the spam checker doc more obvious. diff --git a/changelog.d/10396.doc b/changelog.d/10396.doc deleted file mode 100644 index b521ad9cbf..0000000000 --- a/changelog.d/10396.doc +++ /dev/null @@ -1 +0,0 @@ -Add instructructions on installing Debian packages for release candidates. diff --git a/changelog.d/10398.misc b/changelog.d/10398.misc deleted file mode 100644 index 326e54655a..0000000000 --- a/changelog.d/10398.misc +++ /dev/null @@ -1 +0,0 @@ -Stagger sending of presence update to remote servers, reducing CPU spikes caused by starting many connections to remote servers at once. diff --git a/changelog.d/10399.doc b/changelog.d/10399.doc deleted file mode 100644 index b596ac5627..0000000000 --- a/changelog.d/10399.doc +++ /dev/null @@ -1 +0,0 @@ -Rewrite the text of links to be clearer in the documentation. diff --git a/changelog.d/10400.bugfix b/changelog.d/10400.bugfix deleted file mode 100644 index bfebed8d29..0000000000 --- a/changelog.d/10400.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a number of logged errors caused by remote servers being down. diff --git a/changelog.d/10404.bugfix b/changelog.d/10404.bugfix deleted file mode 100644 index 2e095b6402..0000000000 --- a/changelog.d/10404.bugfix +++ /dev/null @@ -1 +0,0 @@ -Responses from `/make_{join,leave,knock}` no longer include signatures, which will turn out to be invalid after events are returned to `/send_{join,leave,knock}`. diff --git a/changelog.d/10414.bugfix b/changelog.d/10414.bugfix deleted file mode 100644 index bfebed8d29..0000000000 --- a/changelog.d/10414.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a number of logged errors caused by remote servers being down. diff --git a/changelog.d/10418.misc b/changelog.d/10418.misc deleted file mode 100644 index eed2d8552a..0000000000 --- a/changelog.d/10418.misc +++ /dev/null @@ -1 +0,0 @@ -Convert internal type variable syntax to reflect wider ecosystem use. \ No newline at end of file diff --git a/changelog.d/10421.misc b/changelog.d/10421.misc deleted file mode 100644 index 385cbe07af..0000000000 --- a/changelog.d/10421.misc +++ /dev/null @@ -1 +0,0 @@ -Remove unused `events_by_room` code (tech debt). diff --git a/changelog.d/10427.feature b/changelog.d/10427.feature deleted file mode 100644 index 091947ff22..0000000000 --- a/changelog.d/10427.feature +++ /dev/null @@ -1 +0,0 @@ -Add a new version of the R30 phone-home metric, which removes a false impression of retention given by the old R30 metric. diff --git a/changelog.d/10430.misc b/changelog.d/10430.misc deleted file mode 100644 index a017cf4ac9..0000000000 --- a/changelog.d/10430.misc +++ /dev/null @@ -1 +0,0 @@ -Add a github actions job recording success of other jobs. diff --git a/changelog.d/9721.removal b/changelog.d/9721.removal deleted file mode 100644 index da2ba48c84..0000000000 --- a/changelog.d/9721.removal +++ /dev/null @@ -1 +0,0 @@ -Remove functionality associated with the unused `room_stats_historical` and `user_stats_historical` tables. Contributed by @xmunoz. diff --git a/changelog.d/9884.feature b/changelog.d/9884.feature deleted file mode 100644 index 525fd2f93c..0000000000 --- a/changelog.d/9884.feature +++ /dev/null @@ -1 +0,0 @@ -Add a module type for the account validity feature. diff --git a/changelog.d/9971.doc b/changelog.d/9971.doc deleted file mode 100644 index ada68f70ca..0000000000 --- a/changelog.d/9971.doc +++ /dev/null @@ -1 +0,0 @@ -Updated installation dependencies for newer macOS versions and ARM Macs. Contributed by Luke Walsh. diff --git a/synapse/__init__.py b/synapse/__init__.py index 5ecce24eee..46902adab5 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.38.0" +__version__ = "1.39.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From d30a657439b2e169eff022df47e7756864708172 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Jul 2021 12:32:36 +0100 Subject: [PATCH 434/619] changelog word fixes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3179c22dfc..5bd9bbeec0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Synapse 1.39.0rc1 (2021-07-20) ============================== -Note that Third-Party Event Rules module interface is deprecated in favour of the generic module interface introduced in Synapse v1.37.0. Support for the old interface is planned to be rmeoved in September 2021. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. +The Third-Party Event Rules module interface has been deprecated in favour of the generic module interface introduced in Synapse v1.37.0. Support for the old interface is planned to be rmeoved in September 2021. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. Features -------- From c5205e449f44769f05bb276ec5ad8b894f97f068 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Jul 2021 12:35:15 +0100 Subject: [PATCH 435/619] fix typo in changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5bd9bbeec0..4e620dfb2c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Synapse 1.39.0rc1 (2021-07-20) ============================== -The Third-Party Event Rules module interface has been deprecated in favour of the generic module interface introduced in Synapse v1.37.0. Support for the old interface is planned to be rmeoved in September 2021. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. +The Third-Party Event Rules module interface has been deprecated in favour of the generic module interface introduced in Synapse v1.37.0. Support for the old interface is planned to be removed in September 2021. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html#upgrading-to-v1390) for more information. Features -------- From 69226c1ab4e88d1f104ad8aaa13fb9dd0ff5dbb2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 20 Jul 2021 12:59:23 +0100 Subject: [PATCH 436/619] MSC3244 room capabilities implementation (#10283) --- changelog.d/10283.feature | 1 + synapse/api/room_versions.py | 38 ++++++++++++++- synapse/config/experimental.py | 3 ++ synapse/rest/client/v2_alpha/capabilities.py | 8 +++- .../rest/client/v2_alpha/test_capabilities.py | 46 +++++++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10283.feature diff --git a/changelog.d/10283.feature b/changelog.d/10283.feature new file mode 100644 index 0000000000..99d633dbfb --- /dev/null +++ b/changelog.d/10283.feature @@ -0,0 +1 @@ +Initial support for MSC3244, Room version capabilities over the /capabilities API. \ No newline at end of file diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index a20abc5a65..8dd33dcb83 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict +from typing import Callable, Dict, Optional import attr @@ -208,5 +208,39 @@ class RoomVersions: RoomVersions.MSC3083, RoomVersions.V7, ) - # Note that we do not include MSC2043 here unless it is enabled in the config. +} + + +@attr.s(slots=True, frozen=True, auto_attribs=True) +class RoomVersionCapability: + """An object which describes the unique attributes of a room version.""" + + identifier: str # the identifier for this capability + preferred_version: Optional[RoomVersion] + support_check_lambda: Callable[[RoomVersion], bool] + + +MSC3244_CAPABILITIES = { + cap.identifier: { + "preferred": cap.preferred_version.identifier + if cap.preferred_version is not None + else None, + "support": [ + v.identifier + for v in KNOWN_ROOM_VERSIONS.values() + if cap.support_check_lambda(v) + ], + } + for cap in ( + RoomVersionCapability( + "knock", + RoomVersions.V7, + lambda room_version: room_version.msc2403_knocking, + ), + RoomVersionCapability( + "restricted", + None, + lambda room_version: room_version.msc3083_join_rules, + ), + ) } diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index e25ccba9ac..040c4504d8 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -32,3 +32,6 @@ def read_config(self, config: JsonDict, **kwargs): # MSC2716 (backfill existing history) self.msc2716_enabled: bool = experimental.get("msc2716_enabled", False) + + # MSC3244 (room version capabilities) + self.msc3244_enabled: bool = experimental.get("msc3244_enabled", False) diff --git a/synapse/rest/client/v2_alpha/capabilities.py b/synapse/rest/client/v2_alpha/capabilities.py index 6a24021484..88e3aac797 100644 --- a/synapse/rest/client/v2_alpha/capabilities.py +++ b/synapse/rest/client/v2_alpha/capabilities.py @@ -14,7 +14,7 @@ import logging from typing import TYPE_CHECKING, Tuple -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, MSC3244_CAPABILITIES from synapse.http.servlet import RestServlet from synapse.http.site import SynapseRequest from synapse.types import JsonDict @@ -55,6 +55,12 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "m.change_password": {"enabled": change_password}, } } + + if self.config.experimental.msc3244_enabled: + response["capabilities"]["m.room_versions"][ + "org.matrix.msc3244.room_capabilities" + ] = MSC3244_CAPABILITIES + return 200, response diff --git a/tests/rest/client/v2_alpha/test_capabilities.py b/tests/rest/client/v2_alpha/test_capabilities.py index 874052c61c..f80f48a455 100644 --- a/tests/rest/client/v2_alpha/test_capabilities.py +++ b/tests/rest/client/v2_alpha/test_capabilities.py @@ -102,3 +102,49 @@ def test_get_change_password_capabilities_password_disabled(self): self.assertEqual(channel.code, 200) self.assertFalse(capabilities["m.change_password"]["enabled"]) + + def test_get_does_not_include_msc3244_fields_by_default(self): + localpart = "user" + password = "pass" + user = self.register_user(localpart, password) + access_token = self.get_success( + self.auth_handler.get_access_token_for_user_id( + user, device_id=None, valid_until_ms=None + ) + ) + + channel = self.make_request("GET", self.url, access_token=access_token) + capabilities = channel.json_body["capabilities"] + + self.assertEqual(channel.code, 200) + self.assertNotIn( + "org.matrix.msc3244.room_capabilities", capabilities["m.room_versions"] + ) + + @override_config({"experimental_features": {"msc3244_enabled": True}}) + def test_get_does_include_msc3244_fields_when_enabled(self): + localpart = "user" + password = "pass" + user = self.register_user(localpart, password) + access_token = self.get_success( + self.auth_handler.get_access_token_for_user_id( + user, device_id=None, valid_until_ms=None + ) + ) + + channel = self.make_request("GET", self.url, access_token=access_token) + capabilities = channel.json_body["capabilities"] + + self.assertEqual(channel.code, 200) + for details in capabilities["m.room_versions"][ + "org.matrix.msc3244.room_capabilities" + ].values(): + if details["preferred"] is not None: + self.assertTrue( + details["preferred"] in KNOWN_ROOM_VERSIONS, + str(details["preferred"]), + ) + + self.assertGreater(len(details["support"]), 0) + for room_version in details["support"]: + self.assertTrue(room_version in KNOWN_ROOM_VERSIONS, str(room_version)) From 541e58e7d66652c57a602dc2f1c16300e3e81b58 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Jul 2021 13:29:59 +0100 Subject: [PATCH 437/619] Update account validity feature line in changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4e620dfb2c..efa53dd733 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ The Third-Party Event Rules module interface has been deprecated in favour of th Features -------- -- Add a module type for the account validity feature. ([\#9884](https://github.com/matrix-org/synapse/issues/9884)) +- Add the ability to override the account validity feature with a module. ([\#9884](https://github.com/matrix-org/synapse/issues/9884)) - The spaces summary API now returns any joinable rooms, not only rooms which are world-readable. ([\#10298](https://github.com/matrix-org/synapse/issues/10298), [\#10305](https://github.com/matrix-org/synapse/issues/10305)) - Add a new version of the R30 phone-home metric, which removes a false impression of retention given by the old R30 metric. ([\#10332](https://github.com/matrix-org/synapse/issues/10332), [\#10427](https://github.com/matrix-org/synapse/issues/10427)) - Allow providing credentials to `http_proxy`. ([\#10360](https://github.com/matrix-org/synapse/issues/10360)) From 96e63ec7bfff06402ee4f06f14c5a48affdee136 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Jul 2021 13:36:05 +0100 Subject: [PATCH 438/619] Combine some changelog lines in the documentation section --- CHANGES.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index efa53dd733..871b8d8b94 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,14 +32,11 @@ Improved Documentation - Updated installation dependencies for newer macOS versions and ARM Macs. Contributed by Luke Walsh. ([\#9971](https://github.com/matrix-org/synapse/issues/9971)) - Simplify structure of room admin API. ([\#10313](https://github.com/matrix-org/synapse/issues/10313)) -- Fix a broken link in the admin api docs. ([\#10322](https://github.com/matrix-org/synapse/issues/10322)) -- Fix formatting in the logcontext documentation. ([\#10337](https://github.com/matrix-org/synapse/issues/10337)) -- Refresh the logcontext dev documentation. ([\#10353](https://github.com/matrix-org/synapse/issues/10353)) +- Refresh the logcontext dev documentation. ([\#10353](https://github.com/matrix-org/synapse/issues/10353)), ([\#10337](https://github.com/matrix-org/synapse/issues/10337)) - Add delegation example for caddy in the reverse proxy documentation. Contributed by @moritzdietz. ([\#10368](https://github.com/matrix-org/synapse/issues/10368)) -- Fix some links in `docs` and `contrib`. ([\#10370](https://github.com/matrix-org/synapse/issues/10370)) +- Fix and clarify some links in `docs` and `contrib`. ([\#10370](https://github.com/matrix-org/synapse/issues/10370)), ([\#10322](https://github.com/matrix-org/synapse/issues/10322)), ([\#10399](https://github.com/matrix-org/synapse/issues/10399)) - Make deprecation notice of the spam checker doc more obvious. ([\#10395](https://github.com/matrix-org/synapse/issues/10395)) -- Add instructructions on installing Debian packages for release candidates. ([\#10396](https://github.com/matrix-org/synapse/issues/10396)) -- Rewrite the text of links to be clearer in the documentation. ([\#10399](https://github.com/matrix-org/synapse/issues/10399)) +- Add instructions on installing Debian packages for release candidates. ([\#10396](https://github.com/matrix-org/synapse/issues/10396)) Deprecations and Removals From 54389d5697622f1beffaeda96d9c6da7ef7d93a9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 20 Jul 2021 14:24:25 +0100 Subject: [PATCH 439/619] Fix dropping locks on shut down (#10433) --- changelog.d/10433.bugfix | 1 + synapse/storage/databases/main/lock.py | 6 +++++- tests/storage/databases/main/test_lock.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10433.bugfix diff --git a/changelog.d/10433.bugfix b/changelog.d/10433.bugfix new file mode 100644 index 0000000000..ad85a83f96 --- /dev/null +++ b/changelog.d/10433.bugfix @@ -0,0 +1 @@ +Fix error while dropping locks on shutdown. Introduced in v1.38.0. diff --git a/synapse/storage/databases/main/lock.py b/synapse/storage/databases/main/lock.py index 774861074c..3d1dff660b 100644 --- a/synapse/storage/databases/main/lock.py +++ b/synapse/storage/databases/main/lock.py @@ -78,7 +78,11 @@ async def _on_shutdown(self) -> None: """Called when the server is shutting down""" logger.info("Dropping held locks due to shutdown") - for (lock_name, lock_key), token in self._live_tokens.items(): + # We need to take a copy of the tokens dict as dropping the locks will + # cause the dictionary to change. + tokens = dict(self._live_tokens) + + for (lock_name, lock_key), token in tokens.items(): await self._drop_lock(lock_name, lock_key, token) logger.info("Dropped locks due to shutdown") diff --git a/tests/storage/databases/main/test_lock.py b/tests/storage/databases/main/test_lock.py index 9ca70e7367..d326a1d6a6 100644 --- a/tests/storage/databases/main/test_lock.py +++ b/tests/storage/databases/main/test_lock.py @@ -98,3 +98,16 @@ def test_drop(self): lock2 = self.get_success(self.store.try_acquire_lock("name", "key")) self.assertIsNotNone(lock2) + + def test_shutdown(self): + """Test that shutting down Synapse releases the locks""" + # Acquire two locks + lock = self.get_success(self.store.try_acquire_lock("name", "key1")) + self.assertIsNotNone(lock) + lock2 = self.get_success(self.store.try_acquire_lock("name", "key2")) + self.assertIsNotNone(lock2) + + # Now call the shutdown code + self.get_success(self.store._on_shutdown()) + + self.assertEqual(self.store._live_tokens, {}) From f2501f1972b911abbd94fed8d9a11aeccc83b25e Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Jul 2021 14:27:46 +0100 Subject: [PATCH 440/619] Incorporate changelog of #10433 --- CHANGES.md | 1 + changelog.d/10433.bugfix | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 changelog.d/10433.bugfix diff --git a/CHANGES.md b/CHANGES.md index 871b8d8b94..066f798a95 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Features Bugfixes -------- +- Fix error while dropping locks on shutdown. Introduced in v1.38.0. ([\#10433](https://github.com/matrix-org/synapse/issues/10433)) - Add base starting insertion event when no chunk ID is specified in the historical batch send API. ([\#10250](https://github.com/matrix-org/synapse/issues/10250)) - Fix historical batch send endpoint (MSC2716) rejecting batches with messages from multiple senders. ([\#10276](https://github.com/matrix-org/synapse/issues/10276)) - Fix purging rooms that other homeservers are still sending events for. Contributed by @ilmari. ([\#10317](https://github.com/matrix-org/synapse/issues/10317)) diff --git a/changelog.d/10433.bugfix b/changelog.d/10433.bugfix deleted file mode 100644 index ad85a83f96..0000000000 --- a/changelog.d/10433.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix error while dropping locks on shutdown. Introduced in v1.38.0. From e009d2e90a44c05eb842396ccbcc515f087665d8 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 20 Jul 2021 14:28:49 +0100 Subject: [PATCH 441/619] 1.39.0rc1 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 43d26fc133..4d214c23b6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.39.0~rc1) stable; urgency=medium + + * New synapse release 1.39.0rc1. + + -- Synapse Packaging team Tue, 20 Jul 2021 14:28:34 +0100 + matrix-synapse-py3 (1.38.0) stable; urgency=medium * New synapse release 1.38.0. From 2d89c66b8811aa4968aefea3572f174fa00cc3c2 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 21 Jul 2021 05:29:57 -0500 Subject: [PATCH 442/619] Switch to `chunk` events so we can auth via power_levels (MSC2716) (#10432) Previously, we were using `content.chunk_id` to connect one chunk to another. But these events can be from any `sender` and we can't tell who should be able to send historical events. We know we only want the application service to do it but these events have the sender of a real historical message, not the application service user ID as the sender. Other federated homeservers also have no indicator which senders are an application service on the originating homeserver. So we want to auth all of the MSC2716 events via power_levels and have them be sent by the application service with proper PL levels in the room. --- changelog.d/10432.misc | 1 + synapse/api/constants.py | 6 ++++-- synapse/rest/client/v1/room.py | 17 +++++++++++++---- 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10432.misc diff --git a/changelog.d/10432.misc b/changelog.d/10432.misc new file mode 100644 index 0000000000..3a8cdf0ae0 --- /dev/null +++ b/changelog.d/10432.misc @@ -0,0 +1 @@ +Connect historical chunks together with chunk events instead of a content field (MSC2716). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 8363c2bb0f..4caafc0ac9 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -120,6 +120,7 @@ class EventTypes: SpaceParent = "m.space.parent" MSC2716_INSERTION = "org.matrix.msc2716.insertion" + MSC2716_CHUNK = "org.matrix.msc2716.chunk" MSC2716_MARKER = "org.matrix.msc2716.marker" @@ -190,9 +191,10 @@ class EventContentFields: # Used on normal messages to indicate they were historically imported after the fact MSC2716_HISTORICAL = "org.matrix.msc2716.historical" - # For "insertion" events + # For "insertion" events to indicate what the next chunk ID should be in + # order to connect to it MSC2716_NEXT_CHUNK_ID = "org.matrix.msc2716.next_chunk_id" - # Used on normal message events to indicate where the chunk connects to + # Used on "chunk" events to indicate which insertion event it connects to MSC2716_CHUNK_ID = "org.matrix.msc2716.chunk_id" # For "marker" events MSC2716_MARKER_INSERTION = "org.matrix.msc2716.marker.insertion" diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 31a1193cd3..c95c5ae234 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -553,9 +553,18 @@ async def on_POST(self, request, room_id): ] # Connect this current chunk to the insertion event from the previous chunk - last_event_in_chunk["content"][ - EventContentFields.MSC2716_CHUNK_ID - ] = chunk_id_to_connect_to + chunk_event = { + "type": EventTypes.MSC2716_CHUNK, + "sender": requester.user.to_string(), + "room_id": room_id, + "content": {EventContentFields.MSC2716_CHUNK_ID: chunk_id_to_connect_to}, + # Since the chunk event is put at the end of the chunk, + # where the newest-in-time event is, copy the origin_server_ts from + # the last event we're inserting + "origin_server_ts": last_event_in_chunk["origin_server_ts"], + } + # Add the chunk event to the end of the chunk (newest-in-time) + events_to_create.append(chunk_event) # Add an "insertion" event to the start of each chunk (next to the oldest-in-time # event in the chunk) so the next chunk can be connected to this one. @@ -567,7 +576,7 @@ async def on_POST(self, request, room_id): # the first event we're inserting origin_server_ts=events_to_create[0]["origin_server_ts"], ) - # Prepend the insertion event to the start of the chunk + # Prepend the insertion event to the start of the chunk (oldest-in-time) events_to_create = [insertion_event] + events_to_create event_ids = [] From c6509991f362cb559efbb97e1799776cd32a43d8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 21 Jul 2021 12:33:35 +0100 Subject: [PATCH 443/619] Move the docker image build to Github Actions (#10416) it's flaky on circleCI, and having to manage multiple CI providers is painful. --- .circleci/config.yml | 78 ------------------------------------ .github/workflows/docker.yml | 72 +++++++++++++++++++++++++++++++++ changelog.d/10416.misc | 1 + 3 files changed, 73 insertions(+), 78 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/docker.yml create mode 100644 changelog.d/10416.misc diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index cf1989eff9..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,78 +0,0 @@ -version: 2.1 -jobs: - dockerhubuploadrelease: - docker: - - image: docker:git - steps: - - checkout - - docker_prepare - - run: docker login --username $DOCKER_HUB_USERNAME --password $DOCKER_HUB_PASSWORD - # for release builds, we want to get the amd64 image out asap, so first - # we do an amd64-only build, before following up with a multiarch build. - - docker_build: - tag: -t matrixdotorg/synapse:${CIRCLE_TAG} - platforms: linux/amd64 - - docker_build: - tag: -t matrixdotorg/synapse:${CIRCLE_TAG} - platforms: linux/amd64,linux/arm64 - - dockerhubuploadlatest: - docker: - - image: docker:git - steps: - - checkout - - docker_prepare - - run: docker login --username $DOCKER_HUB_USERNAME --password $DOCKER_HUB_PASSWORD - # for `latest`, we don't want the arm images to disappear, so don't update the tag - # until all of the platforms are built. - - docker_build: - tag: -t matrixdotorg/synapse:latest - platforms: linux/amd64,linux/arm64 - -workflows: - build: - jobs: - - dockerhubuploadrelease: - filters: - tags: - only: /v[0-9].[0-9]+.[0-9]+.*/ - branches: - ignore: /.*/ - - dockerhubuploadlatest: - filters: - branches: - only: [ master, main ] - -commands: - docker_prepare: - description: Sets up a remote docker server, downloads the buildx cli plugin, and enables multiarch images - parameters: - buildx_version: - type: string - default: "v0.4.1" - steps: - - setup_remote_docker: - # 19.03.13 was the most recent available on circleci at the time of - # writing. - version: 19.03.13 - - run: apk add --no-cache curl - - run: mkdir -vp ~/.docker/cli-plugins/ ~/dockercache - - run: curl --silent -L "https://github.com/docker/buildx/releases/download/<< parameters.buildx_version >>/buildx-<< parameters.buildx_version >>.linux-amd64" > ~/.docker/cli-plugins/docker-buildx - - run: chmod a+x ~/.docker/cli-plugins/docker-buildx - # install qemu links in /proc/sys/fs/binfmt_misc on the docker instance running the circleci job - - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - # create a context named `builder` for the builds - - run: docker context create builder - # create a buildx builder using the new context, and set it as the default - - run: docker buildx create builder --use - - docker_build: - description: Builds and pushed images to dockerhub using buildx - parameters: - platforms: - type: string - default: linux/amd64 - tag: - type: string - steps: - - run: docker buildx build -f docker/Dockerfile --push --platform << parameters.platforms >> --label gitsha1=${CIRCLE_SHA1} << parameters.tag >> --progress=plain . diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..8bdefb3905 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,72 @@ +# GitHub actions workflow which builds and publishes the docker images. + +name: Build docker images + +on: + push: + tags: ["v*"] + branches: [ master, main ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + id: qemu + uses: docker/setup-qemu-action@v1 + with: + platforms: arm64 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Inspect builder + run: docker buildx inspect + + - name: Log in to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Calculate docker image tag + id: set-tag + run: | + case "${GITHUB_REF}" in + refs/heads/master|refs/heads/main) + tag=latest + ;; + refs/tags/*) + tag=${GITHUB_REF#refs/tags/} + ;; + *) + tag=${GITHUB_SHA} + ;; + esac + echo "::set-output name=tag::$tag" + + # for release builds, we want to get the amd64 image out asap, so first + # we do an amd64-only build, before following up with a multiarch build. + - name: Build and push amd64 + uses: docker/build-push-action@v2 + if: "${{ startsWith(github.ref, 'refs/tags/v' }}" + with: + push: true + labels: "gitsha1=${{ github.sha }}" + tags: "matrixdotorg/synapse:${{ steps.set-tag.outputs.tag }}" + file: "docker/Dockerfile" + platforms: linux/amd64 + + - name: Build and push all platforms + uses: docker/build-push-action@v2 + with: + push: true + labels: "gitsha1=${{ github.sha }}" + tags: "matrixdotorg/synapse:${{ steps.set-tag.outputs.tag }}" + file: "docker/Dockerfile" + platforms: linux/amd64,linux/arm64 diff --git a/changelog.d/10416.misc b/changelog.d/10416.misc new file mode 100644 index 0000000000..fa648372f5 --- /dev/null +++ b/changelog.d/10416.misc @@ -0,0 +1 @@ +Move docker image build to Github Actions. From 5db118626bebb9ce3913758282787d47cd8f375e Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 21 Jul 2021 09:47:56 -0400 Subject: [PATCH 444/619] Add a return type to parse_string. (#10438) And set the required attribute in a few places which will error if a parameter is not provided. --- changelog.d/10438.misc | 1 + synapse/http/servlet.py | 38 ++++++++++++++++- synapse/rest/admin/users.py | 4 +- synapse/rest/client/v1/room.py | 8 ++-- synapse/rest/client/v2_alpha/keys.py | 2 +- synapse/rest/client/v2_alpha/relations.py | 42 +++++++++++-------- synapse/rest/client/v2_alpha/sync.py | 2 +- synapse/rest/consent/consent_resource.py | 2 +- synapse/rest/media/v1/preview_url_resource.py | 10 ++--- synapse/storage/databases/main/__init__.py | 2 +- synapse/storage/databases/main/room.py | 2 +- synapse/storage/databases/main/stats.py | 2 +- synapse/streams/config.py | 16 +++---- 13 files changed, 86 insertions(+), 45 deletions(-) create mode 100644 changelog.d/10438.misc diff --git a/changelog.d/10438.misc b/changelog.d/10438.misc new file mode 100644 index 0000000000..a557578499 --- /dev/null +++ b/changelog.d/10438.misc @@ -0,0 +1 @@ +Improve servlet type hints. diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 04560fb589..cf45b6623b 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -172,6 +172,42 @@ def parse_bytes_from_args( return default +@overload +def parse_string( + request: Request, + name: str, + default: str, + *, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> str: + ... + + +@overload +def parse_string( + request: Request, + name: str, + *, + required: Literal[True], + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> str: + ... + + +@overload +def parse_string( + request: Request, + name: str, + *, + required: bool = False, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> Optional[str]: + ... + + def parse_string( request: Request, name: str, @@ -179,7 +215,7 @@ def parse_string( required: bool = False, allowed_values: Optional[Iterable[str]] = None, encoding: str = "ascii", -): +) -> Optional[str]: """ Parse a string parameter from the request query string. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 589e47fa47..6736536172 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -90,8 +90,8 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: errcode=Codes.INVALID_PARAM, ) - user_id = parse_string(request, "user_id", default=None) - name = parse_string(request, "name", default=None) + user_id = parse_string(request, "user_id") + name = parse_string(request, "name") guests = parse_boolean(request, "guests", default=True) deactivated = parse_boolean(request, "deactivated", default=False) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index c95c5ae234..5d309a534c 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -413,7 +413,7 @@ async def on_POST(self, request, room_id): assert_params_in_dict(body, ["state_events_at_start", "events"]) prev_events_from_query = parse_strings_from_args(request.args, "prev_event") - chunk_id_from_query = parse_string(request, "chunk_id", default=None) + chunk_id_from_query = parse_string(request, "chunk_id") if prev_events_from_query is None: raise SynapseError( @@ -735,7 +735,7 @@ def __init__(self, hs): self.auth = hs.get_auth() async def on_GET(self, request): - server = parse_string(request, "server", default=None) + server = parse_string(request, "server") try: await self.auth.get_user_by_req(request, allow_guest=True) @@ -755,7 +755,7 @@ async def on_GET(self, request): raise e limit = parse_integer(request, "limit", 0) - since_token = parse_string(request, "since", None) + since_token = parse_string(request, "since") if limit == 0: # zero is a special value which corresponds to no limit. @@ -789,7 +789,7 @@ async def on_GET(self, request): async def on_POST(self, request): await self.auth.get_user_by_req(request, allow_guest=True) - server = parse_string(request, "server", default=None) + server = parse_string(request, "server") content = parse_json_object_from_request(request) limit: Optional[int] = int(content.get("limit", 100)) diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 33cf8de186..d0d9d30d40 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -194,7 +194,7 @@ def __init__(self, hs): async def on_GET(self, request): requester = await self.auth.get_user_by_req(request, allow_guest=True) - from_token_string = parse_string(request, "from") + from_token_string = parse_string(request, "from", required=True) set_tag("from", from_token_string) # We want to enforce they do pass us one, but we ignore it and return diff --git a/synapse/rest/client/v2_alpha/relations.py b/synapse/rest/client/v2_alpha/relations.py index c7da6759db..0821cd285f 100644 --- a/synapse/rest/client/v2_alpha/relations.py +++ b/synapse/rest/client/v2_alpha/relations.py @@ -158,19 +158,21 @@ async def on_GET( event = await self.event_handler.get_event(requester.user, room_id, parent_id) limit = parse_integer(request, "limit", default=5) - from_token = parse_string(request, "from") - to_token = parse_string(request, "to") + from_token_str = parse_string(request, "from") + to_token_str = parse_string(request, "to") if event.internal_metadata.is_redacted(): # If the event is redacted, return an empty list of relations pagination_chunk = PaginationChunk(chunk=[]) else: # Return the relations - if from_token: - from_token = RelationPaginationToken.from_string(from_token) + from_token = None + if from_token_str: + from_token = RelationPaginationToken.from_string(from_token_str) - if to_token: - to_token = RelationPaginationToken.from_string(to_token) + to_token = None + if to_token_str: + to_token = RelationPaginationToken.from_string(to_token_str) pagination_chunk = await self.store.get_relations_for_event( event_id=parent_id, @@ -256,19 +258,21 @@ async def on_GET( raise SynapseError(400, "Relation type must be 'annotation'") limit = parse_integer(request, "limit", default=5) - from_token = parse_string(request, "from") - to_token = parse_string(request, "to") + from_token_str = parse_string(request, "from") + to_token_str = parse_string(request, "to") if event.internal_metadata.is_redacted(): # If the event is redacted, return an empty list of relations pagination_chunk = PaginationChunk(chunk=[]) else: # Return the relations - if from_token: - from_token = AggregationPaginationToken.from_string(from_token) + from_token = None + if from_token_str: + from_token = AggregationPaginationToken.from_string(from_token_str) - if to_token: - to_token = AggregationPaginationToken.from_string(to_token) + to_token = None + if to_token_str: + to_token = AggregationPaginationToken.from_string(to_token_str) pagination_chunk = await self.store.get_aggregation_groups_for_event( event_id=parent_id, @@ -336,14 +340,16 @@ async def on_GET(self, request, room_id, parent_id, relation_type, event_type, k raise SynapseError(400, "Relation type must be 'annotation'") limit = parse_integer(request, "limit", default=5) - from_token = parse_string(request, "from") - to_token = parse_string(request, "to") + from_token_str = parse_string(request, "from") + to_token_str = parse_string(request, "to") - if from_token: - from_token = RelationPaginationToken.from_string(from_token) + from_token = None + if from_token_str: + from_token = RelationPaginationToken.from_string(from_token_str) - if to_token: - to_token = RelationPaginationToken.from_string(to_token) + to_token = None + if to_token_str: + to_token = RelationPaginationToken.from_string(to_token_str) result = await self.store.get_relations_for_event( event_id=parent_id, diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index ecbbcf3851..7bb4e6b8aa 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -112,7 +112,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: default="online", allowed_values=self.ALLOWED_PRESENCE, ) - filter_id = parse_string(request, "filter", default=None) + filter_id = parse_string(request, "filter") full_state = parse_boolean(request, "full_state", default=False) logger.debug( diff --git a/synapse/rest/consent/consent_resource.py b/synapse/rest/consent/consent_resource.py index 4282e2b228..11f7320832 100644 --- a/synapse/rest/consent/consent_resource.py +++ b/synapse/rest/consent/consent_resource.py @@ -112,7 +112,7 @@ async def _async_render_GET(self, request): request (twisted.web.http.Request): """ version = parse_string(request, "v", default=self._default_consent_version) - username = parse_string(request, "u", required=False, default="") + username = parse_string(request, "u", default="") userhmac = None has_consented = False public_version = username == "" diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 8e7fead3a2..172212ee3a 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -186,15 +186,11 @@ async def _async_render_OPTIONS(self, request: Request) -> None: respond_with_json(request, 200, {}, send_cors=True) async def _async_render_GET(self, request: SynapseRequest) -> None: - # This will always be set by the time Twisted calls us. - assert request.args is not None - # XXX: if get_user_by_req fails, what should we do in an async render? requester = await self.auth.get_user_by_req(request) - url = parse_string(request, "url") - if b"ts" in request.args: - ts = parse_integer(request, "ts") - else: + url = parse_string(request, "url", required=True) + ts = parse_integer(request, "ts") + if ts is None: ts = self.clock.time_msec() # XXX: we could move this into _do_preview if we wanted. diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index a3fddea042..bacfbce4af 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -249,7 +249,7 @@ async def get_users_paginate( name: Optional[str] = None, guests: bool = True, deactivated: bool = False, - order_by: UserSortOrder = UserSortOrder.USER_ID.value, + order_by: str = UserSortOrder.USER_ID.value, direction: str = "f", ) -> Tuple[List[JsonDict], int]: """Function to retrieve a paginated list of users from diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 6ddafe5434..443e5f3315 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -363,7 +363,7 @@ async def get_rooms_paginate( self, start: int, limit: int, - order_by: RoomSortOrder, + order_by: str, reverse_order: bool, search_term: Optional[str], ) -> Tuple[List[Dict[str, Any]], int]: diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index 59d67c255b..0f9aa54ca9 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -647,7 +647,7 @@ async def get_users_media_usage_paginate( limit: int, from_ts: Optional[int] = None, until_ts: Optional[int] = None, - order_by: Optional[UserSortOrder] = UserSortOrder.USER_ID.value, + order_by: Optional[str] = UserSortOrder.USER_ID.value, direction: Optional[str] = "f", search_term: Optional[str] = None, ) -> Tuple[List[JsonDict], Dict[str, int]]: diff --git a/synapse/streams/config.py b/synapse/streams/config.py index 13d300588b..cf4005984b 100644 --- a/synapse/streams/config.py +++ b/synapse/streams/config.py @@ -47,20 +47,22 @@ async def from_request( ) -> "PaginationConfig": direction = parse_string(request, "dir", default="f", allowed_values=["f", "b"]) - from_tok = parse_string(request, "from") - to_tok = parse_string(request, "to") + from_tok_str = parse_string(request, "from") + to_tok_str = parse_string(request, "to") try: - if from_tok == "END": + from_tok = None + if from_tok_str == "END": from_tok = None # For backwards compat. - elif from_tok: - from_tok = await StreamToken.from_string(store, from_tok) + elif from_tok_str: + from_tok = await StreamToken.from_string(store, from_tok_str) except Exception: raise SynapseError(400, "'from' parameter is invalid") try: - if to_tok: - to_tok = await StreamToken.from_string(store, to_tok) + to_tok = None + if to_tok_str: + to_tok = await StreamToken.from_string(store, to_tok_str) except Exception: raise SynapseError(400, "'to' parameter is invalid") From d15e72e511724f2e4729b31808d410c1b1ad9041 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 21 Jul 2021 13:29:54 -0400 Subject: [PATCH 445/619] Update the notification email subject when invited to a space. (#10426) --- changelog.d/10426.feature | 1 + synapse/config/emailconfig.py | 4 +++- synapse/push/mailer.py | 18 +++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10426.feature diff --git a/changelog.d/10426.feature b/changelog.d/10426.feature new file mode 100644 index 0000000000..9cca6dc456 --- /dev/null +++ b/changelog.d/10426.feature @@ -0,0 +1 @@ +Email notifications now state whether an invitation is to a room or a space. diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index bcecbfec03..8d8f166e9b 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -39,12 +39,13 @@ "messages_from_person_and_others": "[%(app)s] You have messages on %(app)s from %(person)s and others...", "invite_from_person": "[%(app)s] %(person)s has invited you to chat on %(app)s...", "invite_from_person_to_room": "[%(app)s] %(person)s has invited you to join the %(room)s room on %(app)s...", + "invite_from_person_to_space": "[%(app)s] %(person)s has invited you to join the %(space)s space on %(app)s...", "password_reset": "[%(server_name)s] Password reset", "email_validation": "[%(server_name)s] Validate your email", } -@attr.s +@attr.s(slots=True, frozen=True) class EmailSubjectConfig: message_from_person_in_room = attr.ib(type=str) message_from_person = attr.ib(type=str) @@ -54,6 +55,7 @@ class EmailSubjectConfig: messages_from_person_and_others = attr.ib(type=str) invite_from_person = attr.ib(type=str) invite_from_person_to_room = attr.ib(type=str) + invite_from_person_to_space = attr.ib(type=str) password_reset = attr.ib(type=str) email_validation = attr.ib(type=str) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 7be5fe1e9b..941fb238b7 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -19,7 +19,7 @@ import bleach import jinja2 -from synapse.api.constants import EventTypes, Membership +from synapse.api.constants import EventTypes, Membership, RoomTypes from synapse.api.errors import StoreError from synapse.config.emailconfig import EmailSubjectConfig from synapse.events import EventBase @@ -600,6 +600,22 @@ async def _make_summary_text_single_room( "app": self.app_name, } + # If the room is a space, it gets a slightly different topic. + create_event_id = room_state_ids.get(("m.room.create", "")) + if create_event_id: + create_event = await self.store.get_event( + create_event_id, allow_none=True + ) + if ( + create_event + and create_event.content.get("room_type") == RoomTypes.SPACE + ): + return self.email_subjects.invite_from_person_to_space % { + "person": inviter_name, + "space": room_name, + "app": self.app_name, + } + return self.email_subjects.invite_from_person_to_room % { "person": inviter_name, "room": room_name, From 5b68816de9e9861f5113e99cc4c8f0779829db6b Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 21 Jul 2021 13:48:06 -0400 Subject: [PATCH 446/619] Fix the hierarchy of OpenID providers in the docs. (#10445) --- changelog.d/10445.doc | 1 + docs/openid.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10445.doc diff --git a/changelog.d/10445.doc b/changelog.d/10445.doc new file mode 100644 index 0000000000..4c023ded7c --- /dev/null +++ b/changelog.d/10445.doc @@ -0,0 +1 @@ +Fix hierarchy of providers on the OpenID page. diff --git a/docs/openid.md b/docs/openid.md index cfaafc5015..f685fd551a 100644 --- a/docs/openid.md +++ b/docs/openid.md @@ -410,7 +410,7 @@ oidc_providers: display_name_template: "{{ user.name }}" ``` -## Apple +### Apple Configuring "Sign in with Apple" (SiWA) requires an Apple Developer account. From 590cc4e888f072f7f0788da1f93d80c7bc86be4a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 21 Jul 2021 14:12:22 -0400 Subject: [PATCH 447/619] Add type hints to additional servlet functions (#10437) Improves type hints for: * parse_{boolean,integer} * parse_{boolean,integer}_from_args * parse_json_{value,object}_from_request And fixes any incorrect calls that resulted from unknown types. --- changelog.d/10437.misc | 1 + synapse/federation/transport/server.py | 13 +- synapse/http/servlet.py | 220 ++++++++++++++++++------ synapse/rest/client/v1/room.py | 2 +- synapse/storage/databases/main/stats.py | 2 +- tests/rest/admin/test_media.py | 4 +- 6 files changed, 176 insertions(+), 66 deletions(-) create mode 100644 changelog.d/10437.misc diff --git a/changelog.d/10437.misc b/changelog.d/10437.misc new file mode 100644 index 0000000000..a557578499 --- /dev/null +++ b/changelog.d/10437.misc @@ -0,0 +1 @@ +Improve servlet type hints. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 2974d4d0cc..5e059d6e09 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -984,7 +984,7 @@ async def on_GET( limit = parse_integer_from_args(query, "limit", 0) since_token = parse_string_from_args(query, "since", None) include_all_networks = parse_boolean_from_args( - query, "include_all_networks", False + query, "include_all_networks", default=False ) third_party_instance_id = parse_string_from_args( query, "third_party_instance_id", None @@ -1908,16 +1908,7 @@ async def on_GET( suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) max_rooms_per_space = parse_integer_from_args(query, "max_rooms_per_space") - exclude_rooms = [] - if b"exclude_rooms" in query: - try: - exclude_rooms = [ - room_id.decode("ascii") for room_id in query[b"exclude_rooms"] - ] - except Exception: - raise SynapseError( - 400, "Bad query parameter for exclude_rooms", Codes.INVALID_PARAM - ) + exclude_rooms = parse_strings_from_args(query, "exclude_rooms", default=[]) return 200, await self.handler.federation_space_summary( origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index cf45b6623b..732a1e6aeb 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -14,47 +14,86 @@ """ This module contains base REST classes for constructing REST servlets. """ import logging -from typing import Dict, Iterable, List, Optional, overload +from typing import Iterable, List, Mapping, Optional, Sequence, overload from typing_extensions import Literal from twisted.web.server import Request from synapse.api.errors import Codes, SynapseError +from synapse.types import JsonDict from synapse.util import json_decoder logger = logging.getLogger(__name__) -def parse_integer(request, name, default=None, required=False): +@overload +def parse_integer(request: Request, name: str, default: int) -> int: + ... + + +@overload +def parse_integer(request: Request, name: str, *, required: Literal[True]) -> int: + ... + + +@overload +def parse_integer( + request: Request, name: str, default: Optional[int] = None, required: bool = False +) -> Optional[int]: + ... + + +def parse_integer( + request: Request, name: str, default: Optional[int] = None, required: bool = False +) -> Optional[int]: """Parse an integer parameter from the request string Args: request: the twisted HTTP request. - name (bytes/unicode): the name of the query parameter. - default (int|None): value to use if the parameter is absent, defaults - to None. - required (bool): whether to raise a 400 SynapseError if the - parameter is absent, defaults to False. + name: the name of the query parameter. + default: value to use if the parameter is absent, defaults to None. + required: whether to raise a 400 SynapseError if the parameter is absent, + defaults to False. Returns: - int|None: An int value or the default. + An int value or the default. Raises: SynapseError: if the parameter is absent and required, or if the parameter is present and not an integer. """ - return parse_integer_from_args(request.args, name, default, required) + args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore + return parse_integer_from_args(args, name, default, required) -def parse_integer_from_args(args, name, default=None, required=False): +def parse_integer_from_args( + args: Mapping[bytes, Sequence[bytes]], + name: str, + default: Optional[int] = None, + required: bool = False, +) -> Optional[int]: + """Parse an integer parameter from the request string + + Args: + args: A mapping of request args as bytes to a list of bytes (e.g. request.args). + name: the name of the query parameter. + default: value to use if the parameter is absent, defaults to None. + required: whether to raise a 400 SynapseError if the parameter is absent, + defaults to False. + + Returns: + An int value or the default. - if not isinstance(name, bytes): - name = name.encode("ascii") + Raises: + SynapseError: if the parameter is absent and required, or if the + parameter is present and not an integer. + """ + name_bytes = name.encode("ascii") - if name in args: + if name_bytes in args: try: - return int(args[name][0]) + return int(args[name_bytes][0]) except Exception: message = "Query parameter %r must be an integer" % (name,) raise SynapseError(400, message, errcode=Codes.INVALID_PARAM) @@ -66,36 +105,102 @@ def parse_integer_from_args(args, name, default=None, required=False): return default -def parse_boolean(request, name, default=None, required=False): +@overload +def parse_boolean(request: Request, name: str, default: bool) -> bool: + ... + + +@overload +def parse_boolean(request: Request, name: str, *, required: Literal[True]) -> bool: + ... + + +@overload +def parse_boolean( + request: Request, name: str, default: Optional[bool] = None, required: bool = False +) -> Optional[bool]: + ... + + +def parse_boolean( + request: Request, name: str, default: Optional[bool] = None, required: bool = False +) -> Optional[bool]: """Parse a boolean parameter from the request query string Args: request: the twisted HTTP request. - name (bytes/unicode): the name of the query parameter. - default (bool|None): value to use if the parameter is absent, defaults - to None. - required (bool): whether to raise a 400 SynapseError if the - parameter is absent, defaults to False. + name: the name of the query parameter. + default: value to use if the parameter is absent, defaults to None. + required: whether to raise a 400 SynapseError if the parameter is absent, + defaults to False. Returns: - bool|None: A bool value or the default. + A bool value or the default. Raises: SynapseError: if the parameter is absent and required, or if the parameter is present and not one of "true" or "false". """ + args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore + return parse_boolean_from_args(args, name, default, required) - return parse_boolean_from_args(request.args, name, default, required) +@overload +def parse_boolean_from_args( + args: Mapping[bytes, Sequence[bytes]], + name: str, + default: bool, +) -> bool: + ... -def parse_boolean_from_args(args, name, default=None, required=False): - if not isinstance(name, bytes): - name = name.encode("ascii") +@overload +def parse_boolean_from_args( + args: Mapping[bytes, Sequence[bytes]], + name: str, + *, + required: Literal[True], +) -> bool: + ... + - if name in args: +@overload +def parse_boolean_from_args( + args: Mapping[bytes, Sequence[bytes]], + name: str, + default: Optional[bool] = None, + required: bool = False, +) -> Optional[bool]: + ... + + +def parse_boolean_from_args( + args: Mapping[bytes, Sequence[bytes]], + name: str, + default: Optional[bool] = None, + required: bool = False, +) -> Optional[bool]: + """Parse a boolean parameter from the request query string + + Args: + args: A mapping of request args as bytes to a list of bytes (e.g. request.args). + name: the name of the query parameter. + default: value to use if the parameter is absent, defaults to None. + required: whether to raise a 400 SynapseError if the parameter is absent, + defaults to False. + + Returns: + A bool value or the default. + + Raises: + SynapseError: if the parameter is absent and required, or if the + parameter is present and not one of "true" or "false". + """ + name_bytes = name.encode("ascii") + + if name_bytes in args: try: - return {b"true": True, b"false": False}[args[name][0]] + return {b"true": True, b"false": False}[args[name_bytes][0]] except Exception: message = ( "Boolean query parameter %r must be one of ['true', 'false']" @@ -111,7 +216,7 @@ def parse_boolean_from_args(args, name, default=None, required=False): @overload def parse_bytes_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[bytes] = None, ) -> Optional[bytes]: @@ -120,7 +225,7 @@ def parse_bytes_from_args( @overload def parse_bytes_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Literal[None] = None, *, @@ -131,7 +236,7 @@ def parse_bytes_from_args( @overload def parse_bytes_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[bytes] = None, required: bool = False, @@ -140,7 +245,7 @@ def parse_bytes_from_args( def parse_bytes_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[bytes] = None, required: bool = False, @@ -241,7 +346,7 @@ def parse_string( parameter is present, must be one of a list of allowed values and is not one of those allowed values. """ - args: Dict[bytes, List[bytes]] = request.args # type: ignore + args: Mapping[bytes, Sequence[bytes]] = request.args # type: ignore return parse_string_from_args( args, name, @@ -275,9 +380,8 @@ def _parse_string_value( @overload def parse_strings_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, - default: Optional[List[str]] = None, *, allowed_values: Optional[Iterable[str]] = None, encoding: str = "ascii", @@ -287,9 +391,20 @@ def parse_strings_from_args( @overload def parse_strings_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], + name: str, + default: List[str], + *, + allowed_values: Optional[Iterable[str]] = None, + encoding: str = "ascii", +) -> List[str]: + ... + + +@overload +def parse_strings_from_args( + args: Mapping[bytes, Sequence[bytes]], name: str, - default: Optional[List[str]] = None, *, required: Literal[True], allowed_values: Optional[Iterable[str]] = None, @@ -300,7 +415,7 @@ def parse_strings_from_args( @overload def parse_strings_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[List[str]] = None, *, @@ -312,7 +427,7 @@ def parse_strings_from_args( def parse_strings_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[List[str]] = None, required: bool = False, @@ -361,7 +476,7 @@ def parse_strings_from_args( @overload def parse_string_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[str] = None, *, @@ -373,7 +488,7 @@ def parse_string_from_args( @overload def parse_string_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[str] = None, *, @@ -386,7 +501,7 @@ def parse_string_from_args( @overload def parse_string_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[str] = None, required: bool = False, @@ -397,7 +512,7 @@ def parse_string_from_args( def parse_string_from_args( - args: Dict[bytes, List[bytes]], + args: Mapping[bytes, Sequence[bytes]], name: str, default: Optional[str] = None, required: bool = False, @@ -445,13 +560,14 @@ def parse_string_from_args( return strings[0] -def parse_json_value_from_request(request, allow_empty_body=False): +def parse_json_value_from_request( + request: Request, allow_empty_body: bool = False +) -> Optional[JsonDict]: """Parse a JSON value from the body of a twisted HTTP request. Args: request: the twisted HTTP request. - allow_empty_body (bool): if True, an empty body will be accepted and - turned into None + allow_empty_body: if True, an empty body will be accepted and turned into None Returns: The JSON value. @@ -460,7 +576,7 @@ def parse_json_value_from_request(request, allow_empty_body=False): SynapseError if the request body couldn't be decoded as JSON. """ try: - content_bytes = request.content.read() + content_bytes = request.content.read() # type: ignore except Exception: raise SynapseError(400, "Error reading JSON content.") @@ -476,13 +592,15 @@ def parse_json_value_from_request(request, allow_empty_body=False): return content -def parse_json_object_from_request(request, allow_empty_body=False): +def parse_json_object_from_request( + request: Request, allow_empty_body: bool = False +) -> JsonDict: """Parse a JSON object from the body of a twisted HTTP request. Args: request: the twisted HTTP request. - allow_empty_body (bool): if True, an empty body will be accepted and - turned into an empty dict. + allow_empty_body: if True, an empty body will be accepted and turned into + an empty dict. Raises: SynapseError if the request body couldn't be decoded as JSON or @@ -493,14 +611,14 @@ def parse_json_object_from_request(request, allow_empty_body=False): if allow_empty_body and content is None: return {} - if type(content) != dict: + if not isinstance(content, dict): message = "Content must be a JSON object." raise SynapseError(400, message, errcode=Codes.BAD_JSON) return content -def assert_params_in_dict(body, required): +def assert_params_in_dict(body: JsonDict, required: Iterable[str]) -> None: absent = [] for k in required: if k not in body: diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 5d309a534c..25ba52c624 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -754,7 +754,7 @@ async def on_GET(self, request): if server: raise e - limit = parse_integer(request, "limit", 0) + limit: Optional[int] = parse_integer(request, "limit", 0) since_token = parse_string(request, "since") if limit == 0: diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index 0f9aa54ca9..889e0d3625 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -650,7 +650,7 @@ async def get_users_media_usage_paginate( order_by: Optional[str] = UserSortOrder.USER_ID.value, direction: Optional[str] = "f", search_term: Optional[str] = None, - ) -> Tuple[List[JsonDict], Dict[str, int]]: + ) -> Tuple[List[JsonDict], int]: """Function to retrieve a paginated list of users and their uploaded local media (size and number). This will return a json list of users and the total number of users matching the filter criteria. diff --git a/tests/rest/admin/test_media.py b/tests/rest/admin/test_media.py index 6fee0f95b6..7198fd293f 100644 --- a/tests/rest/admin/test_media.py +++ b/tests/rest/admin/test_media.py @@ -261,7 +261,7 @@ def test_missing_parameter(self): self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"]) self.assertEqual( - "Missing integer query parameter b'before_ts'", channel.json_body["error"] + "Missing integer query parameter 'before_ts'", channel.json_body["error"] ) def test_invalid_parameter(self): @@ -303,7 +303,7 @@ def test_invalid_parameter(self): self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) self.assertEqual( - "Boolean query parameter b'keep_profiles' must be one of ['true', 'false']", + "Boolean query parameter 'keep_profiles' must be one of ['true', 'false']", channel.json_body["error"], ) From 8ae0bdca753d2f51b32bc712b66c26f331ec728c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 21 Jul 2021 21:25:28 +0100 Subject: [PATCH 448/619] Drop xenial-support hacks (#10429) --- changelog.d/10429.misc | 1 + debian/build_virtualenv | 4 +--- debian/changelog | 6 ++++++ debian/compat | 2 +- debian/control | 5 +---- debian/rules | 4 +--- docker/Dockerfile-dhvirtualenv | 18 +++++++++++------- 7 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 changelog.d/10429.misc diff --git a/changelog.d/10429.misc b/changelog.d/10429.misc new file mode 100644 index 0000000000..ccb2217f64 --- /dev/null +++ b/changelog.d/10429.misc @@ -0,0 +1 @@ +Drop backwards-compatibility code that was required to support Ubuntu Xenial. diff --git a/debian/build_virtualenv b/debian/build_virtualenv index 21caad90cc..68c8659953 100755 --- a/debian/build_virtualenv +++ b/debian/build_virtualenv @@ -33,13 +33,11 @@ esac # Use --builtin-venv to use the better `venv` module from CPython 3.4+ rather # than the 2/3 compatible `virtualenv`. -# Pin pip to 20.3.4 to fix breakage in 21.0 on py3.5 (xenial) - dh_virtualenv \ --install-suffix "matrix-synapse" \ --builtin-venv \ --python "$SNAKE" \ - --upgrade-pip-to="20.3.4" \ + --upgrade-pip \ --preinstall="lxml" \ --preinstall="mock" \ --extra-pip-arg="--no-cache-dir" \ diff --git a/debian/changelog b/debian/changelog index 4d214c23b6..55f7ee003c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.39.0ubuntu1) UNRELEASED; urgency=medium + + * Drop backwards-compatibility code that was required to support Ubuntu Xenial. + + -- Richard van der Hoff Tue, 20 Jul 2021 00:10:03 +0100 + matrix-synapse-py3 (1.39.0~rc1) stable; urgency=medium * New synapse release 1.39.0rc1. diff --git a/debian/compat b/debian/compat index ec635144f6..f599e28b8a 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -9 +10 diff --git a/debian/control b/debian/control index 8167a901a4..763fabd6f6 100644 --- a/debian/control +++ b/debian/control @@ -3,11 +3,8 @@ Section: contrib/python Priority: extra Maintainer: Synapse Packaging team # keep this list in sync with the build dependencies in docker/Dockerfile-dhvirtualenv. -# TODO: Remove the dependency on dh-systemd after dropping support for Ubuntu xenial -# On all other supported releases, it's merely a transitional package which -# does nothing but depends on debhelper (> 9.20160709) Build-Depends: - debhelper (>= 9.20160709) | dh-systemd, + debhelper (>= 10), dh-virtualenv (>= 1.1), libsystemd-dev, libpq-dev, diff --git a/debian/rules b/debian/rules index c744060a57..b9d490adc9 100755 --- a/debian/rules +++ b/debian/rules @@ -51,7 +51,5 @@ override_dh_shlibdeps: override_dh_virtualenv: ./debian/build_virtualenv -# We are restricted to compat level 9 (because xenial), so have to -# enable the systemd bits manually. %: - dh $@ --with python-virtualenv --with systemd + dh $@ --with python-virtualenv diff --git a/docker/Dockerfile-dhvirtualenv b/docker/Dockerfile-dhvirtualenv index 0d74630370..017be8555e 100644 --- a/docker/Dockerfile-dhvirtualenv +++ b/docker/Dockerfile-dhvirtualenv @@ -15,6 +15,15 @@ ARG distro="" ### ### Stage 0: build a dh-virtualenv ### + +# This is only really needed on bionic and focal, since other distributions we +# care about have a recent version of dh-virtualenv by default. Unfortunately, +# it looks like focal is going to be with us for a while. +# +# (focal doesn't have a dh-virtualenv package at all. There is a PPA at +# https://launchpad.net/~jyrki-pulliainen/+archive/ubuntu/dh-virtualenv, but +# it's not obviously easier to use that than to build our own.) + FROM ${distro} as builder RUN apt-get update -qq -o Acquire::Languages=none @@ -27,7 +36,7 @@ RUN env DEBIAN_FRONTEND=noninteractive apt-get install \ wget # fetch and unpack the package -# TODO: Upgrade to 1.2.2 once xenial is dropped +# TODO: Upgrade to 1.2.2 once bionic is dropped (1.2.2 requires debhelper 12; bionic has only 11) RUN mkdir /dh-virtualenv RUN wget -q -O /dh-virtualenv.tar.gz https://github.com/spotify/dh-virtualenv/archive/ac6e1b1.tar.gz RUN tar -xv --strip-components=1 -C /dh-virtualenv -f /dh-virtualenv.tar.gz @@ -59,8 +68,6 @@ ENV LANG C.UTF-8 # # NB: keep this list in sync with the list of build-deps in debian/control # TODO: it would be nice to do that automatically. -# TODO: Remove the dh-systemd stanza after dropping support for Ubuntu xenial -# it's a transitional package on all other, more recent releases RUN apt-get update -qq -o Acquire::Languages=none \ && env DEBIAN_FRONTEND=noninteractive apt-get install \ -yqq --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \ @@ -76,10 +83,7 @@ RUN apt-get update -qq -o Acquire::Languages=none \ python3-venv \ sqlite3 \ libpq-dev \ - xmlsec1 \ - && ( env DEBIAN_FRONTEND=noninteractive apt-get install \ - -yqq --no-install-recommends -o Dpkg::Options::=--force-unsafe-io \ - dh-systemd || true ) + xmlsec1 COPY --from=builder /dh-virtualenv_1.2~dev-1_all.deb / From f1347bcfdcf7e0ff54a81cd05618af8882e4a757 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 22 Jul 2021 11:10:30 +0100 Subject: [PATCH 449/619] Fix the tests-done Github Actions job (#10444) --- .github/workflows/tests.yml | 19 ++++++++++++++++++- changelog.d/10444.misc | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10444.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cef4439477..9759163290 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -347,7 +347,12 @@ jobs: # a job which marks all the other jobs as complete, thus allowing PRs to be merged. tests-done: + if: ${{ always() }} needs: + - lint + - lint-crlf + - lint-newsfile + - lint-sdist - trial - trial-olddeps - sytest @@ -355,4 +360,16 @@ jobs: - complement runs-on: ubuntu-latest steps: - - run: "true" \ No newline at end of file + - name: Set build result + env: + NEEDS_CONTEXT: ${{ toJSON(needs) }} + # the `jq` incantation dumps out a series of " " lines + run: | + set -o pipefail + jq -r 'to_entries[] | [.key,.value.result] | join(" ")' \ + <<< $NEEDS_CONTEXT | + while read job result; do + if [ "$result" != "success" ]; then + echo "::set-failed ::Job $job returned $result" + fi + done diff --git a/changelog.d/10444.misc b/changelog.d/10444.misc new file mode 100644 index 0000000000..c012e89f4b --- /dev/null +++ b/changelog.d/10444.misc @@ -0,0 +1 @@ +Update the `tests-done` Github Actions status. From 5e2df47f72ab3270853da6019aba1aa4d4b2cc56 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 22 Jul 2021 11:35:06 +0100 Subject: [PATCH 450/619] Cancel redundant GHA workflows (#10451) --- .github/workflows/release-artifacts.yml | 4 ++++ .github/workflows/tests.yml | 4 ++++ changelog.d/10451.misc | 1 + 3 files changed, 9 insertions(+) create mode 100644 changelog.d/10451.misc diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 325c1f7d39..0beb418a07 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -12,6 +12,10 @@ on: # we do the full build on tags. tags: ["v*"] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: write diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9759163290..4e61824ee5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,10 @@ on: branches: ["develop", "release-*"] pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: lint: runs-on: ubuntu-latest diff --git a/changelog.d/10451.misc b/changelog.d/10451.misc new file mode 100644 index 0000000000..e38f4b476d --- /dev/null +++ b/changelog.d/10451.misc @@ -0,0 +1 @@ +Cancel redundant GHA workflows when a new commit is pushed. From d518b05a8667943bd0aa9ab1edc91eec0a8283fe Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 22 Jul 2021 05:58:24 -0500 Subject: [PATCH 451/619] Move dev/ docs to development/ (#10453) --- CONTRIBUTING.md | 2 +- changelog.d/10453.doc | 1 + docs/SUMMARY.md | 6 +++--- docs/{dev => development}/cas.md | 0 docs/{dev => development}/git.md | 6 +++--- docs/{dev => development/img}/git/branches.jpg | Bin docs/{dev => development/img}/git/clean.png | Bin docs/{dev => development/img}/git/squash.png | Bin docs/{dev => development}/saml.md | 0 9 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 changelog.d/10453.doc rename docs/{dev => development}/cas.md (100%) rename docs/{dev => development}/git.md (97%) rename docs/{dev => development/img}/git/branches.jpg (100%) rename docs/{dev => development/img}/git/clean.png (100%) rename docs/{dev => development/img}/git/squash.png (100%) rename docs/{dev => development}/saml.md (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4e6688042..80ef6aa235 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -392,7 +392,7 @@ By now, you know the drill! # Notes for maintainers on merging PRs etc There are some notes for those with commit access to the project on how we -manage git [here](docs/dev/git.md). +manage git [here](docs/development/git.md). # Conclusion diff --git a/changelog.d/10453.doc b/changelog.d/10453.doc new file mode 100644 index 0000000000..5d4db9bca2 --- /dev/null +++ b/changelog.d/10453.doc @@ -0,0 +1 @@ +Consolidate development documentation to `docs/development/`. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index db4ef1a44e..f1bde91420 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -67,7 +67,7 @@ # Development - [Contributing Guide](development/contributing_guide.md) - [Code Style](code_style.md) - - [Git Usage](dev/git.md) + - [Git Usage](development/git.md) - [Testing]() - [OpenTracing](opentracing.md) - [Database Schemas](development/database_schema.md) @@ -77,8 +77,8 @@ - [TCP Replication](tcp_replication.md) - [Internal Documentation](development/internal_documentation/README.md) - [Single Sign-On]() - - [SAML](dev/saml.md) - - [CAS](dev/cas.md) + - [SAML](development/saml.md) + - [CAS](development/cas.md) - [State Resolution]() - [The Auth Chain Difference Algorithm](auth_chain_difference_algorithm.md) - [Media Repository](media_repository.md) diff --git a/docs/dev/cas.md b/docs/development/cas.md similarity index 100% rename from docs/dev/cas.md rename to docs/development/cas.md diff --git a/docs/dev/git.md b/docs/development/git.md similarity index 97% rename from docs/dev/git.md rename to docs/development/git.md index 87950f07b2..9b1ed54b65 100644 --- a/docs/dev/git.md +++ b/docs/development/git.md @@ -9,7 +9,7 @@ commits each of which contains a single change building on what came before. Here, by way of an arbitrary example, is the top of `git log --graph b2dba0607`: -clean git graph +clean git graph Note how the commit comment explains clearly what is changing and why. Also note the *absence* of merge commits, as well as the absence of commits called @@ -61,7 +61,7 @@ Ok, so that's what we'd like to achieve. How do we achieve it? The TL;DR is: when you come to merge a pull request, you *probably* want to “squash and merge”: -![squash and merge](git/squash.png). +![squash and merge](img/git/squash.png). (This applies whether you are merging your own PR, or that of another contributor.) @@ -105,7 +105,7 @@ complicated. Here's how we do it. Let's start with a picture: -![branching model](git/branches.jpg) +![branching model](img/git/branches.jpg) It looks complicated, but it's really not. There's one basic rule: *anyone* is free to merge from *any* more-stable branch to *any* less-stable branch at diff --git a/docs/dev/git/branches.jpg b/docs/development/img/git/branches.jpg similarity index 100% rename from docs/dev/git/branches.jpg rename to docs/development/img/git/branches.jpg diff --git a/docs/dev/git/clean.png b/docs/development/img/git/clean.png similarity index 100% rename from docs/dev/git/clean.png rename to docs/development/img/git/clean.png diff --git a/docs/dev/git/squash.png b/docs/development/img/git/squash.png similarity index 100% rename from docs/dev/git/squash.png rename to docs/development/img/git/squash.png diff --git a/docs/dev/saml.md b/docs/development/saml.md similarity index 100% rename from docs/dev/saml.md rename to docs/development/saml.md From d8324b8238a31b8d749b1dfe507c3bed3bcc6e17 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 22 Jul 2021 12:00:16 +0100 Subject: [PATCH 452/619] Fix a handful of type annotations. (#10446) * switch from `types.CoroutineType` to `typing.Coroutine` these should be identical semantically, and since `defer.ensureDeferred` is defined to take a `typing.Coroutine`, will keep mypy happy * Fix some annotations on inlineCallbacks functions * changelog --- changelog.d/10446.misc | 1 + synapse/http/federation/matrix_federation_agent.py | 4 ++-- synapse/logging/context.py | 4 ++-- synapse/module_api/__init__.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10446.misc diff --git a/changelog.d/10446.misc b/changelog.d/10446.misc new file mode 100644 index 0000000000..a5a0ca80eb --- /dev/null +++ b/changelog.d/10446.misc @@ -0,0 +1 @@ +Update type annotations to work with forthcoming Twisted 21.7.0 release. diff --git a/synapse/http/federation/matrix_federation_agent.py b/synapse/http/federation/matrix_federation_agent.py index 950770201a..c16b7f10e6 100644 --- a/synapse/http/federation/matrix_federation_agent.py +++ b/synapse/http/federation/matrix_federation_agent.py @@ -27,7 +27,7 @@ ) from twisted.web.client import URI, Agent, HTTPConnectionPool from twisted.web.http_headers import Headers -from twisted.web.iweb import IAgent, IAgentEndpointFactory, IBodyProducer +from twisted.web.iweb import IAgent, IAgentEndpointFactory, IBodyProducer, IResponse from synapse.crypto.context_factory import FederationPolicyForHTTPS from synapse.http.client import BlacklistingAgentWrapper @@ -116,7 +116,7 @@ def request( uri: bytes, headers: Optional[Headers] = None, bodyProducer: Optional[IBodyProducer] = None, - ) -> Generator[defer.Deferred, Any, defer.Deferred]: + ) -> Generator[defer.Deferred, Any, IResponse]: """ Args: method: HTTP method: GET/POST/etc diff --git a/synapse/logging/context.py b/synapse/logging/context.py index 18ac507802..02e5ddd2ef 100644 --- a/synapse/logging/context.py +++ b/synapse/logging/context.py @@ -25,7 +25,7 @@ import inspect import logging import threading -import types +import typing import warnings from typing import TYPE_CHECKING, Optional, Tuple, TypeVar, Union @@ -745,7 +745,7 @@ def run_in_background(f, *args, **kwargs) -> defer.Deferred: # by synchronous exceptions, so let's turn them into Failures. return defer.fail() - if isinstance(res, types.CoroutineType): + if isinstance(res, typing.Coroutine): res = defer.ensureDeferred(res) # At this point we should have a Deferred, if not then f was a synchronous diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 1259fc2d90..473812b8e2 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -484,7 +484,7 @@ async def complete_sso_login_async( @defer.inlineCallbacks def get_state_events_in_room( self, room_id: str, types: Iterable[Tuple[str, Optional[str]]] - ) -> Generator[defer.Deferred, Any, defer.Deferred]: + ) -> Generator[defer.Deferred, Any, Iterable[EventBase]]: """Gets current state events for the given room. (This is exposed for compatibility with the old SpamCheckerApi. We should From 38b346a504cd4155b1986d50ebcff2199e1690be Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 22 Jul 2021 12:39:50 +0100 Subject: [PATCH 453/619] Replace `or_ignore` in `simple_insert` with `simple_upsert` (#10442) Now that we have `simple_upsert` that should be used in preference to trying to insert and looking for an exception. The main benefit is that we ERROR message don't get written to postgres logs. We also have tidy up the return value on `simple_upsert`, rather than having a tri-state of inserted/not-inserted/unknown. --- changelog.d/10442.misc | 1 + synapse/storage/database.py | 51 ++++++-------- synapse/storage/databases/main/devices.py | 9 ++- .../databases/main/monthly_active_users.py | 8 +-- .../storage/databases/main/transactions.py | 8 ++- .../storage/databases/main/user_directory.py | 66 ++++--------------- 6 files changed, 44 insertions(+), 99 deletions(-) create mode 100644 changelog.d/10442.misc diff --git a/changelog.d/10442.misc b/changelog.d/10442.misc new file mode 100644 index 0000000000..b8d412d732 --- /dev/null +++ b/changelog.d/10442.misc @@ -0,0 +1 @@ +Replace usage of `or_ignore` in `simple_insert` with `simple_upsert` usage, to stop spamming postgres logs with spurious ERROR messages. diff --git a/synapse/storage/database.py b/synapse/storage/database.py index ccf9ac51ef..4d4643619f 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -832,31 +832,16 @@ async def simple_insert( self, table: str, values: Dict[str, Any], - or_ignore: bool = False, desc: str = "simple_insert", - ) -> bool: + ) -> None: """Executes an INSERT query on the named table. Args: table: string giving the table name values: dict of new column names and values for them - or_ignore: bool stating whether an exception should be raised - when a conflicting row already exists. If True, False will be - returned by the function instead desc: description of the transaction, for logging and metrics - - Returns: - Whether the row was inserted or not. Only useful when `or_ignore` is True """ - try: - await self.runInteraction(desc, self.simple_insert_txn, table, values) - except self.engine.module.IntegrityError: - # We have to do or_ignore flag at this layer, since we can't reuse - # a cursor after we receive an error from the db. - if not or_ignore: - raise - return False - return True + await self.runInteraction(desc, self.simple_insert_txn, table, values) @staticmethod def simple_insert_txn( @@ -930,7 +915,7 @@ async def simple_upsert( insertion_values: Optional[Dict[str, Any]] = None, desc: str = "simple_upsert", lock: bool = True, - ) -> Optional[bool]: + ) -> bool: """ `lock` should generally be set to True (the default), but can be set @@ -951,8 +936,8 @@ async def simple_upsert( desc: description of the transaction, for logging and metrics lock: True to lock the table when doing the upsert. Returns: - Native upserts always return None. Emulated upserts return True if a - new entry was created, False if an existing one was updated. + Returns True if a row was inserted or updated (i.e. if `values` is + not empty then this always returns True) """ insertion_values = insertion_values or {} @@ -995,7 +980,7 @@ def simple_upsert_txn( values: Dict[str, Any], insertion_values: Optional[Dict[str, Any]] = None, lock: bool = True, - ) -> Optional[bool]: + ) -> bool: """ Pick the UPSERT method which works best on the platform. Either the native one (Pg9.5+, recent SQLites), or fall back to an emulated method. @@ -1008,16 +993,15 @@ def simple_upsert_txn( insertion_values: additional key/values to use only when inserting lock: True to lock the table when doing the upsert. Returns: - Native upserts always return None. Emulated upserts return True if a - new entry was created, False if an existing one was updated. + Returns True if a row was inserted or updated (i.e. if `values` is + not empty then this always returns True) """ insertion_values = insertion_values or {} if self.engine.can_native_upsert and table not in self._unsafe_to_upsert_tables: - self.simple_upsert_txn_native_upsert( + return self.simple_upsert_txn_native_upsert( txn, table, keyvalues, values, insertion_values=insertion_values ) - return None else: return self.simple_upsert_txn_emulated( txn, @@ -1045,8 +1029,8 @@ def simple_upsert_txn_emulated( insertion_values: additional key/values to use only when inserting lock: True to lock the table when doing the upsert. Returns: - Returns True if a new entry was created, False if an existing - one was updated. + Returns True if a row was inserted or updated (i.e. if `values` is + not empty then this always returns True) """ insertion_values = insertion_values or {} @@ -1086,8 +1070,7 @@ def _getwhere(key): txn.execute(sql, sqlargs) if txn.rowcount > 0: - # successfully updated at least one row. - return False + return True # We didn't find any existing rows, so insert a new one allvalues: Dict[str, Any] = {} @@ -1111,15 +1094,19 @@ def simple_upsert_txn_native_upsert( keyvalues: Dict[str, Any], values: Dict[str, Any], insertion_values: Optional[Dict[str, Any]] = None, - ) -> None: + ) -> bool: """ - Use the native UPSERT functionality in recent PostgreSQL versions. + Use the native UPSERT functionality in PostgreSQL. Args: table: The table to upsert into keyvalues: The unique key tables and their new values values: The nonunique columns and their new values insertion_values: additional key/values to use only when inserting + + Returns: + Returns True if a row was inserted or updated (i.e. if `values` is + not empty then this always returns True) """ allvalues: Dict[str, Any] = {} allvalues.update(keyvalues) @@ -1140,6 +1127,8 @@ def simple_upsert_txn_native_upsert( ) txn.execute(sql, list(allvalues.values())) + return bool(txn.rowcount) + async def simple_upsert_many( self, table: str, diff --git a/synapse/storage/databases/main/devices.py b/synapse/storage/databases/main/devices.py index 18f07d96dc..3816a0ca53 100644 --- a/synapse/storage/databases/main/devices.py +++ b/synapse/storage/databases/main/devices.py @@ -1078,16 +1078,18 @@ async def store_device( return False try: - inserted = await self.db_pool.simple_insert( + inserted = await self.db_pool.simple_upsert( "devices", - values={ + keyvalues={ "user_id": user_id, "device_id": device_id, + }, + values={}, + insertion_values={ "display_name": initial_device_display_name, "hidden": False, }, desc="store_device", - or_ignore=True, ) if not inserted: # if the device already exists, check if it's a real device, or @@ -1099,6 +1101,7 @@ async def store_device( ) if hidden: raise StoreError(400, "The device ID is in use", Codes.FORBIDDEN) + self.device_id_exists_cache.set(key, True) return inserted except StoreError: diff --git a/synapse/storage/databases/main/monthly_active_users.py b/synapse/storage/databases/main/monthly_active_users.py index fe25638289..d213b26703 100644 --- a/synapse/storage/databases/main/monthly_active_users.py +++ b/synapse/storage/databases/main/monthly_active_users.py @@ -297,17 +297,13 @@ def upsert_monthly_active_user_txn(self, txn, user_id): Args: txn (cursor): user_id (str): user to add/update - - Returns: - bool: True if a new entry was created, False if an - existing one was updated. """ # Am consciously deciding to lock the table on the basis that is ought # never be a big table and alternative approaches (batching multiple # upserts into a single txn) introduced a lot of extra complexity. # See https://github.com/matrix-org/synapse/issues/3854 for more - is_insert = self.db_pool.simple_upsert_txn( + self.db_pool.simple_upsert_txn( txn, table="monthly_active_users", keyvalues={"user_id": user_id}, @@ -322,8 +318,6 @@ def upsert_monthly_active_user_txn(self, txn, user_id): txn, self.user_last_seen_monthly_active, (user_id,) ) - return is_insert - async def populate_monthly_active_users(self, user_id): """Checks on the state of monthly active user limits and optionally add the user to the monthly active tables diff --git a/synapse/storage/databases/main/transactions.py b/synapse/storage/databases/main/transactions.py index d211c423b2..7728d5f102 100644 --- a/synapse/storage/databases/main/transactions.py +++ b/synapse/storage/databases/main/transactions.py @@ -134,16 +134,18 @@ async def set_received_txn_response( response_dict: The response, to be encoded into JSON. """ - await self.db_pool.simple_insert( + await self.db_pool.simple_upsert( table="received_transactions", - values={ + keyvalues={ "transaction_id": transaction_id, "origin": origin, + }, + values={}, + insertion_values={ "response_code": code, "response_json": db_binary_type(encode_canonical_json(response_dict)), "ts": self._clock.time_msec(), }, - or_ignore=True, desc="set_received_txn_response", ) diff --git a/synapse/storage/databases/main/user_directory.py b/synapse/storage/databases/main/user_directory.py index a6bfb4902a..9d28d69ac7 100644 --- a/synapse/storage/databases/main/user_directory.py +++ b/synapse/storage/databases/main/user_directory.py @@ -377,7 +377,7 @@ async def update_profile_in_user_dir( avatar_url = None def _update_profile_in_user_dir_txn(txn): - new_entry = self.db_pool.simple_upsert_txn( + self.db_pool.simple_upsert_txn( txn, table="user_directory", keyvalues={"user_id": user_id}, @@ -388,8 +388,7 @@ def _update_profile_in_user_dir_txn(txn): if isinstance(self.database_engine, PostgresEngine): # We weight the localpart most highly, then display name and finally # server name - if self.database_engine.can_native_upsert: - sql = """ + sql = """ INSERT INTO user_directory_search(user_id, vector) VALUES (?, setweight(to_tsvector('simple', ?), 'A') @@ -397,58 +396,15 @@ def _update_profile_in_user_dir_txn(txn): || setweight(to_tsvector('simple', COALESCE(?, '')), 'B') ) ON CONFLICT (user_id) DO UPDATE SET vector=EXCLUDED.vector """ - txn.execute( - sql, - ( - user_id, - get_localpart_from_id(user_id), - get_domain_from_id(user_id), - display_name, - ), - ) - else: - # TODO: Remove this code after we've bumped the minimum version - # of postgres to always support upserts, so we can get rid of - # `new_entry` usage - if new_entry is True: - sql = """ - INSERT INTO user_directory_search(user_id, vector) - VALUES (?, - setweight(to_tsvector('simple', ?), 'A') - || setweight(to_tsvector('simple', ?), 'D') - || setweight(to_tsvector('simple', COALESCE(?, '')), 'B') - ) - """ - txn.execute( - sql, - ( - user_id, - get_localpart_from_id(user_id), - get_domain_from_id(user_id), - display_name, - ), - ) - elif new_entry is False: - sql = """ - UPDATE user_directory_search - SET vector = setweight(to_tsvector('simple', ?), 'A') - || setweight(to_tsvector('simple', ?), 'D') - || setweight(to_tsvector('simple', COALESCE(?, '')), 'B') - WHERE user_id = ? - """ - txn.execute( - sql, - ( - get_localpart_from_id(user_id), - get_domain_from_id(user_id), - display_name, - user_id, - ), - ) - else: - raise RuntimeError( - "upsert returned None when 'can_native_upsert' is False" - ) + txn.execute( + sql, + ( + user_id, + get_localpart_from_id(user_id), + get_domain_from_id(user_id), + display_name, + ), + ) elif isinstance(self.database_engine, Sqlite3Engine): value = "%s %s" % (user_id, display_name) if display_name else user_id self.db_pool.simple_upsert_txn( From 89c4ca81bb597159e456449c548ba3f166843ddc Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 22 Jul 2021 16:05:16 +0200 Subject: [PATCH 454/619] Add `creation_ts` to list users admin API (#10448) Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10448.feature | 1 + docs/admin_api/user_admin_api.md | 10 +++-- synapse/rest/admin/users.py | 2 + synapse/storage/databases/main/__init__.py | 19 ++++----- synapse/storage/databases/main/stats.py | 2 + tests/rest/admin/test_user.py | 45 +++++++++++++--------- 6 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 changelog.d/10448.feature diff --git a/changelog.d/10448.feature b/changelog.d/10448.feature new file mode 100644 index 0000000000..f6579e0ca8 --- /dev/null +++ b/changelog.d/10448.feature @@ -0,0 +1 @@ +Add `creation_ts` to list users admin API. \ No newline at end of file diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 4a65d0c3bc..160899754e 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -144,7 +144,8 @@ A response body like the following is returned: "deactivated": 0, "shadow_banned": 0, "displayname": "", - "avatar_url": null + "avatar_url": null, + "creation_ts": 1560432668000 }, { "name": "", "is_guest": 0, @@ -153,7 +154,8 @@ A response body like the following is returned: "deactivated": 0, "shadow_banned": 0, "displayname": "", - "avatar_url": "" + "avatar_url": "", + "creation_ts": 1561550621000 } ], "next_token": "100", @@ -197,11 +199,12 @@ The following parameters should be set in the URL: - `shadow_banned` - Users are ordered by `shadow_banned` status. - `displayname` - Users are ordered alphabetically by `displayname`. - `avatar_url` - Users are ordered alphabetically by avatar URL. + - `creation_ts` - Users are ordered by when the users was created in ms. - `dir` - Direction of media order. Either `f` for forwards or `b` for backwards. Setting this value to `b` will reverse the above sort order. Defaults to `f`. -Caution. The database only has indexes on the columns `name` and `created_ts`. +Caution. The database only has indexes on the columns `name` and `creation_ts`. This means that if a different sort order is used (`is_guest`, `admin`, `user_type`, `deactivated`, `shadow_banned`, `avatar_url` or `displayname`), this can cause a large load on the database, especially for large environments. @@ -222,6 +225,7 @@ The following fields are returned in the JSON response body: - `shadow_banned` - bool - Status if that user has been marked as shadow banned. - `displayname` - string - The user's display name if they have set one. - `avatar_url` - string - The user's avatar URL if they have set one. + - `creation_ts` - integer - The user's creation timestamp in ms. - `next_token`: string representing a positive integer - Indication for pagination. See above. - `total` - integer - Total number of media. diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 6736536172..eef76ab18a 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -62,6 +62,7 @@ class UsersRestServletV2(RestServlet): The parameter `name` can be used to filter by user id or display name. The parameter `guests` can be used to exclude guest users. The parameter `deactivated` can be used to include deactivated users. + The parameter `order_by` can be used to order the result. """ def __init__(self, hs: "HomeServer"): @@ -108,6 +109,7 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: UserSortOrder.USER_TYPE.value, UserSortOrder.AVATAR_URL.value, UserSortOrder.SHADOW_BANNED.value, + UserSortOrder.CREATION_TS.value, ), ) diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index bacfbce4af..8d9f07111d 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -297,27 +297,22 @@ def get_users_paginate_txn(txn): where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else "" - sql_base = """ + sql_base = f""" FROM users as u LEFT JOIN profiles AS p ON u.name = '@' || p.user_id || ':' || ? - {} - """.format( - where_clause - ) + {where_clause} + """ sql = "SELECT COUNT(*) as total_users " + sql_base txn.execute(sql, args) count = txn.fetchone()[0] - sql = """ - SELECT name, user_type, is_guest, admin, deactivated, shadow_banned, displayname, avatar_url + sql = f""" + SELECT name, user_type, is_guest, admin, deactivated, shadow_banned, + displayname, avatar_url, creation_ts * 1000 as creation_ts {sql_base} ORDER BY {order_by_column} {order}, u.name ASC LIMIT ? OFFSET ? - """.format( - sql_base=sql_base, - order_by_column=order_by_column, - order=order, - ) + """ args += [limit, start] txn.execute(sql, args) users = self.db_pool.cursor_to_dict(txn) diff --git a/synapse/storage/databases/main/stats.py b/synapse/storage/databases/main/stats.py index 889e0d3625..42edbcc057 100644 --- a/synapse/storage/databases/main/stats.py +++ b/synapse/storage/databases/main/stats.py @@ -75,6 +75,7 @@ class UserSortOrder(Enum): USER_TYPE = ordered alphabetically by `user_type` AVATAR_URL = ordered alphabetically by `avatar_url` SHADOW_BANNED = ordered by `shadow_banned` + CREATION_TS = ordered by `creation_ts` """ MEDIA_LENGTH = "media_length" @@ -88,6 +89,7 @@ class UserSortOrder(Enum): USER_TYPE = "user_type" AVATAR_URL = "avatar_url" SHADOW_BANNED = "shadow_banned" + CREATION_TS = "creation_ts" class StatsStore(StateDeltasStore): diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 4fccce34fd..42f50c0921 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -473,7 +473,7 @@ def test_no_auth(self): """ channel = self.make_request("GET", self.url, b"{}") - self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(401, channel.code, msg=channel.json_body) self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) def test_requester_is_no_admin(self): @@ -485,7 +485,7 @@ def test_requester_is_no_admin(self): channel = self.make_request("GET", self.url, access_token=other_user_token) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) def test_all_users(self): @@ -497,11 +497,11 @@ def test_all_users(self): channel = self.make_request( "GET", self.url + "?deactivated=true", - b"{}", + {}, access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(3, len(channel.json_body["users"])) self.assertEqual(3, channel.json_body["total"]) @@ -532,7 +532,7 @@ def _search_test( ) channel = self.make_request( "GET", - url.encode("ascii"), + url, access_token=self.admin_user_tok, ) self.assertEqual(expected_http_code, channel.code, msg=channel.json_body) @@ -598,7 +598,7 @@ def test_invalid_parameter(self): access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) # negative from @@ -608,7 +608,7 @@ def test_invalid_parameter(self): access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) # invalid guests @@ -618,7 +618,7 @@ def test_invalid_parameter(self): access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) # invalid deactivated @@ -628,7 +628,7 @@ def test_invalid_parameter(self): access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) # unkown order_by @@ -648,7 +648,7 @@ def test_invalid_parameter(self): access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) def test_limit(self): @@ -666,7 +666,7 @@ def test_limit(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_users) self.assertEqual(len(channel.json_body["users"]), 5) self.assertEqual(channel.json_body["next_token"], "5") @@ -687,7 +687,7 @@ def test_from(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_users) self.assertEqual(len(channel.json_body["users"]), 15) self.assertNotIn("next_token", channel.json_body) @@ -708,7 +708,7 @@ def test_limit_and_from(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_users) self.assertEqual(channel.json_body["next_token"], "15") self.assertEqual(len(channel.json_body["users"]), 10) @@ -731,7 +731,7 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_users) self.assertEqual(len(channel.json_body["users"]), number_users) self.assertNotIn("next_token", channel.json_body) @@ -744,7 +744,7 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_users) self.assertEqual(len(channel.json_body["users"]), number_users) self.assertNotIn("next_token", channel.json_body) @@ -757,7 +757,7 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_users) self.assertEqual(len(channel.json_body["users"]), 19) self.assertEqual(channel.json_body["next_token"], "19") @@ -771,7 +771,7 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_users) self.assertEqual(len(channel.json_body["users"]), 1) self.assertNotIn("next_token", channel.json_body) @@ -781,7 +781,10 @@ def test_order_by(self): Testing order list with parameter `order_by` """ + # make sure that the users do not have the same timestamps + self.reactor.advance(10) user1 = self.register_user("user1", "pass1", admin=False, displayname="Name Z") + self.reactor.advance(10) user2 = self.register_user("user2", "pass2", admin=False, displayname="Name Y") # Modify user @@ -841,6 +844,11 @@ def test_order_by(self): self._order_test([self.admin_user, user2, user1], "avatar_url", "f") self._order_test([user1, user2, self.admin_user], "avatar_url", "b") + # order by creation_ts + self._order_test([self.admin_user, user1, user2], "creation_ts") + self._order_test([self.admin_user, user1, user2], "creation_ts", "f") + self._order_test([user2, user1, self.admin_user], "creation_ts", "b") + def _order_test( self, expected_user_list: List[str], @@ -863,7 +871,7 @@ def _order_test( url += "dir=%s" % (dir,) channel = self.make_request( "GET", - url.encode("ascii"), + url, access_token=self.admin_user_tok, ) self.assertEqual(200, channel.code, msg=channel.json_body) @@ -887,6 +895,7 @@ def _check_fields(self, content: JsonDict): self.assertIn("shadow_banned", u) self.assertIn("displayname", u) self.assertIn("avatar_url", u) + self.assertIn("creation_ts", u) def _create_users(self, number_users: int): """ From 7da24b975dfb10c277cf963dfddb88f55b1ca598 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 22 Jul 2021 15:29:27 +0100 Subject: [PATCH 455/619] Always send device_one_time_keys_count (#10457) As per comment Fixes https://github.com/matrix-org/synapse/issues/10456 See also https://github.com/vector-im/element-android/issues/3725 --- changelog.d/10457.bugfix | 1 + synapse/rest/client/v2_alpha/sync.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 changelog.d/10457.bugfix diff --git a/changelog.d/10457.bugfix b/changelog.d/10457.bugfix new file mode 100644 index 0000000000..ec950b5846 --- /dev/null +++ b/changelog.d/10457.bugfix @@ -0,0 +1 @@ +Always include `device_one_time_keys_count` key in `/sync` response to work around a bug in Element Android that broke encryption for new devices. diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index ecbbcf3851..32e8500795 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -252,10 +252,13 @@ async def encode_response(self, time_now, sync_result, access_token_id, filter): if sync_result.device_lists.left: response["device_lists"]["left"] = list(sync_result.device_lists.left) - if sync_result.device_one_time_keys_count: - response[ - "device_one_time_keys_count" - ] = sync_result.device_one_time_keys_count + # We always include this because https://github.com/vector-im/element-android/issues/3725 + # The spec isn't terribly clear on when this can be omitted and how a client would tell + # the difference between "no keys present" and "nothing changed" in terms of whole field + # absent / individual key type entry absent + # Corresponding synapse issue: https://github.com/matrix-org/synapse/issues/10456 + response["device_one_time_keys_count"] = sync_result.device_one_time_keys_count + if sync_result.device_unused_fallback_key_types: response[ "org.matrix.msc2732.device_unused_fallback_key_types" From 283bb5c94eafbec3464de81340f0dc53bb88f629 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 22 Jul 2021 15:37:10 +0100 Subject: [PATCH 456/619] 1.38.1 --- CHANGES.md | 9 +++++++++ changelog.d/10457.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/10457.bugfix diff --git a/CHANGES.md b/CHANGES.md index 82baaa2d1f..6e5a720411 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.38.1 (2021-07-22) +=========================== + +Bugfixes +-------- + +- Always include `device_one_time_keys_count` key in `/sync` response to work around a bug in Element Android that broke encryption for new devices. ([\#10457](https://github.com/matrix-org/synapse/issues/10457)) + + Synapse 1.38.0 (2021-07-13) =========================== diff --git a/changelog.d/10457.bugfix b/changelog.d/10457.bugfix deleted file mode 100644 index ec950b5846..0000000000 --- a/changelog.d/10457.bugfix +++ /dev/null @@ -1 +0,0 @@ -Always include `device_one_time_keys_count` key in `/sync` response to work around a bug in Element Android that broke encryption for new devices. diff --git a/debian/changelog b/debian/changelog index 43d26fc133..5598fdea31 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.38.1) stable; urgency=medium + + * New synapse release 1.38.1. + + -- Synapse Packaging team Thu, 22 Jul 2021 15:37:06 +0100 + matrix-synapse-py3 (1.38.0) stable; urgency=medium * New synapse release 1.38.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 5ecce24eee..7ea5a790db 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.38.0" +__version__ = "1.38.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From f76f8c15679dbd70aeffa13bb4f2da7db2e59a6c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 22 Jul 2021 15:43:26 +0100 Subject: [PATCH 457/619] 1.39.0rc2 --- CHANGES.md | 15 +++++++++++++++ changelog.d/10416.misc | 1 - changelog.d/10457.bugfix | 1 - synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/10416.misc delete mode 100644 changelog.d/10457.bugfix diff --git a/CHANGES.md b/CHANGES.md index 066f798a95..a1dcbf6f58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,18 @@ +Synapse 1.39.0rc2 (2021-07-22) +============================== + +Bugfixes +-------- + +- Always include `device_one_time_keys_count` key in `/sync` response to work around a bug in Element Android that broke encryption for new devices. ([\#10457](https://github.com/matrix-org/synapse/issues/10457)) + + +Internal Changes +---------------- + +- Move docker image build to Github Actions. ([\#10416](https://github.com/matrix-org/synapse/issues/10416)) + + Synapse 1.39.0rc1 (2021-07-20) ============================== diff --git a/changelog.d/10416.misc b/changelog.d/10416.misc deleted file mode 100644 index fa648372f5..0000000000 --- a/changelog.d/10416.misc +++ /dev/null @@ -1 +0,0 @@ -Move docker image build to Github Actions. diff --git a/changelog.d/10457.bugfix b/changelog.d/10457.bugfix deleted file mode 100644 index ec950b5846..0000000000 --- a/changelog.d/10457.bugfix +++ /dev/null @@ -1 +0,0 @@ -Always include `device_one_time_keys_count` key in `/sync` response to work around a bug in Element Android that broke encryption for new devices. diff --git a/synapse/__init__.py b/synapse/__init__.py index 46902adab5..01d6bf17f0 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.39.0rc1" +__version__ = "1.39.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From cd5fcd2731182aa48aa7c0a44c7e547681021296 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 22 Jul 2021 15:19:30 -0500 Subject: [PATCH 458/619] Disable msc2716 until Complement update is merged (#10463) --- changelog.d/10463.misc | 1 + scripts-dev/complement.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10463.misc diff --git a/changelog.d/10463.misc b/changelog.d/10463.misc new file mode 100644 index 0000000000..d7b4d2222e --- /dev/null +++ b/changelog.d/10463.misc @@ -0,0 +1 @@ +Disable `msc2716` Complement tests until Complement updates are merged. diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index aca32edc17..4df224be67 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -65,4 +65,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -go test -v -tags synapse_blacklist,msc2946,msc3083,msc2716,msc2403 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests +go test -v -tags synapse_blacklist,msc2946,msc3083,msc2403 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests From 4c3fdfc808a90b4ba049695e97cbf3e6cc21873e Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Thu, 22 Jul 2021 21:50:30 +0100 Subject: [PATCH 459/619] Fix an error in the docker workflow (#10461) --- .github/workflows/docker.yml | 2 +- changelog.d/10461.misc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10461.misc diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8bdefb3905..af7ed21fce 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -54,7 +54,7 @@ jobs: # we do an amd64-only build, before following up with a multiarch build. - name: Build and push amd64 uses: docker/build-push-action@v2 - if: "${{ startsWith(github.ref, 'refs/tags/v' }}" + if: "${{ startsWith(github.ref, 'refs/tags/v') }}" with: push: true labels: "gitsha1=${{ github.sha }}" diff --git a/changelog.d/10461.misc b/changelog.d/10461.misc new file mode 100644 index 0000000000..5035e26825 --- /dev/null +++ b/changelog.d/10461.misc @@ -0,0 +1 @@ +Fix an error which prevented the Github Actions workflow to build the docker images from running. From f22252d4f9383ebb9134d6592d74da83d537f79a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 26 Jul 2021 11:36:01 +0100 Subject: [PATCH 460/619] Enable docker image caching for the deb build (#10431) --- .github/workflows/release-artifacts.yml | 39 ++++++++++++++++++++++--- changelog.d/10431.misc | 1 + scripts-dev/build_debian_packages | 38 ++++++++++++++++++------ 3 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 changelog.d/10431.misc diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml index 0beb418a07..eb294f1619 100644 --- a/.github/workflows/release-artifacts.yml +++ b/.github/workflows/release-artifacts.yml @@ -48,12 +48,43 @@ jobs: distro: ${{ fromJson(needs.get-distros.outputs.distros) }} steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 with: path: src - - uses: actions/setup-python@v2 - - run: ./src/scripts-dev/build_debian_packages "${{ matrix.distro }}" - - uses: actions/upload-artifact@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + + - name: Set up docker layer caching + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Set up python + uses: actions/setup-python@v2 + + - name: Build the packages + # see https://github.com/docker/build-push-action/issues/252 + # for the cache magic here + run: | + ./src/scripts-dev/build_debian_packages \ + --docker-build-arg=--cache-from=type=local,src=/tmp/.buildx-cache \ + --docker-build-arg=--cache-to=type=local,mode=max,dest=/tmp/.buildx-cache-new \ + --docker-build-arg=--progress=plain \ + --docker-build-arg=--load \ + "${{ matrix.distro }}" + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + - name: Upload debs as artifacts + uses: actions/upload-artifact@v2 with: name: debs path: debs/* diff --git a/changelog.d/10431.misc b/changelog.d/10431.misc new file mode 100644 index 0000000000..34b9b49da6 --- /dev/null +++ b/changelog.d/10431.misc @@ -0,0 +1 @@ +Use a docker image cache for the prerequisites for the debian package build. diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index e25c5bb265..0ed1c679fd 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -17,6 +17,7 @@ import subprocess import sys import threading from concurrent.futures import ThreadPoolExecutor +from typing import Optional, Sequence DISTS = ( "debian:buster", @@ -39,8 +40,11 @@ projdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) class Builder(object): - def __init__(self, redirect_stdout=False): + def __init__( + self, redirect_stdout=False, docker_build_args: Optional[Sequence[str]] = None + ): self.redirect_stdout = redirect_stdout + self._docker_build_args = tuple(docker_build_args or ()) self.active_containers = set() self._lock = threading.Lock() self._failed = False @@ -79,8 +83,8 @@ class Builder(object): stdout = None # first build a docker image for the build environment - subprocess.check_call( - [ + build_args = ( + ( "docker", "build", "--tag", @@ -89,8 +93,13 @@ class Builder(object): "distro=" + dist, "-f", "docker/Dockerfile-dhvirtualenv", - "docker", - ], + ) + + self._docker_build_args + + ("docker",) + ) + + subprocess.check_call( + build_args, stdout=stdout, stderr=subprocess.STDOUT, cwd=projdir, @@ -147,9 +156,7 @@ class Builder(object): self.active_containers.remove(c) -def run_builds(dists, jobs=1, skip_tests=False): - builder = Builder(redirect_stdout=(jobs > 1)) - +def run_builds(builder, dists, jobs=1, skip_tests=False): def sig(signum, _frame): print("Caught SIGINT") builder.kill_containers() @@ -180,6 +187,11 @@ if __name__ == "__main__": action="store_true", help="skip running tests after building", ) + parser.add_argument( + "--docker-build-arg", + action="append", + help="specify an argument to pass to docker build", + ) parser.add_argument( "--show-dists-json", action="store_true", @@ -195,4 +207,12 @@ if __name__ == "__main__": if args.show_dists_json: print(json.dumps(DISTS)) else: - run_builds(dists=args.dist, jobs=args.jobs, skip_tests=args.no_check) + builder = Builder( + redirect_stdout=(args.jobs > 1), docker_build_args=args.docker_build_arg + ) + run_builds( + builder, + dists=args.dist, + jobs=args.jobs, + skip_tests=args.no_check, + ) From 4fb92d93eae3c3519a9a47ea7fdef2d492014f2f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 26 Jul 2021 11:53:09 -0400 Subject: [PATCH 461/619] Add type hints to synapse.federation.transport.client. (#10408) --- changelog.d/10408.misc | 1 + synapse/federation/transport/client.py | 499 +++++++++++++++---------- 2 files changed, 299 insertions(+), 201 deletions(-) create mode 100644 changelog.d/10408.misc diff --git a/changelog.d/10408.misc b/changelog.d/10408.misc new file mode 100644 index 0000000000..abccd210a9 --- /dev/null +++ b/changelog.d/10408.misc @@ -0,0 +1 @@ +Add type hints to `synapse.federation.transport.client` module. diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 98b1bf77fd..e73bdb52b3 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -15,7 +15,7 @@ import logging import urllib -from typing import Any, Dict, List, Optional +from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, Union import attr import ijson @@ -29,6 +29,7 @@ FEDERATION_V2_PREFIX, ) from synapse.events import EventBase, make_event_from_dict +from synapse.federation.units import Transaction from synapse.http.matrixfederationclient import ByteParser from synapse.logging.utils import log_function from synapse.types import JsonDict @@ -49,23 +50,25 @@ def __init__(self, hs): self.client = hs.get_federation_http_client() @log_function - def get_room_state_ids(self, destination, room_id, event_id): + async def get_room_state_ids( + self, destination: str, room_id: str, event_id: str + ) -> JsonDict: """Requests all state for a given room from the given server at the given event. Returns the state's event_id's Args: - destination (str): The host name of the remote homeserver we want + destination: The host name of the remote homeserver we want to get the state from. - context (str): The name of the context we want the state of - event_id (str): The event we want the context at. + context: The name of the context we want the state of + event_id: The event we want the context at. Returns: - Awaitable: Results in a dict received from the remote homeserver. + Results in a dict received from the remote homeserver. """ logger.debug("get_room_state_ids dest=%s, room=%s", destination, room_id) path = _create_v1_path("/state_ids/%s", room_id) - return self.client.get_json( + return await self.client.get_json( destination, path=path, args={"event_id": event_id}, @@ -73,39 +76,43 @@ def get_room_state_ids(self, destination, room_id, event_id): ) @log_function - def get_event(self, destination, event_id, timeout=None): + async def get_event( + self, destination: str, event_id: str, timeout: Optional[int] = None + ) -> JsonDict: """Requests the pdu with give id and origin from the given server. Args: - destination (str): The host name of the remote homeserver we want + destination: The host name of the remote homeserver we want to get the state from. - event_id (str): The id of the event being requested. - timeout (int): How long to try (in ms) the destination for before + event_id: The id of the event being requested. + timeout: How long to try (in ms) the destination for before giving up. None indicates no timeout. Returns: - Awaitable: Results in a dict received from the remote homeserver. + Results in a dict received from the remote homeserver. """ logger.debug("get_pdu dest=%s, event_id=%s", destination, event_id) path = _create_v1_path("/event/%s", event_id) - return self.client.get_json( + return await self.client.get_json( destination, path=path, timeout=timeout, try_trailing_slash_on_400=True ) @log_function - def backfill(self, destination, room_id, event_tuples, limit): + async def backfill( + self, destination: str, room_id: str, event_tuples: Iterable[str], limit: int + ) -> Optional[JsonDict]: """Requests `limit` previous PDUs in a given context before list of PDUs. Args: - dest (str) - room_id (str) - event_tuples (list) - limit (int) + destination + room_id + event_tuples + limit Returns: - Awaitable: Results in a dict received from the remote homeserver. + Results in a dict received from the remote homeserver. """ logger.debug( "backfill dest=%s, room_id=%s, event_tuples=%r, limit=%s", @@ -117,18 +124,22 @@ def backfill(self, destination, room_id, event_tuples, limit): if not event_tuples: # TODO: raise? - return + return None path = _create_v1_path("/backfill/%s", room_id) args = {"v": event_tuples, "limit": [str(limit)]} - return self.client.get_json( + return await self.client.get_json( destination, path=path, args=args, try_trailing_slash_on_400=True ) @log_function - async def send_transaction(self, transaction, json_data_callback=None): + async def send_transaction( + self, + transaction: Transaction, + json_data_callback: Optional[Callable[[], JsonDict]] = None, + ) -> JsonDict: """Sends the given Transaction to its destination Args: @@ -149,21 +160,21 @@ async def send_transaction(self, transaction, json_data_callback=None): """ logger.debug( "send_data dest=%s, txid=%s", - transaction.destination, - transaction.transaction_id, + transaction.destination, # type: ignore + transaction.transaction_id, # type: ignore ) - if transaction.destination == self.server_name: + if transaction.destination == self.server_name: # type: ignore raise RuntimeError("Transport layer cannot send to itself!") # FIXME: This is only used by the tests. The actual json sent is # generated by the json_data_callback. json_data = transaction.get_dict() - path = _create_v1_path("/send/%s", transaction.transaction_id) + path = _create_v1_path("/send/%s", transaction.transaction_id) # type: ignore - response = await self.client.put_json( - transaction.destination, + return await self.client.put_json( + transaction.destination, # type: ignore path=path, data=json_data, json_data_callback=json_data_callback, @@ -172,8 +183,6 @@ async def send_transaction(self, transaction, json_data_callback=None): try_trailing_slash_on_400=True, ) - return response - @log_function async def make_query( self, destination, query_type, args, retry_on_dns_fail, ignore_backoff=False @@ -193,8 +202,13 @@ async def make_query( @log_function async def make_membership_event( - self, destination, room_id, user_id, membership, params - ): + self, + destination: str, + room_id: str, + user_id: str, + membership: str, + params: Optional[Mapping[str, Union[str, Iterable[str]]]], + ) -> JsonDict: """Asks a remote server to build and sign us a membership event Note that this does not append any events to any graphs. @@ -240,7 +254,7 @@ async def make_membership_event( ignore_backoff = True retry_on_dns_fail = True - content = await self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args=params, @@ -249,20 +263,18 @@ async def make_membership_event( ignore_backoff=ignore_backoff, ) - return content - @log_function async def send_join_v1( self, - room_version, - destination, - room_id, - event_id, - content, + room_version: RoomVersion, + destination: str, + room_id: str, + event_id: str, + content: JsonDict, ) -> "SendJoinResponse": path = _create_v1_path("/send_join/%s/%s", room_id, event_id) - response = await self.client.put_json( + return await self.client.put_json( destination=destination, path=path, data=content, @@ -270,15 +282,18 @@ async def send_join_v1( max_response_size=MAX_RESPONSE_SIZE_SEND_JOIN, ) - return response - @log_function async def send_join_v2( - self, room_version, destination, room_id, event_id, content + self, + room_version: RoomVersion, + destination: str, + room_id: str, + event_id: str, + content: JsonDict, ) -> "SendJoinResponse": path = _create_v2_path("/send_join/%s/%s", room_id, event_id) - response = await self.client.put_json( + return await self.client.put_json( destination=destination, path=path, data=content, @@ -286,13 +301,13 @@ async def send_join_v2( max_response_size=MAX_RESPONSE_SIZE_SEND_JOIN, ) - return response - @log_function - async def send_leave_v1(self, destination, room_id, event_id, content): + async def send_leave_v1( + self, destination: str, room_id: str, event_id: str, content: JsonDict + ) -> Tuple[int, JsonDict]: path = _create_v1_path("/send_leave/%s/%s", room_id, event_id) - response = await self.client.put_json( + return await self.client.put_json( destination=destination, path=path, data=content, @@ -303,13 +318,13 @@ async def send_leave_v1(self, destination, room_id, event_id, content): ignore_backoff=True, ) - return response - @log_function - async def send_leave_v2(self, destination, room_id, event_id, content): + async def send_leave_v2( + self, destination: str, room_id: str, event_id: str, content: JsonDict + ) -> JsonDict: path = _create_v2_path("/send_leave/%s/%s", room_id, event_id) - response = await self.client.put_json( + return await self.client.put_json( destination=destination, path=path, data=content, @@ -320,8 +335,6 @@ async def send_leave_v2(self, destination, room_id, event_id, content): ignore_backoff=True, ) - return response - @log_function async def send_knock_v1( self, @@ -357,25 +370,25 @@ async def send_knock_v1( ) @log_function - async def send_invite_v1(self, destination, room_id, event_id, content): + async def send_invite_v1( + self, destination: str, room_id: str, event_id: str, content: JsonDict + ) -> Tuple[int, JsonDict]: path = _create_v1_path("/invite/%s/%s", room_id, event_id) - response = await self.client.put_json( + return await self.client.put_json( destination=destination, path=path, data=content, ignore_backoff=True ) - return response - @log_function - async def send_invite_v2(self, destination, room_id, event_id, content): + async def send_invite_v2( + self, destination: str, room_id: str, event_id: str, content: JsonDict + ) -> JsonDict: path = _create_v2_path("/invite/%s/%s", room_id, event_id) - response = await self.client.put_json( + return await self.client.put_json( destination=destination, path=path, data=content, ignore_backoff=True ) - return response - @log_function async def get_public_rooms( self, @@ -385,7 +398,7 @@ async def get_public_rooms( search_filter: Optional[Dict] = None, include_all_networks: bool = False, third_party_instance_id: Optional[str] = None, - ): + ) -> JsonDict: """Get the list of public rooms from a remote homeserver See synapse.federation.federation_client.FederationClient.get_public_rooms for @@ -450,25 +463,27 @@ async def get_public_rooms( return response @log_function - async def exchange_third_party_invite(self, destination, room_id, event_dict): + async def exchange_third_party_invite( + self, destination: str, room_id: str, event_dict: JsonDict + ) -> JsonDict: path = _create_v1_path("/exchange_third_party_invite/%s", room_id) - response = await self.client.put_json( + return await self.client.put_json( destination=destination, path=path, data=event_dict ) - return response - @log_function - async def get_event_auth(self, destination, room_id, event_id): + async def get_event_auth( + self, destination: str, room_id: str, event_id: str + ) -> JsonDict: path = _create_v1_path("/event_auth/%s/%s", room_id, event_id) - content = await self.client.get_json(destination=destination, path=path) - - return content + return await self.client.get_json(destination=destination, path=path) @log_function - async def query_client_keys(self, destination, query_content, timeout): + async def query_client_keys( + self, destination: str, query_content: JsonDict, timeout: int + ) -> JsonDict: """Query the device keys for a list of user ids hosted on a remote server. @@ -496,20 +511,21 @@ async def query_client_keys(self, destination, query_content, timeout): } Args: - destination(str): The server to query. - query_content(dict): The user ids to query. + destination: The server to query. + query_content: The user ids to query. Returns: A dict containing device and cross-signing keys. """ path = _create_v1_path("/user/keys/query") - content = await self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data=query_content, timeout=timeout ) - return content @log_function - async def query_user_devices(self, destination, user_id, timeout): + async def query_user_devices( + self, destination: str, user_id: str, timeout: int + ) -> JsonDict: """Query the devices for a user id hosted on a remote server. Response: @@ -535,20 +551,21 @@ async def query_user_devices(self, destination, user_id, timeout): } Args: - destination(str): The server to query. - query_content(dict): The user ids to query. + destination: The server to query. + query_content: The user ids to query. Returns: A dict containing device and cross-signing keys. """ path = _create_v1_path("/user/devices/%s", user_id) - content = await self.client.get_json( + return await self.client.get_json( destination=destination, path=path, timeout=timeout ) - return content @log_function - async def claim_client_keys(self, destination, query_content, timeout): + async def claim_client_keys( + self, destination: str, query_content: JsonDict, timeout: int + ) -> JsonDict: """Claim one-time keys for a list of devices hosted on a remote server. Request: @@ -572,33 +589,32 @@ async def claim_client_keys(self, destination, query_content, timeout): } Args: - destination(str): The server to query. - query_content(dict): The user ids to query. + destination: The server to query. + query_content: The user ids to query. Returns: A dict containing the one-time keys. """ path = _create_v1_path("/user/keys/claim") - content = await self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data=query_content, timeout=timeout ) - return content @log_function async def get_missing_events( self, - destination, - room_id, - earliest_events, - latest_events, - limit, - min_depth, - timeout, - ): + destination: str, + room_id: str, + earliest_events: Iterable[str], + latest_events: Iterable[str], + limit: int, + min_depth: int, + timeout: int, + ) -> JsonDict: path = _create_v1_path("/get_missing_events/%s", room_id) - content = await self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data={ @@ -610,14 +626,14 @@ async def get_missing_events( timeout=timeout, ) - return content - @log_function - def get_group_profile(self, destination, group_id, requester_user_id): + async def get_group_profile( + self, destination: str, group_id: str, requester_user_id: str + ) -> JsonDict: """Get a group profile""" path = _create_v1_path("/groups/%s/profile", group_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -625,14 +641,16 @@ def get_group_profile(self, destination, group_id, requester_user_id): ) @log_function - def update_group_profile(self, destination, group_id, requester_user_id, content): + async def update_group_profile( + self, destination: str, group_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: """Update a remote group profile Args: - destination (str) - group_id (str) - requester_user_id (str) - content (dict): The new profile of the group + destination + group_id + requester_user_id + content: The new profile of the group """ path = _create_v1_path("/groups/%s/profile", group_id) @@ -645,11 +663,13 @@ def update_group_profile(self, destination, group_id, requester_user_id, content ) @log_function - def get_group_summary(self, destination, group_id, requester_user_id): + async def get_group_summary( + self, destination: str, group_id: str, requester_user_id: str + ) -> JsonDict: """Get a group summary""" path = _create_v1_path("/groups/%s/summary", group_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -657,24 +677,31 @@ def get_group_summary(self, destination, group_id, requester_user_id): ) @log_function - def get_rooms_in_group(self, destination, group_id, requester_user_id): + async def get_rooms_in_group( + self, destination: str, group_id: str, requester_user_id: str + ) -> JsonDict: """Get all rooms in a group""" path = _create_v1_path("/groups/%s/rooms", group_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, ignore_backoff=True, ) - def add_room_to_group( - self, destination, group_id, requester_user_id, room_id, content - ): + async def add_room_to_group( + self, + destination: str, + group_id: str, + requester_user_id: str, + room_id: str, + content: JsonDict, + ) -> JsonDict: """Add a room to a group""" path = _create_v1_path("/groups/%s/room/%s", group_id, room_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -682,15 +709,21 @@ def add_room_to_group( ignore_backoff=True, ) - def update_room_in_group( - self, destination, group_id, requester_user_id, room_id, config_key, content - ): + async def update_room_in_group( + self, + destination: str, + group_id: str, + requester_user_id: str, + room_id: str, + config_key: str, + content: JsonDict, + ) -> JsonDict: """Update room in group""" path = _create_v1_path( "/groups/%s/room/%s/config/%s", group_id, room_id, config_key ) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -698,11 +731,13 @@ def update_room_in_group( ignore_backoff=True, ) - def remove_room_from_group(self, destination, group_id, requester_user_id, room_id): + async def remove_room_from_group( + self, destination: str, group_id: str, requester_user_id: str, room_id: str + ) -> JsonDict: """Remove a room from a group""" path = _create_v1_path("/groups/%s/room/%s", group_id, room_id) - return self.client.delete_json( + return await self.client.delete_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -710,11 +745,13 @@ def remove_room_from_group(self, destination, group_id, requester_user_id, room_ ) @log_function - def get_users_in_group(self, destination, group_id, requester_user_id): + async def get_users_in_group( + self, destination: str, group_id: str, requester_user_id: str + ) -> JsonDict: """Get users in a group""" path = _create_v1_path("/groups/%s/users", group_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -722,11 +759,13 @@ def get_users_in_group(self, destination, group_id, requester_user_id): ) @log_function - def get_invited_users_in_group(self, destination, group_id, requester_user_id): + async def get_invited_users_in_group( + self, destination: str, group_id: str, requester_user_id: str + ) -> JsonDict: """Get users that have been invited to a group""" path = _create_v1_path("/groups/%s/invited_users", group_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -734,16 +773,20 @@ def get_invited_users_in_group(self, destination, group_id, requester_user_id): ) @log_function - def accept_group_invite(self, destination, group_id, user_id, content): + async def accept_group_invite( + self, destination: str, group_id: str, user_id: str, content: JsonDict + ) -> JsonDict: """Accept a group invite""" path = _create_v1_path("/groups/%s/users/%s/accept_invite", group_id, user_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data=content, ignore_backoff=True ) @log_function - def join_group(self, destination, group_id, user_id, content): + def join_group( + self, destination: str, group_id: str, user_id: str, content: JsonDict + ) -> JsonDict: """Attempts to join a group""" path = _create_v1_path("/groups/%s/users/%s/join", group_id, user_id) @@ -752,13 +795,18 @@ def join_group(self, destination, group_id, user_id, content): ) @log_function - def invite_to_group( - self, destination, group_id, user_id, requester_user_id, content - ): + async def invite_to_group( + self, + destination: str, + group_id: str, + user_id: str, + requester_user_id: str, + content: JsonDict, + ) -> JsonDict: """Invite a user to a group""" path = _create_v1_path("/groups/%s/users/%s/invite", group_id, user_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -767,25 +815,32 @@ def invite_to_group( ) @log_function - def invite_to_group_notification(self, destination, group_id, user_id, content): + async def invite_to_group_notification( + self, destination: str, group_id: str, user_id: str, content: JsonDict + ) -> JsonDict: """Sent by group server to inform a user's server that they have been invited. """ path = _create_v1_path("/groups/local/%s/users/%s/invite", group_id, user_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data=content, ignore_backoff=True ) @log_function - def remove_user_from_group( - self, destination, group_id, requester_user_id, user_id, content - ): + async def remove_user_from_group( + self, + destination: str, + group_id: str, + requester_user_id: str, + user_id: str, + content: JsonDict, + ) -> JsonDict: """Remove a user from a group""" path = _create_v1_path("/groups/%s/users/%s/remove", group_id, user_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -794,35 +849,43 @@ def remove_user_from_group( ) @log_function - def remove_user_from_group_notification( - self, destination, group_id, user_id, content - ): + async def remove_user_from_group_notification( + self, destination: str, group_id: str, user_id: str, content: JsonDict + ) -> JsonDict: """Sent by group server to inform a user's server that they have been kicked from the group. """ path = _create_v1_path("/groups/local/%s/users/%s/remove", group_id, user_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data=content, ignore_backoff=True ) @log_function - def renew_group_attestation(self, destination, group_id, user_id, content): + async def renew_group_attestation( + self, destination: str, group_id: str, user_id: str, content: JsonDict + ) -> JsonDict: """Sent by either a group server or a user's server to periodically update the attestations """ path = _create_v1_path("/groups/%s/renew_attestation/%s", group_id, user_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data=content, ignore_backoff=True ) @log_function - def update_group_summary_room( - self, destination, group_id, user_id, room_id, category_id, content - ): + async def update_group_summary_room( + self, + destination: str, + group_id: str, + user_id: str, + room_id: str, + category_id: str, + content: JsonDict, + ) -> JsonDict: """Update a room entry in a group summary""" if category_id: path = _create_v1_path( @@ -834,7 +897,7 @@ def update_group_summary_room( else: path = _create_v1_path("/groups/%s/summary/rooms/%s", group_id, room_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": user_id}, @@ -843,9 +906,14 @@ def update_group_summary_room( ) @log_function - def delete_group_summary_room( - self, destination, group_id, user_id, room_id, category_id - ): + async def delete_group_summary_room( + self, + destination: str, + group_id: str, + user_id: str, + room_id: str, + category_id: str, + ) -> JsonDict: """Delete a room entry in a group summary""" if category_id: path = _create_v1_path( @@ -857,7 +925,7 @@ def delete_group_summary_room( else: path = _create_v1_path("/groups/%s/summary/rooms/%s", group_id, room_id) - return self.client.delete_json( + return await self.client.delete_json( destination=destination, path=path, args={"requester_user_id": user_id}, @@ -865,11 +933,13 @@ def delete_group_summary_room( ) @log_function - def get_group_categories(self, destination, group_id, requester_user_id): + async def get_group_categories( + self, destination: str, group_id: str, requester_user_id: str + ) -> JsonDict: """Get all categories in a group""" path = _create_v1_path("/groups/%s/categories", group_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -877,11 +947,13 @@ def get_group_categories(self, destination, group_id, requester_user_id): ) @log_function - def get_group_category(self, destination, group_id, requester_user_id, category_id): + async def get_group_category( + self, destination: str, group_id: str, requester_user_id: str, category_id: str + ) -> JsonDict: """Get category info in a group""" path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -889,13 +961,18 @@ def get_group_category(self, destination, group_id, requester_user_id, category_ ) @log_function - def update_group_category( - self, destination, group_id, requester_user_id, category_id, content - ): + async def update_group_category( + self, + destination: str, + group_id: str, + requester_user_id: str, + category_id: str, + content: JsonDict, + ) -> JsonDict: """Update a category in a group""" path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -904,13 +981,13 @@ def update_group_category( ) @log_function - def delete_group_category( - self, destination, group_id, requester_user_id, category_id - ): + async def delete_group_category( + self, destination: str, group_id: str, requester_user_id: str, category_id: str + ) -> JsonDict: """Delete a category in a group""" path = _create_v1_path("/groups/%s/categories/%s", group_id, category_id) - return self.client.delete_json( + return await self.client.delete_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -918,11 +995,13 @@ def delete_group_category( ) @log_function - def get_group_roles(self, destination, group_id, requester_user_id): + async def get_group_roles( + self, destination: str, group_id: str, requester_user_id: str + ) -> JsonDict: """Get all roles in a group""" path = _create_v1_path("/groups/%s/roles", group_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -930,11 +1009,13 @@ def get_group_roles(self, destination, group_id, requester_user_id): ) @log_function - def get_group_role(self, destination, group_id, requester_user_id, role_id): + async def get_group_role( + self, destination: str, group_id: str, requester_user_id: str, role_id: str + ) -> JsonDict: """Get a roles info""" path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id) - return self.client.get_json( + return await self.client.get_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -942,13 +1023,18 @@ def get_group_role(self, destination, group_id, requester_user_id, role_id): ) @log_function - def update_group_role( - self, destination, group_id, requester_user_id, role_id, content - ): + async def update_group_role( + self, + destination: str, + group_id: str, + requester_user_id: str, + role_id: str, + content: JsonDict, + ) -> JsonDict: """Update a role in a group""" path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -957,11 +1043,13 @@ def update_group_role( ) @log_function - def delete_group_role(self, destination, group_id, requester_user_id, role_id): + async def delete_group_role( + self, destination: str, group_id: str, requester_user_id: str, role_id: str + ) -> JsonDict: """Delete a role in a group""" path = _create_v1_path("/groups/%s/roles/%s", group_id, role_id) - return self.client.delete_json( + return await self.client.delete_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -969,9 +1057,15 @@ def delete_group_role(self, destination, group_id, requester_user_id, role_id): ) @log_function - def update_group_summary_user( - self, destination, group_id, requester_user_id, user_id, role_id, content - ): + async def update_group_summary_user( + self, + destination: str, + group_id: str, + requester_user_id: str, + user_id: str, + role_id: str, + content: JsonDict, + ) -> JsonDict: """Update a users entry in a group""" if role_id: path = _create_v1_path( @@ -980,7 +1074,7 @@ def update_group_summary_user( else: path = _create_v1_path("/groups/%s/summary/users/%s", group_id, user_id) - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -989,11 +1083,13 @@ def update_group_summary_user( ) @log_function - def set_group_join_policy(self, destination, group_id, requester_user_id, content): + async def set_group_join_policy( + self, destination: str, group_id: str, requester_user_id: str, content: JsonDict + ) -> JsonDict: """Sets the join policy for a group""" path = _create_v1_path("/groups/%s/settings/m.join_policy", group_id) - return self.client.put_json( + return await self.client.put_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, @@ -1002,9 +1098,14 @@ def set_group_join_policy(self, destination, group_id, requester_user_id, conten ) @log_function - def delete_group_summary_user( - self, destination, group_id, requester_user_id, user_id, role_id - ): + async def delete_group_summary_user( + self, + destination: str, + group_id: str, + requester_user_id: str, + user_id: str, + role_id: str, + ) -> JsonDict: """Delete a users entry in a group""" if role_id: path = _create_v1_path( @@ -1013,33 +1114,35 @@ def delete_group_summary_user( else: path = _create_v1_path("/groups/%s/summary/users/%s", group_id, user_id) - return self.client.delete_json( + return await self.client.delete_json( destination=destination, path=path, args={"requester_user_id": requester_user_id}, ignore_backoff=True, ) - def bulk_get_publicised_groups(self, destination, user_ids): + async def bulk_get_publicised_groups( + self, destination: str, user_ids: Iterable[str] + ) -> JsonDict: """Get the groups a list of users are publicising""" path = _create_v1_path("/get_groups_publicised") content = {"user_ids": user_ids} - return self.client.post_json( + return await self.client.post_json( destination=destination, path=path, data=content, ignore_backoff=True ) - def get_room_complexity(self, destination, room_id): + async def get_room_complexity(self, destination: str, room_id: str) -> JsonDict: """ Args: - destination (str): The remote server - room_id (str): The room ID to ask about. + destination: The remote server + room_id: The room ID to ask about. """ path = _create_path(FEDERATION_UNSTABLE_PREFIX, "/rooms/%s/complexity", room_id) - return self.client.get_json(destination=destination, path=path) + return await self.client.get_json(destination=destination, path=path) async def get_space_summary( self, @@ -1075,14 +1178,14 @@ async def get_space_summary( ) -def _create_path(federation_prefix, path, *args): +def _create_path(federation_prefix: str, path: str, *args: str) -> str: """ Ensures that all args are url encoded. """ return federation_prefix + path % tuple(urllib.parse.quote(arg, "") for arg in args) -def _create_v1_path(path, *args): +def _create_v1_path(path: str, *args: str) -> str: """Creates a path against V1 federation API from the path template and args. Ensures that all args are url encoded. @@ -1091,16 +1194,13 @@ def _create_v1_path(path, *args): _create_v1_path("/event/%s", event_id) Args: - path (str): String template for the path - args: ([str]): Args to insert into path. Each arg will be url encoded - - Returns: - str + path: String template for the path + args: Args to insert into path. Each arg will be url encoded """ return _create_path(FEDERATION_V1_PREFIX, path, *args) -def _create_v2_path(path, *args): +def _create_v2_path(path: str, *args: str) -> str: """Creates a path against V2 federation API from the path template and args. Ensures that all args are url encoded. @@ -1109,11 +1209,8 @@ def _create_v2_path(path, *args): _create_v2_path("/event/%s", event_id) Args: - path (str): String template for the path - args: ([str]): Args to insert into path. Each arg will be url encoded - - Returns: - str + path: String template for the path + args: Args to insert into path. Each arg will be url encoded """ return _create_path(FEDERATION_V2_PREFIX, path, *args) From 228decfce1a71651d64c359d1cf28e10d0a69fc8 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 26 Jul 2021 12:17:00 -0400 Subject: [PATCH 462/619] Update the MSC3083 support to verify if joins are from an authorized server. (#10254) --- changelog.d/10254.feature | 1 + synapse/api/errors.py | 3 + synapse/api/room_versions.py | 2 +- synapse/event_auth.py | 77 ++++++++--- synapse/federation/federation_base.py | 28 ++++ synapse/federation/federation_client.py | 61 +++++++-- synapse/federation/federation_server.py | 41 +++++- synapse/federation/transport/client.py | 30 +++- synapse/handlers/event_auth.py | 85 +++++++++++- synapse/handlers/federation.py | 54 ++++++-- synapse/handlers/room_member.py | 175 ++++++++++++++++++++++-- synapse/state/__init__.py | 12 +- synapse/state/v1.py | 40 ++++-- synapse/state/v2.py | 11 +- tests/state/test_v2.py | 6 +- tests/storage/test_redaction.py | 6 +- tests/test_event_auth.py | 98 +++++++++++-- 17 files changed, 632 insertions(+), 98 deletions(-) create mode 100644 changelog.d/10254.feature diff --git a/changelog.d/10254.feature b/changelog.d/10254.feature new file mode 100644 index 0000000000..df8bb51167 --- /dev/null +++ b/changelog.d/10254.feature @@ -0,0 +1 @@ +Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 054ab14ab6..dc662bca83 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -75,6 +75,9 @@ class Codes: INVALID_SIGNATURE = "M_INVALID_SIGNATURE" USER_DEACTIVATED = "M_USER_DEACTIVATED" BAD_ALIAS = "M_BAD_ALIAS" + # For restricted join rules. + UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN" + UNABLE_TO_GRANT_JOIN = "M_UNABLE_TO_GRANT_JOIN" class CodeMessageException(RuntimeError): diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 8dd33dcb83..697319e52d 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -168,7 +168,7 @@ class RoomVersions: msc2403_knocking=False, ) MSC3083 = RoomVersion( - "org.matrix.msc3083", + "org.matrix.msc3083.v2", RoomDisposition.UNSTABLE, EventFormatVersions.V3, StateResolutionVersions.V2, diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 137dff2513..cc92d35477 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -106,6 +106,18 @@ def check( if not event.signatures.get(event_id_domain): raise AuthError(403, "Event not signed by sending server") + is_invite_via_allow_rule = ( + event.type == EventTypes.Member + and event.membership == Membership.JOIN + and "join_authorised_via_users_server" in event.content + ) + if is_invite_via_allow_rule: + authoriser_domain = get_domain_from_id( + event.content["join_authorised_via_users_server"] + ) + if not event.signatures.get(authoriser_domain): + raise AuthError(403, "Event not signed by authorising server") + # Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules # # 1. If type is m.room.create: @@ -177,7 +189,7 @@ def check( # https://github.com/vector-im/vector-web/issues/1208 hopefully if event.type == EventTypes.ThirdPartyInvite: user_level = get_user_power_level(event.user_id, auth_events) - invite_level = _get_named_level(auth_events, "invite", 0) + invite_level = get_named_level(auth_events, "invite", 0) if user_level < invite_level: raise AuthError(403, "You don't have permission to invite users") @@ -285,8 +297,8 @@ def _is_membership_change_allowed( user_level = get_user_power_level(event.user_id, auth_events) target_level = get_user_power_level(target_user_id, auth_events) - # FIXME (erikj): What should we do here as the default? - ban_level = _get_named_level(auth_events, "ban", 50) + invite_level = get_named_level(auth_events, "invite", 0) + ban_level = get_named_level(auth_events, "ban", 50) logger.debug( "_is_membership_change_allowed: %s", @@ -336,8 +348,6 @@ def _is_membership_change_allowed( elif target_in_room: # the target is already in the room. raise AuthError(403, "%s is already in the room." % target_user_id) else: - invite_level = _get_named_level(auth_events, "invite", 0) - if user_level < invite_level: raise AuthError(403, "You don't have permission to invite users") elif Membership.JOIN == membership: @@ -345,16 +355,41 @@ def _is_membership_change_allowed( # * They are not banned. # * They are accepting a previously sent invitation. # * They are already joined (it's a NOOP). - # * The room is public or restricted. + # * The room is public. + # * The room is restricted and the user meets the allows rules. if event.user_id != target_user_id: raise AuthError(403, "Cannot force another user to join.") elif target_banned: raise AuthError(403, "You are banned from this room") - elif join_rule == JoinRules.PUBLIC or ( + elif join_rule == JoinRules.PUBLIC: + pass + elif ( room_version.msc3083_join_rules and join_rule == JoinRules.MSC3083_RESTRICTED ): - pass + # This is the same as public, but the event must contain a reference + # to the server who authorised the join. If the event does not contain + # the proper content it is rejected. + # + # Note that if the caller is in the room or invited, then they do + # not need to meet the allow rules. + if not caller_in_room and not caller_invited: + authorising_user = event.content.get("join_authorised_via_users_server") + + if authorising_user is None: + raise AuthError(403, "Join event is missing authorising user.") + + # The authorising user must be in the room. + key = (EventTypes.Member, authorising_user) + member_event = auth_events.get(key) + _check_joined_room(member_event, authorising_user, event.room_id) + + authorising_user_level = get_user_power_level( + authorising_user, auth_events + ) + if authorising_user_level < invite_level: + raise AuthError(403, "Join event authorised by invalid server.") + elif join_rule == JoinRules.INVITE or ( room_version.msc2403_knocking and join_rule == JoinRules.KNOCK ): @@ -369,7 +404,7 @@ def _is_membership_change_allowed( if target_banned and user_level < ban_level: raise AuthError(403, "You cannot unban user %s." % (target_user_id,)) elif target_user_id != event.user_id: - kick_level = _get_named_level(auth_events, "kick", 50) + kick_level = get_named_level(auth_events, "kick", 50) if user_level < kick_level or user_level <= target_level: raise AuthError(403, "You cannot kick user %s." % target_user_id) @@ -445,7 +480,7 @@ def get_send_level( def _can_send_event(event: EventBase, auth_events: StateMap[EventBase]) -> bool: - power_levels_event = _get_power_level_event(auth_events) + power_levels_event = get_power_level_event(auth_events) send_level = get_send_level(event.type, event.get("state_key"), power_levels_event) user_level = get_user_power_level(event.user_id, auth_events) @@ -485,7 +520,7 @@ def check_redaction( """ user_level = get_user_power_level(event.user_id, auth_events) - redact_level = _get_named_level(auth_events, "redact", 50) + redact_level = get_named_level(auth_events, "redact", 50) if user_level >= redact_level: return False @@ -600,7 +635,7 @@ def _check_power_levels( ) -def _get_power_level_event(auth_events: StateMap[EventBase]) -> Optional[EventBase]: +def get_power_level_event(auth_events: StateMap[EventBase]) -> Optional[EventBase]: return auth_events.get((EventTypes.PowerLevels, "")) @@ -616,7 +651,7 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int: Returns: the user's power level in this room. """ - power_level_event = _get_power_level_event(auth_events) + power_level_event = get_power_level_event(auth_events) if power_level_event: level = power_level_event.content.get("users", {}).get(user_id) if not level: @@ -640,8 +675,8 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int: return 0 -def _get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int: - power_level_event = _get_power_level_event(auth_events) +def get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int: + power_level_event = get_power_level_event(auth_events) if not power_level_event: return default @@ -728,7 +763,9 @@ def get_public_keys(invite_event: EventBase) -> List[Dict[str, Any]]: return public_keys -def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str, str]]: +def auth_types_for_event( + room_version: RoomVersion, event: Union[EventBase, EventBuilder] +) -> Set[Tuple[str, str]]: """Given an event, return a list of (EventType, StateKey) that may be needed to auth the event. The returned list may be a superset of what would actually be required depending on the full state of the room. @@ -760,4 +797,12 @@ def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str ) auth_types.add(key) + if room_version.msc3083_join_rules and membership == Membership.JOIN: + if "join_authorised_via_users_server" in event.content: + key = ( + EventTypes.Member, + event.content["join_authorised_via_users_server"], + ) + auth_types.add(key) + return auth_types diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 2bfe6a3d37..024e440ff4 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -178,6 +178,34 @@ async def _check_sigs_on_pdu( ) raise SynapseError(403, errmsg, Codes.FORBIDDEN) + # If this is a join event for a restricted room it may have been authorised + # via a different server from the sending server. Check those signatures. + if ( + room_version.msc3083_join_rules + and pdu.type == EventTypes.Member + and pdu.membership == Membership.JOIN + and "join_authorised_via_users_server" in pdu.content + ): + authorising_server = get_domain_from_id( + pdu.content["join_authorised_via_users_server"] + ) + try: + await keyring.verify_event_for_server( + authorising_server, + pdu, + pdu.origin_server_ts if room_version.enforce_key_validity else 0, + ) + except Exception as e: + errmsg = ( + "event id %s: unable to verify signature for authorising server %s: %s" + % ( + pdu.event_id, + authorising_server, + e, + ) + ) + raise SynapseError(403, errmsg, Codes.FORBIDDEN) + def _is_invite_via_3pid(event: EventBase) -> bool: return ( diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index c767d30627..dbadf102f2 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -19,7 +19,6 @@ import logging from typing import ( TYPE_CHECKING, - Any, Awaitable, Callable, Collection, @@ -79,7 +78,15 @@ class InvalidResponseError(RuntimeError): we couldn't parse """ - pass + +@attr.s(slots=True, frozen=True, auto_attribs=True) +class SendJoinResult: + # The event to persist. + event: EventBase + # A string giving the server the event was sent to. + origin: str + state: List[EventBase] + auth_chain: List[EventBase] class FederationClient(FederationBase): @@ -677,7 +684,7 @@ async def send_request(destination: str) -> Tuple[str, EventBase, RoomVersion]: async def send_join( self, destinations: Iterable[str], pdu: EventBase, room_version: RoomVersion - ) -> Dict[str, Any]: + ) -> SendJoinResult: """Sends a join event to one of a list of homeservers. Doing so will cause the remote server to add the event to the graph, @@ -691,18 +698,38 @@ async def send_join( did the make_join) Returns: - a dict with members ``origin`` (a string - giving the server the event was sent to, ``state`` (?) and - ``auth_chain``. + The result of the send join request. Raises: SynapseError: if the chosen remote server returns a 300/400 code, or no servers successfully handle the request. """ - async def send_request(destination) -> Dict[str, Any]: + async def send_request(destination) -> SendJoinResult: response = await self._do_send_join(room_version, destination, pdu) + # If an event was returned (and expected to be returned): + # + # * Ensure it has the same event ID (note that the event ID is a hash + # of the event fields for versions which support MSC3083). + # * Ensure the signatures are good. + # + # Otherwise, fallback to the provided event. + if room_version.msc3083_join_rules and response.event: + event = response.event + + valid_pdu = await self._check_sigs_and_hash_and_fetch_one( + pdu=event, + origin=destination, + outlier=True, + room_version=room_version, + ) + + if valid_pdu is None or event.event_id != pdu.event_id: + raise InvalidResponseError("Returned an invalid join event") + else: + event = pdu + state = response.state auth_chain = response.auth_events @@ -784,11 +811,21 @@ async def _execute(pdu: EventBase) -> None: % (auth_chain_create_events,) ) - return { - "state": signed_state, - "auth_chain": signed_auth, - "origin": destination, - } + return SendJoinResult( + event=event, + state=signed_state, + auth_chain=signed_auth, + origin=destination, + ) + + if room_version.msc3083_join_rules: + # If the join is being authorised via allow rules, we need to send + # the /send_join back to the same server that was originally used + # with /make_join. + if "join_authorised_via_users_server" in pdu.content: + destinations = [ + get_domain_from_id(pdu.content["join_authorised_via_users_server"]) + ] return await self._try_destination_list("send_join", destinations, send_request) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 29619aeeb8..2892a11d7d 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -45,6 +45,7 @@ UnsupportedRoomVersionError, ) from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion +from synapse.crypto.event_signing import compute_event_signature from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.federation.federation_base import FederationBase, event_from_pdu_json @@ -64,7 +65,7 @@ ReplicationGetQueryRestServlet, ) from synapse.storage.databases.main.lock import Lock -from synapse.types import JsonDict +from synapse.types import JsonDict, get_domain_from_id from synapse.util import glob_to_regex, json_decoder, unwrapFirstError from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.caches.response_cache import ResponseCache @@ -586,7 +587,7 @@ async def on_invite_request( async def on_send_join_request( self, origin: str, content: JsonDict, room_id: str ) -> Dict[str, Any]: - context = await self._on_send_membership_event( + event, context = await self._on_send_membership_event( origin, content, Membership.JOIN, room_id ) @@ -597,6 +598,7 @@ async def on_send_join_request( time_now = self._clock.time_msec() return { + "org.matrix.msc3083.v2.event": event.get_pdu_json(), "state": [p.get_pdu_json(time_now) for p in state.values()], "auth_chain": [p.get_pdu_json(time_now) for p in auth_chain], } @@ -681,7 +683,7 @@ async def on_send_knock_request( Returns: The stripped room state. """ - event_context = await self._on_send_membership_event( + _, context = await self._on_send_membership_event( origin, content, Membership.KNOCK, room_id ) @@ -690,14 +692,14 @@ async def on_send_knock_request( # related to the room while the knock request is pending. stripped_room_state = ( await self.store.get_stripped_room_state_from_event_context( - event_context, self._room_prejoin_state_types + context, self._room_prejoin_state_types ) ) return {"knock_state_events": stripped_room_state} async def _on_send_membership_event( self, origin: str, content: JsonDict, membership_type: str, room_id: str - ) -> EventContext: + ) -> Tuple[EventBase, EventContext]: """Handle an on_send_{join,leave,knock} request Does some preliminary validation before passing the request on to the @@ -712,7 +714,7 @@ async def _on_send_membership_event( in the event Returns: - The context of the event after inserting it into the room graph. + The event and context of the event after inserting it into the room graph. Raises: SynapseError if there is a problem with the request, including things like @@ -748,6 +750,33 @@ async def _on_send_membership_event( logger.debug("_on_send_membership_event: pdu sigs: %s", event.signatures) + # Sign the event since we're vouching on behalf of the remote server that + # the event is valid to be sent into the room. Currently this is only done + # if the user is being joined via restricted join rules. + if ( + room_version.msc3083_join_rules + and event.membership == Membership.JOIN + and "join_authorised_via_users_server" in event.content + ): + # We can only authorise our own users. + authorising_server = get_domain_from_id( + event.content["join_authorised_via_users_server"] + ) + if authorising_server != self.server_name: + raise SynapseError( + 400, + f"Cannot authorise request from resident server: {authorising_server}", + ) + + event.signatures.update( + compute_event_signature( + room_version, + event.get_pdu_json(), + self.hs.hostname, + self.hs.signing_key, + ) + ) + event = await self._check_sigs_and_hash(room_version, event) return await self.handler.on_send_membership_event(origin, event) diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index e73bdb52b3..6a8d3ad4fe 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1219,8 +1219,26 @@ def _create_v2_path(path: str, *args: str) -> str: class SendJoinResponse: """The parsed response of a `/send_join` request.""" + # The list of auth events from the /send_join response. auth_events: List[EventBase] + # The list of state from the /send_join response. state: List[EventBase] + # The raw join event from the /send_join response. + event_dict: JsonDict + # The parsed join event from the /send_join response. This will be None if + # "event" is not included in the response. + event: Optional[EventBase] = None + + +@ijson.coroutine +def _event_parser(event_dict: JsonDict): + """Helper function for use with `ijson.kvitems_coro` to parse key-value pairs + to add them to a given dictionary. + """ + + while True: + key, value = yield + event_dict[key] = value @ijson.coroutine @@ -1246,7 +1264,8 @@ class SendJoinParser(ByteParser[SendJoinResponse]): CONTENT_TYPE = "application/json" def __init__(self, room_version: RoomVersion, v1_api: bool): - self._response = SendJoinResponse([], []) + self._response = SendJoinResponse([], [], {}) + self._room_version = room_version # The V1 API has the shape of `[200, {...}]`, which we handle by # prefixing with `item.*`. @@ -1260,12 +1279,21 @@ def __init__(self, room_version: RoomVersion, v1_api: bool): _event_list_parser(room_version, self._response.auth_events), prefix + "auth_chain.item", ) + self._coro_event = ijson.kvitems_coro( + _event_parser(self._response.event_dict), + prefix + "org.matrix.msc3083.v2.event", + ) def write(self, data: bytes) -> int: self._coro_state.send(data) self._coro_auth.send(data) + self._coro_event.send(data) return len(data) def finish(self) -> SendJoinResponse: + if self._response.event_dict: + self._response.event = make_event_from_dict( + self._response.event_dict, self._room_version + ) return self._response diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 41dbdfd0a1..53fac1f8a3 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import logging from typing import TYPE_CHECKING, Collection, List, Optional, Union from synapse import event_auth @@ -20,16 +21,18 @@ Membership, RestrictedJoinRuleTypes, ) -from synapse.api.errors import AuthError +from synapse.api.errors import AuthError, Codes, SynapseError from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.events import EventBase from synapse.events.builder import EventBuilder -from synapse.types import StateMap +from synapse.types import StateMap, get_domain_from_id from synapse.util.metrics import Measure if TYPE_CHECKING: from synapse.server import HomeServer +logger = logging.getLogger(__name__) + class EventAuthHandler: """ @@ -39,6 +42,7 @@ class EventAuthHandler: def __init__(self, hs: "HomeServer"): self._clock = hs.get_clock() self._store = hs.get_datastore() + self._server_name = hs.hostname async def check_from_context( self, room_version: str, event, context, do_sig_check=True @@ -81,15 +85,76 @@ def compute_auth_events( # introduce undesirable "state reset" behaviour. # # All of which sounds a bit tricky so we don't bother for now. - auth_ids = [] - for etype, state_key in event_auth.auth_types_for_event(event): + for etype, state_key in event_auth.auth_types_for_event( + event.room_version, event + ): auth_ev_id = current_state_ids.get((etype, state_key)) if auth_ev_id: auth_ids.append(auth_ev_id) return auth_ids + async def get_user_which_could_invite( + self, room_id: str, current_state_ids: StateMap[str] + ) -> str: + """ + Searches the room state for a local user who has the power level necessary + to invite other users. + + Args: + room_id: The room ID under search. + current_state_ids: The current state of the room. + + Returns: + The MXID of the user which could issue an invite. + + Raises: + SynapseError if no appropriate user is found. + """ + power_level_event_id = current_state_ids.get((EventTypes.PowerLevels, "")) + invite_level = 0 + users_default_level = 0 + if power_level_event_id: + power_level_event = await self._store.get_event(power_level_event_id) + invite_level = power_level_event.content.get("invite", invite_level) + users_default_level = power_level_event.content.get( + "users_default", users_default_level + ) + users = power_level_event.content.get("users", {}) + else: + users = {} + + # Find the user with the highest power level. + users_in_room = await self._store.get_users_in_room(room_id) + # Only interested in local users. + local_users_in_room = [ + u for u in users_in_room if get_domain_from_id(u) == self._server_name + ] + chosen_user = max( + local_users_in_room, + key=lambda user: users.get(user, users_default_level), + default=None, + ) + + # Return the chosen if they can issue invites. + user_power_level = users.get(chosen_user, users_default_level) + if chosen_user and user_power_level >= invite_level: + logger.debug( + "Found a user who can issue invites %s with power level %d >= invite level %d", + chosen_user, + user_power_level, + invite_level, + ) + return chosen_user + + # No user was found. + raise SynapseError( + 400, + "Unable to find a user which could issue an invite", + Codes.UNABLE_TO_GRANT_JOIN, + ) + async def check_host_in_room(self, room_id: str, host: str) -> bool: with Measure(self._clock, "check_host_in_room"): return await self._store.is_host_joined(room_id, host) @@ -134,6 +199,18 @@ async def check_restricted_join_rules( # in any of them. allowed_rooms = await self.get_rooms_that_allow_join(state_ids) if not await self.is_user_in_rooms(allowed_rooms, user_id): + + # If this is a remote request, the user might be in an allowed room + # that we do not know about. + if get_domain_from_id(user_id) != self._server_name: + for room_id in allowed_rooms: + if not await self._store.is_host_joined(room_id, self._server_name): + raise SynapseError( + 400, + f"Unable to check if {user_id} is in allowed rooms.", + Codes.UNABLE_AUTHORISE_JOIN, + ) + raise AuthError( 403, "You do not belong to any of the required rooms to join this room.", diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 5728719909..aba095d2e1 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1494,9 +1494,10 @@ async def do_invite_join( host_list, event, room_version_obj ) - origin = ret["origin"] - state = ret["state"] - auth_chain = ret["auth_chain"] + event = ret.event + origin = ret.origin + state = ret.state + auth_chain = ret.auth_chain auth_chain.sort(key=lambda e: e.depth) logger.debug("do_invite_join auth_chain: %s", auth_chain) @@ -1676,7 +1677,7 @@ async def on_make_join_request( # checking the room version will check that we've actually heard of the room # (and return a 404 otherwise) - room_version = await self.store.get_room_version_id(room_id) + room_version = await self.store.get_room_version(room_id) # now check that we are *still* in the room is_in_room = await self._event_auth_handler.check_host_in_room( @@ -1691,8 +1692,38 @@ async def on_make_join_request( event_content = {"membership": Membership.JOIN} + # If the current room is using restricted join rules, additional information + # may need to be included in the event content in order to efficiently + # validate the event. + # + # Note that this requires the /send_join request to come back to the + # same server. + if room_version.msc3083_join_rules: + state_ids = await self.store.get_current_state_ids(room_id) + if await self._event_auth_handler.has_restricted_join_rules( + state_ids, room_version + ): + prev_member_event_id = state_ids.get((EventTypes.Member, user_id), None) + # If the user is invited or joined to the room already, then + # no additional info is needed. + include_auth_user_id = True + if prev_member_event_id: + prev_member_event = await self.store.get_event(prev_member_event_id) + include_auth_user_id = prev_member_event.membership not in ( + Membership.JOIN, + Membership.INVITE, + ) + + if include_auth_user_id: + event_content[ + "join_authorised_via_users_server" + ] = await self._event_auth_handler.get_user_which_could_invite( + room_id, + state_ids, + ) + builder = self.event_builder_factory.new( - room_version, + room_version.identifier, { "type": EventTypes.Member, "content": event_content, @@ -1710,10 +1741,13 @@ async def on_make_join_request( logger.warning("Failed to create join to %s because %s", room_id, e) raise + # Ensure the user can even join the room. + await self._check_join_restrictions(context, event) + # The remote hasn't signed it yet, obviously. We'll do the full checks # when we get the event back in `on_send_join_request` await self._event_auth_handler.check_from_context( - room_version, event, context, do_sig_check=False + room_version.identifier, event, context, do_sig_check=False ) return event @@ -1958,7 +1992,7 @@ async def on_make_knock_request( @log_function async def on_send_membership_event( self, origin: str, event: EventBase - ) -> EventContext: + ) -> Tuple[EventBase, EventContext]: """ We have received a join/leave/knock event for a room via send_join/leave/knock. @@ -1981,7 +2015,7 @@ async def on_send_membership_event( event: The member event that has been signed by the remote homeserver. Returns: - The context of the event after inserting it into the room graph. + The event and context of the event after inserting it into the room graph. Raises: SynapseError if the event is not accepted into the room @@ -2037,7 +2071,7 @@ async def on_send_membership_event( # all looks good, we can persist the event. await self._run_push_actions_and_persist_event(event, context) - return context + return event, context async def _check_join_restrictions( self, context: EventContext, event: EventBase @@ -2473,7 +2507,7 @@ async def _check_for_soft_fail( ) # Now check if event pass auth against said current state - auth_types = auth_types_for_event(event) + auth_types = auth_types_for_event(room_version_obj, event) current_state_ids_list = [ e for k, e in current_state_ids.items() if k in auth_types ] diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 1192591609..65ad3efa6a 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -16,7 +16,7 @@ import logging import random from http import HTTPStatus -from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple +from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple from synapse import types from synapse.api.constants import AccountDataTypes, EventTypes, Membership @@ -28,6 +28,7 @@ SynapseError, ) from synapse.api.ratelimiting import Ratelimiter +from synapse.event_auth import get_named_level, get_power_level_event from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import ( @@ -340,16 +341,10 @@ async def _local_membership_update( if event.membership == Membership.JOIN: newly_joined = True - prev_member_event = None if prev_member_event_id: prev_member_event = await self.store.get_event(prev_member_event_id) newly_joined = prev_member_event.membership != Membership.JOIN - # Check if the member should be allowed access via membership in a space. - await self.event_auth_handler.check_restricted_join_rules( - prev_state_ids, event.room_version, user_id, prev_member_event - ) - # Only rate-limit if the user actually joined the room, otherwise we'll end # up blocking profile updates. if newly_joined and ratelimit: @@ -701,7 +696,11 @@ async def update_membership_locked( # so don't really fit into the general auth process. raise AuthError(403, "Guest access not allowed") - if not is_host_in_room: + # Check if a remote join should be performed. + remote_join, remote_room_hosts = await self._should_perform_remote_join( + target.to_string(), room_id, remote_room_hosts, content, is_host_in_room + ) + if remote_join: if ratelimit: time_now_s = self.clock.time() ( @@ -826,6 +825,106 @@ async def update_membership_locked( outlier=outlier, ) + async def _should_perform_remote_join( + self, + user_id: str, + room_id: str, + remote_room_hosts: List[str], + content: JsonDict, + is_host_in_room: bool, + ) -> Tuple[bool, List[str]]: + """ + Check whether the server should do a remote join (as opposed to a local + join) for a user. + + Generally a remote join is used if: + + * The server is not yet in the room. + * The server is in the room, the room has restricted join rules, the user + is not joined or invited to the room, and the server does not have + another user who is capable of issuing invites. + + Args: + user_id: The user joining the room. + room_id: The room being joined. + remote_room_hosts: A list of remote room hosts. + content: The content to use as the event body of the join. This may + be modified. + is_host_in_room: True if the host is in the room. + + Returns: + A tuple of: + True if a remote join should be performed. False if the join can be + done locally. + + A list of remote room hosts to use. This is an empty list if a + local join is to be done. + """ + # If the host isn't in the room, pass through the prospective hosts. + if not is_host_in_room: + return True, remote_room_hosts + + # If the host is in the room, but not one of the authorised hosts + # for restricted join rules, a remote join must be used. + room_version = await self.store.get_room_version(room_id) + current_state_ids = await self.store.get_current_state_ids(room_id) + + # If restricted join rules are not being used, a local join can always + # be used. + if not await self.event_auth_handler.has_restricted_join_rules( + current_state_ids, room_version + ): + return False, [] + + # If the user is invited to the room or already joined, the join + # event can always be issued locally. + prev_member_event_id = current_state_ids.get((EventTypes.Member, user_id), None) + prev_member_event = None + if prev_member_event_id: + prev_member_event = await self.store.get_event(prev_member_event_id) + if prev_member_event.membership in ( + Membership.JOIN, + Membership.INVITE, + ): + return False, [] + + # If the local host has a user who can issue invites, then a local + # join can be done. + # + # If not, generate a new list of remote hosts based on which + # can issue invites. + event_map = await self.store.get_events(current_state_ids.values()) + current_state = { + state_key: event_map[event_id] + for state_key, event_id in current_state_ids.items() + } + allowed_servers = get_servers_from_users( + get_users_which_can_issue_invite(current_state) + ) + + # If the local server is not one of allowed servers, then a remote + # join must be done. Return the list of prospective servers based on + # which can issue invites. + if self.hs.hostname not in allowed_servers: + return True, list(allowed_servers) + + # Ensure the member should be allowed access via membership in a room. + await self.event_auth_handler.check_restricted_join_rules( + current_state_ids, room_version, user_id, prev_member_event + ) + + # If this is going to be a local join, additional information must + # be included in the event content in order to efficiently validate + # the event. + content[ + "join_authorised_via_users_server" + ] = await self.event_auth_handler.get_user_which_could_invite( + room_id, + current_state_ids, + ) + + return False, [] + async def transfer_room_state_on_room_upgrade( self, old_room_id: str, room_id: str ) -> None: @@ -1514,3 +1613,63 @@ async def forget(self, user: UserID, room_id: str) -> None: if membership: await self.store.forget(user_id, room_id) + + +def get_users_which_can_issue_invite(auth_events: StateMap[EventBase]) -> List[str]: + """ + Return the list of users which can issue invites. + + This is done by exploring the joined users and comparing their power levels + to the necessyar power level to issue an invite. + + Args: + auth_events: state in force at this point in the room + + Returns: + The users which can issue invites. + """ + invite_level = get_named_level(auth_events, "invite", 0) + users_default_level = get_named_level(auth_events, "users_default", 0) + power_level_event = get_power_level_event(auth_events) + + # Custom power-levels for users. + if power_level_event: + users = power_level_event.content.get("users", {}) + else: + users = {} + + result = [] + + # Check which members are able to invite by ensuring they're joined and have + # the necessary power level. + for (event_type, state_key), event in auth_events.items(): + if event_type != EventTypes.Member: + continue + + if event.membership != Membership.JOIN: + continue + + # Check if the user has a custom power level. + if users.get(state_key, users_default_level) >= invite_level: + result.append(state_key) + + return result + + +def get_servers_from_users(users: List[str]) -> Set[str]: + """ + Resolve a list of users into their servers. + + Args: + users: A list of users. + + Returns: + A set of servers. + """ + servers = set() + for user in users: + try: + servers.add(get_domain_from_id(user)) + except SynapseError: + pass + return servers diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 6223daf522..2e15471435 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -636,16 +636,20 @@ async def resolve_events_with_store( """ try: with Measure(self.clock, "state._resolve_events") as m: - v = KNOWN_ROOM_VERSIONS[room_version] - if v.state_res == StateResolutionVersions.V1: + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] + if room_version_obj.state_res == StateResolutionVersions.V1: return await v1.resolve_events_with_store( - room_id, state_sets, event_map, state_res_store.get_events + room_id, + room_version_obj, + state_sets, + event_map, + state_res_store.get_events, ) else: return await v2.resolve_events_with_store( self.clock, room_id, - room_version, + room_version_obj, state_sets, event_map, state_res_store, diff --git a/synapse/state/v1.py b/synapse/state/v1.py index 267193cedf..92336d7cc8 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -29,7 +29,7 @@ from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError -from synapse.api.room_versions import RoomVersions +from synapse.api.room_versions import RoomVersion, RoomVersions from synapse.events import EventBase from synapse.types import MutableStateMap, StateMap @@ -41,6 +41,7 @@ async def resolve_events_with_store( room_id: str, + room_version: RoomVersion, state_sets: Sequence[StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_map_factory: Callable[[Iterable[str]], Awaitable[Dict[str, EventBase]]], @@ -104,7 +105,7 @@ async def resolve_events_with_store( # get the ids of the auth events which allow us to authenticate the # conflicted state, picking only from the unconflicting state. auth_events = _create_auth_events_from_maps( - unconflicted_state, conflicted_state, state_map + room_version, unconflicted_state, conflicted_state, state_map ) new_needed_events = set(auth_events.values()) @@ -132,7 +133,7 @@ async def resolve_events_with_store( state_map.update(state_map_new) return _resolve_with_state( - unconflicted_state, conflicted_state, auth_events, state_map + room_version, unconflicted_state, conflicted_state, auth_events, state_map ) @@ -187,6 +188,7 @@ def _seperate( def _create_auth_events_from_maps( + room_version: RoomVersion, unconflicted_state: StateMap[str], conflicted_state: StateMap[Set[str]], state_map: Dict[str, EventBase], @@ -194,6 +196,7 @@ def _create_auth_events_from_maps( """ Args: + room_version: The room version. unconflicted_state: The unconflicted state map. conflicted_state: The conflicted state map. state_map: @@ -205,7 +208,9 @@ def _create_auth_events_from_maps( for event_ids in conflicted_state.values(): for event_id in event_ids: if event_id in state_map: - keys = event_auth.auth_types_for_event(state_map[event_id]) + keys = event_auth.auth_types_for_event( + room_version, state_map[event_id] + ) for key in keys: if key not in auth_events: auth_event_id = unconflicted_state.get(key, None) @@ -215,6 +220,7 @@ def _create_auth_events_from_maps( def _resolve_with_state( + room_version: RoomVersion, unconflicted_state_ids: MutableStateMap[str], conflicted_state_ids: StateMap[Set[str]], auth_event_ids: StateMap[str], @@ -235,7 +241,9 @@ def _resolve_with_state( } try: - resolved_state = _resolve_state_events(conflicted_state, auth_events) + resolved_state = _resolve_state_events( + room_version, conflicted_state, auth_events + ) except Exception: logger.exception("Failed to resolve state") raise @@ -248,7 +256,9 @@ def _resolve_with_state( def _resolve_state_events( - conflicted_state: StateMap[List[EventBase]], auth_events: MutableStateMap[EventBase] + room_version: RoomVersion, + conflicted_state: StateMap[List[EventBase]], + auth_events: MutableStateMap[EventBase], ) -> StateMap[EventBase]: """This is where we actually decide which of the conflicted state to use. @@ -263,21 +273,27 @@ def _resolve_state_events( if POWER_KEY in conflicted_state: events = conflicted_state[POWER_KEY] logger.debug("Resolving conflicted power levels %r", events) - resolved_state[POWER_KEY] = _resolve_auth_events(events, auth_events) + resolved_state[POWER_KEY] = _resolve_auth_events( + room_version, events, auth_events + ) auth_events.update(resolved_state) for key, events in conflicted_state.items(): if key[0] == EventTypes.JoinRules: logger.debug("Resolving conflicted join rules %r", events) - resolved_state[key] = _resolve_auth_events(events, auth_events) + resolved_state[key] = _resolve_auth_events( + room_version, events, auth_events + ) auth_events.update(resolved_state) for key, events in conflicted_state.items(): if key[0] == EventTypes.Member: logger.debug("Resolving conflicted member lists %r", events) - resolved_state[key] = _resolve_auth_events(events, auth_events) + resolved_state[key] = _resolve_auth_events( + room_version, events, auth_events + ) auth_events.update(resolved_state) @@ -290,12 +306,14 @@ def _resolve_state_events( def _resolve_auth_events( - events: List[EventBase], auth_events: StateMap[EventBase] + room_version: RoomVersion, events: List[EventBase], auth_events: StateMap[EventBase] ) -> EventBase: reverse = list(reversed(_ordered_events(events))) auth_keys = { - key for event in events for key in event_auth.auth_types_for_event(event) + key + for event in events + for key in event_auth.auth_types_for_event(room_version, event) } new_auth_events = {} diff --git a/synapse/state/v2.py b/synapse/state/v2.py index e66e6571c8..7b1e8361de 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -36,7 +36,7 @@ from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.api.room_versions import RoomVersion from synapse.events import EventBase from synapse.types import MutableStateMap, StateMap from synapse.util import Clock @@ -53,7 +53,7 @@ async def resolve_events_with_store( clock: Clock, room_id: str, - room_version: str, + room_version: RoomVersion, state_sets: Sequence[StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_res_store: "synapse.state.StateResolutionStore", @@ -497,7 +497,7 @@ def _get_power_order(event_id): async def _iterative_auth_checks( clock: Clock, room_id: str, - room_version: str, + room_version: RoomVersion, event_ids: List[str], base_state: StateMap[str], event_map: Dict[str, EventBase], @@ -519,7 +519,6 @@ async def _iterative_auth_checks( Returns the final updated state """ resolved_state = dict(base_state) - room_version_obj = KNOWN_ROOM_VERSIONS[room_version] for idx, event_id in enumerate(event_ids, start=1): event = event_map[event_id] @@ -538,7 +537,7 @@ async def _iterative_auth_checks( if ev.rejected_reason is None: auth_events[(ev.type, ev.state_key)] = ev - for key in event_auth.auth_types_for_event(event): + for key in event_auth.auth_types_for_event(room_version, event): if key in resolved_state: ev_id = resolved_state[key] ev = await _get_event(room_id, ev_id, event_map, state_res_store) @@ -548,7 +547,7 @@ async def _iterative_auth_checks( try: event_auth.check( - room_version_obj, + room_version, event, auth_events, do_sig_check=False, diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 43fc79ca74..8370a27195 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -484,7 +484,7 @@ def do_check(self, events, edges, expected_state_ids): state_d = resolve_events_with_store( FakeClock(), ROOM_ID, - RoomVersions.V2.identifier, + RoomVersions.V2, [state_at_event[n] for n in prev_events], event_map=event_map, state_res_store=TestStateResolutionStore(event_map), @@ -496,7 +496,7 @@ def do_check(self, events, edges, expected_state_ids): if fake_event.state_key is not None: state_after[(fake_event.type, fake_event.state_key)] = event_id - auth_types = set(auth_types_for_event(fake_event)) + auth_types = set(auth_types_for_event(RoomVersions.V6, fake_event)) auth_events = [] for key in auth_types: @@ -633,7 +633,7 @@ def test_event_map_none(self): state_d = resolve_events_with_store( FakeClock(), ROOM_ID, - RoomVersions.V2.identifier, + RoomVersions.V2, [self.state_at_bob, self.state_at_charlie], event_map=None, state_res_store=TestStateResolutionStore(self.event_map), diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index dbacce4380..8c95a0a2fb 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import List, Optional from canonicaljson import json @@ -234,8 +234,8 @@ def __init__(self, base_builder, event_id): async def build( self, - prev_event_ids, - auth_event_ids, + prev_event_ids: List[str], + auth_event_ids: Optional[List[str]], depth: Optional[int] = None, ): built_event = await self._base_builder.build( diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index f73306ecc4..e5550aec4d 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -351,7 +351,11 @@ def test_join_rules_msc3083_restricted(self): """ Test joining a restricted room from MSC3083. - This is pretty much the same test as public. + This is similar to the public test, but has some additional checks on + signatures. + + The checks which care about signatures fake them by simply adding an + object of the proper form, not generating valid signatures. """ creator = "@creator:example.com" pleb = "@joiner:example.com" @@ -359,6 +363,7 @@ def test_join_rules_msc3083_restricted(self): auth_events = { ("m.room.create", ""): _create_event(creator), ("m.room.member", creator): _join_event(creator), + ("m.room.power_levels", ""): _power_levels_event(creator, {"invite": 0}), ("m.room.join_rules", ""): _join_rules_event(creator, "restricted"), } @@ -371,19 +376,81 @@ def test_join_rules_msc3083_restricted(self): do_sig_check=False, ) - # Check join. + # A properly formatted join event should work. + authorised_join_event = _join_event( + pleb, + additional_content={ + "join_authorised_via_users_server": "@creator:example.com" + }, + ) event_auth.check( RoomVersions.MSC3083, - _join_event(pleb), + authorised_join_event, auth_events, do_sig_check=False, ) - # A user cannot be force-joined to a room. + # A join issued by a specific user works (i.e. the power level checks + # are done properly). + pl_auth_events = auth_events.copy() + pl_auth_events[("m.room.power_levels", "")] = _power_levels_event( + creator, {"invite": 100, "users": {"@inviter:foo.test": 150}} + ) + pl_auth_events[("m.room.member", "@inviter:foo.test")] = _join_event( + "@inviter:foo.test" + ) + event_auth.check( + RoomVersions.MSC3083, + _join_event( + pleb, + additional_content={ + "join_authorised_via_users_server": "@inviter:foo.test" + }, + ), + pl_auth_events, + do_sig_check=False, + ) + + # A join which is missing an authorised server is rejected. with self.assertRaises(AuthError): event_auth.check( RoomVersions.MSC3083, - _member_event(pleb, "join", sender=creator), + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + + # An join authorised by a user who is not in the room is rejected. + pl_auth_events = auth_events.copy() + pl_auth_events[("m.room.power_levels", "")] = _power_levels_event( + creator, {"invite": 100, "users": {"@other:example.com": 150}} + ) + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.MSC3083, + _join_event( + pleb, + additional_content={ + "join_authorised_via_users_server": "@other:example.com" + }, + ), + auth_events, + do_sig_check=False, + ) + + # A user cannot be force-joined to a room. (This uses an event which + # *would* be valid, but is sent be a different user.) + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.MSC3083, + _member_event( + pleb, + "join", + sender=creator, + additional_content={ + "join_authorised_via_users_server": "@inviter:foo.test" + }, + ), auth_events, do_sig_check=False, ) @@ -393,7 +460,7 @@ def test_join_rules_msc3083_restricted(self): with self.assertRaises(AuthError): event_auth.check( RoomVersions.MSC3083, - _join_event(pleb), + authorised_join_event, auth_events, do_sig_check=False, ) @@ -402,12 +469,13 @@ def test_join_rules_msc3083_restricted(self): auth_events[("m.room.member", pleb)] = _member_event(pleb, "leave") event_auth.check( RoomVersions.MSC3083, - _join_event(pleb), + authorised_join_event, auth_events, do_sig_check=False, ) - # A user can send a join if they're in the room. + # A user can send a join if they're in the room. (This doesn't need to + # be authorised since the user is already joined.) auth_events[("m.room.member", pleb)] = _member_event(pleb, "join") event_auth.check( RoomVersions.MSC3083, @@ -416,7 +484,8 @@ def test_join_rules_msc3083_restricted(self): do_sig_check=False, ) - # A user can accept an invite. + # A user can accept an invite. (This doesn't need to be authorised since + # the user was invited.) auth_events[("m.room.member", pleb)] = _member_event( pleb, "invite", sender=creator ) @@ -446,7 +515,10 @@ def _create_event(user_id: str) -> EventBase: def _member_event( - user_id: str, membership: str, sender: Optional[str] = None + user_id: str, + membership: str, + sender: Optional[str] = None, + additional_content: Optional[dict] = None, ) -> EventBase: return make_event_from_dict( { @@ -455,14 +527,14 @@ def _member_event( "type": "m.room.member", "sender": sender or user_id, "state_key": user_id, - "content": {"membership": membership}, + "content": {"membership": membership, **(additional_content or {})}, "prev_events": [], } ) -def _join_event(user_id: str) -> EventBase: - return _member_event(user_id, "join") +def _join_event(user_id: str, additional_content: Optional[dict] = None) -> EventBase: + return _member_event(user_id, "join", additional_content=additional_content) def _power_levels_event(sender: str, content: JsonDict) -> EventBase: From b7186c6e8ddacc328ae2c155162b36291a3c2b79 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 26 Jul 2021 12:49:53 -0400 Subject: [PATCH 463/619] Add type hints to state handler. (#10482) --- changelog.d/10482.misc | 1 + synapse/state/__init__.py | 26 ++++++++++++++---------- synapse/storage/databases/state/store.py | 17 ++++++++++------ synapse/storage/state.py | 4 ++-- 4 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 changelog.d/10482.misc diff --git a/changelog.d/10482.misc b/changelog.d/10482.misc new file mode 100644 index 0000000000..4e9e2126e1 --- /dev/null +++ b/changelog.d/10482.misc @@ -0,0 +1 @@ +Additional type hints in the state handler. diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 2e15471435..463ce58dae 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -16,6 +16,7 @@ import logging from collections import defaultdict, namedtuple from typing import ( + TYPE_CHECKING, Any, Awaitable, Callable, @@ -52,6 +53,10 @@ from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.metrics import Measure, measure_func +if TYPE_CHECKING: + from synapse.server import HomeServer + from synapse.storage.databases.main import DataStore + logger = logging.getLogger(__name__) metrics_logger = logging.getLogger("synapse.state.metrics") @@ -74,7 +79,7 @@ POWER_KEY = (EventTypes.PowerLevels, "") -def _gen_state_id(): +def _gen_state_id() -> str: global _NEXT_STATE_ID s = "X%d" % (_NEXT_STATE_ID,) _NEXT_STATE_ID += 1 @@ -109,7 +114,7 @@ def __init__( # `state_id` is either a state_group (and so an int) or a string. This # ensures we don't accidentally persist a state_id as a stateg_group if state_group: - self.state_id = state_group + self.state_id: Union[str, int] = state_group else: self.state_id = _gen_state_id() @@ -122,7 +127,7 @@ class StateHandler: where necessary """ - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.store = hs.get_datastore() self.state_store = hs.get_storage().state @@ -507,7 +512,7 @@ class StateResolutionHandler: be storage-independent. """ - def __init__(self, hs): + def __init__(self, hs: "HomeServer"): self.clock = hs.get_clock() self.resolve_linearizer = Linearizer(name="state_resolve_lock") @@ -657,13 +662,15 @@ async def resolve_events_with_store( finally: self._record_state_res_metrics(room_id, m.get_resource_usage()) - def _record_state_res_metrics(self, room_id: str, rusage: ContextResourceUsage): + def _record_state_res_metrics( + self, room_id: str, rusage: ContextResourceUsage + ) -> None: room_metrics = self._state_res_metrics[room_id] room_metrics.cpu_time += rusage.ru_utime + rusage.ru_stime room_metrics.db_time += rusage.db_txn_duration_sec room_metrics.db_events += rusage.evt_db_fetch_count - def _report_metrics(self): + def _report_metrics(self) -> None: if not self._state_res_metrics: # no state res has happened since the last iteration: don't bother logging. return @@ -773,16 +780,13 @@ def _make_state_cache_entry( ) -@attr.s(slots=True) +@attr.s(slots=True, auto_attribs=True) class StateResolutionStore: """Interface that allows state resolution algorithms to access the database in well defined way. - - Args: - store (DataStore) """ - store = attr.ib() + store: "DataStore" def get_events( self, event_ids: Iterable[str], allow_rejected: bool = False diff --git a/synapse/storage/databases/state/store.py b/synapse/storage/databases/state/store.py index e38461adbc..f839c0c24f 100644 --- a/synapse/storage/databases/state/store.py +++ b/synapse/storage/databases/state/store.py @@ -372,18 +372,23 @@ def _insert_into_cache( ) async def store_state_group( - self, event_id, room_id, prev_group, delta_ids, current_state_ids + self, + event_id: str, + room_id: str, + prev_group: Optional[int], + delta_ids: Optional[StateMap[str]], + current_state_ids: StateMap[str], ) -> int: """Store a new set of state, returning a newly assigned state group. Args: - event_id (str): The event ID for which the state was calculated - room_id (str) - prev_group (int|None): A previous state group for the room, optional. - delta_ids (dict|None): The delta between state at `prev_group` and + event_id: The event ID for which the state was calculated + room_id + prev_group: A previous state group for the room, optional. + delta_ids: The delta between state at `prev_group` and `current_state_ids`, if `prev_group` was given. Same format as `current_state_ids`. - current_state_ids (dict): The state to store. Map of (type, state_key) + current_state_ids: The state to store. Map of (type, state_key) to event_id. Returns: diff --git a/synapse/storage/state.py b/synapse/storage/state.py index f8fbba9d38..e5400d681a 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -570,8 +570,8 @@ async def store_state_group( event_id: str, room_id: str, prev_group: Optional[int], - delta_ids: Optional[dict], - current_state_ids: dict, + delta_ids: Optional[StateMap[str]], + current_state_ids: StateMap[str], ) -> int: """Store a new set of state, returning a newly assigned state group. From b3a757eb3b11151b1fac7833d6be239c9084f725 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 26 Jul 2021 23:28:20 -0600 Subject: [PATCH 464/619] Support MSC2033: Device ID on whoami (#9918) * Fix no-access-token bug in deactivation tests * Support MSC2033: Device ID on whoami * Test for appservices too MSC: https://github.com/matrix-org/matrix-doc/pull/2033 The MSC has passed FCP, which means stable endpoints can be used. --- changelog.d/9918.feature | 1 + synapse/rest/client/v2_alpha/account.py | 9 ++++- tests/rest/client/v2_alpha/test_account.py | 43 +++++++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9918.feature diff --git a/changelog.d/9918.feature b/changelog.d/9918.feature new file mode 100644 index 0000000000..98f0a50893 --- /dev/null +++ b/changelog.d/9918.feature @@ -0,0 +1 @@ +Add support for [MSC2033](https://github.com/matrix-org/matrix-doc/pull/2033): `device_id` on `/account/whoami`. \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 085561d3e9..fb5ad2906e 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -884,7 +884,14 @@ def __init__(self, hs): async def on_GET(self, request): requester = await self.auth.get_user_by_req(request) - return 200, {"user_id": requester.user.to_string()} + response = {"user_id": requester.user.to_string()} + + # Appservices and similar accounts do not have device IDs + # that we can report on, so exclude them for compliance. + if requester.device_id is not None: + response["device_id"] = requester.device_id + + return 200, response def register_servlets(hs, http_server): diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index 4ef19145d1..317a2287e3 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -24,6 +24,7 @@ import synapse.rest.admin from synapse.api.constants import LoginType, Membership from synapse.api.errors import Codes, HttpResponseException +from synapse.appservice import ApplicationService from synapse.rest.client.v1 import login, room from synapse.rest.client.v2_alpha import account, register from synapse.rest.synapse.client.password_reset import PasswordResetSubmitTokenResource @@ -397,7 +398,7 @@ def test_deactivate_account(self): self.assertTrue(self.get_success(store.get_user_deactivated_status(user_id))) # Check that this access token has been invalidated. - channel = self.make_request("GET", "account/whoami") + channel = self.make_request("GET", "account/whoami", access_token=tok) self.assertEqual(channel.code, 401) def test_pending_invites(self): @@ -458,6 +459,46 @@ def deactivate(self, user_id, tok): self.assertEqual(channel.code, 200) +class WhoamiTestCase(unittest.HomeserverTestCase): + + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + login.register_servlets, + account.register_servlets, + register.register_servlets, + ] + + def test_GET_whoami(self): + device_id = "wouldgohere" + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test", device_id=device_id) + + whoami = self.whoami(tok) + self.assertEqual(whoami, {"user_id": user_id, "device_id": device_id}) + + def test_GET_whoami_appservices(self): + user_id = "@as:test" + as_token = "i_am_an_app_service" + + appservice = ApplicationService( + as_token, + self.hs.config.server_name, + id="1234", + namespaces={"users": [{"regex": user_id, "exclusive": True}]}, + sender=user_id, + ) + self.hs.get_datastore().services_cache.append(appservice) + + whoami = self.whoami(as_token) + self.assertEqual(whoami, {"user_id": user_id}) + self.assertFalse(hasattr(whoami, "device_id")) + + def whoami(self, tok): + channel = self.make_request("GET", "account/whoami", {}, access_token=tok) + self.assertEqual(channel.code, 200) + return channel.json_body + + class ThreepidEmailRestTestCase(unittest.HomeserverTestCase): servlets = [ From 92a882254b5a0d33ee1701073e2b1c1a9926ffd8 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 27 Jul 2021 11:59:15 +0100 Subject: [PATCH 465/619] Change release script to update debian changelog for RCs (#10465) --- changelog.d/10465.misc | 1 + scripts-dev/release.py | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10465.misc diff --git a/changelog.d/10465.misc b/changelog.d/10465.misc new file mode 100644 index 0000000000..4de6201dfc --- /dev/null +++ b/changelog.d/10465.misc @@ -0,0 +1 @@ +Fix release script to correctly version debian changelog when doing RCs. diff --git a/scripts-dev/release.py b/scripts-dev/release.py index 5bfaa4ad2f..cff433af2a 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -139,6 +139,11 @@ def run(): # Switch to the release branch. parsed_new_version = version.parse(new_version) + + # We assume for debian changelogs that we only do RCs or full releases. + assert not parsed_new_version.is_devrelease + assert not parsed_new_version.is_postrelease + release_branch_name = ( f"release-v{parsed_new_version.major}.{parsed_new_version.minor}" ) @@ -190,12 +195,21 @@ def run(): # Generate changelogs subprocess.run("python3 -m towncrier", shell=True) - # Generate debian changelogs if its not an RC. - if not rc: - subprocess.run( - f'dch -M -v {new_version} "New synapse release {new_version}."', shell=True - ) - subprocess.run('dch -M -r -D stable ""', shell=True) + # Generate debian changelogs + if parsed_new_version.pre is not None: + # If this is an RC then we need to coerce the version string to match + # Debian norms, e.g. 1.39.0rc2 gets converted to 1.39.0~rc2. + base_ver = parsed_new_version.base_version + pre_type, pre_num = parsed_new_version.pre + debian_version = f"{base_ver}~{pre_type}{pre_num}" + else: + debian_version = new_version + + subprocess.run( + f'dch -M -v {debian_version} "New synapse release {debian_version}."', + shell=True, + ) + subprocess.run('dch -M -r -D stable ""', shell=True) # Show the user the changes and ask if they want to edit the change log. repo.git.add("-u") From 2476d5373cde3a881b6f8f3ccc5d19707e9f600d Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Tue, 27 Jul 2021 11:45:10 +0000 Subject: [PATCH 466/619] Mitigate media repo XSSs on IE11. (#10468) IE11 doesn't support Content-Security-Policy but it has support for a non-standard X-Content-Security-Policy header, which only supports the sandbox directive. This prevents script execution, so it at least offers some protection against media repo-based attacks. Signed-off-by: Denis Kasak --- changelog.d/10468.misc | 1 + synapse/rest/media/v1/download_resource.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/10468.misc diff --git a/changelog.d/10468.misc b/changelog.d/10468.misc new file mode 100644 index 0000000000..b9854bb4c1 --- /dev/null +++ b/changelog.d/10468.misc @@ -0,0 +1 @@ +Mitigate media repo XSS attacks on IE11 via the non-standard X-Content-Security-Policy header. diff --git a/synapse/rest/media/v1/download_resource.py b/synapse/rest/media/v1/download_resource.py index cd2468f9c5..d6d938953e 100644 --- a/synapse/rest/media/v1/download_resource.py +++ b/synapse/rest/media/v1/download_resource.py @@ -49,6 +49,8 @@ async def _async_render_GET(self, request: Request) -> None: b" media-src 'self';" b" object-src 'self';", ) + # Limited non-standard form of CSP for IE11 + request.setHeader(b"X-Content-Security-Policy", b"sandbox;") request.setHeader( b"Referrer-Policy", b"no-referrer", From 13944678c3c696418bfde3463bda4cedc8d289c2 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 27 Jul 2021 08:08:51 -0400 Subject: [PATCH 467/619] Use new go test running syntax for complement. (#10488) Updates CI and the helper script t ensures all tests are run (in parallel). --- .github/workflows/tests.yml | 2 +- changelog.d/10488.misc | 1 + scripts-dev/complement.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10488.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4e61824ee5..0a62c62d02 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -344,7 +344,7 @@ jobs: working-directory: complement/dockerfiles # Run Complement - - run: go test -v -tags synapse_blacklist,msc2403,msc2946,msc3083 ./tests + - run: go test -v -tags synapse_blacklist,msc2403,msc2946,msc3083 ./tests/... env: COMPLEMENT_BASE_IMAGE: complement-synapse:latest working-directory: complement diff --git a/changelog.d/10488.misc b/changelog.d/10488.misc new file mode 100644 index 0000000000..a55502c163 --- /dev/null +++ b/changelog.d/10488.misc @@ -0,0 +1 @@ +Update syntax used to run complement tests. diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index 4df224be67..cba015d942 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -65,4 +65,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -go test -v -tags synapse_blacklist,msc2946,msc3083,msc2403 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests +go test -v -tags synapse_blacklist,msc2946,msc3083,msc2403 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests/... From e16eab29d671504144f4185d4738e5bfd7a3a2c6 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 27 Jul 2021 14:32:05 +0100 Subject: [PATCH 468/619] Add a PeriodicallyFlushingMemoryHandler to prevent logging silence (#10407) Signed-off-by: Olivier Wilkinson (reivilibre) --- changelog.d/10407.feature | 1 + docs/sample_log_config.yaml | 5 ++- synapse/config/logger.py | 5 ++- synapse/logging/handlers.py | 88 +++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10407.feature create mode 100644 synapse/logging/handlers.py diff --git a/changelog.d/10407.feature b/changelog.d/10407.feature new file mode 100644 index 0000000000..db277d9ecd --- /dev/null +++ b/changelog.d/10407.feature @@ -0,0 +1 @@ +Add a buffered logging handler which periodically flushes itself. diff --git a/docs/sample_log_config.yaml b/docs/sample_log_config.yaml index 669e600081..b088c83405 100644 --- a/docs/sample_log_config.yaml +++ b/docs/sample_log_config.yaml @@ -28,7 +28,7 @@ handlers: # will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR # logs will still be flushed immediately. buffer: - class: logging.handlers.MemoryHandler + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler target: file # The capacity is the number of log lines that are buffered before # being written to disk. Increasing this will lead to better @@ -36,6 +36,9 @@ handlers: # be written to disk. capacity: 10 flushLevel: 30 # Flush for WARNING logs as well + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + period: 5 # A handler that writes logs to stderr. Unused by default, but can be used # instead of "buffer" and "file" in the logger handlers. diff --git a/synapse/config/logger.py b/synapse/config/logger.py index ad4e6e61c3..dcd3ed1dac 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -71,7 +71,7 @@ # will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR # logs will still be flushed immediately. buffer: - class: logging.handlers.MemoryHandler + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler target: file # The capacity is the number of log lines that are buffered before # being written to disk. Increasing this will lead to better @@ -79,6 +79,9 @@ # be written to disk. capacity: 10 flushLevel: 30 # Flush for WARNING logs as well + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + period: 5 # A handler that writes logs to stderr. Unused by default, but can be used # instead of "buffer" and "file" in the logger handlers. diff --git a/synapse/logging/handlers.py b/synapse/logging/handlers.py new file mode 100644 index 0000000000..a6c212f300 --- /dev/null +++ b/synapse/logging/handlers.py @@ -0,0 +1,88 @@ +import logging +import time +from logging import Handler, LogRecord +from logging.handlers import MemoryHandler +from threading import Thread +from typing import Optional + +from twisted.internet.interfaces import IReactorCore + + +class PeriodicallyFlushingMemoryHandler(MemoryHandler): + """ + This is a subclass of MemoryHandler that additionally spawns a background + thread to periodically flush the buffer. + + This prevents messages from being buffered for too long. + + Additionally, all messages will be immediately flushed if the reactor has + not yet been started. + """ + + def __init__( + self, + capacity: int, + flushLevel: int = logging.ERROR, + target: Optional[Handler] = None, + flushOnClose: bool = True, + period: float = 5.0, + reactor: Optional[IReactorCore] = None, + ) -> None: + """ + period: the period between automatic flushes + + reactor: if specified, a custom reactor to use. If not specifies, + defaults to the globally-installed reactor. + Log entries will be flushed immediately until this reactor has + started. + """ + super().__init__(capacity, flushLevel, target, flushOnClose) + + self._flush_period: float = period + self._active: bool = True + self._reactor_started = False + + self._flushing_thread: Thread = Thread( + name="PeriodicallyFlushingMemoryHandler flushing thread", + target=self._flush_periodically, + ) + self._flushing_thread.start() + + def on_reactor_running(): + self._reactor_started = True + + reactor_to_use: IReactorCore + if reactor is None: + from twisted.internet import reactor as global_reactor + + reactor_to_use = global_reactor # type: ignore[assignment] + else: + reactor_to_use = reactor + + # call our hook when the reactor start up + reactor_to_use.callWhenRunning(on_reactor_running) + + def shouldFlush(self, record: LogRecord) -> bool: + """ + Before reactor start-up, log everything immediately. + Otherwise, fall back to original behaviour of waiting for the buffer to fill. + """ + + if self._reactor_started: + return super().shouldFlush(record) + else: + return True + + def _flush_periodically(self): + """ + Whilst this handler is active, flush the handler periodically. + """ + + while self._active: + # flush is thread-safe; it acquires and releases the lock internally + self.flush() + time.sleep(self._flush_period) + + def close(self) -> None: + self._active = False + super().close() From 74d09a43d9e0f65f1292aa51f58ea676e4aefc7f Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 27 Jul 2021 14:36:38 +0100 Subject: [PATCH 469/619] Always communicate device OTK counts to clients (#10485) Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- changelog.d/10485.bugfix | 1 + synapse/api/constants.py | 8 ++++++++ synapse/handlers/sync.py | 4 ++++ .../storage/databases/main/end_to_end_keys.py | 9 ++++++++- tests/handlers/test_e2e_keys.py | 20 ++++++++++++++----- 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10485.bugfix diff --git a/changelog.d/10485.bugfix b/changelog.d/10485.bugfix new file mode 100644 index 0000000000..9b44006dc0 --- /dev/null +++ b/changelog.d/10485.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where Synapse would not inform clients that a device had exhausted its one-time-key pool, potentially causing problems decrypting events. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 8363c2bb0f..8c7ad2a407 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -127,6 +127,14 @@ class ToDeviceEventTypes: RoomKeyRequest = "m.room_key_request" +class DeviceKeyAlgorithms: + """Spec'd algorithms for the generation of per-device keys""" + + ED25519 = "ed25519" + CURVE25519 = "curve25519" + SIGNED_CURVE25519 = "signed_curve25519" + + class EduTypes: Presence = "m.presence" diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 150a4f291e..f30bfcc93c 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -1093,6 +1093,10 @@ async def generate_sync_result( one_time_key_counts: JsonDict = {} unused_fallback_key_types: List[str] = [] if device_id: + # TODO: We should have a way to let clients differentiate between the states of: + # * no change in OTK count since the provided since token + # * the server has zero OTKs left for this device + # Spec issue: https://github.com/matrix-org/matrix-doc/issues/3298 one_time_key_counts = await self.store.count_e2e_one_time_keys( user_id, device_id ) diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index 78ae68ec68..1edc96042b 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -21,6 +21,7 @@ from twisted.enterprise.adbapi import Connection +from synapse.api.constants import DeviceKeyAlgorithms from synapse.logging.opentracing import log_kv, set_tag, trace from synapse.storage._base import SQLBaseStore, db_to_json from synapse.storage.database import DatabasePool, make_in_list_sql_clause @@ -381,9 +382,15 @@ def _count_e2e_one_time_keys(txn): " GROUP BY algorithm" ) txn.execute(sql, (user_id, device_id)) - result = {} + + # Initially set the key count to 0. This ensures that the client will always + # receive *some count*, even if it's 0. + result = {DeviceKeyAlgorithms.SIGNED_CURVE25519: 0} + + # Override entries with the count of any keys we pulled from the database for algorithm, key_count in txn: result[algorithm] = key_count + return result return await self.db_pool.runInteraction( diff --git a/tests/handlers/test_e2e_keys.py b/tests/handlers/test_e2e_keys.py index e0a24824cc..39e7b1ab25 100644 --- a/tests/handlers/test_e2e_keys.py +++ b/tests/handlers/test_e2e_keys.py @@ -47,12 +47,16 @@ def test_reupload_one_time_keys(self): "alg2:k3": {"key": "key3"}, } + # Note that "signed_curve25519" is always returned in key count responses. This is necessary until + # https://github.com/matrix-org/matrix-doc/issues/3298 is fixed. res = self.get_success( self.handler.upload_keys_for_user( local_user, device_id, {"one_time_keys": keys} ) ) - self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1, "alg2": 2}}) + self.assertDictEqual( + res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}} + ) # we should be able to change the signature without a problem keys["alg2:k2"]["signatures"]["k1"] = "sig2" @@ -61,7 +65,9 @@ def test_reupload_one_time_keys(self): local_user, device_id, {"one_time_keys": keys} ) ) - self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1, "alg2": 2}}) + self.assertDictEqual( + res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}} + ) def test_change_one_time_keys(self): """attempts to change one-time-keys should be rejected""" @@ -79,7 +85,9 @@ def test_change_one_time_keys(self): local_user, device_id, {"one_time_keys": keys} ) ) - self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1, "alg2": 2}}) + self.assertDictEqual( + res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}} + ) # Error when changing string key self.get_failure( @@ -89,7 +97,7 @@ def test_change_one_time_keys(self): SynapseError, ) - # Error when replacing dict key with strin + # Error when replacing dict key with string self.get_failure( self.handler.upload_keys_for_user( local_user, device_id, {"one_time_keys": {"alg2:k3": "key2"}} @@ -131,7 +139,9 @@ def test_claim_one_time_key(self): local_user, device_id, {"one_time_keys": keys} ) ) - self.assertDictEqual(res, {"one_time_key_counts": {"alg1": 1}}) + self.assertDictEqual( + res, {"one_time_key_counts": {"alg1": 1, "signed_curve25519": 0}} + ) res2 = self.get_success( self.handler.claim_one_time_keys( From 10dcfae46f8c49f5fa544557ccf2e69346289e1d Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 27 Jul 2021 15:25:39 +0100 Subject: [PATCH 470/619] Fix typo that causes R30v2 to actually be old R30 (#10486) Signed-off-by: Olivier Wilkinson (reivilibre) --- changelog.d/10486.bugfix | 1 + synapse/app/phone_stats_home.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10486.bugfix diff --git a/changelog.d/10486.bugfix b/changelog.d/10486.bugfix new file mode 100644 index 0000000000..7c65c16e96 --- /dev/null +++ b/changelog.d/10486.bugfix @@ -0,0 +1 @@ +Fix reporting old R30 stats as R30v2 stats. diff --git a/synapse/app/phone_stats_home.py b/synapse/app/phone_stats_home.py index 96defac1d2..86ad7337a9 100644 --- a/synapse/app/phone_stats_home.py +++ b/synapse/app/phone_stats_home.py @@ -109,7 +109,7 @@ async def phone_stats_home(hs, stats, stats_process=_stats_process): for name, count in r30_results.items(): stats["r30_users_" + name] = count - r30v2_results = await store.count_r30_users() + r30v2_results = await store.count_r30v2_users() for name, count in r30v2_results.items(): stats["r30v2_users_" + name] = count From 31c6b30dd425909d188695e65921e48235f41064 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 27 Jul 2021 18:34:15 +0300 Subject: [PATCH 471/619] Fix import of the default SAML mapping provider. (#10477) Fix a circular import, which was causing exceptions on boot if SAML was configured. --- changelog.d/10477.bugfix | 1 + synapse/handlers/_base.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog.d/10477.bugfix diff --git a/changelog.d/10477.bugfix b/changelog.d/10477.bugfix new file mode 100644 index 0000000000..bcc92de434 --- /dev/null +++ b/changelog.d/10477.bugfix @@ -0,0 +1 @@ +Fix bug introduced in Synapse 1.38 which caused an exception at startup when SAML authentication was enabled. diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index 525f3d39b1..6a05a65305 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -15,8 +15,6 @@ import logging from typing import TYPE_CHECKING, Optional -import synapse.state -import synapse.storage import synapse.types from synapse.api.constants import EventTypes, Membership from synapse.api.ratelimiting import Ratelimiter From 076deade028613da56391758305d645edeab40e5 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 27 Jul 2021 18:31:06 +0200 Subject: [PATCH 472/619] allow specifying https:// proxy (#10411) --- changelog.d/10411.feature | 1 + synapse/http/proxyagent.py | 184 +++++++++------- tests/http/test_proxyagent.py | 398 +++++++++++++++++++++++++++++----- 3 files changed, 450 insertions(+), 133 deletions(-) create mode 100644 changelog.d/10411.feature diff --git a/changelog.d/10411.feature b/changelog.d/10411.feature new file mode 100644 index 0000000000..ef0ab84b17 --- /dev/null +++ b/changelog.d/10411.feature @@ -0,0 +1 @@ +Add support for https connections to a proxy server. Contributed by @Bubu and @dklimpel. \ No newline at end of file diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py index f7193e60bd..19e987f118 100644 --- a/synapse/http/proxyagent.py +++ b/synapse/http/proxyagent.py @@ -14,21 +14,32 @@ import base64 import logging import re -from typing import Optional, Tuple -from urllib.request import getproxies_environment, proxy_bypass_environment +from typing import Any, Dict, Optional, Tuple +from urllib.parse import urlparse +from urllib.request import ( # type: ignore[attr-defined] + getproxies_environment, + proxy_bypass_environment, +) import attr from zope.interface import implementer from twisted.internet import defer from twisted.internet.endpoints import HostnameEndpoint, wrapClientTLS +from twisted.internet.interfaces import IReactorCore, IStreamClientEndpoint from twisted.python.failure import Failure -from twisted.web.client import URI, BrowserLikePolicyForHTTPS, _AgentBase +from twisted.web.client import ( + URI, + BrowserLikePolicyForHTTPS, + HTTPConnectionPool, + _AgentBase, +) from twisted.web.error import SchemeNotSupported from twisted.web.http_headers import Headers -from twisted.web.iweb import IAgent, IPolicyForHTTPS +from twisted.web.iweb import IAgent, IBodyProducer, IPolicyForHTTPS from synapse.http.connectproxyclient import HTTPConnectProxyEndpoint +from synapse.types import ISynapseReactor logger = logging.getLogger(__name__) @@ -63,35 +74,38 @@ class ProxyAgent(_AgentBase): reactor might have some blacklisting applied (i.e. for DNS queries), but we need unblocked access to the proxy. - contextFactory (IPolicyForHTTPS): A factory for TLS contexts, to control the + contextFactory: A factory for TLS contexts, to control the verification parameters of OpenSSL. The default is to use a `BrowserLikePolicyForHTTPS`, so unless you have special requirements you can leave this as-is. - connectTimeout (Optional[float]): The amount of time that this Agent will wait + connectTimeout: The amount of time that this Agent will wait for the peer to accept a connection, in seconds. If 'None', HostnameEndpoint's default (30s) will be used. - This is used for connections to both proxies and destination servers. - bindAddress (bytes): The local address for client sockets to bind to. + bindAddress: The local address for client sockets to bind to. - pool (HTTPConnectionPool|None): connection pool to be used. If None, a + pool: connection pool to be used. If None, a non-persistent pool instance will be created. - use_proxy (bool): Whether proxy settings should be discovered and used + use_proxy: Whether proxy settings should be discovered and used from conventional environment variables. + + Raises: + ValueError if use_proxy is set and the environment variables + contain an invalid proxy specification. """ def __init__( self, - reactor, - proxy_reactor=None, + reactor: IReactorCore, + proxy_reactor: Optional[ISynapseReactor] = None, contextFactory: Optional[IPolicyForHTTPS] = None, - connectTimeout=None, - bindAddress=None, - pool=None, - use_proxy=False, + connectTimeout: Optional[float] = None, + bindAddress: Optional[bytes] = None, + pool: Optional[HTTPConnectionPool] = None, + use_proxy: bool = False, ): contextFactory = contextFactory or BrowserLikePolicyForHTTPS() @@ -102,7 +116,7 @@ def __init__( else: self.proxy_reactor = proxy_reactor - self._endpoint_kwargs = {} + self._endpoint_kwargs: Dict[str, Any] = {} if connectTimeout is not None: self._endpoint_kwargs["timeout"] = connectTimeout if bindAddress is not None: @@ -117,16 +131,12 @@ def __init__( https_proxy = proxies["https"].encode() if "https" in proxies else None no_proxy = proxies["no"] if "no" in proxies else None - # Parse credentials from http and https proxy connection string if present - self.http_proxy_creds, http_proxy = parse_username_password(http_proxy) - self.https_proxy_creds, https_proxy = parse_username_password(https_proxy) - - self.http_proxy_endpoint = _http_proxy_endpoint( - http_proxy, self.proxy_reactor, **self._endpoint_kwargs + self.http_proxy_endpoint, self.http_proxy_creds = _http_proxy_endpoint( + http_proxy, self.proxy_reactor, contextFactory, **self._endpoint_kwargs ) - self.https_proxy_endpoint = _http_proxy_endpoint( - https_proxy, self.proxy_reactor, **self._endpoint_kwargs + self.https_proxy_endpoint, self.https_proxy_creds = _http_proxy_endpoint( + https_proxy, self.proxy_reactor, contextFactory, **self._endpoint_kwargs ) self.no_proxy = no_proxy @@ -134,7 +144,13 @@ def __init__( self._policy_for_https = contextFactory self._reactor = reactor - def request(self, method, uri, headers=None, bodyProducer=None): + def request( + self, + method: bytes, + uri: bytes, + headers: Optional[Headers] = None, + bodyProducer: Optional[IBodyProducer] = None, + ) -> defer.Deferred: """ Issue a request to the server indicated by the given uri. @@ -146,16 +162,15 @@ def request(self, method, uri, headers=None, bodyProducer=None): See also: twisted.web.iweb.IAgent.request Args: - method (bytes): The request method to use, such as `GET`, `POST`, etc + method: The request method to use, such as `GET`, `POST`, etc - uri (bytes): The location of the resource to request. + uri: The location of the resource to request. - headers (Headers|None): Extra headers to send with the request + headers: Extra headers to send with the request - bodyProducer (IBodyProducer|None): An object which can generate bytes to - make up the body of this request (for example, the properly encoded - contents of a file for a file upload). Or, None if the request is to - have no body. + bodyProducer: An object which can generate bytes to make up the body of + this request (for example, the properly encoded contents of a file for + a file upload). Or, None if the request is to have no body. Returns: Deferred[IResponse]: completes when the header of the response has @@ -253,70 +268,89 @@ def request(self, method, uri, headers=None, bodyProducer=None): ) -def _http_proxy_endpoint(proxy: Optional[bytes], reactor, **kwargs): +def _http_proxy_endpoint( + proxy: Optional[bytes], + reactor: IReactorCore, + tls_options_factory: IPolicyForHTTPS, + **kwargs, +) -> Tuple[Optional[IStreamClientEndpoint], Optional[ProxyCredentials]]: """Parses an http proxy setting and returns an endpoint for the proxy Args: - proxy: the proxy setting in the form: [:@][:] - Note that compared to other apps, this function currently lacks support - for specifying a protocol schema (i.e. protocol://...). + proxy: the proxy setting in the form: [scheme://][:@][:] + This currently supports http:// and https:// proxies. + A hostname without scheme is assumed to be http. reactor: reactor to be used to connect to the proxy + tls_options_factory: the TLS options to use when connecting through a https proxy + kwargs: other args to be passed to HostnameEndpoint Returns: - interfaces.IStreamClientEndpoint|None: endpoint to use to connect to the proxy, - or None + a tuple of + endpoint to use to connect to the proxy, or None + ProxyCredentials or if no credentials were found, or None + + Raise: + ValueError if proxy has no hostname or unsupported scheme. """ if proxy is None: - return None + return None, None - # Parse the connection string - host, port = parse_host_port(proxy, default_port=1080) - return HostnameEndpoint(reactor, host, port, **kwargs) + # Note: urlsplit/urlparse cannot be used here as that does not work (for Python + # 3.9+) on scheme-less proxies, e.g. host:port. + scheme, host, port, credentials = parse_proxy(proxy) + proxy_endpoint = HostnameEndpoint(reactor, host, port, **kwargs) -def parse_username_password(proxy: bytes) -> Tuple[Optional[ProxyCredentials], bytes]: - """ - Parses the username and password from a proxy declaration e.g - username:password@hostname:port. + if scheme == b"https": + tls_options = tls_options_factory.creatorForNetloc(host, port) + proxy_endpoint = wrapClientTLS(tls_options, proxy_endpoint) - Args: - proxy: The proxy connection string. + return proxy_endpoint, credentials - Returns - An instance of ProxyCredentials and the proxy connection string with any credentials - stripped, i.e u:p@host:port -> host:port. If no credentials were found, the - ProxyCredentials instance is replaced with None. - """ - if proxy and b"@" in proxy: - # We use rsplit here as the password could contain an @ character - credentials, proxy_without_credentials = proxy.rsplit(b"@", 1) - return ProxyCredentials(credentials), proxy_without_credentials - return None, proxy +def parse_proxy( + proxy: bytes, default_scheme: bytes = b"http", default_port: int = 1080 +) -> Tuple[bytes, bytes, int, Optional[ProxyCredentials]]: + """ + Parse a proxy connection string. + Given a HTTP proxy URL, breaks it down into components and checks that it + has a hostname (otherwise it is not useful to us when trying to find a + proxy) and asserts that the URL has a scheme we support. -def parse_host_port(hostport: bytes, default_port: int = None) -> Tuple[bytes, int]: - """ - Parse the hostname and port from a proxy connection byte string. Args: - hostport: The proxy connection string. Must be in the form 'host[:port]'. - default_port: The default port to return if one is not found in `hostport`. + proxy: The proxy connection string. Must be in the form '[scheme://][:@]host[:port]'. + default_scheme: The default scheme to return if one is not found in `proxy`. Defaults to http + default_port: The default port to return if one is not found in `proxy`. Defaults to 1080 Returns: - A tuple containing the hostname and port. Uses `default_port` if one was not found. + A tuple containing the scheme, hostname, port and ProxyCredentials. + If no credentials were found, the ProxyCredentials instance is replaced with None. + + Raise: + ValueError if proxy has no hostname or unsupported scheme. """ - if b":" in hostport: - host, port = hostport.rsplit(b":", 1) - try: - port = int(port) - return host, port - except ValueError: - # the thing after the : wasn't a valid port; presumably this is an - # IPv6 address. - pass + # First check if we have a scheme present + # Note: urlsplit/urlparse cannot be used (for Python # 3.9+) on scheme-less proxies, e.g. host:port. + if b"://" not in proxy: + proxy = b"".join([default_scheme, b"://", proxy]) + + url = urlparse(proxy) + + if not url.hostname: + raise ValueError("Proxy URL did not contain a hostname! Please specify one.") + + if url.scheme not in (b"http", b"https"): + raise ValueError( + f"Unknown proxy scheme {url.scheme!s}; only 'http' and 'https' is supported." + ) + + credentials = None + if url.username and url.password: + credentials = ProxyCredentials(b"".join([url.username, b":", url.password])) - return hostport, default_port + return url.scheme, url.hostname, url.port or default_port, credentials diff --git a/tests/http/test_proxyagent.py b/tests/http/test_proxyagent.py index 437113929a..e5865c161d 100644 --- a/tests/http/test_proxyagent.py +++ b/tests/http/test_proxyagent.py @@ -14,19 +14,22 @@ import base64 import logging import os -from typing import Optional +from typing import Iterable, Optional from unittest.mock import patch import treq from netaddr import IPSet +from parameterized import parameterized from twisted.internet import interfaces # noqa: F401 +from twisted.internet.endpoints import HostnameEndpoint, _WrapperEndpoint +from twisted.internet.interfaces import IProtocol, IProtocolFactory from twisted.internet.protocol import Factory -from twisted.protocols.tls import TLSMemoryBIOFactory +from twisted.protocols.tls import TLSMemoryBIOFactory, TLSMemoryBIOProtocol from twisted.web.http import HTTPChannel from synapse.http.client import BlacklistingReactorWrapper -from synapse.http.proxyagent import ProxyAgent +from synapse.http.proxyagent import ProxyAgent, ProxyCredentials, parse_proxy from tests.http import TestServerTLSConnectionFactory, get_test_https_policy from tests.server import FakeTransport, ThreadedMemoryReactorClock @@ -37,33 +40,208 @@ HTTPFactory = Factory.forProtocol(HTTPChannel) +class ProxyParserTests(TestCase): + """ + Values for test + [ + proxy_string, + expected_scheme, + expected_hostname, + expected_port, + expected_credentials, + ] + """ + + @parameterized.expand( + [ + # host + [b"localhost", b"http", b"localhost", 1080, None], + [b"localhost:9988", b"http", b"localhost", 9988, None], + # host+scheme + [b"https://localhost", b"https", b"localhost", 1080, None], + [b"https://localhost:1234", b"https", b"localhost", 1234, None], + # ipv4 + [b"1.2.3.4", b"http", b"1.2.3.4", 1080, None], + [b"1.2.3.4:9988", b"http", b"1.2.3.4", 9988, None], + # ipv4+scheme + [b"https://1.2.3.4", b"https", b"1.2.3.4", 1080, None], + [b"https://1.2.3.4:9988", b"https", b"1.2.3.4", 9988, None], + # ipv6 - without brackets is broken + # [ + # b"2001:0db8:85a3:0000:0000:8a2e:0370:effe", + # b"http", + # b"2001:0db8:85a3:0000:0000:8a2e:0370:effe", + # 1080, + # None, + # ], + # [ + # b"2001:0db8:85a3:0000:0000:8a2e:0370:1234", + # b"http", + # b"2001:0db8:85a3:0000:0000:8a2e:0370:1234", + # 1080, + # None, + # ], + # [b"::1", b"http", b"::1", 1080, None], + # [b"::ffff:0.0.0.0", b"http", b"::ffff:0.0.0.0", 1080, None], + # ipv6 - with brackets + [ + b"[2001:0db8:85a3:0000:0000:8a2e:0370:effe]", + b"http", + b"2001:0db8:85a3:0000:0000:8a2e:0370:effe", + 1080, + None, + ], + [ + b"[2001:0db8:85a3:0000:0000:8a2e:0370:1234]", + b"http", + b"2001:0db8:85a3:0000:0000:8a2e:0370:1234", + 1080, + None, + ], + [b"[::1]", b"http", b"::1", 1080, None], + [b"[::ffff:0.0.0.0]", b"http", b"::ffff:0.0.0.0", 1080, None], + # ipv6+port + [ + b"[2001:0db8:85a3:0000:0000:8a2e:0370:effe]:9988", + b"http", + b"2001:0db8:85a3:0000:0000:8a2e:0370:effe", + 9988, + None, + ], + [ + b"[2001:0db8:85a3:0000:0000:8a2e:0370:1234]:9988", + b"http", + b"2001:0db8:85a3:0000:0000:8a2e:0370:1234", + 9988, + None, + ], + [b"[::1]:9988", b"http", b"::1", 9988, None], + [b"[::ffff:0.0.0.0]:9988", b"http", b"::ffff:0.0.0.0", 9988, None], + # ipv6+scheme + [ + b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:effe]", + b"https", + b"2001:0db8:85a3:0000:0000:8a2e:0370:effe", + 1080, + None, + ], + [ + b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:1234]", + b"https", + b"2001:0db8:85a3:0000:0000:8a2e:0370:1234", + 1080, + None, + ], + [b"https://[::1]", b"https", b"::1", 1080, None], + [b"https://[::ffff:0.0.0.0]", b"https", b"::ffff:0.0.0.0", 1080, None], + # ipv6+scheme+port + [ + b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:effe]:9988", + b"https", + b"2001:0db8:85a3:0000:0000:8a2e:0370:effe", + 9988, + None, + ], + [ + b"https://[2001:0db8:85a3:0000:0000:8a2e:0370:1234]:9988", + b"https", + b"2001:0db8:85a3:0000:0000:8a2e:0370:1234", + 9988, + None, + ], + [b"https://[::1]:9988", b"https", b"::1", 9988, None], + # with credentials + [ + b"https://user:pass@1.2.3.4:9988", + b"https", + b"1.2.3.4", + 9988, + b"user:pass", + ], + [b"user:pass@1.2.3.4:9988", b"http", b"1.2.3.4", 9988, b"user:pass"], + [ + b"https://user:pass@proxy.local:9988", + b"https", + b"proxy.local", + 9988, + b"user:pass", + ], + [ + b"user:pass@proxy.local:9988", + b"http", + b"proxy.local", + 9988, + b"user:pass", + ], + ] + ) + def test_parse_proxy( + self, + proxy_string: bytes, + expected_scheme: bytes, + expected_hostname: bytes, + expected_port: int, + expected_credentials: Optional[bytes], + ): + """ + Tests that a given proxy URL will be broken into the components. + Args: + proxy_string: The proxy connection string. + expected_scheme: Expected value of proxy scheme. + expected_hostname: Expected value of proxy hostname. + expected_port: Expected value of proxy port. + expected_credentials: Expected value of credentials. + Must be in form ':' or None + """ + proxy_cred = None + if expected_credentials: + proxy_cred = ProxyCredentials(expected_credentials) + self.assertEqual( + ( + expected_scheme, + expected_hostname, + expected_port, + proxy_cred, + ), + parse_proxy(proxy_string), + ) + + class MatrixFederationAgentTests(TestCase): def setUp(self): self.reactor = ThreadedMemoryReactorClock() def _make_connection( - self, client_factory, server_factory, ssl=False, expected_sni=None - ): + self, + client_factory: IProtocolFactory, + server_factory: IProtocolFactory, + ssl: bool = False, + expected_sni: Optional[bytes] = None, + tls_sanlist: Optional[Iterable[bytes]] = None, + ) -> IProtocol: """Builds a test server, and completes the outgoing client connection Args: - client_factory (interfaces.IProtocolFactory): the the factory that the + client_factory: the the factory that the application is trying to use to make the outbound connection. We will invoke it to build the client Protocol - server_factory (interfaces.IProtocolFactory): a factory to build the + server_factory: a factory to build the server-side protocol - ssl (bool): If true, we will expect an ssl connection and wrap + ssl: If true, we will expect an ssl connection and wrap server_factory with a TLSMemoryBIOFactory - expected_sni (bytes|None): the expected SNI value + expected_sni: the expected SNI value + + tls_sanlist: list of SAN entries for the TLS cert presented by the server. + Defaults to [b'DNS:test.com'] Returns: - IProtocol: the server Protocol returned by server_factory + the server Protocol returned by server_factory """ if ssl: - server_factory = _wrap_server_factory_for_tls(server_factory) + server_factory = _wrap_server_factory_for_tls(server_factory, tls_sanlist) server_protocol = server_factory.buildProtocol(None) @@ -98,22 +276,28 @@ def _make_connection( self.assertEqual( server_name, expected_sni, - "Expected SNI %s but got %s" % (expected_sni, server_name), + f"Expected SNI {expected_sni!s} but got {server_name!s}", ) return http_protocol - def _test_request_direct_connection(self, agent, scheme, hostname, path): + def _test_request_direct_connection( + self, + agent: ProxyAgent, + scheme: bytes, + hostname: bytes, + path: bytes, + ): """Runs a test case for a direct connection not going through a proxy. Args: - agent (ProxyAgent): the proxy agent being tested + agent: the proxy agent being tested - scheme (bytes): expected to be either "http" or "https" + scheme: expected to be either "http" or "https" - hostname (bytes): the hostname to connect to in the test + hostname: the hostname to connect to in the test - path (bytes): the path to connect to in the test + path: the path to connect to in the test """ is_https = scheme == b"https" @@ -208,7 +392,7 @@ def test_http_request_via_proxy(self): """ Tests that requests can be made through a proxy. """ - self._do_http_request_via_proxy(auth_credentials=None) + self._do_http_request_via_proxy(ssl=False, auth_credentials=None) @patch.dict( os.environ, @@ -218,12 +402,28 @@ def test_http_request_via_proxy_with_auth(self): """ Tests that authenticated requests can be made through a proxy. """ - self._do_http_request_via_proxy(auth_credentials="bob:pinkponies") + self._do_http_request_via_proxy(ssl=False, auth_credentials=b"bob:pinkponies") + + @patch.dict( + os.environ, {"http_proxy": "https://proxy.com:8888", "no_proxy": "unused.com"} + ) + def test_http_request_via_https_proxy(self): + self._do_http_request_via_proxy(ssl=True, auth_credentials=None) + + @patch.dict( + os.environ, + { + "http_proxy": "https://bob:pinkponies@proxy.com:8888", + "no_proxy": "unused.com", + }, + ) + def test_http_request_via_https_proxy_with_auth(self): + self._do_http_request_via_proxy(ssl=True, auth_credentials=b"bob:pinkponies") @patch.dict(os.environ, {"https_proxy": "proxy.com", "no_proxy": "unused.com"}) def test_https_request_via_proxy(self): """Tests that TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(auth_credentials=None) + self._do_https_request_via_proxy(ssl=False, auth_credentials=None) @patch.dict( os.environ, @@ -231,16 +431,40 @@ def test_https_request_via_proxy(self): ) def test_https_request_via_proxy_with_auth(self): """Tests that authenticated, TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(auth_credentials="bob:pinkponies") + self._do_https_request_via_proxy(ssl=False, auth_credentials=b"bob:pinkponies") + + @patch.dict( + os.environ, {"https_proxy": "https://proxy.com", "no_proxy": "unused.com"} + ) + def test_https_request_via_https_proxy(self): + """Tests that TLS-encrypted requests can be made through a proxy""" + self._do_https_request_via_proxy(ssl=True, auth_credentials=None) + + @patch.dict( + os.environ, + {"https_proxy": "https://bob:pinkponies@proxy.com", "no_proxy": "unused.com"}, + ) + def test_https_request_via_https_proxy_with_auth(self): + """Tests that authenticated, TLS-encrypted requests can be made through a proxy""" + self._do_https_request_via_proxy(ssl=True, auth_credentials=b"bob:pinkponies") def _do_http_request_via_proxy( self, - auth_credentials: Optional[str] = None, + ssl: bool = False, + auth_credentials: Optional[bytes] = None, ): + """Send a http request via an agent and check that it is correctly received at + the proxy. The proxy can use either http or https. + Args: + ssl: True if we expect the request to connect via https to proxy + auth_credentials: credentials to authenticate at proxy """ - Tests that requests can be made through a proxy. - """ - agent = ProxyAgent(self.reactor, use_proxy=True) + if ssl: + agent = ProxyAgent( + self.reactor, use_proxy=True, contextFactory=get_test_https_policy() + ) + else: + agent = ProxyAgent(self.reactor, use_proxy=True) self.reactor.lookups["proxy.com"] = "1.2.3.5" d = agent.request(b"GET", b"http://test.com") @@ -254,7 +478,11 @@ def _do_http_request_via_proxy( # make a test server, and wire up the client http_server = self._make_connection( - client_factory, _get_test_protocol_factory() + client_factory, + _get_test_protocol_factory(), + ssl=ssl, + tls_sanlist=[b"DNS:proxy.com"] if ssl else None, + expected_sni=b"proxy.com" if ssl else None, ) # the FakeTransport is async, so we need to pump the reactor @@ -272,7 +500,7 @@ def _do_http_request_via_proxy( if auth_credentials is not None: # Compute the correct header value for Proxy-Authorization - encoded_credentials = base64.b64encode(b"bob:pinkponies") + encoded_credentials = base64.b64encode(auth_credentials) expected_header_value = b"Basic " + encoded_credentials # Validate the header's value @@ -295,8 +523,15 @@ def _do_http_request_via_proxy( def _do_https_request_via_proxy( self, - auth_credentials: Optional[str] = None, + ssl: bool = False, + auth_credentials: Optional[bytes] = None, ): + """Send a https request via an agent and check that it is correctly received at + the proxy and client. The proxy can use either http or https. + Args: + ssl: True if we expect the request to connect via https to proxy + auth_credentials: credentials to authenticate at proxy + """ agent = ProxyAgent( self.reactor, contextFactory=get_test_https_policy(), @@ -313,18 +548,15 @@ def _do_https_request_via_proxy( self.assertEqual(host, "1.2.3.5") self.assertEqual(port, 1080) - # make a test HTTP server, and wire up the client + # make a test server to act as the proxy, and wire up the client proxy_server = self._make_connection( - client_factory, _get_test_protocol_factory() + client_factory, + _get_test_protocol_factory(), + ssl=ssl, + tls_sanlist=[b"DNS:proxy.com"] if ssl else None, + expected_sni=b"proxy.com" if ssl else None, ) - - # fish the transports back out so that we can do the old switcheroo - s2c_transport = proxy_server.transport - client_protocol = s2c_transport.other - c2s_transport = client_protocol.transport - - # the FakeTransport is async, so we need to pump the reactor - self.reactor.advance(0) + assert isinstance(proxy_server, HTTPChannel) # now there should be a pending CONNECT request self.assertEqual(len(proxy_server.requests), 1) @@ -340,7 +572,7 @@ def _do_https_request_via_proxy( if auth_credentials is not None: # Compute the correct header value for Proxy-Authorization - encoded_credentials = base64.b64encode(b"bob:pinkponies") + encoded_credentials = base64.b64encode(auth_credentials) expected_header_value = b"Basic " + encoded_credentials # Validate the header's value @@ -352,31 +584,49 @@ def _do_https_request_via_proxy( # tell the proxy server not to close the connection proxy_server.persistent = True - # this just stops the http Request trying to do a chunked response - # request.setHeader(b"Content-Length", b"0") request.finish() - # now we can replace the proxy channel with a new, SSL-wrapped HTTP channel - ssl_factory = _wrap_server_factory_for_tls(_get_test_protocol_factory()) - ssl_protocol = ssl_factory.buildProtocol(None) - http_server = ssl_protocol.wrappedProtocol + # now we make another test server to act as the upstream HTTP server. + server_ssl_protocol = _wrap_server_factory_for_tls( + _get_test_protocol_factory() + ).buildProtocol(None) - ssl_protocol.makeConnection( - FakeTransport(client_protocol, self.reactor, ssl_protocol) - ) - c2s_transport.other = ssl_protocol + # Tell the HTTP server to send outgoing traffic back via the proxy's transport. + proxy_server_transport = proxy_server.transport + server_ssl_protocol.makeConnection(proxy_server_transport) + + # ... and replace the protocol on the proxy's transport with the + # TLSMemoryBIOProtocol for the test server, so that incoming traffic + # to the proxy gets sent over to the HTTP(s) server. + # + # This needs a bit of gut-wrenching, which is different depending on whether + # the proxy is using TLS or not. + # + # (an alternative, possibly more elegant, approach would be to use a custom + # Protocol to implement the proxy, which starts out by forwarding to an + # HTTPChannel (to implement the CONNECT command) and can then be switched + # into a mode where it forwards its traffic to another Protocol.) + if ssl: + assert isinstance(proxy_server_transport, TLSMemoryBIOProtocol) + proxy_server_transport.wrappedProtocol = server_ssl_protocol + else: + assert isinstance(proxy_server_transport, FakeTransport) + client_protocol = proxy_server_transport.other + c2s_transport = client_protocol.transport + c2s_transport.other = server_ssl_protocol self.reactor.advance(0) - server_name = ssl_protocol._tlsConnection.get_servername() + server_name = server_ssl_protocol._tlsConnection.get_servername() expected_sni = b"test.com" self.assertEqual( server_name, expected_sni, - "Expected SNI %s but got %s" % (expected_sni, server_name), + f"Expected SNI {expected_sni!s} but got {server_name!s}", ) # now there should be a pending request + http_server = server_ssl_protocol.wrappedProtocol self.assertEqual(len(http_server.requests), 1) request = http_server.requests[0] @@ -510,7 +760,7 @@ def test_https_request_via_uppercase_proxy_with_blacklist(self): self.assertEqual( server_name, expected_sni, - "Expected SNI %s but got %s" % (expected_sni, server_name), + f"Expected SNI {expected_sni!s} but got {server_name!s}", ) # now there should be a pending request @@ -529,16 +779,48 @@ def test_https_request_via_uppercase_proxy_with_blacklist(self): body = self.successResultOf(treq.content(resp)) self.assertEqual(body, b"result") + @patch.dict(os.environ, {"http_proxy": "proxy.com:8888"}) + def test_proxy_with_no_scheme(self): + http_proxy_agent = ProxyAgent(self.reactor, use_proxy=True) + self.assertIsInstance(http_proxy_agent.http_proxy_endpoint, HostnameEndpoint) + self.assertEqual(http_proxy_agent.http_proxy_endpoint._hostStr, "proxy.com") + self.assertEqual(http_proxy_agent.http_proxy_endpoint._port, 8888) + + @patch.dict(os.environ, {"http_proxy": "socks://proxy.com:8888"}) + def test_proxy_with_unsupported_scheme(self): + with self.assertRaises(ValueError): + ProxyAgent(self.reactor, use_proxy=True) + + @patch.dict(os.environ, {"http_proxy": "http://proxy.com:8888"}) + def test_proxy_with_http_scheme(self): + http_proxy_agent = ProxyAgent(self.reactor, use_proxy=True) + self.assertIsInstance(http_proxy_agent.http_proxy_endpoint, HostnameEndpoint) + self.assertEqual(http_proxy_agent.http_proxy_endpoint._hostStr, "proxy.com") + self.assertEqual(http_proxy_agent.http_proxy_endpoint._port, 8888) + + @patch.dict(os.environ, {"http_proxy": "https://proxy.com:8888"}) + def test_proxy_with_https_scheme(self): + https_proxy_agent = ProxyAgent(self.reactor, use_proxy=True) + self.assertIsInstance(https_proxy_agent.http_proxy_endpoint, _WrapperEndpoint) + self.assertEqual( + https_proxy_agent.http_proxy_endpoint._wrappedEndpoint._hostStr, "proxy.com" + ) + self.assertEqual( + https_proxy_agent.http_proxy_endpoint._wrappedEndpoint._port, 8888 + ) + -def _wrap_server_factory_for_tls(factory, sanlist=None): +def _wrap_server_factory_for_tls( + factory: IProtocolFactory, sanlist: Iterable[bytes] = None +) -> IProtocolFactory: """Wrap an existing Protocol Factory with a test TLSMemoryBIOFactory The resultant factory will create a TLS server which presents a certificate signed by our test CA, valid for the domains in `sanlist` Args: - factory (interfaces.IProtocolFactory): protocol factory to wrap - sanlist (iterable[bytes]): list of domains the cert should be valid for + factory: protocol factory to wrap + sanlist: list of domains the cert should be valid for Returns: interfaces.IProtocolFactory @@ -552,7 +834,7 @@ def _wrap_server_factory_for_tls(factory, sanlist=None): ) -def _get_test_protocol_factory(): +def _get_test_protocol_factory() -> IProtocolFactory: """Get a protocol Factory which will build an HTTPChannel Returns: @@ -566,6 +848,6 @@ def _get_test_protocol_factory(): return server_factory -def _log_request(request): +def _log_request(request: str): """Implements Factory.log, which is expected by Request.finish""" - logger.info("Completed request %s", request) + logger.info(f"Completed request {request}") From 5b22d5ee033f2c251bb06d2bd9e0e729df89f90f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 27 Jul 2021 18:01:04 +0100 Subject: [PATCH 473/619] Fix `oldest_pdu_in_federation_staging` (#10455) If the staging area was empty we'd report an age of 51 years, which is not true or helpful. --- changelog.d/10455.bugfix | 1 + synapse/storage/databases/main/event_federation.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10455.bugfix diff --git a/changelog.d/10455.bugfix b/changelog.d/10455.bugfix new file mode 100644 index 0000000000..23c74a3c89 --- /dev/null +++ b/changelog.d/10455.bugfix @@ -0,0 +1 @@ +Fix `synapse_federation_server_oldest_inbound_pdu_in_staging` Prometheus metric to not report a max age of 51 years when the queue is empty. diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index d39368c20e..f4a00b0736 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -1227,12 +1227,15 @@ def _get_stats_for_federation_staging_txn(txn): (count,) = txn.fetchone() txn.execute( - "SELECT coalesce(min(received_ts), 0) FROM federation_inbound_events_staging" + "SELECT min(received_ts) FROM federation_inbound_events_staging" ) (received_ts,) = txn.fetchone() - age = self._clock.time_msec() - received_ts + # If there is nothing in the staging area default it to 0. + age = 0 + if received_ts is not None: + age = self._clock.time_msec() - received_ts return count, age From 8e1febc6a1e909eeb4334d5572956f669ee2d290 Mon Sep 17 00:00:00 2001 From: sri-vidyut Date: Wed, 28 Jul 2021 02:29:42 +0900 Subject: [PATCH 474/619] Support underscores (in addition to hyphens) for charset detection. (#10410) --- changelog.d/10410.bugfix | 1 + synapse/rest/media/v1/preview_url_resource.py | 6 ++++-- tests/test_preview.py | 13 +++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10410.bugfix diff --git a/changelog.d/10410.bugfix b/changelog.d/10410.bugfix new file mode 100644 index 0000000000..65b418fd35 --- /dev/null +++ b/changelog.d/10410.bugfix @@ -0,0 +1 @@ +Improve character set detection in URL previews by supporting underscores (in addition to hyphens). Contributed by @srividyut. diff --git a/synapse/rest/media/v1/preview_url_resource.py b/synapse/rest/media/v1/preview_url_resource.py index 172212ee3a..0f051d4041 100644 --- a/synapse/rest/media/v1/preview_url_resource.py +++ b/synapse/rest/media/v1/preview_url_resource.py @@ -58,9 +58,11 @@ logger = logging.getLogger(__name__) -_charset_match = re.compile(br'<\s*meta[^>]*charset\s*=\s*"?([a-z0-9-]+)"?', flags=re.I) +_charset_match = re.compile( + br'<\s*meta[^>]*charset\s*=\s*"?([a-z0-9_-]+)"?', flags=re.I +) _xml_encoding_match = re.compile( - br'\s*<\s*\?\s*xml[^>]*encoding="([a-z0-9-]+)"', flags=re.I + br'\s*<\s*\?\s*xml[^>]*encoding="([a-z0-9_-]+)"', flags=re.I ) _content_type_match = re.compile(r'.*; *charset="?(.*?)"?(;|$)', flags=re.I) diff --git a/tests/test_preview.py b/tests/test_preview.py index cac3d81ac1..48e792b55b 100644 --- a/tests/test_preview.py +++ b/tests/test_preview.py @@ -325,6 +325,19 @@ def test_meta_charset(self): ) self.assertEqual(encoding, "ascii") + def test_meta_charset_underscores(self): + """A character encoding contains underscore.""" + encoding = get_html_media_encoding( + b""" + + + + + """, + "text/html", + ) + self.assertEqual(encoding, "Shift_JIS") + def test_xml_encoding(self): """A character encoding is found via the meta tag.""" encoding = get_html_media_encoding( From 048968301278aa6ece0a694d7554b7d7d5f7e9ae Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 27 Jul 2021 14:28:23 -0500 Subject: [PATCH 475/619] Document Complement dev usage (#10483) --- CONTRIBUTING.md | 41 +++++++++++++++++++++++++++++++++++++++-- changelog.d/10483.doc | 1 + 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10483.doc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80ef6aa235..e7eef23419 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -155,7 +155,7 @@ source ./env/bin/activate ./scripts-dev/lint.sh path/to/file1.py path/to/file2.py path/to/folder ``` -## Run the unit tests. +## Run the unit tests (Twisted trial). The unit tests run parts of Synapse, including your changes, to see if anything was broken. They are slower than the linters but will typically catch more errors. @@ -186,7 +186,7 @@ SYNAPSE_TEST_LOG_LEVEL=DEBUG trial tests ``` -## Run the integration tests. +## Run the integration tests ([Sytest](https://github.com/matrix-org/sytest)). The integration tests are a more comprehensive suite of tests. They run a full version of Synapse, including your changes, to check if @@ -203,6 +203,43 @@ $ docker run --rm -it -v /path/where/you/have/cloned/the/repository\:/src:ro -v This configuration should generally cover your needs. For more details about other configurations, see [documentation in the SyTest repo](https://github.com/matrix-org/sytest/blob/develop/docker/README.md). +## Run the integration tests ([Complement](https://github.com/matrix-org/complement)). + +[Complement](https://github.com/matrix-org/complement) is a suite of black box tests that can be run on any homeserver implementation. It can also be thought of as end-to-end (e2e) tests. + +It's often nice to develop on Synapse and write Complement tests at the same time. +Here is how to run your local Synapse checkout against your local Complement checkout. + +(checkout [`complement`](https://github.com/matrix-org/complement) alongside your `synapse` checkout) +```sh +COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh +``` + +To run a specific test file, you can pass the test name at the end of the command. The name passed comes from the naming structure in your Complement tests. If you're unsure of the name, you can do a full run and copy it from the test output: + +```sh +COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory +``` + +To run a specific test, you can specify the whole name structure: + +```sh +COMPLEMENT_DIR=../complement ./scripts-dev/complement.sh TestBackfillingHistory/parallel/Backfilled_historical_events_resolve_with_proper_state_in_correct_order +``` + + +### Access database for homeserver after Complement test runs. + +If you're curious what the database looks like after you run some tests, here are some steps to get you going in Synapse: + + 1. In your Complement test comment out `defer deployment.Destroy(t)` and replace with `defer time.Sleep(2 * time.Hour)` to keep the homeserver running after the tests complete + 1. Start the Complement tests + 1. Find the name of the container, `docker ps -f name=complement_` (this will filter for just the Compelement related Docker containers) + 1. Access the container replacing the name with what you found in the previous step: `docker exec -it complement_1_hs_with_application_service.hs1_2 /bin/bash` + 1. Install sqlite (database driver), `apt-get update && apt-get install -y sqlite3` + 1. Then run `sqlite3` and open the database `.open /conf/homeserver.db` (this db path comes from the Synapse homeserver.yaml) + + # 9. Submit your patch. Once you're happy with your patch, it's time to prepare a Pull Request. diff --git a/changelog.d/10483.doc b/changelog.d/10483.doc new file mode 100644 index 0000000000..0f699fafdd --- /dev/null +++ b/changelog.d/10483.doc @@ -0,0 +1 @@ +Document how to use Complement while developing a new Synapse feature. From c3b037795a927ecf58fd3ab099c2a751f05de4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 28 Jul 2021 10:05:11 +0200 Subject: [PATCH 476/619] Support for MSC2285 (hidden read receipts) (#10413) Implementation of matrix-org/matrix-doc#2285 --- changelog.d/10413.feature | 1 + synapse/api/constants.py | 4 + synapse/config/experimental.py | 3 + synapse/handlers/initial_sync.py | 7 +- synapse/handlers/receipts.py | 58 +++- synapse/replication/tcp/client.py | 5 + synapse/rest/client/v2_alpha/read_marker.py | 14 +- synapse/rest/client/v2_alpha/receipts.py | 22 +- synapse/rest/client/versions.py | 2 + tests/handlers/test_receipts.py | 294 ++++++++++++++++++++ tests/rest/client/v2_alpha/test_sync.py | 97 ++++++- 11 files changed, 495 insertions(+), 12 deletions(-) create mode 100644 changelog.d/10413.feature create mode 100644 tests/handlers/test_receipts.py diff --git a/changelog.d/10413.feature b/changelog.d/10413.feature new file mode 100644 index 0000000000..3964db7e0e --- /dev/null +++ b/changelog.d/10413.feature @@ -0,0 +1 @@ +Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). Contributed by @SimonBrandner. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 4caafc0ac9..56e7233b9e 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -224,3 +224,7 @@ class HistoryVisibility: JOINED = "joined" SHARED = "shared" WORLD_READABLE = "world_readable" + + +class ReadReceiptEventFields: + MSC2285_HIDDEN = "org.matrix.msc2285.hidden" diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 040c4504d8..4c60ee8c28 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -33,5 +33,8 @@ def read_config(self, config: JsonDict, **kwargs): # MSC2716 (backfill existing history) self.msc2716_enabled: bool = experimental.get("msc2716_enabled", False) + # MSC2285 (hidden read receipts) + self.msc2285_enabled: bool = experimental.get("msc2285_enabled", False) + # MSC3244 (room version capabilities) self.msc3244_enabled: bool = experimental.get("msc3244_enabled", False) diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py index 5d49640760..e1c544a3c9 100644 --- a/synapse/handlers/initial_sync.py +++ b/synapse/handlers/initial_sync.py @@ -21,6 +21,7 @@ from synapse.api.errors import SynapseError from synapse.events.validator import EventValidator from synapse.handlers.presence import format_user_presence_state +from synapse.handlers.receipts import ReceiptEventSource from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.storage.roommember import RoomsForUser from synapse.streams.config import PaginationConfig @@ -134,6 +135,8 @@ async def _snapshot_all_rooms( joined_rooms, to_key=int(now_token.receipt_key), ) + if self.hs.config.experimental.msc2285_enabled: + receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id) tags_by_room = await self.store.get_tags_for_user(user_id) @@ -430,7 +433,9 @@ async def get_receipts(): room_id, to_key=now_token.receipt_key ) if not receipts: - receipts = [] + return [] + if self.hs.config.experimental.msc2285_enabled: + receipts = ReceiptEventSource.filter_out_hidden(receipts, user_id) return receipts presence, receipts, (messages, token) = await make_deferred_yieldable( diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 283483fc2c..b9085bbccb 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -14,9 +14,10 @@ import logging from typing import TYPE_CHECKING, List, Optional, Tuple +from synapse.api.constants import ReadReceiptEventFields from synapse.appservice import ApplicationService from synapse.handlers._base import BaseHandler -from synapse.types import JsonDict, ReadReceipt, get_domain_from_id +from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id if TYPE_CHECKING: from synapse.server import HomeServer @@ -137,7 +138,7 @@ async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool: return True async def received_client_receipt( - self, room_id: str, receipt_type: str, user_id: str, event_id: str + self, room_id: str, receipt_type: str, user_id: str, event_id: str, hidden: bool ) -> None: """Called when a client tells us a local user has read up to the given event_id in the room. @@ -147,23 +148,67 @@ async def received_client_receipt( receipt_type=receipt_type, user_id=user_id, event_ids=[event_id], - data={"ts": int(self.clock.time_msec())}, + data={"ts": int(self.clock.time_msec()), "hidden": hidden}, ) is_new = await self._handle_new_receipts([receipt]) if not is_new: return - if self.federation_sender: + if self.federation_sender and not ( + self.hs.config.experimental.msc2285_enabled and hidden + ): await self.federation_sender.send_read_receipt(receipt) class ReceiptEventSource: def __init__(self, hs: "HomeServer"): self.store = hs.get_datastore() + self.config = hs.config + + @staticmethod + def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: + visible_events = [] + + # filter out hidden receipts the user shouldn't see + for event in events: + content = event.get("content", {}) + new_event = event.copy() + new_event["content"] = {} + + for event_id in content.keys(): + event_content = content.get(event_id, {}) + m_read = event_content.get("m.read", {}) + + # If m_read is missing copy over the original event_content as there is nothing to process here + if not m_read: + new_event["content"][event_id] = event_content.copy() + continue + + new_users = {} + for rr_user_id, user_rr in m_read.items(): + hidden = user_rr.get("hidden", None) + if hidden is not True or rr_user_id == user_id: + new_users[rr_user_id] = user_rr.copy() + # If hidden has a value replace hidden with the correct prefixed key + if hidden is not None: + new_users[rr_user_id].pop("hidden") + new_users[rr_user_id][ + ReadReceiptEventFields.MSC2285_HIDDEN + ] = hidden + + # Set new users unless empty + if len(new_users.keys()) > 0: + new_event["content"][event_id] = {"m.read": new_users} + + # Append new_event to visible_events unless empty + if len(new_event["content"].keys()) > 0: + visible_events.append(new_event) + + return visible_events async def get_new_events( - self, from_key: int, room_ids: List[str], **kwargs + self, from_key: int, room_ids: List[str], user: UserID, **kwargs ) -> Tuple[List[JsonDict], int]: from_key = int(from_key) to_key = self.get_current_key() @@ -175,6 +220,9 @@ async def get_new_events( room_ids, from_key=from_key, to_key=to_key ) + if self.config.experimental.msc2285_enabled: + events = ReceiptEventSource.filter_out_hidden(events, user.to_string()) + return (events, to_key) async def get_new_events_as( diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 9d4859798b..e09b857814 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -393,6 +393,11 @@ async def _on_new_receipts(self, rows): # we only want to send on receipts for our own users if not self._is_mine_id(receipt.user_id): continue + if ( + receipt.data.get("hidden", False) + and self._hs.config.experimental.msc2285_enabled + ): + continue receipt_info = ReadReceipt( receipt.room_id, receipt.receipt_type, diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/v2_alpha/read_marker.py index 5988fa47e5..027f8b81fa 100644 --- a/synapse/rest/client/v2_alpha/read_marker.py +++ b/synapse/rest/client/v2_alpha/read_marker.py @@ -14,6 +14,8 @@ import logging +from synapse.api.constants import ReadReceiptEventFields +from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request from ._base import client_patterns @@ -37,14 +39,24 @@ async def on_POST(self, request, room_id): await self.presence_handler.bump_presence_active_time(requester.user) body = parse_json_object_from_request(request) - read_event_id = body.get("m.read", None) + hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) + + if not isinstance(hidden, bool): + raise SynapseError( + 400, + "Param %s must be a boolean, if given" + % ReadReceiptEventFields.MSC2285_HIDDEN, + Codes.BAD_JSON, + ) + if read_event_id: await self.receipts_handler.received_client_receipt( room_id, "m.read", user_id=requester.user.to_string(), event_id=read_event_id, + hidden=hidden, ) read_marker_event_id = body.get("m.fully_read", None) diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 8cf4aebdbe..4b98979b47 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -14,8 +14,9 @@ import logging -from synapse.api.errors import SynapseError -from synapse.http.servlet import RestServlet +from synapse.api.constants import ReadReceiptEventFields +from synapse.api.errors import Codes, SynapseError +from synapse.http.servlet import RestServlet, parse_json_object_from_request from ._base import client_patterns @@ -42,10 +43,25 @@ async def on_POST(self, request, room_id, receipt_type, event_id): if receipt_type != "m.read": raise SynapseError(400, "Receipt type must be 'm.read'") + body = parse_json_object_from_request(request) + hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) + + if not isinstance(hidden, bool): + raise SynapseError( + 400, + "Param %s must be a boolean, if given" + % ReadReceiptEventFields.MSC2285_HIDDEN, + Codes.BAD_JSON, + ) + await self.presence_handler.bump_presence_active_time(requester.user) await self.receipts_handler.received_client_receipt( - room_id, receipt_type, user_id=requester.user.to_string(), event_id=event_id + room_id, + receipt_type, + user_id=requester.user.to_string(), + event_id=event_id, + hidden=hidden, ) return 200, {} diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 4582c274c7..fa2e4e9cba 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -82,6 +82,8 @@ def on_GET(self, request): "io.element.e2ee_forced.trusted_private": self.e2ee_forced_trusted_private, # Supports the busy presence state described in MSC3026. "org.matrix.msc3026.busy_presence": self.config.experimental.msc3026_enabled, + # Supports receiving hidden read receipts as per MSC2285 + "org.matrix.msc2285": self.config.experimental.msc2285_enabled, }, }, ) diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py new file mode 100644 index 0000000000..93a9a084b2 --- /dev/null +++ b/tests/handlers/test_receipts.py @@ -0,0 +1,294 @@ +# Copyright 2021 Šimon Brandner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import List + +from synapse.api.constants import ReadReceiptEventFields +from synapse.types import JsonDict + +from tests import unittest + + +class ReceiptsTestCase(unittest.HomeserverTestCase): + def prepare(self, reactor, clock, hs): + self.event_source = hs.get_event_sources().sources["receipt"] + + # In the first param of _test_filters_hidden we use "hidden" instead of + # ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking + # the data from the database which doesn't use the prefix + + def test_filters_out_hidden_receipt(self): + self._test_filters_hidden( + [ + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [], + ) + + def test_does_not_filter_out_our_hidden_receipt(self): + self._test_filters_hidden( + [ + { + "content": { + "$1435641916hfgh4394fHBLK:matrix.org": { + "m.read": { + "@me:server.org": { + "ts": 1436451550453, + "hidden": True, + }, + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$1435641916hfgh4394fHBLK:matrix.org": { + "m.read": { + "@me:server.org": { + "ts": 1436451550453, + ReadReceiptEventFields.MSC2285_HIDDEN: True, + }, + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + def test_filters_out_hidden_receipt_and_ignores_rest(self): + self._test_filters_hidden( + [ + { + "content": { + "$1dgdgrd5641916114394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + }, + "@user:jki.re": { + "ts": 1436451550453, + }, + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$1dgdgrd5641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + def test_filters_out_event_with_only_hidden_receipts_and_ignores_the_rest(self): + self._test_filters_hidden( + [ + { + "content": { + "$14356419edgd14394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + }, + } + }, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + def test_handles_missing_content_of_m_read(self): + self._test_filters_hidden( + [ + { + "content": { + "$14356419ggffg114394fHBLK:matrix.org": {"m.read": {}}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$14356419ggffg114394fHBLK:matrix.org": {"m.read": {}}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + def test_handles_empty_event(self): + self._test_filters_hidden( + [ + { + "content": { + "$143564gdfg6114394fHBLK:matrix.org": {}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + [ + { + "content": { + "$143564gdfg6114394fHBLK:matrix.org": {}, + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + def test_filters_out_receipt_event_with_only_hidden_receipt_and_ignores_rest(self): + self._test_filters_hidden( + [ + { + "content": { + "$14356419edgd14394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": { + "ts": 1436451550453, + "hidden": True, + }, + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + }, + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + }, + ], + [ + { + "content": { + "$1435641916114394fHBLK:matrix.org": { + "m.read": { + "@user:jki.re": { + "ts": 1436451550453, + } + } + } + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + } + ], + ) + + def _test_filters_hidden( + self, events: List[JsonDict], expected_output: List[JsonDict] + ): + """Tests that the _filter_out_hidden returns the expected output""" + filtered_events = self.event_source.filter_out_hidden(events, "@me:server.org") + self.assertEquals(filtered_events, expected_output) diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index cdca3a3e23..f6ae9ae181 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -15,9 +15,14 @@ import json import synapse.rest.admin -from synapse.api.constants import EventContentFields, EventTypes, RelationTypes +from synapse.api.constants import ( + EventContentFields, + EventTypes, + ReadReceiptEventFields, + RelationTypes, +) from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import knock, read_marker, sync +from synapse.rest.client.v2_alpha import knock, read_marker, receipts, sync from tests import unittest from tests.federation.transport.test_knocking import ( @@ -368,6 +373,76 @@ def test_knock_room_state(self): ) +class ReadReceiptsTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + receipts.register_servlets, + room.register_servlets, + sync.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.url = "/sync?since=%s" + self.next_batch = "s0" + + # Register the first user + self.user_id = self.register_user("kermit", "monkey") + self.tok = self.login("kermit", "monkey") + + # Create the room + self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok) + + # Register the second user + self.user2 = self.register_user("kermit2", "monkey") + self.tok2 = self.login("kermit2", "monkey") + + # Join the second user + self.helper.join(room=self.room_id, user=self.user2, tok=self.tok2) + + @override_config({"experimental_features": {"msc2285_enabled": True}}) + def test_hidden_read_receipts(self): + # Send a message as the first user + res = self.helper.send(self.room_id, body="hello", tok=self.tok) + + # Send a read receipt to tell the server the first user's message was read + body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8") + channel = self.make_request( + "POST", + "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]), + body, + access_token=self.tok2, + ) + self.assertEqual(channel.code, 200) + + # Test that the first user can't see the other user's hidden read receipt + self.assertEqual(self._get_read_receipt(), None) + + def _get_read_receipt(self): + """Syncs and returns the read receipt.""" + + # Checks if event is a read receipt + def is_read_receipt(event): + return event["type"] == "m.receipt" + + # Sync + channel = self.make_request( + "GET", + self.url % self.next_batch, + access_token=self.tok, + ) + self.assertEqual(channel.code, 200) + + # Store the next batch for the next request. + self.next_batch = channel.json_body["next_batch"] + + # Return the read receipt + ephemeral_events = channel.json_body["rooms"]["join"][self.room_id][ + "ephemeral" + ]["events"] + return next(filter(is_read_receipt, ephemeral_events), None) + + class UnreadMessagesTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets, @@ -375,6 +450,7 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase): read_marker.register_servlets, room.register_servlets, sync.register_servlets, + receipts.register_servlets, ] def prepare(self, reactor, clock, hs): @@ -448,6 +524,23 @@ def test_unread_counts(self): # Check that the unread counter is back to 0. self._check_unread_count(0) + # Check that hidden read receipts don't break unread counts + res = self.helper.send(self.room_id, "hello", tok=self.tok2) + self._check_unread_count(1) + + # Send a read receipt to tell the server we've read the latest event. + body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8") + channel = self.make_request( + "POST", + "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]), + body, + access_token=self.tok, + ) + self.assertEqual(channel.code, 200, channel.json_body) + + # Check that the unread counter is back to 0. + self._check_unread_count(0) + # Check that room name changes increase the unread counter. self.helper.send_state( self.room_id, From 752fe0cd9869d25bb3e02a539aba67e98afea514 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 28 Jul 2021 07:03:01 -0400 Subject: [PATCH 477/619] Restricted rooms (MSC3083) should not have their allow key redacted. (#10489) --- changelog.d/10489.feature | 1 + synapse/events/utils.py | 2 ++ tests/events/test_utils.py | 43 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 changelog.d/10489.feature diff --git a/changelog.d/10489.feature b/changelog.d/10489.feature new file mode 100644 index 0000000000..df8bb51167 --- /dev/null +++ b/changelog.d/10489.feature @@ -0,0 +1 @@ +Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. diff --git a/synapse/events/utils.py b/synapse/events/utils.py index ec96999e4e..f4da9e0923 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -109,6 +109,8 @@ def add_fields(*fields): add_fields("creator") elif event_type == EventTypes.JoinRules: add_fields("join_rule") + if room_version.msc3083_join_rules: + add_fields("allow") elif event_type == EventTypes.PowerLevels: add_fields( "users", diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index 9274ce4c39..e2a5fc018c 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -301,6 +301,49 @@ def test_redacts(self): room_version=RoomVersions.MSC2176, ) + def test_join_rules(self): + """Join rules events have changed behavior starting with MSC3083.""" + self.run_test( + { + "type": "m.room.join_rules", + "event_id": "$test:domain", + "content": { + "join_rule": "invite", + "allow": [], + "other_key": "stripped", + }, + }, + { + "type": "m.room.join_rules", + "event_id": "$test:domain", + "content": {"join_rule": "invite"}, + "signatures": {}, + "unsigned": {}, + }, + ) + + # After MSC3083, alias events have no special behavior. + self.run_test( + { + "type": "m.room.join_rules", + "content": { + "join_rule": "invite", + "allow": [], + "other_key": "stripped", + }, + }, + { + "type": "m.room.join_rules", + "content": { + "join_rule": "invite", + "allow": [], + }, + "signatures": {}, + "unsigned": {}, + }, + room_version=RoomVersions.MSC3083, + ) + class SerializeEventTestCase(unittest.TestCase): def serialize(self, ev, fields): From 9643dfde6ac4568682c1cc187fef206debfedbd7 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 28 Jul 2021 12:25:12 +0100 Subject: [PATCH 478/619] improve typing annotations in CachedCall (#10450) tighten up some of the typing in CachedCall, which is going to be needed when Twisted 21.7 brings better typing on Deferred. --- changelog.d/10450.misc | 1 + synapse/util/caches/cached_call.py | 27 +++++++++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10450.misc diff --git a/changelog.d/10450.misc b/changelog.d/10450.misc new file mode 100644 index 0000000000..aa646f0841 --- /dev/null +++ b/changelog.d/10450.misc @@ -0,0 +1 @@ + Update type annotations to work with forthcoming Twisted 21.7.0 release. diff --git a/synapse/util/caches/cached_call.py b/synapse/util/caches/cached_call.py index 891bee0b33..e58dd91eda 100644 --- a/synapse/util/caches/cached_call.py +++ b/synapse/util/caches/cached_call.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import enum from typing import Awaitable, Callable, Generic, Optional, TypeVar, Union from twisted.internet.defer import Deferred @@ -22,6 +22,10 @@ TV = TypeVar("TV") +class _Sentinel(enum.Enum): + sentinel = object() + + class CachedCall(Generic[TV]): """A wrapper for asynchronous calls whose results should be shared @@ -65,7 +69,7 @@ def __init__(self, f: Callable[[], Awaitable[TV]]): """ self._callable: Optional[Callable[[], Awaitable[TV]]] = f self._deferred: Optional[Deferred] = None - self._result: Union[None, Failure, TV] = None + self._result: Union[_Sentinel, TV, Failure] = _Sentinel.sentinel async def get(self) -> TV: """Kick off the call if necessary, and return the result""" @@ -78,8 +82,9 @@ async def get(self) -> TV: self._callable = None # once the deferred completes, store the result. We cannot simply leave the - # result in the deferred, since if it's a Failure, GCing the deferred - # would then log a critical error about unhandled Failures. + # result in the deferred, since `awaiting` a deferred destroys its result. + # (Also, if it's a Failure, GCing the deferred would log a critical error + # about unhandled Failures) def got_result(r): self._result = r @@ -92,13 +97,15 @@ def got_result(r): # and any eventual exception may not be reported. # we can now await the deferred, and once it completes, return the result. - await make_deferred_yieldable(self._deferred) + if isinstance(self._result, _Sentinel): + await make_deferred_yieldable(self._deferred) + assert not isinstance(self._result, _Sentinel) + + if isinstance(self._result, Failure): + self._result.raiseException() + raise AssertionError("unexpected return from Failure.raiseException") - # I *think* this is the easiest way to correctly raise a Failure without having - # to gut-wrench into the implementation of Deferred. - d = Deferred() - d.callback(self._result) - return await d + return self._result class RetryOnExceptionCachedCall(Generic[TV]): From d9cb658c78bdb676762488d08ba44998307c781a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 28 Jul 2021 13:04:11 +0100 Subject: [PATCH 479/619] Fix up type hints for Twisted 21.7 (#10490) Mostly this involves decorating a few Deferred declarations with extra type hints. We wrap the types in quotes to avoid runtime errors when running against older versions of Twisted that don't have generics on Deferred. --- changelog.d/10490.misc | 1 + synapse/http/client.py | 4 ++-- synapse/replication/tcp/client.py | 2 +- synapse/util/async_helpers.py | 16 ++++++++-------- synapse/util/caches/deferred_cache.py | 15 ++++++++++++--- synapse/util/caches/descriptors.py | 2 +- 6 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 changelog.d/10490.misc diff --git a/changelog.d/10490.misc b/changelog.d/10490.misc new file mode 100644 index 0000000000..630c31adae --- /dev/null +++ b/changelog.d/10490.misc @@ -0,0 +1 @@ +Fix up type annotations to work with Twisted 21.7. diff --git a/synapse/http/client.py b/synapse/http/client.py index 2ac76b15c2..c2ea51ee16 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -847,7 +847,7 @@ def connectionLost(self, reason: Failure = connectionDone) -> None: def read_body_with_max_size( response: IResponse, stream: ByteWriteable, max_size: Optional[int] -) -> defer.Deferred: +) -> "defer.Deferred[int]": """ Read a HTTP response body to a file-object. Optionally enforcing a maximum file size. @@ -862,7 +862,7 @@ def read_body_with_max_size( Returns: A Deferred which resolves to the length of the read body. """ - d = defer.Deferred() + d: "defer.Deferred[int]" = defer.Deferred() # If the Content-Length header gives a size larger than the maximum allowed # size, do not bother downloading the body. diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index e09b857814..3fd2811713 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -285,7 +285,7 @@ async def wait_for_stream_position( # Create a new deferred that times out after N seconds, as we don't want # to wedge here forever. - deferred = Deferred() + deferred: "Deferred[None]" = Deferred() deferred = timeout_deferred( deferred, _WAIT_FOR_REPLICATION_TIMEOUT_SECONDS, self._reactor ) diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 014db1355b..912cf85f89 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -49,6 +49,8 @@ logger = logging.getLogger(__name__) +_T = TypeVar("_T") + class ObservableDeferred: """Wraps a deferred object so that we can add observer deferreds. These @@ -121,7 +123,7 @@ def observe(self) -> defer.Deferred: effect the underlying deferred. """ if not self._result: - d = defer.Deferred() + d: "defer.Deferred[Any]" = defer.Deferred() def remove(r): self._observers.discard(d) @@ -415,7 +417,7 @@ def __init__(self): self.key_to_current_writer: Dict[str, defer.Deferred] = {} async def read(self, key: str) -> ContextManager: - new_defer = defer.Deferred() + new_defer: "defer.Deferred[None]" = defer.Deferred() curr_readers = self.key_to_current_readers.setdefault(key, set()) curr_writer = self.key_to_current_writer.get(key, None) @@ -438,7 +440,7 @@ def _ctx_manager(): return _ctx_manager() async def write(self, key: str) -> ContextManager: - new_defer = defer.Deferred() + new_defer: "defer.Deferred[None]" = defer.Deferred() curr_readers = self.key_to_current_readers.get(key, set()) curr_writer = self.key_to_current_writer.get(key, None) @@ -471,10 +473,8 @@ def _ctx_manager(): def timeout_deferred( - deferred: defer.Deferred, - timeout: float, - reactor: IReactorTime, -) -> defer.Deferred: + deferred: "defer.Deferred[_T]", timeout: float, reactor: IReactorTime +) -> "defer.Deferred[_T]": """The in built twisted `Deferred.addTimeout` fails to time out deferreds that have a canceller that throws exceptions. This method creates a new deferred that wraps and times out the given deferred, correctly handling @@ -497,7 +497,7 @@ def timeout_deferred( Returns: A new Deferred, which will errback with defer.TimeoutError on timeout. """ - new_d = defer.Deferred() + new_d: "defer.Deferred[_T]" = defer.Deferred() timed_out = [False] diff --git a/synapse/util/caches/deferred_cache.py b/synapse/util/caches/deferred_cache.py index 8c6fafc677..b6456392cd 100644 --- a/synapse/util/caches/deferred_cache.py +++ b/synapse/util/caches/deferred_cache.py @@ -16,7 +16,16 @@ import enum import threading -from typing import Callable, Generic, Iterable, MutableMapping, Optional, TypeVar, Union +from typing import ( + Callable, + Generic, + Iterable, + MutableMapping, + Optional, + TypeVar, + Union, + cast, +) from prometheus_client import Gauge @@ -166,7 +175,7 @@ def get_immediate( def set( self, key: KT, - value: defer.Deferred, + value: "defer.Deferred[VT]", callback: Optional[Callable[[], None]] = None, ) -> defer.Deferred: """Adds a new entry to the cache (or updates an existing one). @@ -214,7 +223,7 @@ def set( if value.called: result = value.result if not isinstance(result, failure.Failure): - self.cache.set(key, result, callbacks) + self.cache.set(key, cast(VT, result), callbacks) return value # otherwise, we'll add an entry to the _pending_deferred_cache for now, diff --git a/synapse/util/caches/descriptors.py b/synapse/util/caches/descriptors.py index 1e8e6b1d01..1ca31e41ac 100644 --- a/synapse/util/caches/descriptors.py +++ b/synapse/util/caches/descriptors.py @@ -413,7 +413,7 @@ def arg_to_cache_key(arg): # relevant result for that key. deferreds_map = {} for arg in missing: - deferred = defer.Deferred() + deferred: "defer.Deferred[Any]" = defer.Deferred() deferreds_map[arg] = deferred key = arg_to_cache_key(arg) cache.set(key, deferred, callback=invalidate_callback) From 5146e198809c736d6106ff868caee0380d4f28ac Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 28 Jul 2021 13:31:18 +0100 Subject: [PATCH 480/619] 1.39.0rc3 --- CHANGES.md | 18 ++++++++++++++++++ changelog.d/10461.misc | 1 - changelog.d/10465.misc | 1 - changelog.d/10477.bugfix | 1 - changelog.d/10485.bugfix | 1 - changelog.d/10486.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 8 files changed, 25 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/10461.misc delete mode 100644 changelog.d/10465.misc delete mode 100644 changelog.d/10477.bugfix delete mode 100644 changelog.d/10485.bugfix delete mode 100644 changelog.d/10486.bugfix diff --git a/CHANGES.md b/CHANGES.md index 13d3654095..975394b476 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,21 @@ +Synapse 1.39.0rc3 (2021-07-28) +============================== + +Bugfixes +-------- + +- Fix bug introduced in Synapse 1.38 which caused an exception at startup when SAML authentication was enabled. ([\#10477](https://github.com/matrix-org/synapse/issues/10477)) +- Fix a long-standing bug where Synapse would not inform clients that a device had exhausted its one-time-key pool, potentially causing problems decrypting events. ([\#10485](https://github.com/matrix-org/synapse/issues/10485)) +- Fix reporting old R30 stats as R30v2 stats. ([\#10486](https://github.com/matrix-org/synapse/issues/10486)) + + +Internal Changes +---------------- + +- Fix an error which prevented the Github Actions workflow to build the docker images from running. ([\#10461](https://github.com/matrix-org/synapse/issues/10461)) +- Fix release script to correctly version debian changelog when doing RCs. ([\#10465](https://github.com/matrix-org/synapse/issues/10465)) + + Synapse 1.39.0rc2 (2021-07-22) ============================== diff --git a/changelog.d/10461.misc b/changelog.d/10461.misc deleted file mode 100644 index 5035e26825..0000000000 --- a/changelog.d/10461.misc +++ /dev/null @@ -1 +0,0 @@ -Fix an error which prevented the Github Actions workflow to build the docker images from running. diff --git a/changelog.d/10465.misc b/changelog.d/10465.misc deleted file mode 100644 index 4de6201dfc..0000000000 --- a/changelog.d/10465.misc +++ /dev/null @@ -1 +0,0 @@ -Fix release script to correctly version debian changelog when doing RCs. diff --git a/changelog.d/10477.bugfix b/changelog.d/10477.bugfix deleted file mode 100644 index bcc92de434..0000000000 --- a/changelog.d/10477.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug introduced in Synapse 1.38 which caused an exception at startup when SAML authentication was enabled. diff --git a/changelog.d/10485.bugfix b/changelog.d/10485.bugfix deleted file mode 100644 index 9b44006dc0..0000000000 --- a/changelog.d/10485.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where Synapse would not inform clients that a device had exhausted its one-time-key pool, potentially causing problems decrypting events. diff --git a/changelog.d/10486.bugfix b/changelog.d/10486.bugfix deleted file mode 100644 index 7c65c16e96..0000000000 --- a/changelog.d/10486.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix reporting old R30 stats as R30v2 stats. diff --git a/debian/changelog b/debian/changelog index 2062c6caef..4944e55714 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.39.0~rc3) stable; urgency=medium + + * New synapse release 1.39.0~rc3. + + -- Synapse Packaging team Wed, 28 Jul 2021 13:30:58 +0100 + matrix-synapse-py3 (1.38.1) stable; urgency=medium * New synapse release 1.38.1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 01d6bf17f0..c9a445c8fe 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.39.0rc2" +__version__ = "1.39.0rc3" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 2254e6790f4a89c3d8450912bd02fd48d671c92e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 28 Jul 2021 13:34:39 +0100 Subject: [PATCH 481/619] Fixup changelog --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 975394b476..b512d9ff3f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,9 +4,9 @@ Synapse 1.39.0rc3 (2021-07-28) Bugfixes -------- -- Fix bug introduced in Synapse 1.38 which caused an exception at startup when SAML authentication was enabled. ([\#10477](https://github.com/matrix-org/synapse/issues/10477)) +- Fix a bug introduced in Synapse 1.38 which caused an exception at startup when SAML authentication was enabled. ([\#10477](https://github.com/matrix-org/synapse/issues/10477)) - Fix a long-standing bug where Synapse would not inform clients that a device had exhausted its one-time-key pool, potentially causing problems decrypting events. ([\#10485](https://github.com/matrix-org/synapse/issues/10485)) -- Fix reporting old R30 stats as R30v2 stats. ([\#10486](https://github.com/matrix-org/synapse/issues/10486)) +- Fix reporting old R30 stats as R30v2 stats. Introduced in v1.39.0rc1. ([\#10486](https://github.com/matrix-org/synapse/issues/10486)) Internal Changes From d0b294ad974c05621426369a00be6bf05c4af997 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 28 Jul 2021 10:46:37 -0500 Subject: [PATCH 482/619] Make historical events discoverable from backfill for servers without any scrollback history (MSC2716) (#10245) * Make historical messages available to federated servers Part of MSC2716: https://github.com/matrix-org/matrix-doc/pull/2716 Follow-up to https://github.com/matrix-org/synapse/pull/9247 * Debug message not available on federation * Add base starting insertion point when no chunk ID is provided * Fix messages from multiple senders in historical chunk Follow-up to https://github.com/matrix-org/synapse/pull/9247 Part of MSC2716: https://github.com/matrix-org/matrix-doc/pull/2716 --- Previously, Synapse would throw a 403, `Cannot force another user to join.`, because we were trying to use `?user_id` from a single virtual user which did not match with messages from other users in the chunk. * Remove debug lines * Messing with selecting insertion event extremeties * Move db schema change to new version * Add more better comments * Make a fake requester with just what we need See https://github.com/matrix-org/synapse/pull/10276#discussion_r660999080 * Store insertion events in table * Make base insertion event float off on its own See https://github.com/matrix-org/synapse/pull/10250#issuecomment-875711889 Conflicts: synapse/rest/client/v1/room.py * Validate that the app service can actually control the given user See https://github.com/matrix-org/synapse/pull/10276#issuecomment-876316455 Conflicts: synapse/rest/client/v1/room.py * Add some better comments on what we're trying to check for * Continue debugging * Share validation logic * Add inserted historical messages to /backfill response * Remove debug sql queries * Some marker event implemntation trials * Clean up PR * Rename insertion_event_id to just event_id * Add some better sql comments * More accurate description * Add changelog * Make it clear what MSC the change is part of * Add more detail on which insertion event came through * Address review and improve sql queries * Only use event_id as unique constraint * Fix test case where insertion event is already in the normal DAG * Remove debug changes * Switch to chunk events so we can auth via power_levels Previously, we were using `content.chunk_id` to connect one chunk to another. But these events can be from any `sender` and we can't tell who should be able to send historical events. We know we only want the application service to do it but these events have the sender of a real historical message, not the application service user ID as the sender. Other federated homeservers also have no indicator which senders are an application service on the originating homeserver. So we want to auth all of the MSC2716 events via power_levels and have them be sent by the application service with proper PL levels in the room. * Switch to chunk events for federation * Add unstable room version to support new historical PL * Fix federated events being rejected for no state_groups Add fix from https://github.com/matrix-org/synapse/pull/10439 until it merges. * Only connect base insertion event to prev_event_ids Per discussion with @erikjohnston, https://matrix.to/#/!UytJQHLQYfvYWsGrGY:jki.re/$12bTUiObDFdHLAYtT7E-BvYRp3k_xv8w0dUQHibasJk?via=jki.re&via=matrix.org * Make it possible to get the room_version with txn * Allow but ignore historical events in unsupported room version See https://github.com/matrix-org/synapse/pull/10245#discussion_r675592489 We can't reject historical events on unsupported room versions because homeservers without knowledge of MSC2716 or the new room version don't reject historical events either. Since we can't rely on the auth check here to stop historical events on unsupported room versions, I've added some additional checks in the processing/persisting code (`synapse/storage/databases/main/events.py` -> `_handle_insertion_event` and `_handle_chunk_event`). I've had to do some refactoring so there is method to fetch the room version by `txn`. * Move to unique index syntax See https://github.com/matrix-org/synapse/pull/10245#discussion_r675638509 * High-level document how the insertion->chunk lookup works * Remove create_event fallback for room_versions See https://github.com/matrix-org/synapse/pull/10245/files#r677641879 * Use updated method name --- changelog.d/10245.feature | 1 + synapse/api/constants.py | 3 - synapse/api/room_versions.py | 27 ++++++ synapse/event_auth.py | 38 ++++++++ synapse/events/utils.py | 3 + synapse/handlers/federation.py | 6 +- synapse/handlers/room.py | 1 + synapse/rest/client/v1/room.py | 7 +- .../databases/main/event_federation.py | 88 ++++++++++++++++-- synapse/storage/databases/main/events.py | 91 +++++++++++++++++++ synapse/storage/databases/main/state.py | 50 +++++++--- .../delta/61/01insertion_event_lookups.sql | 49 ++++++++++ 12 files changed, 338 insertions(+), 26 deletions(-) create mode 100644 changelog.d/10245.feature create mode 100644 synapse/storage/schema/main/delta/61/01insertion_event_lookups.sql diff --git a/changelog.d/10245.feature b/changelog.d/10245.feature new file mode 100644 index 0000000000..b3c48cc2cc --- /dev/null +++ b/changelog.d/10245.feature @@ -0,0 +1 @@ +Make historical events discoverable from backfill for servers without any scrollback history (part of MSC2716). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index a40d6d7961..a986fdb47a 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -206,9 +206,6 @@ class EventContentFields: MSC2716_CHUNK_ID = "org.matrix.msc2716.chunk_id" # For "marker" events MSC2716_MARKER_INSERTION = "org.matrix.msc2716.marker.insertion" - MSC2716_MARKER_INSERTION_PREV_EVENTS = ( - "org.matrix.msc2716.marker.insertion_prev_events" - ) class RoomTypes: diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 697319e52d..bc678efe49 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -73,6 +73,9 @@ class RoomVersion: # MSC2403: Allows join_rules to be set to 'knock', changes auth rules to allow sending # m.room.membership event with membership 'knock'. msc2403_knocking = attr.ib(type=bool) + # MSC2716: Adds m.room.power_levels -> content.historical field to control + # whether "insertion", "chunk", "marker" events can be sent + msc2716_historical = attr.ib(type=bool) class RoomVersions: @@ -88,6 +91,7 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=False, + msc2716_historical=False, ) V2 = RoomVersion( "2", @@ -101,6 +105,7 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=False, + msc2716_historical=False, ) V3 = RoomVersion( "3", @@ -114,6 +119,7 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=False, + msc2716_historical=False, ) V4 = RoomVersion( "4", @@ -127,6 +133,7 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=False, + msc2716_historical=False, ) V5 = RoomVersion( "5", @@ -140,6 +147,7 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=False, + msc2716_historical=False, ) V6 = RoomVersion( "6", @@ -153,6 +161,7 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=False, + msc2716_historical=False, ) MSC2176 = RoomVersion( "org.matrix.msc2176", @@ -166,6 +175,7 @@ class RoomVersions: msc2176_redaction_rules=True, msc3083_join_rules=False, msc2403_knocking=False, + msc2716_historical=False, ) MSC3083 = RoomVersion( "org.matrix.msc3083.v2", @@ -179,6 +189,7 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=True, msc2403_knocking=False, + msc2716_historical=False, ) V7 = RoomVersion( "7", @@ -192,6 +203,21 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=True, + msc2716_historical=False, + ) + MSC2716 = RoomVersion( + "org.matrix.msc2716", + RoomDisposition.STABLE, + EventFormatVersions.V3, + StateResolutionVersions.V2, + enforce_key_validity=True, + special_case_aliases_auth=False, + strict_canonicaljson=True, + limit_notifications_power_levels=True, + msc2176_redaction_rules=False, + msc3083_join_rules=False, + msc2403_knocking=True, + msc2716_historical=True, ) @@ -207,6 +233,7 @@ class RoomVersions: RoomVersions.MSC2176, RoomVersions.MSC3083, RoomVersions.V7, + RoomVersions.MSC2716, ) } diff --git a/synapse/event_auth.py b/synapse/event_auth.py index cc92d35477..0fa7ffc99f 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -205,6 +205,13 @@ def check( if event.type == EventTypes.Redaction: check_redaction(room_version_obj, event, auth_events) + if ( + event.type == EventTypes.MSC2716_INSERTION + or event.type == EventTypes.MSC2716_CHUNK + or event.type == EventTypes.MSC2716_MARKER + ): + check_historical(room_version_obj, event, auth_events) + logger.debug("Allowing! %s", event) @@ -539,6 +546,37 @@ def check_redaction( raise AuthError(403, "You don't have permission to redact events") +def check_historical( + room_version_obj: RoomVersion, + event: EventBase, + auth_events: StateMap[EventBase], +) -> None: + """Check whether the event sender is allowed to send historical related + events like "insertion", "chunk", and "marker". + + Returns: + None + + Raises: + AuthError if the event sender is not allowed to send historical related events + ("insertion", "chunk", and "marker"). + """ + # Ignore the auth checks in room versions that do not support historical + # events + if not room_version_obj.msc2716_historical: + return + + user_level = get_user_power_level(event.user_id, auth_events) + + historical_level = get_named_level(auth_events, "historical", 100) + + if user_level < historical_level: + raise AuthError( + 403, + 'You don\'t have permission to send send historical related events ("insertion", "chunk", and "marker")', + ) + + def _check_power_levels( room_version_obj: RoomVersion, event: EventBase, diff --git a/synapse/events/utils.py b/synapse/events/utils.py index f4da9e0923..a0c07f62f4 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -126,6 +126,9 @@ def add_fields(*fields): if room_version.msc2176_redaction_rules: add_fields("invite") + if room_version.msc2716_historical: + add_fields("historical") + elif event_type == EventTypes.Aliases and room_version.special_case_aliases_auth: add_fields("aliases") elif event_type == EventTypes.RoomHistoryVisibility: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index aba095d2e1..8197b60b76 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2748,9 +2748,11 @@ async def _update_auth_events_and_context_for_auth( event.event_id, e.event_id, ) - context = await self.state_handler.compute_event_context(e) + missing_auth_event_context = ( + await self.state_handler.compute_event_context(e) + ) await self._auth_and_persist_event( - origin, e, context, auth_events=auth + origin, e, missing_auth_event_context, auth_events=auth ) if e.event_id in event_auth_events: diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 370561e549..b33fe09f77 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -951,6 +951,7 @@ async def send(etype: str, content: JsonDict, **kwargs) -> int: "kick": 50, "redact": 50, "invite": 50, + "historical": 100, } if config["original_invitees_have_ops"]: diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 25ba52c624..502a917588 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -504,7 +504,6 @@ async def on_POST(self, request, room_id): events_to_create = body["events"] - prev_event_ids = prev_events_from_query inherited_depth = await self._inherit_depth_from_prev_ids( prev_events_from_query ) @@ -516,6 +515,10 @@ async def on_POST(self, request, room_id): chunk_id_to_connect_to = chunk_id_from_query base_insertion_event = None if chunk_id_from_query: + # All but the first base insertion event should point at a fake + # event, which causes the HS to ask for the state at the start of + # the chunk later. + prev_event_ids = [fake_prev_event_id] # TODO: Verify the chunk_id_from_query corresponds to an insertion event pass # Otherwise, create an insertion event to act as a starting point. @@ -526,6 +529,8 @@ async def on_POST(self, request, room_id): # an insertion event), in which case we just create a new insertion event # that can then get pointed to by a "marker" event later. else: + prev_event_ids = prev_events_from_query + base_insertion_event_dict = self._create_insertion_event_dict( sender=requester.user.to_string(), room_id=room_id, diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index f4a00b0736..547e43ab98 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -936,15 +936,46 @@ def _get_backfill_events(self, txn, room_id, event_list, limit): # We want to make sure that we do a breadth-first, "depth" ordered # search. - query = ( - "SELECT depth, prev_event_id FROM event_edges" - " INNER JOIN events" - " ON prev_event_id = events.event_id" - " WHERE event_edges.event_id = ?" - " AND event_edges.is_state = ?" - " LIMIT ?" - ) + # Look for the prev_event_id connected to the given event_id + query = """ + SELECT depth, prev_event_id FROM event_edges + /* Get the depth of the prev_event_id from the events table */ + INNER JOIN events + ON prev_event_id = events.event_id + /* Find an event which matches the given event_id */ + WHERE event_edges.event_id = ? + AND event_edges.is_state = ? + LIMIT ? + """ + + # Look for the "insertion" events connected to the given event_id + connected_insertion_event_query = """ + SELECT e.depth, i.event_id FROM insertion_event_edges AS i + /* Get the depth of the insertion event from the events table */ + INNER JOIN events AS e USING (event_id) + /* Find an insertion event which points via prev_events to the given event_id */ + WHERE i.insertion_prev_event_id = ? + LIMIT ? + """ + + # Find any chunk connections of a given insertion event + chunk_connection_query = """ + SELECT e.depth, c.event_id FROM insertion_events AS i + /* Find the chunk that connects to the given insertion event */ + INNER JOIN chunk_events AS c + ON i.next_chunk_id = c.chunk_id + /* Get the depth of the chunk start event from the events table */ + INNER JOIN events AS e USING (event_id) + /* Find an insertion event which matches the given event_id */ + WHERE i.event_id = ? + LIMIT ? + """ + # In a PriorityQueue, the lowest valued entries are retrieved first. + # We're using depth as the priority in the queue. + # Depth is lowest at the oldest-in-time message and highest and + # newest-in-time message. We add events to the queue with a negative depth so that + # we process the newest-in-time messages first going backwards in time. queue = PriorityQueue() for event_id in event_list: @@ -970,9 +1001,48 @@ def _get_backfill_events(self, txn, room_id, event_list, limit): event_results.add(event_id) + # Try and find any potential historical chunks of message history. + # + # First we look for an insertion event connected to the current + # event (by prev_event). If we find any, we need to go and try to + # find any chunk events connected to the insertion event (by + # chunk_id). If we find any, we'll add them to the queue and + # navigate up the DAG like normal in the next iteration of the loop. + txn.execute( + connected_insertion_event_query, (event_id, limit - len(event_results)) + ) + connected_insertion_event_id_results = txn.fetchall() + logger.debug( + "_get_backfill_events: connected_insertion_event_query %s", + connected_insertion_event_id_results, + ) + for row in connected_insertion_event_id_results: + connected_insertion_event_depth = row[0] + connected_insertion_event = row[1] + queue.put((-connected_insertion_event_depth, connected_insertion_event)) + + # Find any chunk connections for the given insertion event + txn.execute( + chunk_connection_query, + (connected_insertion_event, limit - len(event_results)), + ) + chunk_start_event_id_results = txn.fetchall() + logger.debug( + "_get_backfill_events: chunk_start_event_id_results %s", + chunk_start_event_id_results, + ) + for row in chunk_start_event_id_results: + if row[1] not in event_results: + queue.put((-row[0], row[1])) + + # Navigate up the DAG by prev_event txn.execute(query, (event_id, False, limit - len(event_results))) + prev_event_id_results = txn.fetchall() + logger.debug( + "_get_backfill_events: prev_event_ids %s", prev_event_id_results + ) - for row in txn: + for row in prev_event_id_results: if row[1] not in event_results: queue.put((-row[0], row[1])) diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index a396a201d4..86baf397fb 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -1502,6 +1502,9 @@ def _update_metadata_tables_txn( self._handle_event_relations(txn, event) + self._handle_insertion_event(txn, event) + self._handle_chunk_event(txn, event) + # Store the labels for this event. labels = event.content.get(EventContentFields.LABELS) if labels: @@ -1754,6 +1757,94 @@ def _handle_event_relations(self, txn, event): if rel_type == RelationTypes.REPLACE: txn.call_after(self.store.get_applicable_edit.invalidate, (parent_id,)) + def _handle_insertion_event(self, txn: LoggingTransaction, event: EventBase): + """Handles keeping track of insertion events and edges/connections. + Part of MSC2716. + + Args: + txn: The database transaction object + event: The event to process + """ + + if event.type != EventTypes.MSC2716_INSERTION: + # Not a insertion event + return + + # Skip processing a insertion event if the room version doesn't + # support it. + room_version = self.store.get_room_version_txn(txn, event.room_id) + if not room_version.msc2716_historical: + return + + next_chunk_id = event.content.get(EventContentFields.MSC2716_NEXT_CHUNK_ID) + if next_chunk_id is None: + # Invalid insertion event without next chunk ID + return + + logger.debug( + "_handle_insertion_event (next_chunk_id=%s) %s", next_chunk_id, event + ) + + # Keep track of the insertion event and the chunk ID + self.db_pool.simple_insert_txn( + txn, + table="insertion_events", + values={ + "event_id": event.event_id, + "room_id": event.room_id, + "next_chunk_id": next_chunk_id, + }, + ) + + # Insert an edge for every prev_event connection + for prev_event_id in event.prev_events: + self.db_pool.simple_insert_txn( + txn, + table="insertion_event_edges", + values={ + "event_id": event.event_id, + "room_id": event.room_id, + "insertion_prev_event_id": prev_event_id, + }, + ) + + def _handle_chunk_event(self, txn: LoggingTransaction, event: EventBase): + """Handles inserting the chunk edges/connections between the chunk event + and an insertion event. Part of MSC2716. + + Args: + txn: The database transaction object + event: The event to process + """ + + if event.type != EventTypes.MSC2716_CHUNK: + # Not a chunk event + return + + # Skip processing a chunk event if the room version doesn't + # support it. + room_version = self.store.get_room_version_txn(txn, event.room_id) + if not room_version.msc2716_historical: + return + + chunk_id = event.content.get(EventContentFields.MSC2716_CHUNK_ID) + if chunk_id is None: + # Invalid chunk event without a chunk ID + return + + logger.debug("_handle_chunk_event chunk_id=%s %s", chunk_id, event) + + # Keep track of the insertion event and the chunk ID + self.db_pool.simple_insert_txn( + txn, + table="chunk_events", + values={ + "event_id": event.event_id, + "room_id": event.room_id, + "chunk_id": chunk_id, + }, + ) + def _handle_redaction(self, txn, redacted_event_id): """Handles receiving a redaction and checking whether we need to remove any redacted relations from the database. diff --git a/synapse/storage/databases/main/state.py b/synapse/storage/databases/main/state.py index 1757064a68..8e22da99ae 100644 --- a/synapse/storage/databases/main/state.py +++ b/synapse/storage/databases/main/state.py @@ -22,7 +22,7 @@ from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.events import EventBase from synapse.storage._base import SQLBaseStore -from synapse.storage.database import DatabasePool +from synapse.storage.database import DatabasePool, LoggingTransaction from synapse.storage.databases.main.events_worker import EventsWorkerStore from synapse.storage.databases.main.roommember import RoomMemberWorkerStore from synapse.storage.state import StateFilter @@ -58,15 +58,32 @@ def __init__(self, database: DatabasePool, db_conn, hs): async def get_room_version(self, room_id: str) -> RoomVersion: """Get the room_version of a given room - Raises: NotFoundError: if the room is unknown + UnsupportedRoomVersionError: if the room uses an unknown room version. + Typically this happens if support for the room's version has been + removed from Synapse. + """ + return await self.db_pool.runInteraction( + "get_room_version_txn", + self.get_room_version_txn, + room_id, + ) + def get_room_version_txn( + self, txn: LoggingTransaction, room_id: str + ) -> RoomVersion: + """Get the room_version of a given room + Args: + txn: Transaction object + room_id: The room_id of the room you are trying to get the version for + Raises: + NotFoundError: if the room is unknown UnsupportedRoomVersionError: if the room uses an unknown room version. Typically this happens if support for the room's version has been removed from Synapse. """ - room_version_id = await self.get_room_version_id(room_id) + room_version_id = self.get_room_version_id_txn(txn, room_id) v = KNOWN_ROOM_VERSIONS.get(room_version_id) if not v: @@ -80,7 +97,20 @@ async def get_room_version(self, room_id: str) -> RoomVersion: @cached(max_entries=10000) async def get_room_version_id(self, room_id: str) -> str: """Get the room_version of a given room + Raises: + NotFoundError: if the room is unknown + """ + return await self.db_pool.runInteraction( + "get_room_version_id_txn", + self.get_room_version_id_txn, + room_id, + ) + def get_room_version_id_txn(self, txn: LoggingTransaction, room_id: str) -> str: + """Get the room_version of a given room + Args: + txn: Transaction object + room_id: The room_id of the room you are trying to get the version for Raises: NotFoundError: if the room is unknown """ @@ -88,24 +118,22 @@ async def get_room_version_id(self, room_id: str) -> str: # First we try looking up room version from the database, but for old # rooms we might not have added the room version to it yet so we fall # back to previous behaviour and look in current state events. - + # # We really should have an entry in the rooms table for every room we # care about, but let's be a bit paranoid (at least while the background # update is happening) to avoid breaking existing rooms. - version = await self.db_pool.simple_select_one_onecol( + room_version = self.db_pool.simple_select_one_onecol_txn( + txn, table="rooms", keyvalues={"room_id": room_id}, retcol="room_version", - desc="get_room_version", allow_none=True, ) - if version is not None: - return version + if room_version is None: + raise NotFoundError("Could not room_version for %s" % (room_id,)) - # Retrieve the room's create event - create_event = await self.get_create_event_for_room(room_id) - return create_event.content.get("room_version", "1") + return room_version async def get_room_predecessor(self, room_id: str) -> Optional[dict]: """Get the predecessor of an upgraded room if it exists. diff --git a/synapse/storage/schema/main/delta/61/01insertion_event_lookups.sql b/synapse/storage/schema/main/delta/61/01insertion_event_lookups.sql new file mode 100644 index 0000000000..7d7bafc631 --- /dev/null +++ b/synapse/storage/schema/main/delta/61/01insertion_event_lookups.sql @@ -0,0 +1,49 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +-- Add a table that keeps track of "insertion" events and +-- their next_chunk_id's so we can navigate to the next chunk of history. +CREATE TABLE IF NOT EXISTS insertion_events( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + next_chunk_id TEXT NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS insertion_events_event_id ON insertion_events(event_id); +CREATE INDEX IF NOT EXISTS insertion_events_next_chunk_id ON insertion_events(next_chunk_id); + +-- Add a table that keeps track of all of the events we are inserting between. +-- We use this when navigating the DAG and when we hit an event which matches +-- `insertion_prev_event_id`, it should backfill from the "insertion" event and +-- navigate the historical messages from there. +CREATE TABLE IF NOT EXISTS insertion_event_edges( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + insertion_prev_event_id TEXT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS insertion_event_edges_event_id ON insertion_event_edges(event_id); +CREATE INDEX IF NOT EXISTS insertion_event_edges_insertion_room_id ON insertion_event_edges(room_id); +CREATE INDEX IF NOT EXISTS insertion_event_edges_insertion_prev_event_id ON insertion_event_edges(insertion_prev_event_id); + +-- Add a table that keeps track of how each chunk is labeled. The chunks are +-- connected together based on an insertion events `next_chunk_id`. +CREATE TABLE IF NOT EXISTS chunk_events( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL, + chunk_id TEXT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS chunk_events_event_id ON chunk_events(event_id); +CREATE INDEX IF NOT EXISTS chunk_events_chunk_id ON chunk_events(chunk_id); From 858363d0b7e58fd71875b25d183537bb3b5a397f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 28 Jul 2021 20:55:50 +0100 Subject: [PATCH 483/619] Generics for `ObservableDeferred` (#10491) Now that `Deferred` is a generic class, let's update `ObeservableDeferred` to follow suit. --- changelog.d/10491.misc | 1 + synapse/notifier.py | 5 +++-- synapse/storage/persist_events.py | 4 +++- synapse/util/async_helpers.py | 14 ++++++++------ 4 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10491.misc diff --git a/changelog.d/10491.misc b/changelog.d/10491.misc new file mode 100644 index 0000000000..3867cf2682 --- /dev/null +++ b/changelog.d/10491.misc @@ -0,0 +1 @@ +Improve type annotations for `ObservableDeferred`. diff --git a/synapse/notifier.py b/synapse/notifier.py index c5fbebc17d..bbe337949a 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -111,8 +111,9 @@ def __init__( self.last_notified_token = current_token self.last_notified_ms = time_now_ms - with PreserveLoggingContext(): - self.notify_deferred = ObservableDeferred(defer.Deferred()) + self.notify_deferred: ObservableDeferred[StreamToken] = ObservableDeferred( + defer.Deferred() + ) def notify( self, diff --git a/synapse/storage/persist_events.py b/synapse/storage/persist_events.py index a39877f0d5..0e8270746d 100644 --- a/synapse/storage/persist_events.py +++ b/synapse/storage/persist_events.py @@ -170,7 +170,9 @@ async def add_to_queue( end_item = queue[-1] else: # need to make a new queue item - deferred = ObservableDeferred(defer.Deferred(), consumeErrors=True) + deferred: ObservableDeferred[_PersistResult] = ObservableDeferred( + defer.Deferred(), consumeErrors=True + ) end_item = _EventPersistQueueItem( events_and_contexts=[], diff --git a/synapse/util/async_helpers.py b/synapse/util/async_helpers.py index 912cf85f89..a3b65aee27 100644 --- a/synapse/util/async_helpers.py +++ b/synapse/util/async_helpers.py @@ -23,6 +23,7 @@ Awaitable, Callable, Dict, + Generic, Hashable, Iterable, List, @@ -39,6 +40,7 @@ from twisted.internet.defer import CancelledError from twisted.internet.interfaces import IReactorTime from twisted.python import failure +from twisted.python.failure import Failure from synapse.logging.context import ( PreserveLoggingContext, @@ -52,7 +54,7 @@ _T = TypeVar("_T") -class ObservableDeferred: +class ObservableDeferred(Generic[_T]): """Wraps a deferred object so that we can add observer deferreds. These observer deferreds do not affect the callback chain of the original deferred. @@ -70,7 +72,7 @@ class ObservableDeferred: __slots__ = ["_deferred", "_observers", "_result"] - def __init__(self, deferred: defer.Deferred, consumeErrors: bool = False): + def __init__(self, deferred: "defer.Deferred[_T]", consumeErrors: bool = False): object.__setattr__(self, "_deferred", deferred) object.__setattr__(self, "_result", None) object.__setattr__(self, "_observers", set()) @@ -115,7 +117,7 @@ def errback(f): deferred.addCallbacks(callback, errback) - def observe(self) -> defer.Deferred: + def observe(self) -> "defer.Deferred[_T]": """Observe the underlying deferred. This returns a brand new deferred that is resolved when the underlying @@ -123,7 +125,7 @@ def observe(self) -> defer.Deferred: effect the underlying deferred. """ if not self._result: - d: "defer.Deferred[Any]" = defer.Deferred() + d: "defer.Deferred[_T]" = defer.Deferred() def remove(r): self._observers.discard(d) @@ -137,7 +139,7 @@ def remove(r): success, res = self._result return defer.succeed(res) if success else defer.fail(res) - def observers(self) -> List[defer.Deferred]: + def observers(self) -> "List[defer.Deferred[_T]]": return self._observers def has_called(self) -> bool: @@ -146,7 +148,7 @@ def has_called(self) -> bool: def has_succeeded(self) -> bool: return self._result is not None and self._result[0] is True - def get_result(self) -> Any: + def get_result(self) -> Union[_T, Failure]: return self._result[1] def __getattr__(self, name: str) -> Any: From db6e7f15eaee81be54b960d040102900f20e3f74 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 29 Jul 2021 03:46:51 -0500 Subject: [PATCH 484/619] Fix backfilled events being rejected for no `state_groups` (#10439) Reproducible on a federated homeserver when there is a membership auth event as a floating outlier. Then when we try to backfill one of that persons messages, it has missing membership auth to fetch which caused us to mistakenly replace the `context` for the message with that of the floating membership `outlier` event. Since `outliers` have no `state` or `state_group`, the error bubbles up when we continue down the persisting route: `sqlite3.IntegrityError: NOT NULL constraint failed: event_to_state_groups.state_group` Call stack: ``` backfill _auth_and_persist_event _check_event_auth _update_auth_events_and_context_for_auth ``` --- changelog.d/10439.bugfix | 1 + tests/handlers/test_federation.py | 131 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 changelog.d/10439.bugfix diff --git a/changelog.d/10439.bugfix b/changelog.d/10439.bugfix new file mode 100644 index 0000000000..74e5a25126 --- /dev/null +++ b/changelog.d/10439.bugfix @@ -0,0 +1 @@ +Fix events with floating outlier state being rejected over federation. diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index ba8cf44f46..4140fcefc2 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import List from unittest import TestCase from synapse.api.constants import EventTypes @@ -22,6 +23,7 @@ from synapse.logging.context import LoggingContext, run_in_background from synapse.rest import admin from synapse.rest.client.v1 import login, room +from synapse.util.stringutils import random_string from tests import unittest @@ -39,6 +41,8 @@ def make_homeserver(self, reactor, clock): hs = self.setup_test_homeserver(federation_http_client=None) self.handler = hs.get_federation_handler() self.store = hs.get_datastore() + self.state_store = hs.get_storage().state + self._event_auth_handler = hs.get_event_auth_handler() return hs def test_exchange_revoked_invite(self): @@ -190,6 +194,133 @@ def test_rejected_state_event_state(self): self.assertEqual(sg, sg2) + def test_backfill_floating_outlier_membership_auth(self): + """ + As the local homeserver, check that we can properly process a federated + event from the OTHER_SERVER with auth_events that include a floating + membership event from the OTHER_SERVER. + + Regression test, see #10439. + """ + OTHER_SERVER = "otherserver" + OTHER_USER = "@otheruser:" + OTHER_SERVER + + # create the room + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + room_id = self.helper.create_room_as( + room_creator=user_id, + is_public=True, + tok=tok, + extra_content={ + "preset": "public_chat", + }, + ) + room_version = self.get_success(self.store.get_room_version(room_id)) + + prev_event_ids = self.get_success(self.store.get_prev_events_for_room(room_id)) + ( + most_recent_prev_event_id, + most_recent_prev_event_depth, + ) = self.get_success(self.store.get_max_depth_of(prev_event_ids)) + # mapping from (type, state_key) -> state_event_id + prev_state_map = self.get_success( + self.state_store.get_state_ids_for_event(most_recent_prev_event_id) + ) + # List of state event ID's + prev_state_ids = list(prev_state_map.values()) + auth_event_ids = prev_state_ids + auth_events = list( + self.get_success(self.store.get_events(auth_event_ids)).values() + ) + + # build a floating outlier member state event + fake_prev_event_id = "$" + random_string(43) + member_event_dict = { + "type": EventTypes.Member, + "content": { + "membership": "join", + }, + "state_key": OTHER_USER, + "room_id": room_id, + "sender": OTHER_USER, + "depth": most_recent_prev_event_depth, + "prev_events": [fake_prev_event_id], + "origin_server_ts": self.clock.time_msec(), + "signatures": {OTHER_SERVER: {"ed25519:key_version": "SomeSignatureHere"}}, + } + builder = self.hs.get_event_builder_factory().for_room_version( + room_version, member_event_dict + ) + member_event = self.get_success( + builder.build( + prev_event_ids=member_event_dict["prev_events"], + auth_event_ids=self._event_auth_handler.compute_auth_events( + builder, + prev_state_map, + for_verification=False, + ), + depth=member_event_dict["depth"], + ) + ) + # Override the signature added from "test" homeserver that we created the event with + member_event.signatures = member_event_dict["signatures"] + + # Add the new member_event to the StateMap + prev_state_map[ + (member_event.type, member_event.state_key) + ] = member_event.event_id + auth_events.append(member_event) + + # build and send an event authed based on the member event + message_event_dict = { + "type": EventTypes.Message, + "content": {}, + "room_id": room_id, + "sender": OTHER_USER, + "depth": most_recent_prev_event_depth, + "prev_events": prev_event_ids.copy(), + "origin_server_ts": self.clock.time_msec(), + "signatures": {OTHER_SERVER: {"ed25519:key_version": "SomeSignatureHere"}}, + } + builder = self.hs.get_event_builder_factory().for_room_version( + room_version, message_event_dict + ) + message_event = self.get_success( + builder.build( + prev_event_ids=message_event_dict["prev_events"], + auth_event_ids=self._event_auth_handler.compute_auth_events( + builder, + prev_state_map, + for_verification=False, + ), + depth=message_event_dict["depth"], + ) + ) + # Override the signature added from "test" homeserver that we created the event with + message_event.signatures = message_event_dict["signatures"] + + # Stub the /event_auth response from the OTHER_SERVER + async def get_event_auth( + destination: str, room_id: str, event_id: str + ) -> List[EventBase]: + return auth_events + + self.handler.federation_client.get_event_auth = get_event_auth + + with LoggingContext("receive_pdu"): + # Fake the OTHER_SERVER federating the message event over to our local homeserver + d = run_in_background( + self.handler.on_receive_pdu, OTHER_SERVER, message_event + ) + self.get_success(d) + + # Now try and get the events on our local homeserver + stored_event = self.get_success( + self.store.get_event(message_event.event_id, allow_none=True) + ) + self.assertTrue(stored_event is not None) + @unittest.override_config( {"rc_invites": {"per_user": {"per_second": 0.5, "burst_count": 3}}} ) From 5522a103a9883de95d05e088d9cf32848adb5b7f Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 29 Jul 2021 09:59:07 +0100 Subject: [PATCH 485/619] 1.39.0 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b512d9ff3f..284bdf835c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.39.0 (2021-07-29) +=========================== + +No significant changes. + + Synapse 1.39.0rc3 (2021-07-28) ============================== diff --git a/debian/changelog b/debian/changelog index 4944e55714..c8e6fa15fe 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.39.0) stable; urgency=medium + + * New synapse release 1.39.0. + + -- Synapse Packaging team Thu, 29 Jul 2021 09:59:00 +0100 + matrix-synapse-py3 (1.39.0~rc3) stable; urgency=medium * New synapse release 1.39.0~rc3. diff --git a/synapse/__init__.py b/synapse/__init__.py index c9a445c8fe..5da6c924fc 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.39.0rc3" +__version__ = "1.39.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 6449955920157764ba8ba7bcb479de0c04b2c0d1 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 29 Jul 2021 10:06:00 +0100 Subject: [PATCH 486/619] Fixup changelog --- CHANGES.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 284bdf835c..6533249281 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,10 +25,7 @@ Internal Changes Synapse 1.39.0rc2 (2021-07-22) ============================== -Bugfixes --------- - -- Always include `device_one_time_keys_count` key in `/sync` response to work around a bug in Element Android that broke encryption for new devices. ([\#10457](https://github.com/matrix-org/synapse/issues/10457)) +This release also includes the changes in v1.38.1. Internal Changes From 3a541a7daa3191f0d91cb33d76778d450107640c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 29 Jul 2021 07:50:14 -0400 Subject: [PATCH 487/619] Improve failover logic for MSC3083 restricted rooms. (#10447) If the federation client receives an M_UNABLE_TO_AUTHORISE_JOIN or M_UNABLE_TO_GRANT_JOIN response it will attempt another server before giving up completely. --- changelog.d/10447.feature | 1 + synapse/federation/federation_client.py | 43 ++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 changelog.d/10447.feature diff --git a/changelog.d/10447.feature b/changelog.d/10447.feature new file mode 100644 index 0000000000..df8bb51167 --- /dev/null +++ b/changelog.d/10447.feature @@ -0,0 +1 @@ +Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index dbadf102f2..b7a10da15a 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -22,6 +22,7 @@ Awaitable, Callable, Collection, + Container, Dict, Iterable, List, @@ -513,6 +514,7 @@ async def _try_destination_list( description: str, destinations: Iterable[str], callback: Callable[[str], Awaitable[T]], + failover_errcodes: Optional[Container[str]] = None, failover_on_unknown_endpoint: bool = False, ) -> T: """Try an operation on a series of servers, until it succeeds @@ -533,6 +535,9 @@ async def _try_destination_list( next server tried. Normally the stacktrace is logged but this is suppressed if the exception is an InvalidResponseError. + failover_errcodes: Error codes (specific to this endpoint) which should + cause a failover when received as part of an HTTP 400 error. + failover_on_unknown_endpoint: if True, we will try other servers if it looks like a server doesn't support the endpoint. This is typically useful if the endpoint in question is new or experimental. @@ -544,6 +549,9 @@ async def _try_destination_list( SynapseError if the chosen remote server returns a 300/400 code, or no servers were reachable. """ + if failover_errcodes is None: + failover_errcodes = () + for destination in destinations: if destination == self.server_name: continue @@ -558,11 +566,17 @@ async def _try_destination_list( synapse_error = e.to_synapse_error() failover = False - # Failover on an internal server error, or if the destination - # doesn't implemented the endpoint for some reason. + # Failover should occur: + # + # * On internal server errors. + # * If the destination responds that it cannot complete the request. + # * If the destination doesn't implemented the endpoint for some reason. if 500 <= e.code < 600: failover = True + elif e.code == 400 and synapse_error.errcode in failover_errcodes: + failover = True + elif failover_on_unknown_endpoint and self._is_unknown_endpoint( e, synapse_error ): @@ -678,8 +692,20 @@ async def send_request(destination: str) -> Tuple[str, EventBase, RoomVersion]: return destination, ev, room_version + # MSC3083 defines additional error codes for room joins. Unfortunately + # we do not yet know the room version, assume these will only be returned + # by valid room versions. + failover_errcodes = ( + (Codes.UNABLE_AUTHORISE_JOIN, Codes.UNABLE_TO_GRANT_JOIN) + if membership == Membership.JOIN + else None + ) + return await self._try_destination_list( - "make_" + membership, destinations, send_request + "make_" + membership, + destinations, + send_request, + failover_errcodes=failover_errcodes, ) async def send_join( @@ -818,7 +844,14 @@ async def _execute(pdu: EventBase) -> None: origin=destination, ) + # MSC3083 defines additional error codes for room joins. + failover_errcodes = None if room_version.msc3083_join_rules: + failover_errcodes = ( + Codes.UNABLE_AUTHORISE_JOIN, + Codes.UNABLE_TO_GRANT_JOIN, + ) + # If the join is being authorised via allow rules, we need to send # the /send_join back to the same server that was originally used # with /make_join. @@ -827,7 +860,9 @@ async def _execute(pdu: EventBase) -> None: get_domain_from_id(pdu.content["join_authorised_via_users_server"]) ] - return await self._try_destination_list("send_join", destinations, send_request) + return await self._try_destination_list( + "send_join", destinations, send_request, failover_errcodes=failover_errcodes + ) async def _do_send_join( self, room_version: RoomVersion, destination: str, pdu: EventBase From b7f7ca24b1ca79426289c9c26e9314df9a0a96f6 Mon Sep 17 00:00:00 2001 From: V02460 Date: Thu, 29 Jul 2021 22:34:14 +0200 Subject: [PATCH 488/619] Remove shebang line from module files (#10415) Signed-off-by: Kai A. Hiller --- changelog.d/10415.misc | 1 + synapse/_scripts/review_recent_signups.py | 1 - synapse/app/admin_cmd.py | 1 - synapse/app/appservice.py | 1 - synapse/app/client_reader.py | 1 - synapse/app/event_creator.py | 1 - synapse/app/federation_reader.py | 1 - synapse/app/federation_sender.py | 1 - synapse/app/frontend_proxy.py | 1 - synapse/app/generic_worker.py | 1 - synapse/app/homeserver.py | 1 - synapse/app/media_repository.py | 1 - synapse/app/pusher.py | 1 - synapse/app/synchrotron.py | 1 - synapse/app/user_dir.py | 1 - synapse/push/pusherpool.py | 1 - synapse/util/versionstring.py | 1 - 17 files changed, 1 insertion(+), 16 deletions(-) create mode 100644 changelog.d/10415.misc diff --git a/changelog.d/10415.misc b/changelog.d/10415.misc new file mode 100644 index 0000000000..3b9501acbb --- /dev/null +++ b/changelog.d/10415.misc @@ -0,0 +1 @@ +Remove shebang line from module files. diff --git a/synapse/_scripts/review_recent_signups.py b/synapse/_scripts/review_recent_signups.py index 01dc0c4237..9de913db88 100644 --- a/synapse/_scripts/review_recent_signups.py +++ b/synapse/_scripts/review_recent_signups.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2021 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 2878d2c140..3234d9ebba 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2019 Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/appservice.py b/synapse/app/appservice.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/appservice.py +++ b/synapse/app/appservice.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/event_creator.py b/synapse/app/event_creator.py index 57af28f10a..885454ed44 100644 --- a/synapse/app/event_creator.py +++ b/synapse/app/event_creator.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2018 New Vector Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/federation_reader.py b/synapse/app/federation_reader.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/federation_reader.py +++ b/synapse/app/federation_reader.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/federation_sender.py b/synapse/app/federation_sender.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/federation_sender.py +++ b/synapse/app/federation_sender.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/frontend_proxy.py b/synapse/app/frontend_proxy.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/frontend_proxy.py +++ b/synapse/app/frontend_proxy.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index c3d4992518..3b7131af8f 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # Copyright 2020 The Matrix.org Foundation C.I.C. # diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index 920b34d97b..7dae163c1a 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2014-2016 OpenMarket Ltd # Copyright 2019 New Vector Ltd # diff --git a/synapse/app/media_repository.py b/synapse/app/media_repository.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/media_repository.py +++ b/synapse/app/media_repository.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/synchrotron.py b/synapse/app/synchrotron.py index 2d50060ffb..de1bcee0a7 100644 --- a/synapse/app/synchrotron.py +++ b/synapse/app/synchrotron.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/app/user_dir.py b/synapse/app/user_dir.py index a368efb354..14bde27179 100644 --- a/synapse/app/user_dir.py +++ b/synapse/app/user_dir.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2017 Vector Creations Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 85621f33ef..a1436f3930 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2015, 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/synapse/util/versionstring.py b/synapse/util/versionstring.py index dfa30a6229..cb08af7385 100644 --- a/synapse/util/versionstring.py +++ b/synapse/util/versionstring.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright 2016 OpenMarket Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); From c167e09fe58d3a256fb1c763b391ad6633d2507d Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Fri, 30 Jul 2021 12:34:21 +0100 Subject: [PATCH 489/619] Fix explicit assignment of PL 0 from being misinterpreted in rare circumstances (#10499) --- changelog.d/10499.bugfix | 1 + synapse/event_auth.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10499.bugfix diff --git a/changelog.d/10499.bugfix b/changelog.d/10499.bugfix new file mode 100644 index 0000000000..6487af6c96 --- /dev/null +++ b/changelog.d/10499.bugfix @@ -0,0 +1 @@ +Fix a bug which caused an explicit assignment of power-level 0 to a user to be misinterpreted in rare circumstances. diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 0fa7ffc99f..4c92e9a2d4 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -692,7 +692,7 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int: power_level_event = get_power_level_event(auth_events) if power_level_event: level = power_level_event.content.get("users", {}).get(user_id) - if not level: + if level is None: level = power_level_event.content.get("users_default", 0) if level is None: From 2afdb5c98470ab9d5aa793906c1710a65fb3028c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Sun, 1 Aug 2021 10:47:36 +0100 Subject: [PATCH 490/619] Fix deb build script to set prerelease flag correctly (#10500) --- changelog.d/10500.misc | 1 + docker/build_debian.sh | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10500.misc diff --git a/changelog.d/10500.misc b/changelog.d/10500.misc new file mode 100644 index 0000000000..dbaff57364 --- /dev/null +++ b/changelog.d/10500.misc @@ -0,0 +1 @@ +Fix a bug which caused production debian packages to be incorrectly marked as 'prerelease'. diff --git a/docker/build_debian.sh b/docker/build_debian.sh index f572ed9aa0..801ff45471 100644 --- a/docker/build_debian.sh +++ b/docker/build_debian.sh @@ -11,10 +11,6 @@ DIST=`cut -d ':' -f2 <<< $distro` cp -aT /synapse/source /synapse/build cd /synapse/build -# add an entry to the changelog for this distribution -dch -M -l "+$DIST" "build for $DIST" -dch -M -r "" --force-distribution --distribution "$DIST" - # if this is a prerelease, set the Section accordingly. # # When the package is later added to the package repo, reprepro will use the @@ -23,11 +19,14 @@ dch -M -r "" --force-distribution --distribution "$DIST" DEB_VERSION=`dpkg-parsechangelog -SVersion` case $DEB_VERSION in - *rc*|*a*|*b*|*c*) + *~rc*|*~a*|*~b*|*~c*) sed -ie '/^Section:/c\Section: prerelease' debian/control ;; esac +# add an entry to the changelog for this distribution +dch -M -l "+$DIST" "build for $DIST" +dch -M -r "" --force-distribution --distribution "$DIST" dpkg-buildpackage -us -uc From ba5287f5e8be150551824493b3ad685dde00a543 Mon Sep 17 00:00:00 2001 From: Toni Spets Date: Mon, 2 Aug 2021 16:24:43 +0300 Subject: [PATCH 491/619] Allow setting transaction limit for db connections (#10440) Setting the value will help PostgreSQL free up memory by recycling the connections in the connection pool. Signed-off-by: Toni Spets --- changelog.d/10440.feature | 1 + docs/sample_config.yaml | 4 ++++ synapse/config/database.py | 4 ++++ synapse/storage/database.py | 21 +++++++++++++++++++ tests/storage/test_txn_limit.py | 36 +++++++++++++++++++++++++++++++++ tests/utils.py | 3 +++ 6 files changed, 69 insertions(+) create mode 100644 changelog.d/10440.feature create mode 100644 tests/storage/test_txn_limit.py diff --git a/changelog.d/10440.feature b/changelog.d/10440.feature new file mode 100644 index 0000000000..f1833b0bd7 --- /dev/null +++ b/changelog.d/10440.feature @@ -0,0 +1 @@ +Allow setting transaction limit for database connections. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 853c2f6899..1a217f35db 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -720,6 +720,9 @@ caches: # 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or # 'psycopg2' (for PostgreSQL). # +# 'txn_limit' gives the maximum number of transactions to run per connection +# before reconnecting. Defaults to 0, which means no limit. +# # 'args' gives options which are passed through to the database engine, # except for options starting 'cp_', which are used to configure the Twisted # connection pool. For a reference to valid arguments, see: @@ -740,6 +743,7 @@ caches: # #database: # name: psycopg2 +# txn_limit: 10000 # args: # user: synapse_user # password: secretpassword diff --git a/synapse/config/database.py b/synapse/config/database.py index 3d7d92f615..651e31b576 100644 --- a/synapse/config/database.py +++ b/synapse/config/database.py @@ -33,6 +33,9 @@ # 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or # 'psycopg2' (for PostgreSQL). # +# 'txn_limit' gives the maximum number of transactions to run per connection +# before reconnecting. Defaults to 0, which means no limit. +# # 'args' gives options which are passed through to the database engine, # except for options starting 'cp_', which are used to configure the Twisted # connection pool. For a reference to valid arguments, see: @@ -53,6 +56,7 @@ # #database: # name: psycopg2 +# txn_limit: 10000 # args: # user: synapse_user # password: secretpassword diff --git a/synapse/storage/database.py b/synapse/storage/database.py index 4d4643619f..c8015a3848 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -15,6 +15,7 @@ # limitations under the License. import logging import time +from collections import defaultdict from sys import intern from time import monotonic as monotonic_time from typing import ( @@ -397,6 +398,7 @@ def __init__( ): self.hs = hs self._clock = hs.get_clock() + self._txn_limit = database_config.config.get("txn_limit", 0) self._database_config = database_config self._db_pool = make_pool(hs.get_reactor(), database_config, engine) @@ -406,6 +408,9 @@ def __init__( self._current_txn_total_time = 0.0 self._previous_loop_ts = 0.0 + # Transaction counter: key is the twisted thread id, value is the current count + self._txn_counters: Dict[int, int] = defaultdict(int) + # TODO(paul): These can eventually be removed once the metrics code # is running in mainline, and we have some nice monitoring frontends # to watch it @@ -750,10 +755,26 @@ def inner_func(conn, *args, **kwargs): sql_scheduling_timer.observe(sched_duration_sec) context.add_database_scheduled(sched_duration_sec) + if self._txn_limit > 0: + tid = self._db_pool.threadID() + self._txn_counters[tid] += 1 + + if self._txn_counters[tid] > self._txn_limit: + logger.debug( + "Reconnecting database connection over transaction limit" + ) + conn.reconnect() + opentracing.log_kv( + {"message": "reconnected due to txn limit"} + ) + self._txn_counters[tid] = 1 + if self.engine.is_connection_closed(conn): logger.debug("Reconnecting closed database connection") conn.reconnect() opentracing.log_kv({"message": "reconnected"}) + if self._txn_limit > 0: + self._txn_counters[tid] = 1 try: if db_autocommit: diff --git a/tests/storage/test_txn_limit.py b/tests/storage/test_txn_limit.py new file mode 100644 index 0000000000..9be51f9ebd --- /dev/null +++ b/tests/storage/test_txn_limit.py @@ -0,0 +1,36 @@ +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tests import unittest + + +class SQLTransactionLimitTestCase(unittest.HomeserverTestCase): + """Test SQL transaction limit doesn't break transactions.""" + + def make_homeserver(self, reactor, clock): + return self.setup_test_homeserver(db_txn_limit=1000) + + def test_config(self): + db_config = self.hs.config.get_single_database() + self.assertEqual(db_config.config["txn_limit"], 1000) + + def test_select(self): + def do_select(txn): + txn.execute("SELECT 1") + + db_pool = self.hs.get_datastores().databases[0] + + # force txn limit to roll over at least once + for i in range(0, 1001): + self.get_success_or_raise(db_pool.runInteraction("test_select", do_select)) diff --git a/tests/utils.py b/tests/utils.py index 6bd008dcfe..f3458ca88d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -239,6 +239,9 @@ def setup_test_homeserver( "args": {"database": ":memory:", "cp_min": 1, "cp_max": 1}, } + if "db_txn_limit" in kwargs: + database_config["txn_limit"] = kwargs["db_txn_limit"] + database = DatabaseConnectionConfig("master", database_config) config.database.databases = [database] From 01d45fe964d323e7f66358c2db57d00a44bf2274 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 2 Aug 2021 14:37:25 +0100 Subject: [PATCH 492/619] Prune inbound federation queues if they get too long (#10390) --- changelog.d/10390.misc | 1 + synapse/federation/federation_server.py | 17 +++ .../databases/main/event_federation.py | 104 +++++++++++++++++- tests/storage/test_event_federation.py | 57 ++++++++++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10390.misc diff --git a/changelog.d/10390.misc b/changelog.d/10390.misc new file mode 100644 index 0000000000..911a5733ee --- /dev/null +++ b/changelog.d/10390.misc @@ -0,0 +1 @@ +Prune inbound federation inbound queues for a room if they get too large. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 2892a11d7d..145b9161d9 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -1024,6 +1024,23 @@ async def _process_incoming_pdus_in_room_inner( origin, event = next + # Prune the event queue if it's getting large. + # + # We do this *after* handling the first event as the common case is + # that the queue is empty (/has the single event in), and so there's + # no need to do this check. + pruned = await self.store.prune_staged_events_in_room(room_id, room_version) + if pruned: + # If we have pruned the queue check we need to refetch the next + # event to handle. + next = await self.store.get_next_staged_event_for_room( + room_id, room_version + ) + if not next: + break + + origin, event = next + lock = await self.store.try_acquire_lock( _INBOUND_EVENT_HANDLING_LOCK_NAME, room_id ) diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index 547e43ab98..44018c1c31 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -16,11 +16,11 @@ from queue import Empty, PriorityQueue from typing import Collection, Dict, Iterable, List, Optional, Set, Tuple -from prometheus_client import Gauge +from prometheus_client import Counter, Gauge from synapse.api.constants import MAX_DEPTH from synapse.api.errors import StoreError -from synapse.api.room_versions import RoomVersion +from synapse.api.room_versions import EventFormatVersions, RoomVersion from synapse.events import EventBase, make_event_from_dict from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.storage._base import SQLBaseStore, db_to_json, make_in_list_sql_clause @@ -44,6 +44,12 @@ "The total number of events in the inbound federation staging", ) +pdus_pruned_from_federation_queue = Counter( + "synapse_federation_server_number_inbound_pdu_pruned", + "The number of events in the inbound federation staging that have been " + "pruned due to the queue getting too long", +) + logger = logging.getLogger(__name__) @@ -1277,6 +1283,100 @@ def _get_next_staged_event_for_room_txn(txn): return origin, event + async def prune_staged_events_in_room( + self, + room_id: str, + room_version: RoomVersion, + ) -> bool: + """Checks if there are lots of staged events for the room, and if so + prune them down. + + Returns: + Whether any events were pruned + """ + + # First check the size of the queue. + count = await self.db_pool.simple_select_one_onecol( + table="federation_inbound_events_staging", + keyvalues={"room_id": room_id}, + retcol="COALESCE(COUNT(*), 0)", + desc="prune_staged_events_in_room_count", + ) + + if count < 100: + return False + + # If the queue is too large, then we want clear the entire queue, + # keeping only the forward extremities (i.e. the events not referenced + # by other events in the queue). We do this so that we can always + # backpaginate in all the events we have dropped. + rows = await self.db_pool.simple_select_list( + table="federation_inbound_events_staging", + keyvalues={"room_id": room_id}, + retcols=("event_id", "event_json"), + desc="prune_staged_events_in_room_fetch", + ) + + # Find the set of events referenced by those in the queue, as well as + # collecting all the event IDs in the queue. + referenced_events: Set[str] = set() + seen_events: Set[str] = set() + for row in rows: + event_id = row["event_id"] + seen_events.add(event_id) + event_d = db_to_json(row["event_json"]) + + # We don't bother parsing the dicts into full blown event objects, + # as that is needlessly expensive. + + # We haven't checked that the `prev_events` have the right format + # yet, so we check as we go. + prev_events = event_d.get("prev_events", []) + if not isinstance(prev_events, list): + logger.info("Invalid prev_events for %s", event_id) + continue + + if room_version.event_format == EventFormatVersions.V1: + for prev_event_tuple in prev_events: + if not isinstance(prev_event_tuple, list) or len(prev_events) != 2: + logger.info("Invalid prev_events for %s", event_id) + break + + prev_event_id = prev_event_tuple[0] + if not isinstance(prev_event_id, str): + logger.info("Invalid prev_events for %s", event_id) + break + + referenced_events.add(prev_event_id) + else: + for prev_event_id in prev_events: + if not isinstance(prev_event_id, str): + logger.info("Invalid prev_events for %s", event_id) + break + + referenced_events.add(prev_event_id) + + to_delete = referenced_events & seen_events + if not to_delete: + return False + + pdus_pruned_from_federation_queue.inc(len(to_delete)) + logger.info( + "Pruning %d events in room %s from federation queue", + len(to_delete), + room_id, + ) + + await self.db_pool.simple_delete_many( + table="federation_inbound_events_staging", + keyvalues={"room_id": room_id}, + iterable=to_delete, + column="event_id", + desc="prune_staged_events_in_room_delete", + ) + + return True + async def get_all_rooms_with_staged_incoming_events(self) -> List[str]: """Get the room IDs of all events currently staged.""" return await self.db_pool.simple_select_onecol( diff --git a/tests/storage/test_event_federation.py b/tests/storage/test_event_federation.py index a0e2259478..c3fcf7e7b4 100644 --- a/tests/storage/test_event_federation.py +++ b/tests/storage/test_event_federation.py @@ -15,7 +15,9 @@ import attr from parameterized import parameterized +from synapse.api.room_versions import RoomVersions from synapse.events import _EventInternalMetadata +from synapse.util import json_encoder import tests.unittest import tests.utils @@ -504,6 +506,61 @@ def insert_event(txn): ) self.assertSetEqual(difference, set()) + def test_prune_inbound_federation_queue(self): + "Test that pruning of inbound federation queues work" + + room_id = "some_room_id" + + # Insert a bunch of events that all reference the previous one. + self.get_success( + self.store.db_pool.simple_insert_many( + table="federation_inbound_events_staging", + values=[ + { + "origin": "some_origin", + "room_id": room_id, + "received_ts": 0, + "event_id": f"$fake_event_id_{i + 1}", + "event_json": json_encoder.encode( + {"prev_events": [f"$fake_event_id_{i}"]} + ), + "internal_metadata": "{}", + } + for i in range(500) + ], + desc="test_prune_inbound_federation_queue", + ) + ) + + # Calling prune once should return True, i.e. a prune happen. The second + # time it shouldn't. + pruned = self.get_success( + self.store.prune_staged_events_in_room(room_id, RoomVersions.V6) + ) + self.assertTrue(pruned) + + pruned = self.get_success( + self.store.prune_staged_events_in_room(room_id, RoomVersions.V6) + ) + self.assertFalse(pruned) + + # Assert that we only have a single event left in the queue, and that it + # is the last one. + count = self.get_success( + self.store.db_pool.simple_select_one_onecol( + table="federation_inbound_events_staging", + keyvalues={"room_id": room_id}, + retcol="COALESCE(COUNT(*), 0)", + desc="test_prune_inbound_federation_queue", + ) + ) + self.assertEqual(count, 1) + + _, event_id = self.get_success( + self.store.get_next_staged_event_id_for_room(room_id) + ) + self.assertEqual(event_id, "$fake_event_id_500") + @attr.s class FakeEvent: From fb086edaeddf8cdb8a03b8564d1b6883ac5cac6e Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Mon, 2 Aug 2021 16:50:22 +0100 Subject: [PATCH 493/619] Fix codestyle CI from #10440 (#10511) Co-authored-by: Erik Johnston --- changelog.d/10511.feature | 1 + tests/storage/test_txn_limit.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10511.feature diff --git a/changelog.d/10511.feature b/changelog.d/10511.feature new file mode 100644 index 0000000000..f1833b0bd7 --- /dev/null +++ b/changelog.d/10511.feature @@ -0,0 +1 @@ +Allow setting transaction limit for database connections. diff --git a/tests/storage/test_txn_limit.py b/tests/storage/test_txn_limit.py index 9be51f9ebd..6ff3ebb137 100644 --- a/tests/storage/test_txn_limit.py +++ b/tests/storage/test_txn_limit.py @@ -32,5 +32,5 @@ def do_select(txn): db_pool = self.hs.get_datastores().databases[0] # force txn limit to roll over at least once - for i in range(0, 1001): + for _ in range(0, 1001): self.get_success_or_raise(db_pool.runInteraction("test_select", do_select)) From a6ea32a79893b6ee694d036f3bc29a02a79d51e8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 2 Aug 2021 21:06:34 +0100 Subject: [PATCH 494/619] Fix the `tests-done` github actions step, again (#10512) --- .github/workflows/tests.yml | 21 ++++++++++++--------- changelog.d/10512.misc | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10512.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a62c62d02..239553ae13 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -367,13 +367,16 @@ jobs: - name: Set build result env: NEEDS_CONTEXT: ${{ toJSON(needs) }} - # the `jq` incantation dumps out a series of " " lines + # the `jq` incantation dumps out a series of " " lines. + # we set it to an intermediate variable to avoid a pipe, which makes it + # hard to set $rc. run: | - set -o pipefail - jq -r 'to_entries[] | [.key,.value.result] | join(" ")' \ - <<< $NEEDS_CONTEXT | - while read job result; do - if [ "$result" != "success" ]; then - echo "::set-failed ::Job $job returned $result" - fi - done + rc=0 + results=$(jq -r 'to_entries[] | [.key,.value.result] | join(" ")' <<< $NEEDS_CONTEXT) + while read job result ; do + if [ "$result" != "success" ]; then + echo "::set-failed ::Job $job returned $result" + rc=1 + fi + done <<< $results + exit $rc diff --git a/changelog.d/10512.misc b/changelog.d/10512.misc new file mode 100644 index 0000000000..c012e89f4b --- /dev/null +++ b/changelog.d/10512.misc @@ -0,0 +1 @@ +Update the `tests-done` Github Actions status. From 2bae2c632ff595bda770212678521e04288f00a9 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 3 Aug 2021 05:08:57 -0500 Subject: [PATCH 495/619] Add developer documentation to explain room DAG concepts like `outliers` and `state_groups` (#10464) --- changelog.d/10464.doc | 1 + docs/SUMMARY.md | 1 + docs/development/room-dag-concepts.md | 79 +++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 changelog.d/10464.doc create mode 100644 docs/development/room-dag-concepts.md diff --git a/changelog.d/10464.doc b/changelog.d/10464.doc new file mode 100644 index 0000000000..764fb9f65c --- /dev/null +++ b/changelog.d/10464.doc @@ -0,0 +1 @@ +Add some developer docs to explain room DAG concepts like `outliers`, `state_groups`, `depth`, etc. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index f1bde91420..10be12d638 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -79,6 +79,7 @@ - [Single Sign-On]() - [SAML](development/saml.md) - [CAS](development/cas.md) + - [Room DAG concepts](development/room-dag-concepts.md) - [State Resolution]() - [The Auth Chain Difference Algorithm](auth_chain_difference_algorithm.md) - [Media Repository](media_repository.md) diff --git a/docs/development/room-dag-concepts.md b/docs/development/room-dag-concepts.md new file mode 100644 index 0000000000..5eed72bec6 --- /dev/null +++ b/docs/development/room-dag-concepts.md @@ -0,0 +1,79 @@ +# Room DAG concepts + +## Edges + +The word "edge" comes from graph theory lingo. An edge is just a connection +between two events. In Synapse, we connect events by specifying their +`prev_events`. A subsequent event points back at a previous event. + +``` +A (oldest) <---- B <---- C (most recent) +``` + + +## Depth and stream ordering + +Events are normally sorted by `(topological_ordering, stream_ordering)` where +`topological_ordering` is just `depth`. In other words, we first sort by `depth` +and then tie-break based on `stream_ordering`. `depth` is incremented as new +messages are added to the DAG. Normally, `stream_ordering` is an auto +incrementing integer, but backfilled events start with `stream_ordering=-1` and decrement. + +--- + + - `/sync` returns things in the order they arrive at the server (`stream_ordering`). + - `/messages` (and `/backfill` in the federation API) return them in the order determined by the event graph `(topological_ordering, stream_ordering)`. + +The general idea is that, if you're following a room in real-time (i.e. +`/sync`), you probably want to see the messages as they arrive at your server, +rather than skipping any that arrived late; whereas if you're looking at a +historical section of timeline (i.e. `/messages`), you want to see the best +representation of the state of the room as others were seeing it at the time. + + +## Forward extremity + +Most-recent-in-time events in the DAG which are not referenced by any other events' `prev_events` yet. + +The forward extremities of a room are used as the `prev_events` when the next event is sent. + + +## Backwards extremity + +The current marker of where we have backfilled up to and will generally be the +oldest-in-time events we know of in the DAG. + +This is an event where we haven't fetched all of the `prev_events` for. + +Once we have fetched all of its `prev_events`, it's unmarked as a backwards +extremity (although we may have formed new backwards extremities from the prev +events during the backfilling process). + + +## Outliers + +We mark an event as an `outlier` when we haven't figured out the state for the +room at that point in the DAG yet. + +We won't *necessarily* have the `prev_events` of an `outlier` in the database, +but it's entirely possible that we *might*. The status of whether we have all of +the `prev_events` is marked as a [backwards extremity](#backwards-extremity). + +For example, when we fetch the event auth chain or state for a given event, we +mark all of those claimed auth events as outliers because we haven't done the +state calculation ourself. + + +## State groups + +For every non-outlier event we need to know the state at that event. Instead of +storing the full state for each event in the DB (i.e. a `event_id -> state` +mapping), which is *very* space inefficient when state doesn't change, we +instead assign each different set of state a "state group" and then have +mappings of `event_id -> state_group` and `state_group -> state`. + + +### Stage group edges + +TODO: `state_group_edges` is a further optimization... + notes from @Azrenbeth, https://pastebin.com/seUGVGeT From a7bacccd8550b45fc1fa3dcff90f36125827b4ba Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Aug 2021 11:23:45 +0100 Subject: [PATCH 496/619] Extend the release script to tag and create the releases. (#10496) --- changelog.d/10496.misc | 1 + scripts-dev/release.py | 311 ++++++++++++++++++++++++++++++++++++----- setup.py | 2 + 3 files changed, 278 insertions(+), 36 deletions(-) create mode 100644 changelog.d/10496.misc diff --git a/changelog.d/10496.misc b/changelog.d/10496.misc new file mode 100644 index 0000000000..6d0d3e5391 --- /dev/null +++ b/changelog.d/10496.misc @@ -0,0 +1 @@ +Extend release script to also tag and create GitHub releases. diff --git a/scripts-dev/release.py b/scripts-dev/release.py index cff433af2a..e864dc6ed5 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -14,29 +14,57 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""An interactive script for doing a release. See `run()` below. +"""An interactive script for doing a release. See `cli()` below. """ +import re import subprocess import sys -from typing import Optional +import urllib.request +from os import path +from tempfile import TemporaryDirectory +from typing import List, Optional, Tuple +import attr import click +import commonmark import git +import redbaron +from click.exceptions import ClickException +from github import Github from packaging import version -from redbaron import RedBaron -@click.command() -def run(): - """An interactive script to walk through the initial stages of creating a - release, including creating release branch, updating changelog and pushing to - GitHub. +@click.group() +def cli(): + """An interactive script to walk through the parts of creating a release. Requires the dev dependencies be installed, which can be done via: pip install -e .[dev] + Then to use: + + ./scripts-dev/release.py prepare + + # ... ask others to look at the changelog ... + + ./scripts-dev/release.py tag + + # ... wait for asssets to build ... + + ./scripts-dev/release.py publish + ./scripts-dev/release.py upload + + If the env var GH_TOKEN (or GITHUB_TOKEN) is set, or passed into the + `tag`/`publish` command, then a new draft release will be created/published. + """ + + +@cli.command() +def prepare(): + """Do the initial stages of creating a release, including creating release + branch, updating changelog and pushing to GitHub. """ # Make sure we're in a git repo. @@ -51,32 +79,8 @@ def run(): click.secho("Updating git repo...") repo.remote().fetch() - # Parse the AST and load the `__version__` node so that we can edit it - # later. - with open("synapse/__init__.py") as f: - red = RedBaron(f.read()) - - version_node = None - for node in red: - if node.type != "assignment": - continue - - if node.target.type != "name": - continue - - if node.target.value != "__version__": - continue - - version_node = node - break - - if not version_node: - print("Failed to find '__version__' definition in synapse/__init__.py") - sys.exit(1) - - # Parse the current version. - current_version = version.parse(version_node.value.value.strip('"')) - assert isinstance(current_version, version.Version) + # Get the current version and AST from root Synapse module. + current_version, parsed_synapse_ast, version_node = parse_version_from_module() # Figure out what sort of release we're doing and calcuate the new version. rc = click.confirm("RC", default=True) @@ -190,7 +194,7 @@ def run(): # Update the `__version__` variable and write it back to the file. version_node.value = '"' + new_version + '"' with open("synapse/__init__.py", "w") as f: - f.write(red.dumps()) + f.write(parsed_synapse_ast.dumps()) # Generate changelogs subprocess.run("python3 -m towncrier", shell=True) @@ -240,6 +244,180 @@ def run(): ) +@cli.command() +@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"]) +def tag(gh_token: Optional[str]): + """Tags the release and generates a draft GitHub release""" + + # Make sure we're in a git repo. + try: + repo = git.Repo() + except git.InvalidGitRepositoryError: + raise click.ClickException("Not in Synapse repo.") + + if repo.is_dirty(): + raise click.ClickException("Uncommitted changes exist.") + + click.secho("Updating git repo...") + repo.remote().fetch() + + # Find out the version and tag name. + current_version, _, _ = parse_version_from_module() + tag_name = f"v{current_version}" + + # Check we haven't released this version. + if tag_name in repo.tags: + raise click.ClickException(f"Tag {tag_name} already exists!\n") + + # Get the appropriate changelogs and tag. + changes = get_changes_for_version(current_version) + + click.echo_via_pager(changes) + if click.confirm("Edit text?", default=False): + changes = click.edit(changes, require_save=False) + + repo.create_tag(tag_name, message=changes) + + if not click.confirm("Push tag to GitHub?", default=True): + print("") + print("Run when ready to push:") + print("") + print(f"\tgit push {repo.remote().name} tag {current_version}") + print("") + return + + repo.git.push(repo.remote().name, "tag", tag_name) + + # If no token was given, we bail here + if not gh_token: + click.launch(f"https://github.com/matrix-org/synapse/releases/edit/{tag_name}") + return + + # Create a new draft release + gh = Github(gh_token) + gh_repo = gh.get_repo("matrix-org/synapse") + release = gh_repo.create_git_release( + tag=tag_name, + name=tag_name, + message=changes, + draft=True, + prerelease=current_version.is_prerelease, + ) + + # Open the release and the actions where we are building the assets. + click.launch(release.url) + click.launch( + f"https://github.com/matrix-org/synapse/actions?query=branch%3A{tag_name}" + ) + + click.echo("Wait for release assets to be built") + + +@cli.command() +@click.option("--gh-token", envvar=["GH_TOKEN", "GITHUB_TOKEN"], required=True) +def publish(gh_token: str): + """Publish release.""" + + # Make sure we're in a git repo. + try: + repo = git.Repo() + except git.InvalidGitRepositoryError: + raise click.ClickException("Not in Synapse repo.") + + if repo.is_dirty(): + raise click.ClickException("Uncommitted changes exist.") + + current_version, _, _ = parse_version_from_module() + tag_name = f"v{current_version}" + + if not click.confirm(f"Publish {tag_name}?", default=True): + return + + # Publish the draft release + gh = Github(gh_token) + gh_repo = gh.get_repo("matrix-org/synapse") + for release in gh_repo.get_releases(): + if release.title == tag_name: + break + else: + raise ClickException(f"Failed to find GitHub release for {tag_name}") + + assert release.title == tag_name + + if not release.draft: + click.echo("Release already published.") + return + + release = release.update_release( + name=release.title, + message=release.body, + tag_name=release.tag_name, + prerelease=release.prerelease, + draft=False, + ) + + +@cli.command() +def upload(): + """Upload release to pypi.""" + + current_version, _, _ = parse_version_from_module() + tag_name = f"v{current_version}" + + pypi_asset_names = [ + f"matrix_synapse-{current_version}-py3-none-any.whl", + f"matrix-synapse-{current_version}.tar.gz", + ] + + with TemporaryDirectory(prefix=f"synapse_upload_{tag_name}_") as tmpdir: + for name in pypi_asset_names: + filename = path.join(tmpdir, name) + url = f"https://github.com/matrix-org/synapse/releases/download/{tag_name}/{name}" + + click.echo(f"Downloading {name} into {filename}") + urllib.request.urlretrieve(url, filename=filename) + + if click.confirm("Upload to PyPI?", default=True): + subprocess.run("twine upload *", shell=True, cwd=tmpdir) + + click.echo( + f"Done! Remember to merge the tag {tag_name} into the appropriate branches" + ) + + +def parse_version_from_module() -> Tuple[ + version.Version, redbaron.RedBaron, redbaron.Node +]: + # Parse the AST and load the `__version__` node so that we can edit it + # later. + with open("synapse/__init__.py") as f: + red = redbaron.RedBaron(f.read()) + + version_node = None + for node in red: + if node.type != "assignment": + continue + + if node.target.type != "name": + continue + + if node.target.value != "__version__": + continue + + version_node = node + break + + if not version_node: + print("Failed to find '__version__' definition in synapse/__init__.py") + sys.exit(1) + + # Parse the current version. + current_version = version.parse(version_node.value.value.strip('"')) + assert isinstance(current_version, version.Version) + + return current_version, red, version_node + + def find_ref(repo: git.Repo, ref_name: str) -> Optional[git.HEAD]: """Find the branch/ref, looking first locally then in the remote.""" if ref_name in repo.refs: @@ -256,5 +434,66 @@ def update_branch(repo: git.Repo): repo.git.merge(repo.active_branch.tracking_branch().name) +def get_changes_for_version(wanted_version: version.Version) -> str: + """Get the changelogs for the given version. + + If an RC then will only get the changelog for that RC version, otherwise if + its a full release will get the changelog for the release and all its RCs. + """ + + with open("CHANGES.md") as f: + changes = f.read() + + # First we parse the changelog so that we can split it into sections based + # on the release headings. + ast = commonmark.Parser().parse(changes) + + @attr.s(auto_attribs=True) + class VersionSection: + title: str + + # These are 0-based. + start_line: int + end_line: Optional[int] = None # Is none if its the last entry + + headings: List[VersionSection] = [] + for node, _ in ast.walker(): + # We look for all text nodes that are in a level 1 heading. + if node.t != "text": + continue + + if node.parent.t != "heading" or node.parent.level != 1: + continue + + # If we have a previous heading then we update its `end_line`. + if headings: + headings[-1].end_line = node.parent.sourcepos[0][0] - 1 + + headings.append(VersionSection(node.literal, node.parent.sourcepos[0][0] - 1)) + + changes_by_line = changes.split("\n") + + version_changelog = [] # The lines we want to include in the changelog + + # Go through each section and find any that match the requested version. + regex = re.compile(r"^Synapse v?(\S+)") + for section in headings: + groups = regex.match(section.title) + if not groups: + continue + + heading_version = version.parse(groups.group(1)) + heading_base_version = version.parse(heading_version.base_version) + + # Check if heading version matches the requested version, or if its an + # RC of the requested version. + if wanted_version not in (heading_version, heading_base_version): + continue + + version_changelog.extend(changes_by_line[section.start_line : section.end_line]) + + return "\n".join(version_changelog) + + if __name__ == "__main__": - run() + cli() diff --git a/setup.py b/setup.py index 1081548e00..c478563510 100755 --- a/setup.py +++ b/setup.py @@ -108,6 +108,8 @@ def exec_file(path_segments): "click==7.1.2", "redbaron==0.9.2", "GitPython==3.1.14", + "commonmark==0.9.1", + "pygithub==1.55", ] CONDITIONAL_REQUIREMENTS["mypy"] = ["mypy==0.812", "mypy-zope==0.2.13"] From f4ac934afe1d91bb0cc1bb6dafc77a94165aa740 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 3 Aug 2021 11:30:39 +0100 Subject: [PATCH 497/619] Revert use of PeriodicallyFlushingMemoryHandler by default (#10515) --- changelog.d/10515.feature | 1 + docs/sample_log_config.yaml | 5 +---- synapse/config/logger.py | 5 +---- 3 files changed, 3 insertions(+), 8 deletions(-) create mode 100644 changelog.d/10515.feature diff --git a/changelog.d/10515.feature b/changelog.d/10515.feature new file mode 100644 index 0000000000..db277d9ecd --- /dev/null +++ b/changelog.d/10515.feature @@ -0,0 +1 @@ +Add a buffered logging handler which periodically flushes itself. diff --git a/docs/sample_log_config.yaml b/docs/sample_log_config.yaml index b088c83405..669e600081 100644 --- a/docs/sample_log_config.yaml +++ b/docs/sample_log_config.yaml @@ -28,7 +28,7 @@ handlers: # will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR # logs will still be flushed immediately. buffer: - class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler + class: logging.handlers.MemoryHandler target: file # The capacity is the number of log lines that are buffered before # being written to disk. Increasing this will lead to better @@ -36,9 +36,6 @@ handlers: # be written to disk. capacity: 10 flushLevel: 30 # Flush for WARNING logs as well - # The period of time, in seconds, between forced flushes. - # Messages will not be delayed for longer than this time. - period: 5 # A handler that writes logs to stderr. Unused by default, but can be used # instead of "buffer" and "file" in the logger handlers. diff --git a/synapse/config/logger.py b/synapse/config/logger.py index dcd3ed1dac..ad4e6e61c3 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -71,7 +71,7 @@ # will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR # logs will still be flushed immediately. buffer: - class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler + class: logging.handlers.MemoryHandler target: file # The capacity is the number of log lines that are buffered before # being written to disk. Increasing this will lead to better @@ -79,9 +79,6 @@ # be written to disk. capacity: 10 flushLevel: 30 # Flush for WARNING logs as well - # The period of time, in seconds, between forced flushes. - # Messages will not be delayed for longer than this time. - period: 5 # A handler that writes logs to stderr. Unused by default, but can be used # instead of "buffer" and "file" in the logger handlers. From c8566191fcd7edf224db468f860c1b638fb8e763 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Aug 2021 11:32:10 +0100 Subject: [PATCH 498/619] 1.40.0rc1 --- CHANGES.md | 60 +++++++++++++++++++++++++++++++++++++++ changelog.d/10245.feature | 1 - changelog.d/10254.feature | 1 - changelog.d/10283.feature | 1 - changelog.d/10390.misc | 1 - changelog.d/10407.feature | 1 - changelog.d/10408.misc | 1 - changelog.d/10410.bugfix | 1 - changelog.d/10411.feature | 1 - changelog.d/10413.feature | 1 - changelog.d/10415.misc | 1 - changelog.d/10426.feature | 1 - changelog.d/10429.misc | 1 - changelog.d/10431.misc | 1 - changelog.d/10432.misc | 1 - changelog.d/10437.misc | 1 - changelog.d/10438.misc | 1 - changelog.d/10439.bugfix | 1 - changelog.d/10440.feature | 1 - changelog.d/10442.misc | 1 - changelog.d/10444.misc | 1 - changelog.d/10445.doc | 1 - changelog.d/10446.misc | 1 - changelog.d/10447.feature | 1 - changelog.d/10448.feature | 1 - changelog.d/10450.misc | 1 - changelog.d/10451.misc | 1 - changelog.d/10453.doc | 1 - changelog.d/10455.bugfix | 1 - changelog.d/10463.misc | 1 - changelog.d/10464.doc | 1 - changelog.d/10468.misc | 1 - changelog.d/10482.misc | 1 - changelog.d/10483.doc | 1 - changelog.d/10488.misc | 1 - changelog.d/10489.feature | 1 - changelog.d/10490.misc | 1 - changelog.d/10491.misc | 1 - changelog.d/10496.misc | 1 - changelog.d/10499.bugfix | 1 - changelog.d/10500.misc | 1 - changelog.d/10511.feature | 1 - changelog.d/10512.misc | 1 - changelog.d/10515.feature | 1 - changelog.d/9918.feature | 1 - debian/changelog | 8 ++++-- synapse/__init__.py | 2 +- 47 files changed, 67 insertions(+), 47 deletions(-) delete mode 100644 changelog.d/10245.feature delete mode 100644 changelog.d/10254.feature delete mode 100644 changelog.d/10283.feature delete mode 100644 changelog.d/10390.misc delete mode 100644 changelog.d/10407.feature delete mode 100644 changelog.d/10408.misc delete mode 100644 changelog.d/10410.bugfix delete mode 100644 changelog.d/10411.feature delete mode 100644 changelog.d/10413.feature delete mode 100644 changelog.d/10415.misc delete mode 100644 changelog.d/10426.feature delete mode 100644 changelog.d/10429.misc delete mode 100644 changelog.d/10431.misc delete mode 100644 changelog.d/10432.misc delete mode 100644 changelog.d/10437.misc delete mode 100644 changelog.d/10438.misc delete mode 100644 changelog.d/10439.bugfix delete mode 100644 changelog.d/10440.feature delete mode 100644 changelog.d/10442.misc delete mode 100644 changelog.d/10444.misc delete mode 100644 changelog.d/10445.doc delete mode 100644 changelog.d/10446.misc delete mode 100644 changelog.d/10447.feature delete mode 100644 changelog.d/10448.feature delete mode 100644 changelog.d/10450.misc delete mode 100644 changelog.d/10451.misc delete mode 100644 changelog.d/10453.doc delete mode 100644 changelog.d/10455.bugfix delete mode 100644 changelog.d/10463.misc delete mode 100644 changelog.d/10464.doc delete mode 100644 changelog.d/10468.misc delete mode 100644 changelog.d/10482.misc delete mode 100644 changelog.d/10483.doc delete mode 100644 changelog.d/10488.misc delete mode 100644 changelog.d/10489.feature delete mode 100644 changelog.d/10490.misc delete mode 100644 changelog.d/10491.misc delete mode 100644 changelog.d/10496.misc delete mode 100644 changelog.d/10499.bugfix delete mode 100644 changelog.d/10500.misc delete mode 100644 changelog.d/10511.feature delete mode 100644 changelog.d/10512.misc delete mode 100644 changelog.d/10515.feature delete mode 100644 changelog.d/9918.feature diff --git a/CHANGES.md b/CHANGES.md index 6533249281..34274cfe9c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,63 @@ +Synapse 1.40.0rc1 (2021-08-03) +============================== + +Features +-------- + +- Add support for [MSC2033](https://github.com/matrix-org/matrix-doc/pull/2033): `device_id` on `/account/whoami`. ([\#9918](https://github.com/matrix-org/synapse/issues/9918)) +- Make historical events discoverable from backfill for servers without any scrollback history (part of MSC2716). ([\#10245](https://github.com/matrix-org/synapse/issues/10245)) +- Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. ([\#10254](https://github.com/matrix-org/synapse/issues/10254), [\#10447](https://github.com/matrix-org/synapse/issues/10447), [\#10489](https://github.com/matrix-org/synapse/issues/10489)) +- Initial support for MSC3244, Room version capabilities over the /capabilities API. ([\#10283](https://github.com/matrix-org/synapse/issues/10283)) +- Add a buffered logging handler which periodically flushes itself. ([\#10407](https://github.com/matrix-org/synapse/issues/10407), [\#10515](https://github.com/matrix-org/synapse/issues/10515)) +- Add support for https connections to a proxy server. Contributed by @Bubu and @dklimpel. ([\#10411](https://github.com/matrix-org/synapse/issues/10411)) +- Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). Contributed by @SimonBrandner. ([\#10413](https://github.com/matrix-org/synapse/issues/10413)) +- Email notifications now state whether an invitation is to a room or a space. ([\#10426](https://github.com/matrix-org/synapse/issues/10426)) +- Allow setting transaction limit for database connections. ([\#10440](https://github.com/matrix-org/synapse/issues/10440), [\#10511](https://github.com/matrix-org/synapse/issues/10511)) +- Add `creation_ts` to list users admin API. ([\#10448](https://github.com/matrix-org/synapse/issues/10448)) + + +Bugfixes +-------- + +- Improve character set detection in URL previews by supporting underscores (in addition to hyphens). Contributed by @srividyut. ([\#10410](https://github.com/matrix-org/synapse/issues/10410)) +- Fix events with floating outlier state being rejected over federation. ([\#10439](https://github.com/matrix-org/synapse/issues/10439)) +- Fix `synapse_federation_server_oldest_inbound_pdu_in_staging` Prometheus metric to not report a max age of 51 years when the queue is empty. ([\#10455](https://github.com/matrix-org/synapse/issues/10455)) +- Fix a bug which caused an explicit assignment of power-level 0 to a user to be misinterpreted in rare circumstances. ([\#10499](https://github.com/matrix-org/synapse/issues/10499)) + + +Improved Documentation +---------------------- + +- Fix hierarchy of providers on the OpenID page. ([\#10445](https://github.com/matrix-org/synapse/issues/10445)) +- Consolidate development documentation to `docs/development/`. ([\#10453](https://github.com/matrix-org/synapse/issues/10453)) +- Add some developer docs to explain room DAG concepts like `outliers`, `state_groups`, `depth`, etc. ([\#10464](https://github.com/matrix-org/synapse/issues/10464)) +- Document how to use Complement while developing a new Synapse feature. ([\#10483](https://github.com/matrix-org/synapse/issues/10483)) + + +Internal Changes +---------------- + +- Prune inbound federation inbound queues for a room if they get too large. ([\#10390](https://github.com/matrix-org/synapse/issues/10390)) +- Add type hints to `synapse.federation.transport.client` module. ([\#10408](https://github.com/matrix-org/synapse/issues/10408)) +- Remove shebang line from module files. ([\#10415](https://github.com/matrix-org/synapse/issues/10415)) +- Drop backwards-compatibility code that was required to support Ubuntu Xenial. ([\#10429](https://github.com/matrix-org/synapse/issues/10429)) +- Use a docker image cache for the prerequisites for the debian package build. ([\#10431](https://github.com/matrix-org/synapse/issues/10431)) +- Connect historical chunks together with chunk events instead of a content field (MSC2716). ([\#10432](https://github.com/matrix-org/synapse/issues/10432)) +- Improve servlet type hints. ([\#10437](https://github.com/matrix-org/synapse/issues/10437), [\#10438](https://github.com/matrix-org/synapse/issues/10438)) +- Replace usage of `or_ignore` in `simple_insert` with `simple_upsert` usage, to stop spamming postgres logs with spurious ERROR messages. ([\#10442](https://github.com/matrix-org/synapse/issues/10442)) +- Update the `tests-done` Github Actions status. ([\#10444](https://github.com/matrix-org/synapse/issues/10444), [\#10512](https://github.com/matrix-org/synapse/issues/10512)) +- Update type annotations to work with forthcoming Twisted 21.7.0 release. ([\#10446](https://github.com/matrix-org/synapse/issues/10446), [\#10450](https://github.com/matrix-org/synapse/issues/10450)) +- Cancel redundant GHA workflows when a new commit is pushed. ([\#10451](https://github.com/matrix-org/synapse/issues/10451)) +- Disable `msc2716` Complement tests until Complement updates are merged. ([\#10463](https://github.com/matrix-org/synapse/issues/10463)) +- Mitigate media repo XSS attacks on IE11 via the non-standard X-Content-Security-Policy header. ([\#10468](https://github.com/matrix-org/synapse/issues/10468)) +- Additional type hints in the state handler. ([\#10482](https://github.com/matrix-org/synapse/issues/10482)) +- Update syntax used to run complement tests. ([\#10488](https://github.com/matrix-org/synapse/issues/10488)) +- Fix up type annotations to work with Twisted 21.7. ([\#10490](https://github.com/matrix-org/synapse/issues/10490)) +- Improve type annotations for `ObservableDeferred`. ([\#10491](https://github.com/matrix-org/synapse/issues/10491)) +- Extend release script to also tag and create GitHub releases. ([\#10496](https://github.com/matrix-org/synapse/issues/10496)) +- Fix a bug which caused production debian packages to be incorrectly marked as 'prerelease'. ([\#10500](https://github.com/matrix-org/synapse/issues/10500)) + + Synapse 1.39.0 (2021-07-29) =========================== diff --git a/changelog.d/10245.feature b/changelog.d/10245.feature deleted file mode 100644 index b3c48cc2cc..0000000000 --- a/changelog.d/10245.feature +++ /dev/null @@ -1 +0,0 @@ -Make historical events discoverable from backfill for servers without any scrollback history (part of MSC2716). diff --git a/changelog.d/10254.feature b/changelog.d/10254.feature deleted file mode 100644 index df8bb51167..0000000000 --- a/changelog.d/10254.feature +++ /dev/null @@ -1 +0,0 @@ -Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. diff --git a/changelog.d/10283.feature b/changelog.d/10283.feature deleted file mode 100644 index 99d633dbfb..0000000000 --- a/changelog.d/10283.feature +++ /dev/null @@ -1 +0,0 @@ -Initial support for MSC3244, Room version capabilities over the /capabilities API. \ No newline at end of file diff --git a/changelog.d/10390.misc b/changelog.d/10390.misc deleted file mode 100644 index 911a5733ee..0000000000 --- a/changelog.d/10390.misc +++ /dev/null @@ -1 +0,0 @@ -Prune inbound federation inbound queues for a room if they get too large. diff --git a/changelog.d/10407.feature b/changelog.d/10407.feature deleted file mode 100644 index db277d9ecd..0000000000 --- a/changelog.d/10407.feature +++ /dev/null @@ -1 +0,0 @@ -Add a buffered logging handler which periodically flushes itself. diff --git a/changelog.d/10408.misc b/changelog.d/10408.misc deleted file mode 100644 index abccd210a9..0000000000 --- a/changelog.d/10408.misc +++ /dev/null @@ -1 +0,0 @@ -Add type hints to `synapse.federation.transport.client` module. diff --git a/changelog.d/10410.bugfix b/changelog.d/10410.bugfix deleted file mode 100644 index 65b418fd35..0000000000 --- a/changelog.d/10410.bugfix +++ /dev/null @@ -1 +0,0 @@ -Improve character set detection in URL previews by supporting underscores (in addition to hyphens). Contributed by @srividyut. diff --git a/changelog.d/10411.feature b/changelog.d/10411.feature deleted file mode 100644 index ef0ab84b17..0000000000 --- a/changelog.d/10411.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for https connections to a proxy server. Contributed by @Bubu and @dklimpel. \ No newline at end of file diff --git a/changelog.d/10413.feature b/changelog.d/10413.feature deleted file mode 100644 index 3964db7e0e..0000000000 --- a/changelog.d/10413.feature +++ /dev/null @@ -1 +0,0 @@ -Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). Contributed by @SimonBrandner. diff --git a/changelog.d/10415.misc b/changelog.d/10415.misc deleted file mode 100644 index 3b9501acbb..0000000000 --- a/changelog.d/10415.misc +++ /dev/null @@ -1 +0,0 @@ -Remove shebang line from module files. diff --git a/changelog.d/10426.feature b/changelog.d/10426.feature deleted file mode 100644 index 9cca6dc456..0000000000 --- a/changelog.d/10426.feature +++ /dev/null @@ -1 +0,0 @@ -Email notifications now state whether an invitation is to a room or a space. diff --git a/changelog.d/10429.misc b/changelog.d/10429.misc deleted file mode 100644 index ccb2217f64..0000000000 --- a/changelog.d/10429.misc +++ /dev/null @@ -1 +0,0 @@ -Drop backwards-compatibility code that was required to support Ubuntu Xenial. diff --git a/changelog.d/10431.misc b/changelog.d/10431.misc deleted file mode 100644 index 34b9b49da6..0000000000 --- a/changelog.d/10431.misc +++ /dev/null @@ -1 +0,0 @@ -Use a docker image cache for the prerequisites for the debian package build. diff --git a/changelog.d/10432.misc b/changelog.d/10432.misc deleted file mode 100644 index 3a8cdf0ae0..0000000000 --- a/changelog.d/10432.misc +++ /dev/null @@ -1 +0,0 @@ -Connect historical chunks together with chunk events instead of a content field (MSC2716). diff --git a/changelog.d/10437.misc b/changelog.d/10437.misc deleted file mode 100644 index a557578499..0000000000 --- a/changelog.d/10437.misc +++ /dev/null @@ -1 +0,0 @@ -Improve servlet type hints. diff --git a/changelog.d/10438.misc b/changelog.d/10438.misc deleted file mode 100644 index a557578499..0000000000 --- a/changelog.d/10438.misc +++ /dev/null @@ -1 +0,0 @@ -Improve servlet type hints. diff --git a/changelog.d/10439.bugfix b/changelog.d/10439.bugfix deleted file mode 100644 index 74e5a25126..0000000000 --- a/changelog.d/10439.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix events with floating outlier state being rejected over federation. diff --git a/changelog.d/10440.feature b/changelog.d/10440.feature deleted file mode 100644 index f1833b0bd7..0000000000 --- a/changelog.d/10440.feature +++ /dev/null @@ -1 +0,0 @@ -Allow setting transaction limit for database connections. diff --git a/changelog.d/10442.misc b/changelog.d/10442.misc deleted file mode 100644 index b8d412d732..0000000000 --- a/changelog.d/10442.misc +++ /dev/null @@ -1 +0,0 @@ -Replace usage of `or_ignore` in `simple_insert` with `simple_upsert` usage, to stop spamming postgres logs with spurious ERROR messages. diff --git a/changelog.d/10444.misc b/changelog.d/10444.misc deleted file mode 100644 index c012e89f4b..0000000000 --- a/changelog.d/10444.misc +++ /dev/null @@ -1 +0,0 @@ -Update the `tests-done` Github Actions status. diff --git a/changelog.d/10445.doc b/changelog.d/10445.doc deleted file mode 100644 index 4c023ded7c..0000000000 --- a/changelog.d/10445.doc +++ /dev/null @@ -1 +0,0 @@ -Fix hierarchy of providers on the OpenID page. diff --git a/changelog.d/10446.misc b/changelog.d/10446.misc deleted file mode 100644 index a5a0ca80eb..0000000000 --- a/changelog.d/10446.misc +++ /dev/null @@ -1 +0,0 @@ -Update type annotations to work with forthcoming Twisted 21.7.0 release. diff --git a/changelog.d/10447.feature b/changelog.d/10447.feature deleted file mode 100644 index df8bb51167..0000000000 --- a/changelog.d/10447.feature +++ /dev/null @@ -1 +0,0 @@ -Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. diff --git a/changelog.d/10448.feature b/changelog.d/10448.feature deleted file mode 100644 index f6579e0ca8..0000000000 --- a/changelog.d/10448.feature +++ /dev/null @@ -1 +0,0 @@ -Add `creation_ts` to list users admin API. \ No newline at end of file diff --git a/changelog.d/10450.misc b/changelog.d/10450.misc deleted file mode 100644 index aa646f0841..0000000000 --- a/changelog.d/10450.misc +++ /dev/null @@ -1 +0,0 @@ - Update type annotations to work with forthcoming Twisted 21.7.0 release. diff --git a/changelog.d/10451.misc b/changelog.d/10451.misc deleted file mode 100644 index e38f4b476d..0000000000 --- a/changelog.d/10451.misc +++ /dev/null @@ -1 +0,0 @@ -Cancel redundant GHA workflows when a new commit is pushed. diff --git a/changelog.d/10453.doc b/changelog.d/10453.doc deleted file mode 100644 index 5d4db9bca2..0000000000 --- a/changelog.d/10453.doc +++ /dev/null @@ -1 +0,0 @@ -Consolidate development documentation to `docs/development/`. diff --git a/changelog.d/10455.bugfix b/changelog.d/10455.bugfix deleted file mode 100644 index 23c74a3c89..0000000000 --- a/changelog.d/10455.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `synapse_federation_server_oldest_inbound_pdu_in_staging` Prometheus metric to not report a max age of 51 years when the queue is empty. diff --git a/changelog.d/10463.misc b/changelog.d/10463.misc deleted file mode 100644 index d7b4d2222e..0000000000 --- a/changelog.d/10463.misc +++ /dev/null @@ -1 +0,0 @@ -Disable `msc2716` Complement tests until Complement updates are merged. diff --git a/changelog.d/10464.doc b/changelog.d/10464.doc deleted file mode 100644 index 764fb9f65c..0000000000 --- a/changelog.d/10464.doc +++ /dev/null @@ -1 +0,0 @@ -Add some developer docs to explain room DAG concepts like `outliers`, `state_groups`, `depth`, etc. diff --git a/changelog.d/10468.misc b/changelog.d/10468.misc deleted file mode 100644 index b9854bb4c1..0000000000 --- a/changelog.d/10468.misc +++ /dev/null @@ -1 +0,0 @@ -Mitigate media repo XSS attacks on IE11 via the non-standard X-Content-Security-Policy header. diff --git a/changelog.d/10482.misc b/changelog.d/10482.misc deleted file mode 100644 index 4e9e2126e1..0000000000 --- a/changelog.d/10482.misc +++ /dev/null @@ -1 +0,0 @@ -Additional type hints in the state handler. diff --git a/changelog.d/10483.doc b/changelog.d/10483.doc deleted file mode 100644 index 0f699fafdd..0000000000 --- a/changelog.d/10483.doc +++ /dev/null @@ -1 +0,0 @@ -Document how to use Complement while developing a new Synapse feature. diff --git a/changelog.d/10488.misc b/changelog.d/10488.misc deleted file mode 100644 index a55502c163..0000000000 --- a/changelog.d/10488.misc +++ /dev/null @@ -1 +0,0 @@ -Update syntax used to run complement tests. diff --git a/changelog.d/10489.feature b/changelog.d/10489.feature deleted file mode 100644 index df8bb51167..0000000000 --- a/changelog.d/10489.feature +++ /dev/null @@ -1 +0,0 @@ -Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. diff --git a/changelog.d/10490.misc b/changelog.d/10490.misc deleted file mode 100644 index 630c31adae..0000000000 --- a/changelog.d/10490.misc +++ /dev/null @@ -1 +0,0 @@ -Fix up type annotations to work with Twisted 21.7. diff --git a/changelog.d/10491.misc b/changelog.d/10491.misc deleted file mode 100644 index 3867cf2682..0000000000 --- a/changelog.d/10491.misc +++ /dev/null @@ -1 +0,0 @@ -Improve type annotations for `ObservableDeferred`. diff --git a/changelog.d/10496.misc b/changelog.d/10496.misc deleted file mode 100644 index 6d0d3e5391..0000000000 --- a/changelog.d/10496.misc +++ /dev/null @@ -1 +0,0 @@ -Extend release script to also tag and create GitHub releases. diff --git a/changelog.d/10499.bugfix b/changelog.d/10499.bugfix deleted file mode 100644 index 6487af6c96..0000000000 --- a/changelog.d/10499.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which caused an explicit assignment of power-level 0 to a user to be misinterpreted in rare circumstances. diff --git a/changelog.d/10500.misc b/changelog.d/10500.misc deleted file mode 100644 index dbaff57364..0000000000 --- a/changelog.d/10500.misc +++ /dev/null @@ -1 +0,0 @@ -Fix a bug which caused production debian packages to be incorrectly marked as 'prerelease'. diff --git a/changelog.d/10511.feature b/changelog.d/10511.feature deleted file mode 100644 index f1833b0bd7..0000000000 --- a/changelog.d/10511.feature +++ /dev/null @@ -1 +0,0 @@ -Allow setting transaction limit for database connections. diff --git a/changelog.d/10512.misc b/changelog.d/10512.misc deleted file mode 100644 index c012e89f4b..0000000000 --- a/changelog.d/10512.misc +++ /dev/null @@ -1 +0,0 @@ -Update the `tests-done` Github Actions status. diff --git a/changelog.d/10515.feature b/changelog.d/10515.feature deleted file mode 100644 index db277d9ecd..0000000000 --- a/changelog.d/10515.feature +++ /dev/null @@ -1 +0,0 @@ -Add a buffered logging handler which periodically flushes itself. diff --git a/changelog.d/9918.feature b/changelog.d/9918.feature deleted file mode 100644 index 98f0a50893..0000000000 --- a/changelog.d/9918.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for [MSC2033](https://github.com/matrix-org/matrix-doc/pull/2033): `device_id` on `/account/whoami`. \ No newline at end of file diff --git a/debian/changelog b/debian/changelog index 341c1ac992..f0557c35ef 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,12 @@ -matrix-synapse-py3 (1.39.0ubuntu1) UNRELEASED; urgency=medium +matrix-synapse-py3 (1.40.0~rc1) stable; urgency=medium + [ Richard van der Hoff ] * Drop backwards-compatibility code that was required to support Ubuntu Xenial. - -- Richard van der Hoff Tue, 20 Jul 2021 00:10:03 +0100 + [ Synapse Packaging team ] + * New synapse release 1.40.0~rc1. + + -- Synapse Packaging team Tue, 03 Aug 2021 11:31:49 +0100 matrix-synapse-py3 (1.39.0) stable; urgency=medium diff --git a/synapse/__init__.py b/synapse/__init__.py index 5da6c924fc..d6c1765508 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.39.0" +__version__ = "1.40.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From c80ec5d15386a8d3db03d0064b4e87d52d38dff2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Aug 2021 11:48:48 +0100 Subject: [PATCH 499/619] Fixup changelog --- CHANGES.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 34274cfe9c..8b78fe92f6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,22 +5,22 @@ Features -------- - Add support for [MSC2033](https://github.com/matrix-org/matrix-doc/pull/2033): `device_id` on `/account/whoami`. ([\#9918](https://github.com/matrix-org/synapse/issues/9918)) -- Make historical events discoverable from backfill for servers without any scrollback history (part of MSC2716). ([\#10245](https://github.com/matrix-org/synapse/issues/10245)) +- Make historical events discoverable from backfill for servers without any scrollback history (part of [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#10245](https://github.com/matrix-org/synapse/issues/10245)) - Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. ([\#10254](https://github.com/matrix-org/synapse/issues/10254), [\#10447](https://github.com/matrix-org/synapse/issues/10447), [\#10489](https://github.com/matrix-org/synapse/issues/10489)) -- Initial support for MSC3244, Room version capabilities over the /capabilities API. ([\#10283](https://github.com/matrix-org/synapse/issues/10283)) +- Initial support for [MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244), Room version capabilities over the /capabilities API. ([\#10283](https://github.com/matrix-org/synapse/issues/10283)) - Add a buffered logging handler which periodically flushes itself. ([\#10407](https://github.com/matrix-org/synapse/issues/10407), [\#10515](https://github.com/matrix-org/synapse/issues/10515)) - Add support for https connections to a proxy server. Contributed by @Bubu and @dklimpel. ([\#10411](https://github.com/matrix-org/synapse/issues/10411)) - Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). Contributed by @SimonBrandner. ([\#10413](https://github.com/matrix-org/synapse/issues/10413)) - Email notifications now state whether an invitation is to a room or a space. ([\#10426](https://github.com/matrix-org/synapse/issues/10426)) - Allow setting transaction limit for database connections. ([\#10440](https://github.com/matrix-org/synapse/issues/10440), [\#10511](https://github.com/matrix-org/synapse/issues/10511)) -- Add `creation_ts` to list users admin API. ([\#10448](https://github.com/matrix-org/synapse/issues/10448)) +- Add `creation_ts` to "list users" admin API. ([\#10448](https://github.com/matrix-org/synapse/issues/10448)) Bugfixes -------- - Improve character set detection in URL previews by supporting underscores (in addition to hyphens). Contributed by @srividyut. ([\#10410](https://github.com/matrix-org/synapse/issues/10410)) -- Fix events with floating outlier state being rejected over federation. ([\#10439](https://github.com/matrix-org/synapse/issues/10439)) +- Fix events being incorrectly rejected over federation if they reference auth events that the server needed to fetch. ([\#10439](https://github.com/matrix-org/synapse/issues/10439)) - Fix `synapse_federation_server_oldest_inbound_pdu_in_staging` Prometheus metric to not report a max age of 51 years when the queue is empty. ([\#10455](https://github.com/matrix-org/synapse/issues/10455)) - Fix a bug which caused an explicit assignment of power-level 0 to a user to be misinterpreted in rare circumstances. ([\#10499](https://github.com/matrix-org/synapse/issues/10499)) @@ -37,12 +37,12 @@ Improved Documentation Internal Changes ---------------- -- Prune inbound federation inbound queues for a room if they get too large. ([\#10390](https://github.com/matrix-org/synapse/issues/10390)) +- Prune inbound federation queues for a room if they get too large. ([\#10390](https://github.com/matrix-org/synapse/issues/10390)) - Add type hints to `synapse.federation.transport.client` module. ([\#10408](https://github.com/matrix-org/synapse/issues/10408)) - Remove shebang line from module files. ([\#10415](https://github.com/matrix-org/synapse/issues/10415)) - Drop backwards-compatibility code that was required to support Ubuntu Xenial. ([\#10429](https://github.com/matrix-org/synapse/issues/10429)) - Use a docker image cache for the prerequisites for the debian package build. ([\#10431](https://github.com/matrix-org/synapse/issues/10431)) -- Connect historical chunks together with chunk events instead of a content field (MSC2716). ([\#10432](https://github.com/matrix-org/synapse/issues/10432)) +- Connect historical chunks together with chunk events instead of a content field ([MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#10432](https://github.com/matrix-org/synapse/issues/10432)) - Improve servlet type hints. ([\#10437](https://github.com/matrix-org/synapse/issues/10437), [\#10438](https://github.com/matrix-org/synapse/issues/10438)) - Replace usage of `or_ignore` in `simple_insert` with `simple_upsert` usage, to stop spamming postgres logs with spurious ERROR messages. ([\#10442](https://github.com/matrix-org/synapse/issues/10442)) - Update the `tests-done` Github Actions status. ([\#10444](https://github.com/matrix-org/synapse/issues/10444), [\#10512](https://github.com/matrix-org/synapse/issues/10512)) From da6cd82106636d4c8b5143d7c2839f11fb40fbd2 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Aug 2021 12:11:26 +0100 Subject: [PATCH 500/619] Fixup changelog --- CHANGES.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8b78fe92f6..f2d6945886 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features -------- - Add support for [MSC2033](https://github.com/matrix-org/matrix-doc/pull/2033): `device_id` on `/account/whoami`. ([\#9918](https://github.com/matrix-org/synapse/issues/9918)) -- Make historical events discoverable from backfill for servers without any scrollback history (part of [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#10245](https://github.com/matrix-org/synapse/issues/10245)) +- Add support for [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716). ([\#10245](https://github.com/matrix-org/synapse/issues/10245), [\#10432](https://github.com/matrix-org/synapse/issues/10432), [\#10463](https://github.com/matrix-org/synapse/issues/10463)) - Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. ([\#10254](https://github.com/matrix-org/synapse/issues/10254), [\#10447](https://github.com/matrix-org/synapse/issues/10447), [\#10489](https://github.com/matrix-org/synapse/issues/10489)) - Initial support for [MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244), Room version capabilities over the /capabilities API. ([\#10283](https://github.com/matrix-org/synapse/issues/10283)) - Add a buffered logging handler which periodically flushes itself. ([\#10407](https://github.com/matrix-org/synapse/issues/10407), [\#10515](https://github.com/matrix-org/synapse/issues/10515)) @@ -42,13 +42,11 @@ Internal Changes - Remove shebang line from module files. ([\#10415](https://github.com/matrix-org/synapse/issues/10415)) - Drop backwards-compatibility code that was required to support Ubuntu Xenial. ([\#10429](https://github.com/matrix-org/synapse/issues/10429)) - Use a docker image cache for the prerequisites for the debian package build. ([\#10431](https://github.com/matrix-org/synapse/issues/10431)) -- Connect historical chunks together with chunk events instead of a content field ([MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#10432](https://github.com/matrix-org/synapse/issues/10432)) - Improve servlet type hints. ([\#10437](https://github.com/matrix-org/synapse/issues/10437), [\#10438](https://github.com/matrix-org/synapse/issues/10438)) - Replace usage of `or_ignore` in `simple_insert` with `simple_upsert` usage, to stop spamming postgres logs with spurious ERROR messages. ([\#10442](https://github.com/matrix-org/synapse/issues/10442)) - Update the `tests-done` Github Actions status. ([\#10444](https://github.com/matrix-org/synapse/issues/10444), [\#10512](https://github.com/matrix-org/synapse/issues/10512)) - Update type annotations to work with forthcoming Twisted 21.7.0 release. ([\#10446](https://github.com/matrix-org/synapse/issues/10446), [\#10450](https://github.com/matrix-org/synapse/issues/10450)) - Cancel redundant GHA workflows when a new commit is pushed. ([\#10451](https://github.com/matrix-org/synapse/issues/10451)) -- Disable `msc2716` Complement tests until Complement updates are merged. ([\#10463](https://github.com/matrix-org/synapse/issues/10463)) - Mitigate media repo XSS attacks on IE11 via the non-standard X-Content-Security-Policy header. ([\#10468](https://github.com/matrix-org/synapse/issues/10468)) - Additional type hints in the state handler. ([\#10482](https://github.com/matrix-org/synapse/issues/10482)) - Update syntax used to run complement tests. ([\#10488](https://github.com/matrix-org/synapse/issues/10488)) From 42225aa421efa9dd87fc63286f24f4697e4d2572 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Aug 2021 12:12:50 +0100 Subject: [PATCH 501/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f2d6945886..7ce28c4c18 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Features -------- - Add support for [MSC2033](https://github.com/matrix-org/matrix-doc/pull/2033): `device_id` on `/account/whoami`. ([\#9918](https://github.com/matrix-org/synapse/issues/9918)) -- Add support for [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716). ([\#10245](https://github.com/matrix-org/synapse/issues/10245), [\#10432](https://github.com/matrix-org/synapse/issues/10432), [\#10463](https://github.com/matrix-org/synapse/issues/10463)) +- Update support for [MSC2716 - Incrementally importing history into existing rooms](https://github.com/matrix-org/matrix-doc/pull/2716). ([\#10245](https://github.com/matrix-org/synapse/issues/10245), [\#10432](https://github.com/matrix-org/synapse/issues/10432), [\#10463](https://github.com/matrix-org/synapse/issues/10463)) - Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. ([\#10254](https://github.com/matrix-org/synapse/issues/10254), [\#10447](https://github.com/matrix-org/synapse/issues/10447), [\#10489](https://github.com/matrix-org/synapse/issues/10489)) - Initial support for [MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244), Room version capabilities over the /capabilities API. ([\#10283](https://github.com/matrix-org/synapse/issues/10283)) - Add a buffered logging handler which periodically flushes itself. ([\#10407](https://github.com/matrix-org/synapse/issues/10407), [\#10515](https://github.com/matrix-org/synapse/issues/10515)) From 6878e1065308caf0f79e380b4de1433ab1487a34 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Aug 2021 13:29:17 +0100 Subject: [PATCH 502/619] Fix release script URL (#10516) --- changelog.d/10516.misc | 1 + scripts-dev/release.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10516.misc diff --git a/changelog.d/10516.misc b/changelog.d/10516.misc new file mode 100644 index 0000000000..4d8c5e4805 --- /dev/null +++ b/changelog.d/10516.misc @@ -0,0 +1 @@ +Fix release script to open correct URL for the release. diff --git a/scripts-dev/release.py b/scripts-dev/release.py index e864dc6ed5..a339260c43 100755 --- a/scripts-dev/release.py +++ b/scripts-dev/release.py @@ -305,7 +305,7 @@ def tag(gh_token: Optional[str]): ) # Open the release and the actions where we are building the assets. - click.launch(release.url) + click.launch(release.html_url) click.launch( f"https://github.com/matrix-org/synapse/actions?query=branch%3A{tag_name}" ) From 903db99ed552d06f0a9e0379e55e655c5761355b Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 3 Aug 2021 14:28:30 +0100 Subject: [PATCH 503/619] Fix PeriodicallyFlushingMemoryHandler inhibiting application shutdown (#10517) --- changelog.d/10517.bugfix | 1 + synapse/logging/handlers.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/10517.bugfix diff --git a/changelog.d/10517.bugfix b/changelog.d/10517.bugfix new file mode 100644 index 0000000000..5b044bb34d --- /dev/null +++ b/changelog.d/10517.bugfix @@ -0,0 +1 @@ +Fix the `PeriodicallyFlushingMemoryHandler` inhibiting application shutdown because of its background thread. diff --git a/synapse/logging/handlers.py b/synapse/logging/handlers.py index a6c212f300..af5fc407a8 100644 --- a/synapse/logging/handlers.py +++ b/synapse/logging/handlers.py @@ -45,6 +45,7 @@ def __init__( self._flushing_thread: Thread = Thread( name="PeriodicallyFlushingMemoryHandler flushing thread", target=self._flush_periodically, + daemon=True, ) self._flushing_thread.start() From dc46f12725001dde99c536a9189045709cf7e06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dagfinn=20Ilmari=20Manns=C3=A5ker?= Date: Tue, 3 Aug 2021 14:35:49 +0100 Subject: [PATCH 504/619] Include room ID in ignored EDU log messages (#10507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dagfinn Ilmari Mannsåker --- changelog.d/10507.misc | 1 + synapse/handlers/receipts.py | 3 ++- synapse/handlers/typing.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10507.misc diff --git a/changelog.d/10507.misc b/changelog.d/10507.misc new file mode 100644 index 0000000000..5dfd116e60 --- /dev/null +++ b/changelog.d/10507.misc @@ -0,0 +1 @@ +Include room ID in ignored EDU log messages. Contributed by @ilmari. diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index b9085bbccb..5fd4525700 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -70,7 +70,8 @@ async def _received_remote_receipt(self, origin: str, content: JsonDict) -> None ) if not is_in_room: logger.info( - "Ignoring receipt from %s as we're not in the room", + "Ignoring receipt for room %r from server %s as we're not in the room", + room_id, origin, ) continue diff --git a/synapse/handlers/typing.py b/synapse/handlers/typing.py index 0cb651a400..a97c448595 100644 --- a/synapse/handlers/typing.py +++ b/synapse/handlers/typing.py @@ -335,7 +335,8 @@ async def _recv_edu(self, origin: str, content: JsonDict) -> None: ) if not is_in_room: logger.info( - "Ignoring typing update from %s as we're not in the room", + "Ignoring typing update for room %r from server %s as we're not in the room", + room_id, origin, ) return From 4b10880da363efed5d066191190237f1c64fddfd Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 3 Aug 2021 14:45:04 +0100 Subject: [PATCH 505/619] Make sync response cache time configurable. (#10513) --- changelog.d/10513.feature | 1 + docs/sample_config.yaml | 9 +++++++++ synapse/config/cache.py | 13 +++++++++++++ synapse/handlers/sync.py | 14 +++++++++++--- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10513.feature diff --git a/changelog.d/10513.feature b/changelog.d/10513.feature new file mode 100644 index 0000000000..153b2df7b2 --- /dev/null +++ b/changelog.d/10513.feature @@ -0,0 +1 @@ +Add a configuration setting for the time a `/sync` response is cached for. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 1a217f35db..a2efc14100 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -711,6 +711,15 @@ caches: # #expiry_time: 30m + # 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. + # + # By default, this is zero, which means that sync responses are not cached + # at all. + # + #sync_response_cache_duration: 2m + ## Database ## diff --git a/synapse/config/cache.py b/synapse/config/cache.py index 8d5f38b5d9..d119427ad8 100644 --- a/synapse/config/cache.py +++ b/synapse/config/cache.py @@ -151,6 +151,15 @@ def generate_config_section(self, **kwargs): # entries are never evicted based on time. # #expiry_time: 30m + + # 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. + # + # By default, this is zero, which means that sync responses are not cached + # at all. + # + #sync_response_cache_duration: 2m """ def read_config(self, config, **kwargs): @@ -212,6 +221,10 @@ def read_config(self, config, **kwargs): else: self.expiry_time_msec = None + 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() diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index f30bfcc93c..590642f510 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -269,14 +269,22 @@ def __init__(self, hs: "HomeServer"): self.presence_handler = hs.get_presence_handler() self.event_sources = hs.get_event_sources() self.clock = hs.get_clock() - self.response_cache: ResponseCache[SyncRequestKey] = ResponseCache( - hs.get_clock(), "sync" - ) self.state = hs.get_state_handler() self.auth = hs.get_auth() self.storage = hs.get_storage() self.state_store = self.storage.state + # TODO: flush cache entries on subsequent sync request. + # Once we get the next /sync request (ie, one with the same access token + # that sets 'since' to 'next_batch'), we know that device won't need a + # cached result any more, and we could flush the entry from the cache to save + # memory. + self.response_cache: ResponseCache[SyncRequestKey] = ResponseCache( + hs.get_clock(), + "sync", + timeout_ms=hs.config.caches.sync_response_cache_duration, + ) + # ExpiringCache((User, Device)) -> LruCache(user_id => event_id) self.lazy_loaded_members_cache: ExpiringCache[ Tuple[str, Optional[str]], LruCache[str, str] From 951648f26a75948a3b8de8989c98c51698043d71 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 3 Aug 2021 14:45:21 +0100 Subject: [PATCH 506/619] Fix debian package triggers (#10481) Replace the outdated list of dpkg triggers with an autogenerated one. --- debian/build_virtualenv | 15 +++++++++++++++ debian/changelog | 2 ++ debian/matrix-synapse-py3.triggers | 9 --------- 3 files changed, 17 insertions(+), 9 deletions(-) delete mode 100644 debian/matrix-synapse-py3.triggers diff --git a/debian/build_virtualenv b/debian/build_virtualenv index 68c8659953..801ecb9086 100755 --- a/debian/build_virtualenv +++ b/debian/build_virtualenv @@ -100,3 +100,18 @@ esac # add a dependency on the right version of python to substvars. PYPKG=`basename $SNAKE` echo "synapse:pydepends=$PYPKG" >> debian/matrix-synapse-py3.substvars + + +# add a couple of triggers. This is needed so that dh-virtualenv can rebuild +# the venv when the system python changes (see +# https://dh-virtualenv.readthedocs.io/en/latest/tutorial.html#step-2-set-up-packaging-for-your-project) +# +# we do it here rather than the more conventional way of just adding it to +# debian/matrix-synapse-py3.triggers, because we need to add a trigger on the +# right version of python. +cat >>"debian/.debhelper/generated/matrix-synapse-py3/triggers" < Date: Tue, 3 Aug 2021 11:13:34 -0700 Subject: [PATCH 507/619] Add warnings to ip_range_blacklist usage with proxies (#10129) Per issue #9812 using `url_preview_ip_range_blacklist` with a proxy via `HTTPS_PROXY` or `HTTP_PROXY` environment variables has some inconsistent bahavior than mentioned. This PR changes the following: - Changes the Sample Config file to include a note mentioning that `url_preview_ip_range_blacklist` and `ip_range_blacklist` is ignored when using a proxy - Changes some logic in synapse/config/repository.py to send a warning when both `*ip_range_blacklist` configs and a proxy environment variable are set and but no longer throws an error. Signed-off-by: Kento Okamoto --- changelog.d/10129.bugfix | 1 + docs/sample_config.yaml | 4 ++++ synapse/config/repository.py | 24 +++++++++++++++++++----- synapse/config/server.py | 2 ++ 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10129.bugfix diff --git a/changelog.d/10129.bugfix b/changelog.d/10129.bugfix new file mode 100644 index 0000000000..292676ec8d --- /dev/null +++ b/changelog.d/10129.bugfix @@ -0,0 +1 @@ +Add some clarification to the sample config file. Contributed by @Kentokamoto. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index a2efc14100..16843dd8c9 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -210,6 +210,8 @@ presence: # # This option replaces federation_ip_range_blacklist in Synapse v1.25.0. # +# Note: The value is ignored when an HTTP proxy is in use +# #ip_range_blacklist: # - '127.0.0.0/8' # - '10.0.0.0/8' @@ -972,6 +974,8 @@ media_store_path: "DATADIR/media_store" # This must be specified if url_preview_enabled is set. It is recommended that # you uncomment the following list as a starting point. # +# Note: The value is ignored when an HTTP proxy is in use +# #url_preview_ip_range_blacklist: # - '127.0.0.0/8' # - '10.0.0.0/8' diff --git a/synapse/config/repository.py b/synapse/config/repository.py index 0dfb3a227a..7481f3bf5f 100644 --- a/synapse/config/repository.py +++ b/synapse/config/repository.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import os from collections import namedtuple from typing import Dict, List +from urllib.request import getproxies_environment # type: ignore from synapse.config.server import DEFAULT_IP_RANGE_BLACKLIST, generate_ip_set from synapse.python_dependencies import DependencyException, check_requirements @@ -22,6 +24,8 @@ from ._base import Config, ConfigError +logger = logging.getLogger(__name__) + DEFAULT_THUMBNAIL_SIZES = [ {"width": 32, "height": 32, "method": "crop"}, {"width": 96, "height": 96, "method": "crop"}, @@ -36,6 +40,9 @@ # method: %(method)s """ +HTTP_PROXY_SET_WARNING = """\ +The Synapse config url_preview_ip_range_blacklist will be ignored as an HTTP(s) proxy is configured.""" + ThumbnailRequirement = namedtuple( "ThumbnailRequirement", ["width", "height", "method", "media_type"] ) @@ -180,12 +187,17 @@ def read_config(self, config, **kwargs): e.message # noqa: B306, DependencyException.message is a property ) + proxy_env = getproxies_environment() if "url_preview_ip_range_blacklist" not in config: - raise ConfigError( - "For security, you must specify an explicit target IP address " - "blacklist in url_preview_ip_range_blacklist for url previewing " - "to work" - ) + if "http" not in proxy_env or "https" not in proxy_env: + raise ConfigError( + "For security, you must specify an explicit target IP address " + "blacklist in url_preview_ip_range_blacklist for url previewing " + "to work" + ) + else: + if "http" in proxy_env or "https" in proxy_env: + logger.warning("".join(HTTP_PROXY_SET_WARNING)) # we always blacklist '0.0.0.0' and '::', which are supposed to be # unroutable addresses. @@ -292,6 +304,8 @@ def generate_config_section(self, data_dir_path, **kwargs): # This must be specified if url_preview_enabled is set. It is recommended that # you uncomment the following list as a starting point. # + # Note: The value is ignored when an HTTP proxy is in use + # #url_preview_ip_range_blacklist: %(ip_range_blacklist)s diff --git a/synapse/config/server.py b/synapse/config/server.py index b9e0c0b300..187b4301a0 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -960,6 +960,8 @@ def generate_config_section( # # This option replaces federation_ip_range_blacklist in Synapse v1.25.0. # + # Note: The value is ignored when an HTTP proxy is in use + # #ip_range_blacklist: %(ip_range_blacklist)s From c2000ab35b76288a625f598d2382d4e3f29f65f6 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Wed, 4 Aug 2021 13:40:25 +0300 Subject: [PATCH 508/619] Add `get_userinfo_by_id` method to `ModuleApi` (#9581) Makes it easier to fetch user details in for example spam checker modules, without needing to use api._store or figure out database interactions. Signed-off-by: Jason Robinson --- changelog.d/9581.feature | 1 + synapse/module_api/__init__.py | 12 +++++++- .../storage/databases/main/registration.py | 30 ++++++++++++++++++- synapse/types.py | 29 ++++++++++++++++++ tests/module_api/test_api.py | 10 +++++++ 5 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 changelog.d/9581.feature diff --git a/changelog.d/9581.feature b/changelog.d/9581.feature new file mode 100644 index 0000000000..fa1949cd4b --- /dev/null +++ b/changelog.d/9581.feature @@ -0,0 +1 @@ +Add `get_userinfo_by_id` method to ModuleApi. diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 473812b8e2..1cc13fc97b 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -45,7 +45,7 @@ from synapse.storage.database import DatabasePool, LoggingTransaction from synapse.storage.databases.main.roommember import ProfileInfo from synapse.storage.state import StateFilter -from synapse.types import JsonDict, Requester, UserID, create_requester +from synapse.types import JsonDict, Requester, UserID, UserInfo, create_requester from synapse.util import Clock from synapse.util.caches.descriptors import cached @@ -174,6 +174,16 @@ def email_app_name(self) -> str: """The application name configured in the homeserver's configuration.""" return self._hs.config.email.email_app_name + async def get_userinfo_by_id(self, user_id: str) -> Optional[UserInfo]: + """Get user info by user_id + + Args: + user_id: Fully qualified user id. + Returns: + UserInfo object if a user was found, otherwise None + """ + return await self._store.get_userinfo_by_id(user_id) + async def get_user_by_req( self, req: SynapseRequest, diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index 6ad1a0cf7f..14670c2881 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -29,7 +29,7 @@ from synapse.storage.types import Connection, Cursor from synapse.storage.util.id_generators import IdGenerator from synapse.storage.util.sequence import build_sequence_generator -from synapse.types import UserID +from synapse.types import UserID, UserInfo from synapse.util.caches.descriptors import cached if TYPE_CHECKING: @@ -146,6 +146,7 @@ def __init__( @cached() async def get_user_by_id(self, user_id: str) -> Optional[Dict[str, Any]]: + """Deprecated: use get_userinfo_by_id instead""" return await self.db_pool.simple_select_one( table="users", keyvalues={"name": user_id}, @@ -166,6 +167,33 @@ async def get_user_by_id(self, user_id: str) -> Optional[Dict[str, Any]]: desc="get_user_by_id", ) + async def get_userinfo_by_id(self, user_id: str) -> Optional[UserInfo]: + """Get a UserInfo object for a user by user ID. + + Note! Currently uses the cache of `get_user_by_id`. Once that deprecated method is removed, + this method should be cached. + + Args: + user_id: The user to fetch user info for. + Returns: + `UserInfo` object if user found, otherwise `None`. + """ + user_data = await self.get_user_by_id(user_id) + if not user_data: + return None + return UserInfo( + appservice_id=user_data["appservice_id"], + consent_server_notice_sent=user_data["consent_server_notice_sent"], + consent_version=user_data["consent_version"], + creation_ts=user_data["creation_ts"], + is_admin=bool(user_data["admin"]), + is_deactivated=bool(user_data["deactivated"]), + is_guest=bool(user_data["is_guest"]), + is_shadow_banned=bool(user_data["shadow_banned"]), + user_id=UserID.from_string(user_data["name"]), + user_type=user_data["user_type"], + ) + async def is_trial_user(self, user_id: str) -> bool: """Checks if user is in the "trial" period, i.e. within the first N days of registration defined by `mau_trial_days` config diff --git a/synapse/types.py b/synapse/types.py index 429bb013d2..80fa903c4b 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -751,3 +751,32 @@ def get_verify_key_from_cross_signing_key(key_info): # and return that one key for key_id, key_data in keys.items(): return (key_id, decode_verify_key_bytes(key_id, decode_base64(key_data))) + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class UserInfo: + """Holds information about a user. Result of get_userinfo_by_id. + + Attributes: + user_id: ID of the user. + appservice_id: Application service ID that created this user. + consent_server_notice_sent: Version of policy documents the user has been sent. + consent_version: Version of policy documents the user has consented to. + creation_ts: Creation timestamp of the user. + is_admin: True if the user is an admin. + is_deactivated: True if the user has been deactivated. + is_guest: True if the user is a guest user. + is_shadow_banned: True if the user has been shadow-banned. + user_type: User type (None for normal user, 'support' and 'bot' other options). + """ + + user_id: UserID + appservice_id: Optional[int] + consent_server_notice_sent: Optional[str] + consent_version: Optional[str] + user_type: Optional[str] + creation_ts: int + is_admin: bool + is_deactivated: bool + is_guest: bool + is_shadow_banned: bool diff --git a/tests/module_api/test_api.py b/tests/module_api/test_api.py index 81d9e2f484..0b817cc701 100644 --- a/tests/module_api/test_api.py +++ b/tests/module_api/test_api.py @@ -79,6 +79,16 @@ def test_can_register_user(self): displayname = self.get_success(self.store.get_profile_displayname("bob")) self.assertEqual(displayname, "Bobberino") + def test_get_userinfo_by_id(self): + user_id = self.register_user("alice", "1234") + found_user = self.get_success(self.module_api.get_userinfo_by_id(user_id)) + self.assertEqual(found_user.user_id.to_string(), user_id) + self.assertIdentical(found_user.is_admin, False) + + def test_get_userinfo_by_id__no_user_found(self): + found_user = self.get_success(self.module_api.get_userinfo_by_id("@alice:test")) + self.assertIsNone(found_user) + def test_sending_events_into_room(self): """Tests that a module can send events into a room""" # Mock out create_and_send_nonmember_event to check whether events are being sent From 11540be55ed15da920fa6f3ea805315517c02c76 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Aug 2021 13:09:04 +0100 Subject: [PATCH 509/619] Fix `could not serialize access` errors for `claim_e2e_one_time_keys` (#10504) --- changelog.d/10504.misc | 1 + .../storage/databases/main/end_to_end_keys.py | 188 ++++++++++++------ 2 files changed, 127 insertions(+), 62 deletions(-) create mode 100644 changelog.d/10504.misc diff --git a/changelog.d/10504.misc b/changelog.d/10504.misc new file mode 100644 index 0000000000..1479a5022d --- /dev/null +++ b/changelog.d/10504.misc @@ -0,0 +1 @@ +Reduce errors in PostgreSQL logs due to concurrent serialization errors. diff --git a/synapse/storage/databases/main/end_to_end_keys.py b/synapse/storage/databases/main/end_to_end_keys.py index 1edc96042b..1f0a39eac4 100644 --- a/synapse/storage/databases/main/end_to_end_keys.py +++ b/synapse/storage/databases/main/end_to_end_keys.py @@ -755,81 +755,145 @@ async def claim_e2e_one_time_keys( """ @trace - def _claim_e2e_one_time_keys(txn): - sql = ( - "SELECT key_id, key_json FROM e2e_one_time_keys_json" - " WHERE user_id = ? AND device_id = ? AND algorithm = ?" - " LIMIT 1" + def _claim_e2e_one_time_key_simple( + txn, user_id: str, device_id: str, algorithm: str + ) -> Optional[Tuple[str, str]]: + """Claim OTK for device for DBs that don't support RETURNING. + + Returns: + A tuple of key name (algorithm + key ID) and key JSON, if an + OTK was found. + """ + + sql = """ + SELECT key_id, key_json FROM e2e_one_time_keys_json + WHERE user_id = ? AND device_id = ? AND algorithm = ? + LIMIT 1 + """ + + txn.execute(sql, (user_id, device_id, algorithm)) + otk_row = txn.fetchone() + if otk_row is None: + return None + + key_id, key_json = otk_row + + self.db_pool.simple_delete_one_txn( + txn, + table="e2e_one_time_keys_json", + keyvalues={ + "user_id": user_id, + "device_id": device_id, + "algorithm": algorithm, + "key_id": key_id, + }, ) - fallback_sql = ( - "SELECT key_id, key_json, used FROM e2e_fallback_keys_json" - " WHERE user_id = ? AND device_id = ? AND algorithm = ?" - " LIMIT 1" + self._invalidate_cache_and_stream( + txn, self.count_e2e_one_time_keys, (user_id, device_id) ) - result = {} - delete = [] - used_fallbacks = [] - for user_id, device_id, algorithm in query_list: - user_result = result.setdefault(user_id, {}) - device_result = user_result.setdefault(device_id, {}) - txn.execute(sql, (user_id, device_id, algorithm)) - otk_row = txn.fetchone() - if otk_row is not None: - key_id, key_json = otk_row - device_result[algorithm + ":" + key_id] = key_json - delete.append((user_id, device_id, algorithm, key_id)) - else: - # no one-time key available, so see if there's a fallback - # key - txn.execute(fallback_sql, (user_id, device_id, algorithm)) - fallback_row = txn.fetchone() - if fallback_row is not None: - key_id, key_json, used = fallback_row - device_result[algorithm + ":" + key_id] = key_json - if not used: - used_fallbacks.append( - (user_id, device_id, algorithm, key_id) - ) - - # drop any one-time keys that were claimed - sql = ( - "DELETE FROM e2e_one_time_keys_json" - " WHERE user_id = ? AND device_id = ? AND algorithm = ?" - " AND key_id = ?" + + return f"{algorithm}:{key_id}", key_json + + @trace + def _claim_e2e_one_time_key_returning( + txn, user_id: str, device_id: str, algorithm: str + ) -> Optional[Tuple[str, str]]: + """Claim OTK for device for DBs that support RETURNING. + + Returns: + A tuple of key name (algorithm + key ID) and key JSON, if an + OTK was found. + """ + + # We can use RETURNING to do the fetch and DELETE in once step. + sql = """ + DELETE FROM e2e_one_time_keys_json + WHERE user_id = ? AND device_id = ? AND algorithm = ? + AND key_id IN ( + SELECT key_id FROM e2e_one_time_keys_json + WHERE user_id = ? AND device_id = ? AND algorithm = ? + LIMIT 1 + ) + RETURNING key_id, key_json + """ + + txn.execute( + sql, (user_id, device_id, algorithm, user_id, device_id, algorithm) ) - for user_id, device_id, algorithm, key_id in delete: - log_kv( - { - "message": "Executing claim e2e_one_time_keys transaction on database." - } - ) - txn.execute(sql, (user_id, device_id, algorithm, key_id)) - log_kv({"message": "finished executing and invalidating cache"}) - self._invalidate_cache_and_stream( - txn, self.count_e2e_one_time_keys, (user_id, device_id) + otk_row = txn.fetchone() + if otk_row is None: + return None + + key_id, key_json = otk_row + return f"{algorithm}:{key_id}", key_json + + results = {} + for user_id, device_id, algorithm in query_list: + if self.database_engine.supports_returning: + # If we support RETURNING clause we can use a single query that + # allows us to use autocommit mode. + _claim_e2e_one_time_key = _claim_e2e_one_time_key_returning + db_autocommit = True + else: + _claim_e2e_one_time_key = _claim_e2e_one_time_key_simple + db_autocommit = False + + row = await self.db_pool.runInteraction( + "claim_e2e_one_time_keys", + _claim_e2e_one_time_key, + user_id, + device_id, + algorithm, + db_autocommit=db_autocommit, + ) + if row: + device_results = results.setdefault(user_id, {}).setdefault( + device_id, {} ) - # mark fallback keys as used - for user_id, device_id, algorithm, key_id in used_fallbacks: - self.db_pool.simple_update_txn( - txn, - "e2e_fallback_keys_json", - { + device_results[row[0]] = row[1] + continue + + # No one-time key available, so see if there's a fallback + # key + row = await self.db_pool.simple_select_one( + table="e2e_fallback_keys_json", + keyvalues={ + "user_id": user_id, + "device_id": device_id, + "algorithm": algorithm, + }, + retcols=("key_id", "key_json", "used"), + desc="_get_fallback_key", + allow_none=True, + ) + if row is None: + continue + + key_id = row["key_id"] + key_json = row["key_json"] + used = row["used"] + + # Mark fallback key as used if not already. + if not used: + await self.db_pool.simple_update_one( + table="e2e_fallback_keys_json", + keyvalues={ "user_id": user_id, "device_id": device_id, "algorithm": algorithm, "key_id": key_id, }, - {"used": True}, + updatevalues={"used": True}, + desc="_get_fallback_key_set_used", ) - self._invalidate_cache_and_stream( - txn, self.get_e2e_unused_fallback_key_types, (user_id, device_id) + await self.invalidate_cache_and_stream( + "get_e2e_unused_fallback_key_types", (user_id, device_id) ) - return result + device_results = results.setdefault(user_id, {}).setdefault(device_id, {}) + device_results[f"{algorithm}:{key_id}"] = key_json - return await self.db_pool.runInteraction( - "claim_e2e_one_time_keys", _claim_e2e_one_time_keys - ) + return results class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore): From c37dad67ab04980ac934554399f52a27e54292ab Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 4 Aug 2021 13:54:51 +0100 Subject: [PATCH 510/619] Improve event caching code (#10119) Ensure we only load an event from the DB once when the same event is requested multiple times at once. --- changelog.d/10119.misc | 1 + .../storage/databases/main/events_worker.py | 144 +++++++++++++----- synapse/storage/databases/main/roommember.py | 6 +- .../databases/main/test_events_worker.py | 50 ++++++ 4 files changed, 158 insertions(+), 43 deletions(-) create mode 100644 changelog.d/10119.misc diff --git a/changelog.d/10119.misc b/changelog.d/10119.misc new file mode 100644 index 0000000000..f70dc6496f --- /dev/null +++ b/changelog.d/10119.misc @@ -0,0 +1 @@ +Improve event caching mechanism to avoid having multiple copies of an event in memory at a time. diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index 3c86adab56..375463e4e9 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -14,7 +14,6 @@ import logging import threading -from collections import namedtuple from typing import ( Collection, Container, @@ -27,6 +26,7 @@ overload, ) +import attr from constantly import NamedConstant, Names from typing_extensions import Literal @@ -42,7 +42,11 @@ from synapse.events import EventBase, make_event_from_dict from synapse.events.snapshot import EventContext from synapse.events.utils import prune_event -from synapse.logging.context import PreserveLoggingContext, current_context +from synapse.logging.context import ( + PreserveLoggingContext, + current_context, + make_deferred_yieldable, +) from synapse.metrics.background_process_metrics import ( run_as_background_process, wrap_as_background_process, @@ -56,6 +60,8 @@ from synapse.storage.util.id_generators import MultiWriterIdGenerator, StreamIdGenerator from synapse.storage.util.sequence import build_sequence_generator from synapse.types import JsonDict, get_domain_from_id +from synapse.util import unwrapFirstError +from synapse.util.async_helpers import ObservableDeferred from synapse.util.caches.descriptors import cached, cachedList from synapse.util.caches.lrucache import LruCache from synapse.util.iterutils import batch_iter @@ -74,7 +80,10 @@ EVENT_QUEUE_TIMEOUT_S = 0.1 # Timeout when waiting for requests for events -_EventCacheEntry = namedtuple("_EventCacheEntry", ("event", "redacted_event")) +@attr.s(slots=True, auto_attribs=True) +class _EventCacheEntry: + event: EventBase + redacted_event: Optional[EventBase] class EventRedactBehaviour(Names): @@ -161,6 +170,13 @@ def __init__(self, database: DatabasePool, db_conn, hs): max_size=hs.config.caches.event_cache_size, ) + # Map from event ID to a deferred that will result in a map from event + # ID to cache entry. Note that the returned dict may not have the + # requested event in it if the event isn't in the DB. + self._current_event_fetches: Dict[ + str, ObservableDeferred[Dict[str, _EventCacheEntry]] + ] = {} + self._event_fetch_lock = threading.Condition() self._event_fetch_list = [] self._event_fetch_ongoing = 0 @@ -476,7 +492,9 @@ async def get_events_as_list( return events - async def _get_events_from_cache_or_db(self, event_ids, allow_rejected=False): + async def _get_events_from_cache_or_db( + self, event_ids: Iterable[str], allow_rejected: bool = False + ) -> Dict[str, _EventCacheEntry]: """Fetch a bunch of events from the cache or the database. If events are pulled from the database, they will be cached for future lookups. @@ -485,53 +503,107 @@ async def _get_events_from_cache_or_db(self, event_ids, allow_rejected=False): Args: - event_ids (Iterable[str]): The event_ids of the events to fetch + event_ids: The event_ids of the events to fetch - allow_rejected (bool): Whether to include rejected events. If False, + allow_rejected: Whether to include rejected events. If False, rejected events are omitted from the response. Returns: - Dict[str, _EventCacheEntry]: - map from event id to result + map from event id to result """ event_entry_map = self._get_events_from_cache( - event_ids, allow_rejected=allow_rejected + event_ids, ) - missing_events_ids = [e for e in event_ids if e not in event_entry_map] + missing_events_ids = {e for e in event_ids if e not in event_entry_map} + + # We now look up if we're already fetching some of the events in the DB, + # if so we wait for those lookups to finish instead of pulling the same + # events out of the DB multiple times. + already_fetching: Dict[str, defer.Deferred] = {} + + for event_id in missing_events_ids: + deferred = self._current_event_fetches.get(event_id) + if deferred is not None: + # We're already pulling the event out of the DB. Add the deferred + # to the collection of deferreds to wait on. + already_fetching[event_id] = deferred.observe() + + missing_events_ids.difference_update(already_fetching) if missing_events_ids: log_ctx = current_context() log_ctx.record_event_fetch(len(missing_events_ids)) + # Add entries to `self._current_event_fetches` for each event we're + # going to pull from the DB. We use a single deferred that resolves + # to all the events we pulled from the DB (this will result in this + # function returning more events than requested, but that can happen + # already due to `_get_events_from_db`). + fetching_deferred: ObservableDeferred[ + Dict[str, _EventCacheEntry] + ] = ObservableDeferred(defer.Deferred()) + for event_id in missing_events_ids: + self._current_event_fetches[event_id] = fetching_deferred + # Note that _get_events_from_db is also responsible for turning db rows # into FrozenEvents (via _get_event_from_row), which involves seeing if # the events have been redacted, and if so pulling the redaction event out # of the database to check it. # - missing_events = await self._get_events_from_db( - missing_events_ids, allow_rejected=allow_rejected - ) + try: + missing_events = await self._get_events_from_db( + missing_events_ids, + ) - event_entry_map.update(missing_events) + event_entry_map.update(missing_events) + except Exception as e: + with PreserveLoggingContext(): + fetching_deferred.errback(e) + raise e + finally: + # Ensure that we mark these events as no longer being fetched. + for event_id in missing_events_ids: + self._current_event_fetches.pop(event_id, None) + + with PreserveLoggingContext(): + fetching_deferred.callback(missing_events) + + if already_fetching: + # Wait for the other event requests to finish and add their results + # to ours. + results = await make_deferred_yieldable( + defer.gatherResults( + already_fetching.values(), + consumeErrors=True, + ) + ).addErrback(unwrapFirstError) + + for result in results: + event_entry_map.update(result) + + if not allow_rejected: + event_entry_map = { + event_id: entry + for event_id, entry in event_entry_map.items() + if not entry.event.rejected_reason + } return event_entry_map def _invalidate_get_event_cache(self, event_id): self._get_event_cache.invalidate((event_id,)) - def _get_events_from_cache(self, events, allow_rejected, update_metrics=True): - """Fetch events from the caches + def _get_events_from_cache( + self, events: Iterable[str], update_metrics: bool = True + ) -> Dict[str, _EventCacheEntry]: + """Fetch events from the caches. - Args: - events (Iterable[str]): list of event_ids to fetch - allow_rejected (bool): Whether to return events that were rejected - update_metrics (bool): Whether to update the cache hit ratio metrics + May return rejected events. - Returns: - dict of event_id -> _EventCacheEntry for each event_id in cache. If - allow_rejected is `False` then there will still be an entry but it - will be `None` + Args: + events: list of event_ids to fetch + update_metrics: Whether to update the cache hit ratio metrics """ event_map = {} @@ -542,10 +614,7 @@ def _get_events_from_cache(self, events, allow_rejected, update_metrics=True): if not ret: continue - if allow_rejected or not ret.event.rejected_reason: - event_map[event_id] = ret - else: - event_map[event_id] = None + event_map[event_id] = ret return event_map @@ -672,23 +741,23 @@ def fire(evs, exc): with PreserveLoggingContext(): self.hs.get_reactor().callFromThread(fire, event_list, e) - async def _get_events_from_db(self, event_ids, allow_rejected=False): + async def _get_events_from_db( + self, event_ids: Iterable[str] + ) -> Dict[str, _EventCacheEntry]: """Fetch a bunch of events from the database. + May return rejected events. + Returned events will be added to the cache for future lookups. Unknown events are omitted from the response. Args: - event_ids (Iterable[str]): The event_ids of the events to fetch - - allow_rejected (bool): Whether to include rejected events. If False, - rejected events are omitted from the response. + event_ids: The event_ids of the events to fetch Returns: - Dict[str, _EventCacheEntry]: - map from event id to result. May return extra events which - weren't asked for. + map from event id to result. May return extra events which + weren't asked for. """ fetched_events = {} events_to_fetch = event_ids @@ -717,9 +786,6 @@ async def _get_events_from_db(self, event_ids, allow_rejected=False): rejected_reason = row["rejected_reason"] - if not allow_rejected and rejected_reason: - continue - # If the event or metadata cannot be parsed, log the error and act # as if the event is unknown. try: diff --git a/synapse/storage/databases/main/roommember.py b/synapse/storage/databases/main/roommember.py index 68f1b40ea6..e8157ba3d4 100644 --- a/synapse/storage/databases/main/roommember.py +++ b/synapse/storage/databases/main/roommember.py @@ -629,14 +629,12 @@ async def _get_joined_users_from_context( # We don't update the event cache hit ratio as it completely throws off # the hit ratio counts. After all, we don't populate the cache if we # miss it here - event_map = self._get_events_from_cache( - member_event_ids, allow_rejected=False, update_metrics=False - ) + event_map = self._get_events_from_cache(member_event_ids, update_metrics=False) missing_member_event_ids = [] for event_id in member_event_ids: ev_entry = event_map.get(event_id) - if ev_entry: + if ev_entry and not ev_entry.event.rejected_reason: if ev_entry.event.membership == Membership.JOIN: users_in_room[ev_entry.event.state_key] = ProfileInfo( display_name=ev_entry.event.content.get("displayname", None), diff --git a/tests/storage/databases/main/test_events_worker.py b/tests/storage/databases/main/test_events_worker.py index 932970fd9a..d05d367685 100644 --- a/tests/storage/databases/main/test_events_worker.py +++ b/tests/storage/databases/main/test_events_worker.py @@ -14,7 +14,10 @@ import json from synapse.logging.context import LoggingContext +from synapse.rest import admin +from synapse.rest.client.v1 import login, room from synapse.storage.databases.main.events_worker import EventsWorkerStore +from synapse.util.async_helpers import yieldable_gather_results from tests import unittest @@ -94,3 +97,50 @@ def test_query_via_event_cache(self): res = self.get_success(self.store.have_seen_events("room1", ["event10"])) self.assertEquals(res, {"event10"}) self.assertEquals(ctx.get_resource_usage().db_txn_count, 0) + + +class EventCacheTestCase(unittest.HomeserverTestCase): + """Test that the various layers of event cache works.""" + + servlets = [ + admin.register_servlets, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, hs): + self.store: EventsWorkerStore = hs.get_datastore() + + self.user = self.register_user("user", "pass") + self.token = self.login(self.user, "pass") + + self.room = self.helper.create_room_as(self.user, tok=self.token) + + res = self.helper.send(self.room, tok=self.token) + self.event_id = res["event_id"] + + # Reset the event cache so the tests start with it empty + self.store._get_event_cache.clear() + + def test_simple(self): + """Test that we cache events that we pull from the DB.""" + + with LoggingContext("test") as ctx: + self.get_success(self.store.get_event(self.event_id)) + + # We should have fetched the event from the DB + self.assertEqual(ctx.get_resource_usage().evt_db_fetch_count, 1) + + def test_dedupe(self): + """Test that if we request the same event multiple times we only pull it + out once. + """ + + with LoggingContext("test") as ctx: + d = yieldable_gather_results( + self.store.get_event, [self.event_id, self.event_id] + ) + self.get_success(d) + + # We should have fetched the event from the DB + self.assertEqual(ctx.get_resource_usage().evt_db_fetch_count, 1) From e8a3e8140291be0548ad80d0e942a9aaae6c2434 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Aug 2021 16:13:24 +0200 Subject: [PATCH 511/619] Don't fail on empty bodies when sending out read receipts (#10531) Fixes a bug introduced in rc1 that would cause Synapse to 400 on read receipts requests with empty bodies. Broken in #10413 --- changelog.d/10531.bugfix | 1 + synapse/rest/client/v2_alpha/receipts.py | 2 +- tests/rest/client/v2_alpha/test_sync.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10531.bugfix diff --git a/changelog.d/10531.bugfix b/changelog.d/10531.bugfix new file mode 100644 index 0000000000..aaa921ee91 --- /dev/null +++ b/changelog.d/10531.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in Synapse v1.40.0rc1 that would cause Synapse to respond with an error when clients would update their read receipts. diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/v2_alpha/receipts.py index 4b98979b47..d9ab836cd8 100644 --- a/synapse/rest/client/v2_alpha/receipts.py +++ b/synapse/rest/client/v2_alpha/receipts.py @@ -43,7 +43,7 @@ async def on_POST(self, request, room_id, receipt_type, event_id): if receipt_type != "m.read": raise SynapseError(400, "Receipt type must be 'm.read'") - body = parse_json_object_from_request(request) + body = parse_json_object_from_request(request, allow_empty_body=True) hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False) if not isinstance(hidden, bool): diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index f6ae9ae181..15748ed4fd 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -418,6 +418,18 @@ def test_hidden_read_receipts(self): # Test that the first user can't see the other user's hidden read receipt self.assertEqual(self._get_read_receipt(), None) + def test_read_receipt_with_empty_body(self): + # Send a message as the first user + res = self.helper.send(self.room_id, body="hello", tok=self.tok) + + # Send a read receipt for this message with an empty body + channel = self.make_request( + "POST", + "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]), + access_token=self.tok2, + ) + self.assertEqual(channel.code, 200) + def _get_read_receipt(self): """Syncs and returns the read receipt.""" From 02c2f631aed5cc2ef4bcaea25b443b625616f816 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Aug 2021 17:09:27 +0100 Subject: [PATCH 512/619] 1.40.0rc2 --- CHANGES.md | 16 ++++++++++++++++ changelog.d/10516.misc | 1 - changelog.d/10517.bugfix | 1 - changelog.d/10531.bugfix | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 6 files changed, 23 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/10516.misc delete mode 100644 changelog.d/10517.bugfix delete mode 100644 changelog.d/10531.bugfix diff --git a/CHANGES.md b/CHANGES.md index 7ce28c4c18..75031986d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +Synapse 1.40.0rc2 (2021-08-04) +============================== + +Bugfixes +-------- + +- Fix the `PeriodicallyFlushingMemoryHandler` inhibiting application shutdown because of its background thread. ([\#10517](https://github.com/matrix-org/synapse/issues/10517)) +- Fix a bug introduced in Synapse v1.40.0rc1 that would cause Synapse to respond with an error when clients would update their read receipts. ([\#10531](https://github.com/matrix-org/synapse/issues/10531)) + + +Internal Changes +---------------- + +- Fix release script to open correct URL for the release. ([\#10516](https://github.com/matrix-org/synapse/issues/10516)) + + Synapse 1.40.0rc1 (2021-08-03) ============================== diff --git a/changelog.d/10516.misc b/changelog.d/10516.misc deleted file mode 100644 index 4d8c5e4805..0000000000 --- a/changelog.d/10516.misc +++ /dev/null @@ -1 +0,0 @@ -Fix release script to open correct URL for the release. diff --git a/changelog.d/10517.bugfix b/changelog.d/10517.bugfix deleted file mode 100644 index 5b044bb34d..0000000000 --- a/changelog.d/10517.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the `PeriodicallyFlushingMemoryHandler` inhibiting application shutdown because of its background thread. diff --git a/changelog.d/10531.bugfix b/changelog.d/10531.bugfix deleted file mode 100644 index aaa921ee91..0000000000 --- a/changelog.d/10531.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in Synapse v1.40.0rc1 that would cause Synapse to respond with an error when clients would update their read receipts. diff --git a/debian/changelog b/debian/changelog index f0557c35ef..c523101f9a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.40.0~rc2) stable; urgency=medium + + * New synapse release 1.40.0~rc2. + + -- Synapse Packaging team Wed, 04 Aug 2021 17:08:55 +0100 + matrix-synapse-py3 (1.40.0~rc1) stable; urgency=medium [ Richard van der Hoff ] diff --git a/synapse/__init__.py b/synapse/__init__.py index d6c1765508..da52463531 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.40.0rc1" +__version__ = "1.40.0rc2" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 167335bd3da0fcfa0b2ba5ca3dc9d2f7c953c1eb Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Aug 2021 17:11:23 +0100 Subject: [PATCH 513/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 75031986d3..052ab49599 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Bugfixes -------- - Fix the `PeriodicallyFlushingMemoryHandler` inhibiting application shutdown because of its background thread. ([\#10517](https://github.com/matrix-org/synapse/issues/10517)) -- Fix a bug introduced in Synapse v1.40.0rc1 that would cause Synapse to respond with an error when clients would update their read receipts. ([\#10531](https://github.com/matrix-org/synapse/issues/10531)) +- Fix a bug introduced in Synapse v1.40.0rc1 that could cause Synapse to respond with an error when clients would update their a receipts. ([\#10531](https://github.com/matrix-org/synapse/issues/10531)) Internal Changes From cc1cb0ab54654b1a1d938ae464a3471dd1b588a5 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Aug 2021 17:14:55 +0100 Subject: [PATCH 514/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 052ab49599..7d3bdebbde 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Bugfixes -------- - Fix the `PeriodicallyFlushingMemoryHandler` inhibiting application shutdown because of its background thread. ([\#10517](https://github.com/matrix-org/synapse/issues/10517)) -- Fix a bug introduced in Synapse v1.40.0rc1 that could cause Synapse to respond with an error when clients would update their a receipts. ([\#10531](https://github.com/matrix-org/synapse/issues/10531)) +- Fix a bug introduced in Synapse v1.40.0rc1 that could cause Synapse to respond with an error when clients would update read receipts. ([\#10531](https://github.com/matrix-org/synapse/issues/10531)) Internal Changes From 05111f8f26252cc936fce685846322289039128d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 4 Aug 2021 17:16:08 +0100 Subject: [PATCH 515/619] Fixup changelog --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7d3bdebbde..62ea684e58 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ Bugfixes Internal Changes ---------------- -- Fix release script to open correct URL for the release. ([\#10516](https://github.com/matrix-org/synapse/issues/10516)) +- Fix release script to open the correct URL for the release. ([\#10516](https://github.com/matrix-org/synapse/issues/10516)) Synapse 1.40.0rc1 (2021-08-03) From 684d19a11c3b93c9dd5fb90f43d38aa7e8c6005f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 4 Aug 2021 12:07:57 -0500 Subject: [PATCH 516/619] Add support for MSC2716 marker events (#10498) * Make historical messages available to federated servers Part of MSC2716: https://github.com/matrix-org/matrix-doc/pull/2716 Follow-up to https://github.com/matrix-org/synapse/pull/9247 * Debug message not available on federation * Add base starting insertion point when no chunk ID is provided * Fix messages from multiple senders in historical chunk Follow-up to https://github.com/matrix-org/synapse/pull/9247 Part of MSC2716: https://github.com/matrix-org/matrix-doc/pull/2716 --- Previously, Synapse would throw a 403, `Cannot force another user to join.`, because we were trying to use `?user_id` from a single virtual user which did not match with messages from other users in the chunk. * Remove debug lines * Messing with selecting insertion event extremeties * Move db schema change to new version * Add more better comments * Make a fake requester with just what we need See https://github.com/matrix-org/synapse/pull/10276#discussion_r660999080 * Store insertion events in table * Make base insertion event float off on its own See https://github.com/matrix-org/synapse/pull/10250#issuecomment-875711889 Conflicts: synapse/rest/client/v1/room.py * Validate that the app service can actually control the given user See https://github.com/matrix-org/synapse/pull/10276#issuecomment-876316455 Conflicts: synapse/rest/client/v1/room.py * Add some better comments on what we're trying to check for * Continue debugging * Share validation logic * Add inserted historical messages to /backfill response * Remove debug sql queries * Some marker event implemntation trials * Clean up PR * Rename insertion_event_id to just event_id * Add some better sql comments * More accurate description * Add changelog * Make it clear what MSC the change is part of * Add more detail on which insertion event came through * Address review and improve sql queries * Only use event_id as unique constraint * Fix test case where insertion event is already in the normal DAG * Remove debug changes * Add support for MSC2716 marker events * Process markers when we receive it over federation * WIP: make hs2 backfill historical messages after marker event * hs2 to better ask for insertion event extremity But running into the `sqlite3.IntegrityError: NOT NULL constraint failed: event_to_state_groups.state_group` error * Add insertion_event_extremities table * Switch to chunk events so we can auth via power_levels Previously, we were using `content.chunk_id` to connect one chunk to another. But these events can be from any `sender` and we can't tell who should be able to send historical events. We know we only want the application service to do it but these events have the sender of a real historical message, not the application service user ID as the sender. Other federated homeservers also have no indicator which senders are an application service on the originating homeserver. So we want to auth all of the MSC2716 events via power_levels and have them be sent by the application service with proper PL levels in the room. * Switch to chunk events for federation * Add unstable room version to support new historical PL * Messy: Fix undefined state_group for federated historical events ``` 2021-07-13 02:27:57,810 - synapse.handlers.federation - 1248 - ERROR - GET-4 - Failed to backfill from hs1 because NOT NULL constraint failed: event_to_state_groups.state_group Traceback (most recent call last): File "/usr/local/lib/python3.8/site-packages/synapse/handlers/federation.py", line 1216, in try_backfill await self.backfill( File "/usr/local/lib/python3.8/site-packages/synapse/handlers/federation.py", line 1035, in backfill await self._auth_and_persist_event(dest, event, context, backfilled=True) File "/usr/local/lib/python3.8/site-packages/synapse/handlers/federation.py", line 2222, in _auth_and_persist_event await self._run_push_actions_and_persist_event(event, context, backfilled) File "/usr/local/lib/python3.8/site-packages/synapse/handlers/federation.py", line 2244, in _run_push_actions_and_persist_event await self.persist_events_and_notify( File "/usr/local/lib/python3.8/site-packages/synapse/handlers/federation.py", line 3290, in persist_events_and_notify events, max_stream_token = await self.storage.persistence.persist_events( File "/usr/local/lib/python3.8/site-packages/synapse/logging/opentracing.py", line 774, in _trace_inner return await func(*args, **kwargs) File "/usr/local/lib/python3.8/site-packages/synapse/storage/persist_events.py", line 320, in persist_events ret_vals = await yieldable_gather_results(enqueue, partitioned.items()) File "/usr/local/lib/python3.8/site-packages/synapse/storage/persist_events.py", line 237, in handle_queue_loop ret = await self._per_item_callback( File "/usr/local/lib/python3.8/site-packages/synapse/storage/persist_events.py", line 577, in _persist_event_batch await self.persist_events_store._persist_events_and_state_updates( File "/usr/local/lib/python3.8/site-packages/synapse/storage/databases/main/events.py", line 176, in _persist_events_and_state_updates await self.db_pool.runInteraction( File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 681, in runInteraction result = await self.runWithConnection( File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 770, in runWithConnection return await make_deferred_yieldable( File "/usr/local/lib/python3.8/site-packages/twisted/python/threadpool.py", line 238, in inContext result = inContext.theWork() # type: ignore[attr-defined] File "/usr/local/lib/python3.8/site-packages/twisted/python/threadpool.py", line 254, in inContext.theWork = lambda: context.call( # type: ignore[attr-defined] File "/usr/local/lib/python3.8/site-packages/twisted/python/context.py", line 118, in callWithContext return self.currentContext().callWithContext(ctx, func, *args, **kw) File "/usr/local/lib/python3.8/site-packages/twisted/python/context.py", line 83, in callWithContext return func(*args, **kw) File "/usr/local/lib/python3.8/site-packages/twisted/enterprise/adbapi.py", line 293, in _runWithConnection compat.reraise(excValue, excTraceback) File "/usr/local/lib/python3.8/site-packages/twisted/python/deprecate.py", line 298, in deprecatedFunction return function(*args, **kwargs) File "/usr/local/lib/python3.8/site-packages/twisted/python/compat.py", line 403, in reraise raise exception.with_traceback(traceback) File "/usr/local/lib/python3.8/site-packages/twisted/enterprise/adbapi.py", line 284, in _runWithConnection result = func(conn, *args, **kw) File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 765, in inner_func return func(db_conn, *args, **kwargs) File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 549, in new_transaction r = func(cursor, *args, **kwargs) File "/usr/local/lib/python3.8/site-packages/synapse/logging/utils.py", line 69, in wrapped return f(*args, **kwargs) File "/usr/local/lib/python3.8/site-packages/synapse/storage/databases/main/events.py", line 385, in _persist_events_txn self._store_event_state_mappings_txn(txn, events_and_contexts) File "/usr/local/lib/python3.8/site-packages/synapse/storage/databases/main/events.py", line 2065, in _store_event_state_mappings_txn self.db_pool.simple_insert_many_txn( File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 923, in simple_insert_many_txn txn.execute_batch(sql, vals) File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 280, in execute_batch self.executemany(sql, args) File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 300, in executemany self._do_execute(self.txn.executemany, sql, *args) File "/usr/local/lib/python3.8/site-packages/synapse/storage/database.py", line 330, in _do_execute return func(sql, *args) sqlite3.IntegrityError: NOT NULL constraint failed: event_to_state_groups.state_group ``` * Revert "Messy: Fix undefined state_group for federated historical events" This reverts commit 187ab28611546321e02770944c86f30ee2bc742a. * Fix federated events being rejected for no state_groups Add fix from https://github.com/matrix-org/synapse/pull/10439 until it merges. * Adapting to experimental room version * Some log cleanup * Add better comments around extremity fetching code and why * Rename to be more accurate to what the function returns * Add changelog * Ignore rejected events * Use simplified upsert * Add Erik's explanation of extra event checks See https://github.com/matrix-org/synapse/pull/10498#discussion_r680880332 * Clarify that the depth is not directly correlated to the backwards extremity that we return See https://github.com/matrix-org/synapse/pull/10498#discussion_r681725404 * lock only matters for sqlite See https://github.com/matrix-org/synapse/pull/10498#discussion_r681728061 * Move new SQL changes to its own delta file * Clean up upsert docstring * Bump database schema version (62) --- changelog.d/10498.feature | 1 + scripts-dev/complement.sh | 2 +- synapse/handlers/federation.py | 119 +++++++++++++++++- synapse/storage/database.py | 14 +-- .../databases/main/event_federation.py | 114 ++++++++++++++--- synapse/storage/databases/main/events.py | 24 +++- synapse/storage/schema/__init__.py | 2 +- .../62/01insertion_event_extremities.sql | 24 ++++ 8 files changed, 265 insertions(+), 35 deletions(-) create mode 100644 changelog.d/10498.feature create mode 100644 synapse/storage/schema/main/delta/62/01insertion_event_extremities.sql diff --git a/changelog.d/10498.feature b/changelog.d/10498.feature new file mode 100644 index 0000000000..5df896572d --- /dev/null +++ b/changelog.d/10498.feature @@ -0,0 +1 @@ +Add support for "marker" events which makes historical events discoverable for servers that already have all of the scrollback history (part of MSC2716). diff --git a/scripts-dev/complement.sh b/scripts-dev/complement.sh index cba015d942..5d0ef8dd3a 100755 --- a/scripts-dev/complement.sh +++ b/scripts-dev/complement.sh @@ -65,4 +65,4 @@ if [[ -n "$1" ]]; then fi # Run the tests! -go test -v -tags synapse_blacklist,msc2946,msc3083,msc2403 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests/... +go test -v -tags synapse_blacklist,msc2946,msc3083,msc2403,msc2716 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests/... diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8197b60b76..8b602e3813 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -42,6 +42,7 @@ from synapse import event_auth from synapse.api.constants import ( + EventContentFields, EventTypes, Membership, RejectedReason, @@ -262,7 +263,12 @@ async def on_receive_pdu( state = None - # Get missing pdus if necessary. + # Check that the event passes auth based on the state at the event. This is + # done for events that are to be added to the timeline (non-outliers). + # + # Get missing pdus if necessary: + # - Fetching any missing prev events to fill in gaps in the graph + # - Fetching state if we have a hole in the graph if not pdu.internal_metadata.is_outlier(): # We only backfill backwards to the min depth. min_depth = await self.get_min_depth_for_context(pdu.room_id) @@ -432,6 +438,13 @@ async def on_receive_pdu( affected=event_id, ) + # A second round of checks for all events. Check that the event passes auth + # based on `auth_events`, this allows us to assert that the event would + # have been allowed at some point. If an event passes this check its OK + # for it to be used as part of a returned `/state` request, as either + # a) we received the event as part of the original join and so trust it, or + # b) we'll do a state resolution with existing state before it becomes + # part of the "current state", which adds more protection. await self._process_received_pdu(origin, pdu, state=state) async def _get_missing_events_for_pdu( @@ -889,6 +902,79 @@ async def _process_received_pdu( "resync_device_due_to_pdu", self._resync_device, event.sender ) + await self._handle_marker_event(origin, event) + + async def _handle_marker_event(self, origin: str, marker_event: EventBase): + """Handles backfilling the insertion event when we receive a marker + event that points to one. + + Args: + origin: Origin of the event. Will be called to get the insertion event + marker_event: The event to process + """ + + if marker_event.type != EventTypes.MSC2716_MARKER: + # Not a marker event + return + + if marker_event.rejected_reason is not None: + # Rejected event + return + + # Skip processing a marker event if the room version doesn't + # support it. + room_version = await self.store.get_room_version(marker_event.room_id) + if not room_version.msc2716_historical: + return + + logger.debug("_handle_marker_event: received %s", marker_event) + + insertion_event_id = marker_event.content.get( + EventContentFields.MSC2716_MARKER_INSERTION + ) + + if insertion_event_id is None: + # Nothing to retrieve then (invalid marker) + return + + logger.debug( + "_handle_marker_event: backfilling insertion event %s", insertion_event_id + ) + + await self._get_events_and_persist( + origin, + marker_event.room_id, + [insertion_event_id], + ) + + insertion_event = await self.store.get_event( + insertion_event_id, allow_none=True + ) + if insertion_event is None: + logger.warning( + "_handle_marker_event: server %s didn't return insertion event %s for marker %s", + origin, + insertion_event_id, + marker_event.event_id, + ) + return + + logger.debug( + "_handle_marker_event: succesfully backfilled insertion event %s from marker event %s", + insertion_event, + marker_event, + ) + + await self.store.insert_insertion_extremity( + insertion_event_id, marker_event.room_id + ) + + logger.debug( + "_handle_marker_event: insertion extremity added for %s from marker event %s", + insertion_event, + marker_event, + ) + async def _resync_device(self, sender: str) -> None: """We have detected that the device list for the given user may be out of sync, so we try and resync them. @@ -1057,9 +1143,19 @@ async def maybe_backfill( async def _maybe_backfill_inner( self, room_id: str, current_depth: int, limit: int ) -> bool: - extremities = await self.store.get_oldest_events_with_depth_in_room(room_id) + oldest_events_with_depth = ( + await self.store.get_oldest_event_ids_with_depth_in_room(room_id) + ) + insertion_events_to_be_backfilled = ( + await self.store.get_insertion_event_backwards_extremities_in_room(room_id) + ) + logger.debug( + "_maybe_backfill_inner: extremities oldest_events_with_depth=%s insertion_events_to_be_backfilled=%s", + oldest_events_with_depth, + insertion_events_to_be_backfilled, + ) - if not extremities: + if not oldest_events_with_depth and not insertion_events_to_be_backfilled: logger.debug("Not backfilling as no extremeties found.") return False @@ -1089,10 +1185,12 @@ async def _maybe_backfill_inner( # state *before* the event, ignoring the special casing certain event # types have. - forward_events = await self.store.get_successor_events(list(extremities)) + forward_event_ids = await self.store.get_successor_events( + list(oldest_events_with_depth) + ) extremities_events = await self.store.get_events( - forward_events, + forward_event_ids, redact_behaviour=EventRedactBehaviour.AS_IS, get_prev_content=False, ) @@ -1106,10 +1204,19 @@ async def _maybe_backfill_inner( redact=False, check_history_visibility_only=True, ) + logger.debug( + "_maybe_backfill_inner: filtered_extremities %s", filtered_extremities + ) - if not filtered_extremities: + if not filtered_extremities and not insertion_events_to_be_backfilled: return False + extremities = { + **oldest_events_with_depth, + # TODO: insertion_events_to_be_backfilled is currently skipping the filtered_extremities checks + **insertion_events_to_be_backfilled, + } + # Check if we reached a point where we should start backfilling. sorted_extremeties_tuple = sorted(extremities.items(), key=lambda e: -int(e[1])) max_depth = sorted_extremeties_tuple[0][1] diff --git a/synapse/storage/database.py b/synapse/storage/database.py index c8015a3848..95d2caff62 100644 --- a/synapse/storage/database.py +++ b/synapse/storage/database.py @@ -941,13 +941,13 @@ async def simple_upsert( `lock` should generally be set to True (the default), but can be set to False if either of the following are true: - - * there is a UNIQUE INDEX on the key columns. In this case a conflict - will cause an IntegrityError in which case this function will retry - the update. - - * we somehow know that we are the only thread which will be updating - this table. + 1. there is a UNIQUE INDEX on the key columns. In this case a conflict + will cause an IntegrityError in which case this function will retry + the update. + 2. we somehow know that we are the only thread which will be updating + this table. + As an additional note, this parameter only matters for old SQLite versions + because we will use native upserts otherwise. Args: table: The table to upsert into diff --git a/synapse/storage/databases/main/event_federation.py b/synapse/storage/databases/main/event_federation.py index 44018c1c31..bddf5ef192 100644 --- a/synapse/storage/databases/main/event_federation.py +++ b/synapse/storage/databases/main/event_federation.py @@ -671,27 +671,97 @@ def _get_auth_chain_difference_txn( # Return all events where not all sets can reach them. return {eid for eid, n in event_to_missing_sets.items() if n} - async def get_oldest_events_with_depth_in_room(self, room_id): + async def get_oldest_event_ids_with_depth_in_room(self, room_id) -> Dict[str, int]: + """Gets the oldest events(backwards extremities) in the room along with the + aproximate depth. + + We use this function so that we can compare and see if someones current + depth at their current scrollback is within pagination range of the + event extremeties. If the current depth is close to the depth of given + oldest event, we can trigger a backfill. + + Args: + room_id: Room where we want to find the oldest events + + Returns: + Map from event_id to depth + """ + + def get_oldest_event_ids_with_depth_in_room_txn(txn, room_id): + # Assemble a dictionary with event_id -> depth for the oldest events + # we know of in the room. Backwards extremeties are the oldest + # events we know of in the room but we only know of them because + # some other event referenced them by prev_event and aren't peristed + # in our database yet (meaning we don't know their depth + # specifically). So we need to look for the aproximate depth from + # the events connected to the current backwards extremeties. + sql = """ + SELECT b.event_id, MAX(e.depth) FROM events as e + /** + * Get the edge connections from the event_edges table + * so we can see whether this event's prev_events points + * to a backward extremity in the next join. + */ + INNER JOIN event_edges as g + ON g.event_id = e.event_id + /** + * We find the "oldest" events in the room by looking for + * events connected to backwards extremeties (oldest events + * in the room that we know of so far). + */ + INNER JOIN event_backward_extremities as b + ON g.prev_event_id = b.event_id + WHERE b.room_id = ? AND g.is_state is ? + GROUP BY b.event_id + """ + + txn.execute(sql, (room_id, False)) + + return dict(txn) + return await self.db_pool.runInteraction( - "get_oldest_events_with_depth_in_room", - self.get_oldest_events_with_depth_in_room_txn, + "get_oldest_event_ids_with_depth_in_room", + get_oldest_event_ids_with_depth_in_room_txn, room_id, ) - def get_oldest_events_with_depth_in_room_txn(self, txn, room_id): - sql = ( - "SELECT b.event_id, MAX(e.depth) FROM events as e" - " INNER JOIN event_edges as g" - " ON g.event_id = e.event_id" - " INNER JOIN event_backward_extremities as b" - " ON g.prev_event_id = b.event_id" - " WHERE b.room_id = ? AND g.is_state is ?" - " GROUP BY b.event_id" - ) + async def get_insertion_event_backwards_extremities_in_room( + self, room_id + ) -> Dict[str, int]: + """Get the insertion events we know about that we haven't backfilled yet. - txn.execute(sql, (room_id, False)) + We use this function so that we can compare and see if someones current + depth at their current scrollback is within pagination range of the + insertion event. If the current depth is close to the depth of given + insertion event, we can trigger a backfill. - return dict(txn) + Args: + room_id: Room where we want to find the oldest events + + Returns: + Map from event_id to depth + """ + + def get_insertion_event_backwards_extremities_in_room_txn(txn, room_id): + sql = """ + SELECT b.event_id, MAX(e.depth) FROM insertion_events as i + /* We only want insertion events that are also marked as backwards extremities */ + INNER JOIN insertion_event_extremities as b USING (event_id) + /* Get the depth of the insertion event from the events table */ + INNER JOIN events AS e USING (event_id) + WHERE b.room_id = ? + GROUP BY b.event_id + """ + + txn.execute(sql, (room_id,)) + + return dict(txn) + + return await self.db_pool.runInteraction( + "get_insertion_event_backwards_extremities_in_room", + get_insertion_event_backwards_extremities_in_room_txn, + room_id, + ) async def get_max_depth_of(self, event_ids: List[str]) -> Tuple[str, int]: """Returns the event ID and depth for the event that has the max depth from a set of event IDs @@ -1041,7 +1111,6 @@ def _get_backfill_events(self, txn, room_id, event_list, limit): if row[1] not in event_results: queue.put((-row[0], row[1])) - # Navigate up the DAG by prev_event txn.execute(query, (event_id, False, limit - len(event_results))) prev_event_id_results = txn.fetchall() logger.debug( @@ -1136,6 +1205,19 @@ def _delete_old_forward_extrem_cache_txn(txn): _delete_old_forward_extrem_cache_txn, ) + async def insert_insertion_extremity(self, event_id: str, room_id: str) -> None: + await self.db_pool.simple_upsert( + table="insertion_event_extremities", + keyvalues={"event_id": event_id}, + values={ + "event_id": event_id, + "room_id": room_id, + }, + insertion_values={}, + desc="insert_insertion_extremity", + lock=False, + ) + async def insert_received_event_to_staging( self, origin: str, event: EventBase ) -> None: diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index 86baf397fb..40b53274fb 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -1845,6 +1845,18 @@ def _handle_chunk_event(self, txn: LoggingTransaction, event: EventBase): }, ) + # When we receive an event with a `chunk_id` referencing the + # `next_chunk_id` of the insertion event, we can remove it from the + # `insertion_event_extremities` table. + sql = """ + DELETE FROM insertion_event_extremities WHERE event_id IN ( + SELECT event_id FROM insertion_events + WHERE next_chunk_id = ? + ) + """ + + txn.execute(sql, (chunk_id,)) + def _handle_redaction(self, txn, redacted_event_id): """Handles receiving a redaction and checking whether we need to remove any redacted relations from the database. @@ -2101,15 +2113,17 @@ def _update_backward_extremeties(self, txn, events): Forward extremities are handled when we first start persisting the events. """ + # From the events passed in, add all of the prev events as backwards extremities. + # Ignore any events that are already backwards extrems or outliers. query = ( "INSERT INTO event_backward_extremities (event_id, room_id)" " SELECT ?, ? WHERE NOT EXISTS (" - " SELECT 1 FROM event_backward_extremities" - " WHERE event_id = ? AND room_id = ?" + " SELECT 1 FROM event_backward_extremities" + " WHERE event_id = ? AND room_id = ?" " )" " AND NOT EXISTS (" - " SELECT 1 FROM events WHERE event_id = ? AND room_id = ? " - " AND outlier = ?" + " SELECT 1 FROM events WHERE event_id = ? AND room_id = ? " + " AND outlier = ?" " )" ) @@ -2123,6 +2137,8 @@ def _update_backward_extremeties(self, txn, events): ], ) + # Delete all these events that we've already fetched and now know that their + # prev events are the new backwards extremeties. query = ( "DELETE FROM event_backward_extremities" " WHERE event_id = ? AND room_id = ?" diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index 36340a652a..fd4dd67d91 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -SCHEMA_VERSION = 61 +SCHEMA_VERSION = 62 """Represents the expectations made by the codebase about the database schema This should be incremented whenever the codebase changes its requirements on the diff --git a/synapse/storage/schema/main/delta/62/01insertion_event_extremities.sql b/synapse/storage/schema/main/delta/62/01insertion_event_extremities.sql new file mode 100644 index 0000000000..b731ef284a --- /dev/null +++ b/synapse/storage/schema/main/delta/62/01insertion_event_extremities.sql @@ -0,0 +1,24 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +-- Add a table that keeps track of which "insertion" events need to be backfilled +CREATE TABLE IF NOT EXISTS insertion_event_extremities( + event_id TEXT NOT NULL, + room_id TEXT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS insertion_event_extremities_event_id ON insertion_event_extremities(event_id); +CREATE INDEX IF NOT EXISTS insertion_event_extremities_room_id ON insertion_event_extremities(room_id); From 9db24cc50d252b1685a4ac69a736b49ed225dcb6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Aug 2021 18:39:57 +0100 Subject: [PATCH 517/619] Send unstable-prefixed room_type in store-invite IS API requests (#10435) The room type is per MSC3288 to allow the identity-server to change invitation wording based on whether the invitation is to a room or a space. The prefixed key will be replaced once MSC3288 is accepted into the spec. --- changelog.d/10435.feature | 1 + synapse/handlers/identity.py | 6 ++++++ synapse/handlers/room_member.py | 13 ++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10435.feature diff --git a/changelog.d/10435.feature b/changelog.d/10435.feature new file mode 100644 index 0000000000..f93ef5b415 --- /dev/null +++ b/changelog.d/10435.feature @@ -0,0 +1 @@ +Experimental support for [MSC3288](https://github.com/matrix-org/matrix-doc/pull/3288), sending `room_type` to the identity server for 3pid invites over the `/store-invite` API. diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 0961dec5ab..8ffeabacf9 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -824,6 +824,7 @@ async def ask_id_server_for_third_party_invite( room_avatar_url: str, room_join_rules: str, room_name: str, + room_type: Optional[str], inviter_display_name: str, inviter_avatar_url: str, id_access_token: Optional[str] = None, @@ -843,6 +844,7 @@ async def ask_id_server_for_third_party_invite( notifications. room_join_rules: The join rules of the email (e.g. "public"). room_name: The m.room.name of the room. + room_type: The type of the room from its m.room.create event (e.g "m.space"). inviter_display_name: The current display name of the inviter. inviter_avatar_url: The URL of the inviter's avatar. @@ -869,6 +871,10 @@ async def ask_id_server_for_third_party_invite( "sender_display_name": inviter_display_name, "sender_avatar_url": inviter_avatar_url, } + + if room_type is not None: + invite_config["org.matrix.msc3288.room_type"] = room_type + # If a custom web client location is available, include it in the request. if self._web_client_location: invite_config["org.matrix.web_client_location"] = self._web_client_location diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 65ad3efa6a..ba13196218 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -19,7 +19,12 @@ from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple from synapse import types -from synapse.api.constants import AccountDataTypes, EventTypes, Membership +from synapse.api.constants import ( + AccountDataTypes, + EventContentFields, + EventTypes, + Membership, +) from synapse.api.errors import ( AuthError, Codes, @@ -1237,6 +1242,11 @@ async def _make_and_store_3pid_invite( if room_name_event: room_name = room_name_event.content.get("name", "") + room_type = None + room_create_event = room_state.get((EventTypes.Create, "")) + if room_create_event: + room_type = room_create_event.content.get(EventContentFields.ROOM_TYPE) + room_join_rules = "" join_rules_event = room_state.get((EventTypes.JoinRules, "")) if join_rules_event: @@ -1263,6 +1273,7 @@ async def _make_and_store_3pid_invite( room_avatar_url=room_avatar_url, room_join_rules=room_join_rules, room_name=room_name, + room_type=room_type, inviter_display_name=inviter_display_name, inviter_avatar_url=inviter_avatar_url, id_access_token=id_access_token, From e33f14e8d51e33cb86d7791495b73ae4c1e784f9 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Thu, 5 Aug 2021 11:22:27 +0100 Subject: [PATCH 518/619] Don't fail CI when lint-newfile job was skipped (#10529) --- .github/workflows/tests.yml | 7 ++++++- changelog.d/10529.misc | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10529.misc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 239553ae13..75c2976a25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true - + jobs: lint: runs-on: ubuntu-latest @@ -374,6 +374,11 @@ jobs: rc=0 results=$(jq -r 'to_entries[] | [.key,.value.result] | join(" ")' <<< $NEEDS_CONTEXT) while read job result ; do + # The newsfile lint may be skipped on non PR builds + if [ $result == "skipped" ] && [ $job == "lint-newsfile" ]; then + continue + fi + if [ "$result" != "success" ]; then echo "::set-failed ::Job $job returned $result" rc=1 diff --git a/changelog.d/10529.misc b/changelog.d/10529.misc new file mode 100644 index 0000000000..4caf22523c --- /dev/null +++ b/changelog.d/10529.misc @@ -0,0 +1 @@ +Fix CI to not break when run against branches rather than pull requests. From 834cdc3606c9193f7b5a5e93936193b359222690 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 5 Aug 2021 13:20:05 +0200 Subject: [PATCH 519/619] Add documentation for configuring a forward proxy. (#10443) --- changelog.d/10443.doc | 1 + docs/SUMMARY.md | 1 + docs/setup/forward_proxy.md | 74 +++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 changelog.d/10443.doc create mode 100644 docs/setup/forward_proxy.md diff --git a/changelog.d/10443.doc b/changelog.d/10443.doc new file mode 100644 index 0000000000..3588e5487f --- /dev/null +++ b/changelog.d/10443.doc @@ -0,0 +1 @@ +Add documentation for configuration a forward proxy. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 10be12d638..3d320a1c43 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -7,6 +7,7 @@ - [Installation](setup/installation.md) - [Using Postgres](postgres.md) - [Configuring a Reverse Proxy](reverse_proxy.md) + - [Configuring a Forward/Outbound Proxy](setup/forward_proxy.md) - [Configuring a Turn Server](turn-howto.md) - [Delegation](delegate.md) diff --git a/docs/setup/forward_proxy.md b/docs/setup/forward_proxy.md new file mode 100644 index 0000000000..a0720ab342 --- /dev/null +++ b/docs/setup/forward_proxy.md @@ -0,0 +1,74 @@ +# Using a forward proxy with Synapse + +You can use Synapse with a forward or outbound proxy. An example of when +this is necessary is in corporate environments behind a DMZ (demilitarized zone). +Synapse supports routing outbound HTTP(S) requests via a proxy. Only HTTP(S) +proxy is supported, not SOCKS proxy or anything else. + +## Configure + +The `http_proxy`, `https_proxy`, `no_proxy` environment variables are used to +specify proxy settings. The environment variable is not case sensitive. +- `http_proxy`: Proxy server to use for HTTP requests. +- `https_proxy`: Proxy server to use for HTTPS requests. +- `no_proxy`: Comma-separated list of hosts, IP addresses, or IP ranges in CIDR + format which should not use the proxy. Synapse will directly connect to these hosts. + +The `http_proxy` and `https_proxy` environment variables have the form: `[scheme://][:@][:]` +- Supported schemes are `http://` and `https://`. The default scheme is `http://` + for compatibility reasons; it is recommended to set a scheme. If scheme is set + to `https://` the connection uses TLS between Synapse and the proxy. + + **NOTE**: Synapse validates the certificates. If the certificate is not + valid, then the connection is dropped. +- Default port if not given is `1080`. +- Username and password are optional and will be used to authenticate against + the proxy. + +**Examples** +- HTTP_PROXY=http://USERNAME:PASSWORD@10.0.1.1:8080/ +- HTTPS_PROXY=http://USERNAME:PASSWORD@proxy.example.com:8080/ +- NO_PROXY=master.hostname.example.com,10.1.0.0/16,172.30.0.0/16 + +**NOTE**: +Synapse does not apply the IP blacklist to connections through the proxy (since +the DNS resolution is done by the proxy). It is expected that the proxy or firewall +will apply blacklisting of IP addresses. + +## Connection types + +The proxy will be **used** for: + +- push +- url previews +- phone-home stats +- recaptcha validation +- CAS auth validation +- OpenID Connect +- Federation (checking public key revocation) + +It will **not be used** for: + +- Application Services +- Identity servers +- Outbound federation +- In worker configurations + - connections between workers + - connections from workers to Redis +- Fetching public keys of other servers +- Downloading remote media + +## Troubleshooting + +If a proxy server is used with TLS (HTTPS) and no connections are established, +it is most likely due to the proxy's certificates. To test this, the validation +in Synapse can be deactivated. + +**NOTE**: This has an impact on security and is for testing purposes only! + +To deactivate the certificate validation, the following setting must be made in +[homserver.yaml](../usage/configuration/homeserver_sample_config.md). + +```yaml +use_insecure_ssl_client_just_for_testing_do_not_use: true +``` From a8a27b2b8bac2995c3edd20518680366eb543ac9 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 5 Aug 2021 13:22:14 +0100 Subject: [PATCH 520/619] Only return an appservice protocol if it has a service providing it. (#10532) If there are no services providing a protocol, omit it completely instead of returning an empty dictionary. This fixes a long-standing spec compliance bug. --- changelog.d/10532.bugfix | 1 + synapse/handlers/appservice.py | 7 +- tests/handlers/test_appservice.py | 122 +++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10532.bugfix diff --git a/changelog.d/10532.bugfix b/changelog.d/10532.bugfix new file mode 100644 index 0000000000..d95e3d9b59 --- /dev/null +++ b/changelog.d/10532.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where protocols which are not implemented by any appservices were incorrectly returned via `GET /_matrix/client/r0/thirdparty/protocols`. diff --git a/synapse/handlers/appservice.py b/synapse/handlers/appservice.py index 21a17cd2e8..4ab4046650 100644 --- a/synapse/handlers/appservice.py +++ b/synapse/handlers/appservice.py @@ -392,9 +392,6 @@ async def get_3pe_protocols( protocols[p].append(info) def _merge_instances(infos: List[JsonDict]) -> JsonDict: - if not infos: - return {} - # Merge the 'instances' lists of multiple results, but just take # the other fields from the first as they ought to be identical # copy the result so as not to corrupt the cached one @@ -406,7 +403,9 @@ def _merge_instances(infos: List[JsonDict]) -> JsonDict: return combined - return {p: _merge_instances(protocols[p]) for p in protocols.keys()} + return { + p: _merge_instances(protocols[p]) for p in protocols.keys() if protocols[p] + } async def _get_services_for_event( self, event: EventBase diff --git a/tests/handlers/test_appservice.py b/tests/handlers/test_appservice.py index 024c5e963c..43998020b2 100644 --- a/tests/handlers/test_appservice.py +++ b/tests/handlers/test_appservice.py @@ -133,11 +133,131 @@ def test_query_room_alias_exists(self): self.assertEquals(result.room_id, room_id) self.assertEquals(result.servers, servers) - def _mkservice(self, is_interested): + def test_get_3pe_protocols_no_appservices(self): + self.mock_store.get_app_services.return_value = [] + response = self.successResultOf( + defer.ensureDeferred(self.handler.get_3pe_protocols("my-protocol")) + ) + self.mock_as_api.get_3pe_protocol.assert_not_called() + self.assertEquals(response, {}) + + def test_get_3pe_protocols_no_protocols(self): + service = self._mkservice(False, []) + self.mock_store.get_app_services.return_value = [service] + response = self.successResultOf( + defer.ensureDeferred(self.handler.get_3pe_protocols()) + ) + self.mock_as_api.get_3pe_protocol.assert_not_called() + self.assertEquals(response, {}) + + def test_get_3pe_protocols_protocol_no_response(self): + service = self._mkservice(False, ["my-protocol"]) + self.mock_store.get_app_services.return_value = [service] + self.mock_as_api.get_3pe_protocol.return_value = make_awaitable(None) + response = self.successResultOf( + defer.ensureDeferred(self.handler.get_3pe_protocols()) + ) + self.mock_as_api.get_3pe_protocol.assert_called_once_with( + service, "my-protocol" + ) + self.assertEquals(response, {}) + + def test_get_3pe_protocols_select_one_protocol(self): + service = self._mkservice(False, ["my-protocol"]) + self.mock_store.get_app_services.return_value = [service] + self.mock_as_api.get_3pe_protocol.return_value = make_awaitable( + {"x-protocol-data": 42, "instances": []} + ) + response = self.successResultOf( + defer.ensureDeferred(self.handler.get_3pe_protocols("my-protocol")) + ) + self.mock_as_api.get_3pe_protocol.assert_called_once_with( + service, "my-protocol" + ) + self.assertEquals( + response, {"my-protocol": {"x-protocol-data": 42, "instances": []}} + ) + + def test_get_3pe_protocols_one_protocol(self): + service = self._mkservice(False, ["my-protocol"]) + self.mock_store.get_app_services.return_value = [service] + self.mock_as_api.get_3pe_protocol.return_value = make_awaitable( + {"x-protocol-data": 42, "instances": []} + ) + response = self.successResultOf( + defer.ensureDeferred(self.handler.get_3pe_protocols()) + ) + self.mock_as_api.get_3pe_protocol.assert_called_once_with( + service, "my-protocol" + ) + self.assertEquals( + response, {"my-protocol": {"x-protocol-data": 42, "instances": []}} + ) + + def test_get_3pe_protocols_multiple_protocol(self): + service_one = self._mkservice(False, ["my-protocol"]) + service_two = self._mkservice(False, ["other-protocol"]) + self.mock_store.get_app_services.return_value = [service_one, service_two] + self.mock_as_api.get_3pe_protocol.return_value = make_awaitable( + {"x-protocol-data": 42, "instances": []} + ) + response = self.successResultOf( + defer.ensureDeferred(self.handler.get_3pe_protocols()) + ) + self.mock_as_api.get_3pe_protocol.assert_called() + self.assertEquals( + response, + { + "my-protocol": {"x-protocol-data": 42, "instances": []}, + "other-protocol": {"x-protocol-data": 42, "instances": []}, + }, + ) + + def test_get_3pe_protocols_multiple_info(self): + service_one = self._mkservice(False, ["my-protocol"]) + service_two = self._mkservice(False, ["my-protocol"]) + + async def get_3pe_protocol(service, unusedProtocol): + if service == service_one: + return { + "x-protocol-data": 42, + "instances": [{"desc": "Alice's service"}], + } + if service == service_two: + return { + "x-protocol-data": 36, + "x-not-used": 45, + "instances": [{"desc": "Bob's service"}], + } + raise Exception("Unexpected service") + + self.mock_store.get_app_services.return_value = [service_one, service_two] + self.mock_as_api.get_3pe_protocol = get_3pe_protocol + response = self.successResultOf( + defer.ensureDeferred(self.handler.get_3pe_protocols()) + ) + # It's expected that the second service's data doesn't appear in the response + self.assertEquals( + response, + { + "my-protocol": { + "x-protocol-data": 42, + "instances": [ + { + "desc": "Alice's service", + }, + {"desc": "Bob's service"}, + ], + }, + }, + ) + + def _mkservice(self, is_interested, protocols=None): service = Mock() service.is_interested.return_value = make_awaitable(is_interested) service.token = "mock_service_token" service.url = "mock_service_url" + service.protocols = protocols return service def _mkservice_alias(self, is_interested_in_alias): From 3b354faad0e6b1f41ed5dd0269a1785d3f505465 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 5 Aug 2021 08:39:17 -0400 Subject: [PATCH 521/619] Refactoring before implementing the updated spaces summary. (#10527) This should have no user-visible changes, but refactors some pieces of the SpaceSummaryHandler before adding support for the updated MSC2946. --- changelog.d/10527.misc | 1 + synapse/federation/federation_client.py | 23 +-- synapse/handlers/space_summary.py | 125 +++++++++------- tests/handlers/test_space_summary.py | 185 ++++++++++++++---------- 4 files changed, 198 insertions(+), 136 deletions(-) create mode 100644 changelog.d/10527.misc diff --git a/changelog.d/10527.misc b/changelog.d/10527.misc new file mode 100644 index 0000000000..3cf22f9daf --- /dev/null +++ b/changelog.d/10527.misc @@ -0,0 +1 @@ +Prepare for the new spaces summary endpoint (updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946)). diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b7a10da15a..007d1a27dc 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1290,7 +1290,7 @@ async def send_request(destination: str) -> FederationSpaceSummaryResult: ) -@attr.s(frozen=True, slots=True) +@attr.s(frozen=True, slots=True, auto_attribs=True) class FederationSpaceSummaryEventResult: """Represents a single event in the result of a successful get_space_summary call. @@ -1299,12 +1299,13 @@ class FederationSpaceSummaryEventResult: object attributes. """ - event_type = attr.ib(type=str) - state_key = attr.ib(type=str) - via = attr.ib(type=Sequence[str]) + event_type: str + room_id: str + state_key: str + via: Sequence[str] # the raw data, including the above keys - data = attr.ib(type=JsonDict) + data: JsonDict @classmethod def from_json_dict(cls, d: JsonDict) -> "FederationSpaceSummaryEventResult": @@ -1321,6 +1322,10 @@ def from_json_dict(cls, d: JsonDict) -> "FederationSpaceSummaryEventResult": if not isinstance(event_type, str): raise ValueError("Invalid event: 'event_type' must be a str") + room_id = d.get("room_id") + if not isinstance(room_id, str): + raise ValueError("Invalid event: 'room_id' must be a str") + state_key = d.get("state_key") if not isinstance(state_key, str): raise ValueError("Invalid event: 'state_key' must be a str") @@ -1335,15 +1340,15 @@ def from_json_dict(cls, d: JsonDict) -> "FederationSpaceSummaryEventResult": if any(not isinstance(v, str) for v in via): raise ValueError("Invalid event: 'via' must be a list of strings") - return cls(event_type, state_key, via, d) + return cls(event_type, room_id, state_key, via, d) -@attr.s(frozen=True, slots=True) +@attr.s(frozen=True, slots=True, auto_attribs=True) class FederationSpaceSummaryResult: """Represents the data returned by a successful get_space_summary call.""" - rooms = attr.ib(type=Sequence[JsonDict]) - events = attr.ib(type=Sequence[FederationSpaceSummaryEventResult]) + rooms: Sequence[JsonDict] + events: Sequence[FederationSpaceSummaryEventResult] @classmethod def from_json_dict(cls, d: JsonDict) -> "FederationSpaceSummaryResult": diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 5f7d4602bd..3eb232c83e 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -16,7 +16,17 @@ import logging import re from collections import deque -from typing import TYPE_CHECKING, Iterable, List, Optional, Sequence, Set, Tuple +from typing import ( + TYPE_CHECKING, + Collection, + Dict, + Iterable, + List, + Optional, + Sequence, + Set, + Tuple, +) import attr @@ -116,20 +126,22 @@ async def get_space_summary( max_children = max_rooms_per_space if processed_rooms else None if is_in_room: - room, events = await self._summarize_local_room( + room_entry = await self._summarize_local_room( requester, None, room_id, suggested_only, max_children ) + events: Collection[JsonDict] = [] + if room_entry: + rooms_result.append(room_entry.room) + events = room_entry.children + logger.debug( "Query of local room %s returned events %s", room_id, ["%s->%s" % (ev["room_id"], ev["state_key"]) for ev in events], ) - - if room: - rooms_result.append(room) else: - fed_rooms, fed_events = await self._summarize_remote_room( + fed_rooms = await self._summarize_remote_room( queue_entry, suggested_only, max_children, @@ -141,12 +153,10 @@ async def get_space_summary( # user is not permitted see. # # Filter the returned results to only what is accessible to the user. - room_ids = set() events = [] - for room in fed_rooms: - fed_room_id = room.get("room_id") - if not fed_room_id or not isinstance(fed_room_id, str): - continue + for room_entry in fed_rooms: + room = room_entry.room + fed_room_id = room_entry.room_id # The room should only be included in the summary if: # a. the user is in the room; @@ -189,21 +199,17 @@ async def get_space_summary( # The user can see the room, include it! if include_room: rooms_result.append(room) - room_ids.add(fed_room_id) + events.extend(room_entry.children) # All rooms returned don't need visiting again (even if the user # didn't have access to them). processed_rooms.add(fed_room_id) - for event in fed_events: - if event.get("room_id") in room_ids: - events.append(event) - logger.debug( "Query of %s returned rooms %s, events %s", room_id, - [room.get("room_id") for room in fed_rooms], - ["%s->%s" % (ev["room_id"], ev["state_key"]) for ev in fed_events], + [room_entry.room.get("room_id") for room_entry in fed_rooms], + ["%s->%s" % (ev["room_id"], ev["state_key"]) for ev in events], ) # the room we queried may or may not have been returned, but don't process @@ -283,20 +289,20 @@ async def federation_space_summary( # already done this room continue - logger.debug("Processing room %s", room_id) - - room, events = await self._summarize_local_room( + room_entry = await self._summarize_local_room( None, origin, room_id, suggested_only, max_rooms_per_space ) processed_rooms.add(room_id) - if room: - rooms_result.append(room) - events_result.extend(events) + if room_entry: + rooms_result.append(room_entry.room) + events_result.extend(room_entry.children) - # add any children to the queue - room_queue.extend(edge_event["state_key"] for edge_event in events) + # add any children to the queue + room_queue.extend( + edge_event["state_key"] for edge_event in room_entry.children + ) return {"rooms": rooms_result, "events": events_result} @@ -307,7 +313,7 @@ async def _summarize_local_room( room_id: str, suggested_only: bool, max_children: Optional[int], - ) -> Tuple[Optional[JsonDict], Sequence[JsonDict]]: + ) -> Optional["_RoomEntry"]: """ Generate a room entry and a list of event entries for a given room. @@ -326,21 +332,16 @@ async def _summarize_local_room( to a server-set limit. Returns: - A tuple of: - The room information, if the room should be returned to the - user. None, otherwise. - - An iterable of the sorted children events. This may be limited - to a maximum size or may include all children. + A room entry if the room should be returned. None, otherwise. """ if not await self._is_room_accessible(room_id, requester, origin): - return None, () + return None room_entry = await self._build_room_entry(room_id) # If the room is not a space, return just the room information. if room_entry.get("room_type") != RoomTypes.SPACE: - return room_entry, () + return _RoomEntry(room_id, room_entry) # Otherwise, look for child rooms/spaces. child_events = await self._get_child_events(room_id) @@ -363,7 +364,7 @@ async def _summarize_local_room( ) ) - return room_entry, events_result + return _RoomEntry(room_id, room_entry, events_result) async def _summarize_remote_room( self, @@ -371,7 +372,7 @@ async def _summarize_remote_room( suggested_only: bool, max_children: Optional[int], exclude_rooms: Iterable[str], - ) -> Tuple[Sequence[JsonDict], Sequence[JsonDict]]: + ) -> Iterable["_RoomEntry"]: """ Request room entries and a list of event entries for a given room by querying a remote server. @@ -386,11 +387,7 @@ async def _summarize_remote_room( Rooms IDs which do not need to be summarized. Returns: - A tuple of: - An iterable of rooms. - - An iterable of the sorted children events. This may be limited - to a maximum size or may include all children. + An iterable of room entries. """ room_id = room.room_id logger.info("Requesting summary for %s via %s", room_id, room.via) @@ -414,11 +411,30 @@ async def _summarize_remote_room( e, exc_info=logger.isEnabledFor(logging.DEBUG), ) - return (), () + return () + + # Group the events by their room. + children_by_room: Dict[str, List[JsonDict]] = {} + for ev in res.events: + if ev.event_type == EventTypes.SpaceChild: + children_by_room.setdefault(ev.room_id, []).append(ev.data) + + # Generate the final results. + results = [] + for fed_room in res.rooms: + fed_room_id = fed_room.get("room_id") + if not fed_room_id or not isinstance(fed_room_id, str): + continue - return res.rooms, tuple( - ev.data for ev in res.events if ev.event_type == EventTypes.SpaceChild - ) + results.append( + _RoomEntry( + fed_room_id, + fed_room, + children_by_room.get(fed_room_id, []), + ) + ) + + return results async def _is_room_accessible( self, room_id: str, requester: Optional[str], origin: Optional[str] @@ -606,10 +622,21 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: return sorted(filter(_has_valid_via, events), key=_child_events_comparison_key) -@attr.s(frozen=True, slots=True) +@attr.s(frozen=True, slots=True, auto_attribs=True) class _RoomQueueEntry: - room_id = attr.ib(type=str) - via = attr.ib(type=Sequence[str]) + room_id: str + via: Sequence[str] + + +@attr.s(frozen=True, slots=True, auto_attribs=True) +class _RoomEntry: + room_id: str + # The room summary for this room. + room: JsonDict + # An iterable of the sorted, stripped children events for children of this room. + # + # This may not include all children. + children: Collection[JsonDict] = () def _has_valid_via(e: EventBase) -> bool: diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 3f73ad7f94..f982a8c8b4 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -26,7 +26,7 @@ from synapse.api.errors import AuthError from synapse.api.room_versions import RoomVersions from synapse.events import make_event_from_dict -from synapse.handlers.space_summary import _child_events_comparison_key +from synapse.handlers.space_summary import _child_events_comparison_key, _RoomEntry from synapse.rest import admin from synapse.rest.client.v1 import login, room from synapse.server import HomeServer @@ -351,26 +351,30 @@ async def summarize_remote_room( # events before child events). # Note that these entries are brief, but should contain enough info. - rooms = [ - { - "room_id": subspace, - "world_readable": True, - "room_type": RoomTypes.SPACE, - }, - { - "room_id": subroom, - "world_readable": True, - }, - ] - event_content = {"via": [fed_hostname]} - events = [ - { - "room_id": subspace, - "state_key": subroom, - "content": event_content, - }, + return [ + _RoomEntry( + subspace, + { + "room_id": subspace, + "world_readable": True, + "room_type": RoomTypes.SPACE, + }, + [ + { + "room_id": subspace, + "state_key": subroom, + "content": {"via": [fed_hostname]}, + } + ], + ), + _RoomEntry( + subroom, + { + "room_id": subroom, + "world_readable": True, + }, + ), ] - return rooms, events # Add a room to the space which is on another server. self._add_child(self.space, subspace, self.token) @@ -436,70 +440,95 @@ async def summarize_remote_room( ): # Note that these entries are brief, but should contain enough info. rooms = [ - { - "room_id": public_room, - "world_readable": False, - "join_rules": JoinRules.PUBLIC, - }, - { - "room_id": knock_room, - "world_readable": False, - "join_rules": JoinRules.KNOCK, - }, - { - "room_id": not_invited_room, - "world_readable": False, - "join_rules": JoinRules.INVITE, - }, - { - "room_id": invited_room, - "world_readable": False, - "join_rules": JoinRules.INVITE, - }, - { - "room_id": restricted_room, - "world_readable": False, - "join_rules": JoinRules.MSC3083_RESTRICTED, - "allowed_spaces": [], - }, - { - "room_id": restricted_accessible_room, - "world_readable": False, - "join_rules": JoinRules.MSC3083_RESTRICTED, - "allowed_spaces": [self.room], - }, - { - "room_id": world_readable_room, - "world_readable": True, - "join_rules": JoinRules.INVITE, - }, - { - "room_id": joined_room, - "world_readable": False, - "join_rules": JoinRules.INVITE, - }, - ] - - # Place each room in the sub-space. - event_content = {"via": [fed_hostname]} - events = [ - { - "room_id": subspace, - "state_key": room["room_id"], - "content": event_content, - } - for room in rooms + _RoomEntry( + public_room, + { + "room_id": public_room, + "world_readable": False, + "join_rules": JoinRules.PUBLIC, + }, + ), + _RoomEntry( + knock_room, + { + "room_id": knock_room, + "world_readable": False, + "join_rules": JoinRules.KNOCK, + }, + ), + _RoomEntry( + not_invited_room, + { + "room_id": not_invited_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ), + _RoomEntry( + invited_room, + { + "room_id": invited_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ), + _RoomEntry( + restricted_room, + { + "room_id": restricted_room, + "world_readable": False, + "join_rules": JoinRules.MSC3083_RESTRICTED, + "allowed_spaces": [], + }, + ), + _RoomEntry( + restricted_accessible_room, + { + "room_id": restricted_accessible_room, + "world_readable": False, + "join_rules": JoinRules.MSC3083_RESTRICTED, + "allowed_spaces": [self.room], + }, + ), + _RoomEntry( + world_readable_room, + { + "room_id": world_readable_room, + "world_readable": True, + "join_rules": JoinRules.INVITE, + }, + ), + _RoomEntry( + joined_room, + { + "room_id": joined_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ), ] # Also include the subspace. rooms.insert( 0, - { - "room_id": subspace, - "world_readable": True, - }, + _RoomEntry( + subspace, + { + "room_id": subspace, + "world_readable": True, + }, + # Place each room in the sub-space. + [ + { + "room_id": subspace, + "state_key": room.room_id, + "content": {"via": [fed_hostname]}, + } + for room in rooms + ], + ), ) - return rooms, events + return rooms # Add a room to the space which is on another server. self._add_child(self.space, subspace, self.token) From 457853100240fc5e015e10a62ecffdd799128b0c Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Thu, 5 Aug 2021 20:00:44 +0200 Subject: [PATCH 522/619] fix broken links in `upgrade.md` (#10543) Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10543.doc | 1 + docs/upgrade.md | 51 +++++++++++++++++++------------------------ 2 files changed, 24 insertions(+), 28 deletions(-) create mode 100644 changelog.d/10543.doc diff --git a/changelog.d/10543.doc b/changelog.d/10543.doc new file mode 100644 index 0000000000..6c06722eb4 --- /dev/null +++ b/changelog.d/10543.doc @@ -0,0 +1 @@ +Fix broken links in `upgrade.md`. Contributed by @dklimpel. diff --git a/docs/upgrade.md b/docs/upgrade.md index c8f4a2c171..ce9167e6de 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -142,9 +142,9 @@ SQLite databases are unaffected by this change. The current spam checker interface is deprecated in favour of a new generic modules system. Authors of spam checker modules can refer to [this -documentation](https://matrix-org.github.io/synapse/develop/modules.html#porting-an-existing-module-that-uses-the-old-interface) +documentation](modules.md#porting-an-existing-module-that-uses-the-old-interface) to update their modules. Synapse administrators can refer to [this -documentation](https://matrix-org.github.io/synapse/develop/modules.html#using-modules) +documentation](modules.md#using-modules) to update their configuration once the modules they are using have been updated. We plan to remove support for the current spam checker interface in August 2021. @@ -217,8 +217,7 @@ Instructions for doing so are provided ## Dropping support for old Python, Postgres and SQLite versions -In line with our [deprecation -policy](https://github.com/matrix-org/synapse/blob/release-v1.32.0/docs/deprecation_policy.md), +In line with our [deprecation policy](deprecation_policy.md), we've dropped support for Python 3.5 and PostgreSQL 9.5, as they are no longer supported upstream. @@ -231,8 +230,7 @@ The deprecated v1 "list accounts" admin API (`GET /_synapse/admin/v1/users/`) has been removed in this version. -The [v2 list accounts -API](https://github.com/matrix-org/synapse/blob/master/docs/admin_api/user_admin_api.rst#list-accounts) +The [v2 list accounts API](admin_api/user_admin_api.md#list-accounts) has been available since Synapse 1.7.0 (2019-12-13), and is accessible under `GET /_synapse/admin/v2/users`. @@ -267,7 +265,7 @@ by the client. Synapse also requires the [Host]{.title-ref} header to be preserved. -See the [reverse proxy documentation](../reverse_proxy.md), where the +See the [reverse proxy documentation](reverse_proxy.md), where the example configurations have been updated to show how to set these headers. @@ -286,7 +284,7 @@ identity providers: `[synapse public baseurl]/_synapse/client/oidc/callback` to the list of permitted "redirect URIs" at the identity provider. - See the [OpenID docs](../openid.md) for more information on setting + See the [OpenID docs](openid.md) for more information on setting up OpenID Connect. - If your server is configured for single sign-on via a SAML2 identity @@ -486,8 +484,7 @@ lock down external access to the Admin API endpoints. This release deprecates use of the `structured: true` logging configuration for structured logging. If your logging configuration contains `structured: true` then it should be modified based on the -[structured logging -documentation](../structured_logging.md). +[structured logging documentation](structured_logging.md). The `structured` and `drains` logging options are now deprecated and should be replaced by standard logging configuration of `handlers` and @@ -517,14 +514,13 @@ acts the same as the `http_client` argument previously passed to ## Forwarding `/_synapse/client` through your reverse proxy -The [reverse proxy -documentation](https://github.com/matrix-org/synapse/blob/develop/docs/reverse_proxy.md) +The [reverse proxy documentation](reverse_proxy.md) has been updated to include reverse proxy directives for `/_synapse/client/*` endpoints. As the user password reset flow now uses endpoints under this prefix, **you must update your reverse proxy configurations for user password reset to work**. -Additionally, note that the [Synapse worker documentation](https://github.com/matrix-org/synapse/blob/develop/docs/workers.md) has been updated to +Additionally, note that the [Synapse worker documentation](workers.md) has been updated to : state that the `/_synapse/client/password_reset/email/submit_token` endpoint can be handled @@ -588,7 +584,7 @@ updated. When setting up worker processes, we now recommend the use of a Redis server for replication. **The old direct TCP connection method is deprecated and will be removed in a future release.** See -[workers](../workers.md) for more details. +[workers](workers.md) for more details. # Upgrading to v1.14.0 @@ -720,8 +716,7 @@ participating in many rooms. omitting the `CONCURRENTLY` keyword. Note however that this operation may in itself cause Synapse to stop running for some time. Synapse admins are reminded that [SQLite is not recommended for use - outside a test - environment](https://github.com/matrix-org/synapse/blob/master/README.rst#using-postgresql). + outside a test environment](postgres.md). 3. Once the index has been created, the `SELECT` query in step 1 above should complete quickly. It is therefore safe to upgrade to Synapse @@ -739,7 +734,7 @@ participating in many rooms. Synapse will now log a warning on start up if used with a PostgreSQL database that has a non-recommended locale set. -See [Postgres](../postgres.md) for details. +See [Postgres](postgres.md) for details. # Upgrading to v1.8.0 @@ -856,8 +851,8 @@ section headed `email`, and be sure to have at least the You may also need to set `smtp_user`, `smtp_pass`, and `require_transport_security`. -See the [sample configuration file](docs/sample_config.yaml) for more -details on these settings. +See the [sample configuration file](usage/configuration/homeserver_sample_config.md) +for more details on these settings. #### Delegate email to an identity server @@ -959,7 +954,7 @@ back to v1.3.1, subject to the following: Some counter metrics have been renamed, with the old names deprecated. See [the metrics -documentation](../metrics-howto.md#renaming-of-metrics--deprecation-of-old-names-in-12) +documentation](metrics-howto.md#renaming-of-metrics--deprecation-of-old-names-in-12) for details. # Upgrading to v1.1.0 @@ -995,7 +990,7 @@ more details on upgrading your database. Synapse v1.0 is the first release to enforce validation of TLS certificates for the federation API. It is therefore essential that your certificates are correctly configured. See the -[FAQ](../MSC1711_certificates_FAQ.md) for more information. +[FAQ](MSC1711_certificates_FAQ.md) for more information. Note, v1.0 installations will also no longer be able to federate with servers that have not correctly configured their certificates. @@ -1010,8 +1005,8 @@ ways:- - Configure a whitelist of server domains to trust via `federation_certificate_verification_whitelist`. -See the [sample configuration file](docs/sample_config.yaml) for more -details on these settings. +See the [sample configuration file](usage/configuration/homeserver_sample_config.md) +for more details on these settings. ## Email @@ -1036,8 +1031,8 @@ If you are absolutely certain that you wish to continue using an identity server for password resets, set `trust_identity_server_for_password_resets` to `true`. -See the [sample configuration file](docs/sample_config.yaml) for more -details on these settings. +See the [sample configuration file](usage/configuration/homeserver_sample_config.md) +for more details on these settings. ## New email templates @@ -1057,11 +1052,11 @@ sent to them. Please be aware that, before Synapse v1.0 is released around March 2019, you will need to replace any self-signed certificates with those -verified by a root CA. Information on how to do so can be found at [the -ACME docs](../ACME.md). +verified by a root CA. Information on how to do so can be found at the +ACME docs. For more information on configuring TLS certificates see the -[FAQ](../MSC1711_certificates_FAQ.md). +[FAQ](MSC1711_certificates_FAQ.md). # Upgrading to v0.34.0 From f5a368bb48df85dd488afdead01a39f77f50de99 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 5 Aug 2021 20:35:53 -0500 Subject: [PATCH 523/619] Mark all MSC2716 events as historical (#10537) * Mark all MSC2716 events as historical --- changelog.d/10537.misc | 1 + synapse/rest/client/v1/room.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10537.misc diff --git a/changelog.d/10537.misc b/changelog.d/10537.misc new file mode 100644 index 0000000000..c9e045300c --- /dev/null +++ b/changelog.d/10537.misc @@ -0,0 +1 @@ +Mark all events stemming from the MSC2716 `/batch_send` endpoint as historical. diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 502a917588..982f134148 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -458,6 +458,9 @@ async def on_POST(self, request, room_id): "state_key": state_event["state_key"], } + # Mark all events as historical + event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True + # Make the state events float off on their own fake_prev_event_id = "$" + random_string(43) @@ -562,7 +565,10 @@ async def on_POST(self, request, room_id): "type": EventTypes.MSC2716_CHUNK, "sender": requester.user.to_string(), "room_id": room_id, - "content": {EventContentFields.MSC2716_CHUNK_ID: chunk_id_to_connect_to}, + "content": { + EventContentFields.MSC2716_CHUNK_ID: chunk_id_to_connect_to, + EventContentFields.MSC2716_HISTORICAL: True, + }, # Since the chunk event is put at the end of the chunk, # where the newest-in-time event is, copy the origin_server_ts from # the last event we're inserting @@ -589,10 +595,6 @@ async def on_POST(self, request, room_id): for ev in events_to_create: assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"]) - # Mark all events as historical - # This has important semantics within the Synapse internals to backfill properly - ev["content"][EventContentFields.MSC2716_HISTORICAL] = True - event_dict = { "type": ev["type"], "origin_server_ts": ev["origin_server_ts"], @@ -602,6 +604,9 @@ async def on_POST(self, request, room_id): "prev_events": prev_event_ids.copy(), } + # Mark all events as historical + event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True + event, context = await self.event_creation_handler.create_event( await self._create_requester_for_user_id_from_app_service( ev["sender"], requester.app_service From 74d7336686e7de1d0923d67af61b510ec801fa84 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 6 Aug 2021 11:13:34 +0100 Subject: [PATCH 524/619] Add a setting to disable TLS for sending email (#10546) This is mostly useful in case the server offers TLS, but doesn't present a valid certificate. --- changelog.d/10546.feature | 1 + docs/sample_config.yaml | 8 ++ synapse/config/emailconfig.py | 14 +++ synapse/handlers/send_email.py | 94 +++++++++++++++++---- synapse/server.py | 6 -- tests/push/test_email.py | 20 +++-- tests/rest/client/v2_alpha/test_account.py | 33 +++++--- tests/rest/client/v2_alpha/test_register.py | 12 +-- 8 files changed, 138 insertions(+), 50 deletions(-) create mode 100644 changelog.d/10546.feature diff --git a/changelog.d/10546.feature b/changelog.d/10546.feature new file mode 100644 index 0000000000..7709d010b3 --- /dev/null +++ b/changelog.d/10546.feature @@ -0,0 +1 @@ +Add a setting to disable TLS when sending email. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 16843dd8c9..aeebcaf45f 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -2242,6 +2242,14 @@ email: # #require_transport_security: true + # Uncomment the following to disable TLS for SMTP. + # + # By default, if the server supports TLS, it will be used, and the server + # must present a certificate that is valid for 'smtp_host'. If this option + # is set to false, TLS will not be used. + # + #enable_tls: false + # notif_from defines the "From" address to use when sending emails. # It must be set if email sending is enabled. # diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 8d8f166e9b..42526502f0 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -80,6 +80,12 @@ def read_config(self, config, **kwargs): self.require_transport_security = email_config.get( "require_transport_security", False ) + self.enable_smtp_tls = email_config.get("enable_tls", True) + if self.require_transport_security and not self.enable_smtp_tls: + raise ConfigError( + "email.require_transport_security requires email.enable_tls to be true" + ) + if "app_name" in email_config: self.email_app_name = email_config["app_name"] else: @@ -368,6 +374,14 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # #require_transport_security: true + # Uncomment the following to disable TLS for SMTP. + # + # By default, if the server supports TLS, it will be used, and the server + # must present a certificate that is valid for 'smtp_host'. If this option + # is set to false, TLS will not be used. + # + #enable_tls: false + # notif_from defines the "From" address to use when sending emails. # It must be set if email sending is enabled. # diff --git a/synapse/handlers/send_email.py b/synapse/handlers/send_email.py index e9f6aef06f..dda9659c11 100644 --- a/synapse/handlers/send_email.py +++ b/synapse/handlers/send_email.py @@ -16,7 +16,12 @@ import logging from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from typing import TYPE_CHECKING +from io import BytesIO +from typing import TYPE_CHECKING, Optional + +from twisted.internet.defer import Deferred +from twisted.internet.interfaces import IReactorTCP +from twisted.mail.smtp import ESMTPSenderFactory from synapse.logging.context import make_deferred_yieldable @@ -26,19 +31,75 @@ logger = logging.getLogger(__name__) +async def _sendmail( + reactor: IReactorTCP, + smtphost: str, + smtpport: int, + from_addr: str, + to_addr: str, + msg_bytes: bytes, + username: Optional[bytes] = None, + password: Optional[bytes] = None, + require_auth: bool = False, + require_tls: bool = False, + tls_hostname: Optional[str] = None, +) -> None: + """A simple wrapper around ESMTPSenderFactory, to allow substitution in tests + + Params: + reactor: reactor to use to make the outbound connection + smtphost: hostname to connect to + smtpport: port to connect to + from_addr: "From" address for email + to_addr: "To" address for email + msg_bytes: Message content + username: username to authenticate with, if auth is enabled + password: password to give when authenticating + require_auth: if auth is not offered, fail the request + require_tls: if TLS is not offered, fail the reqest + tls_hostname: TLS hostname to check for. None to disable TLS. + """ + msg = BytesIO(msg_bytes) + + d: "Deferred[object]" = Deferred() + + factory = ESMTPSenderFactory( + username, + password, + from_addr, + to_addr, + msg, + d, + heloFallback=True, + requireAuthentication=require_auth, + requireTransportSecurity=require_tls, + hostname=tls_hostname, + ) + + # the IReactorTCP interface claims host has to be a bytes, which seems to be wrong + reactor.connectTCP(smtphost, smtpport, factory, timeout=30, bindAddress=None) # type: ignore[arg-type] + + await make_deferred_yieldable(d) + + class SendEmailHandler: def __init__(self, hs: "HomeServer"): self.hs = hs - self._sendmail = hs.get_sendmail() self._reactor = hs.get_reactor() self._from = hs.config.email.email_notif_from self._smtp_host = hs.config.email.email_smtp_host self._smtp_port = hs.config.email.email_smtp_port - self._smtp_user = hs.config.email.email_smtp_user - self._smtp_pass = hs.config.email.email_smtp_pass + + user = hs.config.email.email_smtp_user + self._smtp_user = user.encode("utf-8") if user is not None else None + passwd = hs.config.email.email_smtp_pass + self._smtp_pass = passwd.encode("utf-8") if passwd is not None else None self._require_transport_security = hs.config.email.require_transport_security + self._enable_tls = hs.config.email.enable_smtp_tls + + self._sendmail = _sendmail async def send_email( self, @@ -82,17 +143,16 @@ async def send_email( logger.info("Sending email to %s" % email_address) - await make_deferred_yieldable( - self._sendmail( - self._smtp_host, - raw_from, - raw_to, - multipart_msg.as_string().encode("utf8"), - reactor=self._reactor, - port=self._smtp_port, - requireAuthentication=self._smtp_user is not None, - username=self._smtp_user, - password=self._smtp_pass, - requireTransportSecurity=self._require_transport_security, - ) + await self._sendmail( + self._reactor, + self._smtp_host, + self._smtp_port, + raw_from, + raw_to, + multipart_msg.as_string().encode("utf8"), + username=self._smtp_user, + password=self._smtp_pass, + require_auth=self._smtp_user is not None, + require_tls=self._require_transport_security, + tls_hostname=self._smtp_host if self._enable_tls else None, ) diff --git a/synapse/server.py b/synapse/server.py index 095dba9ad0..6c867f0f47 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -34,8 +34,6 @@ ) import twisted.internet.tcp -from twisted.internet import defer -from twisted.mail.smtp import sendmail from twisted.web.iweb import IPolicyForHTTPS from twisted.web.resource import IResource @@ -442,10 +440,6 @@ def get_room_creation_handler(self) -> RoomCreationHandler: def get_room_shutdown_handler(self) -> RoomShutdownHandler: return RoomShutdownHandler(self) - @cache_in_self - def get_sendmail(self) -> Callable[..., defer.Deferred]: - return sendmail - @cache_in_self def get_state_handler(self) -> StateHandler: return StateHandler(self) diff --git a/tests/push/test_email.py b/tests/push/test_email.py index e04bc5c9a6..a487706758 100644 --- a/tests/push/test_email.py +++ b/tests/push/test_email.py @@ -45,14 +45,6 @@ class EmailPusherTests(HomeserverTestCase): def make_homeserver(self, reactor, clock): - # List[Tuple[Deferred, args, kwargs]] - self.email_attempts = [] - - def sendmail(*args, **kwargs): - d = Deferred() - self.email_attempts.append((d, args, kwargs)) - return d - config = self.default_config() config["email"] = { "enable_notifs": True, @@ -75,7 +67,17 @@ def sendmail(*args, **kwargs): config["public_baseurl"] = "aaa" config["start_pushers"] = True - hs = self.setup_test_homeserver(config=config, sendmail=sendmail) + hs = self.setup_test_homeserver(config=config) + + # List[Tuple[Deferred, args, kwargs]] + self.email_attempts = [] + + def sendmail(*args, **kwargs): + d = Deferred() + self.email_attempts.append((d, args, kwargs)) + return d + + hs.get_send_email_handler()._sendmail = sendmail return hs diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index 317a2287e3..e7e617e9df 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -47,12 +47,6 @@ def make_homeserver(self, reactor, clock): config = self.default_config() # Email config. - self.email_attempts = [] - - async def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): - self.email_attempts.append(msg) - return - config["email"] = { "enable_notifs": False, "template_dir": os.path.abspath( @@ -67,7 +61,16 @@ async def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): } config["public_baseurl"] = "https://example.com" - hs = self.setup_test_homeserver(config=config, sendmail=sendmail) + hs = self.setup_test_homeserver(config=config) + + async def sendmail( + reactor, smtphost, smtpport, from_addr, to_addrs, msg, **kwargs + ): + self.email_attempts.append(msg) + + self.email_attempts = [] + hs.get_send_email_handler()._sendmail = sendmail + return hs def prepare(self, reactor, clock, hs): @@ -511,11 +514,6 @@ def make_homeserver(self, reactor, clock): config = self.default_config() # Email config. - self.email_attempts = [] - - async def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): - self.email_attempts.append(msg) - config["email"] = { "enable_notifs": False, "template_dir": os.path.abspath( @@ -530,7 +528,16 @@ async def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs): } config["public_baseurl"] = "https://example.com" - self.hs = self.setup_test_homeserver(config=config, sendmail=sendmail) + self.hs = self.setup_test_homeserver(config=config) + + async def sendmail( + reactor, smtphost, smtpport, from_addr, to_addrs, msg, **kwargs + ): + self.email_attempts.append(msg) + + self.email_attempts = [] + self.hs.get_send_email_handler()._sendmail = sendmail + return self.hs def prepare(self, reactor, clock, hs): diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index 1cad5f00eb..a52e5e608a 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -509,10 +509,6 @@ def make_homeserver(self, reactor, clock): } # Email config. - self.email_attempts = [] - - async def sendmail(*args, **kwargs): - self.email_attempts.append((args, kwargs)) config["email"] = { "enable_notifs": True, @@ -532,7 +528,13 @@ async def sendmail(*args, **kwargs): } config["public_baseurl"] = "aaa" - self.hs = self.setup_test_homeserver(config=config, sendmail=sendmail) + self.hs = self.setup_test_homeserver(config=config) + + async def sendmail(*args, **kwargs): + self.email_attempts.append((args, kwargs)) + + self.email_attempts = [] + self.hs.get_send_email_handler()._sendmail = sendmail self.store = self.hs.get_datastore() From f4ade972ada6d61ca9370d26784ac9f3ed8e5282 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 6 Aug 2021 07:40:29 -0400 Subject: [PATCH 525/619] Update the API response for spaces summary over federation. (#10530) This adds 'allowed_room_ids' (in addition to 'allowed_spaces', for backwards compatibility) to the federation response of the spaces summary. A future PR will remove the 'allowed_spaces' flag. --- changelog.d/10530.misc | 1 + synapse/handlers/space_summary.py | 57 ++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 changelog.d/10530.misc diff --git a/changelog.d/10530.misc b/changelog.d/10530.misc new file mode 100644 index 0000000000..3cf22f9daf --- /dev/null +++ b/changelog.d/10530.misc @@ -0,0 +1 @@ +Prepare for the new spaces summary endpoint (updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946)). diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 3eb232c83e..2517f278b6 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -179,7 +179,9 @@ async def get_space_summary( # Check if the user is a member of any of the allowed spaces # from the response. - allowed_rooms = room.get("allowed_spaces") + allowed_rooms = room.get("allowed_room_ids") or room.get( + "allowed_spaces" + ) if ( not include_room and allowed_rooms @@ -198,6 +200,11 @@ async def get_space_summary( # The user can see the room, include it! if include_room: + # Before returning to the client, remove the allowed_room_ids + # and allowed_spaces keys. + room.pop("allowed_room_ids", None) + room.pop("allowed_spaces", None) + rooms_result.append(room) events.extend(room_entry.children) @@ -236,11 +243,6 @@ async def get_space_summary( ) processed_events.add(ev_key) - # Before returning to the client, remove the allowed_spaces key for any - # rooms. - for room in rooms_result: - room.pop("allowed_spaces", None) - return {"rooms": rooms_result, "events": events_result} async def federation_space_summary( @@ -337,7 +339,7 @@ async def _summarize_local_room( if not await self._is_room_accessible(room_id, requester, origin): return None - room_entry = await self._build_room_entry(room_id) + room_entry = await self._build_room_entry(room_id, for_federation=bool(origin)) # If the room is not a space, return just the room information. if room_entry.get("room_type") != RoomTypes.SPACE: @@ -548,8 +550,18 @@ async def _is_room_accessible( ) return False - async def _build_room_entry(self, room_id: str) -> JsonDict: - """Generate en entry suitable for the 'rooms' list in the summary response""" + async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDict: + """ + Generate en entry suitable for the 'rooms' list in the summary response. + + Args: + room_id: The room ID to summarize. + for_federation: True if this is a summary requested over federation + (which includes additional fields). + + Returns: + The JSON dictionary for the room. + """ stats = await self._store.get_room_with_stats(room_id) # currently this should be impossible because we call @@ -562,15 +574,6 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: current_state_ids[(EventTypes.Create, "")] ) - room_version = await self._store.get_room_version(room_id) - allowed_rooms = None - if await self._event_auth_handler.has_restricted_join_rules( - current_state_ids, room_version - ): - allowed_rooms = await self._event_auth_handler.get_rooms_that_allow_join( - current_state_ids - ) - entry = { "room_id": stats["room_id"], "name": stats["name"], @@ -585,9 +588,25 @@ async def _build_room_entry(self, room_id: str) -> JsonDict: "guest_can_join": stats["guest_access"] == "can_join", "creation_ts": create_event.origin_server_ts, "room_type": create_event.content.get(EventContentFields.ROOM_TYPE), - "allowed_spaces": allowed_rooms, } + # Federation requests need to provide additional information so the + # requested server is able to filter the response appropriately. + if for_federation: + room_version = await self._store.get_room_version(room_id) + if await self._event_auth_handler.has_restricted_join_rules( + current_state_ids, room_version + ): + allowed_rooms = ( + await self._event_auth_handler.get_rooms_that_allow_join( + current_state_ids + ) + ) + if allowed_rooms: + entry["allowed_room_ids"] = allowed_rooms + # TODO Remove this key once the API is stable. + entry["allowed_spaces"] = allowed_rooms + # Filter out Nones – rather omit the field altogether room_entry = {k: v for k, v in entry.items() if v is not None} From 1bebc0b78cbedffb6b69fd76327f0eb7663c3c96 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 6 Aug 2021 13:54:23 +0100 Subject: [PATCH 526/619] Clean up federation event auth code (#10539) * drop old-room hack pretty sure we don't need this any more. * Remove incorrect comment about modifying `context` It doesn't look like the supplied context is ever modified. * Stop `_auth_and_persist_event` modifying its parameters This is only called in three places. Two of them don't pass `auth_events`, and the third doesn't use the dict after passing it in, so this should be non-functional. * Stop `_check_event_auth` modifying its parameters `_check_event_auth` is only called in three places. `on_send_membership_event` doesn't pass an `auth_events`, and `prep` and `_auth_and_persist_event` do not use the map after passing it in. * Stop `_update_auth_events_and_context_for_auth` modifying its parameters Return the updated auth event dict, rather than modifying the parameter. This is only called from `_check_event_auth`. * Improve documentation on `_auth_and_persist_event` Rename `auth_events` parameter to better reflect what it contains. * Improve documentation on `_NewEventInfo` * Improve documentation on `_check_event_auth` rename `auth_events` parameter to better describe what it contains * changelog --- changelog.d/10539.misc | 1 + synapse/handlers/federation.py | 118 ++++++++++++++++++--------------- tests/test_federation.py | 6 +- 3 files changed, 69 insertions(+), 56 deletions(-) create mode 100644 changelog.d/10539.misc diff --git a/changelog.d/10539.misc b/changelog.d/10539.misc new file mode 100644 index 0000000000..9a765435db --- /dev/null +++ b/changelog.d/10539.misc @@ -0,0 +1 @@ +Clean up some of the federation event authentication code for clarity. diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 8b602e3813..9a5e726533 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -109,21 +109,33 @@ ) -@attr.s(slots=True) +@attr.s(slots=True, frozen=True, auto_attribs=True) class _NewEventInfo: """Holds information about a received event, ready for passing to _auth_and_persist_events Attributes: event: the received event - state: the state at that event + state: the state at that event, according to /state_ids from a remote + homeserver. Only populated for backfilled events which are going to be a + new backwards extremity. + + claimed_auth_event_map: a map of (type, state_key) => event for the event's + claimed auth_events. + + This can include events which have not yet been persisted, in the case that + we are backfilling a batch of events. + + Note: May be incomplete: if we were unable to find all of the claimed auth + events. Also, treat the contents with caution: the events might also have + been rejected, might not yet have been authorized themselves, or they might + be in the wrong room. - auth_events: the auth_event map for that event """ - event = attr.ib(type=EventBase) - state = attr.ib(type=Optional[Sequence[EventBase]], default=None) - auth_events = attr.ib(type=Optional[MutableStateMap[EventBase]], default=None) + event: EventBase + state: Optional[Sequence[EventBase]] + claimed_auth_event_map: StateMap[EventBase] class FederationHandler(BaseHandler): @@ -1086,7 +1098,7 @@ async def backfill( _NewEventInfo( event=ev, state=events_to_state[e_id], - auth_events={ + claimed_auth_event_map={ ( auth_events[a_id].type, auth_events[a_id].state_key, @@ -2315,7 +2327,7 @@ async def _auth_and_persist_event( event: EventBase, context: EventContext, state: Optional[Iterable[EventBase]] = None, - auth_events: Optional[MutableStateMap[EventBase]] = None, + claimed_auth_event_map: Optional[StateMap[EventBase]] = None, backfilled: bool = False, ) -> None: """ @@ -2327,17 +2339,18 @@ async def _auth_and_persist_event( context: The event context. - NB that this function potentially modifies it. state: The state events used to check the event for soft-fail. If this is not provided the current state events will be used. - auth_events: - Map from (event_type, state_key) to event - Normally, our calculated auth_events based on the state of the room - at the event's position in the DAG, though occasionally (eg if the - event is an outlier), may be the auth events claimed by the remote - server. + claimed_auth_event_map: + A map of (type, state_key) => event for the event's claimed auth_events. + Possibly incomplete, and possibly including events that are not yet + persisted, or authed, or in the right room. + + Only populated where we may not already have persisted these events - + for example, when populating outliers. + backfilled: True if the event was backfilled. """ context = await self._check_event_auth( @@ -2345,7 +2358,7 @@ async def _auth_and_persist_event( event, context, state=state, - auth_events=auth_events, + claimed_auth_event_map=claimed_auth_event_map, backfilled=backfilled, ) @@ -2409,7 +2422,7 @@ async def prep(ev_info: _NewEventInfo): event, res, state=ev_info.state, - auth_events=ev_info.auth_events, + claimed_auth_event_map=ev_info.claimed_auth_event_map, backfilled=backfilled, ) return res @@ -2675,7 +2688,7 @@ async def _check_event_auth( event: EventBase, context: EventContext, state: Optional[Iterable[EventBase]] = None, - auth_events: Optional[MutableStateMap[EventBase]] = None, + claimed_auth_event_map: Optional[StateMap[EventBase]] = None, backfilled: bool = False, ) -> EventContext: """ @@ -2687,21 +2700,19 @@ async def _check_event_auth( context: The event context. - NB that this function potentially modifies it. state: The state events used to check the event for soft-fail. If this is not provided the current state events will be used. - auth_events: - Map from (event_type, state_key) to event - Normally, our calculated auth_events based on the state of the room - at the event's position in the DAG, though occasionally (eg if the - event is an outlier), may be the auth events claimed by the remote - server. + claimed_auth_event_map: + A map of (type, state_key) => event for the event's claimed auth_events. + Possibly incomplete, and possibly including events that are not yet + persisted, or authed, or in the right room. - Also NB that this function adds entries to it. + Only populated where we may not already have persisted these events - + for example, when populating outliers, or the state for a backwards + extremity. - If this is not provided, it is calculated from the previous state IDs. backfilled: True if the event was backfilled. Returns: @@ -2710,7 +2721,12 @@ async def _check_event_auth( room_version = await self.store.get_room_version_id(event.room_id) room_version_obj = KNOWN_ROOM_VERSIONS[room_version] - if not auth_events: + if claimed_auth_event_map: + # if we have a copy of the auth events from the event, use that as the + # basis for auth. + auth_events = claimed_auth_event_map + else: + # otherwise, we calculate what the auth events *should* be, and use that prev_state_ids = await context.get_prev_state_ids() auth_events_ids = self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=True @@ -2718,18 +2734,11 @@ async def _check_event_auth( auth_events_x = await self.store.get_events(auth_events_ids) auth_events = {(e.type, e.state_key): e for e in auth_events_x.values()} - # This is a hack to fix some old rooms where the initial join event - # didn't reference the create event in its auth events. - if event.type == EventTypes.Member and not event.auth_event_ids(): - if len(event.prev_event_ids()) == 1 and event.depth < 5: - c = await self.store.get_event( - event.prev_event_ids()[0], allow_none=True - ) - if c and c.type == EventTypes.Create: - auth_events[(c.type, c.state_key)] = c - try: - context = await self._update_auth_events_and_context_for_auth( + ( + context, + auth_events_for_auth, + ) = await self._update_auth_events_and_context_for_auth( origin, event, context, auth_events ) except Exception: @@ -2742,9 +2751,10 @@ async def _check_event_auth( "Ignoring failure and continuing processing of event.", event.event_id, ) + auth_events_for_auth = auth_events try: - event_auth.check(room_version_obj, event, auth_events=auth_events) + event_auth.check(room_version_obj, event, auth_events=auth_events_for_auth) except AuthError as e: logger.warning("Failed auth resolution for %r because %s", event, e) context.rejected = RejectedReason.AUTH_ERROR @@ -2769,8 +2779,8 @@ async def _update_auth_events_and_context_for_auth( origin: str, event: EventBase, context: EventContext, - auth_events: MutableStateMap[EventBase], - ) -> EventContext: + input_auth_events: StateMap[EventBase], + ) -> Tuple[EventContext, StateMap[EventBase]]: """Helper for _check_event_auth. See there for docs. Checks whether a given event has the expected auth events. If it @@ -2787,7 +2797,7 @@ async def _update_auth_events_and_context_for_auth( event: context: - auth_events: + input_auth_events: Map from (event_type, state_key) to event Normally, our calculated auth_events based on the state of the room @@ -2795,11 +2805,12 @@ async def _update_auth_events_and_context_for_auth( event is an outlier), may be the auth events claimed by the remote server. - Also NB that this function adds entries to it. - Returns: - updated context + updated context, updated auth event map """ + # take a copy of input_auth_events before we modify it. + auth_events: MutableStateMap[EventBase] = dict(input_auth_events) + event_auth_events = set(event.auth_event_ids()) # missing_auth is the set of the event's auth_events which we don't yet have @@ -2828,7 +2839,7 @@ async def _update_auth_events_and_context_for_auth( # The other side isn't around or doesn't implement the # endpoint, so lets just bail out. logger.info("Failed to get event auth from remote: %s", e1) - return context + return context, auth_events seen_remotes = await self.store.have_seen_events( event.room_id, [e.event_id for e in remote_auth_chain] @@ -2859,7 +2870,10 @@ async def _update_auth_events_and_context_for_auth( await self.state_handler.compute_event_context(e) ) await self._auth_and_persist_event( - origin, e, missing_auth_event_context, auth_events=auth + origin, + e, + missing_auth_event_context, + claimed_auth_event_map=auth, ) if e.event_id in event_auth_events: @@ -2877,14 +2891,14 @@ async def _update_auth_events_and_context_for_auth( # obviously be empty # (b) alternatively, why don't we do it earlier? logger.info("Skipping auth_event fetch for outlier") - return context + return context, auth_events different_auth = event_auth_events.difference( e.event_id for e in auth_events.values() ) if not different_auth: - return context + return context, auth_events logger.info( "auth_events refers to events which are not in our calculated auth " @@ -2910,7 +2924,7 @@ async def _update_auth_events_and_context_for_auth( # XXX: should we reject the event in this case? It feels like we should, # but then shouldn't we also do so if we've failed to fetch any of the # auth events? - return context + return context, auth_events # now we state-resolve between our own idea of the auth events, and the remote's # idea of them. @@ -2940,7 +2954,7 @@ async def _update_auth_events_and_context_for_auth( event, context, auth_events ) - return context + return context, auth_events async def _update_context_for_auth_events( self, event: EventBase, context: EventContext, auth_events: StateMap[EventBase] diff --git a/tests/test_federation.py b/tests/test_federation.py index 0ed8326f55..3785799f46 100644 --- a/tests/test_federation.py +++ b/tests/test_federation.py @@ -75,10 +75,8 @@ def setUp(self): ) self.handler = self.homeserver.get_federation_handler() - self.handler._check_event_auth = ( - lambda origin, event, context, state, auth_events, backfilled: succeed( - context - ) + self.handler._check_event_auth = lambda origin, event, context, state, claimed_auth_event_map, backfilled: succeed( + context ) self.client = self.homeserver.get_federation_client() self.client._check_sigs_and_hash_and_fetch = lambda dest, pdus, **k: succeed( From 60f0534b6e910a497800da2454638bcf4aae006e Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 6 Aug 2021 14:05:41 +0100 Subject: [PATCH 527/619] Fix exceptions in logs when failing to get remote room list (#10541) --- changelog.d/10541.bugfix | 1 + synapse/federation/federation_client.py | 3 +- synapse/handlers/room_list.py | 46 ++++++++----- synapse/rest/client/v1/room.py | 30 ++++---- tests/rest/client/v1/test_rooms.py | 92 ++++++++++++++++++++++++- 5 files changed, 134 insertions(+), 38 deletions(-) create mode 100644 changelog.d/10541.bugfix diff --git a/changelog.d/10541.bugfix b/changelog.d/10541.bugfix new file mode 100644 index 0000000000..bb946e0920 --- /dev/null +++ b/changelog.d/10541.bugfix @@ -0,0 +1 @@ +Fix exceptions in logs when failing to get remote room list. diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 007d1a27dc..2eefac04fd 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1108,7 +1108,8 @@ async def get_public_rooms( The response from the remote server. Raises: - HttpResponseException: There was an exception returned from the remote server + HttpResponseException / RequestSendFailed: There was an exception + returned from the remote server SynapseException: M_FORBIDDEN when the remote server has disallowed publicRoom requests over federation diff --git a/synapse/handlers/room_list.py b/synapse/handlers/room_list.py index fae2c098e3..6d433fad41 100644 --- a/synapse/handlers/room_list.py +++ b/synapse/handlers/room_list.py @@ -356,6 +356,12 @@ async def get_remote_public_room_list( include_all_networks: bool = False, third_party_instance_id: Optional[str] = None, ) -> JsonDict: + """Get the public room list from remote server + + Raises: + SynapseError + """ + if not self.enable_room_list_search: return {"chunk": [], "total_room_count_estimate": 0} @@ -395,13 +401,16 @@ async def get_remote_public_room_list( limit = None since_token = None - res = await self._get_remote_list_cached( - server_name, - limit=limit, - since_token=since_token, - include_all_networks=include_all_networks, - third_party_instance_id=third_party_instance_id, - ) + try: + res = await self._get_remote_list_cached( + server_name, + limit=limit, + since_token=since_token, + include_all_networks=include_all_networks, + third_party_instance_id=third_party_instance_id, + ) + except (RequestSendFailed, HttpResponseException): + raise SynapseError(502, "Failed to fetch room list") if search_filter: res = { @@ -423,20 +432,21 @@ async def _get_remote_list_cached( include_all_networks: bool = False, third_party_instance_id: Optional[str] = None, ) -> JsonDict: + """Wrapper around FederationClient.get_public_rooms that caches the + result. + """ + repl_layer = self.hs.get_federation_client() if search_filter: # We can't cache when asking for search - try: - return await repl_layer.get_public_rooms( - server_name, - limit=limit, - since_token=since_token, - search_filter=search_filter, - include_all_networks=include_all_networks, - third_party_instance_id=third_party_instance_id, - ) - except (RequestSendFailed, HttpResponseException): - raise SynapseError(502, "Failed to fetch room list") + return await repl_layer.get_public_rooms( + server_name, + limit=limit, + since_token=since_token, + search_filter=search_filter, + include_all_networks=include_all_networks, + third_party_instance_id=third_party_instance_id, + ) key = ( server_name, diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 982f134148..f887970b76 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -23,7 +23,6 @@ from synapse.api.errors import ( AuthError, Codes, - HttpResponseException, InvalidClientCredentialsError, ShadowBanError, SynapseError, @@ -783,12 +782,9 @@ async def on_GET(self, request): Codes.INVALID_PARAM, ) - try: - data = await handler.get_remote_public_room_list( - server, limit=limit, since_token=since_token - ) - except HttpResponseException as e: - raise e.to_synapse_error() + data = await handler.get_remote_public_room_list( + server, limit=limit, since_token=since_token + ) else: data = await handler.get_local_public_room_list( limit=limit, since_token=since_token @@ -836,17 +832,15 @@ async def on_POST(self, request): Codes.INVALID_PARAM, ) - try: - data = await handler.get_remote_public_room_list( - server, - limit=limit, - since_token=since_token, - search_filter=search_filter, - include_all_networks=include_all_networks, - third_party_instance_id=third_party_instance_id, - ) - except HttpResponseException as e: - raise e.to_synapse_error() + data = await handler.get_remote_public_room_list( + server, + limit=limit, + since_token=since_token, + search_filter=search_filter, + include_all_networks=include_all_networks, + third_party_instance_id=third_party_instance_id, + ) + else: data = await handler.get_local_public_room_list( limit=limit, diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 3df070c936..1a9528ec20 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -19,11 +19,14 @@ import json from typing import Iterable -from unittest.mock import Mock +from unittest.mock import Mock, call from urllib import parse as urlparse +from twisted.internet import defer + import synapse.rest.admin from synapse.api.constants import EventContentFields, EventTypes, Membership +from synapse.api.errors import HttpResponseException from synapse.handlers.pagination import PurgeStatus from synapse.rest import admin from synapse.rest.client.v1 import directory, login, profile, room @@ -1124,6 +1127,93 @@ def test_restricted_auth(self): self.assertEqual(channel.code, 200, channel.result) +class PublicRoomsTestRemoteSearchFallbackTestCase(unittest.HomeserverTestCase): + """Test that we correctly fallback to local filtering if a remote server + doesn't support search. + """ + + servlets = [ + synapse.rest.admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + ] + + def make_homeserver(self, reactor, clock): + return self.setup_test_homeserver(federation_client=Mock()) + + def prepare(self, reactor, clock, hs): + self.register_user("user", "pass") + self.token = self.login("user", "pass") + + self.federation_client = hs.get_federation_client() + + def test_simple(self): + "Simple test for searching rooms over federation" + self.federation_client.get_public_rooms.side_effect = ( + lambda *a, **k: defer.succeed({}) + ) + + search_filter = {"generic_search_term": "foobar"} + + channel = self.make_request( + "POST", + b"/_matrix/client/r0/publicRooms?server=testserv", + content={"filter": search_filter}, + access_token=self.token, + ) + self.assertEqual(channel.code, 200, channel.result) + + self.federation_client.get_public_rooms.assert_called_once_with( + "testserv", + limit=100, + since_token=None, + search_filter=search_filter, + include_all_networks=False, + third_party_instance_id=None, + ) + + def test_fallback(self): + "Test that searching public rooms over federation falls back if it gets a 404" + + # The `get_public_rooms` should be called again if the first call fails + # with a 404, when using search filters. + self.federation_client.get_public_rooms.side_effect = ( + HttpResponseException(404, "Not Found", b""), + defer.succeed({}), + ) + + search_filter = {"generic_search_term": "foobar"} + + channel = self.make_request( + "POST", + b"/_matrix/client/r0/publicRooms?server=testserv", + content={"filter": search_filter}, + access_token=self.token, + ) + self.assertEqual(channel.code, 200, channel.result) + + self.federation_client.get_public_rooms.assert_has_calls( + [ + call( + "testserv", + limit=100, + since_token=None, + search_filter=search_filter, + include_all_networks=False, + third_party_instance_id=None, + ), + call( + "testserv", + limit=None, + since_token=None, + search_filter=None, + include_all_networks=False, + third_party_instance_id=None, + ), + ] + ) + + class PerRoomProfilesForbiddenTestCase(unittest.HomeserverTestCase): servlets = [ From 1de26b346796ec8d6b51b4395017f8107f640c47 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 6 Aug 2021 09:39:59 -0400 Subject: [PATCH 528/619] Convert Transaction and Edu object to attrs (#10542) Instead of wrapping the JSON into an object, this creates concrete instances for Transaction and Edu. This allows for improved type hints and simplified code. --- changelog.d/10542.misc | 1 + synapse/federation/federation_server.py | 50 +++++---- synapse/federation/persistence.py | 4 +- .../federation/sender/transaction_manager.py | 9 +- synapse/federation/transport/client.py | 2 +- synapse/federation/transport/server.py | 11 +- synapse/federation/units.py | 90 ++++++---------- synapse/util/jsonobject.py | 102 ------------------ 8 files changed, 75 insertions(+), 194 deletions(-) create mode 100644 changelog.d/10542.misc delete mode 100644 synapse/util/jsonobject.py diff --git a/changelog.d/10542.misc b/changelog.d/10542.misc new file mode 100644 index 0000000000..44b70b4730 --- /dev/null +++ b/changelog.d/10542.misc @@ -0,0 +1 @@ +Convert `Transaction` and `Edu` objects to attrs. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 145b9161d9..0385aadefa 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -195,13 +195,17 @@ async def on_backfill_request( origin, room_id, versions, limit ) - res = self._transaction_from_pdus(pdus).get_dict() + res = self._transaction_dict_from_pdus(pdus) return 200, res async def on_incoming_transaction( - self, origin: str, transaction_data: JsonDict - ) -> Tuple[int, Dict[str, Any]]: + self, + origin: str, + transaction_id: str, + destination: str, + transaction_data: JsonDict, + ) -> Tuple[int, JsonDict]: # If we receive a transaction we should make sure that kick off handling # any old events in the staging area. if not self._started_handling_of_staged_events: @@ -212,8 +216,14 @@ async def on_incoming_transaction( # accurate as possible. request_time = self._clock.time_msec() - transaction = Transaction(**transaction_data) - transaction_id = transaction.transaction_id # type: ignore + transaction = Transaction( + transaction_id=transaction_id, + destination=destination, + origin=origin, + origin_server_ts=transaction_data.get("origin_server_ts"), # type: ignore + pdus=transaction_data.get("pdus"), # type: ignore + edus=transaction_data.get("edus"), + ) if not transaction_id: raise Exception("Transaction missing transaction_id") @@ -221,9 +231,7 @@ async def on_incoming_transaction( logger.debug("[%s] Got transaction", transaction_id) # Reject malformed transactions early: reject if too many PDUs/EDUs - if len(transaction.pdus) > 50 or ( # type: ignore - hasattr(transaction, "edus") and len(transaction.edus) > 100 # type: ignore - ): + if len(transaction.pdus) > 50 or len(transaction.edus) > 100: logger.info("Transaction PDU or EDU count too large. Returning 400") return 400, {} @@ -263,7 +271,7 @@ async def _on_incoming_transaction_inner( # CRITICAL SECTION: the first thing we must do (before awaiting) is # add an entry to _active_transactions. assert origin not in self._active_transactions - self._active_transactions[origin] = transaction.transaction_id # type: ignore + self._active_transactions[origin] = transaction.transaction_id try: result = await self._handle_incoming_transaction( @@ -291,11 +299,11 @@ async def _handle_incoming_transaction( if response: logger.debug( "[%s] We've already responded to this request", - transaction.transaction_id, # type: ignore + transaction.transaction_id, ) return response - logger.debug("[%s] Transaction is new", transaction.transaction_id) # type: ignore + logger.debug("[%s] Transaction is new", transaction.transaction_id) # We process PDUs and EDUs in parallel. This is important as we don't # want to block things like to device messages from reaching clients @@ -334,7 +342,7 @@ async def _handle_pdus_in_txn( report back to the sending server. """ - received_pdus_counter.inc(len(transaction.pdus)) # type: ignore + received_pdus_counter.inc(len(transaction.pdus)) origin_host, _ = parse_server_name(origin) @@ -342,7 +350,7 @@ async def _handle_pdus_in_txn( newest_pdu_ts = 0 - for p in transaction.pdus: # type: ignore + for p in transaction.pdus: # FIXME (richardv): I don't think this works: # https://github.com/matrix-org/synapse/issues/8429 if "unsigned" in p: @@ -436,10 +444,10 @@ async def process_pdu(pdu: EventBase) -> JsonDict: return pdu_results - async def _handle_edus_in_txn(self, origin: str, transaction: Transaction): + async def _handle_edus_in_txn(self, origin: str, transaction: Transaction) -> None: """Process the EDUs in a received transaction.""" - async def _process_edu(edu_dict): + async def _process_edu(edu_dict: JsonDict) -> None: received_edus_counter.inc() edu = Edu( @@ -452,7 +460,7 @@ async def _process_edu(edu_dict): await concurrently_execute( _process_edu, - getattr(transaction, "edus", []), + transaction.edus, TRANSACTION_CONCURRENCY_LIMIT, ) @@ -538,7 +546,7 @@ async def on_pdu_request( pdu = await self.handler.get_persisted_pdu(origin, event_id) if pdu: - return 200, self._transaction_from_pdus([pdu]).get_dict() + return 200, self._transaction_dict_from_pdus([pdu]) else: return 404, "" @@ -879,18 +887,20 @@ async def on_openid_userinfo(self, token: str) -> Optional[str]: ts_now_ms = self._clock.time_msec() return await self.store.get_user_id_for_open_id_token(token, ts_now_ms) - def _transaction_from_pdus(self, pdu_list: List[EventBase]) -> Transaction: + def _transaction_dict_from_pdus(self, pdu_list: List[EventBase]) -> JsonDict: """Returns a new Transaction containing the given PDUs suitable for transmission. """ time_now = self._clock.time_msec() pdus = [p.get_pdu_json(time_now) for p in pdu_list] return Transaction( + # Just need a dummy transaction ID and destination since it won't be used. + transaction_id="", origin=self.server_name, pdus=pdus, origin_server_ts=int(time_now), - destination=None, - ) + destination="", + ).get_dict() async def _handle_received_pdu(self, origin: str, pdu: EventBase) -> None: """Process a PDU received in a federation /send/ transaction. diff --git a/synapse/federation/persistence.py b/synapse/federation/persistence.py index 2f9c9bc2cd..4fead6ca29 100644 --- a/synapse/federation/persistence.py +++ b/synapse/federation/persistence.py @@ -45,7 +45,7 @@ async def have_responded( `None` if we have not previously responded to this transaction or a 2-tuple of `(int, dict)` representing the response code and response body. """ - transaction_id = transaction.transaction_id # type: ignore + transaction_id = transaction.transaction_id if not transaction_id: raise RuntimeError("Cannot persist a transaction with no transaction_id") @@ -56,7 +56,7 @@ async def set_response( self, origin: str, transaction: Transaction, code: int, response: JsonDict ) -> None: """Persist how we responded to a transaction.""" - transaction_id = transaction.transaction_id # type: ignore + transaction_id = transaction.transaction_id if not transaction_id: raise RuntimeError("Cannot persist a transaction with no transaction_id") diff --git a/synapse/federation/sender/transaction_manager.py b/synapse/federation/sender/transaction_manager.py index 72a635830b..dc555cca0b 100644 --- a/synapse/federation/sender/transaction_manager.py +++ b/synapse/federation/sender/transaction_manager.py @@ -27,6 +27,7 @@ tags, whitelisted_homeserver, ) +from synapse.types import JsonDict from synapse.util import json_decoder from synapse.util.metrics import measure_func @@ -104,13 +105,13 @@ async def send_new_transaction( len(edus), ) - transaction = Transaction.create_new( + transaction = Transaction( origin_server_ts=int(self.clock.time_msec()), transaction_id=txn_id, origin=self._server_name, destination=destination, - pdus=pdus, - edus=edus, + pdus=[p.get_pdu_json() for p in pdus], + edus=[edu.get_dict() for edu in edus], ) self._next_txn_id += 1 @@ -131,7 +132,7 @@ async def send_new_transaction( # FIXME (richardv): I also believe it no longer works. We (now?) store # "age_ts" in "unsigned" rather than at the top level. See # https://github.com/matrix-org/synapse/issues/8429. - def json_data_cb(): + def json_data_cb() -> JsonDict: data = transaction.get_dict() now = int(self.clock.time_msec()) if "pdus" in data: diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 6a8d3ad4fe..90a7c16b62 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -143,7 +143,7 @@ async def send_transaction( """Sends the given Transaction to its destination Args: - transaction (Transaction) + transaction Returns: Succeeds when we get a 2xx HTTP response. The result diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 5e059d6e09..640f46fff6 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -450,21 +450,12 @@ async def on_PUT( len(transaction_data.get("edus", [])), ) - # We should ideally be getting this from the security layer. - # origin = body["origin"] - - # Add some extra data to the transaction dict that isn't included - # in the request body. - transaction_data.update( - transaction_id=transaction_id, destination=self.server_name - ) - except Exception as e: logger.exception(e) return 400, {"error": "Invalid transaction"} code, response = await self.handler.on_incoming_transaction( - origin, transaction_data + origin, transaction_id, self.server_name, transaction_data ) return code, response diff --git a/synapse/federation/units.py b/synapse/federation/units.py index c83a261918..b9b12fbea5 100644 --- a/synapse/federation/units.py +++ b/synapse/federation/units.py @@ -17,18 +17,17 @@ """ import logging -from typing import Optional +from typing import List, Optional import attr from synapse.types import JsonDict -from synapse.util.jsonobject import JsonEncodedObject logger = logging.getLogger(__name__) -@attr.s(slots=True) -class Edu(JsonEncodedObject): +@attr.s(slots=True, frozen=True, auto_attribs=True) +class Edu: """An Edu represents a piece of data sent from one homeserver to another. In comparison to Pdus, Edus are not persisted for a long time on disk, are @@ -36,10 +35,10 @@ class Edu(JsonEncodedObject): internal ID or previous references graph. """ - edu_type = attr.ib(type=str) - content = attr.ib(type=dict) - origin = attr.ib(type=str) - destination = attr.ib(type=str) + edu_type: str + content: dict + origin: str + destination: str def get_dict(self) -> JsonDict: return { @@ -55,14 +54,21 @@ def get_internal_dict(self) -> JsonDict: "destination": self.destination, } - def get_context(self): + def get_context(self) -> str: return getattr(self, "content", {}).get("org.matrix.opentracing_context", "{}") - def strip_context(self): + def strip_context(self) -> None: getattr(self, "content", {})["org.matrix.opentracing_context"] = "{}" -class Transaction(JsonEncodedObject): +def _none_to_list(edus: Optional[List[JsonDict]]) -> List[JsonDict]: + if edus is None: + return [] + return edus + + +@attr.s(slots=True, frozen=True, auto_attribs=True) +class Transaction: """A transaction is a list of Pdus and Edus to be sent to a remote home server with some extra metadata. @@ -78,47 +84,21 @@ class Transaction(JsonEncodedObject): """ - valid_keys = [ - "transaction_id", - "origin", - "destination", - "origin_server_ts", - "previous_ids", - "pdus", - "edus", - ] - - internal_keys = ["transaction_id", "destination"] - - required_keys = [ - "transaction_id", - "origin", - "destination", - "origin_server_ts", - "pdus", - ] - - def __init__(self, transaction_id=None, pdus: Optional[list] = None, **kwargs): - """If we include a list of pdus then we decode then as PDU's - automatically. - """ - - # If there's no EDUs then remove the arg - if "edus" in kwargs and not kwargs["edus"]: - del kwargs["edus"] - - super().__init__(transaction_id=transaction_id, pdus=pdus or [], **kwargs) - - @staticmethod - def create_new(pdus, **kwargs): - """Used to create a new transaction. Will auto fill out - transaction_id and origin_server_ts keys. - """ - if "origin_server_ts" not in kwargs: - raise KeyError("Require 'origin_server_ts' to construct a Transaction") - if "transaction_id" not in kwargs: - raise KeyError("Require 'transaction_id' to construct a Transaction") - - kwargs["pdus"] = [p.get_pdu_json() for p in pdus] - - return Transaction(**kwargs) + # Required keys. + transaction_id: str + origin: str + destination: str + origin_server_ts: int + pdus: List[JsonDict] = attr.ib(factory=list, converter=_none_to_list) + edus: List[JsonDict] = attr.ib(factory=list, converter=_none_to_list) + + def get_dict(self) -> JsonDict: + """A JSON-ready dictionary of valid keys which aren't internal.""" + result = { + "origin": self.origin, + "origin_server_ts": self.origin_server_ts, + "pdus": self.pdus, + } + if self.edus: + result["edus"] = self.edus + return result diff --git a/synapse/util/jsonobject.py b/synapse/util/jsonobject.py deleted file mode 100644 index abc12f0837..0000000000 --- a/synapse/util/jsonobject.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright 2014-2016 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. - - -class JsonEncodedObject: - """A common base class for defining protocol units that are represented - as JSON. - - Attributes: - unrecognized_keys (dict): A dict containing all the key/value pairs we - don't recognize. - """ - - valid_keys = [] # keys we will store - """A list of strings that represent keys we know about - and can handle. If we have values for these keys they will be - included in the `dictionary` instance variable. - """ - - internal_keys = [] # keys to ignore while building dict - """A list of strings that should *not* be encoded into JSON. - """ - - required_keys = [] - """A list of strings that we require to exist. If they are not given upon - construction it raises an exception. - """ - - def __init__(self, **kwargs): - """Takes the dict of `kwargs` and loads all keys that are *valid* - (i.e., are included in the `valid_keys` list) into the dictionary` - instance variable. - - Any keys that aren't recognized are added to the `unrecognized_keys` - attribute. - - Args: - **kwargs: Attributes associated with this protocol unit. - """ - for required_key in self.required_keys: - if required_key not in kwargs: - raise RuntimeError("Key %s is required" % required_key) - - self.unrecognized_keys = {} # Keys we were given not listed as valid - for k, v in kwargs.items(): - if k in self.valid_keys or k in self.internal_keys: - self.__dict__[k] = v - else: - self.unrecognized_keys[k] = v - - def get_dict(self): - """Converts this protocol unit into a :py:class:`dict`, ready to be - encoded as JSON. - - The keys it encodes are: `valid_keys` - `internal_keys` - - Returns - dict - """ - d = { - k: _encode(v) - for (k, v) in self.__dict__.items() - if k in self.valid_keys and k not in self.internal_keys - } - d.update(self.unrecognized_keys) - return d - - def get_internal_dict(self): - d = { - k: _encode(v, internal=True) - for (k, v) in self.__dict__.items() - if k in self.valid_keys - } - d.update(self.unrecognized_keys) - return d - - def __str__(self): - return "(%s, %s)" % (self.__class__.__name__, repr(self.__dict__)) - - -def _encode(obj, internal=False): - if type(obj) is list: - return [_encode(o, internal=internal) for o in obj] - - if isinstance(obj, JsonEncodedObject): - if internal: - return obj.get_internal_dict() - else: - return obj.get_dict() - - return obj From 0c246dd4a09e21e677934d0d83efa573c9127a6f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 9 Aug 2021 04:46:39 -0400 Subject: [PATCH 529/619] Support MSC3289: Room version 8 (#10449) This adds support for MSC3289: room version 8. This is room version 7 + MSC3083. --- changelog.d/10449.bugfix | 1 + changelog.d/10449.feature | 1 + synapse/api/constants.py | 2 +- synapse/api/room_versions.py | 28 ++++++++++++++-------------- synapse/event_auth.py | 5 +---- synapse/handlers/event_auth.py | 2 +- tests/events/test_utils.py | 2 +- tests/handlers/test_space_summary.py | 12 ++++++------ tests/test_event_auth.py | 18 +++++++++--------- 9 files changed, 35 insertions(+), 36 deletions(-) create mode 100644 changelog.d/10449.bugfix create mode 100644 changelog.d/10449.feature diff --git a/changelog.d/10449.bugfix b/changelog.d/10449.bugfix new file mode 100644 index 0000000000..c5e23ba019 --- /dev/null +++ b/changelog.d/10449.bugfix @@ -0,0 +1 @@ +Mark the experimental room version from [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716) as unstable. diff --git a/changelog.d/10449.feature b/changelog.d/10449.feature new file mode 100644 index 0000000000..a45a17cb28 --- /dev/null +++ b/changelog.d/10449.feature @@ -0,0 +1 @@ +Support [MSC3289: room version 8](https://github.com/matrix-org/matrix-doc/pull/3289). diff --git a/synapse/api/constants.py b/synapse/api/constants.py index a986fdb47a..e0e24fddac 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -62,7 +62,7 @@ class JoinRules: INVITE = "invite" PRIVATE = "private" # As defined for MSC3083. - MSC3083_RESTRICTED = "restricted" + RESTRICTED = "restricted" class RestrictedJoinRuleTypes: diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index bc678efe49..f32a40ba4a 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -177,9 +177,9 @@ class RoomVersions: msc2403_knocking=False, msc2716_historical=False, ) - MSC3083 = RoomVersion( - "org.matrix.msc3083.v2", - RoomDisposition.UNSTABLE, + V7 = RoomVersion( + "7", + RoomDisposition.STABLE, EventFormatVersions.V3, StateResolutionVersions.V2, enforce_key_validity=True, @@ -187,13 +187,13 @@ class RoomVersions: strict_canonicaljson=True, limit_notifications_power_levels=True, msc2176_redaction_rules=False, - msc3083_join_rules=True, - msc2403_knocking=False, + msc3083_join_rules=False, + msc2403_knocking=True, msc2716_historical=False, ) - V7 = RoomVersion( - "7", - RoomDisposition.STABLE, + MSC2716 = RoomVersion( + "org.matrix.msc2716", + RoomDisposition.UNSTABLE, EventFormatVersions.V3, StateResolutionVersions.V2, enforce_key_validity=True, @@ -203,10 +203,10 @@ class RoomVersions: msc2176_redaction_rules=False, msc3083_join_rules=False, msc2403_knocking=True, - msc2716_historical=False, + msc2716_historical=True, ) - MSC2716 = RoomVersion( - "org.matrix.msc2716", + V8 = RoomVersion( + "8", RoomDisposition.STABLE, EventFormatVersions.V3, StateResolutionVersions.V2, @@ -215,9 +215,9 @@ class RoomVersions: strict_canonicaljson=True, limit_notifications_power_levels=True, msc2176_redaction_rules=False, - msc3083_join_rules=False, + msc3083_join_rules=True, msc2403_knocking=True, - msc2716_historical=True, + msc2716_historical=False, ) @@ -231,9 +231,9 @@ class RoomVersions: RoomVersions.V5, RoomVersions.V6, RoomVersions.MSC2176, - RoomVersions.MSC3083, RoomVersions.V7, RoomVersions.MSC2716, + RoomVersions.V8, ) } diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 4c92e9a2d4..c3a0c10499 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -370,10 +370,7 @@ def _is_membership_change_allowed( raise AuthError(403, "You are banned from this room") elif join_rule == JoinRules.PUBLIC: pass - elif ( - room_version.msc3083_join_rules - and join_rule == JoinRules.MSC3083_RESTRICTED - ): + elif room_version.msc3083_join_rules and join_rule == JoinRules.RESTRICTED: # This is the same as public, but the event must contain a reference # to the server who authorised the join. If the event does not contain # the proper content it is rejected. diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 53fac1f8a3..e2410e482f 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -240,7 +240,7 @@ async def has_restricted_join_rules( # If the join rule is not restricted, this doesn't apply. join_rules_event = await self._store.get_event(join_rules_event_id) - return join_rules_event.content.get("join_rule") == JoinRules.MSC3083_RESTRICTED + return join_rules_event.content.get("join_rule") == JoinRules.RESTRICTED async def get_rooms_that_allow_join( self, state_ids: StateMap[str] diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index e2a5fc018c..7a826c086e 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -341,7 +341,7 @@ def test_join_rules(self): "signatures": {}, "unsigned": {}, }, - room_version=RoomVersions.MSC3083, + room_version=RoomVersions.V8, ) diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 3f73ad7f94..01975c13d4 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -231,13 +231,13 @@ def test_filtering(self): invited_room = self._create_room_with_join_rule(JoinRules.INVITE) self.helper.invite(invited_room, targ=user2, tok=self.token) restricted_room = self._create_room_with_join_rule( - JoinRules.MSC3083_RESTRICTED, - room_version=RoomVersions.MSC3083.identifier, + JoinRules.RESTRICTED, + room_version=RoomVersions.V8.identifier, allow=[], ) restricted_accessible_room = self._create_room_with_join_rule( - JoinRules.MSC3083_RESTRICTED, - room_version=RoomVersions.MSC3083.identifier, + JoinRules.RESTRICTED, + room_version=RoomVersions.V8.identifier, allow=[ { "type": RestrictedJoinRuleTypes.ROOM_MEMBERSHIP, @@ -459,13 +459,13 @@ async def summarize_remote_room( { "room_id": restricted_room, "world_readable": False, - "join_rules": JoinRules.MSC3083_RESTRICTED, + "join_rules": JoinRules.RESTRICTED, "allowed_spaces": [], }, { "room_id": restricted_accessible_room, "world_readable": False, - "join_rules": JoinRules.MSC3083_RESTRICTED, + "join_rules": JoinRules.RESTRICTED, "allowed_spaces": [self.room], }, { diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index e5550aec4d..6ebd01bcbe 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -384,7 +384,7 @@ def test_join_rules_msc3083_restricted(self): }, ) event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, authorised_join_event, auth_events, do_sig_check=False, @@ -400,7 +400,7 @@ def test_join_rules_msc3083_restricted(self): "@inviter:foo.test" ) event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, _join_event( pleb, additional_content={ @@ -414,7 +414,7 @@ def test_join_rules_msc3083_restricted(self): # A join which is missing an authorised server is rejected. with self.assertRaises(AuthError): event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, _join_event(pleb), auth_events, do_sig_check=False, @@ -427,7 +427,7 @@ def test_join_rules_msc3083_restricted(self): ) with self.assertRaises(AuthError): event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, _join_event( pleb, additional_content={ @@ -442,7 +442,7 @@ def test_join_rules_msc3083_restricted(self): # *would* be valid, but is sent be a different user.) with self.assertRaises(AuthError): event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, _member_event( pleb, "join", @@ -459,7 +459,7 @@ def test_join_rules_msc3083_restricted(self): auth_events[("m.room.member", pleb)] = _member_event(pleb, "ban") with self.assertRaises(AuthError): event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, authorised_join_event, auth_events, do_sig_check=False, @@ -468,7 +468,7 @@ def test_join_rules_msc3083_restricted(self): # A user who left can re-join. auth_events[("m.room.member", pleb)] = _member_event(pleb, "leave") event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, authorised_join_event, auth_events, do_sig_check=False, @@ -478,7 +478,7 @@ def test_join_rules_msc3083_restricted(self): # be authorised since the user is already joined.) auth_events[("m.room.member", pleb)] = _member_event(pleb, "join") event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, _join_event(pleb), auth_events, do_sig_check=False, @@ -490,7 +490,7 @@ def test_join_rules_msc3083_restricted(self): pleb, "invite", sender=creator ) event_auth.check( - RoomVersions.MSC3083, + RoomVersions.V8, _join_event(pleb), auth_events, do_sig_check=False, From ad35b7739e72fe198fa78fa4279f58cacfc9fa37 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 9 Aug 2021 13:41:29 +0100 Subject: [PATCH 530/619] 1.40.0rc3 --- CHANGES.md | 21 +++++++++++++++++++++ changelog.d/10449.bugfix | 1 - changelog.d/10449.feature | 1 - changelog.d/10543.doc | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 6 files changed, 28 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/10449.bugfix delete mode 100644 changelog.d/10449.feature delete mode 100644 changelog.d/10543.doc diff --git a/CHANGES.md b/CHANGES.md index 62ea684e58..b04abbeb4d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,24 @@ +Synapse 1.40.0rc3 (2021-08-09) +============================== + +Features +-------- + +- Support [MSC3289: room version 8](https://github.com/matrix-org/matrix-doc/pull/3289). ([\#10449](https://github.com/matrix-org/synapse/issues/10449)) + + +Bugfixes +-------- + +- Mark the experimental room version from [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716) as unstable. ([\#10449](https://github.com/matrix-org/synapse/issues/10449)) + + +Improved Documentation +---------------------- + +- Fix broken links in `upgrade.md`. Contributed by @dklimpel. ([\#10543](https://github.com/matrix-org/synapse/issues/10543)) + + Synapse 1.40.0rc2 (2021-08-04) ============================== diff --git a/changelog.d/10449.bugfix b/changelog.d/10449.bugfix deleted file mode 100644 index c5e23ba019..0000000000 --- a/changelog.d/10449.bugfix +++ /dev/null @@ -1 +0,0 @@ -Mark the experimental room version from [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716) as unstable. diff --git a/changelog.d/10449.feature b/changelog.d/10449.feature deleted file mode 100644 index a45a17cb28..0000000000 --- a/changelog.d/10449.feature +++ /dev/null @@ -1 +0,0 @@ -Support [MSC3289: room version 8](https://github.com/matrix-org/matrix-doc/pull/3289). diff --git a/changelog.d/10543.doc b/changelog.d/10543.doc deleted file mode 100644 index 6c06722eb4..0000000000 --- a/changelog.d/10543.doc +++ /dev/null @@ -1 +0,0 @@ -Fix broken links in `upgrade.md`. Contributed by @dklimpel. diff --git a/debian/changelog b/debian/changelog index c523101f9a..7b44341bc6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.40.0~rc3) stable; urgency=medium + + * New synapse release 1.40.0~rc3. + + -- Synapse Packaging team Mon, 09 Aug 2021 13:41:08 +0100 + matrix-synapse-py3 (1.40.0~rc2) stable; urgency=medium * New synapse release 1.40.0~rc2. diff --git a/synapse/__init__.py b/synapse/__init__.py index da52463531..5cca899f7d 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.40.0rc2" +__version__ = "1.40.0rc3" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 189c055eb6d8a0db7aa520ecec23819d15bfaa26 Mon Sep 17 00:00:00 2001 From: Drew Short Date: Mon, 9 Aug 2021 10:12:53 -0500 Subject: [PATCH 531/619] Moved homeserver documentation above reverse proxy examples (#10551) Signed-off-by: Drew Short --- changelog.d/10551.doc | 1 + docs/reverse_proxy.md | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10551.doc diff --git a/changelog.d/10551.doc b/changelog.d/10551.doc new file mode 100644 index 0000000000..4a2b0785bf --- /dev/null +++ b/changelog.d/10551.doc @@ -0,0 +1 @@ +Updated the reverse proxy documentation to highlight the homserver configuration that is needed to make Synapse aware that is is intentionally reverse proxied. diff --git a/docs/reverse_proxy.md b/docs/reverse_proxy.md index 76bb45aff2..5f8d20129e 100644 --- a/docs/reverse_proxy.md +++ b/docs/reverse_proxy.md @@ -33,6 +33,19 @@ Let's assume that we expect clients to connect to our server at `https://example.com:8448`. The following sections detail the configuration of the reverse proxy and the homeserver. + +## Homeserver Configuration + +The HTTP configuration will need to be updated for Synapse to correctly record +client IP addresses and generate redirect URLs while behind a reverse proxy. + +In `homeserver.yaml` set `x_forwarded: true` in the port 8008 section and +consider setting `bind_addresses: ['127.0.0.1']` so that the server only +listens to traffic on localhost. (Do not change `bind_addresses` to `127.0.0.1` +when using a containerized Synapse, as that will prevent it from responding +to proxied traffic.) + + ## Reverse-proxy configuration examples **NOTE**: You only need one of these. @@ -239,16 +252,6 @@ relay "matrix_federation" { } ``` -## Homeserver Configuration - -You will also want to set `bind_addresses: ['127.0.0.1']` and -`x_forwarded: true` for port 8008 in `homeserver.yaml` to ensure that -client IP addresses are recorded correctly. - -Having done so, you can then use `https://matrix.example.com` (instead -of `https://matrix.example.com:8448`) as the "Custom server" when -connecting to Synapse from a client. - ## Health check endpoint From 6b61debf5cf571ae9e230b102c758865eee2a788 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Mon, 9 Aug 2021 18:21:04 +0200 Subject: [PATCH 532/619] Do not remove `status_msg` when user going offline (#10550) Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10550.bugfix | 1 + synapse/handlers/presence.py | 11 +-- tests/handlers/test_presence.py | 163 +++++++++++++++++++++++++++++++- 3 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10550.bugfix diff --git a/changelog.d/10550.bugfix b/changelog.d/10550.bugfix new file mode 100644 index 0000000000..2e1b7c8bbb --- /dev/null +++ b/changelog.d/10550.bugfix @@ -0,0 +1 @@ +Fix longstanding bug which caused the user "status" to be reset when the user went offline. Contributed by @dklimpel. diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 016c5df2ca..7ca14e1d84 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -1184,8 +1184,7 @@ async def set_state( new_fields = {"state": presence} if not ignore_status_msg: - msg = status_msg if presence != PresenceState.OFFLINE else None - new_fields["status_msg"] = msg + new_fields["status_msg"] = status_msg if presence == PresenceState.ONLINE or ( presence == PresenceState.BUSY and self._busy_presence_enabled @@ -1478,7 +1477,7 @@ def format_user_presence_state( content["user_id"] = state.user_id if state.last_active_ts: content["last_active_ago"] = now - state.last_active_ts - if state.status_msg and state.state != PresenceState.OFFLINE: + if state.status_msg: content["status_msg"] = state.status_msg if state.state == PresenceState.ONLINE: content["currently_active"] = state.currently_active @@ -1840,9 +1839,7 @@ def handle_timeout( # don't set them as offline. sync_or_active = max(state.last_user_sync_ts, state.last_active_ts) if now - sync_or_active > SYNC_ONLINE_TIMEOUT: - state = state.copy_and_replace( - state=PresenceState.OFFLINE, status_msg=None - ) + state = state.copy_and_replace(state=PresenceState.OFFLINE) changed = True else: # We expect to be poked occasionally by the other side. @@ -1850,7 +1847,7 @@ def handle_timeout( # no one gets stuck online forever. if now - state.last_federation_update_ts > FEDERATION_TIMEOUT: # The other side seems to have disappeared. - state = state.copy_and_replace(state=PresenceState.OFFLINE, status_msg=None) + state = state.copy_and_replace(state=PresenceState.OFFLINE) changed = True return state if changed else None diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 18e92e90d7..29845a80da 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. - +from typing import Optional from unittest.mock import Mock, call from signedjson.key import generate_signing_key @@ -339,8 +339,11 @@ def test_persisting_presence_updates(self): class PresenceTimeoutTestCase(unittest.TestCase): + """Tests different timers and that the timer does not change `status_msg` of user.""" + def test_idle_timer(self): user_id = "@foo:bar" + status_msg = "I'm here!" now = 5000000 state = UserPresenceState.default(user_id) @@ -348,12 +351,14 @@ def test_idle_timer(self): state=PresenceState.ONLINE, last_active_ts=now - IDLE_TIMER - 1, last_user_sync_ts=now, + status_msg=status_msg, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.UNAVAILABLE) + self.assertEquals(new_state.status_msg, status_msg) def test_busy_no_idle(self): """ @@ -361,6 +366,7 @@ def test_busy_no_idle(self): presence state into unavailable. """ user_id = "@foo:bar" + status_msg = "I'm here!" now = 5000000 state = UserPresenceState.default(user_id) @@ -368,15 +374,18 @@ def test_busy_no_idle(self): state=PresenceState.BUSY, last_active_ts=now - IDLE_TIMER - 1, last_user_sync_ts=now, + status_msg=status_msg, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.BUSY) + self.assertEquals(new_state.status_msg, status_msg) def test_sync_timeout(self): user_id = "@foo:bar" + status_msg = "I'm here!" now = 5000000 state = UserPresenceState.default(user_id) @@ -384,15 +393,18 @@ def test_sync_timeout(self): state=PresenceState.ONLINE, last_active_ts=0, last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1, + status_msg=status_msg, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.OFFLINE) + self.assertEquals(new_state.status_msg, status_msg) def test_sync_online(self): user_id = "@foo:bar" + status_msg = "I'm here!" now = 5000000 state = UserPresenceState.default(user_id) @@ -400,6 +412,7 @@ def test_sync_online(self): state=PresenceState.ONLINE, last_active_ts=now - SYNC_ONLINE_TIMEOUT - 1, last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1, + status_msg=status_msg, ) new_state = handle_timeout( @@ -408,9 +421,11 @@ def test_sync_online(self): self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.ONLINE) + self.assertEquals(new_state.status_msg, status_msg) def test_federation_ping(self): user_id = "@foo:bar" + status_msg = "I'm here!" now = 5000000 state = UserPresenceState.default(user_id) @@ -419,12 +434,13 @@ def test_federation_ping(self): last_active_ts=now, last_user_sync_ts=now, last_federation_update_ts=now - FEDERATION_PING_INTERVAL - 1, + status_msg=status_msg, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNotNone(new_state) - self.assertEquals(new_state, new_state) + self.assertEquals(state, new_state) def test_no_timeout(self): user_id = "@foo:bar" @@ -444,6 +460,7 @@ def test_no_timeout(self): def test_federation_timeout(self): user_id = "@foo:bar" + status_msg = "I'm here!" now = 5000000 state = UserPresenceState.default(user_id) @@ -452,6 +469,7 @@ def test_federation_timeout(self): last_active_ts=now, last_user_sync_ts=now, last_federation_update_ts=now - FEDERATION_TIMEOUT - 1, + status_msg=status_msg, ) new_state = handle_timeout( @@ -460,9 +478,11 @@ def test_federation_timeout(self): self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.OFFLINE) + self.assertEquals(new_state.status_msg, status_msg) def test_last_active(self): user_id = "@foo:bar" + status_msg = "I'm here!" now = 5000000 state = UserPresenceState.default(user_id) @@ -471,6 +491,7 @@ def test_last_active(self): last_active_ts=now - LAST_ACTIVE_GRANULARITY - 1, last_user_sync_ts=now, last_federation_update_ts=now, + status_msg=status_msg, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) @@ -516,6 +537,144 @@ def test_external_process_timeout(self): ) self.assertEqual(state.state, PresenceState.OFFLINE) + def test_user_goes_offline_by_timeout_status_msg_remain(self): + """Test that if a user doesn't update the records for a while + users presence goes `OFFLINE` because of timeout and `status_msg` remains. + """ + user_id = "@test:server" + status_msg = "I'm here!" + + # Mark user as online + self._set_presencestate_with_status_msg( + user_id, PresenceState.ONLINE, status_msg + ) + + # Check that if we wait a while without telling the handler the user has + # stopped syncing that their presence state doesn't get timed out. + self.reactor.advance(SYNC_ONLINE_TIMEOUT / 2) + + state = self.get_success( + self.presence_handler.get_state(UserID.from_string(user_id)) + ) + self.assertEqual(state.state, PresenceState.ONLINE) + self.assertEqual(state.status_msg, status_msg) + + # Check that if the timeout fires, then the syncing user gets timed out + self.reactor.advance(SYNC_ONLINE_TIMEOUT) + + state = self.get_success( + self.presence_handler.get_state(UserID.from_string(user_id)) + ) + # status_msg should remain even after going offline + self.assertEqual(state.state, PresenceState.OFFLINE) + self.assertEqual(state.status_msg, status_msg) + + def test_user_goes_offline_manually_with_no_status_msg(self): + """Test that if a user change presence manually to `OFFLINE` + and no status is set, that `status_msg` is `None`. + """ + user_id = "@test:server" + status_msg = "I'm here!" + + # Mark user as online + self._set_presencestate_with_status_msg( + user_id, PresenceState.ONLINE, status_msg + ) + + # Mark user as offline + self.get_success( + self.presence_handler.set_state( + UserID.from_string(user_id), {"presence": PresenceState.OFFLINE} + ) + ) + + state = self.get_success( + self.presence_handler.get_state(UserID.from_string(user_id)) + ) + self.assertEqual(state.state, PresenceState.OFFLINE) + self.assertEqual(state.status_msg, None) + + def test_user_goes_offline_manually_with_status_msg(self): + """Test that if a user change presence manually to `OFFLINE` + and a status is set, that `status_msg` appears. + """ + user_id = "@test:server" + status_msg = "I'm here!" + + # Mark user as online + self._set_presencestate_with_status_msg( + user_id, PresenceState.ONLINE, status_msg + ) + + # Mark user as offline + self._set_presencestate_with_status_msg( + user_id, PresenceState.OFFLINE, "And now here." + ) + + def test_user_reset_online_with_no_status(self): + """Test that if a user set again the presence manually + and no status is set, that `status_msg` is `None`. + """ + user_id = "@test:server" + status_msg = "I'm here!" + + # Mark user as online + self._set_presencestate_with_status_msg( + user_id, PresenceState.ONLINE, status_msg + ) + + # Mark user as online again + self.get_success( + self.presence_handler.set_state( + UserID.from_string(user_id), {"presence": PresenceState.ONLINE} + ) + ) + + state = self.get_success( + self.presence_handler.get_state(UserID.from_string(user_id)) + ) + # status_msg should remain even after going offline + self.assertEqual(state.state, PresenceState.ONLINE) + self.assertEqual(state.status_msg, None) + + def test_set_presence_with_status_msg_none(self): + """Test that if a user set again the presence manually + and status is `None`, that `status_msg` is `None`. + """ + user_id = "@test:server" + status_msg = "I'm here!" + + # Mark user as online + self._set_presencestate_with_status_msg( + user_id, PresenceState.ONLINE, status_msg + ) + + # Mark user as online and `status_msg = None` + self._set_presencestate_with_status_msg(user_id, PresenceState.ONLINE, None) + + def _set_presencestate_with_status_msg( + self, user_id: str, state: PresenceState, status_msg: Optional[str] + ): + """Set a PresenceState and status_msg and check the result. + + Args: + user_id: User for that the status is to be set. + PresenceState: The new PresenceState. + status_msg: Status message that is to be set. + """ + self.get_success( + self.presence_handler.set_state( + UserID.from_string(user_id), + {"presence": state, "status_msg": status_msg}, + ) + ) + + new_state = self.get_success( + self.presence_handler.get_state(UserID.from_string(user_id)) + ) + self.assertEqual(new_state.state, state) + self.assertEqual(new_state.status_msg, status_msg) + class PresenceFederationQueueTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): From 7afb615839a2df05d39f87718016d278ebdadf5c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 9 Aug 2021 20:23:31 -0500 Subject: [PATCH 533/619] When redacting, keep event fields around that maintain the historical event structure intact (MSC2716) (#10538) * Keep event fields that maintain the historical event structure intact Fix https://github.com/matrix-org/synapse/issues/10521 * Add changelog * Bump room version * Better changelog text * Fix up room version after develop merge --- changelog.d/10538.feature | 1 + synapse/api/room_versions.py | 37 +++++++++++++++++++++++++++++++----- synapse/events/utils.py | 8 +++++++- 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 changelog.d/10538.feature diff --git a/changelog.d/10538.feature b/changelog.d/10538.feature new file mode 100644 index 0000000000..120c8e8ca0 --- /dev/null +++ b/changelog.d/10538.feature @@ -0,0 +1 @@ +Add support for new redaction rules for historical events specified in [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716). diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index f32a40ba4a..11280c4462 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -76,6 +76,8 @@ class RoomVersion: # MSC2716: Adds m.room.power_levels -> content.historical field to control # whether "insertion", "chunk", "marker" events can be sent msc2716_historical = attr.ib(type=bool) + # MSC2716: Adds support for redacting "insertion", "chunk", and "marker" events + msc2716_redactions = attr.ib(type=bool) class RoomVersions: @@ -92,6 +94,7 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=False, msc2716_historical=False, + msc2716_redactions=False, ) V2 = RoomVersion( "2", @@ -106,6 +109,7 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=False, msc2716_historical=False, + msc2716_redactions=False, ) V3 = RoomVersion( "3", @@ -120,6 +124,7 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=False, msc2716_historical=False, + msc2716_redactions=False, ) V4 = RoomVersion( "4", @@ -134,6 +139,7 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=False, msc2716_historical=False, + msc2716_redactions=False, ) V5 = RoomVersion( "5", @@ -148,6 +154,7 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=False, msc2716_historical=False, + msc2716_redactions=False, ) V6 = RoomVersion( "6", @@ -162,6 +169,7 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=False, msc2716_historical=False, + msc2716_redactions=False, ) MSC2176 = RoomVersion( "org.matrix.msc2176", @@ -176,6 +184,7 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=False, msc2716_historical=False, + msc2716_redactions=False, ) V7 = RoomVersion( "7", @@ -190,6 +199,22 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=True, msc2716_historical=False, + msc2716_redactions=False, + ) + V8 = RoomVersion( + "8", + RoomDisposition.STABLE, + EventFormatVersions.V3, + StateResolutionVersions.V2, + enforce_key_validity=True, + special_case_aliases_auth=False, + strict_canonicaljson=True, + limit_notifications_power_levels=True, + msc2176_redaction_rules=False, + msc3083_join_rules=True, + msc2403_knocking=True, + msc2716_historical=False, + msc2716_redactions=False, ) MSC2716 = RoomVersion( "org.matrix.msc2716", @@ -204,10 +229,11 @@ class RoomVersions: msc3083_join_rules=False, msc2403_knocking=True, msc2716_historical=True, + msc2716_redactions=False, ) - V8 = RoomVersion( - "8", - RoomDisposition.STABLE, + MSC2716v2 = RoomVersion( + "org.matrix.msc2716v2", + RoomDisposition.UNSTABLE, EventFormatVersions.V3, StateResolutionVersions.V2, enforce_key_validity=True, @@ -215,9 +241,10 @@ class RoomVersions: strict_canonicaljson=True, limit_notifications_power_levels=True, msc2176_redaction_rules=False, - msc3083_join_rules=True, + msc3083_join_rules=False, msc2403_knocking=True, - msc2716_historical=False, + msc2716_historical=True, + msc2716_redactions=True, ) diff --git a/synapse/events/utils.py b/synapse/events/utils.py index a0c07f62f4..b6da2f60af 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -17,7 +17,7 @@ from frozendict import frozendict -from synapse.api.constants import EventTypes, RelationTypes +from synapse.api.constants import EventContentFields, EventTypes, RelationTypes from synapse.api.errors import Codes, SynapseError from synapse.api.room_versions import RoomVersion from synapse.util.async_helpers import yieldable_gather_results @@ -135,6 +135,12 @@ def add_fields(*fields): add_fields("history_visibility") elif event_type == EventTypes.Redaction and room_version.msc2176_redaction_rules: add_fields("redacts") + elif room_version.msc2716_redactions and event_type == EventTypes.MSC2716_INSERTION: + add_fields(EventContentFields.MSC2716_NEXT_CHUNK_ID) + elif room_version.msc2716_redactions and event_type == EventTypes.MSC2716_CHUNK: + add_fields(EventContentFields.MSC2716_CHUNK_ID) + elif room_version.msc2716_redactions and event_type == EventTypes.MSC2716_MARKER: + add_fields(EventContentFields.MSC2716_MARKER_INSERTION) allowed_fields = {k: v for k, v in event_dict.items() if k in allowed_keys} From 9f7c038272318bab09535e85e6bb4345ed2f1368 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 10 Aug 2021 13:50:58 +0100 Subject: [PATCH 534/619] 1.40.0 --- CHANGES.md | 6 ++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b04abbeb4d..0e5e052951 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +Synapse 1.40.0 (2021-08-10) +=========================== + +No significant changes. + + Synapse 1.40.0rc3 (2021-08-09) ============================== diff --git a/debian/changelog b/debian/changelog index 7b44341bc6..d3da448b0f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.40.0) stable; urgency=medium + + * New synapse release 1.40.0. + + -- Synapse Packaging team Tue, 10 Aug 2021 13:50:48 +0100 + matrix-synapse-py3 (1.40.0~rc3) stable; urgency=medium * New synapse release 1.40.0~rc3. diff --git a/synapse/__init__.py b/synapse/__init__.py index 5cca899f7d..919293cd80 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.40.0rc3" +__version__ = "1.40.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From 52bfa2d59a5c372dec3204b20efbe19953b53122 Mon Sep 17 00:00:00 2001 From: Hillery Shay Date: Tue, 10 Aug 2021 06:35:54 -0700 Subject: [PATCH 535/619] Update contributing.md to warn against rebasing an open PR. (#10563) Signed-off-by: H.Shay --- CONTRIBUTING.md | 1 + changelog.d/10563.misc | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/10563.misc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7eef23419..4486a4b2cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -252,6 +252,7 @@ To prepare a Pull Request, please: 4. on GitHub, [create the Pull Request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request); 5. add a [changelog entry](#changelog) and push it to your Pull Request; 6. for most contributors, that's all - however, if you are a member of the organization `matrix-org`, on GitHub, please request a review from `matrix.org / Synapse Core`. +7. if you need to update your PR, please avoid rebasing and just add new commits to your branch. ## Changelog diff --git a/changelog.d/10563.misc b/changelog.d/10563.misc new file mode 100644 index 0000000000..8e4e90c8f4 --- /dev/null +++ b/changelog.d/10563.misc @@ -0,0 +1 @@ +Update contributing.md to warn against rebasing an open PR. From 691593bf719edb4c8b0d7a6bee95fcb41d0c56ae Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 10 Aug 2021 10:56:54 -0400 Subject: [PATCH 536/619] Fix an edge-case with invited rooms over federation in the spaces summary. (#10560) If a room which the requesting user was invited to was queried over federation it will now properly appear in the spaces summary (instead of being stripped out by the requesting server). --- changelog.d/10560.feature | 1 + synapse/handlers/space_summary.py | 93 ++++++++++++----------- tests/handlers/test_space_summary.py | 106 ++++++++++++++++++++++----- 3 files changed, 138 insertions(+), 62 deletions(-) create mode 100644 changelog.d/10560.feature diff --git a/changelog.d/10560.feature b/changelog.d/10560.feature new file mode 100644 index 0000000000..ffc4e4289c --- /dev/null +++ b/changelog.d/10560.feature @@ -0,0 +1 @@ +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 2517f278b6..d04afe6c31 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -158,48 +158,10 @@ async def get_space_summary( room = room_entry.room fed_room_id = room_entry.room_id - # The room should only be included in the summary if: - # a. the user is in the room; - # b. the room is world readable; or - # c. the user could join the room, e.g. the join rules - # are set to public or the user is in a space that - # has been granted access to the room. - # - # Note that we know the user is not in the root room (which is - # why the remote call was made in the first place), but the user - # could be in one of the children rooms and we just didn't know - # about the link. - - # The API doesn't return the room version so assume that a - # join rule of knock is valid. - include_room = ( - room.get("join_rules") in (JoinRules.PUBLIC, JoinRules.KNOCK) - or room.get("world_readable") is True - ) - - # Check if the user is a member of any of the allowed spaces - # from the response. - allowed_rooms = room.get("allowed_room_ids") or room.get( - "allowed_spaces" - ) - if ( - not include_room - and allowed_rooms - and isinstance(allowed_rooms, list) - ): - include_room = await self._event_auth_handler.is_user_in_rooms( - allowed_rooms, requester - ) - - # Finally, if this isn't the requested room, check ourselves - # if we can access the room. - if not include_room and fed_room_id != queue_entry.room_id: - include_room = await self._is_room_accessible( - fed_room_id, requester, None - ) - # The user can see the room, include it! - if include_room: + if await self._is_remote_room_accessible( + requester, fed_room_id, room + ): # Before returning to the client, remove the allowed_room_ids # and allowed_spaces keys. room.pop("allowed_room_ids", None) @@ -336,7 +298,7 @@ async def _summarize_local_room( Returns: A room entry if the room should be returned. None, otherwise. """ - if not await self._is_room_accessible(room_id, requester, origin): + if not await self._is_local_room_accessible(room_id, requester, origin): return None room_entry = await self._build_room_entry(room_id, for_federation=bool(origin)) @@ -438,7 +400,7 @@ async def _summarize_remote_room( return results - async def _is_room_accessible( + async def _is_local_room_accessible( self, room_id: str, requester: Optional[str], origin: Optional[str] ) -> bool: """ @@ -550,6 +512,51 @@ async def _is_room_accessible( ) return False + async def _is_remote_room_accessible( + self, requester: str, room_id: str, room: JsonDict + ) -> bool: + """ + Calculate whether the room received over federation should be shown in the spaces summary. + + It should be included if: + + * The requester is joined or can join the room (per MSC3173). + * The history visibility is set to world readable. + + Note that the local server is not in the requested room (which is why the + remote call was made in the first place), but the user could have access + due to an invite, etc. + + Args: + requester: The user requesting the summary. + room_id: The room ID returned over federation. + room: The summary of the child room returned over federation. + + Returns: + True if the room should be included in the spaces summary. + """ + # The API doesn't return the room version so assume that a + # join rule of knock is valid. + if ( + room.get("join_rules") in (JoinRules.PUBLIC, JoinRules.KNOCK) + or room.get("world_readable") is True + ): + return True + + # Check if the user is a member of any of the allowed spaces + # from the response. + allowed_rooms = room.get("allowed_room_ids") or room.get("allowed_spaces") + if allowed_rooms and isinstance(allowed_rooms, list): + if await self._event_auth_handler.is_user_in_rooms( + allowed_rooms, requester + ): + return True + + # Finally, check locally if we can access the room. The user might + # already be in the room (if it was a child room), or there might be a + # pending invite, etc. + return await self._is_local_room_accessible(room_id, requester, None) + async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDict: """ Generate en entry suitable for the 'rooms' list in the summary response. diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 6cc1a02e12..f470c81ea2 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -30,7 +30,7 @@ from synapse.rest import admin from synapse.rest.client.v1 import login, room from synapse.server import HomeServer -from synapse.types import JsonDict +from synapse.types import JsonDict, UserID from tests import unittest @@ -149,6 +149,36 @@ def _assert_events( events, ) + def _poke_fed_invite(self, room_id: str, from_user: str) -> None: + """ + Creates a invite (as if received over federation) for the room from the + given hostname. + + Args: + room_id: The room ID to issue an invite for. + fed_hostname: The user to invite from. + """ + # Poke an invite over federation into the database. + fed_handler = self.hs.get_federation_handler() + fed_hostname = UserID.from_string(from_user).domain + event = make_event_from_dict( + { + "room_id": room_id, + "event_id": "!abcd:" + fed_hostname, + "type": EventTypes.Member, + "sender": from_user, + "state_key": self.user, + "content": {"membership": Membership.INVITE}, + "prev_events": [], + "auth_events": [], + "depth": 1, + "origin_server_ts": 1234, + } + ) + self.get_success( + fed_handler.on_invite_request(fed_hostname, event, RoomVersions.V6) + ) + def test_simple_space(self): """Test a simple space with a single room.""" result = self.get_success(self.handler.get_space_summary(self.user, self.space)) @@ -416,24 +446,7 @@ def test_fed_filtering(self): joined_room = self.helper.create_room_as(self.user, tok=self.token) # Poke an invite over federation into the database. - fed_handler = self.hs.get_federation_handler() - event = make_event_from_dict( - { - "room_id": invited_room, - "event_id": "!abcd:" + fed_hostname, - "type": EventTypes.Member, - "sender": "@remote:" + fed_hostname, - "state_key": self.user, - "content": {"membership": Membership.INVITE}, - "prev_events": [], - "auth_events": [], - "depth": 1, - "origin_server_ts": 1234, - } - ) - self.get_success( - fed_handler.on_invite_request(fed_hostname, event, RoomVersions.V6) - ) + self._poke_fed_invite(invited_room, "@remote:" + fed_hostname) async def summarize_remote_room( _self, room, suggested_only, max_children, exclude_rooms @@ -570,3 +583,58 @@ async def summarize_remote_room( (subspace, joined_room), ], ) + + def test_fed_invited(self): + """ + A room which the user was invited to should be included in the response. + + This differs from test_fed_filtering in that the room itself is being + queried over federation, instead of it being included as a sub-room of + a space in the response. + """ + fed_hostname = self.hs.hostname + "2" + fed_room = "#subroom:" + fed_hostname + + # Poke an invite over federation into the database. + self._poke_fed_invite(fed_room, "@remote:" + fed_hostname) + + async def summarize_remote_room( + _self, room, suggested_only, max_children, exclude_rooms + ): + return [ + _RoomEntry( + fed_room, + { + "room_id": fed_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ), + ] + + # Add a room to the space which is on another server. + self._add_child(self.space, fed_room, self.token) + + with mock.patch( + "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room", + new=summarize_remote_room, + ): + result = self.get_success( + self.handler.get_space_summary(self.user, self.space) + ) + + self._assert_rooms( + result, + [ + self.space, + self.room, + fed_room, + ], + ) + self._assert_events( + result, + [ + (self.space, self.room), + (self.space, fed_room), + ], + ) From 8da9e3cb69db1bed68889b6a5fbcecf1bf20a235 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Aug 2021 10:59:13 +0100 Subject: [PATCH 537/619] Move test_old_deps.sh to new ci dir --- .github/workflows/tests.yml | 2 +- MANIFEST.in | 1 + {.buildkite => ci}/scripts/test_old_deps.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename {.buildkite => ci}/scripts/test_old_deps.sh (81%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 75c2976a25..8612d1fb3a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -144,7 +144,7 @@ jobs: uses: docker://ubuntu:bionic # For old python and sqlite with: workdir: /github/workspace - entrypoint: .buildkite/scripts/test_old_deps.sh + entrypoint: ci/scripts/test_old_deps.sh env: TRIAL_FLAGS: "--jobs=2" - name: Dump logs diff --git a/MANIFEST.in b/MANIFEST.in index 0522319c40..174e1b1f47 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -47,6 +47,7 @@ recursive-include changelog.d * prune .buildkite prune .circleci prune .github +prune ci prune contrib prune debian prune demo/etc diff --git a/.buildkite/scripts/test_old_deps.sh b/ci/scripts/test_old_deps.sh similarity index 81% rename from .buildkite/scripts/test_old_deps.sh rename to ci/scripts/test_old_deps.sh index 9270d55f04..8b473936f8 100755 --- a/.buildkite/scripts/test_old_deps.sh +++ b/ci/scripts/test_old_deps.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# this script is run by buildkite in a plain `bionic` container; it installs the +# this script is run by GitHub Actions in a plain `bionic` container; it installs the # minimal requirements for tox and hands over to the py3-old tox environment. set -ex From 03fb99a5c8dbe67cf300986e76ea0e8183641211 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Aug 2021 12:15:10 +0100 Subject: [PATCH 538/619] check-newsfragment: pass pr number explicitly use PULL_REQUEST_NUMBER instead of BUILDKITE_PULL_REQUEST remove the other user of BUILDKITE_PULL_REQUEST, namely merge_base_branch.sh --- .buildkite/.env | 1 - .buildkite/merge_base_branch.sh | 35 --------------------------------- .github/workflows/tests.yml | 6 ++---- scripts-dev/check-newsfragment | 2 +- 4 files changed, 3 insertions(+), 41 deletions(-) delete mode 100755 .buildkite/merge_base_branch.sh diff --git a/.buildkite/.env b/.buildkite/.env index 85b102d07f..a2969b96a1 100644 --- a/.buildkite/.env +++ b/.buildkite/.env @@ -7,7 +7,6 @@ BUILDKITE_JOB_ID BUILDKITE_BUILD_URL BUILDKITE_PROJECT_SLUG BUILDKITE_COMMIT -BUILDKITE_PULL_REQUEST BUILDKITE_TAG CODECOV_TOKEN TRIAL_FLAGS diff --git a/.buildkite/merge_base_branch.sh b/.buildkite/merge_base_branch.sh deleted file mode 100755 index 361440fd1a..0000000000 --- a/.buildkite/merge_base_branch.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [[ "$BUILDKITE_BRANCH" =~ ^(develop|master|dinsic|shhs|release-.*)$ ]]; then - echo "Not merging forward, as this is a release branch" - exit 0 -fi - -if [[ -z $BUILDKITE_PULL_REQUEST_BASE_BRANCH ]]; then - echo "Not a pull request, or hasn't had a PR opened yet..." - - # It probably hasn't had a PR opened yet. Since all PRs land on develop, we - # can probably assume it's based on it and will be merged into it. - GITBASE="develop" -else - # Get the reference, using the GitHub API - GITBASE=$BUILDKITE_PULL_REQUEST_BASE_BRANCH -fi - -echo "--- merge_base_branch $GITBASE" - -# Show what we are before -git --no-pager show -s - -# Set up username so it can do a merge -git config --global user.email bot@matrix.org -git config --global user.name "A robot" - -# Fetch and merge. If it doesn't work, it will raise due to set -e. -git fetch -u origin $GITBASE -git merge --no-edit --no-commit origin/$GITBASE - -# Show what we are after. -git --no-pager show -s diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8612d1fb3a..5349e83133 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,11 +47,9 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v2 - run: pip install tox - - name: Patch Buildkite-specific test script - run: | - sed -i -e 's/\$BUILDKITE_PULL_REQUEST/${{ github.event.number }}/' \ - scripts-dev/check-newsfragment - run: scripts-dev/check-newsfragment + env: + PULL_REQUEST_NUMBER: ${{ github.event.number }} lint-sdist: runs-on: ubuntu-latest diff --git a/scripts-dev/check-newsfragment b/scripts-dev/check-newsfragment index af6d32e332..393a548d58 100755 --- a/scripts-dev/check-newsfragment +++ b/scripts-dev/check-newsfragment @@ -11,7 +11,7 @@ set -e git remote set-branches --add origin develop git fetch -q origin develop -pr="$BUILDKITE_PULL_REQUEST" +pr="$PULL_REQUEST_NUMBER" # if there are changes in the debian directory, check that the debian changelog # has been updated From 3d67b8c82b0128660376f81d226c111ad3e272a7 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Aug 2021 12:43:50 +0100 Subject: [PATCH 539/619] Move sytest worker-blacklist to ci directory --- .github/workflows/tests.yml | 2 +- {.buildkite => ci}/worker-blacklist | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {.buildkite => ci}/worker-blacklist (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5349e83133..cd88184488 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -230,7 +230,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Prepare test blacklist - run: cat sytest-blacklist .buildkite/worker-blacklist > synapse-blacklist-with-workers + run: cat sytest-blacklist ci/worker-blacklist > synapse-blacklist-with-workers - name: Run SyTest run: /bootstrap.sh synapse working-directory: /src diff --git a/.buildkite/worker-blacklist b/ci/worker-blacklist similarity index 100% rename from .buildkite/worker-blacklist rename to ci/worker-blacklist From c5988a8eb7279f1de2d09258a41ff21158eb62c5 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Aug 2021 12:45:13 +0100 Subject: [PATCH 540/619] Remove unused BUILDKITE_BRANCH env var --- .buildkite/.env | 1 - .github/workflows/tests.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.buildkite/.env b/.buildkite/.env index a2969b96a1..fc3606ead2 100644 --- a/.buildkite/.env +++ b/.buildkite/.env @@ -1,7 +1,6 @@ CI BUILDKITE BUILDKITE_BUILD_NUMBER -BUILDKITE_BRANCH BUILDKITE_BUILD_NUMBER BUILDKITE_JOB_ID BUILDKITE_BUILD_URL diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cd88184488..a04f6abbed 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -195,7 +195,6 @@ jobs: volumes: - ${{ github.workspace }}:/src env: - BUILDKITE_BRANCH: ${{ github.head_ref }} POSTGRES: ${{ matrix.postgres && 1}} MULTI_POSTGRES: ${{ (matrix.postgres == 'multi-postgres') && 1}} WORKERS: ${{ matrix.workers && 1 }} From 58e5da5aa06ee4dc1ad5b2774e7bcd4eb9911a70 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Aug 2021 13:11:43 +0100 Subject: [PATCH 541/619] Remove buildkite from portdb CI tests --- .coveragerc | 4 ++-- .github/workflows/tests.yml | 8 +------- {.buildkite => ci}/postgres-config.yaml | 4 ++-- {.buildkite => ci}/scripts/postgres_exec.py | 2 +- .../scripts/test_synapse_port_db.sh | 18 +++++++++--------- {.buildkite => ci}/sqlite-config.yaml | 4 ++-- 6 files changed, 17 insertions(+), 23 deletions(-) rename {.buildkite => ci}/postgres-config.yaml (86%) rename {.buildkite => ci}/scripts/postgres_exec.py (92%) rename {.buildkite => ci}/scripts/test_synapse_port_db.sh (60%) rename {.buildkite => ci}/sqlite-config.yaml (80%) diff --git a/.coveragerc b/.coveragerc index 11f2ec8387..bbf9046b06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,8 @@ [run] branch = True parallel = True -include=$TOP/synapse/* -data_file = $TOP/.coverage +include=$GITHUB_WORKSPACE/synapse/* +data_file = $GITHUB_WORKSPACE/.coverage [report] precision = 2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a04f6abbed..572bc81b0f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -278,13 +278,7 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Patch Buildkite-specific test scripts - run: | - sed -i -e 's/host="postgres"/host="localhost"/' .buildkite/scripts/postgres_exec.py - sed -i -e 's/host: postgres/host: localhost/' .buildkite/postgres-config.yaml - sed -i -e 's|/src/||' .buildkite/{sqlite,postgres}-config.yaml - sed -i -e 's/\$TOP/\$GITHUB_WORKSPACE/' .coveragerc - - run: .buildkite/scripts/test_synapse_port_db.sh + - run: ci/scripts/test_synapse_port_db.sh complement: if: ${{ !failure() && !cancelled() }} diff --git a/.buildkite/postgres-config.yaml b/ci/postgres-config.yaml similarity index 86% rename from .buildkite/postgres-config.yaml rename to ci/postgres-config.yaml index 67e17fa9d1..511fef495d 100644 --- a/.buildkite/postgres-config.yaml +++ b/ci/postgres-config.yaml @@ -3,7 +3,7 @@ # CI's Docker setup at the point where this file is considered. server_name: "localhost:8800" -signing_key_path: ".buildkite/test.signing.key" +signing_key_path: "ci/test.signing.key" report_stats: false @@ -11,7 +11,7 @@ database: name: "psycopg2" args: user: postgres - host: postgres + host: localhost password: postgres database: synapse diff --git a/.buildkite/scripts/postgres_exec.py b/ci/scripts/postgres_exec.py similarity index 92% rename from .buildkite/scripts/postgres_exec.py rename to ci/scripts/postgres_exec.py index 086b391724..0f39a336d5 100755 --- a/.buildkite/scripts/postgres_exec.py +++ b/ci/scripts/postgres_exec.py @@ -23,7 +23,7 @@ # We use "postgres" as a database because it's bound to exist and the "synapse" one # doesn't exist yet. db_conn = psycopg2.connect( - user="postgres", host="postgres", password="postgres", dbname="postgres" + user="postgres", host="localhost", password="postgres", dbname="postgres" ) db_conn.autocommit = True cur = db_conn.cursor() diff --git a/.buildkite/scripts/test_synapse_port_db.sh b/ci/scripts/test_synapse_port_db.sh similarity index 60% rename from .buildkite/scripts/test_synapse_port_db.sh rename to ci/scripts/test_synapse_port_db.sh index 82d7d56d4e..9ee0ad42fc 100755 --- a/.buildkite/scripts/test_synapse_port_db.sh +++ b/ci/scripts/test_synapse_port_db.sh @@ -20,22 +20,22 @@ pip install -e . echo "--- Generate the signing key" # Generate the server's signing key. -python -m synapse.app.homeserver --generate-keys -c .buildkite/sqlite-config.yaml +python -m synapse.app.homeserver --generate-keys -c ci/sqlite-config.yaml echo "--- Prepare test database" # Make sure the SQLite3 database is using the latest schema and has no pending background update. -scripts-dev/update_database --database-config .buildkite/sqlite-config.yaml +scripts-dev/update_database --database-config ci/sqlite-config.yaml # Create the PostgreSQL database. -./.buildkite/scripts/postgres_exec.py "CREATE DATABASE synapse" +./ci/scripts/postgres_exec.py "CREATE DATABASE synapse" echo "+++ Run synapse_port_db against test database" -coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml +coverage run scripts/synapse_port_db --sqlite-database ci/test_db.db --postgres-config ci/postgres-config.yaml # We should be able to run twice against the same database. echo "+++ Run synapse_port_db a second time" -coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml +coverage run scripts/synapse_port_db --sqlite-database ci/test_db.db --postgres-config ci/postgres-config.yaml ##### @@ -44,14 +44,14 @@ coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --p echo "--- Prepare empty SQLite database" # we do this by deleting the sqlite db, and then doing the same again. -rm .buildkite/test_db.db +rm ci/test_db.db -scripts-dev/update_database --database-config .buildkite/sqlite-config.yaml +scripts-dev/update_database --database-config ci/sqlite-config.yaml # re-create the PostgreSQL database. -./.buildkite/scripts/postgres_exec.py \ +./ci/scripts/postgres_exec.py \ "DROP DATABASE synapse" \ "CREATE DATABASE synapse" echo "+++ Run synapse_port_db against empty database" -coverage run scripts/synapse_port_db --sqlite-database .buildkite/test_db.db --postgres-config .buildkite/postgres-config.yaml +coverage run scripts/synapse_port_db --sqlite-database ci/test_db.db --postgres-config ci/postgres-config.yaml diff --git a/.buildkite/sqlite-config.yaml b/ci/sqlite-config.yaml similarity index 80% rename from .buildkite/sqlite-config.yaml rename to ci/sqlite-config.yaml index d16459cfd9..fd5a1c1451 100644 --- a/.buildkite/sqlite-config.yaml +++ b/ci/sqlite-config.yaml @@ -3,14 +3,14 @@ # schema and run background updates on it. server_name: "localhost:8800" -signing_key_path: ".buildkite/test.signing.key" +signing_key_path: "ci/test.signing.key" report_stats: false database: name: "sqlite3" args: - database: ".buildkite/test_db.db" + database: "ci/test_db.db" # Suppress the key server warning. trusted_key_servers: [] From c0ebdfc77e8f3e75ea162f12b2188acdde5bf4ef Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Aug 2021 13:28:50 +0100 Subject: [PATCH 542/619] Kill off the .buildkite dir completely --- .buildkite/.env | 11 ----------- MANIFEST.in | 1 - {.buildkite => ci}/test_db.db | Bin scripts-dev/lint.sh | 2 +- tox.ini | 2 +- 5 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 .buildkite/.env rename {.buildkite => ci}/test_db.db (100%) diff --git a/.buildkite/.env b/.buildkite/.env deleted file mode 100644 index fc3606ead2..0000000000 --- a/.buildkite/.env +++ /dev/null @@ -1,11 +0,0 @@ -CI -BUILDKITE -BUILDKITE_BUILD_NUMBER -BUILDKITE_BUILD_NUMBER -BUILDKITE_JOB_ID -BUILDKITE_BUILD_URL -BUILDKITE_PROJECT_SLUG -BUILDKITE_COMMIT -BUILDKITE_TAG -CODECOV_TOKEN -TRIAL_FLAGS diff --git a/MANIFEST.in b/MANIFEST.in index 174e1b1f47..37d61f40de 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -44,7 +44,6 @@ include book.toml include pyproject.toml recursive-include changelog.d * -prune .buildkite prune .circleci prune .github prune ci diff --git a/.buildkite/test_db.db b/ci/test_db.db similarity index 100% rename from .buildkite/test_db.db rename to ci/test_db.db diff --git a/scripts-dev/lint.sh b/scripts-dev/lint.sh index 869eb2372d..2c77643cda 100755 --- a/scripts-dev/lint.sh +++ b/scripts-dev/lint.sh @@ -94,7 +94,7 @@ else "scripts-dev/build_debian_packages" "scripts-dev/sign_json" "scripts-dev/update_database" - "contrib" "synctl" "setup.py" "synmark" "stubs" ".buildkite" + "contrib" "synctl" "setup.py" "synmark" "stubs" "ci" ) fi fi diff --git a/tox.ini b/tox.ini index da77d124fc..b695126019 100644 --- a/tox.ini +++ b/tox.ini @@ -49,7 +49,7 @@ lint_targets = contrib synctl synmark - .buildkite + ci docker # default settings for all tox environments From fe1d0c86180ea025dfb444597c7ad72b036bbb10 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 10 Aug 2021 13:08:17 -0400 Subject: [PATCH 543/619] Add local support for the new spaces summary endpoint (MSC2946) (#10549) This adds support for the /hierarchy endpoint, which is an update to MSC2946. Currently this only supports rooms known locally to the homeserver. --- changelog.d/10527.misc | 2 +- changelog.d/10530.misc | 2 +- changelog.d/10549.feature | 1 + synapse/handlers/space_summary.py | 201 +++++++++++++- synapse/rest/client/v1/room.py | 41 +++ tests/handlers/test_space_summary.py | 386 +++++++++++++++++++-------- 6 files changed, 521 insertions(+), 112 deletions(-) create mode 100644 changelog.d/10549.feature diff --git a/changelog.d/10527.misc b/changelog.d/10527.misc index 3cf22f9daf..ffc4e4289c 100644 --- a/changelog.d/10527.misc +++ b/changelog.d/10527.misc @@ -1 +1 @@ -Prepare for the new spaces summary endpoint (updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946)). +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10530.misc b/changelog.d/10530.misc index 3cf22f9daf..ffc4e4289c 100644 --- a/changelog.d/10530.misc +++ b/changelog.d/10530.misc @@ -1 +1 @@ -Prepare for the new spaces summary endpoint (updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946)). +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10549.feature b/changelog.d/10549.feature new file mode 100644 index 0000000000..ffc4e4289c --- /dev/null +++ b/changelog.d/10549.feature @@ -0,0 +1 @@ +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index d04afe6c31..fd76c34695 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -18,7 +18,7 @@ from collections import deque from typing import ( TYPE_CHECKING, - Collection, + Deque, Dict, Iterable, List, @@ -38,9 +38,12 @@ Membership, RoomTypes, ) +from synapse.api.errors import Codes, SynapseError from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 from synapse.types import JsonDict +from synapse.util.caches.response_cache import ResponseCache +from synapse.util.stringutils import random_string if TYPE_CHECKING: from synapse.server import HomeServer @@ -57,6 +60,29 @@ MAX_SERVERS_PER_SPACE = 3 +@attr.s(slots=True, frozen=True, auto_attribs=True) +class _PaginationKey: + """The key used to find unique pagination session.""" + + # The first three entries match the request parameters (and cannot change + # during a pagination session). + room_id: str + suggested_only: bool + max_depth: Optional[int] + # The randomly generated token. + token: str + + +@attr.s(slots=True, frozen=True, auto_attribs=True) +class _PaginationSession: + """The information that is stored for pagination.""" + + # The queue of rooms which are still to process. + room_queue: Deque["_RoomQueueEntry"] + # A set of rooms which have been processed. + processed_rooms: Set[str] + + class SpaceSummaryHandler: def __init__(self, hs: "HomeServer"): self._clock = hs.get_clock() @@ -67,6 +93,21 @@ def __init__(self, hs: "HomeServer"): self._server_name = hs.hostname self._federation_client = hs.get_federation_client() + # A map of query information to the current pagination state. + # + # TODO Allow for multiple workers to share this data. + # TODO Expire pagination tokens. + self._pagination_sessions: Dict[_PaginationKey, _PaginationSession] = {} + + # If a user tries to fetch the same page multiple times in quick succession, + # only process the first attempt and return its result to subsequent requests. + self._pagination_response_cache: ResponseCache[ + Tuple[str, bool, Optional[int], Optional[int], Optional[str]] + ] = ResponseCache( + hs.get_clock(), + "get_room_hierarchy", + ) + async def get_space_summary( self, requester: str, @@ -130,7 +171,7 @@ async def get_space_summary( requester, None, room_id, suggested_only, max_children ) - events: Collection[JsonDict] = [] + events: Sequence[JsonDict] = [] if room_entry: rooms_result.append(room_entry.room) events = room_entry.children @@ -207,6 +248,154 @@ async def get_space_summary( return {"rooms": rooms_result, "events": events_result} + async def get_room_hierarchy( + self, + requester: str, + requested_room_id: str, + suggested_only: bool = False, + max_depth: Optional[int] = None, + limit: Optional[int] = None, + from_token: Optional[str] = None, + ) -> JsonDict: + """ + Implementation of the room hierarchy C-S API. + + Args: + requester: The user ID of the user making this request. + requested_room_id: The room ID to start the hierarchy at (the "root" room). + suggested_only: Whether we should only return children with the "suggested" + flag set. + max_depth: The maximum depth in the tree to explore, must be a + non-negative integer. + + 0 would correspond to just the root room, 1 would include just + the root room's children, etc. + limit: An optional limit on the number of rooms to return per + page. Must be a positive integer. + from_token: An optional pagination token. + + Returns: + The JSON hierarchy dictionary. + """ + # If a user tries to fetch the same page multiple times in quick succession, + # only process the first attempt and return its result to subsequent requests. + # + # This is due to the pagination process mutating internal state, attempting + # to process multiple requests for the same page will result in errors. + return await self._pagination_response_cache.wrap( + (requested_room_id, suggested_only, max_depth, limit, from_token), + self._get_room_hierarchy, + requester, + requested_room_id, + suggested_only, + max_depth, + limit, + from_token, + ) + + async def _get_room_hierarchy( + self, + requester: str, + requested_room_id: str, + suggested_only: bool = False, + max_depth: Optional[int] = None, + limit: Optional[int] = None, + from_token: Optional[str] = None, + ) -> JsonDict: + """See docstring for SpaceSummaryHandler.get_room_hierarchy.""" + + # first of all, check that the user is in the room in question (or it's + # world-readable) + await self._auth.check_user_in_room_or_world_readable( + requested_room_id, requester + ) + + # If this is continuing a previous session, pull the persisted data. + if from_token: + pagination_key = _PaginationKey( + requested_room_id, suggested_only, max_depth, from_token + ) + if pagination_key not in self._pagination_sessions: + raise SynapseError(400, "Unknown pagination token", Codes.INVALID_PARAM) + + # Load the previous state. + pagination_session = self._pagination_sessions[pagination_key] + room_queue = pagination_session.room_queue + processed_rooms = pagination_session.processed_rooms + else: + # the queue of rooms to process + room_queue = deque((_RoomQueueEntry(requested_room_id, ()),)) + + # Rooms we have already processed. + processed_rooms = set() + + rooms_result: List[JsonDict] = [] + + # Cap the limit to a server-side maximum. + if limit is None: + limit = MAX_ROOMS + else: + limit = min(limit, MAX_ROOMS) + + # Iterate through the queue until we reach the limit or run out of + # rooms to include. + while room_queue and len(rooms_result) < limit: + queue_entry = room_queue.popleft() + room_id = queue_entry.room_id + current_depth = queue_entry.depth + if room_id in processed_rooms: + # already done this room + continue + + logger.debug("Processing room %s", room_id) + + is_in_room = await self._store.is_host_joined(room_id, self._server_name) + if is_in_room: + room_entry = await self._summarize_local_room( + requester, + None, + room_id, + suggested_only, + # TODO Handle max children. + max_children=None, + ) + + if room_entry: + rooms_result.append(room_entry.as_json()) + + # Add the child to the queue. We have already validated + # that the vias are a list of server names. + # + # If the current depth is the maximum depth, do not queue + # more entries. + if max_depth is None or current_depth < max_depth: + room_queue.extendleft( + _RoomQueueEntry( + ev["state_key"], ev["content"]["via"], current_depth + 1 + ) + for ev in reversed(room_entry.children) + ) + + processed_rooms.add(room_id) + else: + # TODO Federation. + pass + + result: JsonDict = {"rooms": rooms_result} + + # If there's additional data, generate a pagination token (and persist state). + if room_queue: + next_token = random_string(24) + result["next_token"] = next_token + pagination_key = _PaginationKey( + requested_room_id, suggested_only, max_depth, next_token + ) + self._pagination_sessions[pagination_key] = _PaginationSession( + room_queue, processed_rooms + ) + + return result + async def federation_space_summary( self, origin: str, @@ -652,6 +841,7 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: class _RoomQueueEntry: room_id: str via: Sequence[str] + depth: int = 0 @attr.s(frozen=True, slots=True, auto_attribs=True) @@ -662,7 +852,12 @@ class _RoomEntry: # An iterable of the sorted, stripped children events for children of this room. # # This may not include all children. - children: Collection[JsonDict] = () + children: Sequence[JsonDict] = () + + def as_json(self) -> JsonDict: + result = dict(self.room) + result["children_state"] = self.children + return result def _has_valid_via(e: EventBase) -> bool: diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index f887970b76..b28b72bfbd 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -1445,6 +1445,46 @@ async def on_POST( ) +class RoomHierarchyRestServlet(RestServlet): + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/org.matrix.msc2946" + "/rooms/(?P[^/]*)/hierarchy$" + ), + ) + + def __init__(self, hs: "HomeServer"): + super().__init__() + self._auth = hs.get_auth() + self._space_summary_handler = hs.get_space_summary_handler() + + async def on_GET( + self, request: SynapseRequest, room_id: str + ) -> Tuple[int, JsonDict]: + requester = await self._auth.get_user_by_req(request, allow_guest=True) + + max_depth = parse_integer(request, "max_depth") + if max_depth is not None and max_depth < 0: + raise SynapseError( + 400, "'max_depth' must be a non-negative integer", Codes.BAD_JSON + ) + + limit = parse_integer(request, "limit") + if limit is not None and limit <= 0: + raise SynapseError( + 400, "'limit' must be a positive integer", Codes.BAD_JSON + ) + + return 200, await self._space_summary_handler.get_room_hierarchy( + requester.user.to_string(), + room_id, + suggested_only=parse_boolean(request, "suggested_only", default=False), + max_depth=max_depth, + limit=limit, + from_token=parse_string(request, "from"), + ) + + def register_servlets(hs: "HomeServer", http_server, is_worker=False): msc2716_enabled = hs.config.experimental.msc2716_enabled @@ -1463,6 +1503,7 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False): RoomTypingRestServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) RoomSpaceSummaryRestServlet(hs).register(http_server) + RoomHierarchyRestServlet(hs).register(http_server) RoomEventServlet(hs).register(http_server) JoinedRoomsRestServlet(hs).register(http_server) RoomAliasListServlet(hs).register(http_server) diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index f470c81ea2..255dd17f86 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -23,7 +23,7 @@ RestrictedJoinRuleTypes, RoomTypes, ) -from synapse.api.errors import AuthError +from synapse.api.errors import AuthError, SynapseError from synapse.api.room_versions import RoomVersions from synapse.events import make_event_from_dict from synapse.handlers.space_summary import _child_events_comparison_key, _RoomEntry @@ -123,32 +123,83 @@ def prepare(self, reactor, clock, hs: HomeServer): self.room = self.helper.create_room_as(self.user, tok=self.token) self._add_child(self.space, self.room, self.token) - def _add_child(self, space_id: str, room_id: str, token: str) -> None: + def _add_child( + self, space_id: str, room_id: str, token: str, order: Optional[str] = None + ) -> None: """Add a child room to a space.""" + content = {"via": [self.hs.hostname]} + if order is not None: + content["order"] = order self.helper.send_state( space_id, event_type=EventTypes.SpaceChild, - body={"via": [self.hs.hostname]}, + body=content, tok=token, state_key=room_id, ) - def _assert_rooms(self, result: JsonDict, rooms: Iterable[str]) -> None: - """Assert that the expected room IDs are in the response.""" - self.assertCountEqual([room.get("room_id") for room in result["rooms"]], rooms) - - def _assert_events( - self, result: JsonDict, events: Iterable[Tuple[str, str]] + def _assert_rooms( + self, result: JsonDict, rooms_and_children: Iterable[Tuple[str, Iterable[str]]] ) -> None: - """Assert that the expected parent / child room IDs are in the response.""" + """ + Assert that the expected room IDs and events are in the response. + + Args: + result: The result from the API call. + rooms_and_children: An iterable of tuples where each tuple is: + The expected room ID. + The expected IDs of any children rooms. + """ + room_ids = [] + children_ids = [] + for room_id, children in rooms_and_children: + room_ids.append(room_id) + if children: + children_ids.extend([(room_id, child_id) for child_id in children]) + self.assertCountEqual( + [room.get("room_id") for room in result["rooms"]], room_ids + ) self.assertCountEqual( [ (event.get("room_id"), event.get("state_key")) for event in result["events"] ], - events, + children_ids, ) + def _assert_hierarchy( + self, result: JsonDict, rooms_and_children: Iterable[Tuple[str, Iterable[str]]] + ) -> None: + """ + Assert that the expected room IDs are in the response. + + Args: + result: The result from the API call. + rooms_and_children: An iterable of tuples where each tuple is: + The expected room ID. + The expected IDs of any children rooms. + """ + result_room_ids = [] + result_children_ids = [] + for result_room in result["rooms"]: + result_room_ids.append(result_room["room_id"]) + result_children_ids.append( + [ + (cs["room_id"], cs["state_key"]) + for cs in result_room.get("children_state") + ] + ) + + room_ids = [] + children_ids = [] + for room_id, children in rooms_and_children: + room_ids.append(room_id) + children_ids.append([(room_id, child_id) for child_id in children]) + + # Note that order matters. + self.assertEqual(result_room_ids, room_ids) + self.assertEqual(result_children_ids, children_ids) + def _poke_fed_invite(self, room_id: str, from_user: str) -> None: """ Creates a invite (as if received over federation) for the room from the @@ -184,8 +235,13 @@ def test_simple_space(self): result = self.get_success(self.handler.get_space_summary(self.user, self.space)) # The result should have the space and the room in it, along with a link # from space -> room. - self._assert_rooms(result, [self.space, self.room]) - self._assert_events(result, [(self.space, self.room)]) + expected = [(self.space, [self.room]), (self.room, ())] + self._assert_rooms(result, expected) + + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space) + ) + self._assert_hierarchy(result, expected) def test_visibility(self): """A user not in a space cannot inspect it.""" @@ -194,6 +250,7 @@ def test_visibility(self): # The user cannot see the space. self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError) + self.get_failure(self.handler.get_room_hierarchy(user2, self.space), AuthError) # If the space is made world-readable it should return a result. self.helper.send_state( @@ -203,8 +260,11 @@ def test_visibility(self): tok=self.token, ) result = self.get_success(self.handler.get_space_summary(user2, self.space)) - self._assert_rooms(result, [self.space, self.room]) - self._assert_events(result, [(self.space, self.room)]) + expected = [(self.space, [self.room]), (self.room, ())] + self._assert_rooms(result, expected) + + result = self.get_success(self.handler.get_room_hierarchy(user2, self.space)) + self._assert_hierarchy(result, expected) # Make it not world-readable again and confirm it results in an error. self.helper.send_state( @@ -214,12 +274,15 @@ def test_visibility(self): tok=self.token, ) self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError) + self.get_failure(self.handler.get_room_hierarchy(user2, self.space), AuthError) # Join the space and results should be returned. self.helper.join(self.space, user2, tok=token2) result = self.get_success(self.handler.get_space_summary(user2, self.space)) - self._assert_rooms(result, [self.space, self.room]) - self._assert_events(result, [(self.space, self.room)]) + self._assert_rooms(result, expected) + + result = self.get_success(self.handler.get_room_hierarchy(user2, self.space)) + self._assert_hierarchy(result, expected) def _create_room_with_join_rule( self, join_rule: str, room_version: Optional[str] = None, **extra_content @@ -290,34 +353,33 @@ def test_filtering(self): # Join the space. self.helper.join(self.space, user2, tok=token2) result = self.get_success(self.handler.get_space_summary(user2, self.space)) - - self._assert_rooms( - result, - [ + expected = [ + ( self.space, - self.room, - public_room, - knock_room, - invited_room, - restricted_accessible_room, - world_readable_room, - joined_room, - ], - ) - self._assert_events( - result, - [ - (self.space, self.room), - (self.space, public_room), - (self.space, knock_room), - (self.space, not_invited_room), - (self.space, invited_room), - (self.space, restricted_room), - (self.space, restricted_accessible_room), - (self.space, world_readable_room), - (self.space, joined_room), - ], - ) + [ + self.room, + public_room, + knock_room, + not_invited_room, + invited_room, + restricted_room, + restricted_accessible_room, + world_readable_room, + joined_room, + ], + ), + (self.room, ()), + (public_room, ()), + (knock_room, ()), + (invited_room, ()), + (restricted_accessible_room, ()), + (world_readable_room, ()), + (joined_room, ()), + ] + self._assert_rooms(result, expected) + + result = self.get_success(self.handler.get_room_hierarchy(user2, self.space)) + self._assert_hierarchy(result, expected) def test_complex_space(self): """ @@ -349,19 +411,145 @@ def test_complex_space(self): result = self.get_success(self.handler.get_space_summary(self.user, self.space)) # The result should include each room a single time and each link. - self._assert_rooms(result, [self.space, self.room, subspace, subroom]) - self._assert_events( - result, - [ - (self.space, self.room), - (self.space, room2), - (self.space, subspace), - (subspace, subroom), - (subspace, self.room), - (subspace, room2), - ], + expected = [ + (self.space, [self.room, room2, subspace]), + (self.room, ()), + (subspace, [subroom, self.room, room2]), + (subroom, ()), + ] + self._assert_rooms(result, expected) + + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space) + ) + self._assert_hierarchy(result, expected) + + def test_pagination(self): + """Test simple pagination works.""" + room_ids = [] + for i in range(1, 10): + room = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(self.space, room, self.token, order=str(i)) + room_ids.append(room) + # The room created initially doesn't have an order, so comes last. + room_ids.append(self.room) + + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space, limit=7) + ) + # The result should have the space and all of the links, plus some of the + # rooms and a pagination token. + expected = [(self.space, room_ids)] + [ + (room_id, ()) for room_id in room_ids[:6] + ] + self._assert_hierarchy(result, expected) + self.assertIn("next_token", result) + + # Check the next page. + result = self.get_success( + self.handler.get_room_hierarchy( + self.user, self.space, limit=5, from_token=result["next_token"] + ) + ) + # The result should have the space and the room in it, along with a link + # from space -> room. + expected = [(room_id, ()) for room_id in room_ids[6:]] + self._assert_hierarchy(result, expected) + self.assertNotIn("next_token", result) + + def test_invalid_pagination_token(self): + """""" + room_ids = [] + for i in range(1, 10): + room = self.helper.create_room_as(self.user, tok=self.token) + self._add_child(self.space, room, self.token, order=str(i)) + room_ids.append(room) + # The room created initially doesn't have an order, so comes last. + room_ids.append(self.room) + + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space, limit=7) + ) + self.assertIn("next_token", result) + + # Changing the room ID, suggested-only, or max-depth causes an error. + self.get_failure( + self.handler.get_room_hierarchy( + self.user, self.room, from_token=result["next_token"] + ), + SynapseError, + ) + self.get_failure( + self.handler.get_room_hierarchy( + self.user, + self.space, + suggested_only=True, + from_token=result["next_token"], + ), + SynapseError, + ) + self.get_failure( + self.handler.get_room_hierarchy( + self.user, self.space, max_depth=0, from_token=result["next_token"] + ), + SynapseError, ) + # An invalid token is ignored. + self.get_failure( + self.handler.get_room_hierarchy(self.user, self.space, from_token="foo"), + SynapseError, + ) + + def test_max_depth(self): + """Create a deep tree to test the max depth against.""" + spaces = [self.space] + rooms = [self.room] + for _ in range(5): + spaces.append( + self.helper.create_room_as( + self.user, + tok=self.token, + extra_content={ + "creation_content": { + EventContentFields.ROOM_TYPE: RoomTypes.SPACE + } + }, + ) + ) + self._add_child(spaces[-2], spaces[-1], self.token) + rooms.append(self.helper.create_room_as(self.user, tok=self.token)) + self._add_child(spaces[-1], rooms[-1], self.token) + + # Test just the space itself. + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space, max_depth=0) + ) + expected = [(spaces[0], [rooms[0], spaces[1]])] + self._assert_hierarchy(result, expected) + + # A single additional layer. + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space, max_depth=1) + ) + expected += [ + (rooms[0], ()), + (spaces[1], [rooms[1], spaces[2]]), + ] + self._assert_hierarchy(result, expected) + + # A few layers. + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space, max_depth=3) + ) + expected += [ + (rooms[1], ()), + (spaces[2], [rooms[2], spaces[3]]), + (rooms[2], ()), + (spaces[3], [rooms[3], spaces[4]]), + ] + self._assert_hierarchy(result, expected) + def test_fed_complex(self): """ Return data over federation and ensure that it is handled properly. @@ -417,15 +605,13 @@ async def summarize_remote_room( self.handler.get_space_summary(self.user, self.space) ) - self._assert_rooms(result, [self.space, self.room, subspace, subroom]) - self._assert_events( - result, - [ - (self.space, self.room), - (self.space, subspace), - (subspace, subroom), - ], - ) + expected = [ + (self.space, [self.room, subspace]), + (self.room, ()), + (subspace, [subroom]), + (subroom, ()), + ] + self._assert_rooms(result, expected) def test_fed_filtering(self): """ @@ -554,35 +740,30 @@ async def summarize_remote_room( self.handler.get_space_summary(self.user, self.space) ) - self._assert_rooms( - result, - [ - self.space, - self.room, + expected = [ + (self.space, [self.room, subspace]), + (self.room, ()), + ( subspace, - public_room, - knock_room, - invited_room, - restricted_accessible_room, - world_readable_room, - joined_room, - ], - ) - self._assert_events( - result, - [ - (self.space, self.room), - (self.space, subspace), - (subspace, public_room), - (subspace, knock_room), - (subspace, not_invited_room), - (subspace, invited_room), - (subspace, restricted_room), - (subspace, restricted_accessible_room), - (subspace, world_readable_room), - (subspace, joined_room), - ], - ) + [ + public_room, + knock_room, + not_invited_room, + invited_room, + restricted_room, + restricted_accessible_room, + world_readable_room, + joined_room, + ], + ), + (public_room, ()), + (knock_room, ()), + (invited_room, ()), + (restricted_accessible_room, ()), + (world_readable_room, ()), + (joined_room, ()), + ] + self._assert_rooms(result, expected) def test_fed_invited(self): """ @@ -623,18 +804,9 @@ async def summarize_remote_room( self.handler.get_space_summary(self.user, self.space) ) - self._assert_rooms( - result, - [ - self.space, - self.room, - fed_room, - ], - ) - self._assert_events( - result, - [ - (self.space, self.room), - (self.space, fed_room), - ], - ) + expected = [ + (self.space, [self.room, fed_room]), + (self.room, ()), + (fed_room, ()), + ] + self._assert_rooms(result, expected) From b924a5c2e493423e1411322c933ceaad35fc4803 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Aug 2021 18:37:40 +0100 Subject: [PATCH 544/619] Add changelog entry and signoff Signed-off-by: David Robertson --- changelog.d/10573.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10573.misc diff --git a/changelog.d/10573.misc b/changelog.d/10573.misc new file mode 100644 index 0000000000..fc9b1a2f70 --- /dev/null +++ b/changelog.d/10573.misc @@ -0,0 +1 @@ +Remove references to BuildKite in favour of GitHub Actions. \ No newline at end of file From 8c654b73095a594b36101aa81cf91a8e1bebc29f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 10 Aug 2021 18:10:40 -0500 Subject: [PATCH 545/619] Only return state events that the AS passed in via `state_events_at_start` (MSC2716) (#10552) * Only return state events that the AS passed in via state_events_at_start As discovered by @Half-Shot in https://github.com/matrix-org/matrix-doc/pull/2716#discussion_r684158448 Part of MSC2716 * Add changelog * Fix changelog extension --- changelog.d/10552.misc | 1 + synapse/rest/client/v1/room.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10552.misc diff --git a/changelog.d/10552.misc b/changelog.d/10552.misc new file mode 100644 index 0000000000..fc5f6aea5f --- /dev/null +++ b/changelog.d/10552.misc @@ -0,0 +1 @@ +Update `/batch_send` endpoint to only return `state_events` created by the `state_events_from_before` passed in. diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index b28b72bfbd..f1bc43be2d 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -437,6 +437,7 @@ async def on_POST(self, request, room_id): prev_state_ids = list(prev_state_map.values()) auth_event_ids = prev_state_ids + state_events_at_start = [] for state_event in body["state_events_at_start"]: assert_params_in_dict( state_event, ["type", "origin_server_ts", "content", "sender"] @@ -502,6 +503,7 @@ async def on_POST(self, request, room_id): ) event_id = event.event_id + state_events_at_start.append(event_id) auth_event_ids.append(event_id) events_to_create = body["events"] @@ -651,7 +653,7 @@ async def on_POST(self, request, room_id): event_ids.append(base_insertion_event.event_id) return 200, { - "state_events": auth_event_ids, + "state_events": state_events_at_start, "events": event_ids, "next_chunk_id": insertion_event["content"][ EventContentFields.MSC2716_NEXT_CHUNK_ID From 339c3918e1301d53b998c98282137b12d9d16c45 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Wed, 11 Aug 2021 16:34:59 +0200 Subject: [PATCH 546/619] support federation queries through http connect proxy (#10475) Signed-off-by: Marcus Hoffmann Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10475.feature | 1 + docs/setup/forward_proxy.md | 6 +- docs/upgrade.md | 27 ++ synapse/http/connectproxyclient.py | 68 ++- .../federation/matrix_federation_agent.py | 100 ++++- synapse/http/matrixfederationclient.py | 12 +- synapse/http/proxyagent.py | 51 +-- .../test_matrix_federation_agent.py | 406 ++++++++++++++---- tests/http/test_proxyagent.py | 75 ++-- 9 files changed, 555 insertions(+), 191 deletions(-) create mode 100644 changelog.d/10475.feature diff --git a/changelog.d/10475.feature b/changelog.d/10475.feature new file mode 100644 index 0000000000..52eab11b03 --- /dev/null +++ b/changelog.d/10475.feature @@ -0,0 +1 @@ +Add support for sending federation requests through a proxy. Contributed by @Bubu and @dklimpel. \ No newline at end of file diff --git a/docs/setup/forward_proxy.md b/docs/setup/forward_proxy.md index a0720ab342..494c14893b 100644 --- a/docs/setup/forward_proxy.md +++ b/docs/setup/forward_proxy.md @@ -45,18 +45,18 @@ The proxy will be **used** for: - recaptcha validation - CAS auth validation - OpenID Connect +- Outbound federation - Federation (checking public key revocation) +- Fetching public keys of other servers +- Downloading remote media It will **not be used** for: - Application Services - Identity servers -- Outbound federation - In worker configurations - connections between workers - connections from workers to Redis -- Fetching public keys of other servers -- Downloading remote media ## Troubleshooting diff --git a/docs/upgrade.md b/docs/upgrade.md index ce9167e6de..8831c9d6cf 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -86,6 +86,33 @@ process, for example: ``` +# Upgrading to v1.xx.0 + +## Add support for routing outbound HTTP requests via a proxy for federation + +Since Synapse 1.6.0 (2019-11-26) you can set a proxy for outbound HTTP requests via +http_proxy/https_proxy environment variables. This proxy was set for: +- push +- url previews +- phone-home stats +- recaptcha validation +- CAS auth validation +- OpenID Connect +- Federation (checking public key revocation) + +In this version we have added support for outbound requests for: +- Outbound federation +- Downloading remote media +- Fetching public keys of other servers + +These requests use the same proxy configuration. If you have a proxy configuration we +recommend to verify the configuration. It may be necessary to adjust the `no_proxy` +environment variable. + +See [using a forward proxy with Synapse documentation](setup/forward_proxy.md) for +details. + + # Upgrading to v1.39.0 ## Deprecation of the current third-party rules module interface diff --git a/synapse/http/connectproxyclient.py b/synapse/http/connectproxyclient.py index 17e1c5abb1..c577142268 100644 --- a/synapse/http/connectproxyclient.py +++ b/synapse/http/connectproxyclient.py @@ -12,8 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import logging +from typing import Optional +import attr from zope.interface import implementer from twisted.internet import defer, protocol @@ -21,7 +24,6 @@ from twisted.internet.interfaces import IReactorCore, IStreamClientEndpoint from twisted.internet.protocol import ClientFactory, Protocol, connectionDone from twisted.web import http -from twisted.web.http_headers import Headers logger = logging.getLogger(__name__) @@ -30,6 +32,22 @@ class ProxyConnectError(ConnectError): pass +@attr.s +class ProxyCredentials: + username_password = attr.ib(type=bytes) + + def as_proxy_authorization_value(self) -> bytes: + """ + Return the value for a Proxy-Authorization header (i.e. 'Basic abdef=='). + + Returns: + A transformation of the authentication string the encoded value for + a Proxy-Authorization header. + """ + # Encode as base64 and prepend the authorization type + return b"Basic " + base64.encodebytes(self.username_password) + + @implementer(IStreamClientEndpoint) class HTTPConnectProxyEndpoint: """An Endpoint implementation which will send a CONNECT request to an http proxy @@ -46,7 +64,7 @@ class HTTPConnectProxyEndpoint: proxy_endpoint: the endpoint to use to connect to the proxy host: hostname that we want to CONNECT to port: port that we want to connect to - headers: Extra HTTP headers to include in the CONNECT request + proxy_creds: credentials to authenticate at proxy """ def __init__( @@ -55,20 +73,20 @@ def __init__( proxy_endpoint: IStreamClientEndpoint, host: bytes, port: int, - headers: Headers, + proxy_creds: Optional[ProxyCredentials], ): self._reactor = reactor self._proxy_endpoint = proxy_endpoint self._host = host self._port = port - self._headers = headers + self._proxy_creds = proxy_creds def __repr__(self): return "" % (self._proxy_endpoint,) def connect(self, protocolFactory: ClientFactory): f = HTTPProxiedClientFactory( - self._host, self._port, protocolFactory, self._headers + self._host, self._port, protocolFactory, self._proxy_creds ) d = self._proxy_endpoint.connect(f) # once the tcp socket connects successfully, we need to wait for the @@ -87,7 +105,7 @@ class HTTPProxiedClientFactory(protocol.ClientFactory): dst_host: hostname that we want to CONNECT to dst_port: port that we want to connect to wrapped_factory: The original Factory - headers: Extra HTTP headers to include in the CONNECT request + proxy_creds: credentials to authenticate at proxy """ def __init__( @@ -95,12 +113,12 @@ def __init__( dst_host: bytes, dst_port: int, wrapped_factory: ClientFactory, - headers: Headers, + proxy_creds: Optional[ProxyCredentials], ): self.dst_host = dst_host self.dst_port = dst_port self.wrapped_factory = wrapped_factory - self.headers = headers + self.proxy_creds = proxy_creds self.on_connection = defer.Deferred() def startedConnecting(self, connector): @@ -114,7 +132,7 @@ def buildProtocol(self, addr): self.dst_port, wrapped_protocol, self.on_connection, - self.headers, + self.proxy_creds, ) def clientConnectionFailed(self, connector, reason): @@ -145,7 +163,7 @@ class HTTPConnectProtocol(protocol.Protocol): connected_deferred: a Deferred which will be callbacked with wrapped_protocol when the CONNECT completes - headers: Extra HTTP headers to include in the CONNECT request + proxy_creds: credentials to authenticate at proxy """ def __init__( @@ -154,16 +172,16 @@ def __init__( port: int, wrapped_protocol: Protocol, connected_deferred: defer.Deferred, - headers: Headers, + proxy_creds: Optional[ProxyCredentials], ): self.host = host self.port = port self.wrapped_protocol = wrapped_protocol self.connected_deferred = connected_deferred - self.headers = headers + self.proxy_creds = proxy_creds self.http_setup_client = HTTPConnectSetupClient( - self.host, self.port, self.headers + self.host, self.port, self.proxy_creds ) self.http_setup_client.on_connected.addCallback(self.proxyConnected) @@ -205,30 +223,38 @@ class HTTPConnectSetupClient(http.HTTPClient): Args: host: The hostname to send in the CONNECT message port: The port to send in the CONNECT message - headers: Extra headers to send with the CONNECT message + proxy_creds: credentials to authenticate at proxy """ - def __init__(self, host: bytes, port: int, headers: Headers): + def __init__( + self, + host: bytes, + port: int, + proxy_creds: Optional[ProxyCredentials], + ): self.host = host self.port = port - self.headers = headers + self.proxy_creds = proxy_creds self.on_connected = defer.Deferred() def connectionMade(self): logger.debug("Connected to proxy, sending CONNECT") self.sendCommand(b"CONNECT", b"%s:%d" % (self.host, self.port)) - # Send any additional specified headers - for name, values in self.headers.getAllRawHeaders(): - for value in values: - self.sendHeader(name, value) + # Determine whether we need to set Proxy-Authorization headers + if self.proxy_creds: + # Set a Proxy-Authorization header + self.sendHeader( + b"Proxy-Authorization", + self.proxy_creds.as_proxy_authorization_value(), + ) self.endHeaders() def handleStatus(self, version: bytes, status: bytes, message: bytes): logger.debug("Got Status: %s %s %s", status, message, version) if status != b"200": - raise ProxyConnectError("Unexpected status on CONNECT: %s" % status) + raise ProxyConnectError(f"Unexpected status on CONNECT: {status!s}") def handleEndHeaders(self): logger.debug("End Headers") diff --git a/synapse/http/federation/matrix_federation_agent.py b/synapse/http/federation/matrix_federation_agent.py index c16b7f10e6..1238bfd287 100644 --- a/synapse/http/federation/matrix_federation_agent.py +++ b/synapse/http/federation/matrix_federation_agent.py @@ -14,6 +14,10 @@ import logging import urllib.parse from typing import Any, Generator, List, Optional +from urllib.request import ( # type: ignore[attr-defined] + getproxies_environment, + proxy_bypass_environment, +) from netaddr import AddrFormatError, IPAddress, IPSet from zope.interface import implementer @@ -30,9 +34,12 @@ from twisted.web.iweb import IAgent, IAgentEndpointFactory, IBodyProducer, IResponse from synapse.crypto.context_factory import FederationPolicyForHTTPS -from synapse.http.client import BlacklistingAgentWrapper +from synapse.http import proxyagent +from synapse.http.client import BlacklistingAgentWrapper, BlacklistingReactorWrapper +from synapse.http.connectproxyclient import HTTPConnectProxyEndpoint from synapse.http.federation.srv_resolver import Server, SrvResolver from synapse.http.federation.well_known_resolver import WellKnownResolver +from synapse.http.proxyagent import ProxyAgent from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.types import ISynapseReactor from synapse.util import Clock @@ -57,6 +64,14 @@ class MatrixFederationAgent: user_agent: The user agent header to use for federation requests. + ip_whitelist: Allowed IP addresses. + + ip_blacklist: Disallowed IP addresses. + + proxy_reactor: twisted reactor to use for connections to the proxy server + reactor might have some blacklisting applied (i.e. for DNS queries), + but we need unblocked access to the proxy. + _srv_resolver: SrvResolver implementation to use for looking up SRV records. None to use a default implementation. @@ -71,11 +86,18 @@ def __init__( reactor: ISynapseReactor, tls_client_options_factory: Optional[FederationPolicyForHTTPS], user_agent: bytes, + ip_whitelist: IPSet, ip_blacklist: IPSet, _srv_resolver: Optional[SrvResolver] = None, _well_known_resolver: Optional[WellKnownResolver] = None, ): - self._reactor = reactor + # proxy_reactor is not blacklisted + proxy_reactor = reactor + + # We need to use a DNS resolver which filters out blacklisted IP + # addresses, to prevent DNS rebinding. + reactor = BlacklistingReactorWrapper(reactor, ip_whitelist, ip_blacklist) + self._clock = Clock(reactor) self._pool = HTTPConnectionPool(reactor) self._pool.retryAutomatically = False @@ -83,24 +105,27 @@ def __init__( self._pool.cachedConnectionTimeout = 2 * 60 self._agent = Agent.usingEndpointFactory( - self._reactor, + reactor, MatrixHostnameEndpointFactory( - reactor, tls_client_options_factory, _srv_resolver + reactor, + proxy_reactor, + tls_client_options_factory, + _srv_resolver, ), pool=self._pool, ) self.user_agent = user_agent if _well_known_resolver is None: - # Note that the name resolver has already been wrapped in a - # IPBlacklistingResolver by MatrixFederationHttpClient. _well_known_resolver = WellKnownResolver( - self._reactor, + reactor, agent=BlacklistingAgentWrapper( - Agent( - self._reactor, + ProxyAgent( + reactor, + proxy_reactor, pool=self._pool, contextFactory=tls_client_options_factory, + use_proxy=True, ), ip_blacklist=ip_blacklist, ), @@ -200,10 +225,12 @@ class MatrixHostnameEndpointFactory: def __init__( self, reactor: IReactorCore, + proxy_reactor: IReactorCore, tls_client_options_factory: Optional[FederationPolicyForHTTPS], srv_resolver: Optional[SrvResolver], ): self._reactor = reactor + self._proxy_reactor = proxy_reactor self._tls_client_options_factory = tls_client_options_factory if srv_resolver is None: @@ -211,9 +238,10 @@ def __init__( self._srv_resolver = srv_resolver - def endpointForURI(self, parsed_uri): + def endpointForURI(self, parsed_uri: URI): return MatrixHostnameEndpoint( self._reactor, + self._proxy_reactor, self._tls_client_options_factory, self._srv_resolver, parsed_uri, @@ -227,23 +255,45 @@ class MatrixHostnameEndpoint: Args: reactor: twisted reactor to use for underlying requests + proxy_reactor: twisted reactor to use for connections to the proxy server. + 'reactor' might have some blacklisting applied (i.e. for DNS queries), + but we need unblocked access to the proxy. tls_client_options_factory: factory to use for fetching client tls options, or none to disable TLS. srv_resolver: The SRV resolver to use parsed_uri: The parsed URI that we're wanting to connect to. + + Raises: + ValueError if the environment variables contain an invalid proxy specification. + RuntimeError if no tls_options_factory is given for a https connection """ def __init__( self, reactor: IReactorCore, + proxy_reactor: IReactorCore, tls_client_options_factory: Optional[FederationPolicyForHTTPS], srv_resolver: SrvResolver, parsed_uri: URI, ): self._reactor = reactor - self._parsed_uri = parsed_uri + # http_proxy is not needed because federation is always over TLS + proxies = getproxies_environment() + https_proxy = proxies["https"].encode() if "https" in proxies else None + self.no_proxy = proxies["no"] if "no" in proxies else None + + # endpoint and credentials to use to connect to the outbound https proxy, if any. + ( + self._https_proxy_endpoint, + self._https_proxy_creds, + ) = proxyagent.http_proxy_endpoint( + https_proxy, + proxy_reactor, + tls_client_options_factory, + ) + # set up the TLS connection params # # XXX disabling TLS is really only supported here for the benefit of the @@ -273,9 +323,33 @@ async def _do_connect(self, protocol_factory: IProtocolFactory) -> None: host = server.host port = server.port + should_skip_proxy = False + if self.no_proxy is not None: + should_skip_proxy = proxy_bypass_environment( + host.decode(), + proxies={"no": self.no_proxy}, + ) + + endpoint: IStreamClientEndpoint try: - logger.debug("Connecting to %s:%i", host.decode("ascii"), port) - endpoint = HostnameEndpoint(self._reactor, host, port) + if self._https_proxy_endpoint and not should_skip_proxy: + logger.debug( + "Connecting to %s:%i via %s", + host.decode("ascii"), + port, + self._https_proxy_endpoint, + ) + endpoint = HTTPConnectProxyEndpoint( + self._reactor, + self._https_proxy_endpoint, + host, + port, + proxy_creds=self._https_proxy_creds, + ) + else: + logger.debug("Connecting to %s:%i", host.decode("ascii"), port) + # not using a proxy + endpoint = HostnameEndpoint(self._reactor, host, port) if self._tls_options: endpoint = wrapClientTLS(self._tls_options, endpoint) result = await make_deferred_yieldable( diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 2efa15bf04..2e9898997c 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -59,7 +59,6 @@ from synapse.http import QuieterFileBodyProducer from synapse.http.client import ( BlacklistingAgentWrapper, - BlacklistingReactorWrapper, BodyExceededMaxSize, ByteWriteable, encode_query_args, @@ -69,7 +68,7 @@ from synapse.logging import opentracing from synapse.logging.context import make_deferred_yieldable from synapse.logging.opentracing import set_tag, start_active_span, tags -from synapse.types import ISynapseReactor, JsonDict +from synapse.types import JsonDict from synapse.util import json_decoder from synapse.util.async_helpers import timeout_deferred from synapse.util.metrics import Measure @@ -325,13 +324,7 @@ def __init__(self, hs, tls_client_options_factory): self.signing_key = hs.signing_key self.server_name = hs.hostname - # We need to use a DNS resolver which filters out blacklisted IP - # addresses, to prevent DNS rebinding. - self.reactor: ISynapseReactor = BlacklistingReactorWrapper( - hs.get_reactor(), - hs.config.federation_ip_range_whitelist, - hs.config.federation_ip_range_blacklist, - ) + self.reactor = hs.get_reactor() user_agent = hs.version_string if hs.config.user_agent_suffix: @@ -342,6 +335,7 @@ def __init__(self, hs, tls_client_options_factory): self.reactor, tls_client_options_factory, user_agent, + hs.config.federation_ip_range_whitelist, hs.config.federation_ip_range_blacklist, ) diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py index 19e987f118..a3f31452d0 100644 --- a/synapse/http/proxyagent.py +++ b/synapse/http/proxyagent.py @@ -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 base64 import logging import re from typing import Any, Dict, Optional, Tuple @@ -21,7 +20,6 @@ proxy_bypass_environment, ) -import attr from zope.interface import implementer from twisted.internet import defer @@ -38,7 +36,7 @@ from twisted.web.http_headers import Headers from twisted.web.iweb import IAgent, IBodyProducer, IPolicyForHTTPS -from synapse.http.connectproxyclient import HTTPConnectProxyEndpoint +from synapse.http.connectproxyclient import HTTPConnectProxyEndpoint, ProxyCredentials from synapse.types import ISynapseReactor logger = logging.getLogger(__name__) @@ -46,22 +44,6 @@ _VALID_URI = re.compile(br"\A[\x21-\x7e]+\Z") -@attr.s -class ProxyCredentials: - username_password = attr.ib(type=bytes) - - def as_proxy_authorization_value(self) -> bytes: - """ - Return the value for a Proxy-Authorization header (i.e. 'Basic abdef=='). - - Returns: - A transformation of the authentication string the encoded value for - a Proxy-Authorization header. - """ - # Encode as base64 and prepend the authorization type - return b"Basic " + base64.encodebytes(self.username_password) - - @implementer(IAgent) class ProxyAgent(_AgentBase): """An Agent implementation which will use an HTTP proxy if one was requested @@ -95,6 +77,7 @@ class ProxyAgent(_AgentBase): Raises: ValueError if use_proxy is set and the environment variables contain an invalid proxy specification. + RuntimeError if no tls_options_factory is given for a https connection """ def __init__( @@ -131,11 +114,11 @@ def __init__( https_proxy = proxies["https"].encode() if "https" in proxies else None no_proxy = proxies["no"] if "no" in proxies else None - self.http_proxy_endpoint, self.http_proxy_creds = _http_proxy_endpoint( + self.http_proxy_endpoint, self.http_proxy_creds = http_proxy_endpoint( http_proxy, self.proxy_reactor, contextFactory, **self._endpoint_kwargs ) - self.https_proxy_endpoint, self.https_proxy_creds = _http_proxy_endpoint( + self.https_proxy_endpoint, self.https_proxy_creds = http_proxy_endpoint( https_proxy, self.proxy_reactor, contextFactory, **self._endpoint_kwargs ) @@ -224,22 +207,12 @@ def request( and self.https_proxy_endpoint and not should_skip_proxy ): - connect_headers = Headers() - - # Determine whether we need to set Proxy-Authorization headers - if self.https_proxy_creds: - # Set a Proxy-Authorization header - connect_headers.addRawHeader( - b"Proxy-Authorization", - self.https_proxy_creds.as_proxy_authorization_value(), - ) - endpoint = HTTPConnectProxyEndpoint( self.proxy_reactor, self.https_proxy_endpoint, parsed_uri.host, parsed_uri.port, - headers=connect_headers, + self.https_proxy_creds, ) else: # not using a proxy @@ -268,10 +241,10 @@ def request( ) -def _http_proxy_endpoint( +def http_proxy_endpoint( proxy: Optional[bytes], reactor: IReactorCore, - tls_options_factory: IPolicyForHTTPS, + tls_options_factory: Optional[IPolicyForHTTPS], **kwargs, ) -> Tuple[Optional[IStreamClientEndpoint], Optional[ProxyCredentials]]: """Parses an http proxy setting and returns an endpoint for the proxy @@ -294,6 +267,7 @@ def _http_proxy_endpoint( Raise: ValueError if proxy has no hostname or unsupported scheme. + RuntimeError if no tls_options_factory is given for a https connection """ if proxy is None: return None, None @@ -305,8 +279,13 @@ def _http_proxy_endpoint( proxy_endpoint = HostnameEndpoint(reactor, host, port, **kwargs) if scheme == b"https": - tls_options = tls_options_factory.creatorForNetloc(host, port) - proxy_endpoint = wrapClientTLS(tls_options, proxy_endpoint) + if tls_options_factory: + tls_options = tls_options_factory.creatorForNetloc(host, port) + proxy_endpoint = wrapClientTLS(tls_options, proxy_endpoint) + else: + raise RuntimeError( + f"No TLS options for a https connection via proxy {proxy!s}" + ) return proxy_endpoint, credentials diff --git a/tests/http/federation/test_matrix_federation_agent.py b/tests/http/federation/test_matrix_federation_agent.py index a37bce08c3..992d8f94fd 100644 --- a/tests/http/federation/test_matrix_federation_agent.py +++ b/tests/http/federation/test_matrix_federation_agent.py @@ -11,9 +11,11 @@ # 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 base64 import logging -from typing import Optional -from unittest.mock import Mock +import os +from typing import Iterable, Optional +from unittest.mock import Mock, patch import treq from netaddr import IPSet @@ -22,11 +24,12 @@ from twisted.internet import defer from twisted.internet._sslverify import ClientTLSOptions, OpenSSLCertificateOptions +from twisted.internet.interfaces import IProtocolFactory from twisted.internet.protocol import Factory -from twisted.protocols.tls import TLSMemoryBIOFactory +from twisted.protocols.tls import TLSMemoryBIOFactory, TLSMemoryBIOProtocol from twisted.web._newclient import ResponseNeverReceived from twisted.web.client import Agent -from twisted.web.http import HTTPChannel +from twisted.web.http import HTTPChannel, Request from twisted.web.http_headers import Headers from twisted.web.iweb import IPolicyForHTTPS @@ -49,24 +52,6 @@ logger = logging.getLogger(__name__) -test_server_connection_factory = None - - -def get_connection_factory(): - # this needs to happen once, but not until we are ready to run the first test - global test_server_connection_factory - if test_server_connection_factory is None: - test_server_connection_factory = TestServerTLSConnectionFactory( - sanlist=[ - b"DNS:testserv", - b"DNS:target-server", - b"DNS:xn--bcher-kva.com", - b"IP:1.2.3.4", - b"IP:::1", - ] - ) - return test_server_connection_factory - # Once Async Mocks or lambdas are supported this can go away. def generate_resolve_service(result): @@ -100,24 +85,38 @@ def setUp(self): had_well_known_cache=self.had_well_known_cache, ) - self.agent = MatrixFederationAgent( - reactor=self.reactor, - tls_client_options_factory=self.tls_factory, - user_agent="test-agent", # Note that this is unused since _well_known_resolver is provided. - ip_blacklist=IPSet(), - _srv_resolver=self.mock_resolver, - _well_known_resolver=self.well_known_resolver, - ) - - def _make_connection(self, client_factory, expected_sni): + def _make_connection( + self, + client_factory: IProtocolFactory, + ssl: bool = True, + expected_sni: bytes = None, + tls_sanlist: Optional[Iterable[bytes]] = None, + ) -> HTTPChannel: """Builds a test server, and completes the outgoing client connection + Args: + client_factory: the the factory that the + application is trying to use to make the outbound connection. We will + invoke it to build the client Protocol + + ssl: If true, we will expect an ssl connection and wrap + server_factory with a TLSMemoryBIOFactory + False is set only for when proxy expect http connection. + Otherwise federation requests use always https. + + expected_sni: the expected SNI value + + tls_sanlist: list of SAN entries for the TLS cert presented by the server. Returns: - HTTPChannel: the test server + the server Protocol returned by server_factory """ # build the test server - server_tls_protocol = _build_test_server(get_connection_factory()) + server_factory = _get_test_protocol_factory() + if ssl: + server_factory = _wrap_server_factory_for_tls(server_factory, tls_sanlist) + + server_protocol = server_factory.buildProtocol(None) # now, tell the client protocol factory to build the client protocol (it will be a # _WrappingProtocol, around a TLSMemoryBIOProtocol, around an @@ -128,35 +127,39 @@ def _make_connection(self, client_factory, expected_sni): # stubbing that out here. client_protocol = client_factory.buildProtocol(None) client_protocol.makeConnection( - FakeTransport(server_tls_protocol, self.reactor, client_protocol) + FakeTransport(server_protocol, self.reactor, client_protocol) ) - # tell the server tls protocol to send its stuff back to the client, too - server_tls_protocol.makeConnection( - FakeTransport(client_protocol, self.reactor, server_tls_protocol) + # tell the server protocol to send its stuff back to the client, too + server_protocol.makeConnection( + FakeTransport(client_protocol, self.reactor, server_protocol) ) - # grab a hold of the TLS connection, in case it gets torn down - server_tls_connection = server_tls_protocol._tlsConnection - - # fish the test server back out of the server-side TLS protocol. - http_protocol = server_tls_protocol.wrappedProtocol + if ssl: + # fish the test server back out of the server-side TLS protocol. + http_protocol = server_protocol.wrappedProtocol + # grab a hold of the TLS connection, in case it gets torn down + tls_connection = server_protocol._tlsConnection + else: + http_protocol = server_protocol + tls_connection = None - # give the reactor a pump to get the TLS juices flowing. - self.reactor.pump((0.1,)) + # give the reactor a pump to get the TLS juices flowing (if needed) + self.reactor.advance(0) # check the SNI - server_name = server_tls_connection.get_servername() - self.assertEqual( - server_name, - expected_sni, - "Expected SNI %s but got %s" % (expected_sni, server_name), - ) + if expected_sni is not None: + server_name = tls_connection.get_servername() + self.assertEqual( + server_name, + expected_sni, + f"Expected SNI {expected_sni!s} but got {server_name!s}", + ) return http_protocol @defer.inlineCallbacks - def _make_get_request(self, uri): + def _make_get_request(self, uri: bytes): """ Sends a simple GET request via the agent, and checks its logcontext management """ @@ -180,20 +183,20 @@ def _make_get_request(self, uri): def _handle_well_known_connection( self, - client_factory, - expected_sni, - content, + client_factory: IProtocolFactory, + expected_sni: bytes, + content: bytes, response_headers: Optional[dict] = None, - ): + ) -> HTTPChannel: """Handle an outgoing HTTPs connection: wire it up to a server, check that the request is for a .well-known, and send the response. Args: - client_factory (IProtocolFactory): outgoing connection - expected_sni (bytes): SNI that we expect the outgoing connection to send - content (bytes): content to send back as the .well-known + client_factory: outgoing connection + expected_sni: SNI that we expect the outgoing connection to send + content: content to send back as the .well-known Returns: - HTTPChannel: server impl + server impl """ # make the connection for .well-known well_known_server = self._make_connection( @@ -209,7 +212,10 @@ def _handle_well_known_connection( return well_known_server def _send_well_known_response( - self, request, content, headers: Optional[dict] = None + self, + request: Request, + content: bytes, + headers: Optional[dict] = None, ): """Check that an incoming request looks like a valid .well-known request, and send back the response. @@ -225,10 +231,37 @@ def _send_well_known_response( self.reactor.pump((0.1,)) - def test_get(self): + def _make_agent(self) -> MatrixFederationAgent: """ - happy-path test of a GET request with an explicit port + If a proxy server is set, the MatrixFederationAgent must be created again + because it is created too early during setUp """ + return MatrixFederationAgent( + reactor=self.reactor, + tls_client_options_factory=self.tls_factory, + user_agent="test-agent", # Note that this is unused since _well_known_resolver is provided. + ip_whitelist=IPSet(), + ip_blacklist=IPSet(), + _srv_resolver=self.mock_resolver, + _well_known_resolver=self.well_known_resolver, + ) + + def test_get(self): + """happy-path test of a GET request with an explicit port""" + self._do_get() + + @patch.dict( + os.environ, + {"https_proxy": "proxy.com", "no_proxy": "testserv"}, + ) + def test_get_bypass_proxy(self): + """test of a GET request with an explicit port and bypass proxy""" + self._do_get() + + def _do_get(self): + """test of a GET request with an explicit port""" + self.agent = self._make_agent() + self.reactor.lookups["testserv"] = "1.2.3.4" test_d = self._make_get_request(b"matrix://testserv:8448/foo/bar") @@ -282,10 +315,188 @@ def test_get(self): json = self.successResultOf(treq.json_content(response)) self.assertEqual(json, {"a": 1}) + @patch.dict( + os.environ, {"https_proxy": "http://proxy.com", "no_proxy": "unused.com"} + ) + def test_get_via_http_proxy(self): + """test for federation request through a http proxy""" + self._do_get_via_proxy(expect_proxy_ssl=False, expected_auth_credentials=None) + + @patch.dict( + os.environ, + {"https_proxy": "http://user:pass@proxy.com", "no_proxy": "unused.com"}, + ) + def test_get_via_http_proxy_with_auth(self): + """test for federation request through a http proxy with authentication""" + self._do_get_via_proxy( + expect_proxy_ssl=False, expected_auth_credentials=b"user:pass" + ) + + @patch.dict( + os.environ, {"https_proxy": "https://proxy.com", "no_proxy": "unused.com"} + ) + def test_get_via_https_proxy(self): + """test for federation request through a https proxy""" + self._do_get_via_proxy(expect_proxy_ssl=True, expected_auth_credentials=None) + + @patch.dict( + os.environ, + {"https_proxy": "https://user:pass@proxy.com", "no_proxy": "unused.com"}, + ) + def test_get_via_https_proxy_with_auth(self): + """test for federation request through a https proxy with authentication""" + self._do_get_via_proxy( + expect_proxy_ssl=True, expected_auth_credentials=b"user:pass" + ) + + def _do_get_via_proxy( + self, + expect_proxy_ssl: bool = False, + expected_auth_credentials: Optional[bytes] = None, + ): + """Send a https federation request via an agent and check that it is correctly + received at the proxy and client. The proxy can use either http or https. + Args: + expect_proxy_ssl: True if we expect the request to connect to the proxy via https. + expected_auth_credentials: credentials we expect to be presented to authenticate at the proxy + """ + self.agent = self._make_agent() + + self.reactor.lookups["testserv"] = "1.2.3.4" + self.reactor.lookups["proxy.com"] = "9.9.9.9" + test_d = self._make_get_request(b"matrix://testserv:8448/foo/bar") + + # Nothing happened yet + self.assertNoResult(test_d) + + # Make sure treq is trying to connect + clients = self.reactor.tcpClients + self.assertEqual(len(clients), 1) + (host, port, client_factory, _timeout, _bindAddress) = clients[0] + # make sure we are connecting to the proxy + self.assertEqual(host, "9.9.9.9") + self.assertEqual(port, 1080) + + # make a test server to act as the proxy, and wire up the client + proxy_server = self._make_connection( + client_factory, + ssl=expect_proxy_ssl, + tls_sanlist=[b"DNS:proxy.com"] if expect_proxy_ssl else None, + expected_sni=b"proxy.com" if expect_proxy_ssl else None, + ) + + assert isinstance(proxy_server, HTTPChannel) + + # now there should be a pending CONNECT request + self.assertEqual(len(proxy_server.requests), 1) + + request = proxy_server.requests[0] + self.assertEqual(request.method, b"CONNECT") + self.assertEqual(request.path, b"testserv:8448") + + # Check whether auth credentials have been supplied to the proxy + proxy_auth_header_values = request.requestHeaders.getRawHeaders( + b"Proxy-Authorization" + ) + + if expected_auth_credentials is not None: + # Compute the correct header value for Proxy-Authorization + encoded_credentials = base64.b64encode(expected_auth_credentials) + expected_header_value = b"Basic " + encoded_credentials + + # Validate the header's value + self.assertIn(expected_header_value, proxy_auth_header_values) + else: + # Check that the Proxy-Authorization header has not been supplied to the proxy + self.assertIsNone(proxy_auth_header_values) + + # tell the proxy server not to close the connection + proxy_server.persistent = True + + request.finish() + + # now we make another test server to act as the upstream HTTP server. + server_ssl_protocol = _wrap_server_factory_for_tls( + _get_test_protocol_factory() + ).buildProtocol(None) + + # Tell the HTTP server to send outgoing traffic back via the proxy's transport. + proxy_server_transport = proxy_server.transport + server_ssl_protocol.makeConnection(proxy_server_transport) + + # ... and replace the protocol on the proxy's transport with the + # TLSMemoryBIOProtocol for the test server, so that incoming traffic + # to the proxy gets sent over to the HTTP(s) server. + + # See also comment at `_do_https_request_via_proxy` + # in ../test_proxyagent.py for more details + if expect_proxy_ssl: + assert isinstance(proxy_server_transport, TLSMemoryBIOProtocol) + proxy_server_transport.wrappedProtocol = server_ssl_protocol + else: + assert isinstance(proxy_server_transport, FakeTransport) + client_protocol = proxy_server_transport.other + c2s_transport = client_protocol.transport + c2s_transport.other = server_ssl_protocol + + self.reactor.advance(0) + + server_name = server_ssl_protocol._tlsConnection.get_servername() + expected_sni = b"testserv" + self.assertEqual( + server_name, + expected_sni, + f"Expected SNI {expected_sni!s} but got {server_name!s}", + ) + + # now there should be a pending request + http_server = server_ssl_protocol.wrappedProtocol + self.assertEqual(len(http_server.requests), 1) + + request = http_server.requests[0] + self.assertEqual(request.method, b"GET") + self.assertEqual(request.path, b"/foo/bar") + self.assertEqual( + request.requestHeaders.getRawHeaders(b"host"), [b"testserv:8448"] + ) + self.assertEqual( + request.requestHeaders.getRawHeaders(b"user-agent"), [b"test-agent"] + ) + # Check that the destination server DID NOT receive proxy credentials + self.assertIsNone(request.requestHeaders.getRawHeaders(b"Proxy-Authorization")) + content = request.content.read() + self.assertEqual(content, b"") + + # Deferred is still without a result + self.assertNoResult(test_d) + + # send the headers + request.responseHeaders.setRawHeaders(b"Content-Type", [b"application/json"]) + request.write("") + + self.reactor.pump((0.1,)) + + response = self.successResultOf(test_d) + + # that should give us a Response object + self.assertEqual(response.code, 200) + + # Send the body + request.write('{ "a": 1 }'.encode("ascii")) + request.finish() + + self.reactor.pump((0.1,)) + + # check it can be read + json = self.successResultOf(treq.json_content(response)) + self.assertEqual(json, {"a": 1}) + def test_get_ip_address(self): """ Test the behaviour when the server name contains an explicit IP (with no port) """ + self.agent = self._make_agent() + # there will be a getaddrinfo on the IP self.reactor.lookups["1.2.3.4"] = "1.2.3.4" @@ -320,6 +531,7 @@ def test_get_ipv6_address(self): Test the behaviour when the server name contains an explicit IPv6 address (with no port) """ + self.agent = self._make_agent() # there will be a getaddrinfo on the IP self.reactor.lookups["::1"] = "::1" @@ -355,6 +567,7 @@ def test_get_ipv6_address_with_port(self): Test the behaviour when the server name contains an explicit IPv6 address (with explicit port) """ + self.agent = self._make_agent() # there will be a getaddrinfo on the IP self.reactor.lookups["::1"] = "::1" @@ -389,6 +602,8 @@ def test_get_hostname_bad_cert(self): """ Test the behaviour when the certificate on the server doesn't match the hostname """ + self.agent = self._make_agent() + self.mock_resolver.resolve_service.side_effect = generate_resolve_service([]) self.reactor.lookups["testserv1"] = "1.2.3.4" @@ -441,6 +656,8 @@ def test_get_ip_address_bad_cert(self): Test the behaviour when the server name contains an explicit IP, but the server cert doesn't cover it """ + self.agent = self._make_agent() + # there will be a getaddrinfo on the IP self.reactor.lookups["1.2.3.5"] = "1.2.3.5" @@ -471,6 +688,7 @@ def test_get_no_srv_no_well_known(self): """ Test the behaviour when the server name has no port, no SRV, and no well-known """ + self.agent = self._make_agent() self.mock_resolver.resolve_service.side_effect = generate_resolve_service([]) self.reactor.lookups["testserv"] = "1.2.3.4" @@ -524,6 +742,7 @@ def test_get_no_srv_no_well_known(self): def test_get_well_known(self): """Test the behaviour when the .well-known delegates elsewhere""" + self.agent = self._make_agent() self.mock_resolver.resolve_service.side_effect = generate_resolve_service([]) self.reactor.lookups["testserv"] = "1.2.3.4" @@ -587,6 +806,8 @@ def test_get_well_known_redirect(self): """Test the behaviour when the server name has no port and no SRV record, but the .well-known has a 300 redirect """ + self.agent = self._make_agent() + self.mock_resolver.resolve_service.side_effect = generate_resolve_service([]) self.reactor.lookups["testserv"] = "1.2.3.4" self.reactor.lookups["target-server"] = "1::f" @@ -675,6 +896,7 @@ def test_get_invalid_well_known(self): """ Test the behaviour when the server name has an *invalid* well-known (and no SRV) """ + self.agent = self._make_agent() self.mock_resolver.resolve_service.side_effect = generate_resolve_service([]) self.reactor.lookups["testserv"] = "1.2.3.4" @@ -743,6 +965,7 @@ def test_get_well_known_unsigned_cert(self): reactor=self.reactor, tls_client_options_factory=tls_factory, user_agent=b"test-agent", # This is unused since _well_known_resolver is passed below. + ip_whitelist=IPSet(), ip_blacklist=IPSet(), _srv_resolver=self.mock_resolver, _well_known_resolver=WellKnownResolver( @@ -780,6 +1003,8 @@ def test_get_hostname_srv(self): """ Test the behaviour when there is a single SRV record """ + self.agent = self._make_agent() + self.mock_resolver.resolve_service.side_effect = generate_resolve_service( [Server(host=b"srvtarget", port=8443)] ) @@ -820,6 +1045,8 @@ def test_get_well_known_srv(self): """Test the behaviour when the .well-known redirects to a place where there is a SRV. """ + self.agent = self._make_agent() + self.reactor.lookups["testserv"] = "1.2.3.4" self.reactor.lookups["srvtarget"] = "5.6.7.8" @@ -876,6 +1103,7 @@ def test_get_well_known_srv(self): def test_idna_servername(self): """test the behaviour when the server name has idna chars in""" + self.agent = self._make_agent() self.mock_resolver.resolve_service.side_effect = generate_resolve_service([]) @@ -937,6 +1165,7 @@ def test_idna_servername(self): def test_idna_srv_target(self): """test the behaviour when the target of a SRV record has idna chars""" + self.agent = self._make_agent() self.mock_resolver.resolve_service.side_effect = generate_resolve_service( [Server(host=b"xn--trget-3qa.com", port=8443)] # târget.com @@ -1140,6 +1369,8 @@ def test_well_known_too_large(self): def test_srv_fallbacks(self): """Test that other SRV results are tried if the first one fails.""" + self.agent = self._make_agent() + self.mock_resolver.resolve_service.side_effect = generate_resolve_service( [ Server(host=b"target.com", port=8443), @@ -1266,34 +1497,49 @@ def _check_logcontext(context): raise AssertionError("Expected logcontext %s but was %s" % (context, current)) -def _build_test_server(connection_creator): - """Construct a test server - - This builds an HTTP channel, wrapped with a TLSMemoryBIOProtocol - +def _wrap_server_factory_for_tls( + factory: IProtocolFactory, sanlist: Iterable[bytes] = None +) -> IProtocolFactory: + """Wrap an existing Protocol Factory with a test TLSMemoryBIOFactory + The resultant factory will create a TLS server which presents a certificate + signed by our test CA, valid for the domains in `sanlist` Args: - connection_creator (IOpenSSLServerConnectionCreator): thing to build - SSL connections - sanlist (list[bytes]): list of the SAN entries for the cert returned - by the server + factory: protocol factory to wrap + sanlist: list of domains the cert should be valid for + Returns: + interfaces.IProtocolFactory + """ + if sanlist is None: + sanlist = [ + b"DNS:testserv", + b"DNS:target-server", + b"DNS:xn--bcher-kva.com", + b"IP:1.2.3.4", + b"IP:::1", + ] + + connection_creator = TestServerTLSConnectionFactory(sanlist=sanlist) + return TLSMemoryBIOFactory( + connection_creator, isClient=False, wrappedFactory=factory + ) + +def _get_test_protocol_factory() -> IProtocolFactory: + """Get a protocol Factory which will build an HTTPChannel Returns: - TLSMemoryBIOProtocol + interfaces.IProtocolFactory """ server_factory = Factory.forProtocol(HTTPChannel) + # Request.finish expects the factory to have a 'log' method. server_factory.log = _log_request - server_tls_factory = TLSMemoryBIOFactory( - connection_creator, isClient=False, wrappedFactory=server_factory - ) - - return server_tls_factory.buildProtocol(None) + return server_factory -def _log_request(request): +def _log_request(request: str): """Implements Factory.log, which is expected by Request.finish""" - logger.info("Completed request %s", request) + logger.info(f"Completed request {request}") @implementer(IPolicyForHTTPS) diff --git a/tests/http/test_proxyagent.py b/tests/http/test_proxyagent.py index e5865c161d..2db77c6a73 100644 --- a/tests/http/test_proxyagent.py +++ b/tests/http/test_proxyagent.py @@ -29,7 +29,8 @@ from twisted.web.http import HTTPChannel from synapse.http.client import BlacklistingReactorWrapper -from synapse.http.proxyagent import ProxyAgent, ProxyCredentials, parse_proxy +from synapse.http.connectproxyclient import ProxyCredentials +from synapse.http.proxyagent import ProxyAgent, parse_proxy from tests.http import TestServerTLSConnectionFactory, get_test_https_policy from tests.server import FakeTransport, ThreadedMemoryReactorClock @@ -392,7 +393,9 @@ def test_http_request_via_proxy(self): """ Tests that requests can be made through a proxy. """ - self._do_http_request_via_proxy(ssl=False, auth_credentials=None) + self._do_http_request_via_proxy( + expect_proxy_ssl=False, expected_auth_credentials=None + ) @patch.dict( os.environ, @@ -402,13 +405,17 @@ def test_http_request_via_proxy_with_auth(self): """ Tests that authenticated requests can be made through a proxy. """ - self._do_http_request_via_proxy(ssl=False, auth_credentials=b"bob:pinkponies") + self._do_http_request_via_proxy( + expect_proxy_ssl=False, expected_auth_credentials=b"bob:pinkponies" + ) @patch.dict( os.environ, {"http_proxy": "https://proxy.com:8888", "no_proxy": "unused.com"} ) def test_http_request_via_https_proxy(self): - self._do_http_request_via_proxy(ssl=True, auth_credentials=None) + self._do_http_request_via_proxy( + expect_proxy_ssl=True, expected_auth_credentials=None + ) @patch.dict( os.environ, @@ -418,12 +425,16 @@ def test_http_request_via_https_proxy(self): }, ) def test_http_request_via_https_proxy_with_auth(self): - self._do_http_request_via_proxy(ssl=True, auth_credentials=b"bob:pinkponies") + self._do_http_request_via_proxy( + expect_proxy_ssl=True, expected_auth_credentials=b"bob:pinkponies" + ) @patch.dict(os.environ, {"https_proxy": "proxy.com", "no_proxy": "unused.com"}) def test_https_request_via_proxy(self): """Tests that TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(ssl=False, auth_credentials=None) + self._do_https_request_via_proxy( + expect_proxy_ssl=False, expected_auth_credentials=None + ) @patch.dict( os.environ, @@ -431,14 +442,18 @@ def test_https_request_via_proxy(self): ) def test_https_request_via_proxy_with_auth(self): """Tests that authenticated, TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(ssl=False, auth_credentials=b"bob:pinkponies") + self._do_https_request_via_proxy( + expect_proxy_ssl=False, expected_auth_credentials=b"bob:pinkponies" + ) @patch.dict( os.environ, {"https_proxy": "https://proxy.com", "no_proxy": "unused.com"} ) def test_https_request_via_https_proxy(self): """Tests that TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(ssl=True, auth_credentials=None) + self._do_https_request_via_proxy( + expect_proxy_ssl=True, expected_auth_credentials=None + ) @patch.dict( os.environ, @@ -446,20 +461,22 @@ def test_https_request_via_https_proxy(self): ) def test_https_request_via_https_proxy_with_auth(self): """Tests that authenticated, TLS-encrypted requests can be made through a proxy""" - self._do_https_request_via_proxy(ssl=True, auth_credentials=b"bob:pinkponies") + self._do_https_request_via_proxy( + expect_proxy_ssl=True, expected_auth_credentials=b"bob:pinkponies" + ) def _do_http_request_via_proxy( self, - ssl: bool = False, - auth_credentials: Optional[bytes] = None, + expect_proxy_ssl: bool = False, + expected_auth_credentials: Optional[bytes] = None, ): """Send a http request via an agent and check that it is correctly received at the proxy. The proxy can use either http or https. Args: - ssl: True if we expect the request to connect via https to proxy - auth_credentials: credentials to authenticate at proxy + expect_proxy_ssl: True if we expect the request to connect via https to proxy + expected_auth_credentials: credentials to authenticate at proxy """ - if ssl: + if expect_proxy_ssl: agent = ProxyAgent( self.reactor, use_proxy=True, contextFactory=get_test_https_policy() ) @@ -480,9 +497,9 @@ def _do_http_request_via_proxy( http_server = self._make_connection( client_factory, _get_test_protocol_factory(), - ssl=ssl, - tls_sanlist=[b"DNS:proxy.com"] if ssl else None, - expected_sni=b"proxy.com" if ssl else None, + ssl=expect_proxy_ssl, + tls_sanlist=[b"DNS:proxy.com"] if expect_proxy_ssl else None, + expected_sni=b"proxy.com" if expect_proxy_ssl else None, ) # the FakeTransport is async, so we need to pump the reactor @@ -498,9 +515,9 @@ def _do_http_request_via_proxy( b"Proxy-Authorization" ) - if auth_credentials is not None: + if expected_auth_credentials is not None: # Compute the correct header value for Proxy-Authorization - encoded_credentials = base64.b64encode(auth_credentials) + encoded_credentials = base64.b64encode(expected_auth_credentials) expected_header_value = b"Basic " + encoded_credentials # Validate the header's value @@ -523,14 +540,14 @@ def _do_http_request_via_proxy( def _do_https_request_via_proxy( self, - ssl: bool = False, - auth_credentials: Optional[bytes] = None, + expect_proxy_ssl: bool = False, + expected_auth_credentials: Optional[bytes] = None, ): """Send a https request via an agent and check that it is correctly received at the proxy and client. The proxy can use either http or https. Args: - ssl: True if we expect the request to connect via https to proxy - auth_credentials: credentials to authenticate at proxy + expect_proxy_ssl: True if we expect the request to connect via https to proxy + expected_auth_credentials: credentials to authenticate at proxy """ agent = ProxyAgent( self.reactor, @@ -552,9 +569,9 @@ def _do_https_request_via_proxy( proxy_server = self._make_connection( client_factory, _get_test_protocol_factory(), - ssl=ssl, - tls_sanlist=[b"DNS:proxy.com"] if ssl else None, - expected_sni=b"proxy.com" if ssl else None, + ssl=expect_proxy_ssl, + tls_sanlist=[b"DNS:proxy.com"] if expect_proxy_ssl else None, + expected_sni=b"proxy.com" if expect_proxy_ssl else None, ) assert isinstance(proxy_server, HTTPChannel) @@ -570,9 +587,9 @@ def _do_https_request_via_proxy( b"Proxy-Authorization" ) - if auth_credentials is not None: + if expected_auth_credentials is not None: # Compute the correct header value for Proxy-Authorization - encoded_credentials = base64.b64encode(auth_credentials) + encoded_credentials = base64.b64encode(expected_auth_credentials) expected_header_value = b"Basic " + encoded_credentials # Validate the header's value @@ -606,7 +623,7 @@ def _do_https_request_via_proxy( # Protocol to implement the proxy, which starts out by forwarding to an # HTTPChannel (to implement the CONNECT command) and can then be switched # into a mode where it forwards its traffic to another Protocol.) - if ssl: + if expect_proxy_ssl: assert isinstance(proxy_server_transport, TLSMemoryBIOProtocol) proxy_server_transport.wrappedProtocol = server_ssl_protocol else: From fab352ac2cb6a9d69a74be6d4255a9b71e0f7945 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 11 Aug 2021 10:43:40 -0400 Subject: [PATCH 547/619] Fix type hints in space summary tests. (#10575) And ensure that the file is checked via mypy. --- changelog.d/10575.feature | 1 + mypy.ini | 1 + tests/handlers/test_space_summary.py | 11 +++++------ tests/rest/client/v1/utils.py | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10575.feature diff --git a/changelog.d/10575.feature b/changelog.d/10575.feature new file mode 100644 index 0000000000..ffc4e4289c --- /dev/null +++ b/changelog.d/10575.feature @@ -0,0 +1 @@ +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/mypy.ini b/mypy.ini index 8717ae738e..5d6cd557bc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -86,6 +86,7 @@ files = tests/test_event_auth.py, tests/test_utils, tests/handlers/test_password_providers.py, + tests/handlers/test_space_summary.py, tests/rest/client/v1/test_login.py, tests/rest/client/v2_alpha/test_auth.py, tests/util/test_itertools.py, diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 255dd17f86..04da9bcc25 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Any, Iterable, Optional, Tuple +from typing import Any, Iterable, List, Optional, Tuple from unittest import mock from synapse.api.constants import ( @@ -127,7 +127,7 @@ def _add_child( self, space_id: str, room_id: str, token: str, order: Optional[str] = None ) -> None: """Add a child room to a space.""" - content = {"via": [self.hs.hostname]} + content: JsonDict = {"via": [self.hs.hostname]} if order is not None: content["order"] = order self.helper.send_state( @@ -439,9 +439,8 @@ def test_pagination(self): ) # The result should have the space and all of the links, plus some of the # rooms and a pagination token. - expected = [(self.space, room_ids)] + [ - (room_id, ()) for room_id in room_ids[:6] - ] + expected: List[Tuple[str, Iterable[str]]] = [(self.space, room_ids)] + expected += [(room_id, ()) for room_id in room_ids[:6]] self._assert_hierarchy(result, expected) self.assertIn("next_token", result) @@ -525,7 +524,7 @@ def test_max_depth(self): result = self.get_success( self.handler.get_room_hierarchy(self.user, self.space, max_depth=0) ) - expected = [(spaces[0], [rooms[0], spaces[1]])] + expected: List[Tuple[str, Iterable[str]]] = [(spaces[0], [rooms[0], spaces[1]])] self._assert_hierarchy(result, expected) # A single additional layer. diff --git a/tests/rest/client/v1/utils.py b/tests/rest/client/v1/utils.py index fc2d35596e..954ad1a1fd 100644 --- a/tests/rest/client/v1/utils.py +++ b/tests/rest/client/v1/utils.py @@ -47,10 +47,10 @@ class RestHelper: def create_room_as( self, - room_creator: str = None, + room_creator: Optional[str] = None, is_public: bool = True, - room_version: str = None, - tok: str = None, + room_version: Optional[str] = None, + tok: Optional[str] = None, expect_code: int = 200, extra_content: Optional[Dict] = None, custom_headers: Optional[ From 2ae2a04616a627eabbf3ca69700462a52f344e69 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 11 Aug 2021 14:31:39 -0400 Subject: [PATCH 548/619] Clarify error message when joining a restricted room. (#10572) --- changelog.d/10572.misc | 1 + synapse/handlers/event_auth.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10572.misc diff --git a/changelog.d/10572.misc b/changelog.d/10572.misc new file mode 100644 index 0000000000..008d7be444 --- /dev/null +++ b/changelog.d/10572.misc @@ -0,0 +1 @@ +Clarify error message when failing to join a restricted room. diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index e2410e482f..4288ffff09 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -213,7 +213,7 @@ async def check_restricted_join_rules( raise AuthError( 403, - "You do not belong to any of the required rooms to join this room.", + "You do not belong to any of the required rooms/spaces to join this room.", ) async def has_restricted_join_rules( From 5acd8b5a960b1c53ce0b9efa304010ec5f0f6682 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 11 Aug 2021 14:52:09 -0400 Subject: [PATCH 549/619] Expire old spaces summary pagination sessions. (#10574) --- changelog.d/10574.feature | 1 + synapse/handlers/space_summary.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10574.feature diff --git a/changelog.d/10574.feature b/changelog.d/10574.feature new file mode 100644 index 0000000000..ffc4e4289c --- /dev/null +++ b/changelog.d/10574.feature @@ -0,0 +1 @@ +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index fd76c34695..8c9852bc89 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -77,6 +77,8 @@ class _PaginationKey: class _PaginationSession: """The information that is stored for pagination.""" + # The time the pagination session was created, in milliseconds. + creation_time_ms: int # The queue of rooms which are still to process. room_queue: Deque["_RoomQueueEntry"] # A set of rooms which have been processed. @@ -84,6 +86,9 @@ class _PaginationSession: class SpaceSummaryHandler: + # The time a pagination session remains valid for. + _PAGINATION_SESSION_VALIDITY_PERIOD_MS = 5 * 60 * 1000 + def __init__(self, hs: "HomeServer"): self._clock = hs.get_clock() self._auth = hs.get_auth() @@ -108,6 +113,21 @@ def __init__(self, hs: "HomeServer"): "get_room_hierarchy", ) + def _expire_pagination_sessions(self): + """Expire pagination session which are old.""" + expire_before = ( + self._clock.time_msec() - self._PAGINATION_SESSION_VALIDITY_PERIOD_MS + ) + to_expire = [] + + for key, value in self._pagination_sessions.items(): + if value.creation_time_ms < expire_before: + to_expire.append(key) + + for key in to_expire: + logger.debug("Expiring pagination session id %s", key) + del self._pagination_sessions[key] + async def get_space_summary( self, requester: str, @@ -312,6 +332,8 @@ async def _get_room_hierarchy( # If this is continuing a previous session, pull the persisted data. if from_token: + self._expire_pagination_sessions() + pagination_key = _PaginationKey( requested_room_id, suggested_only, max_depth, from_token ) @@ -391,7 +413,7 @@ async def _get_room_hierarchy( requested_room_id, suggested_only, max_depth, next_token ) self._pagination_sessions[pagination_key] = _PaginationSession( - room_queue, processed_rooms + self._clock.time_msec(), room_queue, processed_rooms ) return result From 33ef86aa2515f623fa6e8657d16c918b6a6d9da5 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 11 Aug 2021 19:59:57 +0100 Subject: [PATCH 550/619] Rename ci to .ci --- {ci => .ci}/postgres-config.yaml | 2 +- {ci => .ci}/scripts/postgres_exec.py | 0 {ci => .ci}/scripts/test_old_deps.sh | 0 {ci => .ci}/scripts/test_synapse_port_db.sh | 0 {ci => .ci}/sqlite-config.yaml | 4 ++-- {ci => .ci}/test_db.db | Bin {ci => .ci}/worker-blacklist | 0 .github/workflows/tests.yml | 6 +++--- 8 files changed, 6 insertions(+), 6 deletions(-) rename {ci => .ci}/postgres-config.yaml (91%) rename {ci => .ci}/scripts/postgres_exec.py (100%) rename {ci => .ci}/scripts/test_old_deps.sh (100%) rename {ci => .ci}/scripts/test_synapse_port_db.sh (100%) rename {ci => .ci}/sqlite-config.yaml (82%) rename {ci => .ci}/test_db.db (100%) rename {ci => .ci}/worker-blacklist (100%) diff --git a/ci/postgres-config.yaml b/.ci/postgres-config.yaml similarity index 91% rename from ci/postgres-config.yaml rename to .ci/postgres-config.yaml index 511fef495d..f5a4aecd51 100644 --- a/ci/postgres-config.yaml +++ b/.ci/postgres-config.yaml @@ -3,7 +3,7 @@ # CI's Docker setup at the point where this file is considered. server_name: "localhost:8800" -signing_key_path: "ci/test.signing.key" +signing_key_path: ".ci/test.signing.key" report_stats: false diff --git a/ci/scripts/postgres_exec.py b/.ci/scripts/postgres_exec.py similarity index 100% rename from ci/scripts/postgres_exec.py rename to .ci/scripts/postgres_exec.py diff --git a/ci/scripts/test_old_deps.sh b/.ci/scripts/test_old_deps.sh similarity index 100% rename from ci/scripts/test_old_deps.sh rename to .ci/scripts/test_old_deps.sh diff --git a/ci/scripts/test_synapse_port_db.sh b/.ci/scripts/test_synapse_port_db.sh similarity index 100% rename from ci/scripts/test_synapse_port_db.sh rename to .ci/scripts/test_synapse_port_db.sh diff --git a/ci/sqlite-config.yaml b/.ci/sqlite-config.yaml similarity index 82% rename from ci/sqlite-config.yaml rename to .ci/sqlite-config.yaml index fd5a1c1451..3373743da3 100644 --- a/ci/sqlite-config.yaml +++ b/.ci/sqlite-config.yaml @@ -3,14 +3,14 @@ # schema and run background updates on it. server_name: "localhost:8800" -signing_key_path: "ci/test.signing.key" +signing_key_path: ".ci/test.signing.key" report_stats: false database: name: "sqlite3" args: - database: "ci/test_db.db" + database: ".ci/test_db.db" # Suppress the key server warning. trusted_key_servers: [] diff --git a/ci/test_db.db b/.ci/test_db.db similarity index 100% rename from ci/test_db.db rename to .ci/test_db.db diff --git a/ci/worker-blacklist b/.ci/worker-blacklist similarity index 100% rename from ci/worker-blacklist rename to .ci/worker-blacklist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 572bc81b0f..df2e3901cb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -142,7 +142,7 @@ jobs: uses: docker://ubuntu:bionic # For old python and sqlite with: workdir: /github/workspace - entrypoint: ci/scripts/test_old_deps.sh + entrypoint: .ci/scripts/test_old_deps.sh env: TRIAL_FLAGS: "--jobs=2" - name: Dump logs @@ -229,7 +229,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Prepare test blacklist - run: cat sytest-blacklist ci/worker-blacklist > synapse-blacklist-with-workers + run: cat sytest-blacklist .ci/worker-blacklist > synapse-blacklist-with-workers - name: Run SyTest run: /bootstrap.sh synapse working-directory: /src @@ -278,7 +278,7 @@ jobs: - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - run: ci/scripts/test_synapse_port_db.sh + - run: .ci/scripts/test_synapse_port_db.sh complement: if: ${{ !failure() && !cancelled() }} From 3ebb6694f018eedb7d3c4fda829540f07b45a5b1 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 11 Aug 2021 15:04:51 -0400 Subject: [PATCH 551/619] Allow requesting the summary of a space which is joinable. (#10580) As opposed to only allowing the summary of spaces which the user is already in or has world-readable visibility. This makes the logic consistent with whether a space/room is returned as part of a space and whether a space summary can start at a space. --- changelog.d/10580.bugfix | 1 + synapse/handlers/space_summary.py | 31 ++++++++++++++++------------ tests/handlers/test_space_summary.py | 28 +++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 changelog.d/10580.bugfix diff --git a/changelog.d/10580.bugfix b/changelog.d/10580.bugfix new file mode 100644 index 0000000000..f8da7382b7 --- /dev/null +++ b/changelog.d/10580.bugfix @@ -0,0 +1 @@ +Allow public rooms to be previewed in the spaces summary APIs from [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 8c9852bc89..893546e661 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -38,7 +38,7 @@ Membership, RoomTypes, ) -from synapse.api.errors import Codes, SynapseError +from synapse.api.errors import AuthError, Codes, SynapseError from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 from synapse.types import JsonDict @@ -91,7 +91,6 @@ class SpaceSummaryHandler: def __init__(self, hs: "HomeServer"): self._clock = hs.get_clock() - self._auth = hs.get_auth() self._event_auth_handler = hs.get_event_auth_handler() self._store = hs.get_datastore() self._event_serializer = hs.get_event_client_serializer() @@ -153,9 +152,13 @@ async def get_space_summary( Returns: summary dict to return """ - # first of all, check that the user is in the room in question (or it's - # world-readable) - await self._auth.check_user_in_room_or_world_readable(room_id, requester) + # First of all, check that the room is accessible. + if not await self._is_local_room_accessible(room_id, requester): + raise AuthError( + 403, + "User %s not in room %s, and room previews are disabled" + % (requester, room_id), + ) # the queue of rooms to process room_queue = deque((_RoomQueueEntry(room_id, ()),)) @@ -324,11 +327,13 @@ async def _get_room_hierarchy( ) -> JsonDict: """See docstring for SpaceSummaryHandler.get_room_hierarchy.""" - # first of all, check that the user is in the room in question (or it's - # world-readable) - await self._auth.check_user_in_room_or_world_readable( - requested_room_id, requester - ) + # First of all, check that the room is accessible. + if not await self._is_local_room_accessible(requested_room_id, requester): + raise AuthError( + 403, + "User %s not in room %s, and room previews are disabled" + % (requester, requested_room_id), + ) # If this is continuing a previous session, pull the persisted data. if from_token: @@ -612,7 +617,7 @@ async def _summarize_remote_room( return results async def _is_local_room_accessible( - self, room_id: str, requester: Optional[str], origin: Optional[str] + self, room_id: str, requester: Optional[str], origin: Optional[str] = None ) -> bool: """ Calculate whether the room should be shown in the spaces summary. @@ -766,7 +771,7 @@ async def _is_remote_room_accessible( # Finally, check locally if we can access the room. The user might # already be in the room (if it was a child room), or there might be a # pending invite, etc. - return await self._is_local_room_accessible(room_id, requester, None) + return await self._is_local_room_accessible(room_id, requester) async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDict: """ @@ -783,7 +788,7 @@ async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDic stats = await self._store.get_room_with_stats(room_id) # currently this should be impossible because we call - # check_user_in_room_or_world_readable on the room before we get here, so + # _is_local_room_accessible on the room before we get here, so # there should always be an entry assert stats is not None, "unable to retrieve stats for %s" % (room_id,) diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 04da9bcc25..806b886fe4 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -248,7 +248,21 @@ def test_visibility(self): user2 = self.register_user("user2", "pass") token2 = self.login("user2", "pass") - # The user cannot see the space. + # The user can see the space since it is publicly joinable. + result = self.get_success(self.handler.get_space_summary(user2, self.space)) + expected = [(self.space, [self.room]), (self.room, ())] + self._assert_rooms(result, expected) + + result = self.get_success(self.handler.get_room_hierarchy(user2, self.space)) + self._assert_hierarchy(result, expected) + + # If the space is made invite-only, it should no longer be viewable. + self.helper.send_state( + self.space, + event_type=EventTypes.JoinRules, + body={"join_rule": JoinRules.INVITE}, + tok=self.token, + ) self.get_failure(self.handler.get_space_summary(user2, self.space), AuthError) self.get_failure(self.handler.get_room_hierarchy(user2, self.space), AuthError) @@ -260,7 +274,6 @@ def test_visibility(self): tok=self.token, ) result = self.get_success(self.handler.get_space_summary(user2, self.space)) - expected = [(self.space, [self.room]), (self.room, ())] self._assert_rooms(result, expected) result = self.get_success(self.handler.get_room_hierarchy(user2, self.space)) @@ -277,6 +290,7 @@ def test_visibility(self): self.get_failure(self.handler.get_room_hierarchy(user2, self.space), AuthError) # Join the space and results should be returned. + self.helper.invite(self.space, targ=user2, tok=self.token) self.helper.join(self.space, user2, tok=token2) result = self.get_success(self.handler.get_space_summary(user2, self.space)) self._assert_rooms(result, expected) @@ -284,6 +298,16 @@ def test_visibility(self): result = self.get_success(self.handler.get_room_hierarchy(user2, self.space)) self._assert_hierarchy(result, expected) + # Attempting to view an unknown room returns the same error. + self.get_failure( + self.handler.get_space_summary(user2, "#not-a-space:" + self.hs.hostname), + AuthError, + ) + self.get_failure( + self.handler.get_room_hierarchy(user2, "#not-a-space:" + self.hs.hostname), + AuthError, + ) + def _create_room_with_join_rule( self, join_rule: str, room_version: Optional[str] = None, **extra_content ) -> str: From 6fcc3e0bc81b4ed738eee702b0e1d193c052d205 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 11 Aug 2021 20:08:14 +0100 Subject: [PATCH 552/619] Teach MANIFEST and tox about ci->.ci --- MANIFEST.in | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 37d61f40de..44d5cc7618 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -46,7 +46,7 @@ recursive-include changelog.d * prune .circleci prune .github -prune ci +prune .ci prune contrib prune debian prune demo/etc diff --git a/tox.ini b/tox.ini index b695126019..5a62ec76c2 100644 --- a/tox.ini +++ b/tox.ini @@ -49,7 +49,7 @@ lint_targets = contrib synctl synmark - ci + .ci docker # default settings for all tox environments From cb5976ebd71e25da2b2370185d640e80a2245d04 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 11 Aug 2021 20:08:48 +0100 Subject: [PATCH 553/619] set TOP in sytest containers --- .coveragerc | 4 ++-- .github/workflows/tests.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index bbf9046b06..11f2ec8387 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,8 @@ [run] branch = True parallel = True -include=$GITHUB_WORKSPACE/synapse/* -data_file = $GITHUB_WORKSPACE/.coverage +include=$TOP/synapse/* +data_file = $TOP/.coverage [report] precision = 2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index df2e3901cb..de022020cc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -200,6 +200,7 @@ jobs: WORKERS: ${{ matrix.workers && 1 }} REDIS: ${{ matrix.redis && 1 }} BLACKLIST: ${{ matrix.workers && 'synapse-blacklist-with-workers' }} + TOP: ${{ github.workspace }} strategy: fail-fast: false From 92a8e68ba2f617119e0506cee76eed2ff45b323a Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 11 Aug 2021 20:19:56 +0100 Subject: [PATCH 554/619] Missed another ci->.ci Should have been more systematic with my grepping. --- .ci/scripts/test_synapse_port_db.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.ci/scripts/test_synapse_port_db.sh b/.ci/scripts/test_synapse_port_db.sh index 9ee0ad42fc..2b4e5ec170 100755 --- a/.ci/scripts/test_synapse_port_db.sh +++ b/.ci/scripts/test_synapse_port_db.sh @@ -20,22 +20,22 @@ pip install -e . echo "--- Generate the signing key" # Generate the server's signing key. -python -m synapse.app.homeserver --generate-keys -c ci/sqlite-config.yaml +python -m synapse.app.homeserver --generate-keys -c .ci/sqlite-config.yaml echo "--- Prepare test database" # Make sure the SQLite3 database is using the latest schema and has no pending background update. -scripts-dev/update_database --database-config ci/sqlite-config.yaml +scripts-dev/update_database --database-config .ci/sqlite-config.yaml # Create the PostgreSQL database. -./ci/scripts/postgres_exec.py "CREATE DATABASE synapse" +.ci/scripts/postgres_exec.py "CREATE DATABASE synapse" echo "+++ Run synapse_port_db against test database" -coverage run scripts/synapse_port_db --sqlite-database ci/test_db.db --postgres-config ci/postgres-config.yaml +coverage run scripts/synapse_port_db --sqlite-database .ci/test_db.db --postgres-config .ci/postgres-config.yaml # We should be able to run twice against the same database. echo "+++ Run synapse_port_db a second time" -coverage run scripts/synapse_port_db --sqlite-database ci/test_db.db --postgres-config ci/postgres-config.yaml +coverage run scripts/synapse_port_db --sqlite-database .ci/test_db.db --postgres-config .ci/postgres-config.yaml ##### @@ -44,14 +44,14 @@ coverage run scripts/synapse_port_db --sqlite-database ci/test_db.db --postgres- echo "--- Prepare empty SQLite database" # we do this by deleting the sqlite db, and then doing the same again. -rm ci/test_db.db +rm .ci/test_db.db -scripts-dev/update_database --database-config ci/sqlite-config.yaml +scripts-dev/update_database --database-config .ci/sqlite-config.yaml # re-create the PostgreSQL database. -./ci/scripts/postgres_exec.py \ +.ci/scripts/postgres_exec.py \ "DROP DATABASE synapse" \ "CREATE DATABASE synapse" echo "+++ Run synapse_port_db against empty database" -coverage run scripts/synapse_port_db --sqlite-database ci/test_db.db --postgres-config ci/postgres-config.yaml +coverage run scripts/synapse_port_db --sqlite-database .ci/test_db.db --postgres-config .ci/postgres-config.yaml From 915b37e5efd4e0fb9e57ce9895300017b4b3dd43 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Wed, 11 Aug 2021 21:29:59 +0200 Subject: [PATCH 555/619] Admin API to delete media for a specific user (#10558) --- changelog.d/10558.feature | 1 + docs/admin_api/media_admin_api.md | 9 +- docs/admin_api/user_admin_api.md | 54 +++- synapse/rest/admin/media.py | 4 +- synapse/rest/admin/users.py | 80 +++++- synapse/rest/media/v1/media_repository.py | 6 +- tests/rest/admin/test_user.py | 321 ++++++++++++++-------- 7 files changed, 347 insertions(+), 128 deletions(-) create mode 100644 changelog.d/10558.feature diff --git a/changelog.d/10558.feature b/changelog.d/10558.feature new file mode 100644 index 0000000000..1f461bc70a --- /dev/null +++ b/changelog.d/10558.feature @@ -0,0 +1 @@ +Admin API to delete several media for a specific user. Contributed by @dklimpel. diff --git a/docs/admin_api/media_admin_api.md b/docs/admin_api/media_admin_api.md index 61bed1e0d5..ea05bd6e44 100644 --- a/docs/admin_api/media_admin_api.md +++ b/docs/admin_api/media_admin_api.md @@ -12,6 +12,7 @@ - [Delete local media](#delete-local-media) * [Delete a specific local media](#delete-a-specific-local-media) * [Delete local media by date or size](#delete-local-media-by-date-or-size) + * [Delete media uploaded by a user](#delete-media-uploaded-by-a-user) - [Purge Remote Media API](#purge-remote-media-api) # Querying media @@ -47,7 +48,8 @@ The API returns a JSON body like the following: ## List all media uploaded by a user Listing all media that has been uploaded by a local user can be achieved through -the use of the [List media of a user](user_admin_api.md#list-media-of-a-user) +the use of the +[List media uploaded by a user](user_admin_api.md#list-media-uploaded-by-a-user) Admin API. # Quarantine media @@ -281,6 +283,11 @@ The following fields are returned in the JSON response body: * `deleted_media`: an array of strings - List of deleted `media_id` * `total`: integer - Total number of deleted `media_id` +## Delete media uploaded by a user + +You can find details of how to delete multiple media uploaded by a user in +[User Admin API](user_admin_api.md#delete-media-uploaded-by-a-user). + # Purge Remote Media API The purge remote media API allows server admins to purge old cached remote media. diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 160899754e..33811f5bbb 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -443,8 +443,9 @@ The following fields are returned in the JSON response body: - `joined_rooms` - An array of `room_id`. - `total` - Number of rooms. +## User media -## List media of a user +### List media uploaded by a user Gets a list of all local media that a specific `user_id` has created. By default, the response is ordered by descending creation date and ascending media ID. The newest media is on top. You can change the order with parameters @@ -543,7 +544,6 @@ The following fields are returned in the JSON response body: - `media` - An array of objects, each containing information about a media. Media objects contain the following fields: - - `created_ts` - integer - Timestamp when the content was uploaded in ms. - `last_access_ts` - integer - Timestamp when the content was last accessed in ms. - `media_id` - string - The id used to refer to the media. @@ -551,13 +551,58 @@ The following fields are returned in the JSON response body: - `media_type` - string - The MIME-type of the media. - `quarantined_by` - string - The user ID that initiated the quarantine request for this media. - - `safe_from_quarantine` - bool - Status if this media is safe from quarantining. - `upload_name` - string - The name the media was uploaded with. - - `next_token`: integer - Indication for pagination. See above. - `total` - integer - Total number of media. +### Delete media uploaded by a user + +This API deletes the *local* media from the disk of your own server +that a specific `user_id` has created. This includes any local thumbnails. + +This API will not affect media that has been uploaded to external +media repositories (e.g https://github.com/turt2live/matrix-media-repo/). + +By default, the API deletes media ordered by descending creation date and ascending media ID. +The newest media is deleted first. You can change the order with parameters +`order_by` and `dir`. If no `limit` is set the API deletes `100` files per request. + +The API is: + +``` +DELETE /_synapse/admin/v1/users//media +``` + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](../usage/administration/admin_api) + +A response body like the following is returned: + +```json +{ + "deleted_media": [ + "abcdefghijklmnopqrstuvwx" + ], + "total": 1 +} +``` + +The following fields are returned in the JSON response body: + +* `deleted_media`: an array of strings - List of deleted `media_id` +* `total`: integer - Total number of deleted `media_id` + +**Note**: There is no `next_token`. This is not useful for deleting media, because +after deleting media the remaining media have a new order. + +**Parameters** + +This API has the same parameters as +[List media uploaded by a user](#list-media-uploaded-by-a-user). +With the parameters you can for example limit the number of files to delete at once or +delete largest/smallest or newest/oldest files first. + ## Login as a user Get an access token that can be used to authenticate as that user. Useful for @@ -1012,4 +1057,3 @@ The following parameters should be set in the URL: - `user_id` - The fully qualified MXID: for example, `@user:server.com`. The user must be local. - diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index 0a19a333d7..5f0555039d 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -259,7 +259,9 @@ async def on_DELETE( logging.info("Deleting local media by ID: %s", media_id) - deleted_media, total = await self.media_repository.delete_local_media(media_id) + deleted_media, total = await self.media_repository.delete_local_media_ids( + [media_id] + ) return 200, {"deleted_media": deleted_media, "total": total} diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index eef76ab18a..41f21ba118 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -172,7 +172,7 @@ async def on_GET( target_user = UserID.from_string(user_id) if not self.hs.is_mine(target_user): - raise SynapseError(400, "Can only lookup local users") + raise SynapseError(400, "Can only look up local users") ret = await self.admin_handler.get_user(target_user) @@ -796,7 +796,7 @@ async def on_GET( await assert_requester_is_admin(self.auth, request) if not self.is_mine(UserID.from_string(user_id)): - raise SynapseError(400, "Can only lookup local users") + raise SynapseError(400, "Can only look up local users") if not await self.store.get_user_by_id(user_id): raise NotFoundError("User not found") @@ -811,10 +811,10 @@ async def on_GET( class UserMediaRestServlet(RestServlet): """ Gets information about all uploaded local media for a specific `user_id`. + With DELETE request you can delete all this media. Example: - http://localhost:8008/_synapse/admin/v1/users/ - @user:server/media + http://localhost:8008/_synapse/admin/v1/users/@user:server/media Args: The parameters `from` and `limit` are required for pagination. @@ -830,6 +830,7 @@ def __init__(self, hs: "HomeServer"): self.is_mine = hs.is_mine self.auth = hs.get_auth() self.store = hs.get_datastore() + self.media_repository = hs.get_media_repository() async def on_GET( self, request: SynapseRequest, user_id: str @@ -840,7 +841,7 @@ async def on_GET( await assert_requester_is_admin(self.auth, request) if not self.is_mine(UserID.from_string(user_id)): - raise SynapseError(400, "Can only lookup local users") + raise SynapseError(400, "Can only look up local users") user = await self.store.get_user_by_id(user_id) if user is None: @@ -898,6 +899,73 @@ async def on_GET( return 200, ret + async def on_DELETE( + self, request: SynapseRequest, user_id: str + ) -> Tuple[int, JsonDict]: + # This will always be set by the time Twisted calls us. + assert request.args is not None + + await assert_requester_is_admin(self.auth, request) + + if not self.is_mine(UserID.from_string(user_id)): + raise SynapseError(400, "Can only look up local users") + + user = await self.store.get_user_by_id(user_id) + if user is None: + raise NotFoundError("Unknown user") + + start = parse_integer(request, "from", default=0) + limit = parse_integer(request, "limit", default=100) + + if start < 0: + raise SynapseError( + 400, + "Query parameter from must be a string representing a positive integer.", + errcode=Codes.INVALID_PARAM, + ) + + if limit < 0: + raise SynapseError( + 400, + "Query parameter limit must be a string representing a positive integer.", + errcode=Codes.INVALID_PARAM, + ) + + # If neither `order_by` nor `dir` is set, set the default order + # to newest media is on top for backward compatibility. + if b"order_by" not in request.args and b"dir" not in request.args: + order_by = MediaSortOrder.CREATED_TS.value + direction = "b" + else: + order_by = parse_string( + request, + "order_by", + default=MediaSortOrder.CREATED_TS.value, + allowed_values=( + MediaSortOrder.MEDIA_ID.value, + MediaSortOrder.UPLOAD_NAME.value, + MediaSortOrder.CREATED_TS.value, + MediaSortOrder.LAST_ACCESS_TS.value, + MediaSortOrder.MEDIA_LENGTH.value, + MediaSortOrder.MEDIA_TYPE.value, + MediaSortOrder.QUARANTINED_BY.value, + MediaSortOrder.SAFE_FROM_QUARANTINE.value, + ), + ) + direction = parse_string( + request, "dir", default="f", allowed_values=("f", "b") + ) + + media, _ = await self.store.get_local_media_by_user_paginate( + start, limit, user_id, order_by, direction + ) + + deleted_media, total = await self.media_repository.delete_local_media_ids( + ([row["media_id"] for row in media]) + ) + + return 200, {"deleted_media": deleted_media, "total": total} + class UserTokenRestServlet(RestServlet): """An admin API for logging in as a user. @@ -1017,7 +1085,7 @@ async def on_GET( await assert_requester_is_admin(self.auth, request) if not self.hs.is_mine_id(user_id): - raise SynapseError(400, "Can only lookup local users") + raise SynapseError(400, "Can only look up local users") if not await self.store.get_user_by_id(user_id): raise NotFoundError("User not found") diff --git a/synapse/rest/media/v1/media_repository.py b/synapse/rest/media/v1/media_repository.py index 4f702f890c..0f5ce41ff8 100644 --- a/synapse/rest/media/v1/media_repository.py +++ b/synapse/rest/media/v1/media_repository.py @@ -836,7 +836,9 @@ async def delete_old_remote_media(self, before_ts: int) -> Dict[str, int]: return {"deleted": deleted} - async def delete_local_media(self, media_id: str) -> Tuple[List[str], int]: + async def delete_local_media_ids( + self, media_ids: List[str] + ) -> Tuple[List[str], int]: """ Delete the given local or remote media ID from this server @@ -845,7 +847,7 @@ async def delete_local_media(self, media_id: str) -> Tuple[List[str], int]: Returns: A tuple of (list of deleted media IDs, total deleted media IDs). """ - return await self._remove_local_media_from_disk([media_id]) + return await self._remove_local_media_from_disk(media_ids) async def delete_old_local_media( self, diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 42f50c0921..13fab5579b 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -15,17 +15,21 @@ import hashlib import hmac import json +import os import urllib.parse from binascii import unhexlify from typing import List, Optional from unittest.mock import Mock, patch +from parameterized import parameterized + import synapse.rest.admin from synapse.api.constants import UserTypes from synapse.api.errors import Codes, HttpResponseException, ResourceLimitError from synapse.api.room_versions import RoomVersions from synapse.rest.client.v1 import login, logout, profile, room from synapse.rest.client.v2_alpha import devices, sync +from synapse.rest.media.v1.filepath import MediaFilePaths from synapse.types import JsonDict, UserID from tests import unittest @@ -72,7 +76,7 @@ def test_disabled(self): channel = self.make_request("POST", self.url, b"{}") - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual( "Shared secret registration is not enabled", channel.json_body["error"] ) @@ -104,7 +108,7 @@ def test_expired_nonce(self): body = json.dumps({"nonce": nonce}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("username must be specified", channel.json_body["error"]) # 61 seconds @@ -112,7 +116,7 @@ def test_expired_nonce(self): channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("unrecognised nonce", channel.json_body["error"]) def test_register_incorrect_nonce(self): @@ -166,7 +170,7 @@ def test_register_correct_nonce(self): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["user_id"]) def test_nonce_reuse(self): @@ -191,13 +195,13 @@ def test_nonce_reuse(self): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["user_id"]) # Now, try and reuse it channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("unrecognised nonce", channel.json_body["error"]) def test_missing_parts(self): @@ -219,7 +223,7 @@ def nonce(): body = json.dumps({}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("nonce must be specified", channel.json_body["error"]) # @@ -230,28 +234,28 @@ def nonce(): body = json.dumps({"nonce": nonce()}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("username must be specified", channel.json_body["error"]) # Must be a string body = json.dumps({"nonce": nonce(), "username": 1234}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("Invalid username", channel.json_body["error"]) # Must not have null bytes body = json.dumps({"nonce": nonce(), "username": "abcd\u0000"}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("Invalid username", channel.json_body["error"]) # Must not have null bytes body = json.dumps({"nonce": nonce(), "username": "a" * 1000}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("Invalid username", channel.json_body["error"]) # @@ -262,28 +266,28 @@ def nonce(): body = json.dumps({"nonce": nonce(), "username": "a"}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("password must be specified", channel.json_body["error"]) # Must be a string body = json.dumps({"nonce": nonce(), "username": "a", "password": 1234}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("Invalid password", channel.json_body["error"]) # Must not have null bytes body = json.dumps({"nonce": nonce(), "username": "a", "password": "abcd\u0000"}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("Invalid password", channel.json_body["error"]) # Super long body = json.dumps({"nonce": nonce(), "username": "a", "password": "A" * 1000}) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("Invalid password", channel.json_body["error"]) # @@ -301,7 +305,7 @@ def nonce(): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual("Invalid user type", channel.json_body["error"]) def test_displayname(self): @@ -322,11 +326,11 @@ def test_displayname(self): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob1:test", channel.json_body["user_id"]) channel = self.make_request("GET", "/profile/@bob1:test/displayname") - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("bob1", channel.json_body["displayname"]) # displayname is None @@ -348,11 +352,11 @@ def test_displayname(self): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob2:test", channel.json_body["user_id"]) channel = self.make_request("GET", "/profile/@bob2:test/displayname") - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("bob2", channel.json_body["displayname"]) # displayname is empty @@ -374,7 +378,7 @@ def test_displayname(self): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob3:test", channel.json_body["user_id"]) channel = self.make_request("GET", "/profile/@bob3:test/displayname") @@ -399,11 +403,11 @@ def test_displayname(self): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob4:test", channel.json_body["user_id"]) channel = self.make_request("GET", "/profile/@bob4:test/displayname") - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("Bob's Name", channel.json_body["displayname"]) @override_config( @@ -449,7 +453,7 @@ def test_register_mau_limit_reached(self): ) channel = self.make_request("POST", self.url, body.encode("utf8")) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual("@bob:test", channel.json_body["user_id"]) @@ -638,7 +642,7 @@ def test_invalid_parameter(self): access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) # invalid search order @@ -1085,7 +1089,7 @@ def test_deactivate_user_erase_false(self): content={"erase": False}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # Get user channel = self.make_request( @@ -2180,7 +2184,7 @@ def test_user_is_not_local(self): ) self.assertEqual(400, channel.code, msg=channel.json_body) - self.assertEqual("Can only lookup local users", channel.json_body["error"]) + self.assertEqual("Can only look up local users", channel.json_body["error"]) def test_get_pushers(self): """ @@ -2249,6 +2253,7 @@ class UserMediaRestTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs): self.store = hs.get_datastore() self.media_repo = hs.get_media_repository_resource() + self.filepaths = MediaFilePaths(hs.config.media_store_path) self.admin_user = self.register_user("admin", "pass", admin=True) self.admin_user_tok = self.login("admin", "pass") @@ -2258,37 +2263,34 @@ def prepare(self, reactor, clock, hs): self.other_user ) - def test_no_auth(self): - """ - Try to list media of an user without authentication. - """ - channel = self.make_request("GET", self.url, b"{}") + @parameterized.expand(["GET", "DELETE"]) + def test_no_auth(self, method: str): + """Try to list media of an user without authentication.""" + channel = self.make_request(method, self.url, {}) - self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(401, channel.code, msg=channel.json_body) self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) - def test_requester_is_no_admin(self): - """ - If the user is not a server admin, an error is returned. - """ + @parameterized.expand(["GET", "DELETE"]) + def test_requester_is_no_admin(self, method: str): + """If the user is not a server admin, an error is returned.""" other_user_token = self.login("user", "pass") channel = self.make_request( - "GET", + method, self.url, access_token=other_user_token, ) - self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(403, channel.code, msg=channel.json_body) self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) - def test_user_does_not_exist(self): - """ - Tests that a lookup for a user that does not exist returns a 404 - """ + @parameterized.expand(["GET", "DELETE"]) + def test_user_does_not_exist(self, method: str): + """Tests that a lookup for a user that does not exist returns a 404""" url = "/_synapse/admin/v1/users/@unknown_person:test/media" channel = self.make_request( - "GET", + method, url, access_token=self.admin_user_tok, ) @@ -2296,25 +2298,22 @@ def test_user_does_not_exist(self): self.assertEqual(404, channel.code, msg=channel.json_body) self.assertEqual(Codes.NOT_FOUND, channel.json_body["errcode"]) - def test_user_is_not_local(self): - """ - Tests that a lookup for a user that is not a local returns a 400 - """ + @parameterized.expand(["GET", "DELETE"]) + def test_user_is_not_local(self, method: str): + """Tests that a lookup for a user that is not a local returns a 400""" url = "/_synapse/admin/v1/users/@unknown_person:unknown_domain/media" channel = self.make_request( - "GET", + method, url, access_token=self.admin_user_tok, ) self.assertEqual(400, channel.code, msg=channel.json_body) - self.assertEqual("Can only lookup local users", channel.json_body["error"]) + self.assertEqual("Can only look up local users", channel.json_body["error"]) - def test_limit(self): - """ - Testing list of media with limit - """ + def test_limit_GET(self): + """Testing list of media with limit""" number_media = 20 other_user_tok = self.login("user", "pass") @@ -2326,16 +2325,31 @@ def test_limit(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_media) self.assertEqual(len(channel.json_body["media"]), 5) self.assertEqual(channel.json_body["next_token"], 5) self._check_fields(channel.json_body["media"]) - def test_from(self): - """ - Testing list of media with a defined starting point (from) - """ + def test_limit_DELETE(self): + """Testing delete of media with limit""" + + number_media = 20 + other_user_tok = self.login("user", "pass") + self._create_media_for_user(other_user_tok, number_media) + + channel = self.make_request( + "DELETE", + self.url + "?limit=5", + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual(channel.json_body["total"], 5) + self.assertEqual(len(channel.json_body["deleted_media"]), 5) + + def test_from_GET(self): + """Testing list of media with a defined starting point (from)""" number_media = 20 other_user_tok = self.login("user", "pass") @@ -2347,16 +2361,31 @@ def test_from(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_media) self.assertEqual(len(channel.json_body["media"]), 15) self.assertNotIn("next_token", channel.json_body) self._check_fields(channel.json_body["media"]) - def test_limit_and_from(self): - """ - Testing list of media with a defined starting point and limit - """ + def test_from_DELETE(self): + """Testing delete of media with a defined starting point (from)""" + + number_media = 20 + other_user_tok = self.login("user", "pass") + self._create_media_for_user(other_user_tok, number_media) + + channel = self.make_request( + "DELETE", + self.url + "?from=5", + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual(channel.json_body["total"], 15) + self.assertEqual(len(channel.json_body["deleted_media"]), 15) + + def test_limit_and_from_GET(self): + """Testing list of media with a defined starting point and limit""" number_media = 20 other_user_tok = self.login("user", "pass") @@ -2368,59 +2397,78 @@ def test_limit_and_from(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_media) self.assertEqual(channel.json_body["next_token"], 15) self.assertEqual(len(channel.json_body["media"]), 10) self._check_fields(channel.json_body["media"]) - def test_invalid_parameter(self): - """ - If parameters are invalid, an error is returned. - """ + def test_limit_and_from_DELETE(self): + """Testing delete of media with a defined starting point and limit""" + + number_media = 20 + other_user_tok = self.login("user", "pass") + self._create_media_for_user(other_user_tok, number_media) + + channel = self.make_request( + "DELETE", + self.url + "?from=5&limit=10", + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual(channel.json_body["total"], 10) + self.assertEqual(len(channel.json_body["deleted_media"]), 10) + + @parameterized.expand(["GET", "DELETE"]) + def test_invalid_parameter(self, method: str): + """If parameters are invalid, an error is returned.""" # unkown order_by channel = self.make_request( - "GET", + method, self.url + "?order_by=bar", access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) # invalid search order channel = self.make_request( - "GET", + method, self.url + "?dir=bar", access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) # negative limit channel = self.make_request( - "GET", + method, self.url + "?limit=-5", access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) # negative from channel = self.make_request( - "GET", + method, self.url + "?from=-5", access_token=self.admin_user_tok, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) def test_next_token(self): """ Testing that `next_token` appears at the right place + + For deleting media `next_token` is not useful, because + after deleting media the media has a new order. """ number_media = 20 @@ -2435,7 +2483,7 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_media) self.assertEqual(len(channel.json_body["media"]), number_media) self.assertNotIn("next_token", channel.json_body) @@ -2448,7 +2496,7 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_media) self.assertEqual(len(channel.json_body["media"]), number_media) self.assertNotIn("next_token", channel.json_body) @@ -2461,7 +2509,7 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_media) self.assertEqual(len(channel.json_body["media"]), 19) self.assertEqual(channel.json_body["next_token"], 19) @@ -2475,12 +2523,12 @@ def test_next_token(self): access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(channel.json_body["total"], number_media) self.assertEqual(len(channel.json_body["media"]), 1) self.assertNotIn("next_token", channel.json_body) - def test_user_has_no_media(self): + def test_user_has_no_media_GET(self): """ Tests that a normal lookup for media is successfully if user has no media created @@ -2496,11 +2544,24 @@ def test_user_has_no_media(self): self.assertEqual(0, channel.json_body["total"]) self.assertEqual(0, len(channel.json_body["media"])) - def test_get_media(self): + def test_user_has_no_media_DELETE(self): """ - Tests that a normal lookup for media is successfully + Tests that a delete is successful if user has no media """ + channel = self.make_request( + "DELETE", + self.url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual(0, channel.json_body["total"]) + self.assertEqual(0, len(channel.json_body["deleted_media"])) + + def test_get_media(self): + """Tests that a normal lookup for media is successful""" + number_media = 5 other_user_tok = self.login("user", "pass") self._create_media_for_user(other_user_tok, number_media) @@ -2517,6 +2578,35 @@ def test_get_media(self): self.assertNotIn("next_token", channel.json_body) self._check_fields(channel.json_body["media"]) + def test_delete_media(self): + """Tests that a normal delete of media is successful""" + + number_media = 5 + other_user_tok = self.login("user", "pass") + media_ids = self._create_media_for_user(other_user_tok, number_media) + + # Test if the file exists + local_paths = [] + for media_id in media_ids: + local_path = self.filepaths.local_media_filepath(media_id) + self.assertTrue(os.path.exists(local_path)) + local_paths.append(local_path) + + channel = self.make_request( + "DELETE", + self.url, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual(number_media, channel.json_body["total"]) + self.assertEqual(number_media, len(channel.json_body["deleted_media"])) + self.assertCountEqual(channel.json_body["deleted_media"], media_ids) + + # Test if the file is deleted + for local_path in local_paths: + self.assertFalse(os.path.exists(local_path)) + def test_order_by(self): """ Testing order list with parameter `order_by` @@ -2622,13 +2712,16 @@ def test_order_by(self): [media2] + sorted([media1, media3]), "safe_from_quarantine", "b" ) - def _create_media_for_user(self, user_token: str, number_media: int): + def _create_media_for_user(self, user_token: str, number_media: int) -> List[str]: """ Create a number of media for a specific user Args: user_token: Access token of the user number_media: Number of media to be created for the user + Returns: + List of created media ID """ + media_ids = [] for _ in range(number_media): # file size is 67 Byte image_data = unhexlify( @@ -2637,7 +2730,9 @@ def _create_media_for_user(self, user_token: str, number_media: int): b"0a2db40000000049454e44ae426082" ) - self._create_media_and_access(user_token, image_data) + media_ids.append(self._create_media_and_access(user_token, image_data)) + + return media_ids def _create_media_and_access( self, @@ -2680,7 +2775,7 @@ def _create_media_and_access( 200, channel.code, msg=( - "Expected to receive a 200 on accessing media: %s" % server_and_media_id + f"Expected to receive a 200 on accessing media: {server_and_media_id}" ), ) @@ -2718,12 +2813,12 @@ def _order_test( url = self.url + "?" if order_by is not None: - url += "order_by=%s&" % (order_by,) + url += f"order_by={order_by}&" if dir is not None and dir in ("b", "f"): - url += "dir=%s" % (dir,) + url += f"dir={dir}" channel = self.make_request( "GET", - url.encode("ascii"), + url, access_token=self.admin_user_tok, ) self.assertEqual(200, channel.code, msg=channel.json_body) @@ -2762,7 +2857,7 @@ def _get_token(self) -> str: channel = self.make_request( "POST", self.url, b"{}", access_token=self.admin_user_tok ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) return channel.json_body["access_token"] def test_no_auth(self): @@ -2803,7 +2898,7 @@ def test_devices(self): channel = self.make_request( "GET", "devices", b"{}", access_token=self.other_user_tok ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # We should only see the one device (from the login in `prepare`) self.assertEqual(len(channel.json_body["devices"]), 1) @@ -2815,11 +2910,11 @@ def test_logout(self): # Test that we can successfully make a request channel = self.make_request("GET", "devices", b"{}", access_token=puppet_token) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # Logout with the puppet token channel = self.make_request("POST", "logout", b"{}", access_token=puppet_token) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # The puppet token should no longer work channel = self.make_request("GET", "devices", b"{}", access_token=puppet_token) @@ -2829,7 +2924,7 @@ def test_logout(self): channel = self.make_request( "GET", "devices", b"{}", access_token=self.other_user_tok ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) def test_user_logout_all(self): """Tests that the target user calling `/logout/all` does *not* expire @@ -2840,17 +2935,17 @@ def test_user_logout_all(self): # Test that we can successfully make a request channel = self.make_request("GET", "devices", b"{}", access_token=puppet_token) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # Logout all with the real user token channel = self.make_request( "POST", "logout/all", b"{}", access_token=self.other_user_tok ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # The puppet token should still work channel = self.make_request("GET", "devices", b"{}", access_token=puppet_token) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # .. but the real user's tokens shouldn't channel = self.make_request( @@ -2867,13 +2962,13 @@ def test_admin_logout_all(self): # Test that we can successfully make a request channel = self.make_request("GET", "devices", b"{}", access_token=puppet_token) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # Logout all with the admin user token channel = self.make_request( "POST", "logout/all", b"{}", access_token=self.admin_user_tok ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) # The puppet token should no longer work channel = self.make_request("GET", "devices", b"{}", access_token=puppet_token) @@ -2883,7 +2978,7 @@ def test_admin_logout_all(self): channel = self.make_request( "GET", "devices", b"{}", access_token=self.other_user_tok ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) @unittest.override_config( { @@ -3243,7 +3338,7 @@ def test_user_is_not_local(self): ) self.assertEqual(400, channel.code, msg=channel.json_body) - self.assertEqual("Can only lookup local users", channel.json_body["error"]) + self.assertEqual("Can only look up local users", channel.json_body["error"]) channel = self.make_request( "POST", @@ -3279,7 +3374,7 @@ def test_invalid_parameter(self): content={"messages_per_second": "string"}, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) # messages_per_second is negative @@ -3290,7 +3385,7 @@ def test_invalid_parameter(self): content={"messages_per_second": -1}, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) # burst_count is a string @@ -3301,7 +3396,7 @@ def test_invalid_parameter(self): content={"burst_count": "string"}, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) # burst_count is negative @@ -3312,7 +3407,7 @@ def test_invalid_parameter(self): content={"burst_count": -1}, ) - self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(400, channel.code, msg=channel.json_body) self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"]) def test_return_zero_when_null(self): @@ -3337,7 +3432,7 @@ def test_return_zero_when_null(self): self.url, access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(0, channel.json_body["messages_per_second"]) self.assertEqual(0, channel.json_body["burst_count"]) @@ -3351,7 +3446,7 @@ def test_success(self): self.url, access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertNotIn("messages_per_second", channel.json_body) self.assertNotIn("burst_count", channel.json_body) @@ -3362,7 +3457,7 @@ def test_success(self): access_token=self.admin_user_tok, content={"messages_per_second": 10, "burst_count": 11}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(10, channel.json_body["messages_per_second"]) self.assertEqual(11, channel.json_body["burst_count"]) @@ -3373,7 +3468,7 @@ def test_success(self): access_token=self.admin_user_tok, content={"messages_per_second": 20, "burst_count": 21}, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(20, channel.json_body["messages_per_second"]) self.assertEqual(21, channel.json_body["burst_count"]) @@ -3383,7 +3478,7 @@ def test_success(self): self.url, access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertEqual(20, channel.json_body["messages_per_second"]) self.assertEqual(21, channel.json_body["burst_count"]) @@ -3393,7 +3488,7 @@ def test_success(self): self.url, access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertNotIn("messages_per_second", channel.json_body) self.assertNotIn("burst_count", channel.json_body) @@ -3403,6 +3498,6 @@ def test_success(self): self.url, access_token=self.admin_user_tok, ) - self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(200, channel.code, msg=channel.json_body) self.assertNotIn("messages_per_second", channel.json_body) self.assertNotIn("burst_count", channel.json_body) From 98a3355d9a58538cfbc1c88020e6b6d9bccea516 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 11 Aug 2021 15:44:45 -0400 Subject: [PATCH 556/619] Update the pagination parameter name based on MSC2946 review. (#10579) --- changelog.d/10579.feature | 1 + synapse/handlers/space_summary.py | 6 +++--- tests/handlers/test_space_summary.py | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 changelog.d/10579.feature diff --git a/changelog.d/10579.feature b/changelog.d/10579.feature new file mode 100644 index 0000000000..ffc4e4289c --- /dev/null +++ b/changelog.d/10579.feature @@ -0,0 +1 @@ +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index 893546e661..d0060f9046 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -412,10 +412,10 @@ async def _get_room_hierarchy( # If there's additional data, generate a pagination token (and persist state). if room_queue: - next_token = random_string(24) - result["next_token"] = next_token + next_batch = random_string(24) + result["next_batch"] = next_batch pagination_key = _PaginationKey( - requested_room_id, suggested_only, max_depth, next_token + requested_room_id, suggested_only, max_depth, next_batch ) self._pagination_sessions[pagination_key] = _PaginationSession( self._clock.time_msec(), room_queue, processed_rooms diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 806b886fe4..83c2bdd8f9 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -466,19 +466,19 @@ def test_pagination(self): expected: List[Tuple[str, Iterable[str]]] = [(self.space, room_ids)] expected += [(room_id, ()) for room_id in room_ids[:6]] self._assert_hierarchy(result, expected) - self.assertIn("next_token", result) + self.assertIn("next_batch", result) # Check the next page. result = self.get_success( self.handler.get_room_hierarchy( - self.user, self.space, limit=5, from_token=result["next_token"] + self.user, self.space, limit=5, from_token=result["next_batch"] ) ) # The result should have the space and the room in it, along with a link # from space -> room. expected = [(room_id, ()) for room_id in room_ids[6:]] self._assert_hierarchy(result, expected) - self.assertNotIn("next_token", result) + self.assertNotIn("next_batch", result) def test_invalid_pagination_token(self): """""" @@ -493,12 +493,12 @@ def test_invalid_pagination_token(self): result = self.get_success( self.handler.get_room_hierarchy(self.user, self.space, limit=7) ) - self.assertIn("next_token", result) + self.assertIn("next_batch", result) # Changing the room ID, suggested-only, or max-depth causes an error. self.get_failure( self.handler.get_room_hierarchy( - self.user, self.room, from_token=result["next_token"] + self.user, self.room, from_token=result["next_batch"] ), SynapseError, ) @@ -507,13 +507,13 @@ def test_invalid_pagination_token(self): self.user, self.space, suggested_only=True, - from_token=result["next_token"], + from_token=result["next_batch"], ), SynapseError, ) self.get_failure( self.handler.get_room_hierarchy( - self.user, self.space, max_depth=0, from_token=result["next_token"] + self.user, self.space, max_depth=0, from_token=result["next_batch"] ), SynapseError, ) From 314a739160effac7501201cadfc8aaa9c4f34713 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Thu, 12 Aug 2021 10:40:32 +0100 Subject: [PATCH 557/619] Also rename in lint.sh --- scripts-dev/lint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts-dev/lint.sh b/scripts-dev/lint.sh index 2c77643cda..809eff166a 100755 --- a/scripts-dev/lint.sh +++ b/scripts-dev/lint.sh @@ -94,7 +94,7 @@ else "scripts-dev/build_debian_packages" "scripts-dev/sign_json" "scripts-dev/update_database" - "contrib" "synctl" "setup.py" "synmark" "stubs" "ci" + "contrib" "synctl" "setup.py" "synmark" "stubs" ".ci" ) fi fi From 74fcd5aab9de111d5c306d3ed28a3f3ef63f3e3e Mon Sep 17 00:00:00 2001 From: David Robertson Date: Thu, 12 Aug 2021 10:41:01 +0100 Subject: [PATCH 558/619] portdb also uses coverage, so provide $TOP there --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index de022020cc..6874d253ca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -250,6 +250,8 @@ jobs: if: ${{ !failure() && !cancelled() }} # Allow previous steps to be skipped, but not fail needs: linting-done runs-on: ubuntu-latest + env: + TOP: ${{ github.workspace }} strategy: matrix: include: From 878528913d2927bba5ba8795c405f8a7475934cd Mon Sep 17 00:00:00 2001 From: David Robertson Date: Thu, 12 Aug 2021 11:48:36 +0100 Subject: [PATCH 559/619] Remove buildkite-era comment --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6874d253ca..8736699ad8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,9 +38,6 @@ jobs: if: ${{ github.base_ref == 'develop' || contains(github.base_ref, 'release-') }} runs-on: ubuntu-latest steps: - # Note: This and the script can be simplified once we drop Buildkite. See: - # https://github.com/actions/checkout/issues/266#issuecomment-638346893 - # https://github.com/actions/checkout/issues/416 - uses: actions/checkout@v2 with: ref: ${{ github.event.pull_request.head.sha }} From d2ad397d3cbd7e675abbb1f48072f9972c60823d Mon Sep 17 00:00:00 2001 From: David Robertson Date: Thu, 12 Aug 2021 16:50:18 +0100 Subject: [PATCH 560/619] Stop building a debian package for Groovy Gorilla (#10588) --- changelog.d/10588.removal | 1 + scripts-dev/build_debian_packages | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/10588.removal diff --git a/changelog.d/10588.removal b/changelog.d/10588.removal new file mode 100644 index 0000000000..90c4b5cee2 --- /dev/null +++ b/changelog.d/10588.removal @@ -0,0 +1 @@ +No longer build `.dev` packages for Ubuntu 20.10 LTS Groovy Gorilla, which has now EOLed. \ No newline at end of file diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index 0ed1c679fd..6153cb225f 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -25,7 +25,6 @@ DISTS = ( "debian:sid", "ubuntu:bionic", # 18.04 LTS (our EOL forced by Py36 on 2021-12-23) "ubuntu:focal", # 20.04 LTS (our EOL forced by Py38 on 2024-10-14) - "ubuntu:groovy", # 20.10 (EOL 2021-07-07) "ubuntu:hirsute", # 21.04 (EOL 2022-01-05) ) From c12b5577f22ee587b60ad7b65e88322ce1d86b7b Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 13 Aug 2021 07:49:06 -0400 Subject: [PATCH 561/619] Fix a harmless exception when the staged events queue is empty. (#10592) --- changelog.d/10592.bugfix | 1 + synapse/federation/federation_server.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 changelog.d/10592.bugfix diff --git a/changelog.d/10592.bugfix b/changelog.d/10592.bugfix new file mode 100644 index 0000000000..efcdab1136 --- /dev/null +++ b/changelog.d/10592.bugfix @@ -0,0 +1 @@ +Fix a bug introduced in v1.37.1 where an error could occur in the asyncronous processing of PDUs when the queue was empty. diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 0385aadefa..78d5aac6af 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -972,13 +972,18 @@ async def _process_incoming_pdus_in_room_inner( # the room, so instead of pulling the event out of the DB and parsing # the event we just pull out the next event ID and check if that matches. if latest_event is not None and latest_origin is not None: - ( - next_origin, - next_event_id, - ) = await self.store.get_next_staged_event_id_for_room(room_id) - if next_origin != latest_origin or next_event_id != latest_event.event_id: + result = await self.store.get_next_staged_event_id_for_room(room_id) + if result is None: latest_origin = None latest_event = None + else: + next_origin, next_event_id = result + if ( + next_origin != latest_origin + or next_event_id != latest_event.event_id + ): + latest_origin = None + latest_event = None if latest_origin is None or latest_event is None: next = await self.store.get_next_staged_event_for_room( From c8d54be44c1da451f01504664d568dd2f2b37316 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 13 Aug 2021 14:37:24 -0500 Subject: [PATCH 562/619] Move /batch_send to /v2_alpha directory (MSC2716) (#10576) * Move /batch_send to /v2_alpha directory As pointed out by @erikjohnston, https://github.com/matrix-org/synapse/pull/10552#discussion_r685836624 --- changelog.d/10576.misc | 1 + synapse/rest/__init__.py | 2 + synapse/rest/client/v1/room.py | 410 +------------------------ synapse/rest/client/v2_alpha/room.py | 441 +++++++++++++++++++++++++++ 4 files changed, 445 insertions(+), 409 deletions(-) create mode 100644 changelog.d/10576.misc create mode 100644 synapse/rest/client/v2_alpha/room.py diff --git a/changelog.d/10576.misc b/changelog.d/10576.misc new file mode 100644 index 0000000000..f9f9c9a6fd --- /dev/null +++ b/changelog.d/10576.misc @@ -0,0 +1 @@ +Move `/batch_send` endpoint defined by [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716) to the `/v2_alpha` directory. diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index d29f2fea5e..9cffe59ce5 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -47,6 +47,7 @@ register, relations, report_event, + room as roomv2, room_keys, room_upgrade_rest_servlet, sendtodevice, @@ -117,6 +118,7 @@ def register_servlets(client_resource, hs): user_directory.register_servlets(hs, client_resource) groups.register_servlets(hs, client_resource) room_upgrade_rest_servlet.register_servlets(hs, client_resource) + roomv2.register_servlets(hs, client_resource) capabilities.register_servlets(hs, client_resource) account_validity.register_servlets(hs, client_resource) relations.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index f1bc43be2d..2c3be23bc8 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple from urllib import parse as urlparse -from synapse.api.constants import EventContentFields, EventTypes, Membership +from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( AuthError, Codes, @@ -28,7 +28,6 @@ SynapseError, ) from synapse.api.filtering import Filter -from synapse.appservice import ApplicationService from synapse.events.utils import format_event_for_client_v2 from synapse.http.servlet import ( RestServlet, @@ -47,13 +46,11 @@ from synapse.streams.config import PaginationConfig from synapse.types import ( JsonDict, - Requester, RoomAlias, RoomID, StreamToken, ThirdPartyInstanceID, UserID, - create_requester, ) from synapse.util import json_decoder from synapse.util.stringutils import parse_and_validate_server_name, random_string @@ -268,407 +265,6 @@ def on_PUT(self, request, room_id, event_type, txn_id): ) -class RoomBatchSendEventRestServlet(TransactionRestServlet): - """ - API endpoint which can insert a chunk of events historically back in time - next to the given `prev_event`. - - `chunk_id` comes from `next_chunk_id `in the response of the batch send - endpoint and is derived from the "insertion" events added to each chunk. - It's not required for the first batch send. - - `state_events_at_start` is used to define the historical state events - needed to auth the events like join events. These events will float - outside of the normal DAG as outlier's and won't be visible in the chat - history which also allows us to insert multiple chunks without having a bunch - of `@mxid joined the room` noise between each chunk. - - `events` is chronological chunk/list of events you want to insert. - There is a reverse-chronological constraint on chunks so once you insert - some messages, you can only insert older ones after that. - tldr; Insert chunks from your most recent history -> oldest history. - - POST /_matrix/client/unstable/org.matrix.msc2716/rooms//batch_send?prev_event=&chunk_id= - { - "events": [ ... ], - "state_events_at_start": [ ... ] - } - """ - - PATTERNS = ( - re.compile( - "^/_matrix/client/unstable/org.matrix.msc2716" - "/rooms/(?P[^/]*)/batch_send$" - ), - ) - - def __init__(self, hs): - super().__init__(hs) - self.hs = hs - self.store = hs.get_datastore() - self.state_store = hs.get_storage().state - self.event_creation_handler = hs.get_event_creation_handler() - self.room_member_handler = hs.get_room_member_handler() - self.auth = hs.get_auth() - - async def _inherit_depth_from_prev_ids(self, prev_event_ids) -> int: - ( - most_recent_prev_event_id, - most_recent_prev_event_depth, - ) = await self.store.get_max_depth_of(prev_event_ids) - - # We want to insert the historical event after the `prev_event` but before the successor event - # - # We inherit depth from the successor event instead of the `prev_event` - # because events returned from `/messages` are first sorted by `topological_ordering` - # which is just the `depth` and then tie-break with `stream_ordering`. - # - # We mark these inserted historical events as "backfilled" which gives them a - # negative `stream_ordering`. If we use the same depth as the `prev_event`, - # then our historical event will tie-break and be sorted before the `prev_event` - # when it should come after. - # - # We want to use the successor event depth so they appear after `prev_event` because - # it has a larger `depth` but before the successor event because the `stream_ordering` - # is negative before the successor event. - successor_event_ids = await self.store.get_successor_events( - [most_recent_prev_event_id] - ) - - # If we can't find any successor events, then it's a forward extremity of - # historical messages and we can just inherit from the previous historical - # event which we can already assume has the correct depth where we want - # to insert into. - if not successor_event_ids: - depth = most_recent_prev_event_depth - else: - ( - _, - oldest_successor_depth, - ) = await self.store.get_min_depth_of(successor_event_ids) - - depth = oldest_successor_depth - - return depth - - def _create_insertion_event_dict( - self, sender: str, room_id: str, origin_server_ts: int - ): - """Creates an event dict for an "insertion" event with the proper fields - and a random chunk ID. - - Args: - sender: The event author MXID - room_id: The room ID that the event belongs to - origin_server_ts: Timestamp when the event was sent - - Returns: - Tuple of event ID and stream ordering position - """ - - next_chunk_id = random_string(8) - insertion_event = { - "type": EventTypes.MSC2716_INSERTION, - "sender": sender, - "room_id": room_id, - "content": { - EventContentFields.MSC2716_NEXT_CHUNK_ID: next_chunk_id, - EventContentFields.MSC2716_HISTORICAL: True, - }, - "origin_server_ts": origin_server_ts, - } - - return insertion_event - - async def _create_requester_for_user_id_from_app_service( - self, user_id: str, app_service: ApplicationService - ) -> Requester: - """Creates a new requester for the given user_id - and validates that the app service is allowed to control - the given user. - - Args: - user_id: The author MXID that the app service is controlling - app_service: The app service that controls the user - - Returns: - Requester object - """ - - await self.auth.validate_appservice_can_control_user_id(app_service, user_id) - - return create_requester(user_id, app_service=app_service) - - async def on_POST(self, request, room_id): - requester = await self.auth.get_user_by_req(request, allow_guest=False) - - if not requester.app_service: - raise AuthError( - 403, - "Only application services can use the /batchsend endpoint", - ) - - body = parse_json_object_from_request(request) - assert_params_in_dict(body, ["state_events_at_start", "events"]) - - prev_events_from_query = parse_strings_from_args(request.args, "prev_event") - chunk_id_from_query = parse_string(request, "chunk_id") - - if prev_events_from_query is None: - raise SynapseError( - 400, - "prev_event query parameter is required when inserting historical messages back in time", - errcode=Codes.MISSING_PARAM, - ) - - # For the event we are inserting next to (`prev_events_from_query`), - # find the most recent auth events (derived from state events) that - # allowed that message to be sent. We will use that as a base - # to auth our historical messages against. - ( - most_recent_prev_event_id, - _, - ) = await self.store.get_max_depth_of(prev_events_from_query) - # mapping from (type, state_key) -> state_event_id - prev_state_map = await self.state_store.get_state_ids_for_event( - most_recent_prev_event_id - ) - # List of state event ID's - prev_state_ids = list(prev_state_map.values()) - auth_event_ids = prev_state_ids - - state_events_at_start = [] - for state_event in body["state_events_at_start"]: - assert_params_in_dict( - state_event, ["type", "origin_server_ts", "content", "sender"] - ) - - logger.debug( - "RoomBatchSendEventRestServlet inserting state_event=%s, auth_event_ids=%s", - state_event, - auth_event_ids, - ) - - event_dict = { - "type": state_event["type"], - "origin_server_ts": state_event["origin_server_ts"], - "content": state_event["content"], - "room_id": room_id, - "sender": state_event["sender"], - "state_key": state_event["state_key"], - } - - # Mark all events as historical - event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True - - # Make the state events float off on their own - fake_prev_event_id = "$" + random_string(43) - - # TODO: This is pretty much the same as some other code to handle inserting state in this file - if event_dict["type"] == EventTypes.Member: - membership = event_dict["content"].get("membership", None) - event_id, _ = await self.room_member_handler.update_membership( - await self._create_requester_for_user_id_from_app_service( - state_event["sender"], requester.app_service - ), - target=UserID.from_string(event_dict["state_key"]), - room_id=room_id, - action=membership, - content=event_dict["content"], - outlier=True, - prev_event_ids=[fake_prev_event_id], - # Make sure to use a copy of this list because we modify it - # later in the loop here. Otherwise it will be the same - # reference and also update in the event when we append later. - auth_event_ids=auth_event_ids.copy(), - ) - else: - # TODO: Add some complement tests that adds state that is not member joins - # and will use this code path. Maybe we only want to support join state events - # and can get rid of this `else`? - ( - event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - await self._create_requester_for_user_id_from_app_service( - state_event["sender"], requester.app_service - ), - event_dict, - outlier=True, - prev_event_ids=[fake_prev_event_id], - # Make sure to use a copy of this list because we modify it - # later in the loop here. Otherwise it will be the same - # reference and also update in the event when we append later. - auth_event_ids=auth_event_ids.copy(), - ) - event_id = event.event_id - - state_events_at_start.append(event_id) - auth_event_ids.append(event_id) - - events_to_create = body["events"] - - inherited_depth = await self._inherit_depth_from_prev_ids( - prev_events_from_query - ) - - # Figure out which chunk to connect to. If they passed in - # chunk_id_from_query let's use it. The chunk ID passed in comes - # from the chunk_id in the "insertion" event from the previous chunk. - last_event_in_chunk = events_to_create[-1] - chunk_id_to_connect_to = chunk_id_from_query - base_insertion_event = None - if chunk_id_from_query: - # All but the first base insertion event should point at a fake - # event, which causes the HS to ask for the state at the start of - # the chunk later. - prev_event_ids = [fake_prev_event_id] - # TODO: Verify the chunk_id_from_query corresponds to an insertion event - pass - # Otherwise, create an insertion event to act as a starting point. - # - # We don't always have an insertion event to start hanging more history - # off of (ideally there would be one in the main DAG, but that's not the - # case if we're wanting to add history to e.g. existing rooms without - # an insertion event), in which case we just create a new insertion event - # that can then get pointed to by a "marker" event later. - else: - prev_event_ids = prev_events_from_query - - base_insertion_event_dict = self._create_insertion_event_dict( - sender=requester.user.to_string(), - room_id=room_id, - origin_server_ts=last_event_in_chunk["origin_server_ts"], - ) - base_insertion_event_dict["prev_events"] = prev_event_ids.copy() - - ( - base_insertion_event, - _, - ) = await self.event_creation_handler.create_and_send_nonmember_event( - await self._create_requester_for_user_id_from_app_service( - base_insertion_event_dict["sender"], - requester.app_service, - ), - base_insertion_event_dict, - prev_event_ids=base_insertion_event_dict.get("prev_events"), - auth_event_ids=auth_event_ids, - historical=True, - depth=inherited_depth, - ) - - chunk_id_to_connect_to = base_insertion_event["content"][ - EventContentFields.MSC2716_NEXT_CHUNK_ID - ] - - # Connect this current chunk to the insertion event from the previous chunk - chunk_event = { - "type": EventTypes.MSC2716_CHUNK, - "sender": requester.user.to_string(), - "room_id": room_id, - "content": { - EventContentFields.MSC2716_CHUNK_ID: chunk_id_to_connect_to, - EventContentFields.MSC2716_HISTORICAL: True, - }, - # Since the chunk event is put at the end of the chunk, - # where the newest-in-time event is, copy the origin_server_ts from - # the last event we're inserting - "origin_server_ts": last_event_in_chunk["origin_server_ts"], - } - # Add the chunk event to the end of the chunk (newest-in-time) - events_to_create.append(chunk_event) - - # Add an "insertion" event to the start of each chunk (next to the oldest-in-time - # event in the chunk) so the next chunk can be connected to this one. - insertion_event = self._create_insertion_event_dict( - sender=requester.user.to_string(), - room_id=room_id, - # Since the insertion event is put at the start of the chunk, - # where the oldest-in-time event is, copy the origin_server_ts from - # the first event we're inserting - origin_server_ts=events_to_create[0]["origin_server_ts"], - ) - # Prepend the insertion event to the start of the chunk (oldest-in-time) - events_to_create = [insertion_event] + events_to_create - - event_ids = [] - events_to_persist = [] - for ev in events_to_create: - assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"]) - - event_dict = { - "type": ev["type"], - "origin_server_ts": ev["origin_server_ts"], - "content": ev["content"], - "room_id": room_id, - "sender": ev["sender"], # requester.user.to_string(), - "prev_events": prev_event_ids.copy(), - } - - # Mark all events as historical - event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True - - event, context = await self.event_creation_handler.create_event( - await self._create_requester_for_user_id_from_app_service( - ev["sender"], requester.app_service - ), - event_dict, - prev_event_ids=event_dict.get("prev_events"), - auth_event_ids=auth_event_ids, - historical=True, - depth=inherited_depth, - ) - logger.debug( - "RoomBatchSendEventRestServlet inserting event=%s, prev_event_ids=%s, auth_event_ids=%s", - event, - prev_event_ids, - auth_event_ids, - ) - - assert self.hs.is_mine_id(event.sender), "User must be our own: %s" % ( - event.sender, - ) - - events_to_persist.append((event, context)) - event_id = event.event_id - - event_ids.append(event_id) - prev_event_ids = [event_id] - - # Persist events in reverse-chronological order so they have the - # correct stream_ordering as they are backfilled (which decrements). - # Events are sorted by (topological_ordering, stream_ordering) - # where topological_ordering is just depth. - for (event, context) in reversed(events_to_persist): - ev = await self.event_creation_handler.handle_new_client_event( - await self._create_requester_for_user_id_from_app_service( - event["sender"], requester.app_service - ), - event=event, - context=context, - ) - - # Add the base_insertion_event to the bottom of the list we return - if base_insertion_event is not None: - event_ids.append(base_insertion_event.event_id) - - return 200, { - "state_events": state_events_at_start, - "events": event_ids, - "next_chunk_id": insertion_event["content"][ - EventContentFields.MSC2716_NEXT_CHUNK_ID - ], - } - - def on_GET(self, request, room_id): - return 501, "Not implemented" - - def on_PUT(self, request, room_id): - return self.txns.fetch_or_execute_request( - request, self.on_POST, request, room_id - ) - - # TODO: Needs unit testing for room ID + alias joins class JoinRoomAliasServlet(TransactionRestServlet): def __init__(self, hs): @@ -1488,8 +1084,6 @@ async def on_GET( def register_servlets(hs: "HomeServer", http_server, is_worker=False): - msc2716_enabled = hs.config.experimental.msc2716_enabled - RoomStateEventRestServlet(hs).register(http_server) RoomMemberListRestServlet(hs).register(http_server) JoinedRoomMemberListRestServlet(hs).register(http_server) @@ -1497,8 +1091,6 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False): JoinRoomAliasServlet(hs).register(http_server) RoomMembershipRestServlet(hs).register(http_server) RoomSendEventRestServlet(hs).register(http_server) - if msc2716_enabled: - RoomBatchSendEventRestServlet(hs).register(http_server) PublicRoomListRestServlet(hs).register(http_server) RoomStateRestServlet(hs).register(http_server) RoomRedactEventRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/room.py b/synapse/rest/client/v2_alpha/room.py new file mode 100644 index 0000000000..3172aba605 --- /dev/null +++ b/synapse/rest/client/v2_alpha/room.py @@ -0,0 +1,441 @@ +# Copyright 2016 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 +import re + +from synapse.api.constants import EventContentFields, EventTypes +from synapse.api.errors import AuthError, Codes, SynapseError +from synapse.appservice import ApplicationService +from synapse.http.servlet import ( + RestServlet, + assert_params_in_dict, + parse_json_object_from_request, + parse_string, + parse_strings_from_args, +) +from synapse.rest.client.transactions import HttpTransactionCache +from synapse.types import Requester, UserID, create_requester +from synapse.util.stringutils import random_string + +logger = logging.getLogger(__name__) + + +class RoomBatchSendEventRestServlet(RestServlet): + """ + API endpoint which can insert a chunk of events historically back in time + next to the given `prev_event`. + + `chunk_id` comes from `next_chunk_id `in the response of the batch send + endpoint and is derived from the "insertion" events added to each chunk. + It's not required for the first batch send. + + `state_events_at_start` is used to define the historical state events + needed to auth the events like join events. These events will float + outside of the normal DAG as outlier's and won't be visible in the chat + history which also allows us to insert multiple chunks without having a bunch + of `@mxid joined the room` noise between each chunk. + + `events` is chronological chunk/list of events you want to insert. + There is a reverse-chronological constraint on chunks so once you insert + some messages, you can only insert older ones after that. + tldr; Insert chunks from your most recent history -> oldest history. + + POST /_matrix/client/unstable/org.matrix.msc2716/rooms//batch_send?prev_event=&chunk_id= + { + "events": [ ... ], + "state_events_at_start": [ ... ] + } + """ + + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/org.matrix.msc2716" + "/rooms/(?P[^/]*)/batch_send$" + ), + ) + + def __init__(self, hs): + super().__init__() + self.hs = hs + self.store = hs.get_datastore() + self.state_store = hs.get_storage().state + self.event_creation_handler = hs.get_event_creation_handler() + self.room_member_handler = hs.get_room_member_handler() + self.auth = hs.get_auth() + self.txns = HttpTransactionCache(hs) + + async def _inherit_depth_from_prev_ids(self, prev_event_ids) -> int: + ( + most_recent_prev_event_id, + most_recent_prev_event_depth, + ) = await self.store.get_max_depth_of(prev_event_ids) + + # We want to insert the historical event after the `prev_event` but before the successor event + # + # We inherit depth from the successor event instead of the `prev_event` + # because events returned from `/messages` are first sorted by `topological_ordering` + # which is just the `depth` and then tie-break with `stream_ordering`. + # + # We mark these inserted historical events as "backfilled" which gives them a + # negative `stream_ordering`. If we use the same depth as the `prev_event`, + # then our historical event will tie-break and be sorted before the `prev_event` + # when it should come after. + # + # We want to use the successor event depth so they appear after `prev_event` because + # it has a larger `depth` but before the successor event because the `stream_ordering` + # is negative before the successor event. + successor_event_ids = await self.store.get_successor_events( + [most_recent_prev_event_id] + ) + + # If we can't find any successor events, then it's a forward extremity of + # historical messages and we can just inherit from the previous historical + # event which we can already assume has the correct depth where we want + # to insert into. + if not successor_event_ids: + depth = most_recent_prev_event_depth + else: + ( + _, + oldest_successor_depth, + ) = await self.store.get_min_depth_of(successor_event_ids) + + depth = oldest_successor_depth + + return depth + + def _create_insertion_event_dict( + self, sender: str, room_id: str, origin_server_ts: int + ): + """Creates an event dict for an "insertion" event with the proper fields + and a random chunk ID. + + Args: + sender: The event author MXID + room_id: The room ID that the event belongs to + origin_server_ts: Timestamp when the event was sent + + Returns: + Tuple of event ID and stream ordering position + """ + + next_chunk_id = random_string(8) + insertion_event = { + "type": EventTypes.MSC2716_INSERTION, + "sender": sender, + "room_id": room_id, + "content": { + EventContentFields.MSC2716_NEXT_CHUNK_ID: next_chunk_id, + EventContentFields.MSC2716_HISTORICAL: True, + }, + "origin_server_ts": origin_server_ts, + } + + return insertion_event + + async def _create_requester_for_user_id_from_app_service( + self, user_id: str, app_service: ApplicationService + ) -> Requester: + """Creates a new requester for the given user_id + and validates that the app service is allowed to control + the given user. + + Args: + user_id: The author MXID that the app service is controlling + app_service: The app service that controls the user + + Returns: + Requester object + """ + + await self.auth.validate_appservice_can_control_user_id(app_service, user_id) + + return create_requester(user_id, app_service=app_service) + + async def on_POST(self, request, room_id): + requester = await self.auth.get_user_by_req(request, allow_guest=False) + + if not requester.app_service: + raise AuthError( + 403, + "Only application services can use the /batchsend endpoint", + ) + + body = parse_json_object_from_request(request) + assert_params_in_dict(body, ["state_events_at_start", "events"]) + + prev_events_from_query = parse_strings_from_args(request.args, "prev_event") + chunk_id_from_query = parse_string(request, "chunk_id") + + if prev_events_from_query is None: + raise SynapseError( + 400, + "prev_event query parameter is required when inserting historical messages back in time", + errcode=Codes.MISSING_PARAM, + ) + + # For the event we are inserting next to (`prev_events_from_query`), + # find the most recent auth events (derived from state events) that + # allowed that message to be sent. We will use that as a base + # to auth our historical messages against. + ( + most_recent_prev_event_id, + _, + ) = await self.store.get_max_depth_of(prev_events_from_query) + # mapping from (type, state_key) -> state_event_id + prev_state_map = await self.state_store.get_state_ids_for_event( + most_recent_prev_event_id + ) + # List of state event ID's + prev_state_ids = list(prev_state_map.values()) + auth_event_ids = prev_state_ids + + state_events_at_start = [] + for state_event in body["state_events_at_start"]: + assert_params_in_dict( + state_event, ["type", "origin_server_ts", "content", "sender"] + ) + + logger.debug( + "RoomBatchSendEventRestServlet inserting state_event=%s, auth_event_ids=%s", + state_event, + auth_event_ids, + ) + + event_dict = { + "type": state_event["type"], + "origin_server_ts": state_event["origin_server_ts"], + "content": state_event["content"], + "room_id": room_id, + "sender": state_event["sender"], + "state_key": state_event["state_key"], + } + + # Mark all events as historical + event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True + + # Make the state events float off on their own + fake_prev_event_id = "$" + random_string(43) + + # TODO: This is pretty much the same as some other code to handle inserting state in this file + if event_dict["type"] == EventTypes.Member: + membership = event_dict["content"].get("membership", None) + event_id, _ = await self.room_member_handler.update_membership( + await self._create_requester_for_user_id_from_app_service( + state_event["sender"], requester.app_service + ), + target=UserID.from_string(event_dict["state_key"]), + room_id=room_id, + action=membership, + content=event_dict["content"], + outlier=True, + prev_event_ids=[fake_prev_event_id], + # Make sure to use a copy of this list because we modify it + # later in the loop here. Otherwise it will be the same + # reference and also update in the event when we append later. + auth_event_ids=auth_event_ids.copy(), + ) + else: + # TODO: Add some complement tests that adds state that is not member joins + # and will use this code path. Maybe we only want to support join state events + # and can get rid of this `else`? + ( + event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + await self._create_requester_for_user_id_from_app_service( + state_event["sender"], requester.app_service + ), + event_dict, + outlier=True, + prev_event_ids=[fake_prev_event_id], + # Make sure to use a copy of this list because we modify it + # later in the loop here. Otherwise it will be the same + # reference and also update in the event when we append later. + auth_event_ids=auth_event_ids.copy(), + ) + event_id = event.event_id + + state_events_at_start.append(event_id) + auth_event_ids.append(event_id) + + events_to_create = body["events"] + + inherited_depth = await self._inherit_depth_from_prev_ids( + prev_events_from_query + ) + + # Figure out which chunk to connect to. If they passed in + # chunk_id_from_query let's use it. The chunk ID passed in comes + # from the chunk_id in the "insertion" event from the previous chunk. + last_event_in_chunk = events_to_create[-1] + chunk_id_to_connect_to = chunk_id_from_query + base_insertion_event = None + if chunk_id_from_query: + # All but the first base insertion event should point at a fake + # event, which causes the HS to ask for the state at the start of + # the chunk later. + prev_event_ids = [fake_prev_event_id] + # TODO: Verify the chunk_id_from_query corresponds to an insertion event + pass + # Otherwise, create an insertion event to act as a starting point. + # + # We don't always have an insertion event to start hanging more history + # off of (ideally there would be one in the main DAG, but that's not the + # case if we're wanting to add history to e.g. existing rooms without + # an insertion event), in which case we just create a new insertion event + # that can then get pointed to by a "marker" event later. + else: + prev_event_ids = prev_events_from_query + + base_insertion_event_dict = self._create_insertion_event_dict( + sender=requester.user.to_string(), + room_id=room_id, + origin_server_ts=last_event_in_chunk["origin_server_ts"], + ) + base_insertion_event_dict["prev_events"] = prev_event_ids.copy() + + ( + base_insertion_event, + _, + ) = await self.event_creation_handler.create_and_send_nonmember_event( + await self._create_requester_for_user_id_from_app_service( + base_insertion_event_dict["sender"], + requester.app_service, + ), + base_insertion_event_dict, + prev_event_ids=base_insertion_event_dict.get("prev_events"), + auth_event_ids=auth_event_ids, + historical=True, + depth=inherited_depth, + ) + + chunk_id_to_connect_to = base_insertion_event["content"][ + EventContentFields.MSC2716_NEXT_CHUNK_ID + ] + + # Connect this current chunk to the insertion event from the previous chunk + chunk_event = { + "type": EventTypes.MSC2716_CHUNK, + "sender": requester.user.to_string(), + "room_id": room_id, + "content": { + EventContentFields.MSC2716_CHUNK_ID: chunk_id_to_connect_to, + EventContentFields.MSC2716_HISTORICAL: True, + }, + # Since the chunk event is put at the end of the chunk, + # where the newest-in-time event is, copy the origin_server_ts from + # the last event we're inserting + "origin_server_ts": last_event_in_chunk["origin_server_ts"], + } + # Add the chunk event to the end of the chunk (newest-in-time) + events_to_create.append(chunk_event) + + # Add an "insertion" event to the start of each chunk (next to the oldest-in-time + # event in the chunk) so the next chunk can be connected to this one. + insertion_event = self._create_insertion_event_dict( + sender=requester.user.to_string(), + room_id=room_id, + # Since the insertion event is put at the start of the chunk, + # where the oldest-in-time event is, copy the origin_server_ts from + # the first event we're inserting + origin_server_ts=events_to_create[0]["origin_server_ts"], + ) + # Prepend the insertion event to the start of the chunk (oldest-in-time) + events_to_create = [insertion_event] + events_to_create + + event_ids = [] + events_to_persist = [] + for ev in events_to_create: + assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"]) + + event_dict = { + "type": ev["type"], + "origin_server_ts": ev["origin_server_ts"], + "content": ev["content"], + "room_id": room_id, + "sender": ev["sender"], # requester.user.to_string(), + "prev_events": prev_event_ids.copy(), + } + + # Mark all events as historical + event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True + + event, context = await self.event_creation_handler.create_event( + await self._create_requester_for_user_id_from_app_service( + ev["sender"], requester.app_service + ), + event_dict, + prev_event_ids=event_dict.get("prev_events"), + auth_event_ids=auth_event_ids, + historical=True, + depth=inherited_depth, + ) + logger.debug( + "RoomBatchSendEventRestServlet inserting event=%s, prev_event_ids=%s, auth_event_ids=%s", + event, + prev_event_ids, + auth_event_ids, + ) + + assert self.hs.is_mine_id(event.sender), "User must be our own: %s" % ( + event.sender, + ) + + events_to_persist.append((event, context)) + event_id = event.event_id + + event_ids.append(event_id) + prev_event_ids = [event_id] + + # Persist events in reverse-chronological order so they have the + # correct stream_ordering as they are backfilled (which decrements). + # Events are sorted by (topological_ordering, stream_ordering) + # where topological_ordering is just depth. + for (event, context) in reversed(events_to_persist): + ev = await self.event_creation_handler.handle_new_client_event( + await self._create_requester_for_user_id_from_app_service( + event["sender"], requester.app_service + ), + event=event, + context=context, + ) + + # Add the base_insertion_event to the bottom of the list we return + if base_insertion_event is not None: + event_ids.append(base_insertion_event.event_id) + + return 200, { + "state_events": state_events_at_start, + "events": event_ids, + "next_chunk_id": insertion_event["content"][ + EventContentFields.MSC2716_NEXT_CHUNK_ID + ], + } + + def on_GET(self, request, room_id): + return 501, "Not implemented" + + def on_PUT(self, request, room_id): + return self.txns.fetch_or_execute_request( + request, self.on_POST, request, room_id + ) + + +def register_servlets(hs, http_server): + msc2716_enabled = hs.config.experimental.msc2716_enabled + + if msc2716_enabled: + RoomBatchSendEventRestServlet(hs).register(http_server) From d1f43b731ce041023563b1b814f858465a199630 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 16 Aug 2021 12:57:09 +0200 Subject: [PATCH 563/619] Update the Synapse Grafana dashboard (#10570) --- changelog.d/10570.feature | 1 + contrib/grafana/synapse.json | 550 ++++++++++++++++++++++++++++++----- 2 files changed, 485 insertions(+), 66 deletions(-) create mode 100644 changelog.d/10570.feature diff --git a/changelog.d/10570.feature b/changelog.d/10570.feature new file mode 100644 index 0000000000..bd432685b3 --- /dev/null +++ b/changelog.d/10570.feature @@ -0,0 +1 @@ +Update the Synapse Grafana dashboard. diff --git a/contrib/grafana/synapse.json b/contrib/grafana/synapse.json index 0c4816b7cd..ed1e8ba7f8 100644 --- a/contrib/grafana/synapse.json +++ b/contrib/grafana/synapse.json @@ -54,7 +54,7 @@ "gnetId": null, "graphTooltip": 0, "id": null, - "iteration": 1621258266004, + "iteration": 1628606819564, "links": [ { "asDropdown": false, @@ -307,7 +307,6 @@ ], "thresholds": [ { - "$$hashKey": "object:283", "colorMode": "warning", "fill": false, "line": true, @@ -316,7 +315,6 @@ "yaxis": "left" }, { - "$$hashKey": "object:284", "colorMode": "critical", "fill": false, "line": true, @@ -344,7 +342,6 @@ }, "yaxes": [ { - "$$hashKey": "object:255", "decimals": null, "format": "s", "label": "", @@ -354,7 +351,6 @@ "show": true }, { - "$$hashKey": "object:256", "format": "hertz", "label": "", "logBase": 1, @@ -429,7 +425,6 @@ ], "thresholds": [ { - "$$hashKey": "object:566", "colorMode": "critical", "fill": true, "line": true, @@ -457,7 +452,6 @@ }, "yaxes": [ { - "$$hashKey": "object:538", "decimals": null, "format": "percentunit", "label": null, @@ -467,7 +461,6 @@ "show": true }, { - "$$hashKey": "object:539", "format": "short", "label": null, "logBase": 1, @@ -573,7 +566,6 @@ }, "yaxes": [ { - "$$hashKey": "object:1560", "format": "bytes", "logBase": 1, "max": null, @@ -581,7 +573,6 @@ "show": true }, { - "$$hashKey": "object:1561", "format": "short", "logBase": 1, "max": null, @@ -641,7 +632,6 @@ "renderer": "flot", "seriesOverrides": [ { - "$$hashKey": "object:639", "alias": "/max$/", "color": "#890F02", "fill": 0, @@ -693,7 +683,6 @@ }, "yaxes": [ { - "$$hashKey": "object:650", "decimals": null, "format": "none", "label": "", @@ -703,7 +692,6 @@ "show": true }, { - "$$hashKey": "object:651", "decimals": null, "format": "short", "label": null, @@ -783,11 +771,9 @@ "renderer": "flot", "seriesOverrides": [ { - "$$hashKey": "object:1240", "alias": "/user/" }, { - "$$hashKey": "object:1241", "alias": "/system/" } ], @@ -817,7 +803,6 @@ ], "thresholds": [ { - "$$hashKey": "object:1278", "colorMode": "custom", "fillColor": "rgba(255, 255, 255, 1)", "line": true, @@ -827,7 +812,6 @@ "yaxis": "left" }, { - "$$hashKey": "object:1279", "colorMode": "custom", "fillColor": "rgba(255, 255, 255, 1)", "line": true, @@ -837,7 +821,6 @@ "yaxis": "left" }, { - "$$hashKey": "object:1498", "colorMode": "critical", "fill": true, "line": true, @@ -865,7 +848,6 @@ }, "yaxes": [ { - "$$hashKey": "object:1250", "decimals": null, "format": "percentunit", "label": "", @@ -875,7 +857,6 @@ "show": true }, { - "$$hashKey": "object:1251", "format": "short", "logBase": 1, "max": null, @@ -1427,7 +1408,6 @@ }, "yaxes": [ { - "$$hashKey": "object:572", "format": "percentunit", "label": null, "logBase": 1, @@ -1436,7 +1416,6 @@ "show": true }, { - "$$hashKey": "object:573", "format": "short", "label": null, "logBase": 1, @@ -1720,7 +1699,6 @@ }, "yaxes": [ { - "$$hashKey": "object:102", "format": "hertz", "logBase": 1, "max": null, @@ -1728,7 +1706,6 @@ "show": true }, { - "$$hashKey": "object:103", "format": "short", "logBase": 1, "max": null, @@ -3425,7 +3402,7 @@ "h": 9, "w": 12, "x": 0, - "y": 33 + "y": 6 }, "hiddenSeries": false, "id": 79, @@ -3442,9 +3419,12 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, - "pluginVersion": "7.1.3", + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -3526,7 +3506,7 @@ "h": 9, "w": 12, "x": 12, - "y": 33 + "y": 6 }, "hiddenSeries": false, "id": 83, @@ -3543,9 +3523,12 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, - "pluginVersion": "7.1.3", + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -3629,7 +3612,7 @@ "h": 9, "w": 12, "x": 0, - "y": 42 + "y": 15 }, "hiddenSeries": false, "id": 109, @@ -3646,9 +3629,12 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, - "pluginVersion": "7.1.3", + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -3733,7 +3719,7 @@ "h": 9, "w": 12, "x": 12, - "y": 42 + "y": 15 }, "hiddenSeries": false, "id": 111, @@ -3750,9 +3736,12 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, - "pluginVersion": "7.1.3", + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -3831,7 +3820,7 @@ "h": 8, "w": 12, "x": 0, - "y": 51 + "y": 24 }, "hiddenSeries": false, "id": 142, @@ -3847,8 +3836,11 @@ "lines": true, "linewidth": 1, "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "percentage": false, - "pluginVersion": "7.1.3", + "pluginVersion": "7.3.7", "pointradius": 2, "points": false, "renderer": "flot", @@ -3931,7 +3923,7 @@ "h": 9, "w": 12, "x": 12, - "y": 51 + "y": 24 }, "hiddenSeries": false, "id": 140, @@ -3948,9 +3940,12 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, - "pluginVersion": "7.1.3", + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -4079,7 +4074,7 @@ "h": 9, "w": 12, "x": 0, - "y": 59 + "y": 32 }, "heatmap": {}, "hideZeroBuckets": false, @@ -4145,7 +4140,7 @@ "h": 9, "w": 12, "x": 12, - "y": 60 + "y": 33 }, "hiddenSeries": false, "id": 162, @@ -4163,9 +4158,12 @@ "linewidth": 0, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, - "pluginVersion": "7.1.3", + "pluginVersion": "7.3.7", "pointradius": 5, "points": false, "renderer": "flot", @@ -4350,7 +4348,7 @@ "h": 9, "w": 12, "x": 0, - "y": 68 + "y": 41 }, "heatmap": {}, "hideZeroBuckets": false, @@ -4396,6 +4394,311 @@ "yBucketBound": "auto", "yBucketNumber": null, "yBucketSize": null + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 42 + }, + "hiddenSeries": false, + "id": 203, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "synapse_federation_server_oldest_inbound_pdu_in_staging{job=\"$job\",index=~\"$index\",instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "rss {{index}}", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Age of oldest event in staging area", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ms", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "grid": {}, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 50 + }, + "hiddenSeries": false, + "id": 202, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "paceLength": 10, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "synapse_federation_server_number_inbound_pdu_in_staging{job=\"$job\",index=~\"$index\",instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "rss {{index}}", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Number of events in federation staging area", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "none", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "${DS_PROMETHEUS}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 51 + }, + "hiddenSeries": false, + "id": 205, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(synapse_federation_soft_failed_events_total{instance=\"$instance\"}[$bucket_size]))", + "interval": "", + "legendFormat": "soft-failed events", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Soft-failed event rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "hertz", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "title": "Federation", @@ -4647,7 +4950,7 @@ "h": 7, "w": 12, "x": 0, - "y": 8 + "y": 33 }, "hiddenSeries": false, "id": 48, @@ -4749,7 +5052,7 @@ "h": 7, "w": 12, "x": 12, - "y": 8 + "y": 33 }, "hiddenSeries": false, "id": 104, @@ -4877,7 +5180,7 @@ "h": 7, "w": 12, "x": 0, - "y": 15 + "y": 40 }, "hiddenSeries": false, "id": 10, @@ -4981,7 +5284,7 @@ "h": 7, "w": 12, "x": 12, - "y": 15 + "y": 40 }, "hiddenSeries": false, "id": 11, @@ -5086,7 +5389,7 @@ "h": 7, "w": 12, "x": 0, - "y": 22 + "y": 47 }, "hiddenSeries": false, "id": 180, @@ -5168,6 +5471,126 @@ "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 6, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 47 + }, + "hiddenSeries": false, + "id": 200, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(synapse_storage_schedule_time_bucket{index=~\"$index\",instance=\"$instance\",job=\"$job\"}[$bucket_size])) by (le))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "99%", + "refId": "D" + }, + { + "expr": "histogram_quantile(0.9, sum(rate(synapse_storage_schedule_time_bucket{index=~\"$index\",instance=\"$instance\",job=\"$job\"}[$bucket_size])) by (le))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "90%", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.75, sum(rate(synapse_storage_schedule_time_bucket{index=~\"$index\",instance=\"$instance\",job=\"$job\"}[$bucket_size])) by (le))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "75%", + "refId": "C" + }, + { + "expr": "histogram_quantile(0.5, sum(rate(synapse_storage_schedule_time_bucket{index=~\"$index\",instance=\"$instance\",job=\"$job\"}[$bucket_size])) by (le))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "50%", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Time waiting for DB connection quantiles", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "s", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "repeat": null, @@ -5916,7 +6339,7 @@ "h": 10, "w": 12, "x": 0, - "y": 84 + "y": 35 }, "hiddenSeries": false, "id": 1, @@ -6022,7 +6445,7 @@ "h": 10, "w": 12, "x": 12, - "y": 84 + "y": 35 }, "hiddenSeries": false, "id": 8, @@ -6126,7 +6549,7 @@ "h": 10, "w": 12, "x": 0, - "y": 94 + "y": 45 }, "hiddenSeries": false, "id": 38, @@ -6226,7 +6649,7 @@ "h": 10, "w": 12, "x": 12, - "y": 94 + "y": 45 }, "hiddenSeries": false, "id": 39, @@ -6258,8 +6681,9 @@ "steppedLine": false, "targets": [ { - "expr": "topk(10, rate(synapse_util_caches_cache:total{job=\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size]) - rate(synapse_util_caches_cache:hits{job=\"$job\",instance=\"$instance\"}[$bucket_size]))", + "expr": "topk(10, rate(synapse_util_caches_cache:total{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size]) - rate(synapse_util_caches_cache:hits{job=~\"$job\",index=~\"$index\",instance=\"$instance\"}[$bucket_size]))", "format": "time_series", + "interval": "", "intervalFactor": 2, "legendFormat": "{{name}} {{job}}-{{index}}", "refId": "A", @@ -6326,7 +6750,7 @@ "h": 9, "w": 12, "x": 0, - "y": 104 + "y": 55 }, "hiddenSeries": false, "id": 65, @@ -9051,7 +9475,7 @@ "h": 8, "w": 12, "x": 0, - "y": 119 + "y": 41 }, "hiddenSeries": false, "id": 156, @@ -9089,7 +9513,7 @@ "steppedLine": false, "targets": [ { - "expr": "synapse_admin_mau:current{instance=\"$instance\"}", + "expr": "synapse_admin_mau:current{instance=\"$instance\", job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, @@ -9097,7 +9521,7 @@ "refId": "A" }, { - "expr": "synapse_admin_mau:max{instance=\"$instance\"}", + "expr": "synapse_admin_mau:max{instance=\"$instance\", job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, @@ -9164,7 +9588,7 @@ "h": 8, "w": 12, "x": 12, - "y": 119 + "y": 41 }, "hiddenSeries": false, "id": 160, @@ -9484,7 +9908,7 @@ "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 43 }, "hiddenSeries": false, "id": 168, @@ -9516,7 +9940,7 @@ { "expr": "rate(synapse_appservice_api_sent_events{instance=\"$instance\"}[$bucket_size])", "interval": "", - "legendFormat": "{{exported_service}}", + "legendFormat": "{{service}}", "refId": "A" } ], @@ -9579,7 +10003,7 @@ "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 43 }, "hiddenSeries": false, "id": 171, @@ -9611,7 +10035,7 @@ { "expr": "rate(synapse_appservice_api_sent_transactions{instance=\"$instance\"}[$bucket_size])", "interval": "", - "legendFormat": "{{exported_service}}", + "legendFormat": "{{service}}", "refId": "A" } ], @@ -9959,7 +10383,6 @@ }, "yaxes": [ { - "$$hashKey": "object:165", "format": "hertz", "label": null, "logBase": 1, @@ -9968,7 +10391,6 @@ "show": true }, { - "$$hashKey": "object:166", "format": "short", "label": null, "logBase": 1, @@ -10071,7 +10493,6 @@ }, "yaxes": [ { - "$$hashKey": "object:390", "format": "hertz", "label": null, "logBase": 1, @@ -10080,7 +10501,6 @@ "show": true }, { - "$$hashKey": "object:391", "format": "short", "label": null, "logBase": 1, @@ -10169,7 +10589,6 @@ }, "yaxes": [ { - "$$hashKey": "object:390", "format": "hertz", "label": null, "logBase": 1, @@ -10178,7 +10597,6 @@ "show": true }, { - "$$hashKey": "object:391", "format": "short", "label": null, "logBase": 1, @@ -10470,5 +10888,5 @@ "timezone": "", "title": "Synapse", "uid": "000000012", - "version": 90 + "version": 99 } \ No newline at end of file From a3a7514570f21dcad6f7ef4c1ee3ed1e30115825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 16 Aug 2021 13:22:38 +0200 Subject: [PATCH 564/619] Handle string read receipt data (#10606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handle string read receipt data Signed-off-by: Šimon Brandner * Test that we handle string read receipt data Signed-off-by: Šimon Brandner * Add changelog for #10606 Signed-off-by: Šimon Brandner * Add docs Signed-off-by: Šimon Brandner * Ignore malformed RRs Signed-off-by: Šimon Brandner * Only surround hidden = ... Signed-off-by: Šimon Brandner * Remove unnecessary argument Signed-off-by: Šimon Brandner * Update changelog.d/10606.bugfix Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/10606.bugfix | 1 + synapse/handlers/receipts.py | 9 ++++++++- tests/handlers/test_receipts.py | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10606.bugfix diff --git a/changelog.d/10606.bugfix b/changelog.d/10606.bugfix new file mode 100644 index 0000000000..bab9fd2a61 --- /dev/null +++ b/changelog.d/10606.bugfix @@ -0,0 +1 @@ +Fix errors on /sync when read receipt data is a string. Only affects homeservers with the experimental flag for [MSC2285](https://github.com/matrix-org/matrix-doc/pull/2285) enabled. Contributed by @SimonBrandner. diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py index 5fd4525700..fb495229a7 100644 --- a/synapse/handlers/receipts.py +++ b/synapse/handlers/receipts.py @@ -188,7 +188,14 @@ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]: new_users = {} for rr_user_id, user_rr in m_read.items(): - hidden = user_rr.get("hidden", None) + try: + hidden = user_rr.get("hidden") + except AttributeError: + # Due to https://github.com/matrix-org/synapse/issues/10376 + # there are cases where user_rr is a string, in those cases + # we just ignore the read receipt + continue + if hidden is not True or rr_user_id == user_id: new_users[rr_user_id] = user_rr.copy() # If hidden has a value replace hidden with the correct prefixed key diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py index 93a9a084b2..732a12c9bd 100644 --- a/tests/handlers/test_receipts.py +++ b/tests/handlers/test_receipts.py @@ -286,6 +286,29 @@ def test_filters_out_receipt_event_with_only_hidden_receipt_and_ignores_rest(sel ], ) + def test_handles_string_data(self): + """ + Tests that an invalid shape for read-receipts is handled. + Context: https://github.com/matrix-org/synapse/issues/10603 + """ + + self._test_filters_hidden( + [ + { + "content": { + "$14356419edgd14394fHBLK:matrix.org": { + "m.read": { + "@rikj:jki.re": "string", + } + }, + }, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "type": "m.receipt", + }, + ], + [], + ) + def _test_filters_hidden( self, events: List[JsonDict], expected_output: List[JsonDict] ): From 7de445161f2fec115ce8518cde7a3b333a611f16 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 16 Aug 2021 08:06:17 -0400 Subject: [PATCH 565/619] Support federation in the new spaces summary API (MSC2946). (#10569) --- changelog.d/10569.feature | 1 + synapse/federation/federation_client.py | 82 +++++++ synapse/federation/transport/client.py | 22 ++ synapse/federation/transport/server.py | 28 +++ synapse/handlers/space_summary.py | 258 +++++++++++++++++---- tests/handlers/test_space_summary.py | 292 ++++++++++++++---------- 6 files changed, 518 insertions(+), 165 deletions(-) create mode 100644 changelog.d/10569.feature diff --git a/changelog.d/10569.feature b/changelog.d/10569.feature new file mode 100644 index 0000000000..ffc4e4289c --- /dev/null +++ b/changelog.d/10569.feature @@ -0,0 +1 @@ +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 2eefac04fd..0af953a5d6 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1290,6 +1290,88 @@ async def send_request(destination: str) -> FederationSpaceSummaryResult: failover_on_unknown_endpoint=True, ) + async def get_room_hierarchy( + self, + destinations: Iterable[str], + room_id: str, + suggested_only: bool, + ) -> Tuple[JsonDict, Sequence[JsonDict], Sequence[str]]: + """ + Call other servers to get a hierarchy of the given room. + + Performs simple data validates and parsing of the response. + + Args: + destinations: The remote servers. We will try them in turn, omitting any + that have been blacklisted. + room_id: ID of the space to be queried + suggested_only: If true, ask the remote server to only return children + with the "suggested" flag set + + Returns: + A tuple of: + The room as a JSON dictionary. + A list of children rooms, as JSON dictionaries. + A list of inaccessible children room IDs. + + Raises: + SynapseError if we were unable to get a valid summary from any of the + remote servers + """ + + async def send_request( + destination: str, + ) -> Tuple[JsonDict, Sequence[JsonDict], Sequence[str]]: + res = await self.transport_layer.get_room_hierarchy( + destination=destination, + room_id=room_id, + suggested_only=suggested_only, + ) + + room = res.get("room") + if not isinstance(room, dict): + raise InvalidResponseError("'room' must be a dict") + + # Validate children_state of the room. + children_state = room.get("children_state", []) + if not isinstance(children_state, Sequence): + raise InvalidResponseError("'room.children_state' must be a list") + if any(not isinstance(e, dict) for e in children_state): + raise InvalidResponseError("Invalid event in 'children_state' list") + try: + [ + FederationSpaceSummaryEventResult.from_json_dict(e) + for e in children_state + ] + except ValueError as e: + raise InvalidResponseError(str(e)) + + # Validate the children rooms. + children = res.get("children", []) + if not isinstance(children, Sequence): + raise InvalidResponseError("'children' must be a list") + if any(not isinstance(r, dict) for r in children): + raise InvalidResponseError("Invalid room in 'children' list") + + # Validate the inaccessible children. + inaccessible_children = res.get("inaccessible_children", []) + if not isinstance(inaccessible_children, Sequence): + raise InvalidResponseError("'inaccessible_children' must be a list") + if any(not isinstance(r, str) for r in inaccessible_children): + raise InvalidResponseError( + "Invalid room ID in 'inaccessible_children' list" + ) + + return room, children, inaccessible_children + + # TODO Fallback to the old federation API and translate the results. + return await self._try_destination_list( + "fetch room hierarchy", + destinations, + send_request, + failover_on_unknown_endpoint=True, + ) + @attr.s(frozen=True, slots=True, auto_attribs=True) class FederationSpaceSummaryEventResult: diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 90a7c16b62..8b247fe206 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1177,6 +1177,28 @@ async def get_space_summary( destination=destination, path=path, data=params ) + async def get_room_hierarchy( + self, + destination: str, + room_id: str, + suggested_only: bool, + ) -> JsonDict: + """ + Args: + destination: The remote server + room_id: The room ID to ask about. + suggested_only: if True, only suggested rooms will be returned + """ + path = _create_path( + FEDERATION_UNSTABLE_PREFIX, "/org.matrix.msc2946/hierarchy/%s", room_id + ) + + return await self.client.get_json( + destination=destination, + path=path, + args={"suggested_only": "true" if suggested_only else "false"}, + ) + def _create_path(federation_prefix: str, path: str, *args: str) -> str: """ diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py index 640f46fff6..79a2e1afa0 100644 --- a/synapse/federation/transport/server.py +++ b/synapse/federation/transport/server.py @@ -1936,6 +1936,33 @@ async def on_POST( ) +class FederationRoomHierarchyServlet(BaseFederationServlet): + PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" + PATH = "/hierarchy/(?P[^/]*)" + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_space_summary_handler() + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Mapping[bytes, Sequence[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) + return 200, await self.handler.get_federation_hierarchy( + origin, room_id, suggested_only + ) + + class RoomComplexityServlet(BaseFederationServlet): """ Indicates to other servers how complex (and therefore likely @@ -1999,6 +2026,7 @@ async def on_GET( FederationVersionServlet, RoomComplexityServlet, FederationSpaceSummaryServlet, + FederationRoomHierarchyServlet, FederationV1SendKnockServlet, FederationMakeKnockServlet, ) diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/space_summary.py index d0060f9046..c74e90abbc 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/space_summary.py @@ -16,17 +16,7 @@ import logging import re from collections import deque -from typing import ( - TYPE_CHECKING, - Deque, - Dict, - Iterable, - List, - Optional, - Sequence, - Set, - Tuple, -) +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Set, Tuple import attr @@ -80,7 +70,7 @@ class _PaginationSession: # The time the pagination session was created, in milliseconds. creation_time_ms: int # The queue of rooms which are still to process. - room_queue: Deque["_RoomQueueEntry"] + room_queue: List["_RoomQueueEntry"] # A set of rooms which have been processed. processed_rooms: Set[str] @@ -197,7 +187,7 @@ async def get_space_summary( events: Sequence[JsonDict] = [] if room_entry: rooms_result.append(room_entry.room) - events = room_entry.children + events = room_entry.children_state_events logger.debug( "Query of local room %s returned events %s", @@ -232,7 +222,7 @@ async def get_space_summary( room.pop("allowed_spaces", None) rooms_result.append(room) - events.extend(room_entry.children) + events.extend(room_entry.children_state_events) # All rooms returned don't need visiting again (even if the user # didn't have access to them). @@ -350,8 +340,8 @@ async def _get_room_hierarchy( room_queue = pagination_session.room_queue processed_rooms = pagination_session.processed_rooms else: - # the queue of rooms to process - room_queue = deque((_RoomQueueEntry(requested_room_id, ()),)) + # The queue of rooms to process, the next room is last on the stack. + room_queue = [_RoomQueueEntry(requested_room_id, ())] # Rooms we have already processed. processed_rooms = set() @@ -367,7 +357,7 @@ async def _get_room_hierarchy( # Iterate through the queue until we reach the limit or run out of # rooms to include. while room_queue and len(rooms_result) < limit: - queue_entry = room_queue.popleft() + queue_entry = room_queue.pop() room_id = queue_entry.room_id current_depth = queue_entry.depth if room_id in processed_rooms: @@ -376,6 +366,18 @@ async def _get_room_hierarchy( logger.debug("Processing room %s", room_id) + # A map of summaries for children rooms that might be returned over + # federation. The rationale for caching these and *maybe* using them + # is to prefer any information local to the homeserver before trusting + # data received over federation. + children_room_entries: Dict[str, JsonDict] = {} + # A set of room IDs which are children that did not have information + # returned over federation and are known to be inaccessible to the + # current server. We should not reach out over federation to try to + # summarise these rooms. + inaccessible_children: Set[str] = set() + + # If the room is known locally, summarise it! is_in_room = await self._store.is_host_joined(room_id, self._server_name) if is_in_room: room_entry = await self._summarize_local_room( @@ -387,26 +389,68 @@ async def _get_room_hierarchy( max_children=None, ) - if room_entry: - rooms_result.append(room_entry.as_json()) - - # Add the child to the queue. We have already validated - # that the vias are a list of server names. - # - # If the current depth is the maximum depth, do not queue - # more entries. - if max_depth is None or current_depth < max_depth: - room_queue.extendleft( - _RoomQueueEntry( - ev["state_key"], ev["content"]["via"], current_depth + 1 - ) - for ev in reversed(room_entry.children) - ) - - processed_rooms.add(room_id) + # Otherwise, attempt to use information for federation. else: - # TODO Federation. - pass + # A previous call might have included information for this room. + # It can be used if either: + # + # 1. The room is not a space. + # 2. The maximum depth has been achieved (since no children + # information is needed). + if queue_entry.remote_room and ( + queue_entry.remote_room.get("room_type") != RoomTypes.SPACE + or (max_depth is not None and current_depth >= max_depth) + ): + room_entry = _RoomEntry( + queue_entry.room_id, queue_entry.remote_room + ) + + # If the above isn't true, attempt to fetch the room + # information over federation. + else: + ( + room_entry, + children_room_entries, + inaccessible_children, + ) = await self._summarize_remote_room_hiearchy( + queue_entry, + suggested_only, + ) + + # Ensure this room is accessible to the requester (and not just + # the homeserver). + if room_entry and not await self._is_remote_room_accessible( + requester, queue_entry.room_id, room_entry.room + ): + room_entry = None + + # This room has been processed and should be ignored if it appears + # elsewhere in the hierarchy. + processed_rooms.add(room_id) + + # There may or may not be a room entry based on whether it is + # inaccessible to the requesting user. + if room_entry: + # Add the room (including the stripped m.space.child events). + rooms_result.append(room_entry.as_json()) + + # If this room is not at the max-depth, check if there are any + # children to process. + if max_depth is None or current_depth < max_depth: + # The children get added in reverse order so that the next + # room to process, according to the ordering, is the last + # item in the list. + room_queue.extend( + _RoomQueueEntry( + ev["state_key"], + ev["content"]["via"], + current_depth + 1, + children_room_entries.get(ev["state_key"]), + ) + for ev in reversed(room_entry.children_state_events) + if ev["type"] == EventTypes.SpaceChild + and ev["state_key"] not in inaccessible_children + ) result: JsonDict = {"rooms": rooms_result} @@ -477,15 +521,78 @@ async def federation_space_summary( if room_entry: rooms_result.append(room_entry.room) - events_result.extend(room_entry.children) + events_result.extend(room_entry.children_state_events) # add any children to the queue room_queue.extend( - edge_event["state_key"] for edge_event in room_entry.children + edge_event["state_key"] + for edge_event in room_entry.children_state_events ) return {"rooms": rooms_result, "events": events_result} + async def get_federation_hierarchy( + self, + origin: str, + requested_room_id: str, + suggested_only: bool, + ): + """ + Implementation of the room hierarchy Federation API. + + This is similar to get_room_hierarchy, but does not recurse into the space. + It also considers whether anyone on the server may be able to access the + room, as opposed to whether a specific user can. + + Args: + origin: The server requesting the spaces summary. + requested_room_id: The room ID to start the hierarchy at (the "root" room). + suggested_only: whether we should only return children with the "suggested" + flag set. + + Returns: + The JSON hierarchy dictionary. + """ + root_room_entry = await self._summarize_local_room( + None, origin, requested_room_id, suggested_only, max_children=None + ) + if root_room_entry is None: + # Room is inaccessible to the requesting server. + raise SynapseError(404, "Unknown room: %s" % (requested_room_id,)) + + children_rooms_result: List[JsonDict] = [] + inaccessible_children: List[str] = [] + + # Iterate through each child and potentially add it, but not its children, + # to the response. + for child_room in root_room_entry.children_state_events: + room_id = child_room.get("state_key") + assert isinstance(room_id, str) + # If the room is unknown, skip it. + if not await self._store.is_host_joined(room_id, self._server_name): + continue + + room_entry = await self._summarize_local_room( + None, origin, room_id, suggested_only, max_children=0 + ) + # If the room is accessible, include it in the results. + # + # Note that only the room summary (without information on children) + # is included in the summary. + if room_entry: + children_rooms_result.append(room_entry.room) + # Otherwise, note that the requesting server shouldn't bother + # trying to summarize this room - they do not have access to it. + else: + inaccessible_children.append(room_id) + + return { + # Include the requested room (including the stripped children events). + "room": root_room_entry.as_json(), + "children": children_rooms_result, + "inaccessible_children": inaccessible_children, + } + async def _summarize_local_room( self, requester: Optional[str], @@ -519,8 +626,9 @@ async def _summarize_local_room( room_entry = await self._build_room_entry(room_id, for_federation=bool(origin)) - # If the room is not a space, return just the room information. - if room_entry.get("room_type") != RoomTypes.SPACE: + # If the room is not a space or the children don't matter, return just + # the room information. + if room_entry.get("room_type") != RoomTypes.SPACE or max_children == 0: return _RoomEntry(room_id, room_entry) # Otherwise, look for child rooms/spaces. @@ -616,6 +724,59 @@ async def _summarize_remote_room( return results + async def _summarize_remote_room_hiearchy( + self, room: "_RoomQueueEntry", suggested_only: bool + ) -> Tuple[Optional["_RoomEntry"], Dict[str, JsonDict], Set[str]]: + """ + Request room entries and a list of event entries for a given room by querying a remote server. + + Args: + room: The room to summarize. + suggested_only: True if only suggested children should be returned. + Otherwise, all children are returned. + + Returns: + A tuple of: + The room entry. + Partial room data return over federation. + A set of inaccessible children room IDs. + """ + room_id = room.room_id + logger.info("Requesting summary for %s via %s", room_id, room.via) + + via = itertools.islice(room.via, MAX_SERVERS_PER_SPACE) + try: + ( + room_response, + children, + inaccessible_children, + ) = await self._federation_client.get_room_hierarchy( + via, + room_id, + suggested_only=suggested_only, + ) + except Exception as e: + logger.warning( + "Unable to get hierarchy of %s via federation: %s", + room_id, + e, + exc_info=logger.isEnabledFor(logging.DEBUG), + ) + return None, {}, set() + + # Map the children to their room ID. + children_by_room_id = { + c["room_id"]: c + for c in children + if "room_id" in c and isinstance(c["room_id"], str) + } + + return ( + _RoomEntry(room_id, room_response, room_response.pop("children_state", ())), + children_by_room_id, + set(inaccessible_children), + ) + async def _is_local_room_accessible( self, room_id: str, requester: Optional[str], origin: Optional[str] = None ) -> bool: @@ -866,9 +1027,16 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: @attr.s(frozen=True, slots=True, auto_attribs=True) class _RoomQueueEntry: + # The room ID of this entry. room_id: str + # The server to query if the room is not known locally. via: Sequence[str] + # The minimum number of hops necessary to get to this room (compared to the + # originally requested room). depth: int = 0 + # The room summary for this room returned via federation. This will only be + # used if the room is not known locally (and is not a space). + remote_room: Optional[JsonDict] = None @attr.s(frozen=True, slots=True, auto_attribs=True) @@ -879,11 +1047,17 @@ class _RoomEntry: # An iterable of the sorted, stripped children events for children of this room. # # This may not include all children. - children: Sequence[JsonDict] = () + children_state_events: Sequence[JsonDict] = () def as_json(self) -> JsonDict: + """ + Returns a JSON dictionary suitable for the room hierarchy endpoint. + + It returns the room summary including the stripped m.space.child events + as a sub-key. + """ result = dict(self.room) - result["children_state"] = self.children + result["children_state"] = self.children_state_events return result diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_space_summary.py index 83c2bdd8f9..bc8e131f4a 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_space_summary.py @@ -481,7 +481,7 @@ def test_pagination(self): self.assertNotIn("next_batch", result) def test_invalid_pagination_token(self): - """""" + """An invalid pagination token, or changing other parameters, shoudl be rejected.""" room_ids = [] for i in range(1, 10): room = self.helper.create_room_as(self.user, tok=self.token) @@ -581,33 +581,40 @@ def test_fed_complex(self): subspace = "#subspace:" + fed_hostname subroom = "#subroom:" + fed_hostname + # Generate some good data, and some bad data: + # + # * Event *back* to the root room. + # * Unrelated events / rooms + # * Multiple levels of events (in a not-useful order, e.g. grandchild + # events before child events). + + # Note that these entries are brief, but should contain enough info. + requested_room_entry = _RoomEntry( + subspace, + { + "room_id": subspace, + "world_readable": True, + "room_type": RoomTypes.SPACE, + }, + [ + { + "type": EventTypes.SpaceChild, + "room_id": subspace, + "state_key": subroom, + "content": {"via": [fed_hostname]}, + } + ], + ) + child_room = { + "room_id": subroom, + "world_readable": True, + } + async def summarize_remote_room( _self, room, suggested_only, max_children, exclude_rooms ): - # Return some good data, and some bad data: - # - # * Event *back* to the root room. - # * Unrelated events / rooms - # * Multiple levels of events (in a not-useful order, e.g. grandchild - # events before child events). - - # Note that these entries are brief, but should contain enough info. return [ - _RoomEntry( - subspace, - { - "room_id": subspace, - "world_readable": True, - "room_type": RoomTypes.SPACE, - }, - [ - { - "room_id": subspace, - "state_key": subroom, - "content": {"via": [fed_hostname]}, - } - ], - ), + requested_room_entry, _RoomEntry( subroom, { @@ -617,6 +624,9 @@ async def summarize_remote_room( ), ] + async def summarize_remote_room_hiearchy(_self, room, suggested_only): + return requested_room_entry, {subroom: child_room}, set() + # Add a room to the space which is on another server. self._add_child(self.space, subspace, self.token) @@ -636,6 +646,15 @@ async def summarize_remote_room( ] self._assert_rooms(result, expected) + with mock.patch( + "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy", + new=summarize_remote_room_hiearchy, + ): + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space) + ) + self._assert_hierarchy(result, expected) + def test_fed_filtering(self): """ Rooms returned over federation should be properly filtered to only include @@ -657,100 +676,106 @@ def test_fed_filtering(self): # Poke an invite over federation into the database. self._poke_fed_invite(invited_room, "@remote:" + fed_hostname) + # Note that these entries are brief, but should contain enough info. + children_rooms = ( + ( + public_room, + { + "room_id": public_room, + "world_readable": False, + "join_rules": JoinRules.PUBLIC, + }, + ), + ( + knock_room, + { + "room_id": knock_room, + "world_readable": False, + "join_rules": JoinRules.KNOCK, + }, + ), + ( + not_invited_room, + { + "room_id": not_invited_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ), + ( + invited_room, + { + "room_id": invited_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ), + ( + restricted_room, + { + "room_id": restricted_room, + "world_readable": False, + "join_rules": JoinRules.RESTRICTED, + "allowed_spaces": [], + }, + ), + ( + restricted_accessible_room, + { + "room_id": restricted_accessible_room, + "world_readable": False, + "join_rules": JoinRules.RESTRICTED, + "allowed_spaces": [self.room], + }, + ), + ( + world_readable_room, + { + "room_id": world_readable_room, + "world_readable": True, + "join_rules": JoinRules.INVITE, + }, + ), + ( + joined_room, + { + "room_id": joined_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ), + ) + + subspace_room_entry = _RoomEntry( + subspace, + { + "room_id": subspace, + "world_readable": True, + }, + # Place each room in the sub-space. + [ + { + "type": EventTypes.SpaceChild, + "room_id": subspace, + "state_key": room_id, + "content": {"via": [fed_hostname]}, + } + for room_id, _ in children_rooms + ], + ) + async def summarize_remote_room( _self, room, suggested_only, max_children, exclude_rooms ): - # Note that these entries are brief, but should contain enough info. - rooms = [ - _RoomEntry( - public_room, - { - "room_id": public_room, - "world_readable": False, - "join_rules": JoinRules.PUBLIC, - }, - ), - _RoomEntry( - knock_room, - { - "room_id": knock_room, - "world_readable": False, - "join_rules": JoinRules.KNOCK, - }, - ), - _RoomEntry( - not_invited_room, - { - "room_id": not_invited_room, - "world_readable": False, - "join_rules": JoinRules.INVITE, - }, - ), - _RoomEntry( - invited_room, - { - "room_id": invited_room, - "world_readable": False, - "join_rules": JoinRules.INVITE, - }, - ), - _RoomEntry( - restricted_room, - { - "room_id": restricted_room, - "world_readable": False, - "join_rules": JoinRules.RESTRICTED, - "allowed_spaces": [], - }, - ), - _RoomEntry( - restricted_accessible_room, - { - "room_id": restricted_accessible_room, - "world_readable": False, - "join_rules": JoinRules.RESTRICTED, - "allowed_spaces": [self.room], - }, - ), - _RoomEntry( - world_readable_room, - { - "room_id": world_readable_room, - "world_readable": True, - "join_rules": JoinRules.INVITE, - }, - ), - _RoomEntry( - joined_room, - { - "room_id": joined_room, - "world_readable": False, - "join_rules": JoinRules.INVITE, - }, - ), + return [subspace_room_entry] + [ + # A copy is made of the room data since the allowed_spaces key + # is removed. + _RoomEntry(child_room[0], dict(child_room[1])) + for child_room in children_rooms ] - # Also include the subspace. - rooms.insert( - 0, - _RoomEntry( - subspace, - { - "room_id": subspace, - "world_readable": True, - }, - # Place each room in the sub-space. - [ - { - "room_id": subspace, - "state_key": room.room_id, - "content": {"via": [fed_hostname]}, - } - for room in rooms - ], - ), - ) - return rooms + async def summarize_remote_room_hiearchy(_self, room, suggested_only): + return subspace_room_entry, dict(children_rooms), set() # Add a room to the space which is on another server. self._add_child(self.space, subspace, self.token) @@ -788,6 +813,15 @@ async def summarize_remote_room( ] self._assert_rooms(result, expected) + with mock.patch( + "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy", + new=summarize_remote_room_hiearchy, + ): + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space) + ) + self._assert_hierarchy(result, expected) + def test_fed_invited(self): """ A room which the user was invited to should be included in the response. @@ -802,19 +836,22 @@ def test_fed_invited(self): # Poke an invite over federation into the database. self._poke_fed_invite(fed_room, "@remote:" + fed_hostname) + fed_room_entry = _RoomEntry( + fed_room, + { + "room_id": fed_room, + "world_readable": False, + "join_rules": JoinRules.INVITE, + }, + ) + async def summarize_remote_room( _self, room, suggested_only, max_children, exclude_rooms ): - return [ - _RoomEntry( - fed_room, - { - "room_id": fed_room, - "world_readable": False, - "join_rules": JoinRules.INVITE, - }, - ), - ] + return [fed_room_entry] + + async def summarize_remote_room_hiearchy(_self, room, suggested_only): + return fed_room_entry, {}, set() # Add a room to the space which is on another server. self._add_child(self.space, fed_room, self.token) @@ -833,3 +870,12 @@ async def summarize_remote_room( (fed_room, ()), ] self._assert_rooms(result, expected) + + with mock.patch( + "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy", + new=summarize_remote_room_hiearchy, + ): + result = self.get_success( + self.handler.get_room_hierarchy(self.user, self.space) + ) + self._assert_hierarchy(result, expected) From 2d9ca4ca77c2cdf98ddb738aee8d5699c7c8749f Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 16 Aug 2021 13:19:02 +0100 Subject: [PATCH 566/619] Clean up some logging in the federation event handler (#10591) * Include outlier status in `str(event)` In places where we log event objects, knowing whether or not you're dealing with an outlier is super useful. * Remove duplicated logging in get_missing_events When we process events received from get_missing_events, we log them twice (once in `_get_missing_events_for_pdu`, and once in `on_receive_pdu`). Reduce the duplication by removing the logging in `on_receive_pdu`, and ensuring the call sites do sensible logging. * log in `on_receive_pdu` when we already have the event * Log which prev_events we are missing * changelog --- changelog.d/10591.misc | 1 + synapse/events/__init__.py | 3 +- synapse/federation/federation_server.py | 1 + synapse/handlers/federation.py | 52 ++++++++++++------------- 4 files changed, 28 insertions(+), 29 deletions(-) create mode 100644 changelog.d/10591.misc diff --git a/changelog.d/10591.misc b/changelog.d/10591.misc new file mode 100644 index 0000000000..9a765435db --- /dev/null +++ b/changelog.d/10591.misc @@ -0,0 +1 @@ +Clean up some of the federation event authentication code for clarity. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index 0298af4c02..a730c1719a 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -396,10 +396,11 @@ def __str__(self): return self.__repr__() def __repr__(self): - return "" % ( + return "" % ( self.get("event_id", None), self.get("type", None), self.get("state_key", None), + self.internal_metadata.is_outlier(), ) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 78d5aac6af..afd8f8580a 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -1003,6 +1003,7 @@ async def _process_incoming_pdus_in_room_inner( # has started processing). while True: async with lock: + logger.info("handling received PDU: %s", event) try: await self.handler.on_receive_pdu( origin, event, sent_to_us_directly=True diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 9a5e726533..c0e13bdaac 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -220,8 +220,6 @@ async def on_receive_pdu( room_id = pdu.room_id event_id = pdu.event_id - logger.info("handling received PDU: %s", pdu) - # We reprocess pdus when we have seen them only as outliers existing = await self.store.get_event( event_id, allow_none=True, allow_rejected=True @@ -229,14 +227,19 @@ async def on_receive_pdu( # FIXME: Currently we fetch an event again when we already have it # if it has been marked as an outlier. - - already_seen = existing and ( - not existing.internal_metadata.is_outlier() - or pdu.internal_metadata.is_outlier() - ) - if already_seen: - logger.debug("Already seen pdu") - return + if existing: + if not existing.internal_metadata.is_outlier(): + logger.info( + "Ignoring received event %s which we have already seen", event_id + ) + return + if pdu.internal_metadata.is_outlier(): + logger.info( + "Ignoring received outlier %s which we already have as an outlier", + event_id, + ) + return + logger.info("De-outliering event %s", event_id) # do some initial sanity-checking of the event. In particular, make # sure it doesn't have hundreds of prev_events or auth_events, which @@ -331,7 +334,8 @@ async def on_receive_pdu( "Found all missing prev_events", ) - if prevs - seen: + missing_prevs = prevs - seen + if missing_prevs: # We've still not been able to get all of the prev_events for this event. # # In this case, we need to fall back to asking another server in the @@ -359,8 +363,8 @@ async def on_receive_pdu( if sent_to_us_directly: logger.warning( "Rejecting: failed to fetch %d prev events: %s", - len(prevs - seen), - shortstr(prevs - seen), + len(missing_prevs), + shortstr(missing_prevs), ) raise FederationError( "ERROR", @@ -373,9 +377,10 @@ async def on_receive_pdu( ) logger.info( - "Event %s is missing prev_events: calculating state for a " + "Event %s is missing prev_events %s: calculating state for a " "backwards extremity", event_id, + shortstr(missing_prevs), ) # Calculate the state after each of the previous events, and @@ -393,7 +398,7 @@ async def on_receive_pdu( # Ask the remote server for the states we don't # know about - for p in prevs - seen: + for p in missing_prevs: logger.info("Requesting state after missing prev_event %s", p) with nested_logging_context(p): @@ -556,21 +561,14 @@ async def _get_missing_events_for_pdu( logger.warning("Failed to get prev_events: %s", e) return - logger.info( - "Got %d prev_events: %s", - len(missing_events), - shortstr(missing_events), - ) + logger.info("Got %d prev_events", len(missing_events)) # We want to sort these by depth so we process them and # tell clients about them in order. missing_events.sort(key=lambda x: x.depth) for ev in missing_events: - logger.info( - "Handling received prev_event %s", - ev.event_id, - ) + logger.info("Handling received prev_event %s", ev) with nested_logging_context(ev.event_id): try: await self.on_receive_pdu(origin, ev, sent_to_us_directly=False) @@ -1762,10 +1760,8 @@ async def _handle_queued_pdus( for p, origin in room_queue: try: logger.info( - "Processing queued PDU %s which was received " - "while we were joining %s", - p.event_id, - p.room_id, + "Processing queued PDU %s which was received while we were joining", + p, ) with nested_logging_context(p.event_id): await self.on_receive_pdu(origin, p, sent_to_us_directly=True) From 87b62f8bb23f99d76bf0ee62c8217fa45a087673 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 16 Aug 2021 10:14:31 -0400 Subject: [PATCH 567/619] Split `synapse.federation.transport.server` into multiple files. (#10590) --- changelog.d/10590.misc | 1 + synapse/federation/transport/server.py | 2158 ----------------- .../federation/transport/server/__init__.py | 332 +++ synapse/federation/transport/server/_base.py | 328 +++ .../federation/transport/server/federation.py | 692 ++++++ .../transport/server/groups_local.py | 113 + .../transport/server/groups_server.py | 753 ++++++ 7 files changed, 2219 insertions(+), 2158 deletions(-) create mode 100644 changelog.d/10590.misc delete mode 100644 synapse/federation/transport/server.py create mode 100644 synapse/federation/transport/server/__init__.py create mode 100644 synapse/federation/transport/server/_base.py create mode 100644 synapse/federation/transport/server/federation.py create mode 100644 synapse/federation/transport/server/groups_local.py create mode 100644 synapse/federation/transport/server/groups_server.py diff --git a/changelog.d/10590.misc b/changelog.d/10590.misc new file mode 100644 index 0000000000..62fec717da --- /dev/null +++ b/changelog.d/10590.misc @@ -0,0 +1 @@ +Re-organize the `synapse.federation.transport.server` module to create smaller files. diff --git a/synapse/federation/transport/server.py b/synapse/federation/transport/server.py deleted file mode 100644 index 79a2e1afa0..0000000000 --- a/synapse/federation/transport/server.py +++ /dev/null @@ -1,2158 +0,0 @@ -# Copyright 2014-2021 The Matrix.org Foundation C.I.C. -# Copyright 2020 Sorunome -# -# 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 functools -import logging -import re -from typing import ( - Container, - Dict, - List, - Mapping, - Optional, - Sequence, - Tuple, - Type, - Union, -) - -from typing_extensions import Literal - -import synapse -from synapse.api.constants import MAX_GROUP_CATEGORYID_LENGTH, MAX_GROUP_ROLEID_LENGTH -from synapse.api.errors import Codes, FederationDeniedError, SynapseError -from synapse.api.room_versions import RoomVersions -from synapse.api.urls import ( - FEDERATION_UNSTABLE_PREFIX, - FEDERATION_V1_PREFIX, - FEDERATION_V2_PREFIX, -) -from synapse.handlers.groups_local import GroupsLocalHandler -from synapse.http.server import HttpServer, JsonResource -from synapse.http.servlet import ( - parse_boolean_from_args, - parse_integer_from_args, - parse_json_object_from_request, - parse_string_from_args, - parse_strings_from_args, -) -from synapse.logging import opentracing -from synapse.logging.context import run_in_background -from synapse.logging.opentracing import ( - SynapseTags, - start_active_span, - start_active_span_from_request, - tags, - whitelisted_homeserver, -) -from synapse.server import HomeServer -from synapse.types import JsonDict, ThirdPartyInstanceID, get_domain_from_id -from synapse.util.ratelimitutils import FederationRateLimiter -from synapse.util.stringutils import parse_and_validate_server_name -from synapse.util.versionstring import get_version_string - -logger = logging.getLogger(__name__) - - -class TransportLayerServer(JsonResource): - """Handles incoming federation HTTP requests""" - - def __init__(self, hs: HomeServer, servlet_groups: Optional[List[str]] = None): - """Initialize the TransportLayerServer - - Will by default register all servlets. For custom behaviour, pass in - a list of servlet_groups to register. - - Args: - hs: homeserver - servlet_groups: List of servlet groups to register. - Defaults to ``DEFAULT_SERVLET_GROUPS``. - """ - self.hs = hs - self.clock = hs.get_clock() - self.servlet_groups = servlet_groups - - super().__init__(hs, canonical_json=False) - - self.authenticator = Authenticator(hs) - self.ratelimiter = hs.get_federation_ratelimiter() - - self.register_servlets() - - def register_servlets(self) -> None: - register_servlets( - self.hs, - resource=self, - ratelimiter=self.ratelimiter, - authenticator=self.authenticator, - servlet_groups=self.servlet_groups, - ) - - -class AuthenticationError(SynapseError): - """There was a problem authenticating the request""" - - -class NoAuthenticationError(AuthenticationError): - """The request had no authentication information""" - - -class Authenticator: - def __init__(self, hs: HomeServer): - self._clock = hs.get_clock() - self.keyring = hs.get_keyring() - self.server_name = hs.hostname - self.store = hs.get_datastore() - self.federation_domain_whitelist = hs.config.federation_domain_whitelist - self.notifier = hs.get_notifier() - - self.replication_client = None - if hs.config.worker.worker_app: - self.replication_client = hs.get_tcp_replication() - - # A method just so we can pass 'self' as the authenticator to the Servlets - async def authenticate_request(self, request, content): - now = self._clock.time_msec() - json_request = { - "method": request.method.decode("ascii"), - "uri": request.uri.decode("ascii"), - "destination": self.server_name, - "signatures": {}, - } - - if content is not None: - json_request["content"] = content - - origin = None - - auth_headers = request.requestHeaders.getRawHeaders(b"Authorization") - - if not auth_headers: - raise NoAuthenticationError( - 401, "Missing Authorization headers", Codes.UNAUTHORIZED - ) - - for auth in auth_headers: - if auth.startswith(b"X-Matrix"): - (origin, key, sig) = _parse_auth_header(auth) - json_request["origin"] = origin - json_request["signatures"].setdefault(origin, {})[key] = sig - - if ( - self.federation_domain_whitelist is not None - and origin not in self.federation_domain_whitelist - ): - raise FederationDeniedError(origin) - - if origin is None or not json_request["signatures"]: - raise NoAuthenticationError( - 401, "Missing Authorization headers", Codes.UNAUTHORIZED - ) - - await self.keyring.verify_json_for_server( - origin, - json_request, - now, - ) - - logger.debug("Request from %s", origin) - request.requester = origin - - # If we get a valid signed request from the other side, its probably - # alive - retry_timings = await self.store.get_destination_retry_timings(origin) - if retry_timings and retry_timings.retry_last_ts: - run_in_background(self._reset_retry_timings, origin) - - return origin - - async def _reset_retry_timings(self, origin): - try: - logger.info("Marking origin %r as up", origin) - await self.store.set_destination_retry_timings(origin, None, 0, 0) - - # Inform the relevant places that the remote server is back up. - self.notifier.notify_remote_server_up(origin) - if self.replication_client: - # If we're on a worker we try and inform master about this. The - # replication client doesn't hook into the notifier to avoid - # infinite loops where we send a `REMOTE_SERVER_UP` command to - # master, which then echoes it back to us which in turn pokes - # the notifier. - self.replication_client.send_remote_server_up(origin) - - except Exception: - logger.exception("Error resetting retry timings on %s", origin) - - -def _parse_auth_header(header_bytes): - """Parse an X-Matrix auth header - - Args: - header_bytes (bytes): header value - - Returns: - Tuple[str, str, str]: origin, key id, signature. - - Raises: - AuthenticationError if the header could not be parsed - """ - try: - header_str = header_bytes.decode("utf-8") - params = header_str.split(" ")[1].split(",") - param_dict = dict(kv.split("=") for kv in params) - - def strip_quotes(value): - if value.startswith('"'): - return value[1:-1] - else: - return value - - origin = strip_quotes(param_dict["origin"]) - - # ensure that the origin is a valid server name - parse_and_validate_server_name(origin) - - key = strip_quotes(param_dict["key"]) - sig = strip_quotes(param_dict["sig"]) - return origin, key, sig - except Exception as e: - logger.warning( - "Error parsing auth header '%s': %s", - header_bytes.decode("ascii", "replace"), - e, - ) - raise AuthenticationError( - 400, "Malformed Authorization header", Codes.UNAUTHORIZED - ) - - -class BaseFederationServlet: - """Abstract base class for federation servlet classes. - - The servlet object should have a PATH attribute which takes the form of a regexp to - match against the request path (excluding the /federation/v1 prefix). - - The servlet should also implement one or more of on_GET, on_POST, on_PUT, to match - the appropriate HTTP method. These methods must be *asynchronous* and have the - signature: - - on_(self, origin, content, query, **kwargs) - - With arguments: - - origin (unicode|None): The authenticated server_name of the calling server, - unless REQUIRE_AUTH is set to False and authentication failed. - - content (unicode|None): decoded json body of the request. None if the - request was a GET. - - query (dict[bytes, list[bytes]]): Query params from the request. url-decoded - (ie, '+' and '%xx' are decoded) but note that it is *not* utf8-decoded - yet. - - **kwargs (dict[unicode, unicode]): the dict mapping keys to path - components as specified in the path match regexp. - - Returns: - Optional[Tuple[int, object]]: either (response code, response object) to - return a JSON response, or None if the request has already been handled. - - Raises: - SynapseError: to return an error code - - Exception: other exceptions will be caught, logged, and a 500 will be - returned. - """ - - PATH = "" # Overridden in subclasses, the regex to match against the path. - - REQUIRE_AUTH = True - - PREFIX = FEDERATION_V1_PREFIX # Allows specifying the API version - - RATELIMIT = True # Whether to rate limit requests or not - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - self.hs = hs - self.authenticator = authenticator - self.ratelimiter = ratelimiter - self.server_name = server_name - - def _wrap(self, func): - authenticator = self.authenticator - ratelimiter = self.ratelimiter - - @functools.wraps(func) - async def new_func(request, *args, **kwargs): - """A callback which can be passed to HttpServer.RegisterPaths - - Args: - request (twisted.web.http.Request): - *args: unused? - **kwargs (dict[unicode, unicode]): the dict mapping keys to path - components as specified in the path match regexp. - - Returns: - Tuple[int, object]|None: (response code, response object) as returned by - the callback method. None if the request has already been handled. - """ - content = None - if request.method in [b"PUT", b"POST"]: - # TODO: Handle other method types? other content types? - content = parse_json_object_from_request(request) - - try: - origin = await authenticator.authenticate_request(request, content) - except NoAuthenticationError: - origin = None - if self.REQUIRE_AUTH: - logger.warning( - "authenticate_request failed: missing authentication" - ) - raise - except Exception as e: - logger.warning("authenticate_request failed: %s", e) - raise - - request_tags = { - SynapseTags.REQUEST_ID: request.get_request_id(), - tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER, - tags.HTTP_METHOD: request.get_method(), - tags.HTTP_URL: request.get_redacted_uri(), - tags.PEER_HOST_IPV6: request.getClientIP(), - "authenticated_entity": origin, - "servlet_name": request.request_metrics.name, - } - - # Only accept the span context if the origin is authenticated - # and whitelisted - if origin and whitelisted_homeserver(origin): - scope = start_active_span_from_request( - request, "incoming-federation-request", tags=request_tags - ) - else: - scope = start_active_span( - "incoming-federation-request", tags=request_tags - ) - - with scope: - opentracing.inject_response_headers(request.responseHeaders) - - if origin and self.RATELIMIT: - with ratelimiter.ratelimit(origin) as d: - await d - if request._disconnected: - logger.warning( - "client disconnected before we started processing " - "request" - ) - return -1, None - response = await func( - origin, content, request.args, *args, **kwargs - ) - else: - response = await func( - origin, content, request.args, *args, **kwargs - ) - - return response - - return new_func - - def register(self, server): - pattern = re.compile("^" + self.PREFIX + self.PATH + "$") - - for method in ("GET", "PUT", "POST"): - code = getattr(self, "on_%s" % (method), None) - if code is None: - continue - - server.register_paths( - method, - (pattern,), - self._wrap(code), - self.__class__.__name__, - ) - - -class BaseFederationServerServlet(BaseFederationServlet): - """Abstract base class for federation servlet classes which provides a federation server handler. - - See BaseFederationServlet for more information. - """ - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_federation_server() - - -class FederationSendServlet(BaseFederationServerServlet): - PATH = "/send/(?P[^/]*)/?" - - # We ratelimit manually in the handler as we queue up the requests and we - # don't want to fill up the ratelimiter with blocked requests. - RATELIMIT = False - - # This is when someone is trying to send us a bunch of data. - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - transaction_id: str, - ) -> Tuple[int, JsonDict]: - """Called on PUT /send// - - Args: - transaction_id: The transaction_id associated with this request. This - is *not* None. - - Returns: - Tuple of `(code, response)`, where - `response` is a python dict to be converted into JSON that is - used as the response body. - """ - # Parse the request - try: - transaction_data = content - - logger.debug("Decoded %s: %s", transaction_id, str(transaction_data)) - - logger.info( - "Received txn %s from %s. (PDUs: %d, EDUs: %d)", - transaction_id, - origin, - len(transaction_data.get("pdus", [])), - len(transaction_data.get("edus", [])), - ) - - except Exception as e: - logger.exception(e) - return 400, {"error": "Invalid transaction"} - - code, response = await self.handler.on_incoming_transaction( - origin, transaction_id, self.server_name, transaction_data - ) - - return code, response - - -class FederationEventServlet(BaseFederationServerServlet): - PATH = "/event/(?P[^/]*)/?" - - # This is when someone asks for a data item for a given server data_id pair. - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - event_id: str, - ) -> Tuple[int, Union[JsonDict, str]]: - return await self.handler.on_pdu_request(origin, event_id) - - -class FederationStateV1Servlet(BaseFederationServerServlet): - PATH = "/state/(?P[^/]*)/?" - - # This is when someone asks for all data for a given room. - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - return await self.handler.on_room_state_request( - origin, - room_id, - parse_string_from_args(query, "event_id", None, required=False), - ) - - -class FederationStateIdsServlet(BaseFederationServerServlet): - PATH = "/state_ids/(?P[^/]*)/?" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - return await self.handler.on_state_ids_request( - origin, - room_id, - parse_string_from_args(query, "event_id", None, required=True), - ) - - -class FederationBackfillServlet(BaseFederationServerServlet): - PATH = "/backfill/(?P[^/]*)/?" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - versions = [x.decode("ascii") for x in query[b"v"]] - limit = parse_integer_from_args(query, "limit", None) - - if not limit: - return 400, {"error": "Did not include limit param"} - - return await self.handler.on_backfill_request(origin, room_id, versions, limit) - - -class FederationQueryServlet(BaseFederationServerServlet): - PATH = "/query/(?P[^/]*)" - - # This is when we receive a server-server Query - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - query_type: str, - ) -> Tuple[int, JsonDict]: - args = {k.decode("utf8"): v[0].decode("utf-8") for k, v in query.items()} - args["origin"] = origin - return await self.handler.on_query_request(query_type, args) - - -class FederationMakeJoinServlet(BaseFederationServerServlet): - PATH = "/make_join/(?P[^/]*)/(?P[^/]*)" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - """ - Args: - origin: The authenticated server_name of the calling server - - content: (GETs don't have bodies) - - query: Query params from the request. - - **kwargs: the dict mapping keys to path components as specified in - the path match regexp. - - Returns: - Tuple of (response code, response object) - """ - supported_versions = parse_strings_from_args(query, "ver", encoding="utf-8") - if supported_versions is None: - supported_versions = ["1"] - - result = await self.handler.on_make_join_request( - origin, room_id, user_id, supported_versions=supported_versions - ) - return 200, result - - -class FederationMakeLeaveServlet(BaseFederationServerServlet): - PATH = "/make_leave/(?P[^/]*)/(?P[^/]*)" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - result = await self.handler.on_make_leave_request(origin, room_id, user_id) - return 200, result - - -class FederationV1SendLeaveServlet(BaseFederationServerServlet): - PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, Tuple[int, JsonDict]]: - result = await self.handler.on_send_leave_request(origin, content, room_id) - return 200, (200, result) - - -class FederationV2SendLeaveServlet(BaseFederationServerServlet): - PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" - - PREFIX = FEDERATION_V2_PREFIX - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, JsonDict]: - result = await self.handler.on_send_leave_request(origin, content, room_id) - return 200, result - - -class FederationMakeKnockServlet(BaseFederationServerServlet): - PATH = "/make_knock/(?P[^/]*)/(?P[^/]*)" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - # Retrieve the room versions the remote homeserver claims to support - supported_versions = parse_strings_from_args( - query, "ver", required=True, encoding="utf-8" - ) - - result = await self.handler.on_make_knock_request( - origin, room_id, user_id, supported_versions=supported_versions - ) - return 200, result - - -class FederationV1SendKnockServlet(BaseFederationServerServlet): - PATH = "/send_knock/(?P[^/]*)/(?P[^/]*)" - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, JsonDict]: - result = await self.handler.on_send_knock_request(origin, content, room_id) - return 200, result - - -class FederationEventAuthServlet(BaseFederationServerServlet): - PATH = "/event_auth/(?P[^/]*)/(?P[^/]*)" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, JsonDict]: - return await self.handler.on_event_auth(origin, room_id, event_id) - - -class FederationV1SendJoinServlet(BaseFederationServerServlet): - PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, Tuple[int, JsonDict]]: - # TODO(paul): assert that event_id parsed from path actually - # match those given in content - result = await self.handler.on_send_join_request(origin, content, room_id) - return 200, (200, result) - - -class FederationV2SendJoinServlet(BaseFederationServerServlet): - PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" - - PREFIX = FEDERATION_V2_PREFIX - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, JsonDict]: - # TODO(paul): assert that event_id parsed from path actually - # match those given in content - result = await self.handler.on_send_join_request(origin, content, room_id) - return 200, result - - -class FederationV1InviteServlet(BaseFederationServerServlet): - PATH = "/invite/(?P[^/]*)/(?P[^/]*)" - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, Tuple[int, JsonDict]]: - # We don't get a room version, so we have to assume its EITHER v1 or - # v2. This is "fine" as the only difference between V1 and V2 is the - # state resolution algorithm, and we don't use that for processing - # invites - result = await self.handler.on_invite_request( - origin, content, room_version_id=RoomVersions.V1.identifier - ) - - # V1 federation API is defined to return a content of `[200, {...}]` - # due to a historical bug. - return 200, (200, result) - - -class FederationV2InviteServlet(BaseFederationServerServlet): - PATH = "/invite/(?P[^/]*)/(?P[^/]*)" - - PREFIX = FEDERATION_V2_PREFIX - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - event_id: str, - ) -> Tuple[int, JsonDict]: - # TODO(paul): assert that room_id/event_id parsed from path actually - # match those given in content - - room_version = content["room_version"] - event = content["event"] - invite_room_state = content["invite_room_state"] - - # Synapse expects invite_room_state to be in unsigned, as it is in v1 - # API - - event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state - - result = await self.handler.on_invite_request( - origin, event, room_version_id=room_version - ) - return 200, result - - -class FederationThirdPartyInviteExchangeServlet(BaseFederationServerServlet): - PATH = "/exchange_third_party_invite/(?P[^/]*)" - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - await self.handler.on_exchange_third_party_invite_request(content) - return 200, {} - - -class FederationClientKeysQueryServlet(BaseFederationServerServlet): - PATH = "/user/keys/query" - - async def on_POST( - self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] - ) -> Tuple[int, JsonDict]: - return await self.handler.on_query_client_keys(origin, content) - - -class FederationUserDevicesQueryServlet(BaseFederationServerServlet): - PATH = "/user/devices/(?P[^/]*)" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - user_id: str, - ) -> Tuple[int, JsonDict]: - return await self.handler.on_query_user_devices(origin, user_id) - - -class FederationClientKeysClaimServlet(BaseFederationServerServlet): - PATH = "/user/keys/claim" - - async def on_POST( - self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] - ) -> Tuple[int, JsonDict]: - response = await self.handler.on_claim_client_keys(origin, content) - return 200, response - - -class FederationGetMissingEventsServlet(BaseFederationServerServlet): - # TODO(paul): Why does this path alone end with "/?" optional? - PATH = "/get_missing_events/(?P[^/]*)/?" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - limit = int(content.get("limit", 10)) - earliest_events = content.get("earliest_events", []) - latest_events = content.get("latest_events", []) - - result = await self.handler.on_get_missing_events( - origin, - room_id=room_id, - earliest_events=earliest_events, - latest_events=latest_events, - limit=limit, - ) - - return 200, result - - -class On3pidBindServlet(BaseFederationServerServlet): - PATH = "/3pid/onbind" - - REQUIRE_AUTH = False - - async def on_POST( - self, origin: Optional[str], content: JsonDict, query: Dict[bytes, List[bytes]] - ) -> Tuple[int, JsonDict]: - if "invites" in content: - last_exception = None - for invite in content["invites"]: - try: - if "signed" not in invite or "token" not in invite["signed"]: - message = ( - "Rejecting received notification of third-" - "party invite without signed: %s" % (invite,) - ) - logger.info(message) - raise SynapseError(400, message) - await self.handler.exchange_third_party_invite( - invite["sender"], - invite["mxid"], - invite["room_id"], - invite["signed"], - ) - except Exception as e: - last_exception = e - if last_exception: - raise last_exception - return 200, {} - - -class OpenIdUserInfo(BaseFederationServerServlet): - """ - Exchange a bearer token for information about a user. - - The response format should be compatible with: - http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse - - GET /openid/userinfo?access_token=ABDEFGH HTTP/1.1 - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "sub": "@userpart:example.org", - } - """ - - PATH = "/openid/userinfo" - - REQUIRE_AUTH = False - - async def on_GET( - self, - origin: Optional[str], - content: Literal[None], - query: Dict[bytes, List[bytes]], - ) -> Tuple[int, JsonDict]: - token = parse_string_from_args(query, "access_token") - if token is None: - return ( - 401, - {"errcode": "M_MISSING_TOKEN", "error": "Access Token required"}, - ) - - user_id = await self.handler.on_openid_userinfo(token) - - if user_id is None: - return ( - 401, - { - "errcode": "M_UNKNOWN_TOKEN", - "error": "Access Token unknown or expired", - }, - ) - - return 200, {"sub": user_id} - - -class PublicRoomList(BaseFederationServlet): - """ - Fetch the public room list for this server. - - This API returns information in the same format as /publicRooms on the - client API, but will only ever include local public rooms and hence is - intended for consumption by other homeservers. - - GET /publicRooms HTTP/1.1 - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "chunk": [ - { - "aliases": [ - "#test:localhost" - ], - "guest_can_join": false, - "name": "test room", - "num_joined_members": 3, - "room_id": "!whkydVegtvatLfXmPN:localhost", - "world_readable": false - } - ], - "end": "END", - "start": "START" - } - """ - - PATH = "/publicRooms" - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - allow_access: bool, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_room_list_handler() - self.allow_access = allow_access - - async def on_GET( - self, origin: str, content: Literal[None], query: Dict[bytes, List[bytes]] - ) -> Tuple[int, JsonDict]: - if not self.allow_access: - raise FederationDeniedError(origin) - - limit = parse_integer_from_args(query, "limit", 0) - since_token = parse_string_from_args(query, "since", None) - include_all_networks = parse_boolean_from_args( - query, "include_all_networks", default=False - ) - third_party_instance_id = parse_string_from_args( - query, "third_party_instance_id", None - ) - - if include_all_networks: - network_tuple = None - elif third_party_instance_id: - network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) - else: - network_tuple = ThirdPartyInstanceID(None, None) - - if limit == 0: - # zero is a special value which corresponds to no limit. - limit = None - - data = await self.handler.get_local_public_room_list( - limit, since_token, network_tuple=network_tuple, from_federation=True - ) - return 200, data - - async def on_POST( - self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] - ) -> Tuple[int, JsonDict]: - # This implements MSC2197 (Search Filtering over Federation) - if not self.allow_access: - raise FederationDeniedError(origin) - - limit: Optional[int] = int(content.get("limit", 100)) - since_token = content.get("since", None) - search_filter = content.get("filter", None) - - include_all_networks = content.get("include_all_networks", False) - third_party_instance_id = content.get("third_party_instance_id", None) - - if include_all_networks: - network_tuple = None - if third_party_instance_id is not None: - raise SynapseError( - 400, "Can't use include_all_networks with an explicit network" - ) - elif third_party_instance_id is None: - network_tuple = ThirdPartyInstanceID(None, None) - else: - network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) - - if search_filter is None: - logger.warning("Nonefilter") - - if limit == 0: - # zero is a special value which corresponds to no limit. - limit = None - - data = await self.handler.get_local_public_room_list( - limit=limit, - since_token=since_token, - search_filter=search_filter, - network_tuple=network_tuple, - from_federation=True, - ) - - return 200, data - - -class FederationVersionServlet(BaseFederationServlet): - PATH = "/version" - - REQUIRE_AUTH = False - - async def on_GET( - self, - origin: Optional[str], - content: Literal[None], - query: Dict[bytes, List[bytes]], - ) -> Tuple[int, JsonDict]: - return ( - 200, - {"server": {"name": "Synapse", "version": get_version_string(synapse)}}, - ) - - -class BaseGroupsServerServlet(BaseFederationServlet): - """Abstract base class for federation servlet classes which provides a groups server handler. - - See BaseFederationServlet for more information. - """ - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_groups_server_handler() - - -class FederationGroupsProfileServlet(BaseGroupsServerServlet): - """Get/set the basic profile of a group on behalf of a user""" - - PATH = "/groups/(?P[^/]*)/profile" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.get_group_profile(group_id, requester_user_id) - - return 200, new_content - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.update_group_profile( - group_id, requester_user_id, content - ) - - return 200, new_content - - -class FederationGroupsSummaryServlet(BaseGroupsServerServlet): - PATH = "/groups/(?P[^/]*)/summary" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.get_group_summary(group_id, requester_user_id) - - return 200, new_content - - -class FederationGroupsRoomsServlet(BaseGroupsServerServlet): - """Get the rooms in a group on behalf of a user""" - - PATH = "/groups/(?P[^/]*)/rooms" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.get_rooms_in_group(group_id, requester_user_id) - - return 200, new_content - - -class FederationGroupsAddRoomsServlet(BaseGroupsServerServlet): - """Add/remove room from group""" - - PATH = "/groups/(?P[^/]*)/room/(?P[^/]*)" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - room_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.add_room_to_group( - group_id, requester_user_id, room_id, content - ) - - return 200, new_content - - async def on_DELETE( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - room_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.remove_room_from_group( - group_id, requester_user_id, room_id - ) - - return 200, new_content - - -class FederationGroupsAddRoomsConfigServlet(BaseGroupsServerServlet): - """Update room config in group""" - - PATH = ( - "/groups/(?P[^/]*)/room/(?P[^/]*)" - "/config/(?P[^/]*)" - ) - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - room_id: str, - config_key: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - result = await self.handler.update_room_in_group( - group_id, requester_user_id, room_id, config_key, content - ) - - return 200, result - - -class FederationGroupsUsersServlet(BaseGroupsServerServlet): - """Get the users in a group on behalf of a user""" - - PATH = "/groups/(?P[^/]*)/users" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.get_users_in_group(group_id, requester_user_id) - - return 200, new_content - - -class FederationGroupsInvitedUsersServlet(BaseGroupsServerServlet): - """Get the users that have been invited to a group""" - - PATH = "/groups/(?P[^/]*)/invited_users" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.get_invited_users_in_group( - group_id, requester_user_id - ) - - return 200, new_content - - -class FederationGroupsInviteServlet(BaseGroupsServerServlet): - """Ask a group server to invite someone to the group""" - - PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/invite" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.invite_to_group( - group_id, user_id, requester_user_id, content - ) - - return 200, new_content - - -class FederationGroupsAcceptInviteServlet(BaseGroupsServerServlet): - """Accept an invitation from the group server""" - - PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/accept_invite" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - if get_domain_from_id(user_id) != origin: - raise SynapseError(403, "user_id doesn't match origin") - - new_content = await self.handler.accept_invite(group_id, user_id, content) - - return 200, new_content - - -class FederationGroupsJoinServlet(BaseGroupsServerServlet): - """Attempt to join a group""" - - PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/join" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - if get_domain_from_id(user_id) != origin: - raise SynapseError(403, "user_id doesn't match origin") - - new_content = await self.handler.join_group(group_id, user_id, content) - - return 200, new_content - - -class FederationGroupsRemoveUserServlet(BaseGroupsServerServlet): - """Leave or kick a user from the group""" - - PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/remove" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.remove_user_from_group( - group_id, user_id, requester_user_id, content - ) - - return 200, new_content - - -class BaseGroupsLocalServlet(BaseFederationServlet): - """Abstract base class for federation servlet classes which provides a groups local handler. - - See BaseFederationServlet for more information. - """ - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_groups_local_handler() - - -class FederationGroupsLocalInviteServlet(BaseGroupsLocalServlet): - """A group server has invited a local user""" - - PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/invite" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - if get_domain_from_id(group_id) != origin: - raise SynapseError(403, "group_id doesn't match origin") - - assert isinstance( - self.handler, GroupsLocalHandler - ), "Workers cannot handle group invites." - - new_content = await self.handler.on_invite(group_id, user_id, content) - - return 200, new_content - - -class FederationGroupsRemoveLocalUserServlet(BaseGroupsLocalServlet): - """A group server has removed a local user""" - - PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/remove" - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - user_id: str, - ) -> Tuple[int, None]: - if get_domain_from_id(group_id) != origin: - raise SynapseError(403, "user_id doesn't match origin") - - assert isinstance( - self.handler, GroupsLocalHandler - ), "Workers cannot handle group removals." - - await self.handler.user_removed_from_group(group_id, user_id, content) - - return 200, None - - -class FederationGroupsRenewAttestaionServlet(BaseFederationServlet): - """A group or user's server renews their attestation""" - - PATH = "/groups/(?P[^/]*)/renew_attestation/(?P[^/]*)" - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_groups_attestation_renewer() - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - # We don't need to check auth here as we check the attestation signatures - - new_content = await self.handler.on_renew_attestation( - group_id, user_id, content - ) - - return 200, new_content - - -class FederationGroupsSummaryRoomsServlet(BaseGroupsServerServlet): - """Add/remove a room from the group summary, with optional category. - - Matches both: - - /groups/:group/summary/rooms/:room_id - - /groups/:group/summary/categories/:category/rooms/:room_id - """ - - PATH = ( - "/groups/(?P[^/]*)/summary" - "(/categories/(?P[^/]+))?" - "/rooms/(?P[^/]*)" - ) - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - category_id: str, - room_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if category_id == "": - raise SynapseError( - 400, "category_id cannot be empty string", Codes.INVALID_PARAM - ) - - if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: - raise SynapseError( - 400, - "category_id may not be longer than %s characters" - % (MAX_GROUP_CATEGORYID_LENGTH,), - Codes.INVALID_PARAM, - ) - - resp = await self.handler.update_group_summary_room( - group_id, - requester_user_id, - room_id=room_id, - category_id=category_id, - content=content, - ) - - return 200, resp - - async def on_DELETE( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - category_id: str, - room_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if category_id == "": - raise SynapseError(400, "category_id cannot be empty string") - - resp = await self.handler.delete_group_summary_room( - group_id, requester_user_id, room_id=room_id, category_id=category_id - ) - - return 200, resp - - -class FederationGroupsCategoriesServlet(BaseGroupsServerServlet): - """Get all categories for a group""" - - PATH = "/groups/(?P[^/]*)/categories/?" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - resp = await self.handler.get_group_categories(group_id, requester_user_id) - - return 200, resp - - -class FederationGroupsCategoryServlet(BaseGroupsServerServlet): - """Add/remove/get a category in a group""" - - PATH = "/groups/(?P[^/]*)/categories/(?P[^/]+)" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - category_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - resp = await self.handler.get_group_category( - group_id, requester_user_id, category_id - ) - - return 200, resp - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - category_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if category_id == "": - raise SynapseError(400, "category_id cannot be empty string") - - if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: - raise SynapseError( - 400, - "category_id may not be longer than %s characters" - % (MAX_GROUP_CATEGORYID_LENGTH,), - Codes.INVALID_PARAM, - ) - - resp = await self.handler.upsert_group_category( - group_id, requester_user_id, category_id, content - ) - - return 200, resp - - async def on_DELETE( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - category_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if category_id == "": - raise SynapseError(400, "category_id cannot be empty string") - - resp = await self.handler.delete_group_category( - group_id, requester_user_id, category_id - ) - - return 200, resp - - -class FederationGroupsRolesServlet(BaseGroupsServerServlet): - """Get roles in a group""" - - PATH = "/groups/(?P[^/]*)/roles/?" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - resp = await self.handler.get_group_roles(group_id, requester_user_id) - - return 200, resp - - -class FederationGroupsRoleServlet(BaseGroupsServerServlet): - """Add/remove/get a role in a group""" - - PATH = "/groups/(?P[^/]*)/roles/(?P[^/]+)" - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - role_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - resp = await self.handler.get_group_role(group_id, requester_user_id, role_id) - - return 200, resp - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - role_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if role_id == "": - raise SynapseError( - 400, "role_id cannot be empty string", Codes.INVALID_PARAM - ) - - if len(role_id) > MAX_GROUP_ROLEID_LENGTH: - raise SynapseError( - 400, - "role_id may not be longer than %s characters" - % (MAX_GROUP_ROLEID_LENGTH,), - Codes.INVALID_PARAM, - ) - - resp = await self.handler.update_group_role( - group_id, requester_user_id, role_id, content - ) - - return 200, resp - - async def on_DELETE( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - role_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if role_id == "": - raise SynapseError(400, "role_id cannot be empty string") - - resp = await self.handler.delete_group_role( - group_id, requester_user_id, role_id - ) - - return 200, resp - - -class FederationGroupsSummaryUsersServlet(BaseGroupsServerServlet): - """Add/remove a user from the group summary, with optional role. - - Matches both: - - /groups/:group/summary/users/:user_id - - /groups/:group/summary/roles/:role/users/:user_id - """ - - PATH = ( - "/groups/(?P[^/]*)/summary" - "(/roles/(?P[^/]+))?" - "/users/(?P[^/]*)" - ) - - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - role_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if role_id == "": - raise SynapseError(400, "role_id cannot be empty string") - - if len(role_id) > MAX_GROUP_ROLEID_LENGTH: - raise SynapseError( - 400, - "role_id may not be longer than %s characters" - % (MAX_GROUP_ROLEID_LENGTH,), - Codes.INVALID_PARAM, - ) - - resp = await self.handler.update_group_summary_user( - group_id, - requester_user_id, - user_id=user_id, - role_id=role_id, - content=content, - ) - - return 200, resp - - async def on_DELETE( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - group_id: str, - role_id: str, - user_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - if role_id == "": - raise SynapseError(400, "role_id cannot be empty string") - - resp = await self.handler.delete_group_summary_user( - group_id, requester_user_id, user_id=user_id, role_id=role_id - ) - - return 200, resp - - -class FederationGroupsBulkPublicisedServlet(BaseGroupsLocalServlet): - """Get roles in a group""" - - PATH = "/get_groups_publicised" - - async def on_POST( - self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] - ) -> Tuple[int, JsonDict]: - resp = await self.handler.bulk_get_publicised_groups( - content["user_ids"], proxy=False - ) - - return 200, resp - - -class FederationGroupsSettingJoinPolicyServlet(BaseGroupsServerServlet): - """Sets whether a group is joinable without an invite or knock""" - - PATH = "/groups/(?P[^/]*)/settings/m.join_policy" - - async def on_PUT( - self, - origin: str, - content: JsonDict, - query: Dict[bytes, List[bytes]], - group_id: str, - ) -> Tuple[int, JsonDict]: - requester_user_id = parse_string_from_args( - query, "requester_user_id", required=True - ) - if get_domain_from_id(requester_user_id) != origin: - raise SynapseError(403, "requester_user_id doesn't match origin") - - new_content = await self.handler.set_group_join_policy( - group_id, requester_user_id, content - ) - - return 200, new_content - - -class FederationSpaceSummaryServlet(BaseFederationServlet): - PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" - PATH = "/spaces/(?P[^/]*)" - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_space_summary_handler() - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Mapping[bytes, Sequence[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) - max_rooms_per_space = parse_integer_from_args(query, "max_rooms_per_space") - - exclude_rooms = parse_strings_from_args(query, "exclude_rooms", default=[]) - - return 200, await self.handler.federation_space_summary( - origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms - ) - - # TODO When switching to the stable endpoint, remove the POST handler. - async def on_POST( - self, - origin: str, - content: JsonDict, - query: Mapping[bytes, Sequence[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - suggested_only = content.get("suggested_only", False) - if not isinstance(suggested_only, bool): - raise SynapseError( - 400, "'suggested_only' must be a boolean", Codes.BAD_JSON - ) - - exclude_rooms = content.get("exclude_rooms", []) - if not isinstance(exclude_rooms, list) or any( - not isinstance(x, str) for x in exclude_rooms - ): - raise SynapseError(400, "bad value for 'exclude_rooms'", Codes.BAD_JSON) - - max_rooms_per_space = content.get("max_rooms_per_space") - if max_rooms_per_space is not None and not isinstance(max_rooms_per_space, int): - raise SynapseError( - 400, "bad value for 'max_rooms_per_space'", Codes.BAD_JSON - ) - - return 200, await self.handler.federation_space_summary( - origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms - ) - - -class FederationRoomHierarchyServlet(BaseFederationServlet): - PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" - PATH = "/hierarchy/(?P[^/]*)" - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_space_summary_handler() - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Mapping[bytes, Sequence[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) - return 200, await self.handler.get_federation_hierarchy( - origin, room_id, suggested_only - ) - - -class RoomComplexityServlet(BaseFederationServlet): - """ - Indicates to other servers how complex (and therefore likely - resource-intensive) a public room this server knows about is. - """ - - PATH = "/rooms/(?P[^/]*)/complexity" - PREFIX = FEDERATION_UNSTABLE_PREFIX - - def __init__( - self, - hs: HomeServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - server_name: str, - ): - super().__init__(hs, authenticator, ratelimiter, server_name) - self._store = self.hs.get_datastore() - - async def on_GET( - self, - origin: str, - content: Literal[None], - query: Dict[bytes, List[bytes]], - room_id: str, - ) -> Tuple[int, JsonDict]: - is_public = await self._store.is_room_world_readable_or_publicly_joinable( - room_id - ) - - if not is_public: - raise SynapseError(404, "Room not found", errcode=Codes.INVALID_PARAM) - - complexity = await self._store.get_room_complexity(room_id) - return 200, complexity - - -FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( - FederationSendServlet, - FederationEventServlet, - FederationStateV1Servlet, - FederationStateIdsServlet, - FederationBackfillServlet, - FederationQueryServlet, - FederationMakeJoinServlet, - FederationMakeLeaveServlet, - FederationEventServlet, - FederationV1SendJoinServlet, - FederationV2SendJoinServlet, - FederationV1SendLeaveServlet, - FederationV2SendLeaveServlet, - FederationV1InviteServlet, - FederationV2InviteServlet, - FederationGetMissingEventsServlet, - FederationEventAuthServlet, - FederationClientKeysQueryServlet, - FederationUserDevicesQueryServlet, - FederationClientKeysClaimServlet, - FederationThirdPartyInviteExchangeServlet, - On3pidBindServlet, - FederationVersionServlet, - RoomComplexityServlet, - FederationSpaceSummaryServlet, - FederationRoomHierarchyServlet, - FederationV1SendKnockServlet, - FederationMakeKnockServlet, -) - -OPENID_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = (OpenIdUserInfo,) - -ROOM_LIST_CLASSES: Tuple[Type[PublicRoomList], ...] = (PublicRoomList,) - -GROUP_SERVER_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( - FederationGroupsProfileServlet, - FederationGroupsSummaryServlet, - FederationGroupsRoomsServlet, - FederationGroupsUsersServlet, - FederationGroupsInvitedUsersServlet, - FederationGroupsInviteServlet, - FederationGroupsAcceptInviteServlet, - FederationGroupsJoinServlet, - FederationGroupsRemoveUserServlet, - FederationGroupsSummaryRoomsServlet, - FederationGroupsCategoriesServlet, - FederationGroupsCategoryServlet, - FederationGroupsRolesServlet, - FederationGroupsRoleServlet, - FederationGroupsSummaryUsersServlet, - FederationGroupsAddRoomsServlet, - FederationGroupsAddRoomsConfigServlet, - FederationGroupsSettingJoinPolicyServlet, -) - - -GROUP_LOCAL_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( - FederationGroupsLocalInviteServlet, - FederationGroupsRemoveLocalUserServlet, - FederationGroupsBulkPublicisedServlet, -) - - -GROUP_ATTESTATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( - FederationGroupsRenewAttestaionServlet, -) - - -DEFAULT_SERVLET_GROUPS = ( - "federation", - "room_list", - "group_server", - "group_local", - "group_attestation", - "openid", -) - - -def register_servlets( - hs: HomeServer, - resource: HttpServer, - authenticator: Authenticator, - ratelimiter: FederationRateLimiter, - servlet_groups: Optional[Container[str]] = None, -): - """Initialize and register servlet classes. - - Will by default register all servlets. For custom behaviour, pass in - a list of servlet_groups to register. - - Args: - hs: homeserver - resource: resource class to register to - authenticator: authenticator to use - ratelimiter: ratelimiter to use - servlet_groups: List of servlet groups to register. - Defaults to ``DEFAULT_SERVLET_GROUPS``. - """ - if not servlet_groups: - servlet_groups = DEFAULT_SERVLET_GROUPS - - if "federation" in servlet_groups: - for servletclass in FEDERATION_SERVLET_CLASSES: - servletclass( - hs=hs, - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) - - if "openid" in servlet_groups: - for servletclass in OPENID_SERVLET_CLASSES: - servletclass( - hs=hs, - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) - - if "room_list" in servlet_groups: - for servletclass in ROOM_LIST_CLASSES: - servletclass( - hs=hs, - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - allow_access=hs.config.allow_public_rooms_over_federation, - ).register(resource) - - if "group_server" in servlet_groups: - for servletclass in GROUP_SERVER_SERVLET_CLASSES: - servletclass( - hs=hs, - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) - - if "group_local" in servlet_groups: - for servletclass in GROUP_LOCAL_SERVLET_CLASSES: - servletclass( - hs=hs, - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) - - if "group_attestation" in servlet_groups: - for servletclass in GROUP_ATTESTATION_SERVLET_CLASSES: - servletclass( - hs=hs, - authenticator=authenticator, - ratelimiter=ratelimiter, - server_name=hs.hostname, - ).register(resource) diff --git a/synapse/federation/transport/server/__init__.py b/synapse/federation/transport/server/__init__.py new file mode 100644 index 0000000000..95176ba6f9 --- /dev/null +++ b/synapse/federation/transport/server/__init__.py @@ -0,0 +1,332 @@ +# Copyright 2014-2021 The Matrix.org Foundation C.I.C. +# Copyright 2020 Sorunome +# +# 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 Dict, Iterable, List, Optional, Tuple, Type + +from typing_extensions import Literal + +from synapse.api.errors import FederationDeniedError, SynapseError +from synapse.federation.transport.server._base import ( + Authenticator, + BaseFederationServlet, +) +from synapse.federation.transport.server.federation import FEDERATION_SERVLET_CLASSES +from synapse.federation.transport.server.groups_local import GROUP_LOCAL_SERVLET_CLASSES +from synapse.federation.transport.server.groups_server import ( + GROUP_SERVER_SERVLET_CLASSES, +) +from synapse.http.server import HttpServer, JsonResource +from synapse.http.servlet import ( + parse_boolean_from_args, + parse_integer_from_args, + parse_string_from_args, +) +from synapse.server import HomeServer +from synapse.types import JsonDict, ThirdPartyInstanceID +from synapse.util.ratelimitutils import FederationRateLimiter + +logger = logging.getLogger(__name__) + + +class TransportLayerServer(JsonResource): + """Handles incoming federation HTTP requests""" + + def __init__(self, hs: HomeServer, servlet_groups: Optional[List[str]] = None): + """Initialize the TransportLayerServer + + Will by default register all servlets. For custom behaviour, pass in + a list of servlet_groups to register. + + Args: + hs: homeserver + servlet_groups: List of servlet groups to register. + Defaults to ``DEFAULT_SERVLET_GROUPS``. + """ + self.hs = hs + self.clock = hs.get_clock() + self.servlet_groups = servlet_groups + + super().__init__(hs, canonical_json=False) + + self.authenticator = Authenticator(hs) + self.ratelimiter = hs.get_federation_ratelimiter() + + self.register_servlets() + + def register_servlets(self) -> None: + register_servlets( + self.hs, + resource=self, + ratelimiter=self.ratelimiter, + authenticator=self.authenticator, + servlet_groups=self.servlet_groups, + ) + + +class PublicRoomList(BaseFederationServlet): + """ + Fetch the public room list for this server. + + This API returns information in the same format as /publicRooms on the + client API, but will only ever include local public rooms and hence is + intended for consumption by other homeservers. + + GET /publicRooms HTTP/1.1 + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "chunk": [ + { + "aliases": [ + "#test:localhost" + ], + "guest_can_join": false, + "name": "test room", + "num_joined_members": 3, + "room_id": "!whkydVegtvatLfXmPN:localhost", + "world_readable": false + } + ], + "end": "END", + "start": "START" + } + """ + + PATH = "/publicRooms" + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_room_list_handler() + self.allow_access = hs.config.allow_public_rooms_over_federation + + async def on_GET( + self, origin: str, content: Literal[None], query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: + if not self.allow_access: + raise FederationDeniedError(origin) + + limit = parse_integer_from_args(query, "limit", 0) + since_token = parse_string_from_args(query, "since", None) + include_all_networks = parse_boolean_from_args( + query, "include_all_networks", default=False + ) + third_party_instance_id = parse_string_from_args( + query, "third_party_instance_id", None + ) + + if include_all_networks: + network_tuple = None + elif third_party_instance_id: + network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) + else: + network_tuple = ThirdPartyInstanceID(None, None) + + if limit == 0: + # zero is a special value which corresponds to no limit. + limit = None + + data = await self.handler.get_local_public_room_list( + limit, since_token, network_tuple=network_tuple, from_federation=True + ) + return 200, data + + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: + # This implements MSC2197 (Search Filtering over Federation) + if not self.allow_access: + raise FederationDeniedError(origin) + + limit: Optional[int] = int(content.get("limit", 100)) + since_token = content.get("since", None) + search_filter = content.get("filter", None) + + include_all_networks = content.get("include_all_networks", False) + third_party_instance_id = content.get("third_party_instance_id", None) + + if include_all_networks: + network_tuple = None + if third_party_instance_id is not None: + raise SynapseError( + 400, "Can't use include_all_networks with an explicit network" + ) + elif third_party_instance_id is None: + network_tuple = ThirdPartyInstanceID(None, None) + else: + network_tuple = ThirdPartyInstanceID.from_string(third_party_instance_id) + + if search_filter is None: + logger.warning("Nonefilter") + + if limit == 0: + # zero is a special value which corresponds to no limit. + limit = None + + data = await self.handler.get_local_public_room_list( + limit=limit, + since_token=since_token, + search_filter=search_filter, + network_tuple=network_tuple, + from_federation=True, + ) + + return 200, data + + +class FederationGroupsRenewAttestaionServlet(BaseFederationServlet): + """A group or user's server renews their attestation""" + + PATH = "/groups/(?P[^/]*)/renew_attestation/(?P[^/]*)" + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_groups_attestation_renewer() + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + # We don't need to check auth here as we check the attestation signatures + + new_content = await self.handler.on_renew_attestation( + group_id, user_id, content + ) + + return 200, new_content + + +class OpenIdUserInfo(BaseFederationServlet): + """ + Exchange a bearer token for information about a user. + + The response format should be compatible with: + http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + + GET /openid/userinfo?access_token=ABDEFGH HTTP/1.1 + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "sub": "@userpart:example.org", + } + """ + + PATH = "/openid/userinfo" + + REQUIRE_AUTH = False + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_federation_server() + + async def on_GET( + self, + origin: Optional[str], + content: Literal[None], + query: Dict[bytes, List[bytes]], + ) -> Tuple[int, JsonDict]: + token = parse_string_from_args(query, "access_token") + if token is None: + return ( + 401, + {"errcode": "M_MISSING_TOKEN", "error": "Access Token required"}, + ) + + user_id = await self.handler.on_openid_userinfo(token) + + if user_id is None: + return ( + 401, + { + "errcode": "M_UNKNOWN_TOKEN", + "error": "Access Token unknown or expired", + }, + ) + + return 200, {"sub": user_id} + + +DEFAULT_SERVLET_GROUPS: Dict[str, Iterable[Type[BaseFederationServlet]]] = { + "federation": FEDERATION_SERVLET_CLASSES, + "room_list": (PublicRoomList,), + "group_server": GROUP_SERVER_SERVLET_CLASSES, + "group_local": GROUP_LOCAL_SERVLET_CLASSES, + "group_attestation": (FederationGroupsRenewAttestaionServlet,), + "openid": (OpenIdUserInfo,), +} + + +def register_servlets( + hs: HomeServer, + resource: HttpServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + servlet_groups: Optional[Iterable[str]] = None, +): + """Initialize and register servlet classes. + + Will by default register all servlets. For custom behaviour, pass in + a list of servlet_groups to register. + + Args: + hs: homeserver + resource: resource class to register to + authenticator: authenticator to use + ratelimiter: ratelimiter to use + servlet_groups: List of servlet groups to register. + Defaults to ``DEFAULT_SERVLET_GROUPS``. + """ + if not servlet_groups: + servlet_groups = DEFAULT_SERVLET_GROUPS.keys() + + for servlet_group in servlet_groups: + # Skip unknown servlet groups. + if servlet_group not in DEFAULT_SERVLET_GROUPS: + raise RuntimeError( + f"Attempting to register unknown federation servlet: '{servlet_group}'" + ) + + for servletclass in DEFAULT_SERVLET_GROUPS[servlet_group]: + servletclass( + hs=hs, + authenticator=authenticator, + ratelimiter=ratelimiter, + server_name=hs.hostname, + ).register(resource) diff --git a/synapse/federation/transport/server/_base.py b/synapse/federation/transport/server/_base.py new file mode 100644 index 0000000000..624c859f1e --- /dev/null +++ b/synapse/federation/transport/server/_base.py @@ -0,0 +1,328 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import logging +import re + +from synapse.api.errors import Codes, FederationDeniedError, SynapseError +from synapse.api.urls import FEDERATION_V1_PREFIX +from synapse.http.servlet import parse_json_object_from_request +from synapse.logging import opentracing +from synapse.logging.context import run_in_background +from synapse.logging.opentracing import ( + SynapseTags, + start_active_span, + start_active_span_from_request, + tags, + whitelisted_homeserver, +) +from synapse.server import HomeServer +from synapse.util.ratelimitutils import FederationRateLimiter +from synapse.util.stringutils import parse_and_validate_server_name + +logger = logging.getLogger(__name__) + + +class AuthenticationError(SynapseError): + """There was a problem authenticating the request""" + + +class NoAuthenticationError(AuthenticationError): + """The request had no authentication information""" + + +class Authenticator: + def __init__(self, hs: HomeServer): + self._clock = hs.get_clock() + self.keyring = hs.get_keyring() + self.server_name = hs.hostname + self.store = hs.get_datastore() + self.federation_domain_whitelist = hs.config.federation_domain_whitelist + self.notifier = hs.get_notifier() + + self.replication_client = None + if hs.config.worker.worker_app: + self.replication_client = hs.get_tcp_replication() + + # A method just so we can pass 'self' as the authenticator to the Servlets + async def authenticate_request(self, request, content): + now = self._clock.time_msec() + json_request = { + "method": request.method.decode("ascii"), + "uri": request.uri.decode("ascii"), + "destination": self.server_name, + "signatures": {}, + } + + if content is not None: + json_request["content"] = content + + origin = None + + auth_headers = request.requestHeaders.getRawHeaders(b"Authorization") + + if not auth_headers: + raise NoAuthenticationError( + 401, "Missing Authorization headers", Codes.UNAUTHORIZED + ) + + for auth in auth_headers: + if auth.startswith(b"X-Matrix"): + (origin, key, sig) = _parse_auth_header(auth) + json_request["origin"] = origin + json_request["signatures"].setdefault(origin, {})[key] = sig + + if ( + self.federation_domain_whitelist is not None + and origin not in self.federation_domain_whitelist + ): + raise FederationDeniedError(origin) + + if origin is None or not json_request["signatures"]: + raise NoAuthenticationError( + 401, "Missing Authorization headers", Codes.UNAUTHORIZED + ) + + await self.keyring.verify_json_for_server( + origin, + json_request, + now, + ) + + logger.debug("Request from %s", origin) + request.requester = origin + + # If we get a valid signed request from the other side, its probably + # alive + retry_timings = await self.store.get_destination_retry_timings(origin) + if retry_timings and retry_timings.retry_last_ts: + run_in_background(self._reset_retry_timings, origin) + + return origin + + async def _reset_retry_timings(self, origin): + try: + logger.info("Marking origin %r as up", origin) + await self.store.set_destination_retry_timings(origin, None, 0, 0) + + # Inform the relevant places that the remote server is back up. + self.notifier.notify_remote_server_up(origin) + if self.replication_client: + # If we're on a worker we try and inform master about this. The + # replication client doesn't hook into the notifier to avoid + # infinite loops where we send a `REMOTE_SERVER_UP` command to + # master, which then echoes it back to us which in turn pokes + # the notifier. + self.replication_client.send_remote_server_up(origin) + + except Exception: + logger.exception("Error resetting retry timings on %s", origin) + + +def _parse_auth_header(header_bytes): + """Parse an X-Matrix auth header + + Args: + header_bytes (bytes): header value + + Returns: + Tuple[str, str, str]: origin, key id, signature. + + Raises: + AuthenticationError if the header could not be parsed + """ + try: + header_str = header_bytes.decode("utf-8") + params = header_str.split(" ")[1].split(",") + param_dict = dict(kv.split("=") for kv in params) + + def strip_quotes(value): + if value.startswith('"'): + return value[1:-1] + else: + return value + + origin = strip_quotes(param_dict["origin"]) + + # ensure that the origin is a valid server name + parse_and_validate_server_name(origin) + + key = strip_quotes(param_dict["key"]) + sig = strip_quotes(param_dict["sig"]) + return origin, key, sig + except Exception as e: + logger.warning( + "Error parsing auth header '%s': %s", + header_bytes.decode("ascii", "replace"), + e, + ) + raise AuthenticationError( + 400, "Malformed Authorization header", Codes.UNAUTHORIZED + ) + + +class BaseFederationServlet: + """Abstract base class for federation servlet classes. + + The servlet object should have a PATH attribute which takes the form of a regexp to + match against the request path (excluding the /federation/v1 prefix). + + The servlet should also implement one or more of on_GET, on_POST, on_PUT, to match + the appropriate HTTP method. These methods must be *asynchronous* and have the + signature: + + on_(self, origin, content, query, **kwargs) + + With arguments: + + origin (unicode|None): The authenticated server_name of the calling server, + unless REQUIRE_AUTH is set to False and authentication failed. + + content (unicode|None): decoded json body of the request. None if the + request was a GET. + + query (dict[bytes, list[bytes]]): Query params from the request. url-decoded + (ie, '+' and '%xx' are decoded) but note that it is *not* utf8-decoded + yet. + + **kwargs (dict[unicode, unicode]): the dict mapping keys to path + components as specified in the path match regexp. + + Returns: + Optional[Tuple[int, object]]: either (response code, response object) to + return a JSON response, or None if the request has already been handled. + + Raises: + SynapseError: to return an error code + + Exception: other exceptions will be caught, logged, and a 500 will be + returned. + """ + + PATH = "" # Overridden in subclasses, the regex to match against the path. + + REQUIRE_AUTH = True + + PREFIX = FEDERATION_V1_PREFIX # Allows specifying the API version + + RATELIMIT = True # Whether to rate limit requests or not + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + self.hs = hs + self.authenticator = authenticator + self.ratelimiter = ratelimiter + self.server_name = server_name + + def _wrap(self, func): + authenticator = self.authenticator + ratelimiter = self.ratelimiter + + @functools.wraps(func) + async def new_func(request, *args, **kwargs): + """A callback which can be passed to HttpServer.RegisterPaths + + Args: + request (twisted.web.http.Request): + *args: unused? + **kwargs (dict[unicode, unicode]): the dict mapping keys to path + components as specified in the path match regexp. + + Returns: + Tuple[int, object]|None: (response code, response object) as returned by + the callback method. None if the request has already been handled. + """ + content = None + if request.method in [b"PUT", b"POST"]: + # TODO: Handle other method types? other content types? + content = parse_json_object_from_request(request) + + try: + origin = await authenticator.authenticate_request(request, content) + except NoAuthenticationError: + origin = None + if self.REQUIRE_AUTH: + logger.warning( + "authenticate_request failed: missing authentication" + ) + raise + except Exception as e: + logger.warning("authenticate_request failed: %s", e) + raise + + request_tags = { + SynapseTags.REQUEST_ID: request.get_request_id(), + tags.SPAN_KIND: tags.SPAN_KIND_RPC_SERVER, + tags.HTTP_METHOD: request.get_method(), + tags.HTTP_URL: request.get_redacted_uri(), + tags.PEER_HOST_IPV6: request.getClientIP(), + "authenticated_entity": origin, + "servlet_name": request.request_metrics.name, + } + + # Only accept the span context if the origin is authenticated + # and whitelisted + if origin and whitelisted_homeserver(origin): + scope = start_active_span_from_request( + request, "incoming-federation-request", tags=request_tags + ) + else: + scope = start_active_span( + "incoming-federation-request", tags=request_tags + ) + + with scope: + opentracing.inject_response_headers(request.responseHeaders) + + if origin and self.RATELIMIT: + with ratelimiter.ratelimit(origin) as d: + await d + if request._disconnected: + logger.warning( + "client disconnected before we started processing " + "request" + ) + return -1, None + response = await func( + origin, content, request.args, *args, **kwargs + ) + else: + response = await func( + origin, content, request.args, *args, **kwargs + ) + + return response + + return new_func + + def register(self, server): + pattern = re.compile("^" + self.PREFIX + self.PATH + "$") + + for method in ("GET", "PUT", "POST"): + code = getattr(self, "on_%s" % (method), None) + if code is None: + continue + + server.register_paths( + method, + (pattern,), + self._wrap(code), + self.__class__.__name__, + ) diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py new file mode 100644 index 0000000000..2806337846 --- /dev/null +++ b/synapse/federation/transport/server/federation.py @@ -0,0 +1,692 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Type, Union + +from typing_extensions import Literal + +import synapse +from synapse.api.errors import Codes, SynapseError +from synapse.api.room_versions import RoomVersions +from synapse.api.urls import FEDERATION_UNSTABLE_PREFIX, FEDERATION_V2_PREFIX +from synapse.federation.transport.server._base import ( + Authenticator, + BaseFederationServlet, +) +from synapse.http.servlet import ( + parse_boolean_from_args, + parse_integer_from_args, + parse_string_from_args, + parse_strings_from_args, +) +from synapse.server import HomeServer +from synapse.types import JsonDict +from synapse.util.ratelimitutils import FederationRateLimiter +from synapse.util.versionstring import get_version_string + +logger = logging.getLogger(__name__) + + +class BaseFederationServerServlet(BaseFederationServlet): + """Abstract base class for federation servlet classes which provides a federation server handler. + + See BaseFederationServlet for more information. + """ + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_federation_server() + + +class FederationSendServlet(BaseFederationServerServlet): + PATH = "/send/(?P[^/]*)/?" + + # We ratelimit manually in the handler as we queue up the requests and we + # don't want to fill up the ratelimiter with blocked requests. + RATELIMIT = False + + # This is when someone is trying to send us a bunch of data. + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + transaction_id: str, + ) -> Tuple[int, JsonDict]: + """Called on PUT /send// + + Args: + transaction_id: The transaction_id associated with this request. This + is *not* None. + + Returns: + Tuple of `(code, response)`, where + `response` is a python dict to be converted into JSON that is + used as the response body. + """ + # Parse the request + try: + transaction_data = content + + logger.debug("Decoded %s: %s", transaction_id, str(transaction_data)) + + logger.info( + "Received txn %s from %s. (PDUs: %d, EDUs: %d)", + transaction_id, + origin, + len(transaction_data.get("pdus", [])), + len(transaction_data.get("edus", [])), + ) + + except Exception as e: + logger.exception(e) + return 400, {"error": "Invalid transaction"} + + code, response = await self.handler.on_incoming_transaction( + origin, transaction_id, self.server_name, transaction_data + ) + + return code, response + + +class FederationEventServlet(BaseFederationServerServlet): + PATH = "/event/(?P[^/]*)/?" + + # This is when someone asks for a data item for a given server data_id pair. + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + event_id: str, + ) -> Tuple[int, Union[JsonDict, str]]: + return await self.handler.on_pdu_request(origin, event_id) + + +class FederationStateV1Servlet(BaseFederationServerServlet): + PATH = "/state/(?P[^/]*)/?" + + # This is when someone asks for all data for a given room. + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + return await self.handler.on_room_state_request( + origin, + room_id, + parse_string_from_args(query, "event_id", None, required=False), + ) + + +class FederationStateIdsServlet(BaseFederationServerServlet): + PATH = "/state_ids/(?P[^/]*)/?" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + return await self.handler.on_state_ids_request( + origin, + room_id, + parse_string_from_args(query, "event_id", None, required=True), + ) + + +class FederationBackfillServlet(BaseFederationServerServlet): + PATH = "/backfill/(?P[^/]*)/?" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + versions = [x.decode("ascii") for x in query[b"v"]] + limit = parse_integer_from_args(query, "limit", None) + + if not limit: + return 400, {"error": "Did not include limit param"} + + return await self.handler.on_backfill_request(origin, room_id, versions, limit) + + +class FederationQueryServlet(BaseFederationServerServlet): + PATH = "/query/(?P[^/]*)" + + # This is when we receive a server-server Query + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + query_type: str, + ) -> Tuple[int, JsonDict]: + args = {k.decode("utf8"): v[0].decode("utf-8") for k, v in query.items()} + args["origin"] = origin + return await self.handler.on_query_request(query_type, args) + + +class FederationMakeJoinServlet(BaseFederationServerServlet): + PATH = "/make_join/(?P[^/]*)/(?P[^/]*)" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + """ + Args: + origin: The authenticated server_name of the calling server + + content: (GETs don't have bodies) + + query: Query params from the request. + + **kwargs: the dict mapping keys to path components as specified in + the path match regexp. + + Returns: + Tuple of (response code, response object) + """ + supported_versions = parse_strings_from_args(query, "ver", encoding="utf-8") + if supported_versions is None: + supported_versions = ["1"] + + result = await self.handler.on_make_join_request( + origin, room_id, user_id, supported_versions=supported_versions + ) + return 200, result + + +class FederationMakeLeaveServlet(BaseFederationServerServlet): + PATH = "/make_leave/(?P[^/]*)/(?P[^/]*)" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + result = await self.handler.on_make_leave_request(origin, room_id, user_id) + return 200, result + + +class FederationV1SendLeaveServlet(BaseFederationServerServlet): + PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, Tuple[int, JsonDict]]: + result = await self.handler.on_send_leave_request(origin, content, room_id) + return 200, (200, result) + + +class FederationV2SendLeaveServlet(BaseFederationServerServlet): + PATH = "/send_leave/(?P[^/]*)/(?P[^/]*)" + + PREFIX = FEDERATION_V2_PREFIX + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: + result = await self.handler.on_send_leave_request(origin, content, room_id) + return 200, result + + +class FederationMakeKnockServlet(BaseFederationServerServlet): + PATH = "/make_knock/(?P[^/]*)/(?P[^/]*)" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + # Retrieve the room versions the remote homeserver claims to support + supported_versions = parse_strings_from_args( + query, "ver", required=True, encoding="utf-8" + ) + + result = await self.handler.on_make_knock_request( + origin, room_id, user_id, supported_versions=supported_versions + ) + return 200, result + + +class FederationV1SendKnockServlet(BaseFederationServerServlet): + PATH = "/send_knock/(?P[^/]*)/(?P[^/]*)" + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: + result = await self.handler.on_send_knock_request(origin, content, room_id) + return 200, result + + +class FederationEventAuthServlet(BaseFederationServerServlet): + PATH = "/event_auth/(?P[^/]*)/(?P[^/]*)" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: + return await self.handler.on_event_auth(origin, room_id, event_id) + + +class FederationV1SendJoinServlet(BaseFederationServerServlet): + PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, Tuple[int, JsonDict]]: + # TODO(paul): assert that event_id parsed from path actually + # match those given in content + result = await self.handler.on_send_join_request(origin, content, room_id) + return 200, (200, result) + + +class FederationV2SendJoinServlet(BaseFederationServerServlet): + PATH = "/send_join/(?P[^/]*)/(?P[^/]*)" + + PREFIX = FEDERATION_V2_PREFIX + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: + # TODO(paul): assert that event_id parsed from path actually + # match those given in content + result = await self.handler.on_send_join_request(origin, content, room_id) + return 200, result + + +class FederationV1InviteServlet(BaseFederationServerServlet): + PATH = "/invite/(?P[^/]*)/(?P[^/]*)" + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, Tuple[int, JsonDict]]: + # We don't get a room version, so we have to assume its EITHER v1 or + # v2. This is "fine" as the only difference between V1 and V2 is the + # state resolution algorithm, and we don't use that for processing + # invites + result = await self.handler.on_invite_request( + origin, content, room_version_id=RoomVersions.V1.identifier + ) + + # V1 federation API is defined to return a content of `[200, {...}]` + # due to a historical bug. + return 200, (200, result) + + +class FederationV2InviteServlet(BaseFederationServerServlet): + PATH = "/invite/(?P[^/]*)/(?P[^/]*)" + + PREFIX = FEDERATION_V2_PREFIX + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + event_id: str, + ) -> Tuple[int, JsonDict]: + # TODO(paul): assert that room_id/event_id parsed from path actually + # match those given in content + + room_version = content["room_version"] + event = content["event"] + invite_room_state = content["invite_room_state"] + + # Synapse expects invite_room_state to be in unsigned, as it is in v1 + # API + + event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state + + result = await self.handler.on_invite_request( + origin, event, room_version_id=room_version + ) + return 200, result + + +class FederationThirdPartyInviteExchangeServlet(BaseFederationServerServlet): + PATH = "/exchange_third_party_invite/(?P[^/]*)" + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + await self.handler.on_exchange_third_party_invite_request(content) + return 200, {} + + +class FederationClientKeysQueryServlet(BaseFederationServerServlet): + PATH = "/user/keys/query" + + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: + return await self.handler.on_query_client_keys(origin, content) + + +class FederationUserDevicesQueryServlet(BaseFederationServerServlet): + PATH = "/user/devices/(?P[^/]*)" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + user_id: str, + ) -> Tuple[int, JsonDict]: + return await self.handler.on_query_user_devices(origin, user_id) + + +class FederationClientKeysClaimServlet(BaseFederationServerServlet): + PATH = "/user/keys/claim" + + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: + response = await self.handler.on_claim_client_keys(origin, content) + return 200, response + + +class FederationGetMissingEventsServlet(BaseFederationServerServlet): + # TODO(paul): Why does this path alone end with "/?" optional? + PATH = "/get_missing_events/(?P[^/]*)/?" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + limit = int(content.get("limit", 10)) + earliest_events = content.get("earliest_events", []) + latest_events = content.get("latest_events", []) + + result = await self.handler.on_get_missing_events( + origin, + room_id=room_id, + earliest_events=earliest_events, + latest_events=latest_events, + limit=limit, + ) + + return 200, result + + +class On3pidBindServlet(BaseFederationServerServlet): + PATH = "/3pid/onbind" + + REQUIRE_AUTH = False + + async def on_POST( + self, origin: Optional[str], content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: + if "invites" in content: + last_exception = None + for invite in content["invites"]: + try: + if "signed" not in invite or "token" not in invite["signed"]: + message = ( + "Rejecting received notification of third-" + "party invite without signed: %s" % (invite,) + ) + logger.info(message) + raise SynapseError(400, message) + await self.handler.exchange_third_party_invite( + invite["sender"], + invite["mxid"], + invite["room_id"], + invite["signed"], + ) + except Exception as e: + last_exception = e + if last_exception: + raise last_exception + return 200, {} + + +class FederationVersionServlet(BaseFederationServlet): + PATH = "/version" + + REQUIRE_AUTH = False + + async def on_GET( + self, + origin: Optional[str], + content: Literal[None], + query: Dict[bytes, List[bytes]], + ) -> Tuple[int, JsonDict]: + return ( + 200, + {"server": {"name": "Synapse", "version": get_version_string(synapse)}}, + ) + + +class FederationSpaceSummaryServlet(BaseFederationServlet): + PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" + PATH = "/spaces/(?P[^/]*)" + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_space_summary_handler() + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Mapping[bytes, Sequence[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) + max_rooms_per_space = parse_integer_from_args(query, "max_rooms_per_space") + + exclude_rooms = parse_strings_from_args(query, "exclude_rooms", default=[]) + + return 200, await self.handler.federation_space_summary( + origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms + ) + + # TODO When switching to the stable endpoint, remove the POST handler. + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Mapping[bytes, Sequence[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + suggested_only = content.get("suggested_only", False) + if not isinstance(suggested_only, bool): + raise SynapseError( + 400, "'suggested_only' must be a boolean", Codes.BAD_JSON + ) + + exclude_rooms = content.get("exclude_rooms", []) + if not isinstance(exclude_rooms, list) or any( + not isinstance(x, str) for x in exclude_rooms + ): + raise SynapseError(400, "bad value for 'exclude_rooms'", Codes.BAD_JSON) + + max_rooms_per_space = content.get("max_rooms_per_space") + if max_rooms_per_space is not None and not isinstance(max_rooms_per_space, int): + raise SynapseError( + 400, "bad value for 'max_rooms_per_space'", Codes.BAD_JSON + ) + + return 200, await self.handler.federation_space_summary( + origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms + ) + + +class FederationRoomHierarchyServlet(BaseFederationServlet): + PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc2946" + PATH = "/hierarchy/(?P[^/]*)" + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_space_summary_handler() + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Mapping[bytes, Sequence[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) + return 200, await self.handler.get_federation_hierarchy( + origin, room_id, suggested_only + ) + + +class RoomComplexityServlet(BaseFederationServlet): + """ + Indicates to other servers how complex (and therefore likely + resource-intensive) a public room this server knows about is. + """ + + PATH = "/rooms/(?P[^/]*)/complexity" + PREFIX = FEDERATION_UNSTABLE_PREFIX + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self._store = self.hs.get_datastore() + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + room_id: str, + ) -> Tuple[int, JsonDict]: + is_public = await self._store.is_room_world_readable_or_publicly_joinable( + room_id + ) + + if not is_public: + raise SynapseError(404, "Room not found", errcode=Codes.INVALID_PARAM) + + complexity = await self._store.get_room_complexity(room_id) + return 200, complexity + + +FEDERATION_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( + FederationSendServlet, + FederationEventServlet, + FederationStateV1Servlet, + FederationStateIdsServlet, + FederationBackfillServlet, + FederationQueryServlet, + FederationMakeJoinServlet, + FederationMakeLeaveServlet, + FederationEventServlet, + FederationV1SendJoinServlet, + FederationV2SendJoinServlet, + FederationV1SendLeaveServlet, + FederationV2SendLeaveServlet, + FederationV1InviteServlet, + FederationV2InviteServlet, + FederationGetMissingEventsServlet, + FederationEventAuthServlet, + FederationClientKeysQueryServlet, + FederationUserDevicesQueryServlet, + FederationClientKeysClaimServlet, + FederationThirdPartyInviteExchangeServlet, + On3pidBindServlet, + FederationVersionServlet, + RoomComplexityServlet, + FederationSpaceSummaryServlet, + FederationRoomHierarchyServlet, + FederationV1SendKnockServlet, + FederationMakeKnockServlet, +) diff --git a/synapse/federation/transport/server/groups_local.py b/synapse/federation/transport/server/groups_local.py new file mode 100644 index 0000000000..a12cd18d58 --- /dev/null +++ b/synapse/federation/transport/server/groups_local.py @@ -0,0 +1,113 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict, List, Tuple, Type + +from synapse.api.errors import SynapseError +from synapse.federation.transport.server._base import ( + Authenticator, + BaseFederationServlet, +) +from synapse.handlers.groups_local import GroupsLocalHandler +from synapse.server import HomeServer +from synapse.types import JsonDict, get_domain_from_id +from synapse.util.ratelimitutils import FederationRateLimiter + + +class BaseGroupsLocalServlet(BaseFederationServlet): + """Abstract base class for federation servlet classes which provides a groups local handler. + + See BaseFederationServlet for more information. + """ + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_groups_local_handler() + + +class FederationGroupsLocalInviteServlet(BaseGroupsLocalServlet): + """A group server has invited a local user""" + + PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/invite" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + if get_domain_from_id(group_id) != origin: + raise SynapseError(403, "group_id doesn't match origin") + + assert isinstance( + self.handler, GroupsLocalHandler + ), "Workers cannot handle group invites." + + new_content = await self.handler.on_invite(group_id, user_id, content) + + return 200, new_content + + +class FederationGroupsRemoveLocalUserServlet(BaseGroupsLocalServlet): + """A group server has removed a local user""" + + PATH = "/groups/local/(?P[^/]*)/users/(?P[^/]*)/remove" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, None]: + if get_domain_from_id(group_id) != origin: + raise SynapseError(403, "user_id doesn't match origin") + + assert isinstance( + self.handler, GroupsLocalHandler + ), "Workers cannot handle group removals." + + await self.handler.user_removed_from_group(group_id, user_id, content) + + return 200, None + + +class FederationGroupsBulkPublicisedServlet(BaseGroupsLocalServlet): + """Get roles in a group""" + + PATH = "/get_groups_publicised" + + async def on_POST( + self, origin: str, content: JsonDict, query: Dict[bytes, List[bytes]] + ) -> Tuple[int, JsonDict]: + resp = await self.handler.bulk_get_publicised_groups( + content["user_ids"], proxy=False + ) + + return 200, resp + + +GROUP_LOCAL_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( + FederationGroupsLocalInviteServlet, + FederationGroupsRemoveLocalUserServlet, + FederationGroupsBulkPublicisedServlet, +) diff --git a/synapse/federation/transport/server/groups_server.py b/synapse/federation/transport/server/groups_server.py new file mode 100644 index 0000000000..b30e92a5eb --- /dev/null +++ b/synapse/federation/transport/server/groups_server.py @@ -0,0 +1,753 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict, List, Tuple, Type + +from typing_extensions import Literal + +from synapse.api.constants import MAX_GROUP_CATEGORYID_LENGTH, MAX_GROUP_ROLEID_LENGTH +from synapse.api.errors import Codes, SynapseError +from synapse.federation.transport.server._base import ( + Authenticator, + BaseFederationServlet, +) +from synapse.http.servlet import parse_string_from_args +from synapse.server import HomeServer +from synapse.types import JsonDict, get_domain_from_id +from synapse.util.ratelimitutils import FederationRateLimiter + + +class BaseGroupsServerServlet(BaseFederationServlet): + """Abstract base class for federation servlet classes which provides a groups server handler. + + See BaseFederationServlet for more information. + """ + + def __init__( + self, + hs: HomeServer, + authenticator: Authenticator, + ratelimiter: FederationRateLimiter, + server_name: str, + ): + super().__init__(hs, authenticator, ratelimiter, server_name) + self.handler = hs.get_groups_server_handler() + + +class FederationGroupsProfileServlet(BaseGroupsServerServlet): + """Get/set the basic profile of a group on behalf of a user""" + + PATH = "/groups/(?P[^/]*)/profile" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.get_group_profile(group_id, requester_user_id) + + return 200, new_content + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.update_group_profile( + group_id, requester_user_id, content + ) + + return 200, new_content + + +class FederationGroupsSummaryServlet(BaseGroupsServerServlet): + PATH = "/groups/(?P[^/]*)/summary" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.get_group_summary(group_id, requester_user_id) + + return 200, new_content + + +class FederationGroupsRoomsServlet(BaseGroupsServerServlet): + """Get the rooms in a group on behalf of a user""" + + PATH = "/groups/(?P[^/]*)/rooms" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.get_rooms_in_group(group_id, requester_user_id) + + return 200, new_content + + +class FederationGroupsAddRoomsServlet(BaseGroupsServerServlet): + """Add/remove room from group""" + + PATH = "/groups/(?P[^/]*)/room/(?P[^/]*)" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.add_room_to_group( + group_id, requester_user_id, room_id, content + ) + + return 200, new_content + + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.remove_room_from_group( + group_id, requester_user_id, room_id + ) + + return 200, new_content + + +class FederationGroupsAddRoomsConfigServlet(BaseGroupsServerServlet): + """Update room config in group""" + + PATH = ( + "/groups/(?P[^/]*)/room/(?P[^/]*)" + "/config/(?P[^/]*)" + ) + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + room_id: str, + config_key: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + result = await self.handler.update_room_in_group( + group_id, requester_user_id, room_id, config_key, content + ) + + return 200, result + + +class FederationGroupsUsersServlet(BaseGroupsServerServlet): + """Get the users in a group on behalf of a user""" + + PATH = "/groups/(?P[^/]*)/users" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.get_users_in_group(group_id, requester_user_id) + + return 200, new_content + + +class FederationGroupsInvitedUsersServlet(BaseGroupsServerServlet): + """Get the users that have been invited to a group""" + + PATH = "/groups/(?P[^/]*)/invited_users" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.get_invited_users_in_group( + group_id, requester_user_id + ) + + return 200, new_content + + +class FederationGroupsInviteServlet(BaseGroupsServerServlet): + """Ask a group server to invite someone to the group""" + + PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/invite" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.invite_to_group( + group_id, user_id, requester_user_id, content + ) + + return 200, new_content + + +class FederationGroupsAcceptInviteServlet(BaseGroupsServerServlet): + """Accept an invitation from the group server""" + + PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/accept_invite" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + if get_domain_from_id(user_id) != origin: + raise SynapseError(403, "user_id doesn't match origin") + + new_content = await self.handler.accept_invite(group_id, user_id, content) + + return 200, new_content + + +class FederationGroupsJoinServlet(BaseGroupsServerServlet): + """Attempt to join a group""" + + PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/join" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + if get_domain_from_id(user_id) != origin: + raise SynapseError(403, "user_id doesn't match origin") + + new_content = await self.handler.join_group(group_id, user_id, content) + + return 200, new_content + + +class FederationGroupsRemoveUserServlet(BaseGroupsServerServlet): + """Leave or kick a user from the group""" + + PATH = "/groups/(?P[^/]*)/users/(?P[^/]*)/remove" + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.remove_user_from_group( + group_id, user_id, requester_user_id, content + ) + + return 200, new_content + + +class FederationGroupsSummaryRoomsServlet(BaseGroupsServerServlet): + """Add/remove a room from the group summary, with optional category. + + Matches both: + - /groups/:group/summary/rooms/:room_id + - /groups/:group/summary/categories/:category/rooms/:room_id + """ + + PATH = ( + "/groups/(?P[^/]*)/summary" + "(/categories/(?P[^/]+))?" + "/rooms/(?P[^/]*)" + ) + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if category_id == "": + raise SynapseError( + 400, "category_id cannot be empty string", Codes.INVALID_PARAM + ) + + if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) + + resp = await self.handler.update_group_summary_room( + group_id, + requester_user_id, + room_id=room_id, + category_id=category_id, + content=content, + ) + + return 200, resp + + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + room_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if category_id == "": + raise SynapseError(400, "category_id cannot be empty string") + + resp = await self.handler.delete_group_summary_room( + group_id, requester_user_id, room_id=room_id, category_id=category_id + ) + + return 200, resp + + +class FederationGroupsCategoriesServlet(BaseGroupsServerServlet): + """Get all categories for a group""" + + PATH = "/groups/(?P[^/]*)/categories/?" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + resp = await self.handler.get_group_categories(group_id, requester_user_id) + + return 200, resp + + +class FederationGroupsCategoryServlet(BaseGroupsServerServlet): + """Add/remove/get a category in a group""" + + PATH = "/groups/(?P[^/]*)/categories/(?P[^/]+)" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + resp = await self.handler.get_group_category( + group_id, requester_user_id, category_id + ) + + return 200, resp + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if category_id == "": + raise SynapseError(400, "category_id cannot be empty string") + + if len(category_id) > MAX_GROUP_CATEGORYID_LENGTH: + raise SynapseError( + 400, + "category_id may not be longer than %s characters" + % (MAX_GROUP_CATEGORYID_LENGTH,), + Codes.INVALID_PARAM, + ) + + resp = await self.handler.upsert_group_category( + group_id, requester_user_id, category_id, content + ) + + return 200, resp + + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + category_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if category_id == "": + raise SynapseError(400, "category_id cannot be empty string") + + resp = await self.handler.delete_group_category( + group_id, requester_user_id, category_id + ) + + return 200, resp + + +class FederationGroupsRolesServlet(BaseGroupsServerServlet): + """Get roles in a group""" + + PATH = "/groups/(?P[^/]*)/roles/?" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + resp = await self.handler.get_group_roles(group_id, requester_user_id) + + return 200, resp + + +class FederationGroupsRoleServlet(BaseGroupsServerServlet): + """Add/remove/get a role in a group""" + + PATH = "/groups/(?P[^/]*)/roles/(?P[^/]+)" + + async def on_GET( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + resp = await self.handler.get_group_role(group_id, requester_user_id, role_id) + + return 200, resp + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if role_id == "": + raise SynapseError( + 400, "role_id cannot be empty string", Codes.INVALID_PARAM + ) + + if len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) + + resp = await self.handler.update_group_role( + group_id, requester_user_id, role_id, content + ) + + return 200, resp + + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if role_id == "": + raise SynapseError(400, "role_id cannot be empty string") + + resp = await self.handler.delete_group_role( + group_id, requester_user_id, role_id + ) + + return 200, resp + + +class FederationGroupsSummaryUsersServlet(BaseGroupsServerServlet): + """Add/remove a user from the group summary, with optional role. + + Matches both: + - /groups/:group/summary/users/:user_id + - /groups/:group/summary/roles/:role/users/:user_id + """ + + PATH = ( + "/groups/(?P[^/]*)/summary" + "(/roles/(?P[^/]+))?" + "/users/(?P[^/]*)" + ) + + async def on_POST( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if role_id == "": + raise SynapseError(400, "role_id cannot be empty string") + + if len(role_id) > MAX_GROUP_ROLEID_LENGTH: + raise SynapseError( + 400, + "role_id may not be longer than %s characters" + % (MAX_GROUP_ROLEID_LENGTH,), + Codes.INVALID_PARAM, + ) + + resp = await self.handler.update_group_summary_user( + group_id, + requester_user_id, + user_id=user_id, + role_id=role_id, + content=content, + ) + + return 200, resp + + async def on_DELETE( + self, + origin: str, + content: Literal[None], + query: Dict[bytes, List[bytes]], + group_id: str, + role_id: str, + user_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + if role_id == "": + raise SynapseError(400, "role_id cannot be empty string") + + resp = await self.handler.delete_group_summary_user( + group_id, requester_user_id, user_id=user_id, role_id=role_id + ) + + return 200, resp + + +class FederationGroupsSettingJoinPolicyServlet(BaseGroupsServerServlet): + """Sets whether a group is joinable without an invite or knock""" + + PATH = "/groups/(?P[^/]*)/settings/m.join_policy" + + async def on_PUT( + self, + origin: str, + content: JsonDict, + query: Dict[bytes, List[bytes]], + group_id: str, + ) -> Tuple[int, JsonDict]: + requester_user_id = parse_string_from_args( + query, "requester_user_id", required=True + ) + if get_domain_from_id(requester_user_id) != origin: + raise SynapseError(403, "requester_user_id doesn't match origin") + + new_content = await self.handler.set_group_join_policy( + group_id, requester_user_id, content + ) + + return 200, new_content + + +GROUP_SERVER_SERVLET_CLASSES: Tuple[Type[BaseFederationServlet], ...] = ( + FederationGroupsProfileServlet, + FederationGroupsSummaryServlet, + FederationGroupsRoomsServlet, + FederationGroupsUsersServlet, + FederationGroupsInvitedUsersServlet, + FederationGroupsInviteServlet, + FederationGroupsAcceptInviteServlet, + FederationGroupsJoinServlet, + FederationGroupsRemoveUserServlet, + FederationGroupsSummaryRoomsServlet, + FederationGroupsCategoriesServlet, + FederationGroupsCategoryServlet, + FederationGroupsRolesServlet, + FederationGroupsRoleServlet, + FederationGroupsSummaryUsersServlet, + FederationGroupsAddRoomsServlet, + FederationGroupsAddRoomsConfigServlet, + FederationGroupsSettingJoinPolicyServlet, +) From 0ace38b7b310fc1b4f88ac93d01ec900f33f7a07 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 16 Aug 2021 15:49:12 +0100 Subject: [PATCH 568/619] Experimental support for MSC3266 Room Summary API. (#10394) --- changelog.d/10394.feature | 1 + mypy.ini | 2 +- synapse/config/experimental.py | 3 + .../federation/transport/server/federation.py | 4 +- .../{space_summary.py => room_summary.py} | 87 +++++++++++--- synapse/http/servlet.py | 58 +++++++++- synapse/rest/admin/rooms.py | 45 +------- synapse/rest/client/v1/room.py | 90 +++++++++------ synapse/server.py | 6 +- ..._space_summary.py => test_room_summary.py} | 108 +++++++++++++++--- 10 files changed, 289 insertions(+), 115 deletions(-) create mode 100644 changelog.d/10394.feature rename synapse/handlers/{space_summary.py => room_summary.py} (93%) rename tests/handlers/{test_space_summary.py => test_room_summary.py} (88%) diff --git a/changelog.d/10394.feature b/changelog.d/10394.feature new file mode 100644 index 0000000000..c8bbc5a740 --- /dev/null +++ b/changelog.d/10394.feature @@ -0,0 +1 @@ +Initial local support for [MSC3266](https://github.com/matrix-org/synapse/pull/10394), Room Summary over the unstable `/rooms/{roomIdOrAlias}/summary` API. diff --git a/mypy.ini b/mypy.ini index 5d6cd557bc..e1b9405daa 100644 --- a/mypy.ini +++ b/mypy.ini @@ -86,7 +86,7 @@ files = tests/test_event_auth.py, tests/test_utils, tests/handlers/test_password_providers.py, - tests/handlers/test_space_summary.py, + tests/handlers/test_room_summary.py, tests/rest/client/v1/test_login.py, tests/rest/client/v2_alpha/test_auth.py, tests/util/test_itertools.py, diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 4c60ee8c28..b918fb15b0 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -38,3 +38,6 @@ def read_config(self, config: JsonDict, **kwargs): # MSC3244 (room version capabilities) self.msc3244_enabled: bool = experimental.get("msc3244_enabled", False) + + # MSC3266 (room summary api) + self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False) diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py index 2806337846..7d81cc642c 100644 --- a/synapse/federation/transport/server/federation.py +++ b/synapse/federation/transport/server/federation.py @@ -547,7 +547,7 @@ def __init__( server_name: str, ): super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_space_summary_handler() + self.handler = hs.get_room_summary_handler() async def on_GET( self, @@ -608,7 +608,7 @@ def __init__( server_name: str, ): super().__init__(hs, authenticator, ratelimiter, server_name) - self.handler = hs.get_space_summary_handler() + self.handler = hs.get_room_summary_handler() async def on_GET( self, diff --git a/synapse/handlers/space_summary.py b/synapse/handlers/room_summary.py similarity index 93% rename from synapse/handlers/space_summary.py rename to synapse/handlers/room_summary.py index c74e90abbc..ac6cfc0da9 100644 --- a/synapse/handlers/space_summary.py +++ b/synapse/handlers/room_summary.py @@ -28,7 +28,7 @@ Membership, RoomTypes, ) -from synapse.api.errors import AuthError, Codes, SynapseError +from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError from synapse.events import EventBase from synapse.events.utils import format_event_for_client_v2 from synapse.types import JsonDict @@ -75,7 +75,7 @@ class _PaginationSession: processed_rooms: Set[str] -class SpaceSummaryHandler: +class RoomSummaryHandler: # The time a pagination session remains valid for. _PAGINATION_SESSION_VALIDITY_PERIOD_MS = 5 * 60 * 1000 @@ -412,7 +412,7 @@ async def _get_room_hierarchy( room_entry, children_room_entries, inaccessible_children, - ) = await self._summarize_remote_room_hiearchy( + ) = await self._summarize_remote_room_hierarchy( queue_entry, suggested_only, ) @@ -724,7 +724,7 @@ async def _summarize_remote_room( return results - async def _summarize_remote_room_hiearchy( + async def _summarize_remote_room_hierarchy( self, room: "_RoomQueueEntry", suggested_only: bool ) -> Tuple[Optional["_RoomEntry"], Dict[str, JsonDict], Set[str]]: """ @@ -781,25 +781,25 @@ async def _is_local_room_accessible( self, room_id: str, requester: Optional[str], origin: Optional[str] = None ) -> bool: """ - Calculate whether the room should be shown in the spaces summary. + Calculate whether the room should be shown to the requester. - It should be included if: + It should return true if: * The requester is joined or can join the room (per MSC3173). * The origin server has any user that is joined or can join the room. * The history visibility is set to world readable. Args: - room_id: The room ID to summarize. + room_id: The room ID to check accessibility of. requester: - The user requesting the summary, if it is a local request. None - if this is a federation request. + The user making the request, if it is a local request. + None if this is a federation request. origin: - The server requesting the summary, if it is a federation request. + The server making the request, if it is a federation request. None if this is a local request. Returns: - True if the room should be included in the spaces summary. + True if the room is accessible to the requesting user or server. """ state_ids = await self._store.get_current_state_ids(room_id) @@ -893,9 +893,9 @@ async def _is_remote_room_accessible( self, requester: str, room_id: str, room: JsonDict ) -> bool: """ - Calculate whether the room received over federation should be shown in the spaces summary. + Calculate whether the room received over federation should be shown to the requester. - It should be included if: + It should return true if: * The requester is joined or can join the room (per MSC3173). * The history visibility is set to world readable. @@ -907,10 +907,10 @@ async def _is_remote_room_accessible( Args: requester: The user requesting the summary. room_id: The room ID returned over federation. - room: The summary of the child room returned over federation. + room: The summary of the room returned over federation. Returns: - True if the room should be included in the spaces summary. + True if the room is accessible to the requesting user. """ # The API doesn't return the room version so assume that a # join rule of knock is valid. @@ -936,7 +936,7 @@ async def _is_remote_room_accessible( async def _build_room_entry(self, room_id: str, for_federation: bool) -> JsonDict: """ - Generate en entry suitable for the 'rooms' list in the summary response. + Generate en entry summarising a single room. Args: room_id: The room ID to summarize. @@ -1024,6 +1024,61 @@ async def _get_child_events(self, room_id: str) -> Iterable[EventBase]: # and order to ensure we return stable results. return sorted(filter(_has_valid_via, events), key=_child_events_comparison_key) + async def get_room_summary( + self, + requester: Optional[str], + room_id: str, + remote_room_hosts: Optional[List[str]] = None, + ) -> JsonDict: + """ + Implementation of the room summary C-S API from MSC3266 + + Args: + requester: user id of the user making this request, will be None + for unauthenticated requests + + room_id: room id to summarise. + + remote_room_hosts: a list of homeservers to try fetching data through + if we don't know it ourselves + + Returns: + summary dict to return + """ + is_in_room = await self._store.is_host_joined(room_id, self._server_name) + + if is_in_room: + room_entry = await self._summarize_local_room( + requester, + None, + room_id, + # Suggested-only doesn't matter since no children are requested. + suggested_only=False, + max_children=0, + ) + + if not room_entry: + raise NotFoundError("Room not found or is not accessible") + + room_summary = room_entry.room + + # If there was a requester, add their membership. + if requester: + ( + membership, + _, + ) = await self._store.get_local_current_membership_for_user_in_room( + requester, room_id + ) + + room_summary["membership"] = membership or "leave" + else: + # TODO federation API, descoped from initial unstable implementation + # as MSC needs more maturing on that side. + raise SynapseError(400, "Federation is not currently supported.") + + return room_summary + @attr.s(frozen=True, slots=True, auto_attribs=True) class _RoomQueueEntry: diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 732a1e6aeb..a12fa30bfd 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -14,16 +14,28 @@ """ This module contains base REST classes for constructing REST servlets. """ import logging -from typing import Iterable, List, Mapping, Optional, Sequence, overload +from typing import ( + TYPE_CHECKING, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + overload, +) from typing_extensions import Literal from twisted.web.server import Request from synapse.api.errors import Codes, SynapseError -from synapse.types import JsonDict +from synapse.types import JsonDict, RoomAlias, RoomID from synapse.util import json_decoder +if TYPE_CHECKING: + from synapse.server import HomeServer + logger = logging.getLogger(__name__) @@ -663,3 +675,45 @@ def register(self, http_server): else: raise NotImplementedError("RestServlet must register something.") + + +class ResolveRoomIdMixin: + def __init__(self, hs: "HomeServer"): + self.room_member_handler = hs.get_room_member_handler() + + async def resolve_room_id( + self, room_identifier: str, remote_room_hosts: Optional[List[str]] = None + ) -> Tuple[str, Optional[List[str]]]: + """ + Resolve a room identifier to a room ID, if necessary. + + This also performanes checks to ensure the room ID is of the proper form. + + Args: + room_identifier: The room ID or alias. + remote_room_hosts: The potential remote room hosts to use. + + Returns: + The resolved room ID. + + Raises: + SynapseError if the room ID is of the wrong form. + """ + if RoomID.is_valid(room_identifier): + resolved_room_id = room_identifier + elif RoomAlias.is_valid(room_identifier): + room_alias = RoomAlias.from_string(room_identifier) + ( + room_id, + remote_room_hosts, + ) = await self.room_member_handler.lookup_room_alias(room_alias) + resolved_room_id = room_id.to_string() + else: + raise SynapseError( + 400, "%s was not legal room ID or room alias" % (room_identifier,) + ) + if not resolved_room_id: + raise SynapseError( + 400, "Unknown room ID or room alias %s" % room_identifier + ) + return resolved_room_id, remote_room_hosts diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index 40ee33646c..975c28b225 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -20,6 +20,7 @@ from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError from synapse.api.filtering import Filter from synapse.http.servlet import ( + ResolveRoomIdMixin, RestServlet, assert_params_in_dict, parse_integer, @@ -33,7 +34,7 @@ assert_user_is_admin, ) from synapse.storage.databases.main.room import RoomSortOrder -from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester +from synapse.types import JsonDict, UserID, create_requester from synapse.util import json_decoder if TYPE_CHECKING: @@ -45,48 +46,6 @@ logger = logging.getLogger(__name__) -class ResolveRoomIdMixin: - def __init__(self, hs: "HomeServer"): - self.room_member_handler = hs.get_room_member_handler() - - async def resolve_room_id( - self, room_identifier: str, remote_room_hosts: Optional[List[str]] = None - ) -> Tuple[str, Optional[List[str]]]: - """ - Resolve a room identifier to a room ID, if necessary. - - This also performanes checks to ensure the room ID is of the proper form. - - Args: - room_identifier: The room ID or alias. - remote_room_hosts: The potential remote room hosts to use. - - Returns: - The resolved room ID. - - Raises: - SynapseError if the room ID is of the wrong form. - """ - if RoomID.is_valid(room_identifier): - resolved_room_id = room_identifier - elif RoomAlias.is_valid(room_identifier): - room_alias = RoomAlias.from_string(room_identifier) - ( - room_id, - remote_room_hosts, - ) = await self.room_member_handler.lookup_room_alias(room_alias) - resolved_room_id = room_id.to_string() - else: - raise SynapseError( - 400, "%s was not legal room ID or room alias" % (room_identifier,) - ) - if not resolved_room_id: - raise SynapseError( - 400, "Unknown room ID or room alias %s" % room_identifier - ) - return resolved_room_id, remote_room_hosts - - class ShutdownRoomRestServlet(RestServlet): """Shuts down a room by removing all local users from the room and blocking all future invites and joins to the room. Any local aliases will be repointed diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index 2c3be23bc8..d3882a84e2 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -24,12 +24,14 @@ AuthError, Codes, InvalidClientCredentialsError, + MissingClientTokenError, ShadowBanError, SynapseError, ) from synapse.api.filtering import Filter from synapse.events.utils import format_event_for_client_v2 from synapse.http.servlet import ( + ResolveRoomIdMixin, RestServlet, assert_params_in_dict, parse_boolean, @@ -44,14 +46,7 @@ from synapse.rest.client.v2_alpha._base import client_patterns from synapse.storage.state import StateFilter from synapse.streams.config import PaginationConfig -from synapse.types import ( - JsonDict, - RoomAlias, - RoomID, - StreamToken, - ThirdPartyInstanceID, - UserID, -) +from synapse.types import JsonDict, StreamToken, ThirdPartyInstanceID, UserID from synapse.util import json_decoder from synapse.util.stringutils import parse_and_validate_server_name, random_string @@ -266,10 +261,10 @@ def on_PUT(self, request, room_id, event_type, txn_id): # TODO: Needs unit testing for room ID + alias joins -class JoinRoomAliasServlet(TransactionRestServlet): +class JoinRoomAliasServlet(ResolveRoomIdMixin, TransactionRestServlet): def __init__(self, hs): super().__init__(hs) - self.room_member_handler = hs.get_room_member_handler() + super(ResolveRoomIdMixin, self).__init__(hs) # ensure the Mixin is set up self.auth = hs.get_auth() def register(self, http_server): @@ -292,24 +287,13 @@ async def on_POST( # cheekily send invalid bodies. content = {} - if RoomID.is_valid(room_identifier): - room_id = room_identifier - - # twisted.web.server.Request.args is incorrectly defined as Optional[Any] - args: Dict[bytes, List[bytes]] = request.args # type: ignore - - remote_room_hosts = parse_strings_from_args( - args, "server_name", required=False - ) - elif RoomAlias.is_valid(room_identifier): - handler = self.room_member_handler - room_alias = RoomAlias.from_string(room_identifier) - room_id_obj, remote_room_hosts = await handler.lookup_room_alias(room_alias) - room_id = room_id_obj.to_string() - else: - raise SynapseError( - 400, "%s was not legal room ID or room alias" % (room_identifier,) - ) + # twisted.web.server.Request.args is incorrectly defined as Optional[Any] + args: Dict[bytes, List[bytes]] = request.args # type: ignore + remote_room_hosts = parse_strings_from_args(args, "server_name", required=False) + room_id, remote_room_hosts = await self.resolve_room_id( + room_identifier, + remote_room_hosts, + ) await self.room_member_handler.update_membership( requester=requester, @@ -1002,14 +986,14 @@ class RoomSpaceSummaryRestServlet(RestServlet): def __init__(self, hs: "HomeServer"): super().__init__() self._auth = hs.get_auth() - self._space_summary_handler = hs.get_space_summary_handler() + self._room_summary_handler = hs.get_room_summary_handler() async def on_GET( self, request: SynapseRequest, room_id: str ) -> Tuple[int, JsonDict]: requester = await self._auth.get_user_by_req(request, allow_guest=True) - return 200, await self._space_summary_handler.get_space_summary( + return 200, await self._room_summary_handler.get_space_summary( requester.user.to_string(), room_id, suggested_only=parse_boolean(request, "suggested_only", default=False), @@ -1035,7 +1019,7 @@ async def on_POST( 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON ) - return 200, await self._space_summary_handler.get_space_summary( + return 200, await self._room_summary_handler.get_space_summary( requester.user.to_string(), room_id, suggested_only=suggested_only, @@ -1054,7 +1038,7 @@ class RoomHierarchyRestServlet(RestServlet): def __init__(self, hs: "HomeServer"): super().__init__() self._auth = hs.get_auth() - self._space_summary_handler = hs.get_space_summary_handler() + self._room_summary_handler = hs.get_room_summary_handler() async def on_GET( self, request: SynapseRequest, room_id: str @@ -1073,7 +1057,7 @@ async def on_GET( 400, "'limit' must be a positive integer", Codes.BAD_JSON ) - return 200, await self._space_summary_handler.get_room_hierarchy( + return 200, await self._room_summary_handler.get_room_hierarchy( requester.user.to_string(), room_id, suggested_only=parse_boolean(request, "suggested_only", default=False), @@ -1083,6 +1067,44 @@ async def on_GET( ) +class RoomSummaryRestServlet(ResolveRoomIdMixin, RestServlet): + PATTERNS = ( + re.compile( + "^/_matrix/client/unstable/im.nheko.summary" + "/rooms/(?P[^/]*)/summary$" + ), + ) + + def __init__(self, hs: "HomeServer"): + super().__init__(hs) + self._auth = hs.get_auth() + self._room_summary_handler = hs.get_room_summary_handler() + + async def on_GET( + self, request: SynapseRequest, room_identifier: str + ) -> Tuple[int, JsonDict]: + try: + requester = await self._auth.get_user_by_req(request, allow_guest=True) + requester_user_id: Optional[str] = requester.user.to_string() + except MissingClientTokenError: + # auth is optional + requester_user_id = None + + # twisted.web.server.Request.args is incorrectly defined as Optional[Any] + args: Dict[bytes, List[bytes]] = request.args # type: ignore + remote_room_hosts = parse_strings_from_args(args, "via", required=False) + room_id, remote_room_hosts = await self.resolve_room_id( + room_identifier, + remote_room_hosts, + ) + + return 200, await self._room_summary_handler.get_room_summary( + requester_user_id, + room_id, + remote_room_hosts, + ) + + def register_servlets(hs: "HomeServer", http_server, is_worker=False): RoomStateEventRestServlet(hs).register(http_server) RoomMemberListRestServlet(hs).register(http_server) @@ -1098,6 +1120,8 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False): RoomEventContextServlet(hs).register(http_server) RoomSpaceSummaryRestServlet(hs).register(http_server) RoomHierarchyRestServlet(hs).register(http_server) + if hs.config.experimental.msc3266_enabled: + RoomSummaryRestServlet(hs).register(http_server) RoomEventServlet(hs).register(http_server) JoinedRoomsRestServlet(hs).register(http_server) RoomAliasListServlet(hs).register(http_server) diff --git a/synapse/server.py b/synapse/server.py index 6c867f0f47..de6517663e 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -99,10 +99,10 @@ from synapse.handlers.room_list import RoomListHandler from synapse.handlers.room_member import RoomMemberHandler, RoomMemberMasterHandler from synapse.handlers.room_member_worker import RoomMemberWorkerHandler +from synapse.handlers.room_summary import RoomSummaryHandler from synapse.handlers.search import SearchHandler from synapse.handlers.send_email import SendEmailHandler from synapse.handlers.set_password import SetPasswordHandler -from synapse.handlers.space_summary import SpaceSummaryHandler from synapse.handlers.sso import SsoHandler from synapse.handlers.stats import StatsHandler from synapse.handlers.sync import SyncHandler @@ -772,8 +772,8 @@ def get_account_data_handler(self) -> AccountDataHandler: return AccountDataHandler(self) @cache_in_self - def get_space_summary_handler(self) -> SpaceSummaryHandler: - return SpaceSummaryHandler(self) + def get_room_summary_handler(self) -> RoomSummaryHandler: + return RoomSummaryHandler(self) @cache_in_self def get_event_auth_handler(self) -> EventAuthHandler: diff --git a/tests/handlers/test_space_summary.py b/tests/handlers/test_room_summary.py similarity index 88% rename from tests/handlers/test_space_summary.py rename to tests/handlers/test_room_summary.py index bc8e131f4a..732d746e38 100644 --- a/tests/handlers/test_space_summary.py +++ b/tests/handlers/test_room_summary.py @@ -23,10 +23,10 @@ RestrictedJoinRuleTypes, RoomTypes, ) -from synapse.api.errors import AuthError, SynapseError +from synapse.api.errors import AuthError, NotFoundError, SynapseError from synapse.api.room_versions import RoomVersions from synapse.events import make_event_from_dict -from synapse.handlers.space_summary import _child_events_comparison_key, _RoomEntry +from synapse.handlers.room_summary import _child_events_comparison_key, _RoomEntry from synapse.rest import admin from synapse.rest.client.v1 import login, room from synapse.server import HomeServer @@ -106,7 +106,7 @@ class SpaceSummaryTestCase(unittest.HomeserverTestCase): def prepare(self, reactor, clock, hs: HomeServer): self.hs = hs - self.handler = self.hs.get_space_summary_handler() + self.handler = self.hs.get_room_summary_handler() # Create a user. self.user = self.register_user("user", "pass") @@ -624,14 +624,14 @@ async def summarize_remote_room( ), ] - async def summarize_remote_room_hiearchy(_self, room, suggested_only): + async def summarize_remote_room_hierarchy(_self, room, suggested_only): return requested_room_entry, {subroom: child_room}, set() # Add a room to the space which is on another server. self._add_child(self.space, subspace, self.token) with mock.patch( - "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room", + "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room", new=summarize_remote_room, ): result = self.get_success( @@ -647,8 +647,8 @@ async def summarize_remote_room_hiearchy(_self, room, suggested_only): self._assert_rooms(result, expected) with mock.patch( - "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy", - new=summarize_remote_room_hiearchy, + "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy", + new=summarize_remote_room_hierarchy, ): result = self.get_success( self.handler.get_room_hierarchy(self.user, self.space) @@ -774,14 +774,14 @@ async def summarize_remote_room( for child_room in children_rooms ] - async def summarize_remote_room_hiearchy(_self, room, suggested_only): + async def summarize_remote_room_hierarchy(_self, room, suggested_only): return subspace_room_entry, dict(children_rooms), set() # Add a room to the space which is on another server. self._add_child(self.space, subspace, self.token) with mock.patch( - "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room", + "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room", new=summarize_remote_room, ): result = self.get_success( @@ -814,8 +814,8 @@ async def summarize_remote_room_hiearchy(_self, room, suggested_only): self._assert_rooms(result, expected) with mock.patch( - "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy", - new=summarize_remote_room_hiearchy, + "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy", + new=summarize_remote_room_hierarchy, ): result = self.get_success( self.handler.get_room_hierarchy(self.user, self.space) @@ -850,14 +850,14 @@ async def summarize_remote_room( ): return [fed_room_entry] - async def summarize_remote_room_hiearchy(_self, room, suggested_only): + async def summarize_remote_room_hierarchy(_self, room, suggested_only): return fed_room_entry, {}, set() # Add a room to the space which is on another server. self._add_child(self.space, fed_room, self.token) with mock.patch( - "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room", + "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room", new=summarize_remote_room, ): result = self.get_success( @@ -872,10 +872,88 @@ async def summarize_remote_room_hiearchy(_self, room, suggested_only): self._assert_rooms(result, expected) with mock.patch( - "synapse.handlers.space_summary.SpaceSummaryHandler._summarize_remote_room_hiearchy", - new=summarize_remote_room_hiearchy, + "synapse.handlers.room_summary.RoomSummaryHandler._summarize_remote_room_hierarchy", + new=summarize_remote_room_hierarchy, ): result = self.get_success( self.handler.get_room_hierarchy(self.user, self.space) ) self._assert_hierarchy(result, expected) + + +class RoomSummaryTestCase(unittest.HomeserverTestCase): + servlets = [ + admin.register_servlets_for_client_rest_resource, + room.register_servlets, + login.register_servlets, + ] + + def prepare(self, reactor, clock, hs: HomeServer): + self.hs = hs + self.handler = self.hs.get_room_summary_handler() + + # Create a user. + self.user = self.register_user("user", "pass") + self.token = self.login("user", "pass") + + # Create a simple room. + self.room = self.helper.create_room_as(self.user, tok=self.token) + self.helper.send_state( + self.room, + event_type=EventTypes.JoinRules, + body={"join_rule": JoinRules.INVITE}, + tok=self.token, + ) + + def test_own_room(self): + """Test a simple room created by the requester.""" + result = self.get_success(self.handler.get_room_summary(self.user, self.room)) + self.assertEqual(result.get("room_id"), self.room) + + def test_visibility(self): + """A user not in a private room cannot get its summary.""" + user2 = self.register_user("user2", "pass") + token2 = self.login("user2", "pass") + + # The user cannot see the room. + self.get_failure(self.handler.get_room_summary(user2, self.room), NotFoundError) + + # If the room is made world-readable it should return a result. + self.helper.send_state( + self.room, + event_type=EventTypes.RoomHistoryVisibility, + body={"history_visibility": HistoryVisibility.WORLD_READABLE}, + tok=self.token, + ) + result = self.get_success(self.handler.get_room_summary(user2, self.room)) + self.assertEqual(result.get("room_id"), self.room) + + # Make it not world-readable again and confirm it results in an error. + self.helper.send_state( + self.room, + event_type=EventTypes.RoomHistoryVisibility, + body={"history_visibility": HistoryVisibility.JOINED}, + tok=self.token, + ) + self.get_failure(self.handler.get_room_summary(user2, self.room), NotFoundError) + + # If the room is made public it should return a result. + self.helper.send_state( + self.room, + event_type=EventTypes.JoinRules, + body={"join_rule": JoinRules.PUBLIC}, + tok=self.token, + ) + result = self.get_success(self.handler.get_room_summary(user2, self.room)) + self.assertEqual(result.get("room_id"), self.room) + + # Join the space, make it invite-only again and results should be returned. + self.helper.join(self.room, user2, tok=token2) + self.helper.send_state( + self.room, + event_type=EventTypes.JoinRules, + body={"join_rule": JoinRules.INVITE}, + tok=self.token, + ) + result = self.get_success(self.handler.get_room_summary(user2, self.room)) + self.assertEqual(result.get("room_id"), self.room) From 5af83efe8d106ee6fe6568f6758d458159341531 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 16 Aug 2021 12:01:30 -0400 Subject: [PATCH 569/619] Validate the max_rooms_per_space parameter to ensure it is non-negative. (#10611) --- changelog.d/10611.bugfix | 1 + .../federation/transport/server/federation.py | 22 +++++++++++++--- synapse/rest/client/v1/room.py | 25 +++++++++++++++---- 3 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10611.bugfix diff --git a/changelog.d/10611.bugfix b/changelog.d/10611.bugfix new file mode 100644 index 0000000000..ecbe408b47 --- /dev/null +++ b/changelog.d/10611.bugfix @@ -0,0 +1 @@ +Additional validation for the spaces summary API to avoid errors like `ValueError: Stop argument for islice() must be None or an integer`. The missing validation has existed since v1.31.0. diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py index 7d81cc642c..2fdf6cc99e 100644 --- a/synapse/federation/transport/server/federation.py +++ b/synapse/federation/transport/server/federation.py @@ -557,7 +557,14 @@ async def on_GET( room_id: str, ) -> Tuple[int, JsonDict]: suggested_only = parse_boolean_from_args(query, "suggested_only", default=False) + max_rooms_per_space = parse_integer_from_args(query, "max_rooms_per_space") + if max_rooms_per_space is not None and max_rooms_per_space < 0: + raise SynapseError( + 400, + "Value for 'max_rooms_per_space' must be a non-negative integer", + Codes.BAD_JSON, + ) exclude_rooms = parse_strings_from_args(query, "exclude_rooms", default=[]) @@ -586,10 +593,17 @@ async def on_POST( raise SynapseError(400, "bad value for 'exclude_rooms'", Codes.BAD_JSON) max_rooms_per_space = content.get("max_rooms_per_space") - if max_rooms_per_space is not None and not isinstance(max_rooms_per_space, int): - raise SynapseError( - 400, "bad value for 'max_rooms_per_space'", Codes.BAD_JSON - ) + if max_rooms_per_space is not None: + if not isinstance(max_rooms_per_space, int): + raise SynapseError( + 400, "bad value for 'max_rooms_per_space'", Codes.BAD_JSON + ) + if max_rooms_per_space < 0: + raise SynapseError( + 400, + "Value for 'max_rooms_per_space' must be a non-negative integer", + Codes.BAD_JSON, + ) return 200, await self.handler.federation_space_summary( origin, room_id, suggested_only, max_rooms_per_space, exclude_rooms diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/v1/room.py index d3882a84e2..ba7250ad8e 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/v1/room.py @@ -993,11 +993,19 @@ async def on_GET( ) -> Tuple[int, JsonDict]: requester = await self._auth.get_user_by_req(request, allow_guest=True) + max_rooms_per_space = parse_integer(request, "max_rooms_per_space") + if max_rooms_per_space is not None and max_rooms_per_space < 0: + raise SynapseError( + 400, + "Value for 'max_rooms_per_space' must be a non-negative integer", + Codes.BAD_JSON, + ) + return 200, await self._room_summary_handler.get_space_summary( requester.user.to_string(), room_id, suggested_only=parse_boolean(request, "suggested_only", default=False), - max_rooms_per_space=parse_integer(request, "max_rooms_per_space"), + max_rooms_per_space=max_rooms_per_space, ) # TODO When switching to the stable endpoint, remove the POST handler. @@ -1014,10 +1022,17 @@ async def on_POST( ) max_rooms_per_space = content.get("max_rooms_per_space") - if max_rooms_per_space is not None and not isinstance(max_rooms_per_space, int): - raise SynapseError( - 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON - ) + if max_rooms_per_space is not None: + if not isinstance(max_rooms_per_space, int): + raise SynapseError( + 400, "'max_rooms_per_space' must be an integer", Codes.BAD_JSON + ) + if max_rooms_per_space < 0: + raise SynapseError( + 400, + "Value for 'max_rooms_per_space' must be a non-negative integer", + Codes.BAD_JSON, + ) return 200, await self._room_summary_handler.get_space_summary( requester.user.to_string(), From 0db8cab72c8a39b4e8154295d473fbbc154854b4 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Mon, 16 Aug 2021 18:09:47 +0100 Subject: [PATCH 570/619] Update CONTRIBUTING.md to fix index links and SyTest instructions (#10599) Signed-off-by: Olivier Wilkinson (reivilibre) --- CONTRIBUTING.md | 7 ++++--- changelog.d/10599.doc | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10599.doc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4486a4b2cd..cd6c34df85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,8 +13,9 @@ This document aims to get you started with contributing to this repo! - [7. Turn coffee and documentation into code and documentation!](#7-turn-coffee-and-documentation-into-code-and-documentation) - [8. Test, test, test!](#8-test-test-test) * [Run the linters.](#run-the-linters) - * [Run the unit tests.](#run-the-unit-tests) - * [Run the integration tests.](#run-the-integration-tests) + * [Run the unit tests.](#run-the-unit-tests-twisted-trial) + * [Run the integration tests (SyTest).](#run-the-integration-tests-sytest) + * [Run the integration tests (Complement).](#run-the-integration-tests-complement) - [9. Submit your patch.](#9-submit-your-patch) * [Changelog](#changelog) + [How do I know what to call the changelog file before I create the PR?](#how-do-i-know-what-to-call-the-changelog-file-before-i-create-the-pr) @@ -197,7 +198,7 @@ The following command will let you run the integration test with the most common configuration: ```sh -$ docker run --rm -it -v /path/where/you/have/cloned/the/repository\:/src:ro -v /path/to/where/you/want/logs\:/logs matrixdotorg/sytest-synapse:py37 +$ docker run --rm -it -v /path/where/you/have/cloned/the/repository\:/src:ro -v /path/to/where/you/want/logs\:/logs matrixdotorg/sytest-synapse:buster ``` This configuration should generally cover your needs. For more details about other configurations, see [documentation in the SyTest repo](https://github.com/matrix-org/sytest/blob/develop/docker/README.md). diff --git a/changelog.d/10599.doc b/changelog.d/10599.doc new file mode 100644 index 0000000000..66e72078f0 --- /dev/null +++ b/changelog.d/10599.doc @@ -0,0 +1 @@ +Update CONTRIBUTING.md to fix index links and the instructions for SyTest in docker. From 19e51b14d23f756883688fd8238da61c6ff29cc3 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Mon, 16 Aug 2021 18:11:48 +0100 Subject: [PATCH 571/619] Manhole: wrap coroutines in `defer.ensureDeferred` automatically (#10602) --- changelog.d/10602.feature | 1 + docs/manhole.md | 2 +- synapse/util/manhole.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10602.feature diff --git a/changelog.d/10602.feature b/changelog.d/10602.feature new file mode 100644 index 0000000000..ab18291a20 --- /dev/null +++ b/changelog.d/10602.feature @@ -0,0 +1 @@ +The Synapse manhole no longer needs coroutines to be wrapped in `defer.ensureDeferred`. diff --git a/docs/manhole.md b/docs/manhole.md index 37d1d7823c..db92df88dc 100644 --- a/docs/manhole.md +++ b/docs/manhole.md @@ -67,7 +67,7 @@ This gives a Python REPL in which `hs` gives access to the `synapse.server.HomeServer` object - which in turn gives access to many other parts of the process. -Note that any call which returns a coroutine will need to be wrapped in `ensureDeferred`. +Note that, prior to Synapse 1.41, any call which returns a coroutine will need to be wrapped in `ensureDeferred`. As a simple example, retrieving an event from the database: diff --git a/synapse/util/manhole.py b/synapse/util/manhole.py index da24ba0470..522daa323d 100644 --- a/synapse/util/manhole.py +++ b/synapse/util/manhole.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import inspect import sys import traceback @@ -20,6 +21,7 @@ from twisted.conch.manhole import ColoredManhole, ManholeInterpreter from twisted.conch.ssh.keys import Key from twisted.cred import checkers, portal +from twisted.internet import defer PUBLIC_KEY = ( "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDHhGATaW4KhE23+7nrH4jFx3yLq9OjaEs5" @@ -141,3 +143,15 @@ def showtraceback(self): self.write("".join(lines)) finally: last_tb = ei = None + + def displayhook(self, obj): + """ + We override the displayhook so that we automatically convert coroutines + into Deferreds. (Our superclass' displayhook will take care of the rest, + by displaying the Deferred if it's ready, or registering a callback + if it's not). + """ + if inspect.iscoroutine(obj): + super().displayhook(defer.ensureDeferred(obj)) + else: + super().displayhook(obj) From a933c2c7d8ef49c3c98ef443d959f955600bfb6b Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 17 Aug 2021 10:52:38 +0100 Subject: [PATCH 572/619] Add an admin API to check if a username is available (#10578) This adds a new API GET /_synapse/admin/v1/username_available?username=foo to check if a username is available. It is the counterpart to https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-register-available, except that it works even if registration is disabled. --- changelog.d/10578.feature | 1 + docs/admin_api/user_admin_api.md | 20 +++++++ synapse/rest/admin/__init__.py | 2 + synapse/rest/admin/username_available.py | 51 +++++++++++++++++ tests/rest/admin/test_username_available.py | 62 +++++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 changelog.d/10578.feature create mode 100644 synapse/rest/admin/username_available.py create mode 100644 tests/rest/admin/test_username_available.py diff --git a/changelog.d/10578.feature b/changelog.d/10578.feature new file mode 100644 index 0000000000..02397f0009 --- /dev/null +++ b/changelog.d/10578.feature @@ -0,0 +1 @@ +Add an admin API (`GET /_synapse/admin/username_available`) to check if a username is available (regardless of registration settings). \ No newline at end of file diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 33811f5bbb..4b5dd4685a 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -1057,3 +1057,23 @@ The following parameters should be set in the URL: - `user_id` - The fully qualified MXID: for example, `@user:server.com`. The user must be local. + +### Check username availability + +Checks to see if a username is available, and valid, for the server. See [the client-server +API](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-register-available) +for more information. + +This endpoint will work even if registration is disabled on the server, unlike +`/_matrix/client/r0/register/available`. + +The API is: + +``` +POST /_synapse/admin/v1/username_availabile?username=$localpart +``` + +The request and response format is the same as the [/_matrix/client/r0/register/available](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-register-available) API. + +To use it, you will need to authenticate by providing an `access_token` for a +server admin: [Admin API](../usage/administration/admin_api) diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index abf749b001..8a91068092 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -51,6 +51,7 @@ ) from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet from synapse.rest.admin.statistics import UserMediaStatisticsRestServlet +from synapse.rest.admin.username_available import UsernameAvailableRestServlet from synapse.rest.admin.users import ( AccountValidityRenewServlet, DeactivateAccountRestServlet, @@ -241,6 +242,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: ForwardExtremitiesRestServlet(hs).register(http_server) RoomEventContextServlet(hs).register(http_server) RateLimitRestServlet(hs).register(http_server) + UsernameAvailableRestServlet(hs).register(http_server) def register_servlets_for_client_rest_resource( diff --git a/synapse/rest/admin/username_available.py b/synapse/rest/admin/username_available.py new file mode 100644 index 0000000000..2bf1472967 --- /dev/null +++ b/synapse/rest/admin/username_available.py @@ -0,0 +1,51 @@ +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from http import HTTPStatus +from typing import TYPE_CHECKING, Tuple + +from synapse.http.servlet import RestServlet, parse_string +from synapse.http.site import SynapseRequest +from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin +from synapse.types import JsonDict + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +class UsernameAvailableRestServlet(RestServlet): + """An admin API to check if a given username is available, regardless of whether registration is enabled. + + Example: + GET /_synapse/admin/v1/username_available?username=foo + 200 OK + { + "available": true + } + """ + + PATTERNS = admin_patterns("/username_available") + + def __init__(self, hs: "HomeServer"): + self.auth = hs.get_auth() + self.registration_handler = hs.get_registration_handler() + + async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]: + await assert_requester_is_admin(self.auth, request) + + username = parse_string(request, "username", required=True) + await self.registration_handler.check_username(username) + return HTTPStatus.OK, {"available": True} diff --git a/tests/rest/admin/test_username_available.py b/tests/rest/admin/test_username_available.py new file mode 100644 index 0000000000..53cbc8ddab --- /dev/null +++ b/tests/rest/admin/test_username_available.py @@ -0,0 +1,62 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import synapse.rest.admin +from synapse.api.errors import Codes, SynapseError +from synapse.rest.client.v1 import login + +from tests import unittest + + +class UsernameAvailableTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + ] + url = "/_synapse/admin/v1/username_available" + + def prepare(self, reactor, clock, hs): + self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + async def check_username(username): + if username == "allowed": + return True + raise SynapseError(400, "User ID already taken.", errcode=Codes.USER_IN_USE) + + handler = self.hs.get_registration_handler() + handler.check_username = check_username + + def test_username_available(self): + """ + The endpoint should return a 200 response if the username does not exist + """ + + url = "%s?username=%s" % (self.url, "allowed") + channel = self.make_request("GET", url, None, self.admin_user_tok) + + self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertTrue(channel.json_body["available"]) + + def test_username_unavailable(self): + """ + The endpoint should return a 200 response if the username does not exist + """ + + url = "%s?username=%s" % (self.url, "disallowed") + channel = self.make_request("GET", url, None, self.admin_user_tok) + + self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEqual(channel.json_body["errcode"], "M_USER_IN_USE") + self.assertEqual(channel.json_body["error"], "User ID already taken.") From ae2714c1f31f2a843e19dc44501784401181162c Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 17 Aug 2021 12:23:14 +0200 Subject: [PATCH 573/619] Allow using several custom template directories (#10587) Allow using several directories in read_templates. --- changelog.d/10587.misc | 1 + synapse/config/_base.py | 43 +++++++++++--------- synapse/config/account_validity.py | 2 +- synapse/config/emailconfig.py | 8 ++-- synapse/config/sso.py | 2 +- synapse/module_api/__init__.py | 5 ++- tests/config/test_base.py | 64 ++++++++++++++++++++++++++++-- 7 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 changelog.d/10587.misc diff --git a/changelog.d/10587.misc b/changelog.d/10587.misc new file mode 100644 index 0000000000..4c6167977c --- /dev/null +++ b/changelog.d/10587.misc @@ -0,0 +1 @@ +Allow multiple custom directories in `read_templates`. diff --git a/synapse/config/_base.py b/synapse/config/_base.py index d6ec618f8f..2cc242782a 100644 --- a/synapse/config/_base.py +++ b/synapse/config/_base.py @@ -237,13 +237,14 @@ def read_template(self, filename: str) -> jinja2.Template: def read_templates( self, filenames: List[str], - custom_template_directory: Optional[str] = None, + custom_template_directories: Optional[Iterable[str]] = None, ) -> List[jinja2.Template]: """Load a list of template files from disk using the given variables. This function will attempt to load the given templates from the default Synapse - template directory. If `custom_template_directory` is supplied, that directory - is tried first. + template directory. If `custom_template_directories` is supplied, any directory + in this list is tried (in the order they appear in the list) before trying + Synapse's default directory. Files read are treated as Jinja templates. The templates are not rendered yet and have autoescape enabled. @@ -251,8 +252,8 @@ def read_templates( Args: filenames: A list of template filenames to read. - custom_template_directory: A directory to try to look for the templates - before using the default Synapse template directory instead. + custom_template_directories: A list of directory to try to look for the + templates before using the default Synapse template directory instead. Raises: ConfigError: if the file's path is incorrect or otherwise cannot be read. @@ -260,20 +261,26 @@ def read_templates( Returns: A list of jinja2 templates. """ - search_directories = [self.default_template_dir] - - # The loader will first look in the custom template directory (if specified) for the - # given filename. If it doesn't find it, it will use the default template dir instead - if custom_template_directory: - # Check that the given template directory exists - if not self.path_exists(custom_template_directory): - raise ConfigError( - "Configured template directory does not exist: %s" - % (custom_template_directory,) - ) + search_directories = [] + + # The loader will first look in the custom template directories (if specified) + # for the given filename. If it doesn't find it, it will use the default + # template dir instead. + if custom_template_directories is not None: + for custom_template_directory in custom_template_directories: + # Check that the given template directory exists + if not self.path_exists(custom_template_directory): + raise ConfigError( + "Configured template directory does not exist: %s" + % (custom_template_directory,) + ) + + # Search the custom template directory as well + search_directories.append(custom_template_directory) - # Search the custom template directory as well - search_directories.insert(0, custom_template_directory) + # Append the default directory at the end of the list so Jinja can fallback on it + # if a template is missing from any custom directory. + search_directories.append(self.default_template_dir) # TODO: switch to synapse.util.templates.build_jinja_env loader = jinja2.FileSystemLoader(search_directories) diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index 6be4eafe55..9acce5996e 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -88,5 +88,5 @@ def read_config(self, config, **kwargs): "account_previously_renewed.html", invalid_token_template_filename, ], - account_validity_template_dir, + (td for td in (account_validity_template_dir,) if td), ) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 42526502f0..fc74b4a8b9 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -257,7 +257,9 @@ def read_config(self, config, **kwargs): registration_template_success_html, add_threepid_template_success_html, ], - template_dir, + ( + td for td in (template_dir,) if td + ), # Filter out template_dir if not provided ) # Render templates that do not contain any placeholders @@ -297,7 +299,7 @@ def read_config(self, config, **kwargs): self.email_notif_template_text, ) = self.read_templates( [notif_template_html, notif_template_text], - template_dir, + (td for td in (template_dir,) if td), ) self.email_notif_for_new_users = email_config.get( @@ -320,7 +322,7 @@ def read_config(self, config, **kwargs): self.account_validity_template_text, ) = self.read_templates( [expiry_template_html, expiry_template_text], - template_dir, + (td for td in (template_dir,) if td), ) subjects_config = email_config.get("subjects", {}) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index d0f04cf8e6..4b590e0535 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -63,7 +63,7 @@ def read_config(self, config, **kwargs): "sso_auth_success.html", "sso_auth_bad_user.html", ], - self.sso_template_dir, + (td for td in (self.sso_template_dir,) if td), ) # These templates have no placeholders, so render them here diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 1cc13fc97b..82725853bc 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -677,7 +677,10 @@ def read_templates( A list containing the loaded templates, with the orders matching the one of the filenames parameter. """ - return self._hs.config.read_templates(filenames, custom_template_directory) + return self._hs.config.read_templates( + filenames, + (td for td in (custom_template_directory,) if td), + ) class PublicRoomListManager: diff --git a/tests/config/test_base.py b/tests/config/test_base.py index 84ae3b88ae..baa5313fb3 100644 --- a/tests/config/test_base.py +++ b/tests/config/test_base.py @@ -30,7 +30,7 @@ def test_loading_missing_templates(self): # contain template files with tempfile.TemporaryDirectory() as tmp_dir: # Attempt to load an HTML template from our custom template directory - template = self.hs.config.read_templates(["sso_error.html"], tmp_dir)[0] + template = self.hs.config.read_templates(["sso_error.html"], (tmp_dir,))[0] # If no errors, we should've gotten the default template instead @@ -60,7 +60,7 @@ def test_loading_custom_templates(self): # Attempt to load the template from our custom template directory template = ( - self.hs.config.read_templates([template_filename], tmp_dir) + self.hs.config.read_templates([template_filename], (tmp_dir,)) )[0] # Render the template @@ -74,8 +74,66 @@ def test_loading_custom_templates(self): "Template file did not contain our test string", ) + def test_multiple_custom_template_directories(self): + """Tests that directories are searched in the right order if multiple custom + template directories are provided. + """ + # Create two temporary directories on the filesystem. + tempdirs = [ + tempfile.TemporaryDirectory(), + tempfile.TemporaryDirectory(), + ] + + # Create one template in each directory, whose content is the index of the + # directory in the list. + template_filename = "my_template.html.j2" + for i in range(len(tempdirs)): + tempdir = tempdirs[i] + template_path = os.path.join(tempdir.name, template_filename) + + with open(template_path, "w") as fp: + fp.write(str(i)) + fp.flush() + + # Retrieve the template. + template = ( + self.hs.config.read_templates( + [template_filename], + (td.name for td in tempdirs), + ) + )[0] + + # Test that we got the template we dropped in the first directory in the list. + self.assertEqual(template.render(), "0") + + # Add another template, this one only in the second directory in the list, so we + # can test that the second directory is still searched into when no matching file + # could be found in the first one. + other_template_name = "my_other_template.html.j2" + other_template_path = os.path.join(tempdirs[1].name, other_template_name) + + with open(other_template_path, "w") as fp: + fp.write("hello world") + fp.flush() + + # Retrieve the template. + template = ( + self.hs.config.read_templates( + [other_template_name], + (td.name for td in tempdirs), + ) + )[0] + + # Test that the file has the expected content. + self.assertEqual(template.render(), "hello world") + + # Cleanup the temporary directories manually since we're not using a context + # manager. + for td in tempdirs: + td.cleanup() + def test_loading_template_from_nonexistent_custom_directory(self): with self.assertRaises(ConfigError): self.hs.config.read_templates( - ["some_filename.html"], "a_nonexistent_directory" + ["some_filename.html"], ("a_nonexistent_directory",) ) From 58f0d97275e9ffc134f9aaf59ce01c0e745ec041 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 17 Aug 2021 11:45:35 +0100 Subject: [PATCH 574/619] update links to schema doc (#10620) --- changelog.d/10620.misc | 1 + synapse/storage/schema/README.md | 2 +- synapse/storage/schema/__init__.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 changelog.d/10620.misc diff --git a/changelog.d/10620.misc b/changelog.d/10620.misc new file mode 100644 index 0000000000..8b29668a1f --- /dev/null +++ b/changelog.d/10620.misc @@ -0,0 +1 @@ +Fix up a couple of links to the database schema documentation. diff --git a/synapse/storage/schema/README.md b/synapse/storage/schema/README.md index 729f44ea6c..4fc2061a3d 100644 --- a/synapse/storage/schema/README.md +++ b/synapse/storage/schema/README.md @@ -1,4 +1,4 @@ # Synapse Database Schemas This directory contains the schema files used to build Synapse databases. For more -information, see /docs/development/database_schema.md. +information, see https://matrix-org.github.io/synapse/develop/development/database_schema.html. diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index fd4dd67d91..7e0687e197 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -19,8 +19,8 @@ shape of the database schema (even if those requirements are backwards-compatible with older versions of Synapse). -See `README.md `_ for more information on how this -works. +See https://matrix-org.github.io/synapse/develop/development/database_schema.html +for more information on how this works. Changes in SCHEMA_VERSION = 61: - The `user_stats_historical` and `room_stats_historical` tables are not written and From 3bcd525b46678ff228c4275acad47c12974c9a33 Mon Sep 17 00:00:00 2001 From: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:56:11 +0200 Subject: [PATCH 575/619] Allow to edit `external_ids` by Edit User admin API (#10598) Signed-off-by: Dirk Klimpel dirk@klimpel.org --- changelog.d/10598.feature | 1 + docs/admin_api/user_admin_api.md | 40 ++- synapse/rest/admin/users.py | 139 +++++++---- .../storage/databases/main/registration.py | 22 ++ tests/rest/admin/test_user.py | 227 +++++++++++++++--- 5 files changed, 340 insertions(+), 89 deletions(-) create mode 100644 changelog.d/10598.feature diff --git a/changelog.d/10598.feature b/changelog.d/10598.feature new file mode 100644 index 0000000000..92c159118b --- /dev/null +++ b/changelog.d/10598.feature @@ -0,0 +1 @@ +Allow editing a user's `external_ids` via the "Edit User" admin API. Contributed by @dklimpel. \ No newline at end of file diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 4b5dd4685a..6a9335d6ec 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -81,6 +81,16 @@ with a body of: "address": "" } ], + "external_ids": [ + { + "auth_provider": "", + "external_id": "" + }, + { + "auth_provider": "", + "external_id": "" + } + ], "avatar_url": "", "admin": false, "deactivated": false @@ -90,26 +100,34 @@ with a body of: To use it, you will need to authenticate by providing an `access_token` for a server admin: [Admin API](../usage/administration/admin_api) +Returns HTTP status code: +- `201` - When a new user object was created. +- `200` - When a user was modified. + URL parameters: - `user_id`: fully-qualified user id: for example, `@user:server.com`. Body parameters: -- `password`, optional. If provided, the user's password is updated and all +- `password` - string, optional. If provided, the user's password is updated and all devices are logged out. - -- `displayname`, optional, defaults to the value of `user_id`. - -- `threepids`, optional, allows setting the third-party IDs (email, msisdn) +- `displayname` - string, optional, defaults to the value of `user_id`. +- `threepids` - array, optional, allows setting the third-party IDs (email, msisdn) + - `medium` - string. Kind of third-party ID, either `email` or `msisdn`. + - `address` - string. Value of third-party ID. belonging to a user. - -- `avatar_url`, optional, must be a +- `external_ids` - array, optional. Allow setting the identifier of the external identity + provider for SSO (Single sign-on). Details in + [Sample Configuration File](../usage/configuration/homeserver_sample_config.html) + section `sso` and `oidc_providers`. + - `auth_provider` - string. ID of the external identity provider. Value of `idp_id` + in homeserver configuration. + - `external_id` - string, user ID in the external identity provider. +- `avatar_url` - string, optional, must be a [MXC URI](https://matrix.org/docs/spec/client_server/r0.6.0#matrix-content-mxc-uris). - -- `admin`, optional, defaults to `false`. - -- `deactivated`, optional. If unspecified, deactivation state will be left +- `admin` - bool, optional, defaults to `false`. +- `deactivated` - bool, optional. If unspecified, deactivation state will be left unchanged on existing accounts and set to `false` for new accounts. A user cannot be erased by deactivating with this API. For details on deactivating users see [Deactivate Account](#deactivate-account). diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 41f21ba118..c885fd77ab 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -196,20 +196,57 @@ async def on_PUT( user = await self.admin_handler.get_user(target_user) user_id = target_user.to_string() + # check for required parameters for each threepid + threepids = body.get("threepids") + if threepids is not None: + for threepid in threepids: + assert_params_in_dict(threepid, ["medium", "address"]) + + # check for required parameters for each external_id + external_ids = body.get("external_ids") + if external_ids is not None: + for external_id in external_ids: + assert_params_in_dict(external_id, ["auth_provider", "external_id"]) + + user_type = body.get("user_type", None) + if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES: + raise SynapseError(400, "Invalid user type") + + set_admin_to = body.get("admin", False) + if not isinstance(set_admin_to, bool): + raise SynapseError( + HTTPStatus.BAD_REQUEST, + "Param 'admin' must be a boolean, if given", + Codes.BAD_JSON, + ) + + password = body.get("password", None) + if password is not None: + if not isinstance(password, str) or len(password) > 512: + raise SynapseError(400, "Invalid password") + + deactivate = body.get("deactivated", False) + if not isinstance(deactivate, bool): + raise SynapseError(400, "'deactivated' parameter is not of type boolean") + + # convert into List[Tuple[str, str]] + if external_ids is not None: + new_external_ids = [] + for external_id in external_ids: + new_external_ids.append( + (external_id["auth_provider"], external_id["external_id"]) + ) + if user: # modify user if "displayname" in body: await self.profile_handler.set_displayname( target_user, requester, body["displayname"], True ) - if "threepids" in body: - # check for required parameters for each threepid - for threepid in body["threepids"]: - assert_params_in_dict(threepid, ["medium", "address"]) - + if threepids is not None: # remove old threepids from user - threepids = await self.store.user_get_threepids(user_id) - for threepid in threepids: + old_threepids = await self.store.user_get_threepids(user_id) + for threepid in old_threepids: try: await self.auth_handler.delete_threepid( user_id, threepid["medium"], threepid["address"], None @@ -220,18 +257,39 @@ async def on_PUT( # add new threepids to user current_time = self.hs.get_clock().time_msec() - for threepid in body["threepids"]: + for threepid in threepids: await self.auth_handler.add_threepid( user_id, threepid["medium"], threepid["address"], current_time ) - if "avatar_url" in body and type(body["avatar_url"]) == str: + if external_ids is not None: + # get changed external_ids (added and removed) + cur_external_ids = await self.store.get_external_ids_by_user(user_id) + add_external_ids = set(new_external_ids) - set(cur_external_ids) + del_external_ids = set(cur_external_ids) - set(new_external_ids) + + # remove old external_ids + for auth_provider, external_id in del_external_ids: + await self.store.remove_user_external_id( + auth_provider, + external_id, + user_id, + ) + + # add new external_ids + for auth_provider, external_id in add_external_ids: + await self.store.record_user_external_id( + auth_provider, + external_id, + user_id, + ) + + if "avatar_url" in body and isinstance(body["avatar_url"], str): await self.profile_handler.set_avatar_url( target_user, requester, body["avatar_url"], True ) if "admin" in body: - set_admin_to = bool(body["admin"]) if set_admin_to != user["admin"]: auth_user = requester.user if target_user == auth_user and not set_admin_to: @@ -239,29 +297,18 @@ async def on_PUT( await self.store.set_server_admin(target_user, set_admin_to) - if "password" in body: - if not isinstance(body["password"], str) or len(body["password"]) > 512: - raise SynapseError(400, "Invalid password") - else: - new_password = body["password"] - logout_devices = True - - new_password_hash = await self.auth_handler.hash(new_password) - - await self.set_password_handler.set_password( - target_user.to_string(), - new_password_hash, - logout_devices, - requester, - ) + if password is not None: + logout_devices = True + new_password_hash = await self.auth_handler.hash(password) + + await self.set_password_handler.set_password( + target_user.to_string(), + new_password_hash, + logout_devices, + requester, + ) if "deactivated" in body: - deactivate = body["deactivated"] - if not isinstance(deactivate, bool): - raise SynapseError( - 400, "'deactivated' parameter is not of type boolean" - ) - if deactivate and not user["deactivated"]: await self.deactivate_account_handler.deactivate_account( target_user.to_string(), False, requester, by_admin=True @@ -285,36 +332,24 @@ async def on_PUT( return 200, user else: # create user - password = body.get("password") + displayname = body.get("displayname", None) + password_hash = None if password is not None: - if not isinstance(password, str) or len(password) > 512: - raise SynapseError(400, "Invalid password") password_hash = await self.auth_handler.hash(password) - admin = body.get("admin", None) - user_type = body.get("user_type", None) - displayname = body.get("displayname", None) - - if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES: - raise SynapseError(400, "Invalid user type") - user_id = await self.registration_handler.register_user( localpart=target_user.localpart, password_hash=password_hash, - admin=bool(admin), + admin=set_admin_to, default_display_name=displayname, user_type=user_type, by_admin=True, ) - if "threepids" in body: - # check for required parameters for each threepid - for threepid in body["threepids"]: - assert_params_in_dict(threepid, ["medium", "address"]) - + if threepids is not None: current_time = self.hs.get_clock().time_msec() - for threepid in body["threepids"]: + for threepid in threepids: await self.auth_handler.add_threepid( user_id, threepid["medium"], threepid["address"], current_time ) @@ -334,6 +369,14 @@ async def on_PUT( data={}, ) + if external_ids is not None: + for auth_provider, external_id in new_external_ids: + await self.store.record_user_external_id( + auth_provider, + external_id, + user_id, + ) + if "avatar_url" in body and isinstance(body["avatar_url"], str): await self.profile_handler.set_avatar_url( target_user, requester, body["avatar_url"], True diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py index 14670c2881..c67bea81c6 100644 --- a/synapse/storage/databases/main/registration.py +++ b/synapse/storage/databases/main/registration.py @@ -599,6 +599,28 @@ async def record_user_external_id( desc="record_user_external_id", ) + async def remove_user_external_id( + self, auth_provider: str, external_id: str, user_id: str + ) -> None: + """Remove a mapping from an external user id to a mxid + + If the mapping is not found, this method does nothing. + + Args: + auth_provider: identifier for the remote auth provider + external_id: id on that system + user_id: complete mxid that it is mapped to + """ + await self.db_pool.simple_delete( + table="user_external_ids", + keyvalues={ + "auth_provider": auth_provider, + "external_id": external_id, + "user_id": user_id, + }, + desc="remove_user_external_id", + ) + async def get_user_by_external_id( self, auth_provider: str, external_id: str ) -> Optional[str]: diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index 13fab5579b..a736ec4754 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -1240,56 +1240,114 @@ def test_user_does_not_exist(self): self.assertEqual(404, channel.code, msg=channel.json_body) self.assertEqual("M_NOT_FOUND", channel.json_body["errcode"]) - def test_get_user(self): + def test_invalid_parameter(self): """ - Test a simple get of a user. + If parameters are invalid, an error is returned. """ + + # admin not bool channel = self.make_request( - "GET", + "PUT", self.url_other_user, access_token=self.admin_user_tok, + content={"admin": "not_bool"}, ) + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.BAD_JSON, channel.json_body["errcode"]) - self.assertEqual(200, channel.code, msg=channel.json_body) - self.assertEqual("@user:test", channel.json_body["name"]) - self.assertEqual("User", channel.json_body["displayname"]) - self._check_fields(channel.json_body) + # deactivated not bool + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={"deactivated": "not_bool"}, + ) + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) - def test_get_user_with_sso(self): - """ - Test get a user with SSO details. - """ - self.get_success( - self.store.record_user_external_id( - "auth_provider1", "external_id1", self.other_user - ) + # password not str + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={"password": True}, ) - self.get_success( - self.store.record_user_external_id( - "auth_provider2", "external_id2", self.other_user - ) + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) + + # password not length + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={"password": "x" * 513}, ) + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) + # user_type not valid channel = self.make_request( - "GET", + "PUT", self.url_other_user, access_token=self.admin_user_tok, + content={"user_type": "new type"}, ) + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"]) - self.assertEqual(200, channel.code, msg=channel.json_body) - self.assertEqual("@user:test", channel.json_body["name"]) - self.assertEqual( - "external_id1", channel.json_body["external_ids"][0]["external_id"] + # external_ids not valid + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={ + "external_ids": {"auth_provider": "prov", "wrong_external_id": "id"} + }, ) - self.assertEqual( - "auth_provider1", channel.json_body["external_ids"][0]["auth_provider"] + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"]) + + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={"external_ids": {"external_id": "id"}}, ) - self.assertEqual( - "external_id2", channel.json_body["external_ids"][1]["external_id"] + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"]) + + # threepids not valid + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={"threepids": {"medium": "email", "wrong_address": "id"}}, ) - self.assertEqual( - "auth_provider2", channel.json_body["external_ids"][1]["auth_provider"] + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"]) + + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={"threepids": {"address": "value"}}, ) + self.assertEqual(400, channel.code, msg=channel.json_body) + self.assertEqual(Codes.MISSING_PARAM, channel.json_body["errcode"]) + + def test_get_user(self): + """ + Test a simple get of a user. + """ + channel = self.make_request( + "GET", + self.url_other_user, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual("User", channel.json_body["displayname"]) self._check_fields(channel.json_body) def test_create_server_admin(self): @@ -1353,6 +1411,12 @@ def test_create_user(self): "admin": False, "displayname": "Bob's name", "threepids": [{"medium": "email", "address": "bob@bob.bob"}], + "external_ids": [ + { + "external_id": "external_id1", + "auth_provider": "auth_provider1", + }, + ], "avatar_url": "mxc://fibble/wibble", } @@ -1368,6 +1432,12 @@ def test_create_user(self): self.assertEqual("Bob's name", channel.json_body["displayname"]) self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob@bob.bob", channel.json_body["threepids"][0]["address"]) + self.assertEqual( + "external_id1", channel.json_body["external_ids"][0]["external_id"] + ) + self.assertEqual( + "auth_provider1", channel.json_body["external_ids"][0]["auth_provider"] + ) self.assertFalse(channel.json_body["admin"]) self.assertEqual("mxc://fibble/wibble", channel.json_body["avatar_url"]) self._check_fields(channel.json_body) @@ -1632,6 +1702,103 @@ def test_set_threepid(self): self.assertEqual("email", channel.json_body["threepids"][0]["medium"]) self.assertEqual("bob3@bob.bob", channel.json_body["threepids"][0]["address"]) + def test_set_external_id(self): + """ + Test setting external id for an other user. + """ + + # Add two external_ids + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={ + "external_ids": [ + { + "external_id": "external_id1", + "auth_provider": "auth_provider1", + }, + { + "external_id": "external_id2", + "auth_provider": "auth_provider2", + }, + ] + }, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(2, len(channel.json_body["external_ids"])) + # result does not always have the same sort order, therefore it becomes sorted + self.assertEqual( + sorted(channel.json_body["external_ids"], key=lambda k: k["auth_provider"]), + [ + {"auth_provider": "auth_provider1", "external_id": "external_id1"}, + {"auth_provider": "auth_provider2", "external_id": "external_id2"}, + ], + ) + self._check_fields(channel.json_body) + + # Set a new and remove an external_id + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={ + "external_ids": [ + { + "external_id": "external_id2", + "auth_provider": "auth_provider2", + }, + { + "external_id": "external_id3", + "auth_provider": "auth_provider3", + }, + ] + }, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(2, len(channel.json_body["external_ids"])) + self.assertEqual( + channel.json_body["external_ids"], + [ + {"auth_provider": "auth_provider2", "external_id": "external_id2"}, + {"auth_provider": "auth_provider3", "external_id": "external_id3"}, + ], + ) + self._check_fields(channel.json_body) + + # Get user + channel = self.make_request( + "GET", + self.url_other_user, + access_token=self.admin_user_tok, + ) + + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual( + channel.json_body["external_ids"], + [ + {"auth_provider": "auth_provider2", "external_id": "external_id2"}, + {"auth_provider": "auth_provider3", "external_id": "external_id3"}, + ], + ) + self._check_fields(channel.json_body) + + # Remove external_ids + channel = self.make_request( + "PUT", + self.url_other_user, + access_token=self.admin_user_tok, + content={"external_ids": []}, + ) + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual("@user:test", channel.json_body["name"]) + self.assertEqual(0, len(channel.json_body["external_ids"])) + def test_deactivate_user(self): """ Test deactivating another user. From b62eba770522fde7bf1204eb5771ee24d9a5e7bc Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:32:25 +0100 Subject: [PATCH 576/619] Always list fallback key types in /sync (#10623) --- changelog.d/10623.bugfix | 1 + synapse/rest/client/v2_alpha/sync.py | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/10623.bugfix diff --git a/changelog.d/10623.bugfix b/changelog.d/10623.bugfix new file mode 100644 index 0000000000..759fba3513 --- /dev/null +++ b/changelog.d/10623.bugfix @@ -0,0 +1 @@ +Revert behaviour introduced in v1.38.0 that strips `org.matrix.msc2732.device_unused_fallback_key_types` from `/sync` when its value is empty. This field should instead always be present according to [MSC2732](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2732-olm-fallback-keys.md). \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/v2_alpha/sync.py index e321668698..e18f4d01b3 100644 --- a/synapse/rest/client/v2_alpha/sync.py +++ b/synapse/rest/client/v2_alpha/sync.py @@ -259,10 +259,11 @@ async def encode_response(self, time_now, sync_result, access_token_id, filter): # Corresponding synapse issue: https://github.com/matrix-org/synapse/issues/10456 response["device_one_time_keys_count"] = sync_result.device_one_time_keys_count - if sync_result.device_unused_fallback_key_types: - response[ - "org.matrix.msc2732.device_unused_fallback_key_types" - ] = sync_result.device_unused_fallback_key_types + # https://github.com/matrix-org/matrix-doc/blob/54255851f642f84a4f1aaf7bc063eebe3d76752b/proposals/2732-olm-fallback-keys.md + # states that this field should always be included, as long as the server supports the feature. + response[ + "org.matrix.msc2732.device_unused_fallback_key_types" + ] = sync_result.device_unused_fallback_key_types if joined: response["rooms"][Membership.JOIN] = joined From 642a42eddece60afbbd5e5a6659fa9b939238b4a Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:57:58 +0100 Subject: [PATCH 577/619] Flatten the synapse.rest.client package (#10600) --- changelog.d/10600.misc | 1 + synapse/app/generic_worker.py | 40 +++++++++---------- synapse/handlers/auth.py | 6 +-- synapse/rest/__init__.py | 35 +++++++--------- synapse/rest/admin/users.py | 4 +- synapse/rest/client/__init__.py | 2 +- synapse/rest/client/{v2_alpha => }/_base.py | 0 synapse/rest/client/{v2_alpha => }/account.py | 0 .../client/{v2_alpha => }/account_data.py | 0 .../client/{v2_alpha => }/account_validity.py | 0 synapse/rest/client/{v2_alpha => }/auth.py | 0 .../client/{v2_alpha => }/capabilities.py | 0 synapse/rest/client/{v2_alpha => }/devices.py | 0 synapse/rest/client/{v1 => }/directory.py | 2 +- synapse/rest/client/{v1 => }/events.py | 2 +- synapse/rest/client/{v2_alpha => }/filter.py | 0 synapse/rest/client/{v2_alpha => }/groups.py | 0 synapse/rest/client/{v1 => }/initial_sync.py | 2 +- synapse/rest/client/{v2_alpha => }/keys.py | 0 synapse/rest/client/{v2_alpha => }/knock.py | 0 synapse/rest/client/{v1 => }/login.py | 2 +- synapse/rest/client/{v1 => }/logout.py | 2 +- .../client/{v2_alpha => }/notifications.py | 0 synapse/rest/client/{v2_alpha => }/openid.py | 0 .../client/{v2_alpha => }/password_policy.py | 0 synapse/rest/client/{v1 => }/presence.py | 2 +- synapse/rest/client/{v1 => }/profile.py | 2 +- synapse/rest/client/{v1 => }/push_rule.py | 2 +- synapse/rest/client/{v1 => }/pusher.py | 2 +- .../rest/client/{v2_alpha => }/read_marker.py | 0 .../rest/client/{v2_alpha => }/receipts.py | 0 .../rest/client/{v2_alpha => }/register.py | 4 +- .../rest/client/{v2_alpha => }/relations.py | 0 .../client/{v2_alpha => }/report_event.py | 0 synapse/rest/client/{v1 => }/room.py | 2 +- .../{v2_alpha/room.py => room_batch.py} | 0 .../rest/client/{v2_alpha => }/room_keys.py | 0 .../room_upgrade_rest_servlet.py | 0 .../client/{v2_alpha => }/sendtodevice.py | 0 .../client/{v2_alpha => }/shared_rooms.py | 0 synapse/rest/client/{v2_alpha => }/sync.py | 0 synapse/rest/client/{v2_alpha => }/tags.py | 0 .../rest/client/{v2_alpha => }/thirdparty.py | 0 .../client/{v2_alpha => }/tokenrefresh.py | 0 .../client/{v2_alpha => }/user_directory.py | 0 synapse/rest/client/v1/__init__.py | 13 ------ synapse/rest/client/v2_alpha/__init__.py | 13 ------ synapse/rest/client/{v1 => }/voip.py | 2 +- tests/app/test_phone_stats_home.py | 2 +- tests/events/test_presence_router.py | 2 +- tests/events/test_snapshot.py | 2 +- tests/federation/test_complexity.py | 2 +- tests/federation/test_federation_catch_up.py | 2 +- tests/federation/test_federation_sender.py | 2 +- tests/federation/test_federation_server.py | 2 +- tests/federation/transport/test_knocking.py | 2 +- tests/handlers/test_admin.py | 4 +- tests/handlers/test_directory.py | 2 +- tests/handlers/test_federation.py | 2 +- tests/handlers/test_message.py | 2 +- tests/handlers/test_password_providers.py | 3 +- tests/handlers/test_presence.py | 2 +- tests/handlers/test_room_summary.py | 2 +- tests/handlers/test_stats.py | 2 +- tests/handlers/test_user_directory.py | 3 +- tests/module_api/test_api.py | 2 +- tests/push/test_email.py | 2 +- tests/push/test_http.py | 3 +- tests/replication/tcp/streams/test_events.py | 2 +- tests/replication/test_auth.py | 2 +- tests/replication/test_client_reader_shard.py | 2 +- .../test_federation_sender_shard.py | 2 +- tests/replication/test_multi_media_repo.py | 2 +- tests/replication/test_pusher_shard.py | 2 +- .../test_sharded_event_persister.py | 3 +- tests/rest/admin/test_admin.py | 3 +- tests/rest/admin/test_device.py | 2 +- tests/rest/admin/test_event_reports.py | 3 +- tests/rest/admin/test_media.py | 2 +- tests/rest/admin/test_room.py | 2 +- tests/rest/admin/test_statistics.py | 2 +- tests/rest/admin/test_user.py | 3 +- tests/rest/admin/test_username_available.py | 2 +- tests/rest/client/test_consent.py | 2 +- tests/rest/client/test_ephemeral_message.py | 2 +- tests/rest/client/test_identity.py | 2 +- tests/rest/client/test_power_levels.py | 3 +- tests/rest/client/test_redactions.py | 3 +- tests/rest/client/test_retention.py | 2 +- tests/rest/client/test_shadow_banned.py | 9 ++++- tests/rest/client/test_third_party_rules.py | 2 +- tests/rest/client/v1/test_directory.py | 2 +- tests/rest/client/v1/test_events.py | 2 +- tests/rest/client/v1/test_login.py | 5 +-- tests/rest/client/v1/test_presence.py | 2 +- tests/rest/client/v1/test_profile.py | 2 +- tests/rest/client/v1/test_push_rule_attrs.py | 2 +- tests/rest/client/v1/test_rooms.py | 3 +- tests/rest/client/v1/test_typing.py | 2 +- tests/rest/client/v2_alpha/test_account.py | 3 +- tests/rest/client/v2_alpha/test_auth.py | 3 +- .../rest/client/v2_alpha/test_capabilities.py | 3 +- tests/rest/client/v2_alpha/test_filter.py | 2 +- .../client/v2_alpha/test_password_policy.py | 3 +- tests/rest/client/v2_alpha/test_register.py | 3 +- tests/rest/client/v2_alpha/test_relations.py | 3 +- .../rest/client/v2_alpha/test_report_event.py | 3 +- .../rest/client/v2_alpha/test_sendtodevice.py | 3 +- .../rest/client/v2_alpha/test_shared_rooms.py | 3 +- tests/rest/client/v2_alpha/test_sync.py | 3 +- .../rest/client/v2_alpha/test_upgrade_room.py | 3 +- tests/rest/media/v1/test_media_storage.py | 2 +- tests/server_notices/test_consent.py | 3 +- .../test_resource_limits_server_notices.py | 3 +- .../databases/main/test_events_worker.py | 2 +- tests/storage/test_cleanup_extrems.py | 2 +- tests/storage/test_client_ips.py | 2 +- tests/storage/test_event_chain.py | 2 +- tests/storage/test_events.py | 2 +- tests/storage/test_purge.py | 2 +- tests/storage/test_roommember.py | 2 +- tests/test_mau.py | 2 +- tests/test_terms_auth.py | 2 +- 123 files changed, 137 insertions(+), 188 deletions(-) create mode 100644 changelog.d/10600.misc rename synapse/rest/client/{v2_alpha => }/_base.py (100%) rename synapse/rest/client/{v2_alpha => }/account.py (100%) rename synapse/rest/client/{v2_alpha => }/account_data.py (100%) rename synapse/rest/client/{v2_alpha => }/account_validity.py (100%) rename synapse/rest/client/{v2_alpha => }/auth.py (100%) rename synapse/rest/client/{v2_alpha => }/capabilities.py (100%) rename synapse/rest/client/{v2_alpha => }/devices.py (100%) rename synapse/rest/client/{v1 => }/directory.py (98%) rename synapse/rest/client/{v1 => }/events.py (98%) rename synapse/rest/client/{v2_alpha => }/filter.py (100%) rename synapse/rest/client/{v2_alpha => }/groups.py (100%) rename synapse/rest/client/{v1 => }/initial_sync.py (96%) rename synapse/rest/client/{v2_alpha => }/keys.py (100%) rename synapse/rest/client/{v2_alpha => }/knock.py (100%) rename synapse/rest/client/{v1 => }/login.py (99%) rename synapse/rest/client/{v1 => }/logout.py (97%) rename synapse/rest/client/{v2_alpha => }/notifications.py (100%) rename synapse/rest/client/{v2_alpha => }/openid.py (100%) rename synapse/rest/client/{v2_alpha => }/password_policy.py (100%) rename synapse/rest/client/{v1 => }/presence.py (98%) rename synapse/rest/client/{v1 => }/profile.py (98%) rename synapse/rest/client/{v1 => }/push_rule.py (99%) rename synapse/rest/client/{v1 => }/pusher.py (98%) rename synapse/rest/client/{v2_alpha => }/read_marker.py (100%) rename synapse/rest/client/{v2_alpha => }/receipts.py (100%) rename synapse/rest/client/{v2_alpha => }/register.py (99%) rename synapse/rest/client/{v2_alpha => }/relations.py (100%) rename synapse/rest/client/{v2_alpha => }/report_event.py (100%) rename synapse/rest/client/{v1 => }/room.py (99%) rename synapse/rest/client/{v2_alpha/room.py => room_batch.py} (100%) rename synapse/rest/client/{v2_alpha => }/room_keys.py (100%) rename synapse/rest/client/{v2_alpha => }/room_upgrade_rest_servlet.py (100%) rename synapse/rest/client/{v2_alpha => }/sendtodevice.py (100%) rename synapse/rest/client/{v2_alpha => }/shared_rooms.py (100%) rename synapse/rest/client/{v2_alpha => }/sync.py (100%) rename synapse/rest/client/{v2_alpha => }/tags.py (100%) rename synapse/rest/client/{v2_alpha => }/thirdparty.py (100%) rename synapse/rest/client/{v2_alpha => }/tokenrefresh.py (100%) rename synapse/rest/client/{v2_alpha => }/user_directory.py (100%) delete mode 100644 synapse/rest/client/v1/__init__.py delete mode 100644 synapse/rest/client/v2_alpha/__init__.py rename synapse/rest/client/{v1 => }/voip.py (97%) diff --git a/changelog.d/10600.misc b/changelog.d/10600.misc new file mode 100644 index 0000000000..489dc20b11 --- /dev/null +++ b/changelog.d/10600.misc @@ -0,0 +1 @@ +Flatten the `synapse.rest.client` package by moving the contents of `v1` and `v2_alpha` into the parent. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 3b7131af8f..d7b425a7ab 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -66,40 +66,40 @@ from synapse.replication.slave.storage.registration import SlavedRegistrationStore from synapse.replication.slave.storage.room import RoomStore from synapse.rest.admin import register_servlets_for_media_repo -from synapse.rest.client.v1 import events, login, presence, room -from synapse.rest.client.v1.initial_sync import InitialSyncRestServlet -from synapse.rest.client.v1.profile import ( - ProfileAvatarURLRestServlet, - ProfileDisplaynameRestServlet, - ProfileRestServlet, -) -from synapse.rest.client.v1.push_rule import PushRuleRestServlet -from synapse.rest.client.v1.voip import VoipRestServlet -from synapse.rest.client.v2_alpha import ( +from synapse.rest.client import ( account_data, + events, groups, + login, + presence, read_marker, receipts, + room, room_keys, sync, tags, user_directory, ) -from synapse.rest.client.v2_alpha._base import client_patterns -from synapse.rest.client.v2_alpha.account import ThreepidRestServlet -from synapse.rest.client.v2_alpha.account_data import ( - AccountDataServlet, - RoomAccountDataServlet, -) -from synapse.rest.client.v2_alpha.devices import DevicesRestServlet -from synapse.rest.client.v2_alpha.keys import ( +from synapse.rest.client._base import client_patterns +from synapse.rest.client.account import ThreepidRestServlet +from synapse.rest.client.account_data import AccountDataServlet, RoomAccountDataServlet +from synapse.rest.client.devices import DevicesRestServlet +from synapse.rest.client.initial_sync import InitialSyncRestServlet +from synapse.rest.client.keys import ( KeyChangesServlet, KeyQueryServlet, OneTimeKeyServlet, ) -from synapse.rest.client.v2_alpha.register import RegisterRestServlet -from synapse.rest.client.v2_alpha.sendtodevice import SendToDeviceRestServlet +from synapse.rest.client.profile import ( + ProfileAvatarURLRestServlet, + ProfileDisplaynameRestServlet, + ProfileRestServlet, +) +from synapse.rest.client.push_rule import PushRuleRestServlet +from synapse.rest.client.register import RegisterRestServlet +from synapse.rest.client.sendtodevice import SendToDeviceRestServlet from synapse.rest.client.versions import VersionsRestServlet +from synapse.rest.client.voip import VoipRestServlet from synapse.rest.health import HealthResource from synapse.rest.key.v2 import KeyApiV2Resource from synapse.rest.synapse.client import build_synapse_client_resource_tree diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 22a8552241..161b3c933c 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -73,7 +73,7 @@ from synapse.util.threepids import canonicalise_email if TYPE_CHECKING: - from synapse.rest.client.v1.login import LoginResponse + from synapse.rest.client.login import LoginResponse from synapse.server import HomeServer logger = logging.getLogger(__name__) @@ -461,7 +461,7 @@ async def check_ui_auth( If no auth flows have been completed successfully, raises an InteractiveAuthIncompleteError. To handle this, you can use - synapse.rest.client.v2_alpha._base.interactive_auth_handler as a + synapse.rest.client._base.interactive_auth_handler as a decorator. Args: @@ -543,7 +543,7 @@ async def check_ui_auth( # Note that the registration endpoint explicitly removes the # "initial_device_display_name" parameter if it is provided # without a "password" parameter. See the changes to - # synapse.rest.client.v2_alpha.register.RegisterRestServlet.on_POST + # synapse.rest.client.register.RegisterRestServlet.on_POST # in commit 544722bad23fc31056b9240189c3cbbbf0ffd3f9. if not clientdict: clientdict = session.clientdict diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 9cffe59ce5..3adc576124 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -14,40 +14,36 @@ # limitations under the License. from synapse.http.server import JsonResource from synapse.rest import admin -from synapse.rest.client import versions -from synapse.rest.client.v1 import ( - directory, - events, - initial_sync, - login as v1_login, - logout, - presence, - profile, - push_rule, - pusher, - room, - voip, -) -from synapse.rest.client.v2_alpha import ( +from synapse.rest.client import ( account, account_data, account_validity, auth, capabilities, devices, + directory, + events, filter, groups, + initial_sync, keys, knock, + login as v1_login, + logout, notifications, openid, password_policy, + presence, + profile, + push_rule, + pusher, read_marker, receipts, register, relations, report_event, - room as roomv2, + room, + room_batch, room_keys, room_upgrade_rest_servlet, sendtodevice, @@ -57,6 +53,8 @@ thirdparty, tokenrefresh, user_directory, + versions, + voip, ) @@ -85,7 +83,6 @@ def register_servlets(client_resource, hs): # Partially deprecated in r0 events.register_servlets(hs, client_resource) - # "v1" + "r0" room.register_servlets(hs, client_resource) v1_login.register_servlets(hs, client_resource) profile.register_servlets(hs, client_resource) @@ -95,8 +92,6 @@ def register_servlets(client_resource, hs): pusher.register_servlets(hs, client_resource) push_rule.register_servlets(hs, client_resource) logout.register_servlets(hs, client_resource) - - # "v2" sync.register_servlets(hs, client_resource) filter.register_servlets(hs, client_resource) account.register_servlets(hs, client_resource) @@ -118,7 +113,7 @@ def register_servlets(client_resource, hs): user_directory.register_servlets(hs, client_resource) groups.register_servlets(hs, client_resource) room_upgrade_rest_servlet.register_servlets(hs, client_resource) - roomv2.register_servlets(hs, client_resource) + room_batch.register_servlets(hs, client_resource) capabilities.register_servlets(hs, client_resource) account_validity.register_servlets(hs, client_resource) relations.register_servlets(hs, client_resource) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index c885fd77ab..93193b0864 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -34,7 +34,7 @@ assert_requester_is_admin, assert_user_is_admin, ) -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.storage.databases.main.media_repository import MediaSortOrder from synapse.storage.databases.main.stats import UserSortOrder from synapse.types import JsonDict, UserID @@ -504,7 +504,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: raise SynapseError(403, "HMAC incorrect") # Reuse the parts of RegisterRestServlet to reduce code duplication - from synapse.rest.client.v2_alpha.register import RegisterRestServlet + from synapse.rest.client.register import RegisterRestServlet register = RegisterRestServlet(self.hs) diff --git a/synapse/rest/client/__init__.py b/synapse/rest/client/__init__.py index 629e2df74a..f9830cc51f 100644 --- a/synapse/rest/client/__init__.py +++ b/synapse/rest/client/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2015, 2016 OpenMarket Ltd +# Copyright 2014-2016 The Matrix.org Foundation C.I.C. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/_base.py similarity index 100% rename from synapse/rest/client/v2_alpha/_base.py rename to synapse/rest/client/_base.py diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/account.py similarity index 100% rename from synapse/rest/client/v2_alpha/account.py rename to synapse/rest/client/account.py diff --git a/synapse/rest/client/v2_alpha/account_data.py b/synapse/rest/client/account_data.py similarity index 100% rename from synapse/rest/client/v2_alpha/account_data.py rename to synapse/rest/client/account_data.py diff --git a/synapse/rest/client/v2_alpha/account_validity.py b/synapse/rest/client/account_validity.py similarity index 100% rename from synapse/rest/client/v2_alpha/account_validity.py rename to synapse/rest/client/account_validity.py diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/auth.py similarity index 100% rename from synapse/rest/client/v2_alpha/auth.py rename to synapse/rest/client/auth.py diff --git a/synapse/rest/client/v2_alpha/capabilities.py b/synapse/rest/client/capabilities.py similarity index 100% rename from synapse/rest/client/v2_alpha/capabilities.py rename to synapse/rest/client/capabilities.py diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/devices.py similarity index 100% rename from synapse/rest/client/v2_alpha/devices.py rename to synapse/rest/client/devices.py diff --git a/synapse/rest/client/v1/directory.py b/synapse/rest/client/directory.py similarity index 98% rename from synapse/rest/client/v1/directory.py rename to synapse/rest/client/directory.py index ae92a3df8e..ffa075c8e5 100644 --- a/synapse/rest/client/v1/directory.py +++ b/synapse/rest/client/directory.py @@ -23,7 +23,7 @@ SynapseError, ) from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.types import RoomAlias logger = logging.getLogger(__name__) diff --git a/synapse/rest/client/v1/events.py b/synapse/rest/client/events.py similarity index 98% rename from synapse/rest/client/v1/events.py rename to synapse/rest/client/events.py index ee7454996e..52bb579cfd 100644 --- a/synapse/rest/client/v1/events.py +++ b/synapse/rest/client/events.py @@ -17,7 +17,7 @@ from synapse.api.errors import SynapseError from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.streams.config import PaginationConfig logger = logging.getLogger(__name__) diff --git a/synapse/rest/client/v2_alpha/filter.py b/synapse/rest/client/filter.py similarity index 100% rename from synapse/rest/client/v2_alpha/filter.py rename to synapse/rest/client/filter.py diff --git a/synapse/rest/client/v2_alpha/groups.py b/synapse/rest/client/groups.py similarity index 100% rename from synapse/rest/client/v2_alpha/groups.py rename to synapse/rest/client/groups.py diff --git a/synapse/rest/client/v1/initial_sync.py b/synapse/rest/client/initial_sync.py similarity index 96% rename from synapse/rest/client/v1/initial_sync.py rename to synapse/rest/client/initial_sync.py index bef1edc838..12ba0e91db 100644 --- a/synapse/rest/client/v1/initial_sync.py +++ b/synapse/rest/client/initial_sync.py @@ -14,7 +14,7 @@ from synapse.http.servlet import RestServlet, parse_boolean -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.streams.config import PaginationConfig diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/keys.py similarity index 100% rename from synapse/rest/client/v2_alpha/keys.py rename to synapse/rest/client/keys.py diff --git a/synapse/rest/client/v2_alpha/knock.py b/synapse/rest/client/knock.py similarity index 100% rename from synapse/rest/client/v2_alpha/knock.py rename to synapse/rest/client/knock.py diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/login.py similarity index 99% rename from synapse/rest/client/v1/login.py rename to synapse/rest/client/login.py index 11567bf32c..0c8d8967b7 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/login.py @@ -34,7 +34,7 @@ parse_string, ) from synapse.http.site import SynapseRequest -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.rest.well_known import WellKnownBuilder from synapse.types import JsonDict, UserID diff --git a/synapse/rest/client/v1/logout.py b/synapse/rest/client/logout.py similarity index 97% rename from synapse/rest/client/v1/logout.py rename to synapse/rest/client/logout.py index 5aa7908d73..6055cac2bd 100644 --- a/synapse/rest/client/v1/logout.py +++ b/synapse/rest/client/logout.py @@ -15,7 +15,7 @@ import logging from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns logger = logging.getLogger(__name__) diff --git a/synapse/rest/client/v2_alpha/notifications.py b/synapse/rest/client/notifications.py similarity index 100% rename from synapse/rest/client/v2_alpha/notifications.py rename to synapse/rest/client/notifications.py diff --git a/synapse/rest/client/v2_alpha/openid.py b/synapse/rest/client/openid.py similarity index 100% rename from synapse/rest/client/v2_alpha/openid.py rename to synapse/rest/client/openid.py diff --git a/synapse/rest/client/v2_alpha/password_policy.py b/synapse/rest/client/password_policy.py similarity index 100% rename from synapse/rest/client/v2_alpha/password_policy.py rename to synapse/rest/client/password_policy.py diff --git a/synapse/rest/client/v1/presence.py b/synapse/rest/client/presence.py similarity index 98% rename from synapse/rest/client/v1/presence.py rename to synapse/rest/client/presence.py index 2b24fe5aa6..6c27e5faf9 100644 --- a/synapse/rest/client/v1/presence.py +++ b/synapse/rest/client/presence.py @@ -19,7 +19,7 @@ from synapse.api.errors import AuthError, SynapseError from synapse.handlers.presence import format_user_presence_state from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.types import UserID logger = logging.getLogger(__name__) diff --git a/synapse/rest/client/v1/profile.py b/synapse/rest/client/profile.py similarity index 98% rename from synapse/rest/client/v1/profile.py rename to synapse/rest/client/profile.py index f42f4b3567..5463ed2c4f 100644 --- a/synapse/rest/client/v1/profile.py +++ b/synapse/rest/client/profile.py @@ -16,7 +16,7 @@ from synapse.api.errors import Codes, SynapseError from synapse.http.servlet import RestServlet, parse_json_object_from_request -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.types import UserID diff --git a/synapse/rest/client/v1/push_rule.py b/synapse/rest/client/push_rule.py similarity index 99% rename from synapse/rest/client/v1/push_rule.py rename to synapse/rest/client/push_rule.py index be29a0b39e..702b351d18 100644 --- a/synapse/rest/client/v1/push_rule.py +++ b/synapse/rest/client/push_rule.py @@ -26,7 +26,7 @@ from synapse.push.baserules import BASE_RULE_IDS, NEW_RULE_IDS from synapse.push.clientformat import format_push_rules_for_user from synapse.push.rulekinds import PRIORITY_CLASS_MAP -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/pusher.py similarity index 98% rename from synapse/rest/client/v1/pusher.py rename to synapse/rest/client/pusher.py index 18102eca6c..84619c5e41 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/pusher.py @@ -23,7 +23,7 @@ parse_string, ) from synapse.push import PusherConfigException -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns logger = logging.getLogger(__name__) diff --git a/synapse/rest/client/v2_alpha/read_marker.py b/synapse/rest/client/read_marker.py similarity index 100% rename from synapse/rest/client/v2_alpha/read_marker.py rename to synapse/rest/client/read_marker.py diff --git a/synapse/rest/client/v2_alpha/receipts.py b/synapse/rest/client/receipts.py similarity index 100% rename from synapse/rest/client/v2_alpha/receipts.py rename to synapse/rest/client/receipts.py diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/register.py similarity index 99% rename from synapse/rest/client/v2_alpha/register.py rename to synapse/rest/client/register.py index 4d31584acd..58b8e8f261 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/register.py @@ -115,7 +115,7 @@ async def on_POST(self, request): # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See on_POST in EmailThreepidRequestTokenRestServlet - # in synapse/rest/client/v2_alpha/account.py) + # in synapse/rest/client/account.py) try: email = validate_email(body["email"]) except ValueError as e: @@ -631,7 +631,7 @@ async def on_POST(self, request): # For emails, canonicalise the address. # We store all email addresses canonicalised in the DB. # (See on_POST in EmailThreepidRequestTokenRestServlet - # in synapse/rest/client/v2_alpha/account.py) + # in synapse/rest/client/account.py) if medium == "email": try: address = canonicalise_email(address) diff --git a/synapse/rest/client/v2_alpha/relations.py b/synapse/rest/client/relations.py similarity index 100% rename from synapse/rest/client/v2_alpha/relations.py rename to synapse/rest/client/relations.py diff --git a/synapse/rest/client/v2_alpha/report_event.py b/synapse/rest/client/report_event.py similarity index 100% rename from synapse/rest/client/v2_alpha/report_event.py rename to synapse/rest/client/report_event.py diff --git a/synapse/rest/client/v1/room.py b/synapse/rest/client/room.py similarity index 99% rename from synapse/rest/client/v1/room.py rename to synapse/rest/client/room.py index ba7250ad8e..ed238b2141 100644 --- a/synapse/rest/client/v1/room.py +++ b/synapse/rest/client/room.py @@ -42,8 +42,8 @@ ) from synapse.http.site import SynapseRequest from synapse.logging.opentracing import set_tag +from synapse.rest.client._base import client_patterns from synapse.rest.client.transactions import HttpTransactionCache -from synapse.rest.client.v2_alpha._base import client_patterns from synapse.storage.state import StateFilter from synapse.streams.config import PaginationConfig from synapse.types import JsonDict, StreamToken, ThirdPartyInstanceID, UserID diff --git a/synapse/rest/client/v2_alpha/room.py b/synapse/rest/client/room_batch.py similarity index 100% rename from synapse/rest/client/v2_alpha/room.py rename to synapse/rest/client/room_batch.py diff --git a/synapse/rest/client/v2_alpha/room_keys.py b/synapse/rest/client/room_keys.py similarity index 100% rename from synapse/rest/client/v2_alpha/room_keys.py rename to synapse/rest/client/room_keys.py diff --git a/synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py b/synapse/rest/client/room_upgrade_rest_servlet.py similarity index 100% rename from synapse/rest/client/v2_alpha/room_upgrade_rest_servlet.py rename to synapse/rest/client/room_upgrade_rest_servlet.py diff --git a/synapse/rest/client/v2_alpha/sendtodevice.py b/synapse/rest/client/sendtodevice.py similarity index 100% rename from synapse/rest/client/v2_alpha/sendtodevice.py rename to synapse/rest/client/sendtodevice.py diff --git a/synapse/rest/client/v2_alpha/shared_rooms.py b/synapse/rest/client/shared_rooms.py similarity index 100% rename from synapse/rest/client/v2_alpha/shared_rooms.py rename to synapse/rest/client/shared_rooms.py diff --git a/synapse/rest/client/v2_alpha/sync.py b/synapse/rest/client/sync.py similarity index 100% rename from synapse/rest/client/v2_alpha/sync.py rename to synapse/rest/client/sync.py diff --git a/synapse/rest/client/v2_alpha/tags.py b/synapse/rest/client/tags.py similarity index 100% rename from synapse/rest/client/v2_alpha/tags.py rename to synapse/rest/client/tags.py diff --git a/synapse/rest/client/v2_alpha/thirdparty.py b/synapse/rest/client/thirdparty.py similarity index 100% rename from synapse/rest/client/v2_alpha/thirdparty.py rename to synapse/rest/client/thirdparty.py diff --git a/synapse/rest/client/v2_alpha/tokenrefresh.py b/synapse/rest/client/tokenrefresh.py similarity index 100% rename from synapse/rest/client/v2_alpha/tokenrefresh.py rename to synapse/rest/client/tokenrefresh.py diff --git a/synapse/rest/client/v2_alpha/user_directory.py b/synapse/rest/client/user_directory.py similarity index 100% rename from synapse/rest/client/v2_alpha/user_directory.py rename to synapse/rest/client/user_directory.py diff --git a/synapse/rest/client/v1/__init__.py b/synapse/rest/client/v1/__init__.py deleted file mode 100644 index 5e83dba2ed..0000000000 --- a/synapse/rest/client/v1/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2014-2016 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. diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py deleted file mode 100644 index 5e83dba2ed..0000000000 --- a/synapse/rest/client/v2_alpha/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2014-2016 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. diff --git a/synapse/rest/client/v1/voip.py b/synapse/rest/client/voip.py similarity index 97% rename from synapse/rest/client/v1/voip.py rename to synapse/rest/client/voip.py index c780ffded5..f53020520d 100644 --- a/synapse/rest/client/v1/voip.py +++ b/synapse/rest/client/voip.py @@ -17,7 +17,7 @@ import hmac from synapse.http.servlet import RestServlet -from synapse.rest.client.v2_alpha._base import client_patterns +from synapse.rest.client._base import client_patterns class VoipRestServlet(RestServlet): diff --git a/tests/app/test_phone_stats_home.py b/tests/app/test_phone_stats_home.py index 5527e278db..d66aeb00eb 100644 --- a/tests/app/test_phone_stats_home.py +++ b/tests/app/test_phone_stats_home.py @@ -1,6 +1,6 @@ import synapse from synapse.app.phone_stats_home import start_phone_stats_home -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests import unittest from tests.unittest import HomeserverTestCase diff --git a/tests/events/test_presence_router.py b/tests/events/test_presence_router.py index 3f41e99950..6b87f571b8 100644 --- a/tests/events/test_presence_router.py +++ b/tests/events/test_presence_router.py @@ -22,7 +22,7 @@ from synapse.handlers.presence import UserPresenceState from synapse.module_api import ModuleApi from synapse.rest import admin -from synapse.rest.client.v1 import login, presence, room +from synapse.rest.client import login, presence, room from synapse.types import JsonDict, StreamToken, create_requester from tests.handlers.test_sync import generate_sync_config diff --git a/tests/events/test_snapshot.py b/tests/events/test_snapshot.py index 48e98aac79..ca27388ae8 100644 --- a/tests/events/test_snapshot.py +++ b/tests/events/test_snapshot.py @@ -14,7 +14,7 @@ from synapse.events.snapshot import EventContext from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests import unittest from tests.test_utils.event_injection import create_event diff --git a/tests/federation/test_complexity.py b/tests/federation/test_complexity.py index 1a809b2a6a..7b486aba4a 100644 --- a/tests/federation/test_complexity.py +++ b/tests/federation/test_complexity.py @@ -16,7 +16,7 @@ from synapse.api.errors import Codes, SynapseError from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.types import UserID from tests import unittest diff --git a/tests/federation/test_federation_catch_up.py b/tests/federation/test_federation_catch_up.py index 802c5ad299..f0aa8ed9db 100644 --- a/tests/federation/test_federation_catch_up.py +++ b/tests/federation/test_federation_catch_up.py @@ -6,7 +6,7 @@ from synapse.federation.sender import PerDestinationQueue, TransactionManager from synapse.federation.units import Edu from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.util.retryutils import NotRetryingDestination from tests.test_utils import event_injection, make_awaitable diff --git a/tests/federation/test_federation_sender.py b/tests/federation/test_federation_sender.py index b00dd143d6..65b18fbd7a 100644 --- a/tests/federation/test_federation_sender.py +++ b/tests/federation/test_federation_sender.py @@ -21,7 +21,7 @@ from synapse.api.constants import RoomEncryptionAlgorithms from synapse.rest import admin -from synapse.rest.client.v1 import login +from synapse.rest.client import login from synapse.types import JsonDict, ReadReceipt from tests.test_utils import make_awaitable diff --git a/tests/federation/test_federation_server.py b/tests/federation/test_federation_server.py index 1737891564..0b60cc4261 100644 --- a/tests/federation/test_federation_server.py +++ b/tests/federation/test_federation_server.py @@ -19,7 +19,7 @@ from synapse.events import make_event_from_dict from synapse.federation.federation_server import server_matches_acl_event from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests import unittest diff --git a/tests/federation/transport/test_knocking.py b/tests/federation/transport/test_knocking.py index aab44bce4a..383214ab50 100644 --- a/tests/federation/transport/test_knocking.py +++ b/tests/federation/transport/test_knocking.py @@ -18,7 +18,7 @@ from synapse.api.room_versions import RoomVersions from synapse.events import builder from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.server import HomeServer from synapse.types import RoomAlias diff --git a/tests/handlers/test_admin.py b/tests/handlers/test_admin.py index 18a734daf4..59de1142b1 100644 --- a/tests/handlers/test_admin.py +++ b/tests/handlers/test_admin.py @@ -15,12 +15,10 @@ from collections import Counter from unittest.mock import Mock -import synapse.api.errors -import synapse.handlers.admin import synapse.rest.admin import synapse.storage from synapse.api.constants import EventTypes -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests import unittest diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index 7a8041ab44..a0a48b564e 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -19,7 +19,7 @@ import synapse.api.errors from synapse.api.constants import EventTypes from synapse.config.room_directory import RoomDirectoryConfig -from synapse.rest.client.v1 import directory, login, room +from synapse.rest.client import directory, login, room from synapse.types import RoomAlias, create_requester from tests import unittest diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index 4140fcefc2..c72a8972a3 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -22,7 +22,7 @@ from synapse.federation.federation_base import event_from_pdu_json from synapse.logging.context import LoggingContext, run_in_background from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.util.stringutils import random_string from tests import unittest diff --git a/tests/handlers/test_message.py b/tests/handlers/test_message.py index a8a9fc5b62..8a8d369fac 100644 --- a/tests/handlers/test_message.py +++ b/tests/handlers/test_message.py @@ -18,7 +18,7 @@ from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.types import create_requester from synapse.util.stringutils import random_string diff --git a/tests/handlers/test_password_providers.py b/tests/handlers/test_password_providers.py index 32651db096..38e6d9f536 100644 --- a/tests/handlers/test_password_providers.py +++ b/tests/handlers/test_password_providers.py @@ -20,8 +20,7 @@ from twisted.internet import defer import synapse -from synapse.rest.client.v1 import login -from synapse.rest.client.v2_alpha import devices +from synapse.rest.client import devices, login from synapse.types import JsonDict from tests import unittest diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index 29845a80da..0a52bc8b72 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -33,7 +33,7 @@ handle_update, ) from synapse.rest import admin -from synapse.rest.client.v1 import room +from synapse.rest.client import room from synapse.types import UserID, get_domain_from_id from tests import unittest diff --git a/tests/handlers/test_room_summary.py b/tests/handlers/test_room_summary.py index 732d746e38..ac800afa7d 100644 --- a/tests/handlers/test_room_summary.py +++ b/tests/handlers/test_room_summary.py @@ -28,7 +28,7 @@ from synapse.events import make_event_from_dict from synapse.handlers.room_summary import _child_events_comparison_key, _RoomEntry from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.server import HomeServer from synapse.types import JsonDict, UserID diff --git a/tests/handlers/test_stats.py b/tests/handlers/test_stats.py index e4059acda3..1ba4c05b9b 100644 --- a/tests/handlers/test_stats.py +++ b/tests/handlers/test_stats.py @@ -13,7 +13,7 @@ # limitations under the License. from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.storage.databases.main import stats from tests import unittest diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py index 549876dc85..e44bf2b3b1 100644 --- a/tests/handlers/test_user_directory.py +++ b/tests/handlers/test_user_directory.py @@ -18,8 +18,7 @@ import synapse.rest.admin from synapse.api.constants import EventTypes, RoomEncryptionAlgorithms, UserTypes from synapse.api.room_versions import RoomVersion, RoomVersions -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import user_directory +from synapse.rest.client import login, room, user_directory from synapse.storage.roommember import ProfileInfo from tests import unittest diff --git a/tests/module_api/test_api.py b/tests/module_api/test_api.py index 0b817cc701..7dd519cd44 100644 --- a/tests/module_api/test_api.py +++ b/tests/module_api/test_api.py @@ -20,7 +20,7 @@ from synapse.federation.units import Transaction from synapse.handlers.presence import UserPresenceState from synapse.rest import admin -from synapse.rest.client.v1 import login, presence, room +from synapse.rest.client import login, presence, room from synapse.types import create_requester from tests.events.test_presence_router import send_presence_update, sync_presence diff --git a/tests/push/test_email.py b/tests/push/test_email.py index a487706758..e0a3342088 100644 --- a/tests/push/test_email.py +++ b/tests/push/test_email.py @@ -21,7 +21,7 @@ import synapse.rest.admin from synapse.api.errors import Codes, SynapseError -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests.unittest import HomeserverTestCase diff --git a/tests/push/test_http.py b/tests/push/test_http.py index ffd75b1491..c068d329a9 100644 --- a/tests/push/test_http.py +++ b/tests/push/test_http.py @@ -18,8 +18,7 @@ import synapse.rest.admin from synapse.logging.context import make_deferred_yieldable from synapse.push import PusherConfigException -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import receipts +from synapse.rest.client import login, receipts, room from tests.unittest import HomeserverTestCase, override_config diff --git a/tests/replication/tcp/streams/test_events.py b/tests/replication/tcp/streams/test_events.py index 666008425a..f198a94887 100644 --- a/tests/replication/tcp/streams/test_events.py +++ b/tests/replication/tcp/streams/test_events.py @@ -24,7 +24,7 @@ EventsStreamRow, ) from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests.replication._base import BaseStreamTestCase from tests.test_utils.event_injection import inject_event, inject_member_event diff --git a/tests/replication/test_auth.py b/tests/replication/test_auth.py index 1346e0e160..43a16bb141 100644 --- a/tests/replication/test_auth.py +++ b/tests/replication/test_auth.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from synapse.rest.client.v2_alpha import register +from synapse.rest.client import register from tests.replication._base import BaseMultiWorkerStreamTestCase from tests.server import FakeChannel, make_request diff --git a/tests/replication/test_client_reader_shard.py b/tests/replication/test_client_reader_shard.py index b9751efdc5..995097d72c 100644 --- a/tests/replication/test_client_reader_shard.py +++ b/tests/replication/test_client_reader_shard.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from synapse.rest.client.v2_alpha import register +from synapse.rest.client import register from tests.replication._base import BaseMultiWorkerStreamTestCase from tests.server import make_request diff --git a/tests/replication/test_federation_sender_shard.py b/tests/replication/test_federation_sender_shard.py index a0c710f855..af5dfca752 100644 --- a/tests/replication/test_federation_sender_shard.py +++ b/tests/replication/test_federation_sender_shard.py @@ -17,7 +17,7 @@ from synapse.api.constants import EventTypes, Membership from synapse.events.builder import EventBuilderFactory from synapse.rest.admin import register_servlets_for_client_rest_resource -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.types import UserID, create_requester from tests.replication._base import BaseMultiWorkerStreamTestCase diff --git a/tests/replication/test_multi_media_repo.py b/tests/replication/test_multi_media_repo.py index ffa425328f..ac419f0db3 100644 --- a/tests/replication/test_multi_media_repo.py +++ b/tests/replication/test_multi_media_repo.py @@ -22,7 +22,7 @@ from twisted.web.server import Request from synapse.rest import admin -from synapse.rest.client.v1 import login +from synapse.rest.client import login from synapse.server import HomeServer from tests.http import TestServerTLSConnectionFactory, get_test_ca_cert_file diff --git a/tests/replication/test_pusher_shard.py b/tests/replication/test_pusher_shard.py index 1e4e3821b9..4094a75f36 100644 --- a/tests/replication/test_pusher_shard.py +++ b/tests/replication/test_pusher_shard.py @@ -17,7 +17,7 @@ from twisted.internet import defer from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests.replication._base import BaseMultiWorkerStreamTestCase diff --git a/tests/replication/test_sharded_event_persister.py b/tests/replication/test_sharded_event_persister.py index f3615af97e..0a6e4795ee 100644 --- a/tests/replication/test_sharded_event_persister.py +++ b/tests/replication/test_sharded_event_persister.py @@ -16,8 +16,7 @@ from synapse.api.room_versions import RoomVersion from synapse.rest import admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import sync +from synapse.rest.client import login, room, sync from tests.replication._base import BaseMultiWorkerStreamTestCase from tests.server import make_request diff --git a/tests/rest/admin/test_admin.py b/tests/rest/admin/test_admin.py index a7c6e595b9..bfa638fb4b 100644 --- a/tests/rest/admin/test_admin.py +++ b/tests/rest/admin/test_admin.py @@ -24,8 +24,7 @@ from synapse.http.server import JsonResource from synapse.logging.context import make_deferred_yieldable from synapse.rest.admin import VersionServlet -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import groups +from synapse.rest.client import groups, login, room from tests import unittest from tests.server import FakeSite, make_request diff --git a/tests/rest/admin/test_device.py b/tests/rest/admin/test_device.py index 120730b764..c4afe5c3d9 100644 --- a/tests/rest/admin/test_device.py +++ b/tests/rest/admin/test_device.py @@ -17,7 +17,7 @@ import synapse.rest.admin from synapse.api.errors import Codes -from synapse.rest.client.v1 import login +from synapse.rest.client import login from tests import unittest diff --git a/tests/rest/admin/test_event_reports.py b/tests/rest/admin/test_event_reports.py index f15d1cf6f7..e9ef89731f 100644 --- a/tests/rest/admin/test_event_reports.py +++ b/tests/rest/admin/test_event_reports.py @@ -16,8 +16,7 @@ import synapse.rest.admin from synapse.api.errors import Codes -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import report_event +from synapse.rest.client import login, report_event, room from tests import unittest diff --git a/tests/rest/admin/test_media.py b/tests/rest/admin/test_media.py index 7198fd293f..972d60570c 100644 --- a/tests/rest/admin/test_media.py +++ b/tests/rest/admin/test_media.py @@ -20,7 +20,7 @@ import synapse.rest.admin from synapse.api.errors import Codes -from synapse.rest.client.v1 import login, profile, room +from synapse.rest.client import login, profile, room from synapse.rest.media.v1.filepath import MediaFilePaths from tests import unittest diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index 17ec8bfd3b..c9d4731017 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -22,7 +22,7 @@ import synapse.rest.admin from synapse.api.constants import EventTypes, Membership from synapse.api.errors import Codes -from synapse.rest.client.v1 import directory, events, login, room +from synapse.rest.client import directory, events, login, room from tests import unittest diff --git a/tests/rest/admin/test_statistics.py b/tests/rest/admin/test_statistics.py index 79cac4266b..5cd82209c4 100644 --- a/tests/rest/admin/test_statistics.py +++ b/tests/rest/admin/test_statistics.py @@ -18,7 +18,7 @@ import synapse.rest.admin from synapse.api.errors import Codes -from synapse.rest.client.v1 import login +from synapse.rest.client import login from tests import unittest diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py index a736ec4754..ef77275238 100644 --- a/tests/rest/admin/test_user.py +++ b/tests/rest/admin/test_user.py @@ -27,8 +27,7 @@ from synapse.api.constants import UserTypes from synapse.api.errors import Codes, HttpResponseException, ResourceLimitError from synapse.api.room_versions import RoomVersions -from synapse.rest.client.v1 import login, logout, profile, room -from synapse.rest.client.v2_alpha import devices, sync +from synapse.rest.client import devices, login, logout, profile, room, sync from synapse.rest.media.v1.filepath import MediaFilePaths from synapse.types import JsonDict, UserID diff --git a/tests/rest/admin/test_username_available.py b/tests/rest/admin/test_username_available.py index 53cbc8ddab..4e1c49c28b 100644 --- a/tests/rest/admin/test_username_available.py +++ b/tests/rest/admin/test_username_available.py @@ -14,7 +14,7 @@ import synapse.rest.admin from synapse.api.errors import Codes, SynapseError -from synapse.rest.client.v1 import login +from synapse.rest.client import login from tests import unittest diff --git a/tests/rest/client/test_consent.py b/tests/rest/client/test_consent.py index 5cc62a910a..65c58ce70a 100644 --- a/tests/rest/client/test_consent.py +++ b/tests/rest/client/test_consent.py @@ -16,7 +16,7 @@ import synapse.rest.admin from synapse.api.urls import ConsentURIBuilder -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.rest.consent import consent_resource from tests import unittest diff --git a/tests/rest/client/test_ephemeral_message.py b/tests/rest/client/test_ephemeral_message.py index eec0fc01f9..3d7aa8ec86 100644 --- a/tests/rest/client/test_ephemeral_message.py +++ b/tests/rest/client/test_ephemeral_message.py @@ -13,7 +13,7 @@ # limitations under the License. from synapse.api.constants import EventContentFields, EventTypes from synapse.rest import admin -from synapse.rest.client.v1 import room +from synapse.rest.client import room from tests import unittest diff --git a/tests/rest/client/test_identity.py b/tests/rest/client/test_identity.py index 478296ba0e..ca2e8ff8ef 100644 --- a/tests/rest/client/test_identity.py +++ b/tests/rest/client/test_identity.py @@ -15,7 +15,7 @@ import json import synapse.rest.admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests import unittest diff --git a/tests/rest/client/test_power_levels.py b/tests/rest/client/test_power_levels.py index ba5ad47df5..91d0762cb0 100644 --- a/tests/rest/client/test_power_levels.py +++ b/tests/rest/client/test_power_levels.py @@ -13,8 +13,7 @@ # limitations under the License. from synapse.rest import admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import sync +from synapse.rest.client import login, room, sync from tests.unittest import HomeserverTestCase diff --git a/tests/rest/client/test_redactions.py b/tests/rest/client/test_redactions.py index dfd85221d0..433d715f69 100644 --- a/tests/rest/client/test_redactions.py +++ b/tests/rest/client/test_redactions.py @@ -13,8 +13,7 @@ # limitations under the License. from synapse.rest import admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import sync +from synapse.rest.client import login, room, sync from tests.unittest import HomeserverTestCase diff --git a/tests/rest/client/test_retention.py b/tests/rest/client/test_retention.py index e1a6e73e17..b58452195a 100644 --- a/tests/rest/client/test_retention.py +++ b/tests/rest/client/test_retention.py @@ -15,7 +15,7 @@ from synapse.api.constants import EventTypes from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.visibility import filter_events_for_client from tests import unittest diff --git a/tests/rest/client/test_shadow_banned.py b/tests/rest/client/test_shadow_banned.py index 288ee12888..6a0d9a82be 100644 --- a/tests/rest/client/test_shadow_banned.py +++ b/tests/rest/client/test_shadow_banned.py @@ -16,8 +16,13 @@ import synapse.rest.admin from synapse.api.constants import EventTypes -from synapse.rest.client.v1 import directory, login, profile, room -from synapse.rest.client.v2_alpha import room_upgrade_rest_servlet +from synapse.rest.client import ( + directory, + login, + profile, + room, + room_upgrade_rest_servlet, +) from synapse.types import UserID from tests import unittest diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index 28dd47a28b..0ae4029640 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -19,7 +19,7 @@ from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.module_api import ModuleApi from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.types import Requester, StateMap from synapse.util.frozenutils import unfreeze diff --git a/tests/rest/client/v1/test_directory.py b/tests/rest/client/v1/test_directory.py index 8ed470490b..d2181ea907 100644 --- a/tests/rest/client/v1/test_directory.py +++ b/tests/rest/client/v1/test_directory.py @@ -15,7 +15,7 @@ import json from synapse.rest import admin -from synapse.rest.client.v1 import directory, login, room +from synapse.rest.client import directory, login, room from synapse.types import RoomAlias from synapse.util.stringutils import random_string diff --git a/tests/rest/client/v1/test_events.py b/tests/rest/client/v1/test_events.py index 2789d51546..a90294003e 100644 --- a/tests/rest/client/v1/test_events.py +++ b/tests/rest/client/v1/test_events.py @@ -17,7 +17,7 @@ from unittest.mock import Mock import synapse.rest.admin -from synapse.rest.client.v1 import events, login, room +from synapse.rest.client import events, login, room from tests import unittest diff --git a/tests/rest/client/v1/test_login.py b/tests/rest/client/v1/test_login.py index 7eba69642a..eba3552b19 100644 --- a/tests/rest/client/v1/test_login.py +++ b/tests/rest/client/v1/test_login.py @@ -24,9 +24,8 @@ import synapse.rest.admin from synapse.appservice import ApplicationService -from synapse.rest.client.v1 import login, logout -from synapse.rest.client.v2_alpha import devices, register -from synapse.rest.client.v2_alpha.account import WhoamiRestServlet +from synapse.rest.client import devices, login, logout, register +from synapse.rest.client.account import WhoamiRestServlet from synapse.rest.synapse.client import build_synapse_client_resource_tree from synapse.types import create_requester diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index 597e4c67de..1d152352d1 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -17,7 +17,7 @@ from twisted.internet import defer from synapse.handlers.presence import PresenceHandler -from synapse.rest.client.v1 import presence +from synapse.rest.client import presence from synapse.types import UserID from tests import unittest diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py index 165ad33fb7..2860579c2e 100644 --- a/tests/rest/client/v1/test_profile.py +++ b/tests/rest/client/v1/test_profile.py @@ -14,7 +14,7 @@ """Tests REST events for /profile paths.""" from synapse.rest import admin -from synapse.rest.client.v1 import login, profile, room +from synapse.rest.client import login, profile, room from tests import unittest diff --git a/tests/rest/client/v1/test_push_rule_attrs.py b/tests/rest/client/v1/test_push_rule_attrs.py index d077616082..d0ce91ccd9 100644 --- a/tests/rest/client/v1/test_push_rule_attrs.py +++ b/tests/rest/client/v1/test_push_rule_attrs.py @@ -13,7 +13,7 @@ # limitations under the License. import synapse from synapse.api.errors import Codes -from synapse.rest.client.v1 import login, push_rule, room +from synapse.rest.client import login, push_rule, room from tests.unittest import HomeserverTestCase diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 1a9528ec20..0c9cbb9aff 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -29,8 +29,7 @@ from synapse.api.errors import HttpResponseException from synapse.handlers.pagination import PurgeStatus from synapse.rest import admin -from synapse.rest.client.v1 import directory, login, profile, room -from synapse.rest.client.v2_alpha import account +from synapse.rest.client import account, directory, login, profile, room from synapse.types import JsonDict, RoomAlias, UserID, create_requester from synapse.util.stringutils import random_string diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 44e22ca999..b54b004733 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -17,7 +17,7 @@ from unittest.mock import Mock -from synapse.rest.client.v1 import room +from synapse.rest.client import room from synapse.types import UserID from tests import unittest diff --git a/tests/rest/client/v2_alpha/test_account.py b/tests/rest/client/v2_alpha/test_account.py index e7e617e9df..b946fca8b3 100644 --- a/tests/rest/client/v2_alpha/test_account.py +++ b/tests/rest/client/v2_alpha/test_account.py @@ -25,8 +25,7 @@ from synapse.api.constants import LoginType, Membership from synapse.api.errors import Codes, HttpResponseException from synapse.appservice import ApplicationService -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import account, register +from synapse.rest.client import account, login, register, room from synapse.rest.synapse.client.password_reset import PasswordResetSubmitTokenResource from tests import unittest diff --git a/tests/rest/client/v2_alpha/test_auth.py b/tests/rest/client/v2_alpha/test_auth.py index 6b90f838b6..cf5cfb910c 100644 --- a/tests/rest/client/v2_alpha/test_auth.py +++ b/tests/rest/client/v2_alpha/test_auth.py @@ -19,8 +19,7 @@ import synapse.rest.admin from synapse.api.constants import LoginType from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker -from synapse.rest.client.v1 import login -from synapse.rest.client.v2_alpha import account, auth, devices, register +from synapse.rest.client import account, auth, devices, login, register from synapse.rest.synapse.client import build_synapse_client_resource_tree from synapse.types import JsonDict, UserID diff --git a/tests/rest/client/v2_alpha/test_capabilities.py b/tests/rest/client/v2_alpha/test_capabilities.py index f80f48a455..ad83b3d2ff 100644 --- a/tests/rest/client/v2_alpha/test_capabilities.py +++ b/tests/rest/client/v2_alpha/test_capabilities.py @@ -13,8 +13,7 @@ # limitations under the License. import synapse.rest.admin from synapse.api.room_versions import KNOWN_ROOM_VERSIONS -from synapse.rest.client.v1 import login -from synapse.rest.client.v2_alpha import capabilities +from synapse.rest.client import capabilities, login from tests import unittest from tests.unittest import override_config diff --git a/tests/rest/client/v2_alpha/test_filter.py b/tests/rest/client/v2_alpha/test_filter.py index c7e47725b7..475c6bed3d 100644 --- a/tests/rest/client/v2_alpha/test_filter.py +++ b/tests/rest/client/v2_alpha/test_filter.py @@ -15,7 +15,7 @@ from twisted.internet import defer from synapse.api.errors import Codes -from synapse.rest.client.v2_alpha import filter +from synapse.rest.client import filter from tests import unittest diff --git a/tests/rest/client/v2_alpha/test_password_policy.py b/tests/rest/client/v2_alpha/test_password_policy.py index 6f07ff6cbb..3cf5871899 100644 --- a/tests/rest/client/v2_alpha/test_password_policy.py +++ b/tests/rest/client/v2_alpha/test_password_policy.py @@ -17,8 +17,7 @@ from synapse.api.constants import LoginType from synapse.api.errors import Codes from synapse.rest import admin -from synapse.rest.client.v1 import login -from synapse.rest.client.v2_alpha import account, password_policy, register +from synapse.rest.client import account, login, password_policy, register from tests import unittest diff --git a/tests/rest/client/v2_alpha/test_register.py b/tests/rest/client/v2_alpha/test_register.py index a52e5e608a..fecda037a5 100644 --- a/tests/rest/client/v2_alpha/test_register.py +++ b/tests/rest/client/v2_alpha/test_register.py @@ -23,8 +23,7 @@ from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType from synapse.api.errors import Codes from synapse.appservice import ApplicationService -from synapse.rest.client.v1 import login, logout -from synapse.rest.client.v2_alpha import account, account_validity, register, sync +from synapse.rest.client import account, account_validity, login, logout, register, sync from tests import unittest from tests.unittest import override_config diff --git a/tests/rest/client/v2_alpha/test_relations.py b/tests/rest/client/v2_alpha/test_relations.py index 2e2f94742e..02b5e9a8d0 100644 --- a/tests/rest/client/v2_alpha/test_relations.py +++ b/tests/rest/client/v2_alpha/test_relations.py @@ -19,8 +19,7 @@ from synapse.api.constants import EventTypes, RelationTypes from synapse.rest import admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import register, relations +from synapse.rest.client import login, register, relations, room from tests import unittest diff --git a/tests/rest/client/v2_alpha/test_report_event.py b/tests/rest/client/v2_alpha/test_report_event.py index a76a6fef1e..ee6b0b9ebf 100644 --- a/tests/rest/client/v2_alpha/test_report_event.py +++ b/tests/rest/client/v2_alpha/test_report_event.py @@ -15,8 +15,7 @@ import json import synapse.rest.admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import report_event +from synapse.rest.client import login, report_event, room from tests import unittest diff --git a/tests/rest/client/v2_alpha/test_sendtodevice.py b/tests/rest/client/v2_alpha/test_sendtodevice.py index c9c99cc5d7..6db7062a8e 100644 --- a/tests/rest/client/v2_alpha/test_sendtodevice.py +++ b/tests/rest/client/v2_alpha/test_sendtodevice.py @@ -13,8 +13,7 @@ # limitations under the License. from synapse.rest import admin -from synapse.rest.client.v1 import login -from synapse.rest.client.v2_alpha import sendtodevice, sync +from synapse.rest.client import login, sendtodevice, sync from tests.unittest import HomeserverTestCase, override_config diff --git a/tests/rest/client/v2_alpha/test_shared_rooms.py b/tests/rest/client/v2_alpha/test_shared_rooms.py index cedb9614a8..283eccd53f 100644 --- a/tests/rest/client/v2_alpha/test_shared_rooms.py +++ b/tests/rest/client/v2_alpha/test_shared_rooms.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import synapse.rest.admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import shared_rooms +from synapse.rest.client import login, room, shared_rooms from tests import unittest from tests.server import FakeChannel diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py index 15748ed4fd..95be369d4b 100644 --- a/tests/rest/client/v2_alpha/test_sync.py +++ b/tests/rest/client/v2_alpha/test_sync.py @@ -21,8 +21,7 @@ ReadReceiptEventFields, RelationTypes, ) -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import knock, read_marker, receipts, sync +from synapse.rest.client import knock, login, read_marker, receipts, room, sync from tests import unittest from tests.federation.transport.test_knocking import ( diff --git a/tests/rest/client/v2_alpha/test_upgrade_room.py b/tests/rest/client/v2_alpha/test_upgrade_room.py index 5f3f15fc57..72f976d8e2 100644 --- a/tests/rest/client/v2_alpha/test_upgrade_room.py +++ b/tests/rest/client/v2_alpha/test_upgrade_room.py @@ -15,8 +15,7 @@ from synapse.config.server import DEFAULT_ROOM_VERSION from synapse.rest import admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import room_upgrade_rest_servlet +from synapse.rest.client import login, room, room_upgrade_rest_servlet from tests import unittest from tests.server import FakeChannel diff --git a/tests/rest/media/v1/test_media_storage.py b/tests/rest/media/v1/test_media_storage.py index 2d6b49692e..6085444b9d 100644 --- a/tests/rest/media/v1/test_media_storage.py +++ b/tests/rest/media/v1/test_media_storage.py @@ -30,7 +30,7 @@ from synapse.events.spamcheck import load_legacy_spam_checkers from synapse.logging.context import make_deferred_yieldable from synapse.rest import admin -from synapse.rest.client.v1 import login +from synapse.rest.client import login from synapse.rest.media.v1._base import FileInfo from synapse.rest.media.v1.filepath import MediaFilePaths from synapse.rest.media.v1.media_storage import MediaStorage diff --git a/tests/server_notices/test_consent.py b/tests/server_notices/test_consent.py index ac98259b7e..58b399a043 100644 --- a/tests/server_notices/test_consent.py +++ b/tests/server_notices/test_consent.py @@ -15,8 +15,7 @@ import os import synapse.rest.admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import sync +from synapse.rest.client import login, room, sync from tests import unittest diff --git a/tests/server_notices/test_resource_limits_server_notices.py b/tests/server_notices/test_resource_limits_server_notices.py index 3245aa91ca..8701b5f7e3 100644 --- a/tests/server_notices/test_resource_limits_server_notices.py +++ b/tests/server_notices/test_resource_limits_server_notices.py @@ -19,8 +19,7 @@ from synapse.api.constants import EventTypes, LimitBlockingTypes, ServerNoticeMsgType from synapse.api.errors import ResourceLimitError from synapse.rest import admin -from synapse.rest.client.v1 import login, room -from synapse.rest.client.v2_alpha import sync +from synapse.rest.client import login, room, sync from synapse.server_notices.resource_limits_server_notices import ( ResourceLimitsServerNotices, ) diff --git a/tests/storage/databases/main/test_events_worker.py b/tests/storage/databases/main/test_events_worker.py index d05d367685..a649e8c618 100644 --- a/tests/storage/databases/main/test_events_worker.py +++ b/tests/storage/databases/main/test_events_worker.py @@ -15,7 +15,7 @@ from synapse.logging.context import LoggingContext from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.storage.databases.main.events_worker import EventsWorkerStore from synapse.util.async_helpers import yieldable_gather_results diff --git a/tests/storage/test_cleanup_extrems.py b/tests/storage/test_cleanup_extrems.py index 77c4fe721c..da98733ce8 100644 --- a/tests/storage/test_cleanup_extrems.py +++ b/tests/storage/test_cleanup_extrems.py @@ -17,7 +17,7 @@ import synapse.rest.admin from synapse.api.constants import EventTypes -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.storage import prepare_database from synapse.types import UserID, create_requester diff --git a/tests/storage/test_client_ips.py b/tests/storage/test_client_ips.py index e57fce9694..1c2df54ecc 100644 --- a/tests/storage/test_client_ips.py +++ b/tests/storage/test_client_ips.py @@ -17,7 +17,7 @@ import synapse.rest.admin from synapse.http.site import XForwardedForRequest -from synapse.rest.client.v1 import login +from synapse.rest.client import login from tests import unittest from tests.server import make_request diff --git a/tests/storage/test_event_chain.py b/tests/storage/test_event_chain.py index d87f124c26..93136f0717 100644 --- a/tests/storage/test_event_chain.py +++ b/tests/storage/test_event_chain.py @@ -20,7 +20,7 @@ from synapse.api.room_versions import RoomVersions from synapse.events import EventBase from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.storage.databases.main.events import _LinkMap from synapse.types import create_requester diff --git a/tests/storage/test_events.py b/tests/storage/test_events.py index 617bc8091f..f462a8b1c7 100644 --- a/tests/storage/test_events.py +++ b/tests/storage/test_events.py @@ -17,7 +17,7 @@ from synapse.api.room_versions import RoomVersions from synapse.federation.federation_base import event_from_pdu_json from synapse.rest import admin -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from tests.unittest import HomeserverTestCase diff --git a/tests/storage/test_purge.py b/tests/storage/test_purge.py index e5574063f1..22a77c3ccc 100644 --- a/tests/storage/test_purge.py +++ b/tests/storage/test_purge.py @@ -13,7 +13,7 @@ # limitations under the License. from synapse.api.errors import NotFoundError, SynapseError -from synapse.rest.client.v1 import room +from synapse.rest.client import room from tests.unittest import HomeserverTestCase diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py index 9fa968f6bb..c72dc40510 100644 --- a/tests/storage/test_roommember.py +++ b/tests/storage/test_roommember.py @@ -15,7 +15,7 @@ from synapse.api.constants import Membership from synapse.rest.admin import register_servlets_for_client_rest_resource -from synapse.rest.client.v1 import login, room +from synapse.rest.client import login, room from synapse.types import UserID, create_requester from tests import unittest diff --git a/tests/test_mau.py b/tests/test_mau.py index fa6ef92b3b..66111eb367 100644 --- a/tests/test_mau.py +++ b/tests/test_mau.py @@ -17,7 +17,7 @@ from synapse.api.constants import APP_SERVICE_REGISTRATION_TYPE, LoginType from synapse.api.errors import Codes, HttpResponseException, SynapseError from synapse.appservice import ApplicationService -from synapse.rest.client.v2_alpha import register, sync +from synapse.rest.client import register, sync from tests import unittest from tests.unittest import override_config diff --git a/tests/test_terms_auth.py b/tests/test_terms_auth.py index 0df480db9f..67dcf567cd 100644 --- a/tests/test_terms_auth.py +++ b/tests/test_terms_auth.py @@ -17,7 +17,7 @@ from twisted.test.proto_helpers import MemoryReactorClock -from synapse.rest.client.v2_alpha.register import register_servlets +from synapse.rest.client.register import register_servlets from synapse.util import Clock from tests import unittest From 5f7b1e1f276fdd25304ff06076e1cd77cf3a9640 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 17 Aug 2021 13:13:11 +0100 Subject: [PATCH 578/619] Make `PeriodicallyFlushingMemoryHandler` the default logging handler. (#10518) --- changelog.d/10518.feature | 1 + docker/conf/log.config | 27 ++++++++++++++++++++------- docs/sample_log_config.yaml | 27 ++++++++++++++++++++------- synapse/config/logger.py | 27 ++++++++++++++++++++------- 4 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 changelog.d/10518.feature diff --git a/changelog.d/10518.feature b/changelog.d/10518.feature new file mode 100644 index 0000000000..112e4d105c --- /dev/null +++ b/changelog.d/10518.feature @@ -0,0 +1 @@ +The default logging handler for new installations is now `PeriodicallyFlushingMemoryHandler`, a buffered logging handler which periodically flushes itself. diff --git a/docker/conf/log.config b/docker/conf/log.config index a994626926..7a216a36a0 100644 --- a/docker/conf/log.config +++ b/docker/conf/log.config @@ -18,18 +18,31 @@ handlers: backupCount: 6 # Does not include the current log file. encoding: utf8 - # Default to buffering writes to log file for efficiency. This means that - # there will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR - # logs will still be flushed immediately. + # Default to buffering writes to log file for efficiency. + # WARNING/ERROR logs will still be flushed immediately, but there will be a + # delay (of up to `period` seconds, or until the buffer is full with + # `capacity` messages) before INFO/DEBUG logs get written. buffer: - class: logging.handlers.MemoryHandler + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler target: file - # The capacity is the number of log lines that are buffered before - # being written to disk. Increasing this will lead to better + + # The capacity is the maximum number of log lines that are buffered + # before being written to disk. Increasing this will lead to better # performance, at the expensive of it taking longer for log lines to # be written to disk. + # This parameter is required. capacity: 10 - flushLevel: 30 # Flush for WARNING logs as well + + # Logs with a level at or above the flush level will cause the buffer to + # be flushed immediately. + # Default value: 40 (ERROR) + # Other values: 50 (CRITICAL), 30 (WARNING), 20 (INFO), 10 (DEBUG) + flushLevel: 30 # Flush immediately for WARNING logs and higher + + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + # Default value: 5 seconds + period: 5 {% endif %} console: diff --git a/docs/sample_log_config.yaml b/docs/sample_log_config.yaml index 669e600081..2485ad25ed 100644 --- a/docs/sample_log_config.yaml +++ b/docs/sample_log_config.yaml @@ -24,18 +24,31 @@ handlers: backupCount: 3 # Does not include the current log file. encoding: utf8 - # Default to buffering writes to log file for efficiency. This means that - # will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR - # logs will still be flushed immediately. + # Default to buffering writes to log file for efficiency. + # WARNING/ERROR logs will still be flushed immediately, but there will be a + # delay (of up to `period` seconds, or until the buffer is full with + # `capacity` messages) before INFO/DEBUG logs get written. buffer: - class: logging.handlers.MemoryHandler + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler target: file - # The capacity is the number of log lines that are buffered before - # being written to disk. Increasing this will lead to better + + # The capacity is the maximum number of log lines that are buffered + # before being written to disk. Increasing this will lead to better # performance, at the expensive of it taking longer for log lines to # be written to disk. + # This parameter is required. capacity: 10 - flushLevel: 30 # Flush for WARNING logs as well + + # Logs with a level at or above the flush level will cause the buffer to + # be flushed immediately. + # Default value: 40 (ERROR) + # Other values: 50 (CRITICAL), 30 (WARNING), 20 (INFO), 10 (DEBUG) + flushLevel: 30 # Flush immediately for WARNING logs and higher + + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + # Default value: 5 seconds + period: 5 # A handler that writes logs to stderr. Unused by default, but can be used # instead of "buffer" and "file" in the logger handlers. diff --git a/synapse/config/logger.py b/synapse/config/logger.py index ad4e6e61c3..4a398a7932 100644 --- a/synapse/config/logger.py +++ b/synapse/config/logger.py @@ -67,18 +67,31 @@ backupCount: 3 # Does not include the current log file. encoding: utf8 - # Default to buffering writes to log file for efficiency. This means that - # will be a delay for INFO/DEBUG logs to get written, but WARNING/ERROR - # logs will still be flushed immediately. + # Default to buffering writes to log file for efficiency. + # WARNING/ERROR logs will still be flushed immediately, but there will be a + # delay (of up to `period` seconds, or until the buffer is full with + # `capacity` messages) before INFO/DEBUG logs get written. buffer: - class: logging.handlers.MemoryHandler + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler target: file - # The capacity is the number of log lines that are buffered before - # being written to disk. Increasing this will lead to better + + # The capacity is the maximum number of log lines that are buffered + # before being written to disk. Increasing this will lead to better # performance, at the expensive of it taking longer for log lines to # be written to disk. + # This parameter is required. capacity: 10 - flushLevel: 30 # Flush for WARNING logs as well + + # Logs with a level at or above the flush level will cause the buffer to + # be flushed immediately. + # Default value: 40 (ERROR) + # Other values: 50 (CRITICAL), 30 (WARNING), 20 (INFO), 10 (DEBUG) + flushLevel: 30 # Flush immediately for WARNING logs and higher + + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + # Default value: 5 seconds + period: 5 # A handler that writes logs to stderr. Unused by default, but can be used # instead of "buffer" and "file" in the logger handlers. From c4cf0c047329e125f0940281fd53688474d26581 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 17 Aug 2021 08:19:12 -0400 Subject: [PATCH 579/619] Attempt to pull from the legacy spaces summary API over federation. (#10583) If the new /hierarchy API does not exist on all destinations, fallback to querying the /spaces API and translating the results. This is a backwards compatibility hack since not all of the federated homeservers will update at the same time. --- changelog.d/10583.feature | 1 + synapse/federation/federation_client.py | 64 +++++++++++++++++++++---- 2 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 changelog.d/10583.feature diff --git a/changelog.d/10583.feature b/changelog.d/10583.feature new file mode 100644 index 0000000000..ffc4e4289c --- /dev/null +++ b/changelog.d/10583.feature @@ -0,0 +1 @@ +Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 0af953a5d6..29979414e3 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -1364,13 +1364,59 @@ async def send_request( return room, children, inaccessible_children - # TODO Fallback to the old federation API and translate the results. - return await self._try_destination_list( - "fetch room hierarchy", - destinations, - send_request, - failover_on_unknown_endpoint=True, - ) + try: + return await self._try_destination_list( + "fetch room hierarchy", + destinations, + send_request, + failover_on_unknown_endpoint=True, + ) + except SynapseError as e: + # Fallback to the old federation API and translate the results if + # no servers implement the new API. + # + # The algorithm below is a bit inefficient as it only attempts to + # get information for the requested room, but the legacy API may + # return additional layers. + if e.code == 502: + legacy_result = await self.get_space_summary( + destinations, + room_id, + suggested_only, + max_rooms_per_space=None, + exclude_rooms=[], + ) + + # Find the requested room in the response (and remove it). + for _i, room in enumerate(legacy_result.rooms): + if room.get("room_id") == room_id: + break + else: + # The requested room was not returned, nothing we can do. + raise + requested_room = legacy_result.rooms.pop(_i) + + # Find any children events of the requested room. + children_events = [] + children_room_ids = set() + for event in legacy_result.events: + if event.room_id == room_id: + children_events.append(event.data) + children_room_ids.add(event.state_key) + # And add them under the requested room. + requested_room["children_state"] = children_events + + # Find the children rooms. + children = [] + for room in legacy_result.rooms: + if room.get("room_id") in children_room_ids: + children.append(room) + + # It isn't clear from the response whether some of the rooms are + # not accessible. + return requested_room, children, () + + raise @attr.s(frozen=True, slots=True, auto_attribs=True) @@ -1430,7 +1476,7 @@ def from_json_dict(cls, d: JsonDict) -> "FederationSpaceSummaryEventResult": class FederationSpaceSummaryResult: """Represents the data returned by a successful get_space_summary call.""" - rooms: Sequence[JsonDict] + rooms: List[JsonDict] events: Sequence[FederationSpaceSummaryEventResult] @classmethod @@ -1444,7 +1490,7 @@ def from_json_dict(cls, d: JsonDict) -> "FederationSpaceSummaryResult": ValueError if d is not a valid /spaces/ response """ rooms = d.get("rooms") - if not isinstance(rooms, Sequence): + if not isinstance(rooms, List): raise ValueError("'rooms' must be a list") if any(not isinstance(r, dict) for r in rooms): raise ValueError("Invalid room in 'rooms' list") From 56397599809e131174daaeb4c6dc18fde9db6c3f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 17 Aug 2021 14:45:24 +0200 Subject: [PATCH 580/619] Centralise the custom template directory (#10596) Several configuration sections are using separate settings for custom template directories, which can be confusing. This PR adds a new top-level configuration for a custom template directory which is then used for every module. The only exception is the consent templates, since the consent template directory require a specific hierarchy, so it's probably better that it stays separate from everything else. --- changelog.d/10596.removal | 1 + docs/SUMMARY.md | 1 + docs/sample_config.yaml | 225 ++--------------- docs/templates.md | 239 ++++++++++++++++++ docs/upgrade.md | 11 + synapse/config/account_validity.py | 7 +- synapse/config/emailconfig.py | 71 ++---- synapse/config/server.py | 25 ++ synapse/config/sso.py | 173 +------------ synapse/module_api/__init__.py | 3 +- .../rest/synapse/client/new_user_consent.py | 2 + synapse/rest/synapse/client/pick_username.py | 2 + 12 files changed, 342 insertions(+), 418 deletions(-) create mode 100644 changelog.d/10596.removal create mode 100644 docs/templates.md diff --git a/changelog.d/10596.removal b/changelog.d/10596.removal new file mode 100644 index 0000000000..e69f632db4 --- /dev/null +++ b/changelog.d/10596.removal @@ -0,0 +1 @@ +The `template_dir` configuration settings in the `sso`, `account_validity` and `email` sections of the configuration file are now deprecated in favour of the global `templates.custom_template_directory` setting. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 3d320a1c43..56e0141c2b 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -21,6 +21,7 @@ - [Homeserver Sample Config File](usage/configuration/homeserver_sample_config.md) - [Logging Sample Config File](usage/configuration/logging_sample_config.md) - [Structured Logging](structured_logging.md) + - [Templates](templates.md) - [User Authentication](usage/configuration/user_authentication/README.md) - [Single-Sign On]() - [OpenID Connect](openid.md) diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index aeebcaf45f..3ec76d5abf 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -565,6 +565,19 @@ retention: # #next_link_domain_whitelist: ["matrix.org"] +# Templates to use when generating email or HTML page contents. +# +templates: + # Directory in which Synapse will try to find template files to use to generate + # email or HTML page contents. + # If not set, or a file is not found within the template directory, a default + # template from within the Synapse package will be used. + # + # See https://matrix-org.github.io/synapse/latest/templates.html for more + # information about using custom templates. + # + #custom_template_directory: /path/to/custom/templates/ + ## TLS ## @@ -1895,6 +1908,9 @@ cas_config: # Additional settings to use with single-sign on systems such as OpenID Connect, # SAML2 and CAS. # +# Server admins can configure custom templates for pages related to SSO. See +# https://matrix-org.github.io/synapse/latest/templates.html for more information. +# sso: # A list of client URLs which are whitelisted so that the user does not # have to confirm giving access to their account to the URL. Any client @@ -1927,169 +1943,6 @@ sso: # #update_profile_information: true - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * HTML page to prompt the user to choose an Identity Provider during - # login: 'sso_login_idp_picker.html'. - # - # This is only used if multiple SSO Identity Providers are configured. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL that the user will be redirected to after - # login. - # - # * server_name: the homeserver's name. - # - # * providers: a list of available Identity Providers. Each element is - # an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # The rendered HTML page should contain a form which submits its results - # back as a GET request, with the following query parameters: - # - # * redirectUrl: the client redirect URI (ie, the `redirect_url` passed - # to the template) - # - # * idp: the 'idp_id' of the chosen IDP. - # - # * HTML page to prompt new users to enter a userid and confirm other - # details: 'sso_auth_account_details.html'. This is only shown if the - # SSO implementation (with any user_mapping_provider) does not return - # a localpart. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * idp: details of the SSO Identity Provider that the user logged in - # with: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * user_attributes: an object containing details about the user that - # we received from the IdP. May have the following attributes: - # - # * display_name: the user's display_name - # * emails: a list of email addresses - # - # The template should render a form which submits the following fields: - # - # * username: the localpart of the user's chosen user id - # - # * HTML page allowing the user to consent to the server's terms and - # conditions. This is only shown for new users, and only if - # `user_consent.require_at_registration` is set. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * user_id: the user's matrix proposed ID. - # - # * user_profile.display_name: the user's proposed display name, if any. - # - # * consent_version: the version of the terms that the user will be - # shown - # - # * terms_url: a link to the page showing the terms. - # - # The template should render a form which submits the following fields: - # - # * accepted_version: the version of the terms accepted by the user - # (ie, 'consent_version' from the input variables). - # - # * HTML page for a confirmation step before redirecting back to the client - # with the login token: 'sso_redirect_confirm.html'. - # - # When rendering, this template is given the following variables: - # - # * redirect_url: the URL the user is about to be redirected to. - # - # * display_url: the same as `redirect_url`, but with the query - # parameters stripped. The intention is to have a - # human-readable URL to show to users, not to use it as - # the final address to redirect to. - # - # * server_name: the homeserver's name. - # - # * new_user: a boolean indicating whether this is the user's first time - # logging in. - # - # * user_id: the user's matrix ID. - # - # * user_profile.avatar_url: an MXC URI for the user's avatar, if any. - # None if the user has not set an avatar. - # - # * user_profile.display_name: the user's display name. None if the user - # has not set a display name. - # - # * HTML page which notifies the user that they are authenticating to confirm - # an operation on their account during the user interactive authentication - # process: 'sso_auth_confirm.html'. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL the user is about to be redirected to. - # - # * description: the operation which the user is being asked to confirm - # - # * idp: details of the Identity Provider that we will use to confirm - # the user's identity: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * HTML page shown after a successful user interactive authentication session: - # 'sso_auth_success.html'. - # - # Note that this page must include the JavaScript which notifies of a successful authentication - # (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback). - # - # This template has no additional variables. - # - # * HTML page shown after a user-interactive authentication session which - # does not map correctly onto the expected user: 'sso_auth_bad_user.html'. - # - # When rendering, this template is given the following variables: - # * server_name: the homeserver's name. - # * user_id_to_verify: the MXID of the user that we are trying to - # validate. - # - # * HTML page shown during single sign-on if a deactivated user (according to Synapse's database) - # attempts to login: 'sso_account_deactivated.html'. - # - # This template has no additional variables. - # - # * HTML page to display to users if something goes wrong during the - # OpenID Connect authentication process: 'sso_error.html'. - # - # When rendering, this template is given two variables: - # * error: the technical name of the error - # * error_description: a human-readable message for the error - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" - # JSON web token integration. The following settings can be used to make # Synapse JSON web tokens for authentication, instead of its internal @@ -2220,6 +2073,9 @@ ui_auth: # Configuration for sending emails from Synapse. # +# Server admins can configure custom templates for email content. See +# https://matrix-org.github.io/synapse/latest/templates.html for more information. +# email: # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. # @@ -2296,49 +2152,6 @@ email: # #invite_client_location: https://app.element.io - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * The contents of email notifications of missed events: 'notif_mail.html' and - # 'notif_mail.txt'. - # - # * The contents of account expiry notice emails: 'notice_expiry.html' and - # 'notice_expiry.txt'. - # - # * The contents of password reset emails sent by the homeserver: - # 'password_reset.html' and 'password_reset.txt' - # - # * An HTML page that a user will see when they follow the link in the password - # reset email. The user will be asked to confirm the action before their - # password is reset: 'password_reset_confirmation.html' - # - # * HTML pages for success and failure that a user will see when they confirm - # the password reset flow using the page above: 'password_reset_success.html' - # and 'password_reset_failure.html' - # - # * The contents of address verification emails sent during registration: - # 'registration.html' and 'registration.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent during registration: - # 'registration_success.html' and 'registration_failure.html' - # - # * The contents of address verification emails sent when an address is added - # to a Matrix account: 'add_threepid.html' and 'add_threepid.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent when an address is added - # to a Matrix account: 'add_threepid_success.html' and - # 'add_threepid_failure.html' - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" - # Subjects to use when sending emails from Synapse. # # The placeholder '%(app)s' will be replaced with the value of the 'app_name' diff --git a/docs/templates.md b/docs/templates.md new file mode 100644 index 0000000000..a240f58b54 --- /dev/null +++ b/docs/templates.md @@ -0,0 +1,239 @@ +# Templates + +Synapse uses parametrised templates to generate the content of emails it sends and +webpages it shows to users. + +By default, Synapse will use the templates listed [here](https://github.com/matrix-org/synapse/tree/master/synapse/res/templates). +Server admins can configure an additional directory for Synapse to look for templates +in, allowing them to specify custom templates: + +```yaml +templates: + custom_templates_directory: /path/to/custom/templates/ +``` + +If this setting is not set, or the files named below are not found within the directory, +default templates from within the Synapse package will be used. + +Templates that are given variables when being rendered are rendered using [Jinja 2](https://jinja.palletsprojects.com/en/2.11.x/). +Templates rendered by Jinja 2 can also access two functions on top of the functions +already available as part of Jinja 2: + +```python +format_ts(value: int, format: str) -> str +``` + +Formats a timestamp in milliseconds. + +Example: `reason.last_sent_ts|format_ts("%c")` + +```python +mxc_to_http(value: str, width: int, height: int, resize_method: str = "crop") -> str +``` + +Turns a `mxc://` URL for media content into an HTTP(S) one using the homeserver's +`public_baseurl` configuration setting as the URL's base. + +Example: `message.sender_avatar_url|mxc_to_http(32,32)` + + +## Email templates + +Below are the templates Synapse will look for when generating the content of an email: + +* `notif_mail.html` and `notif_mail.txt`: The contents of email notifications of missed + events. + When rendering, this template is given the following variables: + * `user_display_name`: the display name for the user receiving the notification + * `unsubscribe_link`: the link users can click to unsubscribe from email notifications + * `summary_text`: a summary of the notification(s). The text used can be customised + by configuring the various settings in the `email.subjects` section of the + configuration file. + * `rooms`: a list of rooms containing events to include in the email. Each element is + an object with the following attributes: + * `title`: a human-readable name for the room + * `hash`: a hash of the ID of the room + * `invite`: a boolean, which is `True` if the room is an invite the user hasn't + accepted yet, `False` otherwise + * `notifs`: a list of events, or an empty list if `invite` is `True`. Each element + is an object with the following attributes: + * `link`: a `matrix.to` link to the event + * `ts`: the time in milliseconds at which the event was received + * `messages`: a list of messages containing one message before the event, the + message in the event, and one message after the event. Each element is an + object with the following attributes: + * `event_type`: the type of the event + * `is_historical`: a boolean, which is `False` if the message is the one + that triggered the notification, `True` otherwise + * `id`: the ID of the event + * `ts`: the time in milliseconds at which the event was sent + * `sender_name`: the display name for the event's sender + * `sender_avatar_url`: the avatar URL (as a `mxc://` URL) for the event's + sender + * `sender_hash`: a hash of the user ID of the sender + * `link`: a `matrix.to` link to the room + * `reason`: information on the event that triggered the email to be sent. It's an + object with the following attributes: + * `room_id`: the ID of the room the event was sent in + * `room_name`: a human-readable name for the room the event was sent in + * `now`: the current time in milliseconds + * `received_at`: the time in milliseconds at which the event was received + * `delay_before_mail_ms`: the amount of time in milliseconds Synapse always waits + before ever emailing about a notification (to give the user a chance to respond + to other push or notice the window) + * `last_sent_ts`: the time in milliseconds at which a notification was last sent + for an event in this room + * `throttle_ms`: the minimum amount of time in milliseconds between two + notifications can be sent for this room +* `password_reset.html` and `password_reset.txt`: The contents of password reset emails + sent by the homeserver. + When rendering, these templates are given a `link` variable which contains the link the + user must click in order to reset their password. +* `registration.html` and `registration.txt`: The contents of address verification emails + sent during registration. + When rendering, these templates are given a `link` variable which contains the link the + user must click in order to validate their email address. +* `add_threepid.html` and `add_threepid.txt`: The contents of address verification emails + sent when an address is added to a Matrix account. + When rendering, these templates are given a `link` variable which contains the link the + user must click in order to validate their email address. + + +## HTML page templates for registration and password reset + +Below are the templates Synapse will look for when generating pages related to +registration and password reset: + +* `password_reset_confirmation.html`: An HTML page that a user will see when they follow + the link in the password reset email. The user will be asked to confirm the action + before their password is reset. + When rendering, this template is given the following variables: + * `sid`: the session ID for the password reset + * `token`: the token for the password reset + * `client_secret`: the client secret for the password reset +* `password_reset_success.html` and `password_reset_failure.html`: HTML pages for success + and failure that a user will see when they confirm the password reset flow using the + page above. + When rendering, `password_reset_success.html` is given no variable, and + `password_reset_failure.html` is given a `failure_reason`, which contains the reason + for the password reset failure. +* `registration_success.html` and `registration_failure.html`: HTML pages for success and + failure that a user will see when they follow the link in an address verification email + sent during registration. + When rendering, `registration_success.html` is given no variable, and + `registration_failure.html` is given a `failure_reason`, which contains the reason + for the registration failure. +* `add_threepid_success.html` and `add_threepid_failure.html`: HTML pages for success and + failure that a user will see when they follow the link in an address verification email + sent when an address is added to a Matrix account. + When rendering, `add_threepid_success.html` is given no variable, and + `add_threepid_failure.html` is given a `failure_reason`, which contains the reason + for the registration failure. + + +## HTML page templates for Single Sign-On (SSO) + +Below are the templates Synapse will look for when generating pages related to SSO: + +* `sso_login_idp_picker.html`: HTML page to prompt the user to choose an + Identity Provider during login. + This is only used if multiple SSO Identity Providers are configured. + When rendering, this template is given the following variables: + * `redirect_url`: the URL that the user will be redirected to after + login. + * `server_name`: the homeserver's name. + * `providers`: a list of available Identity Providers. Each element is + an object with the following attributes: + * `idp_id`: unique identifier for the IdP + * `idp_name`: user-facing name for the IdP + * `idp_icon`: if specified in the IdP config, an MXC URI for an icon + for the IdP + * `idp_brand`: if specified in the IdP config, a textual identifier + for the brand of the IdP + The rendered HTML page should contain a form which submits its results + back as a GET request, with the following query parameters: + * `redirectUrl`: the client redirect URI (ie, the `redirect_url` passed + to the template) + * `idp`: the 'idp_id' of the chosen IDP. +* `sso_auth_account_details.html`: HTML page to prompt new users to enter a + userid and confirm other details. This is only shown if the + SSO implementation (with any `user_mapping_provider`) does not return + a localpart. + When rendering, this template is given the following variables: + * `server_name`: the homeserver's name. + * `idp`: details of the SSO Identity Provider that the user logged in + with: an object with the following attributes: + * `idp_id`: unique identifier for the IdP + * `idp_name`: user-facing name for the IdP + * `idp_icon`: if specified in the IdP config, an MXC URI for an icon + for the IdP + * `idp_brand`: if specified in the IdP config, a textual identifier + for the brand of the IdP + * `user_attributes`: an object containing details about the user that + we received from the IdP. May have the following attributes: + * display_name: the user's display_name + * emails: a list of email addresses + The template should render a form which submits the following fields: + * `username`: the localpart of the user's chosen user id +* `sso_new_user_consent.html`: HTML page allowing the user to consent to the + server's terms and conditions. This is only shown for new users, and only if + `user_consent.require_at_registration` is set. + When rendering, this template is given the following variables: + * `server_name`: the homeserver's name. + * `user_id`: the user's matrix proposed ID. + * `user_profile.display_name`: the user's proposed display name, if any. + * consent_version: the version of the terms that the user will be + shown + * `terms_url`: a link to the page showing the terms. + The template should render a form which submits the following fields: + * `accepted_version`: the version of the terms accepted by the user + (ie, 'consent_version' from the input variables). +* `sso_redirect_confirm.html`: HTML page for a confirmation step before redirecting back + to the client with the login token. + When rendering, this template is given the following variables: + * `redirect_url`: the URL the user is about to be redirected to. + * `display_url`: the same as `redirect_url`, but with the query + parameters stripped. The intention is to have a + human-readable URL to show to users, not to use it as + the final address to redirect to. + * `server_name`: the homeserver's name. + * `new_user`: a boolean indicating whether this is the user's first time + logging in. + * `user_id`: the user's matrix ID. + * `user_profile.avatar_url`: an MXC URI for the user's avatar, if any. + `None` if the user has not set an avatar. + * `user_profile.display_name`: the user's display name. `None` if the user + has not set a display name. +* `sso_auth_confirm.html`: HTML page which notifies the user that they are authenticating + to confirm an operation on their account during the user interactive authentication + process. + When rendering, this template is given the following variables: + * `redirect_url`: the URL the user is about to be redirected to. + * `description`: the operation which the user is being asked to confirm + * `idp`: details of the Identity Provider that we will use to confirm + the user's identity: an object with the following attributes: + * `idp_id`: unique identifier for the IdP + * `idp_name`: user-facing name for the IdP + * `idp_icon`: if specified in the IdP config, an MXC URI for an icon + for the IdP + * `idp_brand`: if specified in the IdP config, a textual identifier + for the brand of the IdP +* `sso_auth_success.html`: HTML page shown after a successful user interactive + authentication session. + Note that this page must include the JavaScript which notifies of a successful + authentication (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback). + This template has no additional variables. +* `sso_auth_bad_user.html`: HTML page shown after a user-interactive authentication + session which does not map correctly onto the expected user. + When rendering, this template is given the following variables: + * `server_name`: the homeserver's name. + * `user_id_to_verify`: the MXID of the user that we are trying to + validate. +* `sso_account_deactivated.html`: HTML page shown during single sign-on if a deactivated + user (according to Synapse's database) attempts to login. + This template has no additional variables. +* `sso_error.html`: HTML page to display to users if something goes wrong during the + OpenID Connect authentication process. + When rendering, this template is given two variables: + * `error`: the technical name of the error + * `error_description`: a human-readable message for the error diff --git a/docs/upgrade.md b/docs/upgrade.md index 8831c9d6cf..1c459d8e2b 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -112,6 +112,17 @@ environment variable. See [using a forward proxy with Synapse documentation](setup/forward_proxy.md) for details. +## Deprecation of `template_dir` + +The `template_dir` settings in the `sso`, `account_validity` and `email` sections of the +configuration file are now deprecated. Server admins should use the new +`templates.custom_template_directory` setting in the configuration file and use one single +custom template directory for all aforementioned features. Template file names remain +unchanged. See [the related documentation](https://matrix-org.github.io/synapse/latest/templates.html) +for more information and examples. + +We plan to remove support for these settings in October 2021. + # Upgrading to v1.39.0 diff --git a/synapse/config/account_validity.py b/synapse/config/account_validity.py index 9acce5996e..52e63ab1f6 100644 --- a/synapse/config/account_validity.py +++ b/synapse/config/account_validity.py @@ -78,6 +78,11 @@ def read_config(self, config, **kwargs): ) # Read and store template content + custom_template_directories = ( + self.root.server.custom_template_directory, + account_validity_template_dir, + ) + ( self.account_validity_account_renewed_template, self.account_validity_account_previously_renewed_template, @@ -88,5 +93,5 @@ def read_config(self, config, **kwargs): "account_previously_renewed.html", invalid_token_template_filename, ], - (td for td in (account_validity_template_dir,) if td), + (td for td in custom_template_directories if td), ) diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index fc74b4a8b9..4477419196 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -258,7 +258,12 @@ def read_config(self, config, **kwargs): add_threepid_template_success_html, ], ( - td for td in (template_dir,) if td + td + for td in ( + self.root.server.custom_template_directory, + template_dir, + ) + if td ), # Filter out template_dir if not provided ) @@ -299,7 +304,14 @@ def read_config(self, config, **kwargs): self.email_notif_template_text, ) = self.read_templates( [notif_template_html, notif_template_text], - (td for td in (template_dir,) if td), + ( + td + for td in ( + self.root.server.custom_template_directory, + template_dir, + ) + if td + ), # Filter out template_dir if not provided ) self.email_notif_for_new_users = email_config.get( @@ -322,7 +334,14 @@ def read_config(self, config, **kwargs): self.account_validity_template_text, ) = self.read_templates( [expiry_template_html, expiry_template_text], - (td for td in (template_dir,) if td), + ( + td + for td in ( + self.root.server.custom_template_directory, + template_dir, + ) + if td + ), # Filter out template_dir if not provided ) subjects_config = email_config.get("subjects", {}) @@ -354,6 +373,9 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): """\ # Configuration for sending emails from Synapse. # + # Server admins can configure custom templates for email content. See + # https://matrix-org.github.io/synapse/latest/templates.html for more information. + # email: # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. # @@ -430,49 +452,6 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # #invite_client_location: https://app.element.io - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * The contents of email notifications of missed events: 'notif_mail.html' and - # 'notif_mail.txt'. - # - # * The contents of account expiry notice emails: 'notice_expiry.html' and - # 'notice_expiry.txt'. - # - # * The contents of password reset emails sent by the homeserver: - # 'password_reset.html' and 'password_reset.txt' - # - # * An HTML page that a user will see when they follow the link in the password - # reset email. The user will be asked to confirm the action before their - # password is reset: 'password_reset_confirmation.html' - # - # * HTML pages for success and failure that a user will see when they confirm - # the password reset flow using the page above: 'password_reset_success.html' - # and 'password_reset_failure.html' - # - # * The contents of address verification emails sent during registration: - # 'registration.html' and 'registration.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent during registration: - # 'registration_success.html' and 'registration_failure.html' - # - # * The contents of address verification emails sent when an address is added - # to a Matrix account: 'add_threepid.html' and 'add_threepid.txt' - # - # * HTML pages for success and failure that a user will see when they follow - # the link in an address verification email sent when an address is added - # to a Matrix account: 'add_threepid_success.html' and - # 'add_threepid_failure.html' - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" - # Subjects to use when sending emails from Synapse. # # The placeholder '%%(app)s' will be replaced with the value of the 'app_name' diff --git a/synapse/config/server.py b/synapse/config/server.py index 187b4301a0..8494795919 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -710,6 +710,18 @@ class LimitRemoteRoomsConfig: # Turn the list into a set to improve lookup speed. self.next_link_domain_whitelist = set(next_link_domain_whitelist) + templates_config = config.get("templates") or {} + if not isinstance(templates_config, dict): + raise ConfigError("The 'templates' section must be a dictionary") + + self.custom_template_directory = templates_config.get( + "custom_template_directory" + ) + if self.custom_template_directory is not None and not isinstance( + self.custom_template_directory, str + ): + raise ConfigError("'custom_template_directory' must be a string") + def has_tls_listener(self) -> bool: return any(listener.tls for listener in self.listeners) @@ -1284,6 +1296,19 @@ def generate_config_section( # all domains. # #next_link_domain_whitelist: ["matrix.org"] + + # Templates to use when generating email or HTML page contents. + # + templates: + # Directory in which Synapse will try to find template files to use to generate + # email or HTML page contents. + # If not set, or a file is not found within the template directory, a default + # template from within the Synapse package will be used. + # + # See https://matrix-org.github.io/synapse/latest/templates.html for more + # information about using custom templates. + # + #custom_template_directory: /path/to/custom/templates/ """ % locals() ) diff --git a/synapse/config/sso.py b/synapse/config/sso.py index 4b590e0535..fe1177ab81 100644 --- a/synapse/config/sso.py +++ b/synapse/config/sso.py @@ -45,6 +45,11 @@ def read_config(self, config, **kwargs): self.sso_template_dir = sso_config.get("template_dir") # Read templates from disk + custom_template_directories = ( + self.root.server.custom_template_directory, + self.sso_template_dir, + ) + ( self.sso_login_idp_picker_template, self.sso_redirect_confirm_template, @@ -63,7 +68,7 @@ def read_config(self, config, **kwargs): "sso_auth_success.html", "sso_auth_bad_user.html", ], - (td for td in (self.sso_template_dir,) if td), + (td for td in custom_template_directories if td), ) # These templates have no placeholders, so render them here @@ -94,6 +99,9 @@ def generate_config_section(self, **kwargs): # Additional settings to use with single-sign on systems such as OpenID Connect, # SAML2 and CAS. # + # Server admins can configure custom templates for pages related to SSO. See + # https://matrix-org.github.io/synapse/latest/templates.html for more information. + # sso: # A list of client URLs which are whitelisted so that the user does not # have to confirm giving access to their account to the URL. Any client @@ -125,167 +133,4 @@ def generate_config_section(self, **kwargs): # information when first signing in. Defaults to false. # #update_profile_information: true - - # Directory in which Synapse will try to find the template files below. - # If not set, or the files named below are not found within the template - # directory, default templates from within the Synapse package will be used. - # - # Synapse will look for the following templates in this directory: - # - # * HTML page to prompt the user to choose an Identity Provider during - # login: 'sso_login_idp_picker.html'. - # - # This is only used if multiple SSO Identity Providers are configured. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL that the user will be redirected to after - # login. - # - # * server_name: the homeserver's name. - # - # * providers: a list of available Identity Providers. Each element is - # an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # The rendered HTML page should contain a form which submits its results - # back as a GET request, with the following query parameters: - # - # * redirectUrl: the client redirect URI (ie, the `redirect_url` passed - # to the template) - # - # * idp: the 'idp_id' of the chosen IDP. - # - # * HTML page to prompt new users to enter a userid and confirm other - # details: 'sso_auth_account_details.html'. This is only shown if the - # SSO implementation (with any user_mapping_provider) does not return - # a localpart. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * idp: details of the SSO Identity Provider that the user logged in - # with: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * user_attributes: an object containing details about the user that - # we received from the IdP. May have the following attributes: - # - # * display_name: the user's display_name - # * emails: a list of email addresses - # - # The template should render a form which submits the following fields: - # - # * username: the localpart of the user's chosen user id - # - # * HTML page allowing the user to consent to the server's terms and - # conditions. This is only shown for new users, and only if - # `user_consent.require_at_registration` is set. - # - # When rendering, this template is given the following variables: - # - # * server_name: the homeserver's name. - # - # * user_id: the user's matrix proposed ID. - # - # * user_profile.display_name: the user's proposed display name, if any. - # - # * consent_version: the version of the terms that the user will be - # shown - # - # * terms_url: a link to the page showing the terms. - # - # The template should render a form which submits the following fields: - # - # * accepted_version: the version of the terms accepted by the user - # (ie, 'consent_version' from the input variables). - # - # * HTML page for a confirmation step before redirecting back to the client - # with the login token: 'sso_redirect_confirm.html'. - # - # When rendering, this template is given the following variables: - # - # * redirect_url: the URL the user is about to be redirected to. - # - # * display_url: the same as `redirect_url`, but with the query - # parameters stripped. The intention is to have a - # human-readable URL to show to users, not to use it as - # the final address to redirect to. - # - # * server_name: the homeserver's name. - # - # * new_user: a boolean indicating whether this is the user's first time - # logging in. - # - # * user_id: the user's matrix ID. - # - # * user_profile.avatar_url: an MXC URI for the user's avatar, if any. - # None if the user has not set an avatar. - # - # * user_profile.display_name: the user's display name. None if the user - # has not set a display name. - # - # * HTML page which notifies the user that they are authenticating to confirm - # an operation on their account during the user interactive authentication - # process: 'sso_auth_confirm.html'. - # - # When rendering, this template is given the following variables: - # * redirect_url: the URL the user is about to be redirected to. - # - # * description: the operation which the user is being asked to confirm - # - # * idp: details of the Identity Provider that we will use to confirm - # the user's identity: an object with the following attributes: - # - # * idp_id: unique identifier for the IdP - # * idp_name: user-facing name for the IdP - # * idp_icon: if specified in the IdP config, an MXC URI for an icon - # for the IdP - # * idp_brand: if specified in the IdP config, a textual identifier - # for the brand of the IdP - # - # * HTML page shown after a successful user interactive authentication session: - # 'sso_auth_success.html'. - # - # Note that this page must include the JavaScript which notifies of a successful authentication - # (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback). - # - # This template has no additional variables. - # - # * HTML page shown after a user-interactive authentication session which - # does not map correctly onto the expected user: 'sso_auth_bad_user.html'. - # - # When rendering, this template is given the following variables: - # * server_name: the homeserver's name. - # * user_id_to_verify: the MXID of the user that we are trying to - # validate. - # - # * HTML page shown during single sign-on if a deactivated user (according to Synapse's database) - # attempts to login: 'sso_account_deactivated.html'. - # - # This template has no additional variables. - # - # * HTML page to display to users if something goes wrong during the - # OpenID Connect authentication process: 'sso_error.html'. - # - # When rendering, this template is given two variables: - # * error: the technical name of the error - # * error_description: a human-readable message for the error - # - # You can see the default templates at: - # https://github.com/matrix-org/synapse/tree/master/synapse/res/templates - # - #template_dir: "res/templates" """ diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 82725853bc..2f99d31c42 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -91,6 +91,7 @@ def __init__(self, hs: "HomeServer", auth_handler): self._state = hs.get_state_handler() self._clock: Clock = hs.get_clock() self._send_email_handler = hs.get_send_email_handler() + self.custom_template_dir = hs.config.server.custom_template_directory try: app_name = self._hs.config.email_app_name @@ -679,7 +680,7 @@ def read_templates( """ return self._hs.config.read_templates( filenames, - (td for td in (custom_template_directory,) if td), + (td for td in (self.custom_template_dir, custom_template_directory) if td), ) diff --git a/synapse/rest/synapse/client/new_user_consent.py b/synapse/rest/synapse/client/new_user_consent.py index 488b97b32e..fc62a09b7f 100644 --- a/synapse/rest/synapse/client/new_user_consent.py +++ b/synapse/rest/synapse/client/new_user_consent.py @@ -46,6 +46,8 @@ def __init__(self, hs: "HomeServer"): self._consent_version = hs.config.consent.user_consent_version def template_search_dirs(): + if hs.config.server.custom_template_directory: + yield hs.config.server.custom_template_directory if hs.config.sso.sso_template_dir: yield hs.config.sso.sso_template_dir yield hs.config.sso.default_template_dir diff --git a/synapse/rest/synapse/client/pick_username.py b/synapse/rest/synapse/client/pick_username.py index ab24ec0a8e..c15b83c387 100644 --- a/synapse/rest/synapse/client/pick_username.py +++ b/synapse/rest/synapse/client/pick_username.py @@ -74,6 +74,8 @@ def __init__(self, hs: "HomeServer"): self._sso_handler = hs.get_sso_handler() def template_search_dirs(): + if hs.config.server.custom_template_directory: + yield hs.config.server.custom_template_directory if hs.config.sso.sso_template_dir: yield hs.config.sso.sso_template_dir yield hs.config.sso.default_template_dir From c8132f4a31be2717976052424abeb1ed40e947c8 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 17 Aug 2021 13:48:59 +0100 Subject: [PATCH 581/619] Build debs for bookworm (#10612) --- changelog.d/10612.misc | 1 + scripts-dev/build_debian_packages | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10612.misc diff --git a/changelog.d/10612.misc b/changelog.d/10612.misc new file mode 100644 index 0000000000..c7a9457022 --- /dev/null +++ b/changelog.d/10612.misc @@ -0,0 +1 @@ +Build Debian packages for Debian 12 (Bookworm). diff --git a/scripts-dev/build_debian_packages b/scripts-dev/build_debian_packages index 6153cb225f..e9f89e38ef 100755 --- a/scripts-dev/build_debian_packages +++ b/scripts-dev/build_debian_packages @@ -20,8 +20,9 @@ from concurrent.futures import ThreadPoolExecutor from typing import Optional, Sequence DISTS = ( - "debian:buster", + "debian:buster", # oldstable: EOL 2022-08 "debian:bullseye", + "debian:bookworm", "debian:sid", "ubuntu:bionic", # 18.04 LTS (our EOL forced by Py36 on 2021-12-23) "ubuntu:focal", # 20.04 LTS (our EOL forced by Py38 on 2024-10-14) From 84469bdac773ddb79cfc99f31bbac78d27450682 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 17 Aug 2021 14:02:50 +0100 Subject: [PATCH 582/619] Remove the unused public_room_list_stream (#10565) Co-authored-by: Patrick Cloke --- changelog.d/10565.misc | 1 + synapse/app/admin_cmd.py | 2 - synapse/app/generic_worker.py | 4 +- synapse/replication/slave/storage/room.py | 37 ---- synapse/replication/tcp/streams/__init__.py | 3 - synapse/replication/tcp/streams/_base.py | 25 --- synapse/storage/databases/main/__init__.py | 4 +- synapse/storage/databases/main/room.py | 215 ++++---------------- synapse/storage/schema/__init__.py | 7 +- 9 files changed, 48 insertions(+), 250 deletions(-) create mode 100644 changelog.d/10565.misc delete mode 100644 synapse/replication/slave/storage/room.py diff --git a/changelog.d/10565.misc b/changelog.d/10565.misc new file mode 100644 index 0000000000..06796b61ab --- /dev/null +++ b/changelog.d/10565.misc @@ -0,0 +1 @@ +Remove the unused public rooms replication stream. \ No newline at end of file diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index 3234d9ebba..7396db93c6 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -38,7 +38,6 @@ from synapse.replication.slave.storage.push_rule import SlavedPushRuleStore from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.room import RoomStore from synapse.server import HomeServer from synapse.util.logcontext import LoggingContext from synapse.util.versionstring import get_version_string @@ -58,7 +57,6 @@ class AdminCmdSlavedStore( SlavedPushRuleStore, SlavedEventStore, SlavedClientIpStore, - RoomStore, BaseSlavedStore, ): pass diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index d7b425a7ab..845e6a8220 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -64,7 +64,6 @@ from synapse.replication.slave.storage.pushers import SlavedPusherStore from synapse.replication.slave.storage.receipts import SlavedReceiptsStore from synapse.replication.slave.storage.registration import SlavedRegistrationStore -from synapse.replication.slave.storage.room import RoomStore from synapse.rest.admin import register_servlets_for_media_repo from synapse.rest.client import ( account_data, @@ -114,6 +113,7 @@ MonthlyActiveUsersWorkerStore, ) from synapse.storage.databases.main.presence import PresenceStore +from synapse.storage.databases.main.room import RoomWorkerStore from synapse.storage.databases.main.search import SearchStore from synapse.storage.databases.main.stats import StatsStore from synapse.storage.databases.main.transactions import TransactionWorkerStore @@ -237,7 +237,7 @@ class GenericWorkerSlavedStore( ClientIpWorkerStore, SlavedEventStore, SlavedKeyStore, - RoomStore, + RoomWorkerStore, DirectoryStore, SlavedApplicationServiceStore, SlavedRegistrationStore, diff --git a/synapse/replication/slave/storage/room.py b/synapse/replication/slave/storage/room.py deleted file mode 100644 index 8cc6de3f46..0000000000 --- a/synapse/replication/slave/storage/room.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2015, 2016 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. - -from synapse.replication.tcp.streams import PublicRoomsStream -from synapse.storage.database import DatabasePool -from synapse.storage.databases.main.room import RoomWorkerStore - -from ._base import BaseSlavedStore -from ._slaved_id_tracker import SlavedIdTracker - - -class RoomStore(RoomWorkerStore, BaseSlavedStore): - def __init__(self, database: DatabasePool, db_conn, hs): - super().__init__(database, db_conn, hs) - self._public_room_id_gen = SlavedIdTracker( - db_conn, "public_room_list_stream", "stream_id" - ) - - def get_current_public_room_stream_id(self): - return self._public_room_id_gen.get_current_token() - - def process_replication_rows(self, stream_name, instance_name, token, rows): - if stream_name == PublicRoomsStream.NAME: - self._public_room_id_gen.advance(instance_name, token) - - return super().process_replication_rows(stream_name, instance_name, token, rows) diff --git a/synapse/replication/tcp/streams/__init__.py b/synapse/replication/tcp/streams/__init__.py index 4c0023c68a..f41eabd85e 100644 --- a/synapse/replication/tcp/streams/__init__.py +++ b/synapse/replication/tcp/streams/__init__.py @@ -32,7 +32,6 @@ GroupServerStream, PresenceFederationStream, PresenceStream, - PublicRoomsStream, PushersStream, PushRulesStream, ReceiptsStream, @@ -57,7 +56,6 @@ PushRulesStream, PushersStream, CachesStream, - PublicRoomsStream, DeviceListsStream, ToDeviceStream, FederationStream, @@ -79,7 +77,6 @@ "PushRulesStream", "PushersStream", "CachesStream", - "PublicRoomsStream", "DeviceListsStream", "ToDeviceStream", "TagAccountDataStream", diff --git a/synapse/replication/tcp/streams/_base.py b/synapse/replication/tcp/streams/_base.py index 3716c41bea..9b905aba9d 100644 --- a/synapse/replication/tcp/streams/_base.py +++ b/synapse/replication/tcp/streams/_base.py @@ -447,31 +447,6 @@ def __init__(self, hs): ) -class PublicRoomsStream(Stream): - """The public rooms list changed""" - - PublicRoomsStreamRow = namedtuple( - "PublicRoomsStreamRow", - ( - "room_id", # str - "visibility", # str - "appservice_id", # str, optional - "network_id", # str, optional - ), - ) - - NAME = "public_rooms" - ROW_TYPE = PublicRoomsStreamRow - - def __init__(self, hs): - store = hs.get_datastore() - super().__init__( - hs.get_instance_name(), - current_token_without_instance(store.get_current_public_room_stream_id), - store.get_all_new_public_rooms, - ) - - class DeviceListsStream(Stream): """Either a user has updated their devices or a remote server needs to be told about a device update. diff --git a/synapse/storage/databases/main/__init__.py b/synapse/storage/databases/main/__init__.py index 8d9f07111d..01b918e12e 100644 --- a/synapse/storage/databases/main/__init__.py +++ b/synapse/storage/databases/main/__init__.py @@ -127,9 +127,6 @@ def __init__(self, database: DatabasePool, db_conn, hs): self._clock = hs.get_clock() self.database_engine = database.engine - self._public_room_id_gen = StreamIdGenerator( - db_conn, "public_room_list_stream", "stream_id" - ) self._device_list_id_gen = StreamIdGenerator( db_conn, "device_lists_stream", @@ -170,6 +167,7 @@ def __init__(self, database: DatabasePool, db_conn, hs): sequence_name="cache_invalidation_stream_seq", writers=[], ) + else: self._cache_id_gen = None diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index 443e5f3315..c7a1c1e8d9 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -890,55 +890,6 @@ def _quarantine_media_txn( return total_media_quarantined - async def get_all_new_public_rooms( - self, instance_name: str, last_id: int, current_id: int, limit: int - ) -> Tuple[List[Tuple[int, tuple]], int, bool]: - """Get updates for public rooms replication stream. - - Args: - instance_name: The writer we want to fetch updates from. Unused - here since there is only ever one writer. - last_id: The token to fetch updates from. Exclusive. - current_id: The token to fetch updates up to. Inclusive. - limit: The requested limit for the number of rows to return. The - function may return more or fewer rows. - - Returns: - A tuple consisting of: the updates, a token to use to fetch - subsequent updates, and whether we returned fewer rows than exists - between the requested tokens due to the limit. - - The token returned can be used in a subsequent call to this - function to get further updatees. - - The updates are a list of 2-tuples of stream ID and the row data - """ - if last_id == current_id: - return [], current_id, False - - def get_all_new_public_rooms(txn): - sql = """ - SELECT stream_id, room_id, visibility, appservice_id, network_id - FROM public_room_list_stream - WHERE stream_id > ? AND stream_id <= ? - ORDER BY stream_id ASC - LIMIT ? - """ - - txn.execute(sql, (last_id, current_id, limit)) - updates = [(row[0], row[1:]) for row in txn] - limited = False - upto_token = current_id - if len(updates) >= limit: - upto_token = updates[-1][0] - limited = True - - return updates, upto_token, limited - - return await self.db_pool.runInteraction( - "get_all_new_public_rooms", get_all_new_public_rooms - ) - async def get_rooms_for_retention_period_in_range( self, min_ms: Optional[int], max_ms: Optional[int], include_null: bool = False ) -> Dict[str, dict]: @@ -1410,34 +1361,17 @@ async def store_room( StoreError if the room could not be stored. """ try: - - def store_room_txn(txn, next_id): - self.db_pool.simple_insert_txn( - txn, - "rooms", - { - "room_id": room_id, - "creator": room_creator_user_id, - "is_public": is_public, - "room_version": room_version.identifier, - "has_auth_chain_index": True, - }, - ) - if is_public: - self.db_pool.simple_insert_txn( - txn, - table="public_room_list_stream", - values={ - "stream_id": next_id, - "room_id": room_id, - "visibility": is_public, - }, - ) - - async with self._public_room_id_gen.get_next() as next_id: - await self.db_pool.runInteraction( - "store_room_txn", store_room_txn, next_id - ) + await self.db_pool.simple_insert( + "rooms", + { + "room_id": room_id, + "creator": room_creator_user_id, + "is_public": is_public, + "room_version": room_version.identifier, + "has_auth_chain_index": True, + }, + desc="store_room", + ) except Exception as e: logger.error("store_room with room_id=%s failed: %s", room_id, e) raise StoreError(500, "Problem creating room.") @@ -1470,49 +1404,14 @@ async def maybe_store_room_on_outlier_membership( lock=False, ) - async def set_room_is_public(self, room_id, is_public): - def set_room_is_public_txn(txn, next_id): - self.db_pool.simple_update_one_txn( - txn, - table="rooms", - keyvalues={"room_id": room_id}, - updatevalues={"is_public": is_public}, - ) - - entries = self.db_pool.simple_select_list_txn( - txn, - table="public_room_list_stream", - keyvalues={ - "room_id": room_id, - "appservice_id": None, - "network_id": None, - }, - retcols=("stream_id", "visibility"), - ) - - entries.sort(key=lambda r: r["stream_id"]) - - add_to_stream = True - if entries: - add_to_stream = bool(entries[-1]["visibility"]) != is_public - - if add_to_stream: - self.db_pool.simple_insert_txn( - txn, - table="public_room_list_stream", - values={ - "stream_id": next_id, - "room_id": room_id, - "visibility": is_public, - "appservice_id": None, - "network_id": None, - }, - ) + async def set_room_is_public(self, room_id: str, is_public: bool) -> None: + await self.db_pool.simple_update_one( + table="rooms", + keyvalues={"room_id": room_id}, + updatevalues={"is_public": is_public}, + desc="set_room_is_public", + ) - async with self._public_room_id_gen.get_next() as next_id: - await self.db_pool.runInteraction( - "set_room_is_public", set_room_is_public_txn, next_id - ) self.hs.get_notifier().on_new_replication_data() async def set_room_is_public_appservice( @@ -1533,68 +1432,33 @@ async def set_room_is_public_appservice( list. """ - def set_room_is_public_appservice_txn(txn, next_id): - if is_public: - try: - self.db_pool.simple_insert_txn( - txn, - table="appservice_room_list", - values={ - "appservice_id": appservice_id, - "network_id": network_id, - "room_id": room_id, - }, - ) - except self.database_engine.module.IntegrityError: - # We've already inserted, nothing to do. - return - else: - self.db_pool.simple_delete_txn( - txn, - table="appservice_room_list", - keyvalues={ - "appservice_id": appservice_id, - "network_id": network_id, - "room_id": room_id, - }, - ) - - entries = self.db_pool.simple_select_list_txn( - txn, - table="public_room_list_stream", + if is_public: + await self.db_pool.simple_upsert( + table="appservice_room_list", keyvalues={ + "appservice_id": appservice_id, + "network_id": network_id, "room_id": room_id, + }, + values={}, + insertion_values={ "appservice_id": appservice_id, "network_id": network_id, + "room_id": room_id, }, - retcols=("stream_id", "visibility"), + desc="set_room_is_public_appservice_true", ) - - entries.sort(key=lambda r: r["stream_id"]) - - add_to_stream = True - if entries: - add_to_stream = bool(entries[-1]["visibility"]) != is_public - - if add_to_stream: - self.db_pool.simple_insert_txn( - txn, - table="public_room_list_stream", - values={ - "stream_id": next_id, - "room_id": room_id, - "visibility": is_public, - "appservice_id": appservice_id, - "network_id": network_id, - }, - ) - - async with self._public_room_id_gen.get_next() as next_id: - await self.db_pool.runInteraction( - "set_room_is_public_appservice", - set_room_is_public_appservice_txn, - next_id, + else: + await self.db_pool.simple_delete( + table="appservice_room_list", + keyvalues={ + "appservice_id": appservice_id, + "network_id": network_id, + "room_id": room_id, + }, + desc="set_room_is_public_appservice_false", ) + self.hs.get_notifier().on_new_replication_data() async def add_event_report( @@ -1787,9 +1651,6 @@ def _get_event_reports_paginate_txn(txn): "get_event_reports_paginate", _get_event_reports_paginate_txn ) - def get_current_public_room_stream_id(self): - return self._public_room_id_gen.get_current_token() - async def block_room(self, room_id: str, user_id: str) -> None: """Marks the room as blocked. Can be called multiple times. diff --git a/synapse/storage/schema/__init__.py b/synapse/storage/schema/__init__.py index 7e0687e197..a5bc0ee8a5 100644 --- a/synapse/storage/schema/__init__.py +++ b/synapse/storage/schema/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -SCHEMA_VERSION = 62 +SCHEMA_VERSION = 63 """Represents the expectations made by the codebase about the database schema This should be incremented whenever the codebase changes its requirements on the @@ -25,6 +25,11 @@ Changes in SCHEMA_VERSION = 61: - The `user_stats_historical` and `room_stats_historical` tables are not written and are not read (previously, they were written but not read). + +Changes in SCHEMA_VERSION = 63: + - The `public_room_list_stream` table is not written nor read to + (previously, it was written and read to, but not for any significant purpose). + https://github.com/matrix-org/synapse/pull/10565 """ From 703e3a9e853b7c2212045ec52eb6b2c6e370c6f9 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Tue, 17 Aug 2021 14:33:16 +0100 Subject: [PATCH 583/619] Allow /createRoom to be run on workers (#10564) Fixes https://github.com/matrix-org/synapse/issues/7867 --- changelog.d/10564.feature | 1 + docs/workers.md | 1 + synapse/rest/client/room.py | 2 +- synapse/storage/databases/main/room.py | 68 +++++++++++++------------- 4 files changed, 37 insertions(+), 35 deletions(-) create mode 100644 changelog.d/10564.feature diff --git a/changelog.d/10564.feature b/changelog.d/10564.feature new file mode 100644 index 0000000000..4de32240b2 --- /dev/null +++ b/changelog.d/10564.feature @@ -0,0 +1 @@ +Add support for routing `/createRoom` to workers. diff --git a/docs/workers.md b/docs/workers.md index d8672324c3..1657dfc759 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -214,6 +214,7 @@ expressions: ^/_matrix/federation/v1/send/ # Client API requests + ^/_matrix/client/(api/v1|r0|unstable)/createRoom$ ^/_matrix/client/(api/v1|r0|unstable)/publicRooms$ ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/joined_members$ ^/_matrix/client/(api/v1|r0|unstable)/rooms/.*/context/.*$ diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py index ed238b2141..c5c54564be 100644 --- a/synapse/rest/client/room.py +++ b/synapse/rest/client/room.py @@ -1141,10 +1141,10 @@ def register_servlets(hs: "HomeServer", http_server, is_worker=False): JoinedRoomsRestServlet(hs).register(http_server) RoomAliasListServlet(hs).register(http_server) SearchRestServlet(hs).register(http_server) + RoomCreateRestServlet(hs).register(http_server) # Some servlets only get registered for the main process. if not is_worker: - RoomCreateRestServlet(hs).register(http_server) RoomForgetRestServlet(hs).register(http_server) diff --git a/synapse/storage/databases/main/room.py b/synapse/storage/databases/main/room.py index c7a1c1e8d9..f98b892598 100644 --- a/synapse/storage/databases/main/room.py +++ b/synapse/storage/databases/main/room.py @@ -73,6 +73,40 @@ def __init__(self, database: DatabasePool, db_conn, hs): self.config = hs.config + async def store_room( + self, + room_id: str, + room_creator_user_id: str, + is_public: bool, + room_version: RoomVersion, + ): + """Stores a room. + + Args: + room_id: The desired room ID, can be None. + room_creator_user_id: The user ID of the room creator. + is_public: True to indicate that this room should appear in + public room lists. + room_version: The version of the room + Raises: + StoreError if the room could not be stored. + """ + try: + await self.db_pool.simple_insert( + "rooms", + { + "room_id": room_id, + "creator": room_creator_user_id, + "is_public": is_public, + "room_version": room_version.identifier, + "has_auth_chain_index": True, + }, + desc="store_room", + ) + except Exception as e: + logger.error("store_room with room_id=%s failed: %s", room_id, e) + raise StoreError(500, "Problem creating room.") + async def get_room(self, room_id: str) -> dict: """Retrieve a room. @@ -1342,40 +1376,6 @@ async def upsert_room_on_join(self, room_id: str, room_version: RoomVersion): lock=False, ) - async def store_room( - self, - room_id: str, - room_creator_user_id: str, - is_public: bool, - room_version: RoomVersion, - ): - """Stores a room. - - Args: - room_id: The desired room ID, can be None. - room_creator_user_id: The user ID of the room creator. - is_public: True to indicate that this room should appear in - public room lists. - room_version: The version of the room - Raises: - StoreError if the room could not be stored. - """ - try: - await self.db_pool.simple_insert( - "rooms", - { - "room_id": room_id, - "creator": room_creator_user_id, - "is_public": is_public, - "room_version": room_version.identifier, - "has_auth_chain_index": True, - }, - desc="store_room", - ) - except Exception as e: - logger.error("store_room with room_id=%s failed: %s", room_id, e) - raise StoreError(500, "Problem creating room.") - async def maybe_store_room_on_outlier_membership( self, room_id: str, room_version: RoomVersion ): From 5581dd7bf7b1d1fb10d4852587d2712c8391c07c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 18 Aug 2021 11:21:11 +0100 Subject: [PATCH 584/619] Allow modules to run looping call on all instances (#10638) By default the calls only ran on the worker configured to run background tasks. --- changelog.d/10638.feature | 1 + synapse/module_api/__init__.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10638.feature diff --git a/changelog.d/10638.feature b/changelog.d/10638.feature new file mode 100644 index 0000000000..c1de91f334 --- /dev/null +++ b/changelog.d/10638.feature @@ -0,0 +1 @@ +Add option to allow modules to run periodic tasks on all instances, rather than just the one configured to run background tasks. diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 2f99d31c42..2d2ed229e2 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -604,10 +604,15 @@ def looping_background_call( msec: float, *args, desc: Optional[str] = None, + run_on_all_instances: bool = False, **kwargs, ): """Wraps a function as a background process and calls it repeatedly. + NOTE: Will only run on the instance that is configured to run + background processes (which is the main process by default), unless + `run_on_all_workers` is set. + Waits `msec` initially before calling `f` for the first time. Args: @@ -618,12 +623,14 @@ def looping_background_call( msec: How long to wait between calls in milliseconds. *args: Positional arguments to pass to function. desc: The background task's description. Default to the function's name. + run_on_all_instances: Whether to run this on all instances, rather + than just the instance configured to run background tasks. **kwargs: Key arguments to pass to function. """ if desc is None: desc = f.__name__ - if self._hs.config.run_background_tasks: + if self._hs.config.run_background_tasks or run_on_all_instances: self._clock.looping_call( run_as_background_process, msec, From eea28735958804cd2b0d54bd19e1e25e8570209d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Wed, 18 Aug 2021 12:38:37 +0100 Subject: [PATCH 585/619] fix broken link to upgrade notes (#10631) --- UPGRADE.rst | 2 +- changelog.d/10631.misc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/10631.misc diff --git a/UPGRADE.rst b/UPGRADE.rst index 17ecd935fd..6c7f9cb18e 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -1,7 +1,7 @@ Upgrading Synapse ================= -This document has moved to the `Synapse documentation website `_. +This document has moved to the `Synapse documentation website `_. Please update your links. The markdown source is available in `docs/upgrade.md `_. diff --git a/changelog.d/10631.misc b/changelog.d/10631.misc new file mode 100644 index 0000000000..d2a4624d53 --- /dev/null +++ b/changelog.d/10631.misc @@ -0,0 +1 @@ +Fix a broken link to the upgrade notes. From 3692f7fd33ec2a28991ab325a46df5e7eba1f056 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 18 Aug 2021 13:25:12 +0100 Subject: [PATCH 586/619] Mount /_synapse/admin/v1/users/{userId}/media admin API on media workers only (#10628) Co-authored-by: Patrick Cloke --- changelog.d/10628.feature | 1 + docs/upgrade.md | 6 ++ docs/workers.md | 4 +- synapse/rest/admin/__init__.py | 2 - synapse/rest/admin/media.py | 165 ++++++++++++++++++++++++++++++++- synapse/rest/admin/users.py | 160 -------------------------------- 6 files changed, 173 insertions(+), 165 deletions(-) create mode 100644 changelog.d/10628.feature diff --git a/changelog.d/10628.feature b/changelog.d/10628.feature new file mode 100644 index 0000000000..708cb9b599 --- /dev/null +++ b/changelog.d/10628.feature @@ -0,0 +1 @@ +Admin API to delete several media for a specific user. Contributed by @dklimpel. \ No newline at end of file diff --git a/docs/upgrade.md b/docs/upgrade.md index 1c459d8e2b..99e32034c8 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -123,6 +123,12 @@ for more information and examples. We plan to remove support for these settings in October 2021. +## `/_synapse/admin/v1/users/{userId}/media` must be handled by media workers + +The [media repository worker documentation](https://matrix-org.github.io/synapse/latest/workers.html#synapseappmedia_repository) +has been updated to reflect that calls to `/_synapse/admin/v1/users/{userId}/media` +must now be handled by media repository workers. This is due to the new `DELETE` method +of this endpoint modifying the media store. # Upgrading to v1.39.0 diff --git a/docs/workers.md b/docs/workers.md index 1657dfc759..2e63f03452 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -426,10 +426,12 @@ Handles the media repository. It can handle all endpoints starting with: ^/_synapse/admin/v1/user/.*/media.*$ ^/_synapse/admin/v1/media/.*$ ^/_synapse/admin/v1/quarantine_media/.*$ + ^/_synapse/admin/v1/users/.*/media$ You should also set `enable_media_repo: False` in the shared configuration file to stop the main synapse running background jobs related to managing the -media repository. +media repository. Note that doing so will prevent the main process from being +able to handle the above endpoints. In the `media_repository` worker configuration file, configure the http listener to expose the `media` resource. For example: diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 8a91068092..d5862a4da4 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -61,7 +61,6 @@ SearchUsersRestServlet, ShadowBanRestServlet, UserAdminServlet, - UserMediaRestServlet, UserMembershipRestServlet, UserRegisterServlet, UserRestServletV2, @@ -225,7 +224,6 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: SendServerNoticeServlet(hs).register(http_server) VersionServlet(hs).register(http_server) UserAdminServlet(hs).register(http_server) - UserMediaRestServlet(hs).register(http_server) UserMembershipRestServlet(hs).register(http_server) UserTokenRestServlet(hs).register(http_server) UserRestServletV2(hs).register(http_server) diff --git a/synapse/rest/admin/media.py b/synapse/rest/admin/media.py index 5f0555039d..8ce443049e 100644 --- a/synapse/rest/admin/media.py +++ b/synapse/rest/admin/media.py @@ -18,14 +18,15 @@ from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError from synapse.http.server import HttpServer -from synapse.http.servlet import RestServlet, parse_boolean, parse_integer +from synapse.http.servlet import RestServlet, parse_boolean, parse_integer, parse_string from synapse.http.site import SynapseRequest from synapse.rest.admin._base import ( admin_patterns, assert_requester_is_admin, assert_user_is_admin, ) -from synapse.types import JsonDict +from synapse.storage.databases.main.media_repository import MediaSortOrder +from synapse.types import JsonDict, UserID if TYPE_CHECKING: from synapse.server import HomeServer @@ -314,6 +315,165 @@ async def on_POST( return 200, {"deleted_media": deleted_media, "total": total} +class UserMediaRestServlet(RestServlet): + """ + Gets information about all uploaded local media for a specific `user_id`. + With DELETE request you can delete all this media. + + Example: + http://localhost:8008/_synapse/admin/v1/users/@user:server/media + + Args: + The parameters `from` and `limit` are required for pagination. + By default, a `limit` of 100 is used. + Returns: + A list of media and an integer representing the total number of + media that exist given for this user + """ + + PATTERNS = admin_patterns("/users/(?P[^/]+)/media$") + + def __init__(self, hs: "HomeServer"): + self.is_mine = hs.is_mine + self.auth = hs.get_auth() + self.store = hs.get_datastore() + self.media_repository = hs.get_media_repository() + + async def on_GET( + self, request: SynapseRequest, user_id: str + ) -> Tuple[int, JsonDict]: + # This will always be set by the time Twisted calls us. + assert request.args is not None + + await assert_requester_is_admin(self.auth, request) + + if not self.is_mine(UserID.from_string(user_id)): + raise SynapseError(400, "Can only look up local users") + + user = await self.store.get_user_by_id(user_id) + if user is None: + raise NotFoundError("Unknown user") + + start = parse_integer(request, "from", default=0) + limit = parse_integer(request, "limit", default=100) + + if start < 0: + raise SynapseError( + 400, + "Query parameter from must be a string representing a positive integer.", + errcode=Codes.INVALID_PARAM, + ) + + if limit < 0: + raise SynapseError( + 400, + "Query parameter limit must be a string representing a positive integer.", + errcode=Codes.INVALID_PARAM, + ) + + # If neither `order_by` nor `dir` is set, set the default order + # to newest media is on top for backward compatibility. + if b"order_by" not in request.args and b"dir" not in request.args: + order_by = MediaSortOrder.CREATED_TS.value + direction = "b" + else: + order_by = parse_string( + request, + "order_by", + default=MediaSortOrder.CREATED_TS.value, + allowed_values=( + MediaSortOrder.MEDIA_ID.value, + MediaSortOrder.UPLOAD_NAME.value, + MediaSortOrder.CREATED_TS.value, + MediaSortOrder.LAST_ACCESS_TS.value, + MediaSortOrder.MEDIA_LENGTH.value, + MediaSortOrder.MEDIA_TYPE.value, + MediaSortOrder.QUARANTINED_BY.value, + MediaSortOrder.SAFE_FROM_QUARANTINE.value, + ), + ) + direction = parse_string( + request, "dir", default="f", allowed_values=("f", "b") + ) + + media, total = await self.store.get_local_media_by_user_paginate( + start, limit, user_id, order_by, direction + ) + + ret = {"media": media, "total": total} + if (start + limit) < total: + ret["next_token"] = start + len(media) + + return 200, ret + + async def on_DELETE( + self, request: SynapseRequest, user_id: str + ) -> Tuple[int, JsonDict]: + # This will always be set by the time Twisted calls us. + assert request.args is not None + + await assert_requester_is_admin(self.auth, request) + + if not self.is_mine(UserID.from_string(user_id)): + raise SynapseError(400, "Can only look up local users") + + user = await self.store.get_user_by_id(user_id) + if user is None: + raise NotFoundError("Unknown user") + + start = parse_integer(request, "from", default=0) + limit = parse_integer(request, "limit", default=100) + + if start < 0: + raise SynapseError( + 400, + "Query parameter from must be a string representing a positive integer.", + errcode=Codes.INVALID_PARAM, + ) + + if limit < 0: + raise SynapseError( + 400, + "Query parameter limit must be a string representing a positive integer.", + errcode=Codes.INVALID_PARAM, + ) + + # If neither `order_by` nor `dir` is set, set the default order + # to newest media is on top for backward compatibility. + if b"order_by" not in request.args and b"dir" not in request.args: + order_by = MediaSortOrder.CREATED_TS.value + direction = "b" + else: + order_by = parse_string( + request, + "order_by", + default=MediaSortOrder.CREATED_TS.value, + allowed_values=( + MediaSortOrder.MEDIA_ID.value, + MediaSortOrder.UPLOAD_NAME.value, + MediaSortOrder.CREATED_TS.value, + MediaSortOrder.LAST_ACCESS_TS.value, + MediaSortOrder.MEDIA_LENGTH.value, + MediaSortOrder.MEDIA_TYPE.value, + MediaSortOrder.QUARANTINED_BY.value, + MediaSortOrder.SAFE_FROM_QUARANTINE.value, + ), + ) + direction = parse_string( + request, "dir", default="f", allowed_values=("f", "b") + ) + + media, _ = await self.store.get_local_media_by_user_paginate( + start, limit, user_id, order_by, direction + ) + + deleted_media, total = await self.media_repository.delete_local_media_ids( + ([row["media_id"] for row in media]) + ) + + return 200, {"deleted_media": deleted_media, "total": total} + + def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer) -> None: """ Media repo specific APIs. @@ -328,3 +488,4 @@ def register_servlets_for_media_repo(hs: "HomeServer", http_server: HttpServer) ListMediaInRoom(hs).register(http_server) DeleteMediaByID(hs).register(http_server) DeleteMediaByDateSize(hs).register(http_server) + UserMediaRestServlet(hs).register(http_server) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index 93193b0864..3c8a0c6883 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -35,7 +35,6 @@ assert_user_is_admin, ) from synapse.rest.client._base import client_patterns -from synapse.storage.databases.main.media_repository import MediaSortOrder from synapse.storage.databases.main.stats import UserSortOrder from synapse.types import JsonDict, UserID @@ -851,165 +850,6 @@ async def on_GET( return 200, {"pushers": filtered_pushers, "total": len(filtered_pushers)} -class UserMediaRestServlet(RestServlet): - """ - Gets information about all uploaded local media for a specific `user_id`. - With DELETE request you can delete all this media. - - Example: - http://localhost:8008/_synapse/admin/v1/users/@user:server/media - - Args: - The parameters `from` and `limit` are required for pagination. - By default, a `limit` of 100 is used. - Returns: - A list of media and an integer representing the total number of - media that exist given for this user - """ - - PATTERNS = admin_patterns("/users/(?P[^/]+)/media$") - - def __init__(self, hs: "HomeServer"): - self.is_mine = hs.is_mine - self.auth = hs.get_auth() - self.store = hs.get_datastore() - self.media_repository = hs.get_media_repository() - - async def on_GET( - self, request: SynapseRequest, user_id: str - ) -> Tuple[int, JsonDict]: - # This will always be set by the time Twisted calls us. - assert request.args is not None - - await assert_requester_is_admin(self.auth, request) - - if not self.is_mine(UserID.from_string(user_id)): - raise SynapseError(400, "Can only look up local users") - - user = await self.store.get_user_by_id(user_id) - if user is None: - raise NotFoundError("Unknown user") - - start = parse_integer(request, "from", default=0) - limit = parse_integer(request, "limit", default=100) - - if start < 0: - raise SynapseError( - 400, - "Query parameter from must be a string representing a positive integer.", - errcode=Codes.INVALID_PARAM, - ) - - if limit < 0: - raise SynapseError( - 400, - "Query parameter limit must be a string representing a positive integer.", - errcode=Codes.INVALID_PARAM, - ) - - # If neither `order_by` nor `dir` is set, set the default order - # to newest media is on top for backward compatibility. - if b"order_by" not in request.args and b"dir" not in request.args: - order_by = MediaSortOrder.CREATED_TS.value - direction = "b" - else: - order_by = parse_string( - request, - "order_by", - default=MediaSortOrder.CREATED_TS.value, - allowed_values=( - MediaSortOrder.MEDIA_ID.value, - MediaSortOrder.UPLOAD_NAME.value, - MediaSortOrder.CREATED_TS.value, - MediaSortOrder.LAST_ACCESS_TS.value, - MediaSortOrder.MEDIA_LENGTH.value, - MediaSortOrder.MEDIA_TYPE.value, - MediaSortOrder.QUARANTINED_BY.value, - MediaSortOrder.SAFE_FROM_QUARANTINE.value, - ), - ) - direction = parse_string( - request, "dir", default="f", allowed_values=("f", "b") - ) - - media, total = await self.store.get_local_media_by_user_paginate( - start, limit, user_id, order_by, direction - ) - - ret = {"media": media, "total": total} - if (start + limit) < total: - ret["next_token"] = start + len(media) - - return 200, ret - - async def on_DELETE( - self, request: SynapseRequest, user_id: str - ) -> Tuple[int, JsonDict]: - # This will always be set by the time Twisted calls us. - assert request.args is not None - - await assert_requester_is_admin(self.auth, request) - - if not self.is_mine(UserID.from_string(user_id)): - raise SynapseError(400, "Can only look up local users") - - user = await self.store.get_user_by_id(user_id) - if user is None: - raise NotFoundError("Unknown user") - - start = parse_integer(request, "from", default=0) - limit = parse_integer(request, "limit", default=100) - - if start < 0: - raise SynapseError( - 400, - "Query parameter from must be a string representing a positive integer.", - errcode=Codes.INVALID_PARAM, - ) - - if limit < 0: - raise SynapseError( - 400, - "Query parameter limit must be a string representing a positive integer.", - errcode=Codes.INVALID_PARAM, - ) - - # If neither `order_by` nor `dir` is set, set the default order - # to newest media is on top for backward compatibility. - if b"order_by" not in request.args and b"dir" not in request.args: - order_by = MediaSortOrder.CREATED_TS.value - direction = "b" - else: - order_by = parse_string( - request, - "order_by", - default=MediaSortOrder.CREATED_TS.value, - allowed_values=( - MediaSortOrder.MEDIA_ID.value, - MediaSortOrder.UPLOAD_NAME.value, - MediaSortOrder.CREATED_TS.value, - MediaSortOrder.LAST_ACCESS_TS.value, - MediaSortOrder.MEDIA_LENGTH.value, - MediaSortOrder.MEDIA_TYPE.value, - MediaSortOrder.QUARANTINED_BY.value, - MediaSortOrder.SAFE_FROM_QUARANTINE.value, - ), - ) - direction = parse_string( - request, "dir", default="f", allowed_values=("f", "b") - ) - - media, _ = await self.store.get_local_media_by_user_paginate( - start, limit, user_id, order_by, direction - ) - - deleted_media, total = await self.media_repository.delete_local_media_ids( - ([row["media_id"] for row in media]) - ) - - return 200, {"deleted_media": deleted_media, "total": total} - - class UserTokenRestServlet(RestServlet): """An admin API for logging in as a user. From 49cb7eae97bbe8916a5a3ec4f9d030f6304cd76c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 18 Aug 2021 15:52:11 +0100 Subject: [PATCH 587/619] 1.41.0rc1 --- CHANGES.md | 79 +++++++++++++++++++++++++++++++++++++++ changelog.d/10119.misc | 1 - changelog.d/10129.bugfix | 1 - changelog.d/10394.feature | 1 - changelog.d/10435.feature | 1 - changelog.d/10443.doc | 1 - changelog.d/10475.feature | 1 - changelog.d/10498.feature | 1 - changelog.d/10504.misc | 1 - changelog.d/10507.misc | 1 - changelog.d/10513.feature | 1 - changelog.d/10518.feature | 1 - changelog.d/10527.misc | 1 - changelog.d/10529.misc | 1 - changelog.d/10530.misc | 1 - changelog.d/10532.bugfix | 1 - changelog.d/10537.misc | 1 - changelog.d/10538.feature | 1 - changelog.d/10539.misc | 1 - changelog.d/10541.bugfix | 1 - changelog.d/10542.misc | 1 - changelog.d/10546.feature | 1 - changelog.d/10549.feature | 1 - changelog.d/10550.bugfix | 1 - changelog.d/10551.doc | 1 - changelog.d/10552.misc | 1 - changelog.d/10558.feature | 1 - changelog.d/10560.feature | 1 - changelog.d/10563.misc | 1 - changelog.d/10564.feature | 1 - changelog.d/10565.misc | 1 - changelog.d/10569.feature | 1 - changelog.d/10570.feature | 1 - changelog.d/10572.misc | 1 - changelog.d/10573.misc | 1 - changelog.d/10574.feature | 1 - changelog.d/10575.feature | 1 - changelog.d/10576.misc | 1 - changelog.d/10578.feature | 1 - changelog.d/10579.feature | 1 - changelog.d/10580.bugfix | 1 - changelog.d/10583.feature | 1 - changelog.d/10587.misc | 1 - changelog.d/10588.removal | 1 - changelog.d/10590.misc | 1 - changelog.d/10591.misc | 1 - changelog.d/10592.bugfix | 1 - changelog.d/10596.removal | 1 - changelog.d/10598.feature | 1 - changelog.d/10599.doc | 1 - changelog.d/10600.misc | 1 - changelog.d/10602.feature | 1 - changelog.d/10606.bugfix | 1 - changelog.d/10611.bugfix | 1 - changelog.d/10612.misc | 1 - changelog.d/10620.misc | 1 - changelog.d/10623.bugfix | 1 - changelog.d/10628.feature | 1 - changelog.d/10631.misc | 1 - changelog.d/10638.feature | 1 - changelog.d/9581.feature | 1 - debian/changelog | 6 +++ synapse/__init__.py | 2 +- 63 files changed, 86 insertions(+), 61 deletions(-) delete mode 100644 changelog.d/10119.misc delete mode 100644 changelog.d/10129.bugfix delete mode 100644 changelog.d/10394.feature delete mode 100644 changelog.d/10435.feature delete mode 100644 changelog.d/10443.doc delete mode 100644 changelog.d/10475.feature delete mode 100644 changelog.d/10498.feature delete mode 100644 changelog.d/10504.misc delete mode 100644 changelog.d/10507.misc delete mode 100644 changelog.d/10513.feature delete mode 100644 changelog.d/10518.feature delete mode 100644 changelog.d/10527.misc delete mode 100644 changelog.d/10529.misc delete mode 100644 changelog.d/10530.misc delete mode 100644 changelog.d/10532.bugfix delete mode 100644 changelog.d/10537.misc delete mode 100644 changelog.d/10538.feature delete mode 100644 changelog.d/10539.misc delete mode 100644 changelog.d/10541.bugfix delete mode 100644 changelog.d/10542.misc delete mode 100644 changelog.d/10546.feature delete mode 100644 changelog.d/10549.feature delete mode 100644 changelog.d/10550.bugfix delete mode 100644 changelog.d/10551.doc delete mode 100644 changelog.d/10552.misc delete mode 100644 changelog.d/10558.feature delete mode 100644 changelog.d/10560.feature delete mode 100644 changelog.d/10563.misc delete mode 100644 changelog.d/10564.feature delete mode 100644 changelog.d/10565.misc delete mode 100644 changelog.d/10569.feature delete mode 100644 changelog.d/10570.feature delete mode 100644 changelog.d/10572.misc delete mode 100644 changelog.d/10573.misc delete mode 100644 changelog.d/10574.feature delete mode 100644 changelog.d/10575.feature delete mode 100644 changelog.d/10576.misc delete mode 100644 changelog.d/10578.feature delete mode 100644 changelog.d/10579.feature delete mode 100644 changelog.d/10580.bugfix delete mode 100644 changelog.d/10583.feature delete mode 100644 changelog.d/10587.misc delete mode 100644 changelog.d/10588.removal delete mode 100644 changelog.d/10590.misc delete mode 100644 changelog.d/10591.misc delete mode 100644 changelog.d/10592.bugfix delete mode 100644 changelog.d/10596.removal delete mode 100644 changelog.d/10598.feature delete mode 100644 changelog.d/10599.doc delete mode 100644 changelog.d/10600.misc delete mode 100644 changelog.d/10602.feature delete mode 100644 changelog.d/10606.bugfix delete mode 100644 changelog.d/10611.bugfix delete mode 100644 changelog.d/10612.misc delete mode 100644 changelog.d/10620.misc delete mode 100644 changelog.d/10623.bugfix delete mode 100644 changelog.d/10628.feature delete mode 100644 changelog.d/10631.misc delete mode 100644 changelog.d/10638.feature delete mode 100644 changelog.d/9581.feature diff --git a/CHANGES.md b/CHANGES.md index 0e5e052951..b96ac701d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,82 @@ +Synapse 1.41.0rc1 (2021-08-18) +============================== + +Features +-------- + +- Add `get_userinfo_by_id` method to ModuleApi. ([\#9581](https://github.com/matrix-org/synapse/issues/9581)) +- Initial local support for [MSC3266](https://github.com/matrix-org/synapse/pull/10394), Room Summary over the unstable `/rooms/{roomIdOrAlias}/summary` API. ([\#10394](https://github.com/matrix-org/synapse/issues/10394)) +- Experimental support for [MSC3288](https://github.com/matrix-org/matrix-doc/pull/3288), sending `room_type` to the identity server for 3pid invites over the `/store-invite` API. ([\#10435](https://github.com/matrix-org/synapse/issues/10435)) +- Add support for sending federation requests through a proxy. Contributed by @Bubu and @dklimpel. ([\#10475](https://github.com/matrix-org/synapse/issues/10475)) +- Add support for "marker" events which makes historical events discoverable for servers that already have all of the scrollback history (part of MSC2716). ([\#10498](https://github.com/matrix-org/synapse/issues/10498)) +- Add a configuration setting for the time a `/sync` response is cached for. ([\#10513](https://github.com/matrix-org/synapse/issues/10513)) +- The default logging handler for new installations is now `PeriodicallyFlushingMemoryHandler`, a buffered logging handler which periodically flushes itself. ([\#10518](https://github.com/matrix-org/synapse/issues/10518)) +- Add support for new redaction rules for historical events specified in [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716). ([\#10538](https://github.com/matrix-org/synapse/issues/10538)) +- Add a setting to disable TLS when sending email. ([\#10546](https://github.com/matrix-org/synapse/issues/10546)) +- Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). ([\#10549](https://github.com/matrix-org/synapse/issues/10549), [\#10560](https://github.com/matrix-org/synapse/issues/10560), [\#10569](https://github.com/matrix-org/synapse/issues/10569), [\#10574](https://github.com/matrix-org/synapse/issues/10574), [\#10575](https://github.com/matrix-org/synapse/issues/10575), [\#10579](https://github.com/matrix-org/synapse/issues/10579), [\#10583](https://github.com/matrix-org/synapse/issues/10583)) +- Admin API to delete several media for a specific user. Contributed by @dklimpel. ([\#10558](https://github.com/matrix-org/synapse/issues/10558), [\#10628](https://github.com/matrix-org/synapse/issues/10628)) +- Add support for routing `/createRoom` to workers. ([\#10564](https://github.com/matrix-org/synapse/issues/10564)) +- Update the Synapse Grafana dashboard. ([\#10570](https://github.com/matrix-org/synapse/issues/10570)) +- Add an admin API (`GET /_synapse/admin/username_available`) to check if a username is available (regardless of registration settings). ([\#10578](https://github.com/matrix-org/synapse/issues/10578)) +- Allow editing a user's `external_ids` via the "Edit User" admin API. Contributed by @dklimpel. ([\#10598](https://github.com/matrix-org/synapse/issues/10598)) +- The Synapse manhole no longer needs coroutines to be wrapped in `defer.ensureDeferred`. ([\#10602](https://github.com/matrix-org/synapse/issues/10602)) +- Add option to allow modules to run periodic tasks on all instances, rather than just the one configured to run background tasks. ([\#10638](https://github.com/matrix-org/synapse/issues/10638)) + + +Bugfixes +-------- + +- Add some clarification to the sample config file. Contributed by @Kentokamoto. ([\#10129](https://github.com/matrix-org/synapse/issues/10129)) +- Fix a long-standing bug where protocols which are not implemented by any appservices were incorrectly returned via `GET /_matrix/client/r0/thirdparty/protocols`. ([\#10532](https://github.com/matrix-org/synapse/issues/10532)) +- Fix exceptions in logs when failing to get remote room list. ([\#10541](https://github.com/matrix-org/synapse/issues/10541)) +- Fix longstanding bug which caused the user "status" to be reset when the user went offline. Contributed by @dklimpel. ([\#10550](https://github.com/matrix-org/synapse/issues/10550)) +- Allow public rooms to be previewed in the spaces summary APIs from [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). ([\#10580](https://github.com/matrix-org/synapse/issues/10580)) +- Fix a bug introduced in v1.37.1 where an error could occur in the asyncronous processing of PDUs when the queue was empty. ([\#10592](https://github.com/matrix-org/synapse/issues/10592)) +- Fix errors on /sync when read receipt data is a string. Only affects homeservers with the experimental flag for [MSC2285](https://github.com/matrix-org/matrix-doc/pull/2285) enabled. Contributed by @SimonBrandner. ([\#10606](https://github.com/matrix-org/synapse/issues/10606)) +- Additional validation for the spaces summary API to avoid errors like `ValueError: Stop argument for islice() must be None or an integer`. The missing validation has existed since v1.31.0. ([\#10611](https://github.com/matrix-org/synapse/issues/10611)) +- Revert behaviour introduced in v1.38.0 that strips `org.matrix.msc2732.device_unused_fallback_key_types` from `/sync` when its value is empty. This field should instead always be present according to [MSC2732](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2732-olm-fallback-keys.md). ([\#10623](https://github.com/matrix-org/synapse/issues/10623)) + + +Improved Documentation +---------------------- + +- Add documentation for configuration a forward proxy. ([\#10443](https://github.com/matrix-org/synapse/issues/10443)) +- Updated the reverse proxy documentation to highlight the homserver configuration that is needed to make Synapse aware that is is intentionally reverse proxied. ([\#10551](https://github.com/matrix-org/synapse/issues/10551)) +- Update CONTRIBUTING.md to fix index links and the instructions for SyTest in docker. ([\#10599](https://github.com/matrix-org/synapse/issues/10599)) + + +Deprecations and Removals +------------------------- + +- No longer build `.dev` packages for Ubuntu 20.10 LTS Groovy Gorilla, which has now EOLed. ([\#10588](https://github.com/matrix-org/synapse/issues/10588)) +- The `template_dir` configuration settings in the `sso`, `account_validity` and `email` sections of the configuration file are now deprecated in favour of the global `templates.custom_template_directory` setting. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. ([\#10596](https://github.com/matrix-org/synapse/issues/10596)) + + +Internal Changes +---------------- + +- Improve event caching mechanism to avoid having multiple copies of an event in memory at a time. ([\#10119](https://github.com/matrix-org/synapse/issues/10119)) +- Reduce errors in PostgreSQL logs due to concurrent serialization errors. ([\#10504](https://github.com/matrix-org/synapse/issues/10504)) +- Include room ID in ignored EDU log messages. Contributed by @ilmari. ([\#10507](https://github.com/matrix-org/synapse/issues/10507)) +- Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). ([\#10527](https://github.com/matrix-org/synapse/issues/10527), [\#10530](https://github.com/matrix-org/synapse/issues/10530)) +- Fix CI to not break when run against branches rather than pull requests. ([\#10529](https://github.com/matrix-org/synapse/issues/10529)) +- Mark all events stemming from the MSC2716 `/batch_send` endpoint as historical. ([\#10537](https://github.com/matrix-org/synapse/issues/10537)) +- Clean up some of the federation event authentication code for clarity. ([\#10539](https://github.com/matrix-org/synapse/issues/10539), [\#10591](https://github.com/matrix-org/synapse/issues/10591)) +- Convert `Transaction` and `Edu` objects to attrs. ([\#10542](https://github.com/matrix-org/synapse/issues/10542)) +- Update `/batch_send` endpoint to only return `state_events` created by the `state_events_from_before` passed in. ([\#10552](https://github.com/matrix-org/synapse/issues/10552)) +- Update contributing.md to warn against rebasing an open PR. ([\#10563](https://github.com/matrix-org/synapse/issues/10563)) +- Remove the unused public rooms replication stream. ([\#10565](https://github.com/matrix-org/synapse/issues/10565)) +- Clarify error message when failing to join a restricted room. ([\#10572](https://github.com/matrix-org/synapse/issues/10572)) +- Remove references to BuildKite in favour of GitHub Actions. ([\#10573](https://github.com/matrix-org/synapse/issues/10573)) +- Move `/batch_send` endpoint defined by [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716) to the `/v2_alpha` directory. ([\#10576](https://github.com/matrix-org/synapse/issues/10576)) +- Allow multiple custom directories in `read_templates`. ([\#10587](https://github.com/matrix-org/synapse/issues/10587)) +- Re-organize the `synapse.federation.transport.server` module to create smaller files. ([\#10590](https://github.com/matrix-org/synapse/issues/10590)) +- Flatten the `synapse.rest.client` package by moving the contents of `v1` and `v2_alpha` into the parent. ([\#10600](https://github.com/matrix-org/synapse/issues/10600)) +- Build Debian packages for Debian 12 (Bookworm). ([\#10612](https://github.com/matrix-org/synapse/issues/10612)) +- Fix up a couple of links to the database schema documentation. ([\#10620](https://github.com/matrix-org/synapse/issues/10620)) +- Fix a broken link to the upgrade notes. ([\#10631](https://github.com/matrix-org/synapse/issues/10631)) + + Synapse 1.40.0 (2021-08-10) =========================== diff --git a/changelog.d/10119.misc b/changelog.d/10119.misc deleted file mode 100644 index f70dc6496f..0000000000 --- a/changelog.d/10119.misc +++ /dev/null @@ -1 +0,0 @@ -Improve event caching mechanism to avoid having multiple copies of an event in memory at a time. diff --git a/changelog.d/10129.bugfix b/changelog.d/10129.bugfix deleted file mode 100644 index 292676ec8d..0000000000 --- a/changelog.d/10129.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add some clarification to the sample config file. Contributed by @Kentokamoto. diff --git a/changelog.d/10394.feature b/changelog.d/10394.feature deleted file mode 100644 index c8bbc5a740..0000000000 --- a/changelog.d/10394.feature +++ /dev/null @@ -1 +0,0 @@ -Initial local support for [MSC3266](https://github.com/matrix-org/synapse/pull/10394), Room Summary over the unstable `/rooms/{roomIdOrAlias}/summary` API. diff --git a/changelog.d/10435.feature b/changelog.d/10435.feature deleted file mode 100644 index f93ef5b415..0000000000 --- a/changelog.d/10435.feature +++ /dev/null @@ -1 +0,0 @@ -Experimental support for [MSC3288](https://github.com/matrix-org/matrix-doc/pull/3288), sending `room_type` to the identity server for 3pid invites over the `/store-invite` API. diff --git a/changelog.d/10443.doc b/changelog.d/10443.doc deleted file mode 100644 index 3588e5487f..0000000000 --- a/changelog.d/10443.doc +++ /dev/null @@ -1 +0,0 @@ -Add documentation for configuration a forward proxy. diff --git a/changelog.d/10475.feature b/changelog.d/10475.feature deleted file mode 100644 index 52eab11b03..0000000000 --- a/changelog.d/10475.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for sending federation requests through a proxy. Contributed by @Bubu and @dklimpel. \ No newline at end of file diff --git a/changelog.d/10498.feature b/changelog.d/10498.feature deleted file mode 100644 index 5df896572d..0000000000 --- a/changelog.d/10498.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for "marker" events which makes historical events discoverable for servers that already have all of the scrollback history (part of MSC2716). diff --git a/changelog.d/10504.misc b/changelog.d/10504.misc deleted file mode 100644 index 1479a5022d..0000000000 --- a/changelog.d/10504.misc +++ /dev/null @@ -1 +0,0 @@ -Reduce errors in PostgreSQL logs due to concurrent serialization errors. diff --git a/changelog.d/10507.misc b/changelog.d/10507.misc deleted file mode 100644 index 5dfd116e60..0000000000 --- a/changelog.d/10507.misc +++ /dev/null @@ -1 +0,0 @@ -Include room ID in ignored EDU log messages. Contributed by @ilmari. diff --git a/changelog.d/10513.feature b/changelog.d/10513.feature deleted file mode 100644 index 153b2df7b2..0000000000 --- a/changelog.d/10513.feature +++ /dev/null @@ -1 +0,0 @@ -Add a configuration setting for the time a `/sync` response is cached for. diff --git a/changelog.d/10518.feature b/changelog.d/10518.feature deleted file mode 100644 index 112e4d105c..0000000000 --- a/changelog.d/10518.feature +++ /dev/null @@ -1 +0,0 @@ -The default logging handler for new installations is now `PeriodicallyFlushingMemoryHandler`, a buffered logging handler which periodically flushes itself. diff --git a/changelog.d/10527.misc b/changelog.d/10527.misc deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10527.misc +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10529.misc b/changelog.d/10529.misc deleted file mode 100644 index 4caf22523c..0000000000 --- a/changelog.d/10529.misc +++ /dev/null @@ -1 +0,0 @@ -Fix CI to not break when run against branches rather than pull requests. diff --git a/changelog.d/10530.misc b/changelog.d/10530.misc deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10530.misc +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10532.bugfix b/changelog.d/10532.bugfix deleted file mode 100644 index d95e3d9b59..0000000000 --- a/changelog.d/10532.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a long-standing bug where protocols which are not implemented by any appservices were incorrectly returned via `GET /_matrix/client/r0/thirdparty/protocols`. diff --git a/changelog.d/10537.misc b/changelog.d/10537.misc deleted file mode 100644 index c9e045300c..0000000000 --- a/changelog.d/10537.misc +++ /dev/null @@ -1 +0,0 @@ -Mark all events stemming from the MSC2716 `/batch_send` endpoint as historical. diff --git a/changelog.d/10538.feature b/changelog.d/10538.feature deleted file mode 100644 index 120c8e8ca0..0000000000 --- a/changelog.d/10538.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for new redaction rules for historical events specified in [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716). diff --git a/changelog.d/10539.misc b/changelog.d/10539.misc deleted file mode 100644 index 9a765435db..0000000000 --- a/changelog.d/10539.misc +++ /dev/null @@ -1 +0,0 @@ -Clean up some of the federation event authentication code for clarity. diff --git a/changelog.d/10541.bugfix b/changelog.d/10541.bugfix deleted file mode 100644 index bb946e0920..0000000000 --- a/changelog.d/10541.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix exceptions in logs when failing to get remote room list. diff --git a/changelog.d/10542.misc b/changelog.d/10542.misc deleted file mode 100644 index 44b70b4730..0000000000 --- a/changelog.d/10542.misc +++ /dev/null @@ -1 +0,0 @@ -Convert `Transaction` and `Edu` objects to attrs. diff --git a/changelog.d/10546.feature b/changelog.d/10546.feature deleted file mode 100644 index 7709d010b3..0000000000 --- a/changelog.d/10546.feature +++ /dev/null @@ -1 +0,0 @@ -Add a setting to disable TLS when sending email. diff --git a/changelog.d/10549.feature b/changelog.d/10549.feature deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10549.feature +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10550.bugfix b/changelog.d/10550.bugfix deleted file mode 100644 index 2e1b7c8bbb..0000000000 --- a/changelog.d/10550.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix longstanding bug which caused the user "status" to be reset when the user went offline. Contributed by @dklimpel. diff --git a/changelog.d/10551.doc b/changelog.d/10551.doc deleted file mode 100644 index 4a2b0785bf..0000000000 --- a/changelog.d/10551.doc +++ /dev/null @@ -1 +0,0 @@ -Updated the reverse proxy documentation to highlight the homserver configuration that is needed to make Synapse aware that is is intentionally reverse proxied. diff --git a/changelog.d/10552.misc b/changelog.d/10552.misc deleted file mode 100644 index fc5f6aea5f..0000000000 --- a/changelog.d/10552.misc +++ /dev/null @@ -1 +0,0 @@ -Update `/batch_send` endpoint to only return `state_events` created by the `state_events_from_before` passed in. diff --git a/changelog.d/10558.feature b/changelog.d/10558.feature deleted file mode 100644 index 1f461bc70a..0000000000 --- a/changelog.d/10558.feature +++ /dev/null @@ -1 +0,0 @@ -Admin API to delete several media for a specific user. Contributed by @dklimpel. diff --git a/changelog.d/10560.feature b/changelog.d/10560.feature deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10560.feature +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10563.misc b/changelog.d/10563.misc deleted file mode 100644 index 8e4e90c8f4..0000000000 --- a/changelog.d/10563.misc +++ /dev/null @@ -1 +0,0 @@ -Update contributing.md to warn against rebasing an open PR. diff --git a/changelog.d/10564.feature b/changelog.d/10564.feature deleted file mode 100644 index 4de32240b2..0000000000 --- a/changelog.d/10564.feature +++ /dev/null @@ -1 +0,0 @@ -Add support for routing `/createRoom` to workers. diff --git a/changelog.d/10565.misc b/changelog.d/10565.misc deleted file mode 100644 index 06796b61ab..0000000000 --- a/changelog.d/10565.misc +++ /dev/null @@ -1 +0,0 @@ -Remove the unused public rooms replication stream. \ No newline at end of file diff --git a/changelog.d/10569.feature b/changelog.d/10569.feature deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10569.feature +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10570.feature b/changelog.d/10570.feature deleted file mode 100644 index bd432685b3..0000000000 --- a/changelog.d/10570.feature +++ /dev/null @@ -1 +0,0 @@ -Update the Synapse Grafana dashboard. diff --git a/changelog.d/10572.misc b/changelog.d/10572.misc deleted file mode 100644 index 008d7be444..0000000000 --- a/changelog.d/10572.misc +++ /dev/null @@ -1 +0,0 @@ -Clarify error message when failing to join a restricted room. diff --git a/changelog.d/10573.misc b/changelog.d/10573.misc deleted file mode 100644 index fc9b1a2f70..0000000000 --- a/changelog.d/10573.misc +++ /dev/null @@ -1 +0,0 @@ -Remove references to BuildKite in favour of GitHub Actions. \ No newline at end of file diff --git a/changelog.d/10574.feature b/changelog.d/10574.feature deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10574.feature +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10575.feature b/changelog.d/10575.feature deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10575.feature +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10576.misc b/changelog.d/10576.misc deleted file mode 100644 index f9f9c9a6fd..0000000000 --- a/changelog.d/10576.misc +++ /dev/null @@ -1 +0,0 @@ -Move `/batch_send` endpoint defined by [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716) to the `/v2_alpha` directory. diff --git a/changelog.d/10578.feature b/changelog.d/10578.feature deleted file mode 100644 index 02397f0009..0000000000 --- a/changelog.d/10578.feature +++ /dev/null @@ -1 +0,0 @@ -Add an admin API (`GET /_synapse/admin/username_available`) to check if a username is available (regardless of registration settings). \ No newline at end of file diff --git a/changelog.d/10579.feature b/changelog.d/10579.feature deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10579.feature +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10580.bugfix b/changelog.d/10580.bugfix deleted file mode 100644 index f8da7382b7..0000000000 --- a/changelog.d/10580.bugfix +++ /dev/null @@ -1 +0,0 @@ -Allow public rooms to be previewed in the spaces summary APIs from [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10583.feature b/changelog.d/10583.feature deleted file mode 100644 index ffc4e4289c..0000000000 --- a/changelog.d/10583.feature +++ /dev/null @@ -1 +0,0 @@ -Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). diff --git a/changelog.d/10587.misc b/changelog.d/10587.misc deleted file mode 100644 index 4c6167977c..0000000000 --- a/changelog.d/10587.misc +++ /dev/null @@ -1 +0,0 @@ -Allow multiple custom directories in `read_templates`. diff --git a/changelog.d/10588.removal b/changelog.d/10588.removal deleted file mode 100644 index 90c4b5cee2..0000000000 --- a/changelog.d/10588.removal +++ /dev/null @@ -1 +0,0 @@ -No longer build `.dev` packages for Ubuntu 20.10 LTS Groovy Gorilla, which has now EOLed. \ No newline at end of file diff --git a/changelog.d/10590.misc b/changelog.d/10590.misc deleted file mode 100644 index 62fec717da..0000000000 --- a/changelog.d/10590.misc +++ /dev/null @@ -1 +0,0 @@ -Re-organize the `synapse.federation.transport.server` module to create smaller files. diff --git a/changelog.d/10591.misc b/changelog.d/10591.misc deleted file mode 100644 index 9a765435db..0000000000 --- a/changelog.d/10591.misc +++ /dev/null @@ -1 +0,0 @@ -Clean up some of the federation event authentication code for clarity. diff --git a/changelog.d/10592.bugfix b/changelog.d/10592.bugfix deleted file mode 100644 index efcdab1136..0000000000 --- a/changelog.d/10592.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a bug introduced in v1.37.1 where an error could occur in the asyncronous processing of PDUs when the queue was empty. diff --git a/changelog.d/10596.removal b/changelog.d/10596.removal deleted file mode 100644 index e69f632db4..0000000000 --- a/changelog.d/10596.removal +++ /dev/null @@ -1 +0,0 @@ -The `template_dir` configuration settings in the `sso`, `account_validity` and `email` sections of the configuration file are now deprecated in favour of the global `templates.custom_template_directory` setting. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. diff --git a/changelog.d/10598.feature b/changelog.d/10598.feature deleted file mode 100644 index 92c159118b..0000000000 --- a/changelog.d/10598.feature +++ /dev/null @@ -1 +0,0 @@ -Allow editing a user's `external_ids` via the "Edit User" admin API. Contributed by @dklimpel. \ No newline at end of file diff --git a/changelog.d/10599.doc b/changelog.d/10599.doc deleted file mode 100644 index 66e72078f0..0000000000 --- a/changelog.d/10599.doc +++ /dev/null @@ -1 +0,0 @@ -Update CONTRIBUTING.md to fix index links and the instructions for SyTest in docker. diff --git a/changelog.d/10600.misc b/changelog.d/10600.misc deleted file mode 100644 index 489dc20b11..0000000000 --- a/changelog.d/10600.misc +++ /dev/null @@ -1 +0,0 @@ -Flatten the `synapse.rest.client` package by moving the contents of `v1` and `v2_alpha` into the parent. diff --git a/changelog.d/10602.feature b/changelog.d/10602.feature deleted file mode 100644 index ab18291a20..0000000000 --- a/changelog.d/10602.feature +++ /dev/null @@ -1 +0,0 @@ -The Synapse manhole no longer needs coroutines to be wrapped in `defer.ensureDeferred`. diff --git a/changelog.d/10606.bugfix b/changelog.d/10606.bugfix deleted file mode 100644 index bab9fd2a61..0000000000 --- a/changelog.d/10606.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix errors on /sync when read receipt data is a string. Only affects homeservers with the experimental flag for [MSC2285](https://github.com/matrix-org/matrix-doc/pull/2285) enabled. Contributed by @SimonBrandner. diff --git a/changelog.d/10611.bugfix b/changelog.d/10611.bugfix deleted file mode 100644 index ecbe408b47..0000000000 --- a/changelog.d/10611.bugfix +++ /dev/null @@ -1 +0,0 @@ -Additional validation for the spaces summary API to avoid errors like `ValueError: Stop argument for islice() must be None or an integer`. The missing validation has existed since v1.31.0. diff --git a/changelog.d/10612.misc b/changelog.d/10612.misc deleted file mode 100644 index c7a9457022..0000000000 --- a/changelog.d/10612.misc +++ /dev/null @@ -1 +0,0 @@ -Build Debian packages for Debian 12 (Bookworm). diff --git a/changelog.d/10620.misc b/changelog.d/10620.misc deleted file mode 100644 index 8b29668a1f..0000000000 --- a/changelog.d/10620.misc +++ /dev/null @@ -1 +0,0 @@ -Fix up a couple of links to the database schema documentation. diff --git a/changelog.d/10623.bugfix b/changelog.d/10623.bugfix deleted file mode 100644 index 759fba3513..0000000000 --- a/changelog.d/10623.bugfix +++ /dev/null @@ -1 +0,0 @@ -Revert behaviour introduced in v1.38.0 that strips `org.matrix.msc2732.device_unused_fallback_key_types` from `/sync` when its value is empty. This field should instead always be present according to [MSC2732](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2732-olm-fallback-keys.md). \ No newline at end of file diff --git a/changelog.d/10628.feature b/changelog.d/10628.feature deleted file mode 100644 index 708cb9b599..0000000000 --- a/changelog.d/10628.feature +++ /dev/null @@ -1 +0,0 @@ -Admin API to delete several media for a specific user. Contributed by @dklimpel. \ No newline at end of file diff --git a/changelog.d/10631.misc b/changelog.d/10631.misc deleted file mode 100644 index d2a4624d53..0000000000 --- a/changelog.d/10631.misc +++ /dev/null @@ -1 +0,0 @@ -Fix a broken link to the upgrade notes. diff --git a/changelog.d/10638.feature b/changelog.d/10638.feature deleted file mode 100644 index c1de91f334..0000000000 --- a/changelog.d/10638.feature +++ /dev/null @@ -1 +0,0 @@ -Add option to allow modules to run periodic tasks on all instances, rather than just the one configured to run background tasks. diff --git a/changelog.d/9581.feature b/changelog.d/9581.feature deleted file mode 100644 index fa1949cd4b..0000000000 --- a/changelog.d/9581.feature +++ /dev/null @@ -1 +0,0 @@ -Add `get_userinfo_by_id` method to ModuleApi. diff --git a/debian/changelog b/debian/changelog index e101423fe4..68f309b0b2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.41.0~rc1) stable; urgency=medium + + * New synapse release 1.41.0~rc1. + + -- Synapse Packaging team Wed, 18 Aug 2021 15:52:00 +0100 + matrix-synapse-py3 (1.40.0) stable; urgency=medium * New synapse release 1.40.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index 919293cd80..6ada20a77f 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.40.0" +__version__ = "1.41.0rc1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From e328d8ffd93247f5b6be9a1fd1e5dea9b99149e7 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 18 Aug 2021 15:54:50 +0100 Subject: [PATCH 588/619] Update changelog --- CHANGES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b96ac701d5..01766af39c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Features - Initial local support for [MSC3266](https://github.com/matrix-org/synapse/pull/10394), Room Summary over the unstable `/rooms/{roomIdOrAlias}/summary` API. ([\#10394](https://github.com/matrix-org/synapse/issues/10394)) - Experimental support for [MSC3288](https://github.com/matrix-org/matrix-doc/pull/3288), sending `room_type` to the identity server for 3pid invites over the `/store-invite` API. ([\#10435](https://github.com/matrix-org/synapse/issues/10435)) - Add support for sending federation requests through a proxy. Contributed by @Bubu and @dklimpel. ([\#10475](https://github.com/matrix-org/synapse/issues/10475)) -- Add support for "marker" events which makes historical events discoverable for servers that already have all of the scrollback history (part of MSC2716). ([\#10498](https://github.com/matrix-org/synapse/issues/10498)) +- Add support for "marker" events which makes historical events discoverable for servers that already have all of the scrollback history (part of [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#10498](https://github.com/matrix-org/synapse/issues/10498)) - Add a configuration setting for the time a `/sync` response is cached for. ([\#10513](https://github.com/matrix-org/synapse/issues/10513)) - The default logging handler for new installations is now `PeriodicallyFlushingMemoryHandler`, a buffered logging handler which periodically flushes itself. ([\#10518](https://github.com/matrix-org/synapse/issues/10518)) - Add support for new redaction rules for historical events specified in [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716). ([\#10538](https://github.com/matrix-org/synapse/issues/10538)) @@ -31,7 +31,7 @@ Bugfixes - Fix exceptions in logs when failing to get remote room list. ([\#10541](https://github.com/matrix-org/synapse/issues/10541)) - Fix longstanding bug which caused the user "status" to be reset when the user went offline. Contributed by @dklimpel. ([\#10550](https://github.com/matrix-org/synapse/issues/10550)) - Allow public rooms to be previewed in the spaces summary APIs from [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). ([\#10580](https://github.com/matrix-org/synapse/issues/10580)) -- Fix a bug introduced in v1.37.1 where an error could occur in the asyncronous processing of PDUs when the queue was empty. ([\#10592](https://github.com/matrix-org/synapse/issues/10592)) +- Fix a bug introduced in v1.37.1 where an error could occur in the asynchronous processing of PDUs when the queue was empty. ([\#10592](https://github.com/matrix-org/synapse/issues/10592)) - Fix errors on /sync when read receipt data is a string. Only affects homeservers with the experimental flag for [MSC2285](https://github.com/matrix-org/matrix-doc/pull/2285) enabled. Contributed by @SimonBrandner. ([\#10606](https://github.com/matrix-org/synapse/issues/10606)) - Additional validation for the spaces summary API to avoid errors like `ValueError: Stop argument for islice() must be None or an integer`. The missing validation has existed since v1.31.0. ([\#10611](https://github.com/matrix-org/synapse/issues/10611)) - Revert behaviour introduced in v1.38.0 that strips `org.matrix.msc2732.device_unused_fallback_key_types` from `/sync` when its value is empty. This field should instead always be present according to [MSC2732](https://github.com/matrix-org/matrix-doc/blob/master/proposals/2732-olm-fallback-keys.md). ([\#10623](https://github.com/matrix-org/synapse/issues/10623)) @@ -48,7 +48,7 @@ Improved Documentation Deprecations and Removals ------------------------- -- No longer build `.dev` packages for Ubuntu 20.10 LTS Groovy Gorilla, which has now EOLed. ([\#10588](https://github.com/matrix-org/synapse/issues/10588)) +- No longer build `.deb` packages for Ubuntu 20.10 LTS Groovy Gorilla, which has now EOLed. ([\#10588](https://github.com/matrix-org/synapse/issues/10588)) - The `template_dir` configuration settings in the `sso`, `account_validity` and `email` sections of the configuration file are now deprecated in favour of the global `templates.custom_template_directory` setting. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. ([\#10596](https://github.com/matrix-org/synapse/issues/10596)) @@ -60,7 +60,7 @@ Internal Changes - Include room ID in ignored EDU log messages. Contributed by @ilmari. ([\#10507](https://github.com/matrix-org/synapse/issues/10507)) - Add pagination to the spaces summary based on updates to [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). ([\#10527](https://github.com/matrix-org/synapse/issues/10527), [\#10530](https://github.com/matrix-org/synapse/issues/10530)) - Fix CI to not break when run against branches rather than pull requests. ([\#10529](https://github.com/matrix-org/synapse/issues/10529)) -- Mark all events stemming from the MSC2716 `/batch_send` endpoint as historical. ([\#10537](https://github.com/matrix-org/synapse/issues/10537)) +- Mark all events stemming from the [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716) `/batch_send` endpoint as historical. ([\#10537](https://github.com/matrix-org/synapse/issues/10537)) - Clean up some of the federation event authentication code for clarity. ([\#10539](https://github.com/matrix-org/synapse/issues/10539), [\#10591](https://github.com/matrix-org/synapse/issues/10591)) - Convert `Transaction` and `Edu` objects to attrs. ([\#10542](https://github.com/matrix-org/synapse/issues/10542)) - Update `/batch_send` endpoint to only return `state_events` created by the `state_events_from_before` passed in. ([\#10552](https://github.com/matrix-org/synapse/issues/10552)) From b9c35586a4fadae271b3fefb90a3108f74e9e3d5 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Wed, 18 Aug 2021 16:59:36 +0100 Subject: [PATCH 589/619] Update docs/upgrade.md with new version --- docs/upgrade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrade.md b/docs/upgrade.md index 99e32034c8..e5d386b02f 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -86,7 +86,7 @@ process, for example: ``` -# Upgrading to v1.xx.0 +# Upgrading to v1.41.0 ## Add support for routing outbound HTTP requests via a proxy for federation From ce6819a7015847084208ff09758c5a13c1c4c429 Mon Sep 17 00:00:00 2001 From: John-Scott Atlakson <24574+jsma@users.noreply.github.com> Date: Thu, 19 Aug 2021 03:16:00 -0700 Subject: [PATCH 590/619] Fix typo in release notes (#10646) Ubuntu 20.10 was not an LTS release Signed-off-by: John-Scott Atlakson 24574+jsma@users.noreply.github.com --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 01766af39c..cad9423ebd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -48,7 +48,7 @@ Improved Documentation Deprecations and Removals ------------------------- -- No longer build `.deb` packages for Ubuntu 20.10 LTS Groovy Gorilla, which has now EOLed. ([\#10588](https://github.com/matrix-org/synapse/issues/10588)) +- No longer build `.deb` packages for Ubuntu 20.10 Groovy Gorilla, which has now EOLed. ([\#10588](https://github.com/matrix-org/synapse/issues/10588)) - The `template_dir` configuration settings in the `sso`, `account_validity` and `email` sections of the configuration file are now deprecated in favour of the global `templates.custom_template_directory` setting. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. ([\#10596](https://github.com/matrix-org/synapse/issues/10596)) From 5cda75fedef3dd02d3b456231be0a1b4bff2a31a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 20 Aug 2021 07:17:50 -0400 Subject: [PATCH 591/619] Set room version 8 as preferred for restricted rooms. (#10571) --- changelog.d/10571.feature | 1 + synapse/api/room_versions.py | 2 +- synapse/config/experimental.py | 2 +- tests/rest/client/v2_alpha/test_capabilities.py | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/10571.feature diff --git a/changelog.d/10571.feature b/changelog.d/10571.feature new file mode 100644 index 0000000000..0da318cd5b --- /dev/null +++ b/changelog.d/10571.feature @@ -0,0 +1 @@ +Enable room capabilities ([MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244)) by default and set room version 8 as the preferred room version for restricted rooms. diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 11280c4462..8abcdfd4fd 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -293,7 +293,7 @@ class RoomVersionCapability: ), RoomVersionCapability( "restricted", - None, + RoomVersions.V8, lambda room_version: room_version.msc3083_join_rules, ), ) diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index b918fb15b0..907df9591a 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -37,7 +37,7 @@ def read_config(self, config: JsonDict, **kwargs): self.msc2285_enabled: bool = experimental.get("msc2285_enabled", False) # MSC3244 (room version capabilities) - self.msc3244_enabled: bool = experimental.get("msc3244_enabled", False) + self.msc3244_enabled: bool = experimental.get("msc3244_enabled", True) # MSC3266 (room summary api) self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False) diff --git a/tests/rest/client/v2_alpha/test_capabilities.py b/tests/rest/client/v2_alpha/test_capabilities.py index ad83b3d2ff..13b3c5f499 100644 --- a/tests/rest/client/v2_alpha/test_capabilities.py +++ b/tests/rest/client/v2_alpha/test_capabilities.py @@ -102,7 +102,8 @@ def test_get_change_password_capabilities_password_disabled(self): self.assertEqual(channel.code, 200) self.assertFalse(capabilities["m.change_password"]["enabled"]) - def test_get_does_not_include_msc3244_fields_by_default(self): + @override_config({"experimental_features": {"msc3244_enabled": False}}) + def test_get_does_not_include_msc3244_fields_when_disabled(self): localpart = "user" password = "pass" user = self.register_user(localpart, password) @@ -120,7 +121,6 @@ def test_get_does_not_include_msc3244_fields_by_default(self): "org.matrix.msc3244.room_capabilities", capabilities["m.room_versions"] ) - @override_config({"experimental_features": {"msc3244_enabled": True}}) def test_get_does_include_msc3244_fields_when_enabled(self): localpart = "user" password = "pass" From 6f77a3d433c683223024075e805f87bec3327036 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 24 Aug 2021 15:31:55 +0100 Subject: [PATCH 592/619] 1.41.0 --- CHANGES.md | 9 +++++++++ changelog.d/10571.feature | 1 - debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/10571.feature diff --git a/CHANGES.md b/CHANGES.md index cad9423ebd..35456cded6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,12 @@ +Synapse 1.41.0 (2021-08-24) +=========================== + +Features +-------- + +- Enable room capabilities ([MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244)) by default and set room version 8 as the preferred room version for restricted rooms. ([\#10571](https://github.com/matrix-org/synapse/issues/10571)) + + Synapse 1.41.0rc1 (2021-08-18) ============================== diff --git a/changelog.d/10571.feature b/changelog.d/10571.feature deleted file mode 100644 index 0da318cd5b..0000000000 --- a/changelog.d/10571.feature +++ /dev/null @@ -1 +0,0 @@ -Enable room capabilities ([MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244)) by default and set room version 8 as the preferred room version for restricted rooms. diff --git a/debian/changelog b/debian/changelog index 68f309b0b2..4da4bc018c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.41.0) stable; urgency=medium + + * New synapse release 1.41.0. + + -- Synapse Packaging team Tue, 24 Aug 2021 15:31:45 +0100 + matrix-synapse-py3 (1.41.0~rc1) stable; urgency=medium * New synapse release 1.41.0~rc1. diff --git a/synapse/__init__.py b/synapse/__init__.py index 6ada20a77f..ef3770262e 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.41.0rc1" +__version__ = "1.41.0" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From f03cafb50c49a1569f1f99485f9cc42abfdc7b21 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 24 Aug 2021 16:06:33 +0100 Subject: [PATCH 593/619] Update changelog --- CHANGES.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 35456cded6..f8da8771aa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,15 @@ Synapse 1.41.0 (2021-08-24) =========================== +This release adds support for Debian 12 (Bookworm), but **removes support for Ubuntu 20.10 (Groovy Gorilla)**, which reached End of Life last month. + +Note that when using workers the `/_synapse/admin/v1/users/{userId}/media` must now be handled by media workers. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. + + Features -------- -- Enable room capabilities ([MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244)) by default and set room version 8 as the preferred room version for restricted rooms. ([\#10571](https://github.com/matrix-org/synapse/issues/10571)) +- Enable room capabilities ([MSC3244](https://github.com/matrix-org/matrix-doc/pull/3244)) by default and set room version 8 as the preferred room version when creating restricted rooms. ([\#10571](https://github.com/matrix-org/synapse/issues/10571)) Synapse 1.41.0rc1 (2021-08-18) @@ -16,7 +21,7 @@ Features - Add `get_userinfo_by_id` method to ModuleApi. ([\#9581](https://github.com/matrix-org/synapse/issues/9581)) - Initial local support for [MSC3266](https://github.com/matrix-org/synapse/pull/10394), Room Summary over the unstable `/rooms/{roomIdOrAlias}/summary` API. ([\#10394](https://github.com/matrix-org/synapse/issues/10394)) - Experimental support for [MSC3288](https://github.com/matrix-org/matrix-doc/pull/3288), sending `room_type` to the identity server for 3pid invites over the `/store-invite` API. ([\#10435](https://github.com/matrix-org/synapse/issues/10435)) -- Add support for sending federation requests through a proxy. Contributed by @Bubu and @dklimpel. ([\#10475](https://github.com/matrix-org/synapse/issues/10475)) +- Add support for sending federation requests through a proxy. Contributed by @Bubu and @dklimpel. See the [upgrade notes](https://matrix-org.github.io/synapse/latest/upgrade.html) for more information. ([\#10596](https://github.com/matrix-org/synapse/issues/10596)). ([\#10475](https://github.com/matrix-org/synapse/issues/10475)) - Add support for "marker" events which makes historical events discoverable for servers that already have all of the scrollback history (part of [MSC2716](https://github.com/matrix-org/matrix-doc/pull/2716)). ([\#10498](https://github.com/matrix-org/synapse/issues/10498)) - Add a configuration setting for the time a `/sync` response is cached for. ([\#10513](https://github.com/matrix-org/synapse/issues/10513)) - The default logging handler for new installations is now `PeriodicallyFlushingMemoryHandler`, a buffered logging handler which periodically flushes itself. ([\#10518](https://github.com/matrix-org/synapse/issues/10518)) @@ -38,7 +43,7 @@ Bugfixes - Add some clarification to the sample config file. Contributed by @Kentokamoto. ([\#10129](https://github.com/matrix-org/synapse/issues/10129)) - Fix a long-standing bug where protocols which are not implemented by any appservices were incorrectly returned via `GET /_matrix/client/r0/thirdparty/protocols`. ([\#10532](https://github.com/matrix-org/synapse/issues/10532)) - Fix exceptions in logs when failing to get remote room list. ([\#10541](https://github.com/matrix-org/synapse/issues/10541)) -- Fix longstanding bug which caused the user "status" to be reset when the user went offline. Contributed by @dklimpel. ([\#10550](https://github.com/matrix-org/synapse/issues/10550)) +- Fix longstanding bug which caused the user's presence "status message" to be reset when the user went offline. Contributed by @dklimpel. ([\#10550](https://github.com/matrix-org/synapse/issues/10550)) - Allow public rooms to be previewed in the spaces summary APIs from [MSC2946](https://github.com/matrix-org/matrix-doc/pull/2946). ([\#10580](https://github.com/matrix-org/synapse/issues/10580)) - Fix a bug introduced in v1.37.1 where an error could occur in the asynchronous processing of PDUs when the queue was empty. ([\#10592](https://github.com/matrix-org/synapse/issues/10592)) - Fix errors on /sync when read receipt data is a string. Only affects homeservers with the experimental flag for [MSC2285](https://github.com/matrix-org/matrix-doc/pull/2285) enabled. Contributed by @SimonBrandner. ([\#10606](https://github.com/matrix-org/synapse/issues/10606)) @@ -49,7 +54,7 @@ Bugfixes Improved Documentation ---------------------- -- Add documentation for configuration a forward proxy. ([\#10443](https://github.com/matrix-org/synapse/issues/10443)) +- Add documentation for configuring a forward proxy. ([\#10443](https://github.com/matrix-org/synapse/issues/10443)) - Updated the reverse proxy documentation to highlight the homserver configuration that is needed to make Synapse aware that is is intentionally reverse proxied. ([\#10551](https://github.com/matrix-org/synapse/issues/10551)) - Update CONTRIBUTING.md to fix index links and the instructions for SyTest in docker. ([\#10599](https://github.com/matrix-org/synapse/issues/10599)) From 8f98260552f4f39f003bc1fbf6da159d9138081d Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Fri, 27 Aug 2021 16:33:41 +0100 Subject: [PATCH 594/619] Fix incompatibility with Twisted < 21. (#10713) Turns out that the functionality added in #10546 to skip TLS was incompatible with older Twisted versions, so we need to be a bit more inventive. Also, add a test to (hopefully) not break this in future. Sadly, testing TLS is really hard. --- changelog.d/10713.bugfix | 1 + mypy.ini | 1 + synapse/handlers/send_email.py | 65 ++++++++++++----- tests/handlers/test_send_email.py | 112 ++++++++++++++++++++++++++++++ tests/server.py | 15 +++- 5 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 changelog.d/10713.bugfix create mode 100644 tests/handlers/test_send_email.py diff --git a/changelog.d/10713.bugfix b/changelog.d/10713.bugfix new file mode 100644 index 0000000000..e8caf3d23a --- /dev/null +++ b/changelog.d/10713.bugfix @@ -0,0 +1 @@ +Fix a regression introduced in Synapse 1.41 which broke email transmission on Systems using older versions of the Twisted library. diff --git a/mypy.ini b/mypy.ini index e1b9405daa..349efe37bb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -87,6 +87,7 @@ files = tests/test_utils, tests/handlers/test_password_providers.py, tests/handlers/test_room_summary.py, + tests/handlers/test_send_email.py, tests/rest/client/v1/test_login.py, tests/rest/client/v2_alpha/test_auth.py, tests/util/test_itertools.py, diff --git a/synapse/handlers/send_email.py b/synapse/handlers/send_email.py index dda9659c11..a31fe3e3c7 100644 --- a/synapse/handlers/send_email.py +++ b/synapse/handlers/send_email.py @@ -19,9 +19,12 @@ from io import BytesIO from typing import TYPE_CHECKING, Optional +from pkg_resources import parse_version + +import twisted from twisted.internet.defer import Deferred -from twisted.internet.interfaces import IReactorTCP -from twisted.mail.smtp import ESMTPSenderFactory +from twisted.internet.interfaces import IOpenSSLContextFactory, IReactorTCP +from twisted.mail.smtp import ESMTPSender, ESMTPSenderFactory from synapse.logging.context import make_deferred_yieldable @@ -30,6 +33,19 @@ logger = logging.getLogger(__name__) +_is_old_twisted = parse_version(twisted.__version__) < parse_version("21") + + +class _NoTLSESMTPSender(ESMTPSender): + """Extend ESMTPSender to disable TLS + + Unfortunately, before Twisted 21.2, ESMTPSender doesn't give an easy way to disable + TLS, so we override its internal method which it uses to generate a context factory. + """ + + def _getContextFactory(self) -> Optional[IOpenSSLContextFactory]: + return None + async def _sendmail( reactor: IReactorTCP, @@ -42,7 +58,7 @@ async def _sendmail( password: Optional[bytes] = None, require_auth: bool = False, require_tls: bool = False, - tls_hostname: Optional[str] = None, + enable_tls: bool = True, ) -> None: """A simple wrapper around ESMTPSenderFactory, to allow substitution in tests @@ -57,24 +73,37 @@ async def _sendmail( password: password to give when authenticating require_auth: if auth is not offered, fail the request require_tls: if TLS is not offered, fail the reqest - tls_hostname: TLS hostname to check for. None to disable TLS. + enable_tls: True to enable TLS. If this is False and require_tls is True, + the request will fail. """ msg = BytesIO(msg_bytes) - d: "Deferred[object]" = Deferred() - factory = ESMTPSenderFactory( - username, - password, - from_addr, - to_addr, - msg, - d, - heloFallback=True, - requireAuthentication=require_auth, - requireTransportSecurity=require_tls, - hostname=tls_hostname, - ) + def build_sender_factory(**kwargs) -> ESMTPSenderFactory: + return ESMTPSenderFactory( + username, + password, + from_addr, + to_addr, + msg, + d, + heloFallback=True, + requireAuthentication=require_auth, + requireTransportSecurity=require_tls, + **kwargs, + ) + + if _is_old_twisted: + # before twisted 21.2, we have to override the ESMTPSender protocol to disable + # TLS + factory = build_sender_factory() + + if not enable_tls: + factory.protocol = _NoTLSESMTPSender + else: + # for twisted 21.2 and later, there is a 'hostname' parameter which we should + # set to enable TLS. + factory = build_sender_factory(hostname=smtphost if enable_tls else None) # the IReactorTCP interface claims host has to be a bytes, which seems to be wrong reactor.connectTCP(smtphost, smtpport, factory, timeout=30, bindAddress=None) # type: ignore[arg-type] @@ -154,5 +183,5 @@ async def send_email( password=self._smtp_pass, require_auth=self._smtp_user is not None, require_tls=self._require_transport_security, - tls_hostname=self._smtp_host if self._enable_tls else None, + enable_tls=self._enable_tls, ) diff --git a/tests/handlers/test_send_email.py b/tests/handlers/test_send_email.py new file mode 100644 index 0000000000..6f77b1237c --- /dev/null +++ b/tests/handlers/test_send_email.py @@ -0,0 +1,112 @@ +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import List, Tuple + +from zope.interface import implementer + +from twisted.internet import defer +from twisted.internet.address import IPv4Address +from twisted.internet.defer import ensureDeferred +from twisted.mail import interfaces, smtp + +from tests.server import FakeTransport +from tests.unittest import HomeserverTestCase + + +@implementer(interfaces.IMessageDelivery) +class _DummyMessageDelivery: + def __init__(self): + # (recipient, message) tuples + self.messages: List[Tuple[smtp.Address, bytes]] = [] + + def receivedHeader(self, helo, origin, recipients): + return None + + def validateFrom(self, helo, origin): + return origin + + def record_message(self, recipient: smtp.Address, message: bytes): + self.messages.append((recipient, message)) + + def validateTo(self, user: smtp.User): + return lambda: _DummyMessage(self, user) + + +@implementer(interfaces.IMessageSMTP) +class _DummyMessage: + """IMessageSMTP implementation which saves the message delivered to it + to the _DummyMessageDelivery object. + """ + + def __init__(self, delivery: _DummyMessageDelivery, user: smtp.User): + self._delivery = delivery + self._user = user + self._buffer: List[bytes] = [] + + def lineReceived(self, line): + self._buffer.append(line) + + def eomReceived(self): + message = b"\n".join(self._buffer) + b"\n" + self._delivery.record_message(self._user.dest, message) + return defer.succeed(b"saved") + + def connectionLost(self): + pass + + +class SendEmailHandlerTestCase(HomeserverTestCase): + def test_send_email(self): + """Happy-path test that we can send email to a non-TLS server.""" + h = self.hs.get_send_email_handler() + d = ensureDeferred( + h.send_email( + "foo@bar.com", "test subject", "Tests", "HTML content", "Text content" + ) + ) + # there should be an attempt to connect to localhost:25 + self.assertEqual(len(self.reactor.tcpClients), 1) + (host, port, client_factory, _timeout, _bindAddress) = self.reactor.tcpClients[ + 0 + ] + self.assertEqual(host, "localhost") + self.assertEqual(port, 25) + + # wire it up to an SMTP server + message_delivery = _DummyMessageDelivery() + server_protocol = smtp.ESMTP() + server_protocol.delivery = message_delivery + # make sure that the server uses the test reactor to set timeouts + server_protocol.callLater = self.reactor.callLater # type: ignore[assignment] + + client_protocol = client_factory.buildProtocol(None) + client_protocol.makeConnection(FakeTransport(server_protocol, self.reactor)) + server_protocol.makeConnection( + FakeTransport( + client_protocol, + self.reactor, + peer_address=IPv4Address("TCP", "127.0.0.1", 1234), + ) + ) + + # the message should now get delivered + self.get_success(d, by=0.1) + + # check it arrived + self.assertEqual(len(message_delivery.messages), 1) + user, msg = message_delivery.messages.pop() + self.assertEqual(str(user), "foo@bar.com") + self.assertIn(b"Subject: test subject", msg) diff --git a/tests/server.py b/tests/server.py index 6fddd3b305..b861c7b866 100644 --- a/tests/server.py +++ b/tests/server.py @@ -10,9 +10,10 @@ from twisted.internet import address, threads, udp from twisted.internet._resolver import SimpleResolverComplexifier -from twisted.internet.defer import Deferred, fail, succeed +from twisted.internet.defer import Deferred, fail, maybeDeferred, succeed from twisted.internet.error import DNSLookupError from twisted.internet.interfaces import ( + IAddress, IHostnameResolver, IProtocol, IPullProducer, @@ -511,6 +512,9 @@ class FakeTransport: will get called back for connectionLost() notifications etc. """ + _peer_address: Optional[IAddress] = attr.ib(default=None) + """The value to be returend by getPeer""" + disconnecting = False disconnected = False connected = True @@ -519,7 +523,7 @@ class FakeTransport: autoflush = attr.ib(default=True) def getPeer(self): - return None + return self._peer_address def getHost(self): return None @@ -572,7 +576,12 @@ def registerProducer(self, producer, streaming): self.producerStreaming = streaming def _produce(): - d = self.producer.resumeProducing() + if not self.producer: + # we've been unregistered + return + # some implementations of IProducer (for example, FileSender) + # don't return a deferred. + d = maybeDeferred(self.producer.resumeProducing) d.addCallback(lambda x: self._reactor.callLater(0.1, _produce)) if not streaming: From 52c7a51cfc568ed61d800df99dcca25dc3b5fe3e Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 31 Aug 2021 10:09:58 +0100 Subject: [PATCH 595/619] Merge pull request from GHSA-3x4c-pq33-4w3q * Add some tests to characterise the problem Some failing. Current states: RoomsMemberListTestCase test_get_member_list ... [OK] test_get_member_list_mixed_memberships ... [OK] test_get_member_list_no_permission ... [OK] test_get_member_list_no_permission_former_member ... [OK] test_get_member_list_no_permission_former_member_with_at_token ... [FAIL] test_get_member_list_no_room ... [OK] test_get_member_list_no_permission_with_at_token ... [FAIL] * Correct the tests * Check user is/was member before divulging room membership * Pull out only the 1 membership event we want. * Update tests/rest/client/v1/test_rooms.py Co-authored-by: Erik Johnston * Fixup tests (following apply review suggestion) Co-authored-by: Erik Johnston --- synapse/handlers/message.py | 23 ++++++-- tests/rest/client/v1/test_rooms.py | 84 +++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 8a0024ce84..101a29c6d3 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -183,20 +183,37 @@ async def get_state_events( if not last_events: raise NotFoundError("Can't find event for token %s" % (at_token,)) + last_event = last_events[0] + + # check whether the user is in the room at that time to determine + # whether they should be treated as peeking. + state_map = await self.state_store.get_state_for_event( + last_event.event_id, + StateFilter.from_types([(EventTypes.Member, user_id)]), + ) + + joined = False + membership_event = state_map.get((EventTypes.Member, user_id)) + if membership_event: + joined = membership_event.membership == Membership.JOIN + + is_peeking = not joined visible_events = await filter_events_for_client( self.storage, user_id, last_events, filter_send_to_client=False, + is_peeking=is_peeking, ) - event = last_events[0] if visible_events: room_state_events = await self.state_store.get_state_for_events( - [event.event_id], state_filter=state_filter + [last_event.event_id], state_filter=state_filter ) - room_state: Mapping[Any, EventBase] = room_state_events[event.event_id] + room_state: Mapping[Any, EventBase] = room_state_events[ + last_event.event_id + ] else: raise AuthError( 403, diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 0c9cbb9aff..50100a5ae4 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -29,7 +29,7 @@ from synapse.api.errors import HttpResponseException from synapse.handlers.pagination import PurgeStatus from synapse.rest import admin -from synapse.rest.client import account, directory, login, profile, room +from synapse.rest.client import account, directory, login, profile, room, sync from synapse.types import JsonDict, RoomAlias, UserID, create_requester from synapse.util.stringutils import random_string @@ -381,6 +381,8 @@ def test_leave_permissions(self): class RoomsMemberListTestCase(RoomBase): """Tests /rooms/$room_id/members/list REST events.""" + servlets = RoomBase.servlets + [sync.register_servlets] + user_id = "@sid1:red" def test_get_member_list(self): @@ -397,6 +399,86 @@ def test_get_member_list_no_permission(self): channel = self.make_request("GET", "/rooms/%s/members" % room_id) self.assertEquals(403, channel.code, msg=channel.result["body"]) + def test_get_member_list_no_permission_with_at_token(self): + """ + Tests that a stranger to the room cannot get the member list + (in the case that they use an at token). + """ + room_id = self.helper.create_room_as("@someone.else:red") + + # first sync to get an at token + channel = self.make_request("GET", "/sync") + self.assertEquals(200, channel.code) + sync_token = channel.json_body["next_batch"] + + # check that permission is denied for @sid1:red to get the + # memberships of @someone.else:red's room. + channel = self.make_request( + "GET", + f"/rooms/{room_id}/members?at={sync_token}", + ) + self.assertEquals(403, channel.code, msg=channel.result["body"]) + + def test_get_member_list_no_permission_former_member(self): + """ + Tests that a former member of the room can not get the member list. + """ + # create a room, invite the user and the user joins + room_id = self.helper.create_room_as("@alice:red") + self.helper.invite(room_id, "@alice:red", self.user_id) + self.helper.join(room_id, self.user_id) + + # check that the user can see the member list to start with + channel = self.make_request("GET", "/rooms/%s/members" % room_id) + self.assertEquals(200, channel.code, msg=channel.result["body"]) + + # ban the user + self.helper.change_membership(room_id, "@alice:red", self.user_id, "ban") + + # check the user can no longer see the member list + channel = self.make_request("GET", "/rooms/%s/members" % room_id) + self.assertEquals(403, channel.code, msg=channel.result["body"]) + + def test_get_member_list_no_permission_former_member_with_at_token(self): + """ + Tests that a former member of the room can not get the member list + (in the case that they use an at token). + """ + # create a room, invite the user and the user joins + room_id = self.helper.create_room_as("@alice:red") + self.helper.invite(room_id, "@alice:red", self.user_id) + self.helper.join(room_id, self.user_id) + + # sync to get an at token + channel = self.make_request("GET", "/sync") + self.assertEquals(200, channel.code) + sync_token = channel.json_body["next_batch"] + + # check that the user can see the member list to start with + channel = self.make_request( + "GET", "/rooms/%s/members?at=%s" % (room_id, sync_token) + ) + self.assertEquals(200, channel.code, msg=channel.result["body"]) + + # ban the user (Note: the user is actually allowed to see this event and + # state so that they know they're banned!) + self.helper.change_membership(room_id, "@alice:red", self.user_id, "ban") + + # invite a third user and let them join + self.helper.invite(room_id, "@alice:red", "@bob:red") + self.helper.join(room_id, "@bob:red") + + # now, with the original user, sync again to get a new at token + channel = self.make_request("GET", "/sync") + self.assertEquals(200, channel.code) + sync_token = channel.json_body["next_batch"] + + # check the user can no longer see the updated member list + channel = self.make_request( + "GET", "/rooms/%s/members?at=%s" % (room_id, sync_token) + ) + self.assertEquals(403, channel.code, msg=channel.result["body"]) + def test_get_member_list_mixed_memberships(self): room_creator = "@some_other_guy:red" room_id = self.helper.create_room_as(room_creator) From cb35df940a828bc40b96daed997b5ad4c7842fd3 Mon Sep 17 00:00:00 2001 From: reivilibre <38398653+reivilibre@users.noreply.github.com> Date: Tue, 31 Aug 2021 11:24:09 +0100 Subject: [PATCH 596/619] Merge pull request from GHSA-jj53-8fmw-f2w2 --- synapse/groups/groups_server.py | 18 ++++++++-- tests/rest/client/v2_alpha/test_groups.py | 43 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/rest/client/v2_alpha/test_groups.py diff --git a/synapse/groups/groups_server.py b/synapse/groups/groups_server.py index 3dc55ab861..d6b75ac27f 100644 --- a/synapse/groups/groups_server.py +++ b/synapse/groups/groups_server.py @@ -332,6 +332,13 @@ async def get_rooms_in_group( requester_user_id, group_id ) + # Note! room_results["is_public"] is about whether the room is considered + # public from the group's point of view. (i.e. whether non-group members + # should be able to see the room is in the group). + # This is not the same as whether the room itself is public (in the sense + # of being visible in the room directory). + # As such, room_results["is_public"] itself is not sufficient to determine + # whether any given user is permitted to see the room's metadata. room_results = await self.store.get_rooms_in_group( group_id, include_private=is_user_in_group ) @@ -341,8 +348,15 @@ async def get_rooms_in_group( room_id = room_result["room_id"] joined_users = await self.store.get_users_in_room(room_id) + + # check the user is actually allowed to see the room before showing it to them + allow_private = requester_user_id in joined_users + entry = await self.room_list_handler.generate_room_entry( - room_id, len(joined_users), with_alias=False, allow_private=True + room_id, + len(joined_users), + with_alias=False, + allow_private=allow_private, ) if not entry: @@ -354,7 +368,7 @@ async def get_rooms_in_group( chunk.sort(key=lambda e: -e["num_joined_members"]) - return {"chunk": chunk, "total_room_count_estimate": len(room_results)} + return {"chunk": chunk, "total_room_count_estimate": len(chunk)} class GroupsServerHandler(GroupsServerWorkerHandler): diff --git a/tests/rest/client/v2_alpha/test_groups.py b/tests/rest/client/v2_alpha/test_groups.py new file mode 100644 index 0000000000..bfa9336baa --- /dev/null +++ b/tests/rest/client/v2_alpha/test_groups.py @@ -0,0 +1,43 @@ +from synapse.rest.client.v1 import room +from synapse.rest.client.v2_alpha import groups + +from tests import unittest +from tests.unittest import override_config + + +class GroupsTestCase(unittest.HomeserverTestCase): + user_id = "@alice:test" + room_creator_user_id = "@bob:test" + + servlets = [room.register_servlets, groups.register_servlets] + + @override_config({"enable_group_creation": True}) + def test_rooms_limited_by_visibility(self): + group_id = "+spqr:test" + + # Alice creates a group + channel = self.make_request("POST", "/create_group", {"localpart": "spqr"}) + self.assertEquals(channel.code, 200, msg=channel.text_body) + self.assertEquals(channel.json_body, {"group_id": group_id}) + + # Bob creates a private room + room_id = self.helper.create_room_as(self.room_creator_user_id, is_public=False) + self.helper.auth_user_id = self.room_creator_user_id + self.helper.send_state( + room_id, "m.room.name", {"name": "bob's secret room"}, tok=None + ) + self.helper.auth_user_id = self.user_id + + # Alice adds the room to her group. + channel = self.make_request( + "PUT", f"/groups/{group_id}/admin/rooms/{room_id}", {} + ) + self.assertEquals(channel.code, 200, msg=channel.text_body) + self.assertEquals(channel.json_body, {}) + + # Alice now tries to retrieve the room list of the space. + channel = self.make_request("GET", f"/groups/{group_id}/rooms") + self.assertEquals(channel.code, 200, msg=channel.text_body) + self.assertEquals( + channel.json_body, {"chunk": [], "total_room_count_estimate": 0} + ) From 8c26f16c76b475e0ace7b58920d90368b180454c Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:56:22 +0100 Subject: [PATCH 597/619] Fix up unit tests (#10723) These were broken in an incorrect merge of GHSA-jj53-8fmw-f2w2 (cb35df9) --- changelog.d/10723.bugfix | 1 + tests/rest/client/v2_alpha/test_groups.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changelog.d/10723.bugfix diff --git a/changelog.d/10723.bugfix b/changelog.d/10723.bugfix new file mode 100644 index 0000000000..e6ffdc9512 --- /dev/null +++ b/changelog.d/10723.bugfix @@ -0,0 +1 @@ +Fix unauthorised exposure of room metadata to communities. diff --git a/tests/rest/client/v2_alpha/test_groups.py b/tests/rest/client/v2_alpha/test_groups.py index bfa9336baa..ad0425ae65 100644 --- a/tests/rest/client/v2_alpha/test_groups.py +++ b/tests/rest/client/v2_alpha/test_groups.py @@ -1,5 +1,18 @@ -from synapse.rest.client.v1 import room -from synapse.rest.client.v2_alpha import groups +# Copyright 2021 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.rest.client import groups, room from tests import unittest from tests.unittest import override_config From a4c8a2f08b735266fbbe2f259e640f00dc5e3a00 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 31 Aug 2021 13:42:51 +0100 Subject: [PATCH 598/619] 1.41.1 --- CHANGES.md | 32 ++++++++++++++++++++++++++++++++ debian/changelog | 6 ++++++ synapse/__init__.py | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f8da8771aa..fab27b874e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,35 @@ +Synapse 1.41.1 (2021-08-31) +=========================== + +Due to the two security issues highlighted below, server administrators are encouraged to update Synapse. We are not aware of these vulnerabilities being exploited in the wild. + +Security advisory +----------------- + +The following issues are fixed in v1.41.1. + +- **[GHSA-3x4c-pq33-4w3q](https://github.com/matrix-org/synapse/security/advisories/GHSA-3x4c-pq33-4w3q) / [CVE-2021-39164](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-39164): Enumerating a private room's list of members and their display names.** + + If an unauthorized user both knows the Room ID of a private room *and* that room's history visibility is set to `shared`, then they may be able to enumerate the room's members, including their display names. + + The unauthorized user must be on the same homeserver as a user who is a member of the target room. + + Fixed by [52c7a51cf](https://github.com/matrix-org/synapse/commit/52c7a51cf). + +- **[GHSA-jj53-8fmw-f2w2](https://github.com/matrix-org/synapse/security/advisories/GHSA-jj53-8fmw-f2w2) / [CVE-2021-39163](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-39163): Disclosing a private room's name, avatar, topic, and number of members.** + + If an unauthorized user knows the Room ID of a private room, then its name, avatar, topic, and number of members may be disclosed through Group / Community features. + + The unauthorized user must be on the same homeserver as a user who is a member of the target room, and their homeserver must allow non-administrators to create groups (`enable_group_creation` in the Synapse configuration; off by default). + + Fixed by [cb35df940a](https://github.com/matrix-org/synapse/commit/cb35df940a), [\#10723](https://github.com/matrix-org/synapse/issues/10723). + +Bugfixes +-------- + +- Fix a regression introduced in Synapse 1.41 which broke email transmission on systems using older versions of the Twisted library. ([\#10713](https://github.com/matrix-org/synapse/issues/10713)) + + Synapse 1.41.0 (2021-08-24) =========================== diff --git a/debian/changelog b/debian/changelog index 4da4bc018c..5f7a795b6e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +matrix-synapse-py3 (1.41.1) stable; urgency=high + + * New synapse release 1.41.1. + + -- Synapse Packaging team Tue, 31 Aug 2021 12:59:10 +0100 + matrix-synapse-py3 (1.41.0) stable; urgency=medium * New synapse release 1.41.0. diff --git a/synapse/__init__.py b/synapse/__init__.py index ef3770262e..06d80f79b3 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -47,7 +47,7 @@ except ImportError: pass -__version__ = "1.41.0" +__version__ = "1.41.1" if bool(os.environ.get("SYNAPSE_TEST_PATCH_LOG_CONTEXTS", False)): # We import here so that we don't have to install a bunch of deps when From b7814024db033f5a9caba136cc1c803b4bbf9fe7 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 1 Sep 2021 13:40:15 +0100 Subject: [PATCH 599/619] Add buildkite pipeline back --- .buildkite/docker-compose-env | 13 + .buildkite/docker-compose.yaml | 23 + .buildkite/pipeline.yml | 497 +++++++++++++++++++++ .buildkite/postgres-config.yaml | 19 + .buildkite/scripts/postgres_exec.py | 31 ++ .buildkite/scripts/test_old_deps.sh | 16 + .buildkite/scripts/test_synapse_port_db.sh | 57 +++ .buildkite/sqlite-config.yaml | 16 + .buildkite/test_db.db | Bin 0 -> 19296256 bytes .buildkite/worker-blacklist | 10 + 10 files changed, 682 insertions(+) create mode 100644 .buildkite/docker-compose-env create mode 100644 .buildkite/docker-compose.yaml create mode 100644 .buildkite/pipeline.yml create mode 100644 .buildkite/postgres-config.yaml create mode 100755 .buildkite/scripts/postgres_exec.py create mode 100755 .buildkite/scripts/test_old_deps.sh create mode 100755 .buildkite/scripts/test_synapse_port_db.sh create mode 100644 .buildkite/sqlite-config.yaml create mode 100644 .buildkite/test_db.db create mode 100644 .buildkite/worker-blacklist diff --git a/.buildkite/docker-compose-env b/.buildkite/docker-compose-env new file mode 100644 index 0000000000..85b102d07f --- /dev/null +++ b/.buildkite/docker-compose-env @@ -0,0 +1,13 @@ +CI +BUILDKITE +BUILDKITE_BUILD_NUMBER +BUILDKITE_BRANCH +BUILDKITE_BUILD_NUMBER +BUILDKITE_JOB_ID +BUILDKITE_BUILD_URL +BUILDKITE_PROJECT_SLUG +BUILDKITE_COMMIT +BUILDKITE_PULL_REQUEST +BUILDKITE_TAG +CODECOV_TOKEN +TRIAL_FLAGS diff --git a/.buildkite/docker-compose.yaml b/.buildkite/docker-compose.yaml new file mode 100644 index 0000000000..73d5ccdd5e --- /dev/null +++ b/.buildkite/docker-compose.yaml @@ -0,0 +1,23 @@ +version: '3.1' + +services: + + postgres: + image: postgres:${POSTGRES_VERSION?} + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8" + command: -c fsync=off + + testenv: + image: python:${PYTHON_VERSION?} + depends_on: + - postgres + env_file: docker-compose-env + environment: + SYNAPSE_POSTGRES_HOST: postgres + SYNAPSE_POSTGRES_USER: postgres + SYNAPSE_POSTGRES_PASSWORD: postgres + working_dir: /src + volumes: + - ${BUILDKITE_BUILD_CHECKOUT_PATH}:/src diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml new file mode 100644 index 0000000000..a99d068e96 --- /dev/null +++ b/.buildkite/pipeline.yml @@ -0,0 +1,497 @@ +# This is just a dummy entry (the `x-yaml-aliases` key is not an official pipeline key, and will be ignored by BuildKite) +# that we use only to store YAML anchors (`&xxx`), that we plan to use and reference later in the YAML file (using `*xxx`) +# without having to copy/paste the same values over and over. +# Note: keys like `agent`, `env`, … used here are totally arbitrary; the only point is to define various separate `&xxx` anchors there. +# +x-yaml-aliases: + commands: + - &trial_setup | + # Install additional packages that are not part of buildpack-deps / python images. + apt-get update && apt-get install -y xmlsec1 + python -m pip install tox + + retry: &retry_setup + automatic: + - exit_status: -1 + limit: 2 + - exit_status: 2 + limit: 2 + +env: + COVERALLS_REPO_TOKEN: wsJWOby6j0uCYFiCes3r0XauxO27mx8lD + +steps: + - label: "\U0001F9F9 Check Style" + command: + - "python -m pip install tox" + - "tox -e check_codestyle" + plugins: + - docker#v3.7.0: + image: "python:3.6" + mount-buildkite-agent: false + + - label: "\U0001F9F9 packaging" + command: + - "python -m pip install tox" + - "tox -e packaging" + plugins: + - docker#v3.7.0: + image: "python:3.6" + mount-buildkite-agent: false + + - label: "\U0001F9F9 isort" + command: + - "python -m pip install tox" + - "tox -e check_isort" + plugins: + - docker#v3.7.0: + image: "python:3.6" + mount-buildkite-agent: false + + - label: "\U0001F9F9 check-sample-config" + command: + - "python -m pip install tox" + - "tox -e check-sampleconfig" + plugins: + - docker#v3.7.0: + image: "python:3.6" + mount-buildkite-agent: false + + - label: "\U0001F5A5 check unix line-endings" + command: + - "scripts-dev/check_line_terminators.sh" + plugins: + - docker#v3.7.0: + image: "python:3.6" + mount-buildkite-agent: false + + - label: ":mypy: mypy" + command: + - "python -m pip install tox" + - "tox -e mypy" + plugins: + - docker#v3.7.0: + image: "python:3.7" + mount-buildkite-agent: false + + - label: ":package: build distribution files" + branches: "release-*" + agents: + queue: ephemeral-small + command: + - python setup.py sdist bdist_wheel + plugins: + - docker#v3.7.0: + image: "python:3.7" + mount-buildkite-agent: false + - artifacts#v1.3.0: + upload: + - dist/* + + - wait + + ################################################################################ + # + # Twisted `trial` tests + # + # Our Intent is to test: + # - All supported Python versions (using SQLite) with current dependencies + # - The oldest and newest supported pairings of Python and PostgreSQL + # + # We also test two special cases: + # - The newest supported Python, without any optional dependencies + # - The oldest supported Python, with its oldest supported dependency versions + # + ################################################################################ + + # -- Special Case: Oldest Python w/ Oldest Deps + + # anoa: I've commented this out for DINUM as it was breaking on the 1.31.0 merge + # and it was taking way too long to solve. DINUM aren't even using Python 3.6 + # anyways. +# - label: ":python: 3.6 (Old Deps)" +# command: +# - ".buildkite/scripts/test_old_deps.sh" +# env: +# TRIAL_FLAGS: "-j 2" +# plugins: +# - docker#v3.7.0: +# # We use bionic to get an old python (3.6.5) and sqlite (3.22) +# image: "ubuntu:bionic" +# workdir: "/src" +# mount-buildkite-agent: false +# propagate-environment: true +# - artifacts#v1.3.0: +# upload: [ "_trial_temp/*/*.log" ] +# retry: *retry_setup + + # -- Special Case: Newest Python w/o Optional Deps + + - label: ":python: 3.9 (No Extras)" + command: + - *trial_setup + - "tox -e py39-noextras,combine" + env: + TRIAL_FLAGS: "-j 2" + plugins: + - docker#v3.7.0: + image: "python:3.9" + workdir: "/src" + mount-buildkite-agent: false + propagate-environment: true + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + # -- All Supported Python Versions (SQLite) + + - label: ":python: 3.6" + command: + - *trial_setup + - "tox -e py36,combine" + env: + TRIAL_FLAGS: "-j 2" + plugins: + - docker#v3.7.0: + image: "python:3.6" + workdir: "/src" + mount-buildkite-agent: false + propagate-environment: true + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + - label: ":python: 3.7" + command: + - *trial_setup + - "tox -e py37,combine" + env: + TRIAL_FLAGS: "-j 2" + plugins: + - docker#v3.7.0: + image: "python:3.7" + workdir: "/src" + mount-buildkite-agent: false + propagate-environment: true + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + - label: ":python: 3.8" + command: + - *trial_setup + - "tox -e py38,combine" + env: + TRIAL_FLAGS: "-j 2" + plugins: + - docker#v3.7.0: + image: "python:3.8" + workdir: "/src" + mount-buildkite-agent: false + propagate-environment: true + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + - label: ":python: 3.9" + command: + - *trial_setup + - "tox -e py39,combine" + env: + TRIAL_FLAGS: "-j 2" + plugins: + - docker#v3.7.0: + image: "python:3.9" + workdir: "/src" + mount-buildkite-agent: false + propagate-environment: true + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + # -- Oldest and Newest Supported Python and Postgres Pairings + + - label: ":python: 3.6 :postgres: 9.6" + agents: + queue: "medium" + env: + TRIAL_FLAGS: "-j 8" + PYTHON_VERSION: "3.6" + POSTGRES_VERSION: "9.6" + command: + - *trial_setup + - "python -m tox -e py36-postgres,combine" + plugins: + - matrix-org/download#v1.1.0: + urls: + - https://raw.githubusercontent.com/matrix-org/pipelines/master/synapse/docker-compose.yaml + - https://raw.githubusercontent.com/matrix-org/pipelines/master/synapse/docker-compose-env + - docker-compose#v3.7.0: + run: testenv + config: + - /tmp/download-${BUILDKITE_BUILD_ID}/docker-compose.yaml + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + - label: ":python: 3.9 :postgres: 13" + agents: + queue: "medium" + env: + TRIAL_FLAGS: "-j 8" + PYTHON_VERSION: "3.9" + POSTGRES_VERSION: "13" + command: + - *trial_setup + - "python -m tox -e py39-postgres,combine" + plugins: + - matrix-org/download#v1.1.0: + urls: + - https://raw.githubusercontent.com/matrix-org/pipelines/master/synapse/docker-compose.yaml + - https://raw.githubusercontent.com/matrix-org/pipelines/master/synapse/docker-compose-env + - docker-compose#v3.7.0: + run: testenv + config: + - /tmp/download-${BUILDKITE_BUILD_ID}/docker-compose.yaml + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + # -- Experimentally test against PyPy + # Only runs when the build message includes the string "pypy" + # This step is allowed to fail + + - label: ":python: PyPy3.6" + if: "build.message =~ /pypy/i || build.branch =~ /pypy/i" + soft_fail: true + command: + # No *trial_setup due to docker-library/pypy#52 + - "apt-get update && apt-get install -y xmlsec1" + - "pypy -m pip install tox" + + - "tox -e pypy36,combine" + env: + TRIAL_FLAGS: "-j 2" + plugins: + - docker#v3.7.0: + image: "pypy:3.6" + workdir: "/src" + mount-buildkite-agent: false + propagate-environment: true + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + retry: *retry_setup + + + ################################################################################ + # + # Sytest + # + # Our tests have three dimensions: + # 1. Topology (Monolith, Workers, Workers w/ Redis) + # 2. Database (SQLite, PostgreSQL) + # 3. Python Version + # + # Tests can run against either a single or multiple PostgreSQL databases. + # This is configured by setting `MULTI_POSTGRES=1` in the environment. + # + # We mostly care about testing each topology. + # For DINSIC specifically, we currently test across one Linux distribution, + # Debian buster (10), which has Python 3.7 and Postgres 11 + # + # TODO: this leaves us without sytests for Postgres 9.6. How much do we care + # about that? + # + # Our intent is to test: + # - Monolith: + # - Older Distro + SQLite + # - Older Python + Older PostgreSQL + # - Newer Python + Newer PostgreSQL + # - Workers: + # - Older Python + Older PostgreSQL (MULTI_POSTGRES) + # - Newer Python + Newer PostgreSQL (MULTI_POSTGRES) + # - Workers w/ Redis: + # - Newer Python + Newer PostgreSQL + # + ################################################################################ + + - label: "SyTest Monolith :postgres::debian: 10" + agents: + queue: "medium" + env: + POSTGRES: "1" + command: + - "bash .buildkite/merge_base_branch.sh" + - "bash /bootstrap.sh synapse" + plugins: + - docker#v3.7.0: + image: "matrixdotorg/sytest-synapse:dinsic" + propagate-environment: true + always-pull: true + workdir: "/src" + entrypoint: "/bin/sh" + init: false + shell: ["-x", "-c"] + mount-buildkite-agent: false + volumes: ["./logs:/logs"] + - artifacts#v1.3.0: + upload: [ "logs/**/*.log", "logs/**/*.log.*", "logs/results.tap" ] + - matrix-org/annotate: + path: "logs/annotate.md" + style: "error" + retry: *retry_setup + + - label: "SyTest Workers :postgres::debian: 10" + agents: + queue: "xlarge" + env: + MULTI_POSTGRES: "1" # Test with split out databases + POSTGRES: "1" + WORKERS: "1" + BLACKLIST: "synapse-blacklist-with-workers" + command: + - "bash .buildkite/merge_base_branch.sh" + - "bash -c 'cat /src/sytest-blacklist /src/.buildkite/worker-blacklist > /src/synapse-blacklist-with-workers'" + - "bash /bootstrap.sh synapse" + plugins: + - docker#v3.7.0: + image: "matrixdotorg/sytest-synapse:dinsic" + propagate-environment: true + always-pull: true + workdir: "/src" + entrypoint: "/bin/sh" + init: false + shell: ["-x", "-c"] + mount-buildkite-agent: false + volumes: ["./logs:/logs"] + - artifacts#v1.3.0: + upload: [ "logs/**/*.log", "logs/**/*.log.*", "logs/results.tap" ] + - matrix-org/annotate: + path: "logs/annotate.md" + style: "error" + retry: *retry_setup + + - label: "SyTest Workers :redis::postgres::debian: 10" + agents: + # this one seems to need a lot of memory. + queue: "xlarge" + env: + POSTGRES: "1" + WORKERS: "1" + REDIS: "1" + BLACKLIST: "synapse-blacklist-with-workers" + command: + - "bash .buildkite/merge_base_branch.sh" + - "bash -c 'cat /src/sytest-blacklist /src/.buildkite/worker-blacklist > /src/synapse-blacklist-with-workers'" + - "bash /bootstrap.sh synapse" + plugins: + - docker#v3.7.0: + image: "matrixdotorg/sytest-synapse:dinsic" + propagate-environment: true + always-pull: true + workdir: "/src" + entrypoint: "/bin/sh" + init: false + shell: ["-x", "-c"] + mount-buildkite-agent: false + volumes: ["./logs:/logs"] + - artifacts#v1.3.0: + upload: [ "logs/**/*.log", "logs/**/*.log.*", "logs/results.tap" ] + - matrix-org/annotate: + path: "logs/annotate.md" + style: "error" + retry: *retry_setup + + ################################################################################ + # + # synapse_port_db + # + # Tests the oldest and newest supported pairings of Python and PostgreSQL + # + ################################################################################ + + - label: "Port DB :python: 3.6 :postgres: 9.6" + agents: + queue: "medium" + env: + PYTHON_VERSION: "3.6" + POSTGRES_VERSION: "9.6" + command: + - "bash .buildkite/scripts/test_synapse_port_db.sh" + plugins: + - matrix-org/download#v1.1.0: + urls: + - https://raw.githubusercontent.com/matrix-org/synapse-dinsic/anoa/dinsic_release_1_31_0/.buildkite/docker-compose.yaml + - https://raw.githubusercontent.com/matrix-org/synapse-dinsic/anoa/dinsic_release_1_31_0/.buildkite/docker-compose-env + - docker-compose#v2.1.0: + run: testenv + config: + - /tmp/download-${BUILDKITE_BUILD_ID}/docker-compose.yaml + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + + - label: "Port DB :python: 3.9 :postgres: 13" + agents: + queue: "medium" + env: + PYTHON_VERSION: "3.9" + POSTGRES_VERSION: "13" + command: + - "bash .buildkite/scripts/test_synapse_port_db.sh" + plugins: + - matrix-org/download#v1.1.0: + urls: + - https://raw.githubusercontent.com/matrix-org/synapse-dinsic/anoa/dinsic_release_1_31_0/.buildkite/docker-compose.yaml + - https://raw.githubusercontent.com/matrix-org/synapse-dinsic/anoa/dinsic_release_1_31_0/.buildkite/docker-compose-env + - docker-compose#v2.1.0: + run: testenv + config: + - /tmp/download-${BUILDKITE_BUILD_ID}/docker-compose.yaml + - artifacts#v1.3.0: + upload: [ "_trial_temp/*/*.log" ] + +# - wait: ~ +# continue_on_failure: true +# +# - label: Trigger webhook +# command: "curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d \"payload[build_num]=$BUILDKITE_BUILD_NUMBER&payload[status]=done\"" + + ################################################################################ + # + # Complement Test Suite + # + ################################################################################ + + - command: + # Build a docker image from the checked out Synapse source + - "docker build -t matrixdotorg/synapse:latest -f docker/Dockerfile ." + # We use the complement:latest image to provide Complement's dependencies, but want + # to actually run against the latest version of Complement, so download it here. + # NOTE: We use the `anoa/knock_room_v7` branch here while knocking is still experimental on mainline. + # This branch essentially uses the stable identifiers for all knock-related room state so that things + # don't clash when rooms created on dinsic's Synapse potentially federate with mainline Synapse's in + # the future. + - "wget https://github.com/matrix-org/complement/archive/anoa/knock_room_v7.tar.gz" + - "tar -xzf knock_room_v7.tar.gz" + # Build a second docker image on top of the above image. This one sets up Synapse with a generated config file, + # signing and SSL keys so Synapse can run and federate + - "docker build -t complement-synapse -f complement-anoa-knock_room_v7/dockerfiles/Synapse.Dockerfile complement-anoa-knock_room_v7/dockerfiles" + # Finally, compile and run the tests. + - "cd complement-anoa-knock_room_v7" + - "COMPLEMENT_BASE_IMAGE=complement-synapse:latest go test -v -tags synapse_blacklist,msc2403 ./tests" + label: "\U0001F9EA Complement" + agents: + queue: "medium" + plugins: + - docker#v3.7.0: + # The dockerfile for this image is at https://github.com/matrix-org/complement/blob/master/dockerfiles/ComplementCIBuildkite.Dockerfile. + image: "matrixdotorg/complement:latest" + mount-buildkite-agent: false + # Complement needs to know if it is running under CI + environment: + - "CI=true" + publish: [ "8448:8448" ] + # Complement uses Docker so pass through the docker socket. This means Complement shares + # the hosts Docker. + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" \ No newline at end of file diff --git a/.buildkite/postgres-config.yaml b/.buildkite/postgres-config.yaml new file mode 100644 index 0000000000..f5a4aecd51 --- /dev/null +++ b/.buildkite/postgres-config.yaml @@ -0,0 +1,19 @@ +# Configuration file used for testing the 'synapse_port_db' script. +# Tells the script to connect to the postgresql database that will be available in the +# CI's Docker setup at the point where this file is considered. +server_name: "localhost:8800" + +signing_key_path: ".ci/test.signing.key" + +report_stats: false + +database: + name: "psycopg2" + args: + user: postgres + host: localhost + password: postgres + database: synapse + +# Suppress the key server warning. +trusted_key_servers: [] diff --git a/.buildkite/scripts/postgres_exec.py b/.buildkite/scripts/postgres_exec.py new file mode 100755 index 0000000000..0f39a336d5 --- /dev/null +++ b/.buildkite/scripts/postgres_exec.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# Copyright 2019 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import psycopg2 + +# a very simple replacment for `psql`, to make up for the lack of the postgres client +# libraries in the synapse docker image. + +# We use "postgres" as a database because it's bound to exist and the "synapse" one +# doesn't exist yet. +db_conn = psycopg2.connect( + user="postgres", host="localhost", password="postgres", dbname="postgres" +) +db_conn.autocommit = True +cur = db_conn.cursor() +for c in sys.argv[1:]: + cur.execute(c) diff --git a/.buildkite/scripts/test_old_deps.sh b/.buildkite/scripts/test_old_deps.sh new file mode 100755 index 0000000000..8b473936f8 --- /dev/null +++ b/.buildkite/scripts/test_old_deps.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# this script is run by GitHub Actions in a plain `bionic` container; it installs the +# minimal requirements for tox and hands over to the py3-old tox environment. + +set -ex + +apt-get update +apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt-dev xmlsec1 zlib1g-dev tox + +export LANG="C.UTF-8" + +# Prevent virtualenv from auto-updating pip to an incompatible version +export VIRTUALENV_NO_DOWNLOAD=1 + +exec tox -e py3-old,combine diff --git a/.buildkite/scripts/test_synapse_port_db.sh b/.buildkite/scripts/test_synapse_port_db.sh new file mode 100755 index 0000000000..2b4e5ec170 --- /dev/null +++ b/.buildkite/scripts/test_synapse_port_db.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# Test script for 'synapse_port_db'. +# - sets up synapse and deps +# - runs the port script on a prepopulated test sqlite db +# - also runs it against an new sqlite db + + +set -xe +cd `dirname $0`/../.. + +echo "--- Install dependencies" + +# Install dependencies for this test. +pip install psycopg2 coverage coverage-enable-subprocess + +# Install Synapse itself. This won't update any libraries. +pip install -e . + +echo "--- Generate the signing key" + +# Generate the server's signing key. +python -m synapse.app.homeserver --generate-keys -c .ci/sqlite-config.yaml + +echo "--- Prepare test database" + +# Make sure the SQLite3 database is using the latest schema and has no pending background update. +scripts-dev/update_database --database-config .ci/sqlite-config.yaml + +# Create the PostgreSQL database. +.ci/scripts/postgres_exec.py "CREATE DATABASE synapse" + +echo "+++ Run synapse_port_db against test database" +coverage run scripts/synapse_port_db --sqlite-database .ci/test_db.db --postgres-config .ci/postgres-config.yaml + +# We should be able to run twice against the same database. +echo "+++ Run synapse_port_db a second time" +coverage run scripts/synapse_port_db --sqlite-database .ci/test_db.db --postgres-config .ci/postgres-config.yaml + +##### + +# Now do the same again, on an empty database. + +echo "--- Prepare empty SQLite database" + +# we do this by deleting the sqlite db, and then doing the same again. +rm .ci/test_db.db + +scripts-dev/update_database --database-config .ci/sqlite-config.yaml + +# re-create the PostgreSQL database. +.ci/scripts/postgres_exec.py \ + "DROP DATABASE synapse" \ + "CREATE DATABASE synapse" + +echo "+++ Run synapse_port_db against empty database" +coverage run scripts/synapse_port_db --sqlite-database .ci/test_db.db --postgres-config .ci/postgres-config.yaml diff --git a/.buildkite/sqlite-config.yaml b/.buildkite/sqlite-config.yaml new file mode 100644 index 0000000000..3373743da3 --- /dev/null +++ b/.buildkite/sqlite-config.yaml @@ -0,0 +1,16 @@ +# Configuration file used for testing the 'synapse_port_db' script. +# Tells the 'update_database' script to connect to the test SQLite database to upgrade its +# schema and run background updates on it. +server_name: "localhost:8800" + +signing_key_path: ".ci/test.signing.key" + +report_stats: false + +database: + name: "sqlite3" + args: + database: ".ci/test_db.db" + +# Suppress the key server warning. +trusted_key_servers: [] diff --git a/.buildkite/test_db.db b/.buildkite/test_db.db new file mode 100644 index 0000000000000000000000000000000000000000..a0d9f16a7522c17f91ace4175a2234b3f6a93941 GIT binary patch literal 19296256 zcmeFa2fQQIb@;FHp1NjsX^({k-U7?+!fH`{Phw3#z4tD_@<KI+ECuhB84_rx`h z?ZmhQcViQqU^{UkZUN%p-ffI+Y!YJocV?tlqcz%iV8OrWg9so3hyWsh2p|H803v`0 zAOeU0B7g`W0DQzml-?n|QW}$9AmvMbDfznO-I7&FTyj(*5dTvARq;E-tKyhgCKigmD|$q9m#88# ziS`QrDEzkYVc|`}me4LdQ#dR5x!}`~75X3ohyWsh2p|H803xt6fjt^7d+{QT_CkfG zU2T#?CYi3atF0qcqkO)a%Z@XrR86u{>yXKMlcdOMnoJVqa;=*zR8ydvp>^j}T(*kQ zRW@t2N|I_3Es8mzTtA^`Q)Dw)$RsJUaircqq~NlnjDSdD%7gUdX0i)!J5vc zN>oTXjYIMc)l9QiPnK(GqMU4!MKVnrQIH#yyuxI6;V3(~j>;1mC}fJL(kCuF!eyuF z?Bv>Hh0GL)Bt>M&WVQ)~ZM2CdQEe5fOm4I`{obaG%QiCxi*>R|6{uFST}Tq`Rz6v% zGgaGrn9IJH(OAfoYh^0gt`-_?5~|pP`t7}7{j6%jp3dOB^SNw4W2+po&9v*uTD!#% z1q>yU$>YEwE<4ER$x}qSlxxz!lkIwjXpzZU6Sx(sIXc0E2f6I9PP2F+jh{+(=pqBp zbgkU3RFf)NWmK_)2aczy+@g8bd0e(}TAH-p{{39Gmr)q{2RKJ-n@XI0E|+bm)rJk1 zZa15>Vuh@vpqTkWopy(Fpp9vr4B06_+m~tJG#aR9nn>qK+NH`&1D$_1_~lTwOwt(2 z;IyMOX}=?rWCzk`_?@*6s-V>@TsS;I^Q$Kr52afPs&IIo%f5n9DCDZZGFc+~RI*yD zjvAAK!$hkMj!$dvISYCVt<5CSgxtYxYK()OdM1~x**FEMB*_-RG>zg6E<4AZ8B%K1 zDk-W}tCAU}B-GFeGleF&bFJB@^Vz!>oM&i~As8o5rpYQ*YpUt9hfW7p^x0~=1eLAS zTYYHgN(GACDv;~xrD)Z2Pvf%Pv}(SP$&l6LkS?ecnA+1S;OmF>j1pwPCCfyL1b05h z;b`r-Q=x;=32s$oPRxy+Aba(~ur6nx0M%{p zGy>B%GbDW=!xW}H?009MG)i#PM9jF7EtFgI@KVi?bnlxxaa0<*{nne6F6Q17rjTVE z|E%33cd55i$0hMt&g5vlUff%amf z?AIHjOn{$LB+2;DxzbT<(N&@c!sM26z~4ic-76WT!Bm{=LA|Rm9u_i;i=HOtvUPMZ zXpe`Xk*veu1_bFMo+;w8_4GN$eK-5xddjB@x$L9Nd2-zC7-PZ67<^|4hH0fCaY+1Z z{zf}fFqSgoIXD_oC`0c_4!h$K9+yqf8En^Iw+sl$N{MNY^Jk%}zX*N0eCN)L;1L

9n*de3~RJbs|jOnvXyU=WF7pq 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%0DXqh|OQm#Xj5T8jU*V zon7|1ZrkGMv}1}FJ9QQQM~Od`GwMYz=6cQbn9P$o^Hg6f z;~I4?Br}tqgx8$1kMd%dzbbzf7lNXmTa@_Gy-Iy~E431As&W{BLQfW(Spl86yFOk~k zwa?>+#ShDwPaEfLeyeNJ>rVG{j|_Q2rLpVvs@GMorET76hif{XneiAyJagJYTd?o1?9b-RT?=+k zC^VmR$2@kA(V1fT?TqXy8Ll2`f6ja^mJAOpdZ(@Z(V*KA@3Zj_mHGqzM`=#anTOLh zv(uE0_fL0wEN*i;!t$IEzbt+k=jy#V^U2WE=-k}g)JUHrG?!d-runUyWK1%K&wTl2 zX~<(~|PDP!l1-~xJBh2@h2blYqd+>J$Z)0v@ zKFM6qe2lq*xtO_-IiER~*~QE(KUUyR;{3 z+q8P^ChdCdaoQubhiJ80xt7xWL-QBStD46(4{N@o`MTySnmaYOYHrqiLUWDg3e7&v z1)4pY4`^mJ3C)xys5w*P)eLIvnr_YMn(dkv%~nmlrcQH=<_OIijanmBzpeh8`cLZL zt6x_CT>TUElj_IR52?SUzEAyS^%vBiQ-4Z*z4~hPW$KTpKd3%ey{JyBW9muuxcUtB zuzEnio;{ZaK>)k~@uRL`n@sCrcOJ=Hf= z_p0tv-LAStb(88^)m5rXR2QnwQ=P4vS0z;u)r9IS)tJhya;SP$U8<8+Z7RKLlWM){ zIMtD=L+}Y!uA-FxQ2s^vs`3@(FLCqswDNJ~!^-a{zpng>@=oQg%A1v+P+p_FLb*?Q zfpU-X1Ik%tLOG=jD$m5P)EHFSmEFqImD`mq%B{+JWu5XE%M>3`d{A+&Vo{M+#1xZ?am5*m zVa0&LtTPW_Y+_X5Wwe5iBgQ4Z%!X3`hB7g5q*&8_lSO%=y!;Io9MTQK0x%FM884w z>qPG-dLPlR5xtk_SBZXw=siTgO!RJ|cM<&((L0HLk?0qQ-a+(sqPG#fmFVY*evas8 ziQYo=GemDD`e~w{BKk?9Hxa#&=qHHYK=gW|*Acyz=*Nj(L-b=ruO|9YqE`{UlIRsg zFDH5#(MyS5LiA#y`-omd^dm$+O!PvTL?sq|h=ea7`aeWJNc4Q7=MmjYbPv%i(Q}EO zL-cH-yNP~)=q{p*L>Gw86P+VEOEg0?O*BO`Npyy2f@qv*jA)c-gy=NUDWYMblSD&A zCy25{gG2*F$BFuho<-D0^h}~ViJn1p2hlO2qeQ(#M~Ds+^$>Lv9U?kN)J1fFsFSFJ zXg^UqQ5(@dqE@24M0<#K6SWXE6EzX-B5EXhI?>aJo=Wr-q9+qQiRgBsokTl`wi9h5 z+Df#AXfx3!q6VURqT7gWCAx)ZBhk%7HxX?hT2FK%(G!VoAiAFD2}J9N))LhbJ)Y=s zM2{tU4AG;Bt|NLB(Ibi05IutE;Y1H3dMMFDh^{5NhA1M+5T%J~iE4Vyi7JT7 ziOPsdiAsoyiHe9)oKpXf=-Wj9N%SqEZxa0n(Z3V@8`1wG`UcUz68#I&KNI~E(btLo zk?3ngUnTknqQ58lJEFfO`WvFJ5dAgLmx=z0=u1RjB>GFDzaaW^qCX@00@3G*K1cMY zM1Ml`$3&kc`V7&hi9SX2Nuoa@`a_~m5Ph8J{}O$S=nsfKO7sz;4-@@9(TBLUhDPr{!g`dOm45d93%n~8p!=%r$$!<rj9 zoQ!cY%88eg5pL@+Cmv4RoD6X?$cc-S0ZyEpI5_F&#LkJ0lRi$Yob+;iQ?9CQb~T=sDTO$yQFb zaMH-hW==M7(!fbQCmT6Ak&_LatmotePU<+RLG$&e4G@Pip2vl51$%%p!IVUnsq?|}N5lbX0 z6?gx?lDU~;ZbrA@cm6w2EqaRi3-cgcT>}9i00e*l5C8%|00;m9AOHk_01){16VOR4 zqSJjoJd5q{`L;9{f8MX9sin15*(lj5I^7?O$F?--4V?zPzR73kFtqFSTiV)Md6Vjj z>c-8T{O=OV1&lj7c`?hX8yh<{NU)B*;BFZ^l_yGYR00e*l z5C8%|00;m9AOHk_01$W|6VRIVQt?i)_~VuDu5WE^F1Z!IsZ)$!4ye-Sc9K`#sil>F zv%eTm%F8>*P9kgNoth2B&p>cD@{4-$PVHnT>h{VzHR;<*+|%o++j9N?4xFdRi5^4O zqAA4v0Y4xB1b_e#00KY&2mk>f00e*l5C8%|fDq7rc8g@ExN=XswWI8r{0HzrK(_iG zE$#f535Yv!hkmsB9?h-oyxzR%YP^TH`W{WqW&Qu>X?!da`p*>F#+->4@B;!s00;m9 zAOHk_01yBIKmZ5;frE!YSfUn*cZQGRjz}tdt#S{xSyN(*Ql$TW*4}%oa4T5kXw`Rr_*dQ#;vVg zG4i_IJ9pH@;`nof3w1lQ4pIJql=y(evOoN5=)Ic*S}|)%4v2gIp9H-|<(~aNcnUcj z2@n7RKmZ5;0U!VbfB+Bx0zd!=0D<>00hvT3l1ez0h@}#dN}TKerz!M9^huO{ACrMY z2LeC<2mk>f00e*l5C8%|00;m9AaH;QY?ba5Rry+B`E3_c$*RmH;w?D#YG3~=yQxD= z?*B)(Qs^c05MIC!2mk>f00e*l5C8%|00;m9AOHk_01)^O6Idsaikii5tuJ}^UrSR< zYpeQ`>%`o)f37zazZ4+1?WT1iZrj`I4aJZD=eFIrbpQWr_}hPP{)Z0`k_Q4n00;m9 zAOHk_01yBIKmZ5;0U!Vb4g>-2&i}W_{r~?Y_y51m&Hqzt4n))t2M_=PKmZ5;0U!Vb zfB+Bx0zd!=00AKI-%5a+|A+qne{1f5UqAo|00AHX1b_e#00KY&2mk>f00a&U0U3@) zf?mhE<}UmVfHz3+z(ftP00AHX1b_e#00KY&2mk>f00e*l5C8)IIRdK7Po*f?S@l?a zhja7)7Dku*{@<(U*XZZy$M{k7 z>POw^G}M6%XcIaCG3Xd{I8q>r`8)F`=6B3X%=66C%>ObEF%K~JGGAhDWj@VZ&wP}* zg!vG&huOuXnJ6>C_!uwaV)_^pb28J)Y-KiLSKtQ(fB+Bx0zd!=00AHX1b_e#00RF3 z0&V{X;;h5T;;_TW;?P66h4`97 z$l|iKWU+4zSu7&5m|@5wPLstMTCy0_kcC}M7B&@GbSb%oD54;XpqwlQWn|GRC5w&Z z`F}5Y{{L_>E>j|OHdd9LEdB#!43Y!_KmZ5;0U!VbfB+Bx0zd!=00AIyuoF<(cFT8C ztgG7h09%^3N8$m0WHO#gZ|~^P>(@JbJNp(A1JRbn%;G}-n16IMVi=kkwV5X7!`4qM zo!i)Dx9F-^>FRayWH=O#>D(5tTjy}%|2=klqb|y(QvML@3kUgIl5Btt&$2;ZI;HEf z_E;UuyXhLTr)Bku8lzFXM?rk{ClV<(N&HTwldM1LOT<%Q&iU$FsC!C0uWnecv&RF| zI#aK4u-?$Hdq{aZBW$oH;A(aQmx_%7B^?O zO^Hvlu@v^sH|vk!zcaCPID#E3-A;WWPsQpmTfDl`jpaM*oDQB%eOUqx+2-sCXG67|D0R(eDE*tdLmi=1xte=I_C!UE%OK7=NrvP~EthXTe}HSRQ$Ai3T5dtz zR+25{2K*>=xPp~i)$<$cWf%?F+N>6C9Gp7iCpm2ROv zU*cKyy||)Mvb8=JH`|z79a+Gw*%TW{vq7#EN<{ntd?p-USgss_?0R|4mW|?bD)<); zmc&hcPQWYNP?h6@mqt=Tq3&5l#V*_{M%`E^Y^lg3v?z95@Q zhhzEcq+m5O>i&`}sAk5Cjmw#8ru9V$FI{%v9NV33QPiB$F3zqcelMTFlKxn#P{q=z zitDMbm3UdvhHKHWTNStA=B^>D%{D7)OdG`6lZwHSOUPWMSt?!mLnE80drOio-o)Fh z2nyM=K3@|X3#h`l$}FYPkZs5|DQY%s5brsZw711E7l)~@mXzP(O}M44(n=Pr$}V1) z(l0+{7Or@w3a9<0#u#7LHfQT_>u3>YcMy2^l*zIQe0~aLa52LzWB^|-7fxO)T2Qy- z4=LZ6=gwj)$H@^EvZ{}=!Eh$psPhMdN$gMqZrOML$F4cEo!MGNjRiAHkqj2{Txc8h znUVq^WLl5AEgXS-Ym|EoN!@*5_?Nt}lz&0RxRRwu9VY^!!!W#@gknTdq)g|f00e*l5C8)IHUixD|3v663jYT` zAOHk_01yBIKmZ5;0U!VbfB+Bx0zlxuh(Mo6dWvjkWH#j(9GQqmJ#(XhapPiaI^$eO zM`uLQeTU!ZKHVRS$F?--4V?zPzR73kFtqFSTUt9>OCA8w>$&-V5qgQj|G^Ik00AHX z1b_e#00KY&2mk>f00e*l5O@z0I9hUws@iBluK)iAh2D4%A0W661b_e#00KY&2mk>f z00e*l5C8%|00>kdppwW`x%&jT`~OSd%2L4`RsjJZ00e*l5C8%|00;m9AOHk_01yBI z?`Zf00e*l5C8%|00`_C0qFnl7bY+Q z0zd!=00AHX1b_e#00KY&2mk>f@SY|B_y51Avk#mH0zd!=00AHX1b_e#00KY&2mk>f zuwMkY{=W$Qjl%!I4+sDOAOHk_01yBIKmZ5;0U!VbfB+CU$Oyf00e*l5C8%|00;m9AaL*zkV=rsORN4wX%weZXo$I)c4|MVc|&7W zU#IF;UZeN}UJXAW00e*l5C8%|paOyHrZw`NwW4hNbbl-!+tQ>rbQ<*fCZD0h(5}~S zY3=OT9*GD1k;!-}y}hGDuXp!2tbJX5cIV*QU3GzYEX~H!b=!5j>Y@w2X?6hzxzMaX zl40w1H|pxr3kf#2a%-ARrMHs5@%j`S3$jUWOSP0cOHyv?tlOR4goK@KFWChTs z*z6YD(3*d-v;35?vkeS!Hd^iY+IY@-?IZ5dp0VMf9>jB&O{M%HLNUc+XuTtZZ-3K?eDdBIXpIJuUo_O^*}cSEhQ=E8Ulr|v-*-<^!gFou(ik9KRhs~{1-dR zPnoM+a=}v2QQFX9XNUUx`yIW#gDwTn*#p}UlqJmhDi!vXe>yPr_6-d7cm|9f*}v9T ze!_LTagBe^Gz6Q(g5|Y?fBG2i>2o@JUA@B=2@mT5u3Tj)b1i{L*jfJh!|Zf-ImY_> zM@`~?v9tV?>vnVV{~CHPg&sr~p`)0e;cv+R?Z2YEZ;7BgKC*RwmVH~O8o}K*ht)h`Nh2WlLBMn7k9O4eH45_M= zp$>kxzi7aL>;KQIe@vk_(C^UC(NpMQ^bK?u`YgI0U5+k5yHOH_&>6^uy3t8Uk2aun z2q7u+cji^*Mdn%N2h6vadzf398=0$^4>RX78D@&{F>c1noXRva^~|x%A&i24i+-Jc znf@vLIQ?DvUiuFDQ}oB^eRzano{rMv^ayRIjdUBmnby&V(`xPi)BZ*K8|@3)A88-b z-mkq=`x))E+Do+`)b7$Iw5)ba>(rXHo!YJ16SPNaX{}iEf12NGexZ3<^N8k~n!7ch z)7+rBLh~Wb*_xDQQnOPtsOiz1tTAX#)Euo@qmilqp?*#M5J`*kkzxVCQLU88zP$782 zS0)NU-Cx*3P^JkMf}(Q*`QS@0e`&lBeAw$R1i$Avs}TIwtgjHCIP<}ug@zpY;0w?Ev%e60_*#2D_?-JGTOl}fY+pY3)7Lgw z3&CHH_ZEULJ=;?VzUb{P1YHhGA-Lg>=0b4&hfRgx35#8YV6EO*2+IC;dLbzN+-ZfN z_`y^2!JoWz!Pdt*3c;ptwC96Ae&WiuLh!L4 zwHAV3_+v{Uc$>Pp5d3JWsSu36WGDn1f2J=4Pn_MB4?g>i?OO}M`-Zj@f*-%Wu@Jnv zWpg3eH?gS@JYh{kA$X*%z7Rb0@QwN4Gk3I}SO|XpHyaAUi<;IKf)`wHLLqqA33Y`a z{a9^2`1ISy>k7eJ?mWH_{Pfq3D+E8-a%>^E_pxINLFdUw7lQSNtt$l8mmO6I%F&Ve z;8UO4R#OPx`1lcp;NCAEUI=DQhvkDmkiB(iK6roo#3A|MeV_l@+CuPi`_|-xU%MiO z3c+(e%;baje#lD~f;CEQA&6eo6oT3jbs?yBsR}{aZhhHHn1eH&TRT7yBH;!L=(o%T;Kl7V|tNDkc0Rlh(2mk>f00e*l5C8%| z00;nq_bmZY&HMJys~f00e*l5O_}! zIE?QtH~%k2w^Hb7Ebs#YKmZ5;0U!VbfB+Bx0zd!=00AHX1pea$>c!HAEwbX*2{(04 zI>xQWp=8?a7`9J%t)uv^{uiFPy$58mq|DQwWAUA4Y-e!Kp z+`@c-8DQ4af1@9zKS8JIUTgwCAOHk_01yBIKmZ5;0U!VbfWSdZVAEQ;*rmktqZik@ z7VMr-Xg=wVdF&peGsVtqFP>M`>o={zwkmA9uhtguIz}85?RkYfbQNQO=fn zxOQk_H0}3Hn!4jD$3*XhXE@5YW%9P)uXPLrr=r%0RM)W8Iz2nmwP@zs(s|p5YG)Tr z-4h;npKY{n$m|bxPrCWG+Pv+9wc~7;*A<_LJ16EHUFpSCCe63iU|Y%?!FH zdb&e>!I0H#3|U9`w(5Mmx79|SNn6*zB%2OT*m})TOQfG~tIFHnx-#DerCh}EJ)?H9 zJLq8NBDR<(+}|}H8%;WR(KaY@raNkFY*%E&;|kkHJ#nkuHtp`>o62*hW3?m0i&GwU zpf|Z_nwnT}k4J5MQ(4Y*v^HYwaR)}Nnf@+sYIv~6FJQgGVJMFh=nrl8Do5epSum>oT<7tH83*V6ALG%!r>8DVqwnV ze zjqXEtqubGE&?nHxunGKt01yBIKmZ5;0U!VbfB+Bx0zd!=0D%KWKrRuBL>I3mi+yXj z1@$l@i|;dJ@eoZG4{FKc4h>n{rY4J9RWgZ4bcT}C9STmzC-4W%>0r$iN2q?m_CiZ9PLCJW-a<4YGGJ(2mNd2tLWJSc7PBO5C8%|00;m9 zAOHk_01yBIK;XSdph4;qi&mb!#{IdR<%;(Pi1w|?S+00z0PYp$ELXfQfcid@vt046 z0O}z+XSw1%0n~%qoaKsl1WVNWJgSwa|HU#~>y+qm ztZOIHKhR&$tLPQS00AHX1b_e#00KY&2mk>f00e-*e#kM|NfsB87WF}@`X`~lJ#D26h%osaJ~iWu01ofe1NKgy`M@@eH}#a{Vq)LpV`rGJq2NWL!0Qm;~{slPxDUv$R5 zokG@{wJK_iM)97b)Bf=YJHZCoq(2>w$9$=DlJ!S@iFhi^{at+v^@M5AVsu+{Zey3- zqN{FLuS+i^Se@JAb?Y2X{J+O;Z`9?}2?upnhuhL)8C=??Yt)5fskA>9V0|%vl((nO zZ0R<7>~7u0s6UlvlN%fMtjjtSH71jIZznmJAUhkzyGFvPbjlacq{ri#SP&o3G@BCG zL_J+{I0BZsdR>rBrNc1-tom_vS$puIEge>)E|X%DK77DD68WUcH>B8DTG!=t+AT&$ zQ8ek4ju$m17EfT~q(2ajg?xc{EX~fRNjeRgY^S28wN;$e6vK)7=L9|G-T_tc12Ayb|hS!QN_De^5V%#*;KrfP%bT{zMn->*O1+u zwJBqq`X%2J^_+)Uyzx{-;{|RQ- zWP5RIdx|)_xwygepo+2-X+)R0}D?NQX6f*UzM@yVY^ z;HH-3Xfhs;l2ZZpXTH*U>VNajcpO%j$AZsBTr*S2l8b?JsM5Cj;X8!sb<11o>hqD7*KXov`RZ!8w`98%HT}4u8YM@WZ_(mOd~W1U>FGGv ztg^|d5C4bDVb$%_!zC1|+U4Wn_*b>5FO=M+tF?kZxF{c&HRAGt9X77uu%Nf!-%oG3 zsweC!?n#*d;!^%c12qpro2LRw+H$c}4a# ze1X2LQ2p@bFg~+}lJQI;U-T16b~aZMcw49kc}#iReT6-_Dp3}1J=t2cx?w(hs-mVH zdvbU_qxtcX4Cbw*zPExCT!vOmzc}eE%ATUA*@n~3U2PO?%TXv>L4AjpWjw#QOV*`T z{LAP@9sl6@7C2*$&z`KPvEs{G3&D}dj7P!&-|}fDUrKV-w8~cM+a+|WSQS;#Di-Ax zl$>SOWKY6pXzbz^K^JirREd}R)_(byuas3HDefIKWJj{w6*VWFB;I2pm9fe`RaR3E zl;mm^tL18lDeP|roDHYKgZ|GEk+fdCKy0zd!=00AHX1b_e#00KY&2mpZtMgX0tkch>aF4Y})hF`{o zRZl8EKvC-Zxv;HAEHOm_s}=dz349d6@XjNP3T&56}kjnh|WW2qj{7>5j26$LSy)=fezG*y3om} z4e8M)v>qLYjzotbEs`UO`3LhC=2hku{C>gbn5UV?nTMJ0FkffB!raN+%G}I+g1Lsd zg4xGh!0f@_9Gqnm%oG!3&SbpIAY*5`nbVo=ObfG>sb}h#W0)hDHN@F>!4P%@0zd!= z00AHX1b_e#00KY&2mpb#d#}fzgI_yv-C0-QdaUG@59f?D}Q%U1najh!hR}0deQ%9?@R#ODDM5g zySlHPm>fz70ofrWCQc&xb~u6$+3_Xc@sYZ!EUj(Xx~#)j8d@pgOt{Y$+Crg)mZOyN z`g*?J*Q2iorLUCU7j2=hkMdf2(Ej`X?Mim6k+hOi0s(%4jO3l~{N^{G`Ry^gv%BD@ z_RYt^Pp##64a}K;;kX9UFC04n>8;1kgLLxhIY`T|hHLHWzhAut(r;fCg7lqNH9|Ud z6+k}&bS;rSN{BR@Lc)G<>0w8 zb{TlCyyPIium8ha|8ABtAP+xC00|%gB!C2v01`j~ zNB{{S0VIF~kig4BfV@q-CJ)cw`vsf}`J4FLc+?1U_>DgCx4~q9^9ly7_*-|s4(9Od z9uW&jOl$b_O)qJ;=u60Y?FS7P8cfE1O}X~_ z2Cr$K{_Xl7^TYhV8*bN)8@{6L;GfZHjXyJeoqWgeBDq&{y{?8tGz#^Trgv!%>AtAD zg*0h?&tIwUdwEEq6(oQJkN^@u0@(@Z9zGvl%)}Lk2V3U)`h@ZD$YRfM<8br-$X;L2 z?ea|qBhkIJwN|V41s$Ab*A67*GkpVn9UX&HP4n&+@7TbIr=OB5FI66aJ+eC~?;o`H z4-HI=H;=f+n>=GP9etFXwX#+??e0j*n*sy-gY#jbMVNQ^r}_?rW+=JlO`4;yUw>0l z?(g?Cc8v5i`p27MJ(0Hg`4BB%uh|3EeMxz9{qRg^(Ys$bFcO<+Y;k#~D7o?(kb1l!sNC^_@L@E@aiTT_bWU*!0|jSJ*!?*VoxL9_wqLrJJg~Q~MNLl4(lHXS>FQ zk=e0mD7Jq*IMX-T?4ztJ-ll+v)Nx69tgm6FUs!ZCyE_BE;9SH$PRUh&Rs9pL`}{B| zZ)hKBiv*h;lirU0reTNM*Feen2_CNeDgGlVcl1oSX2X7OWB0%T*MX@>WPy?^rW64K zT-q{LCEruEIN2B(cDv?;1I=w69gew%MXEsAPB=#0k+eQE5ghBBXqa-in|j*&$yL8qK>(}%l$4Kjg?guE2PUTnhua#O zA&$E!Ie!a(1zcX+os{?bJ(G=7v%%Pb4*x>y{wY@{C1)F0xcbQdG1<_bkx56`5t{27 z8)<1933=-2Ake{uJ^q@c?E{ko_0zs!z~LHf?+tWJyTg=i!?Q-XPHLP?%EugiLw)@& zPgmz;Yj`-+xwxN_YmRF^3IX=Vq}&}Dv=5CRupgNB%z7QO6ZIZSu03B1R})p&CFRpo zF`==2zkk>@ER5Ujp`Kw%u6{!O9Qan1z~C$;S5_+P0ORqb zyf56@6Kd*g?zOk~cG^4U#yTju=9`+o@*jtd_^#|5@~lhOT%WXVuW#s`@(jm@WB$&` zj=s587iC?kPy%=D&y(`WmPogMa0njTJTemP@w8kxySf)9 zN4nkZ)8W}Ze^cvh#s>9HHEd;yB1w7J)i`)yu%{(3H0GZc=0lAR%DVcq>hFT@pHIrY z9sAqHBF&M-*jPBQFxKRtS3JXajBrt1u`VfZAL<-ftnUbQc6PSU1bc=C=pC8jTZ$*a z_tdJ_>T78WO)Z9JXX~3|bN%%bLCP8ZQ-*c04NGl*T^;k`nb1tf^w{D;>wL>Zh~62f zzNh*T$lsTAW}(CFZWx-Hvb!So`A}rY7p1K0p3#@U&LEzYyGG_Z+lQNm+IoF`?NdFD zL3&NqRBMJno+2X*_Il=`3p4v?WAh`;vy%?Gsk$?Da7kbP=cM(nHm|R5CeSn6F%cZ< zo9T58QqGuuWcqKg9!kpR>KDd>UCk4buI{;M|3qhfD+(Z z$VbT|{E{zd+I{#pK){Ezvk_$T0Fy-8{Of5yKUUoig8_zb)O;c4TO#%~zEYW%$MQ^t=PA2EK=c(3u@ z#@mfI!m-HZ#*l@q$y@optw;HZDTx~dNxYRIj2pfEc zal=~-BZhv1!?54b0B2sR4OYYXhI0*P8#WrwFc_uo62k&NNB{{S0VIF~kN^@u0w+zN z=mU?9!!ht}kF`KL^VkkZU61J??f=LWq>UeGhjiCR${^kJ5fh}^N8y~&nm<3<1?jII zh0}a%zWrzsq@VwA7}9_HFkI_e^S%#PLVEj$#bM&5kIX{qd<14OuW5e-PEW0=eFTo} z*PQvGAdDYg`cN~ZKl#wvkUst}oG&6DfA}qs-tq7SkY4jJSRmntphhzKP%ET+A36up z?GM7UzsRNsVJa{G&j&Ap^fwQJ1^&Aaz;gZ*55TyGfByqjkY4}5T1emi!6>AY9~^+x z{=sTUt3POlbo&S9VbJi*2jEMh4d#M*aYdf?_YrQbN7!x`l0*56VqMyp9Sfy z_Z@~bejm)yHU;k62dVQu3#1p{2eXw;=e{3iG@I7FA7(up|MC8FA^qL^VVGh3{`-Ki z@yqW6!p2A52ZW7xyblN)6ZZmP3=I^e6)N?lsKn?rvia}a^m$+N#^3b=os;KLgUWw}l{OZ|jBu<=<`tKAK`&7vL4%@COy z{Y_wB{pXu{ApO}*5V7j-+yqgi{^*SmMe6%*1b@`G+z3&mK6)cWkve<>M3LHa14NO! z?S{RO*4+T-vDD{Z57DSLUk_2LHeP=*oO^oldWc@tPp*fxRrT%bT#$a@I_OQ+qt|VP z^zQ56)P(BTwJ>8{H80M`SB+o09nz+2wUFAc@j`03rVY}y*X)E;c@4B$`NDBOq`y1v zfb^%w_dxok<7*)O^syO8KX9xU(svyLddka=L2M|4S3_(lJy%0)D2J|w*id#{4Y8rD zxC&xJdG=Kh8_Km;L2M|MS3ztj{(L3GhT?y&gxFC0>`I6Y#aFL{*id} z<$cl@{K5|sKmter2_OL^fCP{L5P9N9iO$$ zI<4d0nvTVA%S3~xwK60ORXJ;hq76N}JmceY+%NTFbKpD)evkkXKmter2_OL^fCP{L z5bb#ak*KhY?%a8yPKmter2_OL^fCP{L5Kmter2_OL^fCP{L5E6ayXs4Dr;A~WV^DwvZ|_aptrTFueE>J+1qu%-g~ITBg}f;g45%T zgnX_AXTaqbO7>bx8e?H^Fyf6aSbBrALb#;NvZVBSr2KR{V*a$S5P_^iCw`2H%5k47 zDrURIKO&Q^aH!;P0oEekiGV8_3kyk`a&3|-6+GotRkpglZ5{Q^?QMg7jeTN$p1!VH zYrWMrPMuZT5xn-0wHu}>|!f(!2b?x~qN-+0fEH8vQTnfH}XcspCYecipau1ZgN zoo&$OoosLG*yGyiUl{7EsID38uXfb8x*O_C4vQ^t`6hy4Z*oz~&QzayJvqBCDnz1|RG(U; zJ_T2nJ1A9t>F>jb569~js?p*^@g=T6FtDrKYOAwZt>sQzt*ypt-Bndpx5AU!+VVPk zUuWY`Q+sE3&$0{2HlD~x3yEcq#ZF1r(9TfI3Umm%Bwo+SwxNP*!$8k)SNF*NrltX^ z4JR^kw;^3~a&4d;65FuILK}{(&Cw5)R;mq6{jEa{{f?IYj^#GcYjQ!MknFq@*2IRN zRHHIHlveg&cw)ET6%BjmcLzf-_z%0>-oS*@9SlT;`KTBZhYpt<7RUc;c-tfSHhC|J zkS0x_U!t+107r++d|-c?vs>57aKMCH4b ziqWDRXAd&Y;#61fQpm{Z^Apt-ySUu)%5v)5L{;rBRz^;r%BZSTL4TUaV~`eog}g*w zg!=%VCBGy;CQp$k$k)l2$!EzYr0myOzhiTd01`j~NB{{S0VIF~kN^@u0!RP}oc;ug zF1ma>ytZ%KWl@IVe|6bD7{h<;=mNtS-+FW}!{}c+s$dx1c}E%)709W`miTwT^w9`sHM+RZ#}R#*31LOgRdbSEXH4 zwpzL8VU&49D#U#@d!g#8O1jYdVNhyM7MjamsH!IY76+*5%`h7ECJR}z7pkmFzvc~v z*1}--38@hK{Tzj?E3ewu)>^IX*I{gYU9!-P*$Y)v(Dc~LU?^RkEYz00PNGc zBku!JA?EMd3)$*u_n7~M(fQ_Np%3OLRFQr)-)d#v1%=MsrDPAX!`aJJ(!l$~`Txus znE(I4>wHzg<{<$jfCP{L5aP`=a^@?U%GStIt;*Q|Xju#e$(-{~~!C`&V{A{EzXNzRJ`y z|CR!x|0cUG4;%eAnLp%_yuPyImbhDEZf)h_by3%tPl!c?u+!rW3+`wzyx@!ouCRL& zz9#%aAR3X+Wu9y7wb%FCE&cTk9d^rhdG+=kW4@q!+Uv2jI{NJ`_FhYOZ)<0L@35ua zK3ryzGA#`qT@9u2?eQ^B zdbXEYw!3Y!E?-R8UJCK^-Pc!uKx^V>#@_@nQqRS!yaA6e?~aATK-w9Jx}t*9Blw~& z_#GBpey7(Xlgs=nMZeY2WFNA~o3V5`WE4A=>nn{1;s-V6`g$%QNYRnoZ0;Q9muYd4 zTWHx~S+3dA(Aol_YjJeJzk!a9GD|oZ6cJeZ?L#XUM;Af@y)4OYT3Dbngjo@7%1D_d z6c%QkY0E67Z;Ov;%yl*{evZ@~Vdw>)*YAxwgR??7?DYs@H)JklevxYAfTOi%zz(aO z*bA9Dv9w4(GiOIC6BToXdX8G?;d zM1b|vlY$T1iC8!ib-IJGKy;}?O5^9ohcxD%MlQZjBIFTfy>7wj^G2c(XD}8W1GUE) z3QmK`WOvFan4hIZt&B2dOJpabS`oTOE{+dq%$qlJm-k2*L$S!D5RRn2m>;JBD@iRb z0ZtGeso+c7W=i-1E`LU}x!i6*=!`;(14|{F?9KH99ZR7b3J1r%J^{LOf~G$W4C!UA zP-rPeSH!=#)Z>kWe69s&HaAjK)p9{hv zwT=eE9wF=nRca5jlpB+9q%4+M#$8??sGJdRz%48+oM<}6SBV1BwgexQ&7wxt3;@x=Sp=DD6+H6%9; zqS_gW`TZ_&pPk$kFE!q2%dcU67#k+Z)?0J4A~YK|$!PxGvrcy9(HB?Uec_ zi$~Hb6!vH4Fi)p#1~LWCR8evD&&*xgbT02CN{wn}djW?Ez45oG&BDH1{V!Fz;&?6D z_n~0KY0H_#{5D<_cWTW0_Hpq;Qg7#!=FDJzlh(I6btluM%UicHhy=P1;Tm950oYI? zLm)m7?~!)jiM|{wdu9ML-%soCOhNA5caFoNKrreZPv3K&*tk<0F?#K`z-BlAN1UFt z@}=>e@otT|6L$PP(h4rK8dV_(~9>nV@jhDu| z;J~^dcfB4N7ntuA>@2KYd5%xgTut=@ID2W_r7_pkXyaS5ZZf2SBE1=q7N%S~(AwKS zP~VXn9A;Ho2KrhZEtc_Ur1C^Gm06^*m8Ep)`G4e~OlV^k5H7b_WCRN%0VIF~kN^@u0!RP}AOR$R1dsp{Kmw;V0Tp=n7L$1Mf6;fzOXNlJ z2l5>G75NGIKKTy$FY*=gIr2&JZ{$Pd1LPj^E^-^WfgB^3k++cr5+QyvL0se@86aJx zl{AuzNDZ-(on#w1hioEiiHT?k%fH0`iT_{zxBM^oAMxMgzs*0+e~JGL|8f2!{6qYG z{N4N={4M--{8juB{xCns&+yZ{z&rV2zK`$ZTljkZ0=|mh!T_<&f6iz6R1Yw}lvH%`>-kL;BgZy1ot4 z+Uw7URCPTpH+<(h&>QZ!&JJnpx~-5lT*pCLbgc`#`o^`FK>Chr&w+I6n!}LpyJi&9 z9oJmQFuLzwb2g+mUjsyS)5oDjx{Bk_BAw|tycJaY)NvT?YTtXz4e6m{^^o=*13TJt zuLe6><<;;CJk8Iqu7&i|SHnB)HSfM^2GZzNU64Aif|hDZuL6eZzg`K;)!(|Z7t*`0 z1m^0uUs(ic_Z1;Xw_gEz)jzJVLHgtspjSO|c@Wb0&@Uin+|sFv=fY z)&c1!F9QO~8!yvA>OKktlouZbl1lz4ELZ&Dr~%S1ADM>q`XfM6F?D1Iq-{sEke-w9 zG7R_kL>r_(NC08(k%SS_V{sp(`{SLEmd4BAC7&B zX@Yl(zZI3-ui*DP#NX_r?Azcx1e^&v^{v1fkN^@u0!RP}AOR$R1dsp{Kmter2_S)2 zj{wXE!14d9haYQ30!RP}AOR$R1dsp{Kmter2_OL^@QM%+um7{;FK`>kD^eX+f&`EN z5QRO#?^Z;r_<^kMQvHzyC`_APppj;N=H@Ou)+z z^cxZucoN{|1eDTV5{Fj;XsYAz$^rG|ad<_6>f>>EexUOE@a_iXhCz5Cz=L&=Jg`L^ z|7*yPAmuH~Pj03-fS#0}#8|3&1j40$We`2P-hoP2?Nl6-_bK<*)TkekRc za)kVqyg;5MKa>6MSx~@#g;tL`B!C2v01`j~NB{{S0VIF~kN^@u0|F zV{XtWbu4FKu9v>`%yrVYj=5I))-u;f-x}t)^sQ!&N#82wYUx|aTqS)gm@B1kj=4gu z)af|pa`*!4muZzO%W(?ks0Mz(FSlCBLVXSJrDeI|6W9Mu*Gkg>UcvTbRY(8{AOR$R z1dsp{Kmter2_OL^fCP}hsV88%CO(>c|KAG?dEwO8hczGpB!C2v01`j~NB{{S0VIF~ zkN^@u0eqMRkS;uz$^OuU``2WKU`S2@MKh}c;kN^@u0!RP}AOR$R1dsp{ zKmter3FIZv$trqAix+2l+Qyq3dwqTOrtWrMPxr!JU(oIHO$H;;y|uMg>m{y0FtDrK zYOAwZt>sQzt*ypt-BoR^q;%r_|15c$f&cM?1dsp{Kmter2_OL^fCP{L5=X53HBigk+}v)j-!rz`9rW*!jO~d=tu|Xt zmDRc@Rl@6cO-RQ4^X@dq#0%|v#B1!kY}Q?HmA#?2Wx^4)&y7btj_%I!*;ILt5OIgS zp{O?)fSLm_pD*d7WOwhJ*AtzD<>fVXD;G}+-igU5EVq@XEsv%eCKcWj3QUNV_+8Pk zcYbfuV8pv9dRtyzQFnNSbFP_5m%XKX#x*`WG&dKRVExxT^P4}2I>`lx=j3*-?xF51tXDnP= zz&^J03%_3*1cu^*+B|LN7;nHM%tvN?-l*Vo#iBv!Z)YqbgqnBO(_+Uxu6mj3#N4!dQ$oOb&TpD-)< zEUk`ydyBod%_oRCSr)NuTJy3fEITa8 zrCyJv-#*lDadg4IfsT$ci^L*xaX9D`oSBOybujD^!j^{C7GSi(k~iWE#m0PIx22)0 ztHWOJSkAMQw#Dbv=Fys5L6US1`1qY}S5%k?hP}YlmUS`9#NF{(jk%_VOSo51qDp5i z%ltD7DXO3htCVjGS-^zC!Evuopy}pjQf3)&wDt_xEjyNarp&U!eyOE&Bpy?n{Vnp$ zmSTfy#1gw4+06e-sNzwLxuu0WqFKQ(hdk+Fj!fnsS(xU~o*Y}i;>;0qvJ54l$&#gI zmd5?|#&*k&6UHvKSb=i|~2EM1OlnJWT7>P4?7JrbJZVYPXw{r2}6{JL2 zijee^`D+$3QqJm$=5o3)bm;O6J8>;P z%~@E}g!vF0EJnmFZr(QiadmB4VY;BkEa{_>()d(-N@MQq{O4ybOkx(!Y5uEOzM_4`(wFptyL`reO^VDHlnI*WCw-j|L>59%;I;1O| zj(aucJ+Q$sNPQ+9@CqKMv}qGNCT#)phg4J2c7kaVv5ZJOZQ%~l9ORTn;*-hE+g1r} znkelL=J_;eX?d`*N!g=!D7m(7BtD@r*Vl9L?NYUpEOVR^bwzXMF#nt8P_A(W*x0@U zj-^vW3d@S4|1!(0%NG-*Q9)_^%=ox;OcuAvxa0}C#c_LfuTq)J?+bA(CEmURe&ljb ztSaeUa{m8|40-XjF)l?fkpL1v0!RP}AOR$R1dsp{Kmter2_S(&3Fs6mU2+mda{T`! zL!Kb~#evRqVrYVyd7UBm9AOR$R1dsp{ zKmter2_OL^aGDY5(JS3Oy4-`K@>*)pQ*7^Om}qcy_O*68rl*?wV*dSh0b7)r?WrWNtx{ClrFir%;@k?|E#AeIz17bjkJ2|J@buz+9E4&(MIWu zCmoG*3!&CUPt!=#0e_=?+(RkE@xO|9FysMpfUx`nyu|8cV8{{eW)jj zW3}$8wbG9Ol&;u^gFaydUiLRC`wGDdYk_spzHr0v&c9LF7YP<@+eY~?3~&CEH(gFo zQpk>`rjFK$*Z-BIo*~~Rcas@XPl`@;KcjslfCP{L5J zlk*x&Aps;GNTE54;GV_yU>B@8vmdu6PqbFFa*HdRc+^7Jwdj3&2U5T=5!! z6DHvm01kKsz&SN?Ueb3EED!Zhw0zlO$>HSl|9`}guaM)UhnV>vEq5ILKmter2_OL^ zfCP{L5bUMYi z1vs4}a^@>)(~4QGIm}b9=o8J~`MsfE zkHW3}a^`L6Uga`hQ&XMhhG_oo7Y;roUjJ9{FEZqQ(gk1mK>|ns2_OL^fCP{L5ys>^OUa~42y{Qpyi zJV$;?@cRGD+QVoJ2_OL^fCP{L5e~!Q{-a)deiSr^+v()8N*in+jVj6ueAp>m#d#p?^AXu z-UH&Nn9G?S13&PX&YxcpA9hst0#Hul=<`|XxiN0WWXLgk()X6bU!nU)>PMoZ&k z@kNcfqJm3!qpmTZFcA*MLe5Cc?{|e4oMCtZW+clZR*|Z_zrLZvZposs>_{#3dMy3+ zp?-^_3;qpsbd*^{qtKqLi-Td05Vkb5wzN7{78!7~_6*o9JCYT7Ju%O;V^iqx`Y?(n%HQ76>ko-VD9 zA6A=Nw{nc+Mr6k41vgx=Xi)mwnS?Hds?)aO4^|z&DE?NBdFxg#v2R6iuLx@CC#y;$ zy~4ukgE$3;ChU#C`aI_Ix-AV|T^;s%$BNYZ62@XJ63m@}+__7V@~FLL|!~c5Rx+Srqasd@^b?x2ieX7+nY@8@D15-N8Uq z2t?Ck7usV=Xdx`>xcGJ{ELOVa7yM(e?VR+6lH0)SdF&afwk0DXyXeG5G<*Jv?dXPt zCh1U}oI^2jd!F4ETU3Zck^yo1l-;X z>WX@UfrvA;CeEJN*JjCPNm}>hF>_+So5j?QS(nf2amE5sug~d^l*TtElt~wDd0dow zKWEh}U!aT2L6pA z?Bf#2&0EfP<;-A>X_1~&xB6gD?`(S04BPP&q3E271OpkHy6p*8WA19?;w`y*ARx@S zd=Y2v5KanMeg0lpX)0$wBt;p4Y)LRlSCbpTtQ1rBbBA)uRb9cZ%7=5x)#Xq*KmN9) zkDYmO<__tUkJ^HLln?8ak4au>fqilOQjNK>ap_n?hNw&ytH~eSNf%!3@V1mn&;K*b zF#I9oi|~aXB!C2v01`j~NB{{S0VIF~UQPl>IE5M>=ykO3lsu5^-Qlk~50xwpR7&<* zN>))#NtvZ2;++V%qOq_Lfx?GMr1MUb!ANv(ZEbm-v`FxjS5?{S_PU${^Q|pn6;1w$ z{{8KaSaZ-}^{7H=q#cHxoY0i8so7jx>Q9YEl zRn=H4%54?3RaMnBhmUYvzD_vlTeac3>RGIy2_OL^fCP{L5y$vz$IToxjV1R$TJ%}z>vh^xY!(tg0!RP}AOR$R1dsp{Kmter2_S*f zoBWNbHXHauteB)P|M%y7`KQ~#3was3_oY5fIygYE|;s(V0pNY}30tbI=VN$oN1 zLG4b>OPa?ucW9 zxp!3=&)})ZqqkAJw(>lBdRJ&$rQRy9uc)K^*^@_KOX+v7Qok#YzJ{`2wo3iZJo;+N z{`q_VWYerp3t8@ zDlq5K)BU?4kDl(|^?CGk|E|lUr~7wp9zET^XXMe-{adt3{hB;_Iv&U>^?V*Z9Y3a3 z>Wz8ybi5gMD_k=BYdYNY@^U)bbn6{i8vR}ckqMucG5i1o}6j+>oc46}R|DPCm_TQf<$M8=ifCP{L5-&ZH6})iuKRvAJB*NRl2|FKBJ51wrhW&y-wSx`LpH$O-OUD`YH8sb&Hx) zeOi@JwWy5B?yV|jMus)>#W;eKvDXgH>slM6;L$&2{Be{vS{Nrt-DyHD+8$_ z{dq7}Yd@T2wzPzUu~5p$xdj-Z&pKxq*;;@R`rI>Zq^(5j-jlC6^xJeqBi_J-B@*-d zU16^fNw%u3xBy%9t9EkPvJ_y8j>R0d>dq;kx9Q+aHROY!w*hb>{rxR8EI^GMwr* z6i}3o>QvG71r*I5#C7ZP7tMHeU8>f#1r()YHdXYD0*cZhnkrgUKv6m(Q$^PlP?Qe9 zR8dkuQ99mIMfn1X(&3dVYAT>;j%YF#P&9iqwHflS4>mfQa%?|p^;(y7ydkqkq~BDR zY!8SvnY~{Yt?97`Sz@I4Cf-O&d(`( zOxLRNm&_j0af>oPVLGH=S;Do7{Ef@L=G1ceOJ-kj8d$9>-{D`j^=9SKpLqR$r{*Pw zJYjmt^tkB`)3oUVlfn1{;{(P+#&&X#akJq$!zT^L3RgApid7 zjI(k(^XTbgl=JiG=_8X;d40x-w;g%(^uhY}JbL;F{k%MS`gnO;9zA_fd~O~+eJH#& zkDfmMEy<&&k4B60=;{8oU)LEBDPxtTIJbJo+&&Z>v`?n~Mp6=f@ zdGvJul0164fB8Imx_?c1^mP9k^XTdRHORk*o<6IhmzUGMtCN@0ovW3X(|xOvm(yLV zmY35#tCE+~9jla=)BUQDm($(K?NYdOxv!bgy{du(5+nHpq(wi5djMV}&y#1#FUgO| zQ{)Nqb@FBMSt;{0I&48#kN^@u0!RP}AOR$R1dsp{Kmter3B0igtohy1D8rC%9ld~I z_^Xa87{=6p#KACzza3e}F#7*Xj6$kSz{C&DU*hu&qyB0fa#d%?OBhD+t_eu)tmXh2 zOJ0PO{8svUVY9KT z6z9%i&u;9s*Z12k{q+qUc1v!dWk)L0>#_9Phx#p!F8DXl(NSiJMTD?3b8*TCS{(>R zz2la?&iamyR>w*+lirC*AN-4EW?G<3gMR0< zun=+13gL(dK*+k3wWc+?B)h5vnw+C)Df5jmh?RBfeQi8X3-@g{D@8}JD8(pzF9fIsz%Ju}s| z0Y_`kfZfvSXtEDkPN*zhj?`Z}q=itjG(M6b8uO-2+!2$+GO13c?51>9$)zHjrQ&pp z0xrKGEi1FQW}(GlXDsYXEr?78!%?UBF59p-B!*~ex%iUXEQ`Gn2hwcF*M#7dIv9Gezf64Z@0=^_5j;+JFcyd=7XluzT>j5;n}FWF9O;POf%22=JN)|M@}vTH>vVz6b;-H}?e$}A}s zrSUb1b2aAnDlWcL^35L%L??X<(xz-yaEkhfGwSsV;zrFM%9hRUNx7E{`fQRF7DVT= zNp^^pNOhFP4T-J5OWr5RvuuXg-PvemQzl!N)1efr(zr2EqA^#2J9SbdXET*;8M`Yj zuCqyUZ|jz6&|5o8sW(xqG4I*N#dXPO3cDf@pvj&RQ(?sdwk#Wrl?th#6frA`EyYRc zP{N`%Z`;N}$B4VS6_S;|*_~-EU73}ug_hE{CeG2AFWSS!%~CTY+8&n|VsF+P@kSx) zr0r|Qa`yaGWyx^M$WN6Mo0O5aGRQQEvo+>@P=7M;GqlcFE(c2UIFKrs-26Lb~lrK-r> z!6tohN+sx;miCrqmZdr;6K87772CNuyQ0qIPAlS!ov1anh~1W@X=(BkRi+j%uRPAg zrVM~$SEbb>Env^hQp*bIi8`c(%Qeg-Hf91$mRkWZS;f}e09TZtYdQf?6ak*ng@ic? zaBCKTD{5T=SdzQmC5}|rEpg1OJFZAod}vTj4Rx>GA4 zUH4gO4ALzphZdXhbvCQD+-a+|)mW{&D(ENsWqG*+WH1ulTU%?j zW*`37)X_SNc>TXZqi4vIRhko$c>wdLt&*URfFvqYBtJB0*Ynhz5iW2XX6Q_5I)q1|1nC_HPIWhfu zs2y@*x(Bz*h^_ST*LiZ{bk0`mHaRhk?p!%BO=GK^n5I!8C#Go>=N4x<8kG~%G|rI| z(=^VO6Vo)d$cgC&o+Tr$rMr8xoS1IlnQ~%!wb~>nrfF=H6Vt1eSx!u&+aM>V18KdS znC84rMqER4UMnZ2IiDdXPUl=+S0pED`1u zPE2#w%ZbxDTdg`dF&&~>IWf&yBPXUqR4pf_J4GcUuA(_B<-{~+g`Ak?%T;X_ zsG>P1=l?$`jsH)x12S|62_OL^fCP{L5y2Mxh@O0i|99TQ7n%OUG;P{w{1O>0xIz>n0VIF~kN^@u0!RP}AOR$R1YRiuy-IhF z-0W!ja%!=?qhX@K)!EnD>6o5s?u+^NQxlK$`ApImU5gvr9plmV_C{A%VBzRfcsI~IM=8>LKY^O>Y4Bot5iTKYU-%k3VXpBnF&o*Sf0 zYF2G>#L+s{IO`sqoN&)Yrd@%le#)depGo?1ezBv?Gd51TMq}5aJnQ!z{7P)S)DDxNqdPYL=q@!_eA=J9)X&Pxd;BT~# zdnk+2_@6(-@bBdhk&DT3V&=a_o+bkSJo(5gwfbT`NB{{S0VIF~kN^@u0!RP}AOR%s zx+ZX*qK7TuOpGMUe|ANZFXDgDG<~;);$?~67kYxE!6>L-V z=<<*MRS;r&@+C;ue22V5{zU$l{FeNJ{D^#ye49KjWxTF~5F3XCkN^@u0!RP}AOR$R z1dsp{KmthMHAH~ido;o@y!&Vs!AL}v1=)YVkoG_23fkyr_oWJc9tOC(WC~VR(9cUu6}$`v&QHn|tf;0Z@TUs4 z!YKQ3xq{Xlt{P!T{wtY+{gCyxI)@>59He2Y9U{g4S3 z%t8W400|%gB!C2v01`j~NB{{S0VIF~PEP{5JqCq}OP!kd9GncWdB!C2v01`j~NB{{S0VIF~kih9npiD8!7CQXra=-sCbNnw} z|7X^mz70pukN^@u0!RP}AOR$R1dsp{Kmter2_S)goq%}#AIJayI!CmC1dsp{Kmter z2_OL^fCP{L5&^!;evkkXKmter z2_OL^aQYGG*`OX}ZM(GD@0qNsreFEA!N1!tOay&?=a|bq9SiNAwUxL6!H{bvCPboP zZ(yRNXZ`9-t}npky49InSAfa2t24Q_0F!5|&g2;dm@HbI$)W;Gu34SQH3gU?t20Ro zFv+jZBwv6@)9Or`3NUG0ok?Q>CJn1IX(+&?UNWgGbRE|hU{bd_lez*-YFF2AZ2=}V zt23!7z@&P0Ce;O)RISdWssNM9)tOWlU{bLP&*&I(4%W`&X!>Z)#*RM*x zZr!TnYuBzye#RNAk{1=NO1@^zs^o;MO3w4ElABDck{gYyk{b-GlI!)WlIwJ{a#UCFNip&Izk-+;@+f%=G4Y>xO>M-`StNi2 zkN^@u0!RP}AOR$R1dsp{cw-RgdzqWrqtK0-*R!lvTT|$yl494G+hZ%Qs8n&{gt6$P z5Kg~h&q*eACu35X)nW&`66v)b_f|Cc$2&>Rv#0!RP}AOR$R1dsp{Kmter z2_S*fkbrpoe+%bh$kk*WeqFAg}O<}dpu92zNT3G*9Su^^# zx5-{V6rLIC@h-+D8wZ-ZXXtu~T)nit9PQ2UuQ-3beC~QH>k8X$RMw^Y2b{y91H&Et zZ7uF*ho@z+d2wipZm&tM-t>OUwg0WGqxYA^O@eQ{p}u9dG2A~i;h6V34$$=)bJoe#OWVuAIm5pK>eb5BTU}s0SEu_2oV(q#{>CQZ zz(C(Z+t^U3-#bCKS0h(%fpJxx-oL=Pd91C^+Y=m#j!e6n=N1=xGWu67S1)ZZ2j^a8p=o-5P)7dQ_E21dNY zZB2s*94+IKhCs_AU9Uo}UfNy`&KdsYuQ$pm4<5`hPN^;I@T!)M`eIM4u|4LR?QG~9 z?;Y!J?vBt$IZ;-oR@z+l#%4H|zgF@3{}!&B;or#@o9;L5F@Dr|k>QJmcKs8&pX!Ho z6WZTvW17Efj%f7ijp`dz?@^tntW-Xv_@ttN`x@6x){?6W8v3E=4MJdE1w;HCH#vuuxUhqbnhXc? z5BAr|S4^)aIr`W}uXg!I-D~A5rVZw*n07FK#b?M>d@9HLMY0uV45+iW_+$qBYvd~~ z?1-`|!@>L`ej-=#sT}h2@)f6#1zyfEze&F0!uH1}Gw3(UR(uLa{RY{JGltdKqxxiq z{d)O|3ybQ?3ID`r(N`p+?h$rxd0Z}3SB|GAi0ZI1i)GwkK4mOdaXaQtt$ zl`&26yLdG`{qK2lvC&}orSU%F+xf4VK4!Xs?>7z`%gJ#f82@Da26>u%#Pk!xrwzCA z@8u7f8ck=tK?b(iOeBB=kN^@u0!RP}AOR$B$_SjN=wb6;de~B!>?}!^ch=zM!enPk zvb=NhHWen@D9Q59JTn(2+aSsE&L>-6m~5RS%R5_ZZDF!ABw60MP(_j~?-Zgng~^B{ z%R4cMmt=Y8@t6ve8I!){n*(1MUSq)@xT6AhIEm)k`n%_{Ga$c_7X6!Zm#kkGzxZ&M~Cc}*WS%aB;mwe!qZ3NLU5dw@FS+Ul`mdBhHwoW|kAvv+g#? zh%@f}S}!N2FY&IE6Vsg6%86;tXXF-V-p^DdBhI)9X^otip3Fvai!*Qc;pN11Je%ai z^sEJ=j5yr_JQt-RHzOmq#?Gj<$x8q;{E@r@&CS4+72|11dsp{Kmter2_OL^fCP{L57q9XLJi@$3 znDx2^r_URSMx2poSaA8Bv4{|MdOcaP*#5>|dwsv%(&}ik4_UG(mI_l8?z3?TlT@$diZc|8Ogdfes5cmhI48XmlRo$d9_Em+-D!>$U_*2* zQ?{ZT!@-~*f~Cw74Tgfg;Di_6%-{@$Jwn(Um?*QP5HBq#jh~rlgub?MaeEFQov{-Q z$*h^}%GZJw#q&C!;hizjpfO*xiHmox@Xj3!MBy}O#2E|wR*;Rbjx4@pikH2JSaz(E zRwPlcG26Cq@vV|O$si3yoJ;F~XK4xBndZvU;uAvAiKV40dT-fKnZ@UdK$r_cp!BVY zOEl&SwsY}fsaBWUEkq*DXmDBp2FoSV7PB2`^{3^XC?YzMmX*zu()i}YK8?9$9~Zws zs$XIzt_TsQ;0pV^0{AI817SCxGm~vkc{Jc??HO3wV&#;JwnX=G3YNOAG;U5@tTFEs z-HW=$eDD@uSOK!RmotNHOS#rx-_T*tU8QA5GSBO=^xKE}EsieuH_*``u4~X2mWI}r zR>z6O7IEdOf}Y#S#fMjfZ&>iTk{bx=mk>#=Rm+Rn`n0~Ia-FF{%Tb(MAZ~DWq_Utl zq%FXbMQ3y&B$QcP6BA)!LaJKYf`t>65X|7*+N{obA+p3R?UI!Udr7t)q(o40HqVkO zaoV5 z9V@~a_OQYn>^72NEya3id}G222P!pOyk2UvSh=`ggI(U3&+B%IYZq*h#q~C87JFf~ z=*cQgS`ou2Yxapl7HN$ZgUXQDqcPWjZxs^Sti`e}WG_gAoK^Gx*?SWJN3QZvS5m9@ zmTJbvZrg0#He=7&(=*x^8!siPv{#i%DoIr`gHn=8YpJx9b|ASe4`9P13n2@<1PFOI zVNG&@BqUq{;X(qz1PCS&AjB^a!jgm}4+wb#?mb$1rn|JzhHl&*j%>^7s{jAbIp6u~ z)Y*@m^wr0H9R5KU0*}4Ee*vJnN3}b0kAc->BVXtFDyTMrf5G3aVU=!(YRWC8eHN@K z!UN=cu%_I;;wR^~%6z8KEQ69Dlc|GO$oZ`+aRHHjpuYpks(V!Z8?G>%Vr@oRl(q?u zQ3RToCt2@a@9N_no%C#&^BYfyh1`RbsM z9Q*1F5v6sUs!{d&5a#ysK}yard3Jcv{9fJp-0s=_U85N9f2wBaCI_v69}r{96NvH9 z+z*cNm8ZttAzeD~8 z`6TjD}*{D=A#g<|!u`)-9|rQCm-Lb3eQ zyWq;wZ|-^(ipAFMtqO%&>M0Zp_k_UL-Dg!B?@(ygFM$E{(-)DqBFx6~5ViJqwF&K_ z=JzxYf}Q`C*WbQ==|rR-MgkIm1Rw!O01|)%AOT1K5;*AuPHl$NmnhZr)w@=M{LJ?4 zE|GO|4m`y7n)P27s_u<mIR}BI5ZrO0o`_(G0M8*F2lL35RO? z@^~ss_1KiZk_u(o-Ga=fZZQ8}aq7q)Cuk57fCL}`NB|Om1Rw!O01|)%AOT1K5`Y9w z5P`w|f3W?3f-(d~1rmS+AOT1K5`Y9C0Z0H6fCL}`NB|Nz1_E&W{}`Y`laK%;00}?> zkN_kA2|xmn03-kjKmw4!2_pc<|4&$k!012%kN_kA2|xmn03-kjKmw2eBmfCO0>?lA zw*QX-Dl`cRKmw2eBmfCO0+0YC00}?>kN_kA37jwjgZckTSBHOId+CKKf&?G|NB|Om1Rw!O z01|)%AOT1K5`YA*Z33F&Lt2Gm?VkQ~6^iBO?}`eA`Zc@H2H&p^sgO4-)W{i?Lb364 zNEmqxIfK0D+J*sB2MIs|kN_kA2|xmn03-kjKmw2eB=8eO00}G3A3nh=kKD09skXD8 zQWO(2HBY_33ypTlnZH}8q&Xp9ks5c~Y&xCt9t4c1-+uc7rQ8ni4j~?;3PCnn@dX1V z+94B8C=}ZNIka2%=Of`>Iqsmmf-m26F|?9suM2-$wow`5N*ikN_m`BNDjo-j}=(wAJr<$vxoP{^B0^w!YX7zRj0A z!1SrhR`B)Rhgx7h|7{PM6$*{L-vmSdzt;!XSFl|mzjWI!m@`x3*=c-~&+01NZ`DefO^9tn)K)dYjk&2XMQY|!1M*aDo zrya+t3Asw+J^9Zo7AT-%8>xt`wBV{#3yvU01{}UT@13r)1ICF1BNeVB28m!N>&y$m zmYdC&{Bo6H#1vlvZOadiR2UCh7PDNeRdh8df3un8 zH%dEeLS694r{OC{De(dO$Vdf49pPezZL-ZWo)1RazUhfq{wGj*qR~3_z$lEOhax-- zpKsdZ6+Rx$v>V-;x7F+<+^sw@1Mru?!hgjA@^SEe-F;y0 z|Kr1-;4k=r1Rw!O01|)%AOT1K5`Y9C0Z0H6fCM0clT6?`%lg8iYTQ%tWiS!oFZcZo zj{DpA=kN_kA2|xmn03-kjKmw4!2`4a^|G%NR9ekhuF!H~Ummof5L;Er9 zE45MWO`6YXUaR4TH&1vfVDumXNB|Om1Rw!O01|)%AOT1K5;!^nr?VFIgWJjnII$w` z7<78OUZ*o8^)|g#r`xd^WDBzWScGQTIO8LWV05)>Q(xLvO;@#AO)_1~8;pn1V3dff z{tj&B_UU=bdc91UrsDCCC!8Qq)o+7E{qLEhY%v>U%E5rk=cPlKC#nJq?>BCmqinXD zWy;=A$Q|`N@Cc;>Tg|V1d7iStDpPi#&VV-@aV7}m_rQMiPn)A`GFfEG{o`H!kp0}!g3N+0&=i1Q8+y9QRBkpwLi69PaeBle9Q-L$? z*Izi#hS_A7wJjYi8F2$65fa$=+;6>J`Cp(t|NMD2OjfxK!iOaiI2j0efsIe6{z~~# zunYZr=i1QAJ3{;<>I`~WpPK{t9T_KVzN^qeY%AOn`955`JeG12+EhgAXe+ z3dQ0-&$XeG=L?f?`9f?wPP%}N5C6xvm0%w~bz`0lgIS(0A>4(r3AYRMQIuctffxLp z0(5hz`g3jQCejk55^R)=#p1xmpZwir#e0E`-TMYl%M35o8|7*7;vUNBVG@zS)AC2Y zchyy^4=XM#dBHLOZFA{!tN*BZ*Rr@SYQCnuWkbIj*M4@*yYWj)Z(iG6-dMg@+gbV6 z^6OR%D<53`*yvSb^l6SjL4GxJI?jrny`+B~w^Y z{7>+YLiyNO0j;2|ve?E;dDcNmDW?>aDJ-l#zV%_A% z1=V_fJrDZ)KQUH_IIwD6NEDhv!A(T6M7bwZSWs4!U{nQhj1{n~KON6!iUb-?HN9-T z7L_ZgFR3qq*RpiiBDD&z#!$?abNOSna)(KDQ6UnSDJb8id>5#h_{IUS6iTOZsi3dy zbaCZ^%j=ZMFRy%H-1u1#o89nB7i60$N@V0^W5&*x5+ydEm#t9IKZnSv^!ihzNhKN}~S z7W__*6`UoKjP=|uA=r`0tCvg5B~WSn^|3SN@Yrt~ZQAPc@Zj(>U_dCJMM5){+^6?x=vpp;nZlj$r)sk;y zu974z$hen}<$HOj1YQYkKIZlZgGAdQf7+HVf^5?~Zyck%NRbY{<7TTZzu3T>dHJUL zODi|8+zjGV-&1h63%LxND79H4(&mM*{Ap9IN7g|Bv$il+$PD7&OgmzfBakR&#S9@! z^TNVw7G49&`Nzi!ZK=i3ex}llm9RwG>6YZ#wxE86`W3*Fu(5(mjCw1bI`82-M7bD= zRpp+fS`ICT03_AN#zlY^brTM*?rFvn&A2nyj?2q1DunuN;{;eBp>2l}bThFd&Q0(&8%@UkNhpIb(&O+mWwy{ME9f=B~G~o+l(zSk(Tp_K!gU zQy44op$1NdLS&9iJA62isLDNCy~?eEB5dQ~u|k)o%0U;}Nl582>g}sw zmi^L;#tJ2}?4-MeLeE>vI{lfvAb+tfuO-$J;LZ8ju|kTBbb5`3tC1GOdRz=vTJkh3 zrkN_kA2|xmn03-kj9G-wm!z&Ptav7}ue+|;o<~85byh5YYd`SD($lH+> z?Qd!8+GWJ0F>1i#3;2NqAOT1K5`Y9C0Z0H6fCL}`NZ=_>VB;5d7B0=$Z$Ph;@A;;D z4p=_$x!D?4y?o8}x<+ePfa&yy`7AmY7c{E1Z-Ny~-#WkW;EY&JY`&)i>u*kdXm+IZ z6ARK+&jyhKbLtld;jJ)W!NT`vtD6l6DH!Z4UgYj4x@_eAgP<0F7pwcX_I}Iv zfuI!2e}31(gEN-7Ppon;1AK3p9fsZ}&mz_5z-od2JzK*#u^3p+&rak)gO|mbm=1fwiId=~?3P7Wb*4`1&ky`JR$1L-FMq z;x_qahf728#aZI=$f$?nD>K9=PAFI&iZ9F%x5+`Oc0jJK&geKYPxO3%y*3m#Dt}#p zK`oPYYD2;9T2p7g|O_dqzZ;{0G`&(y60i5&qFD-3evd+r6 z>BjX_FDtzdMf%TM-n3d(FL4fYY^vDQi%R!6O*HBVVzc5rx?ad^(HKK-5hVC88VsJ_ z8l`2+;q&;2{nt~?bct^yr9zLNyqRv+>%0i=0`z>6Z-L*^q*M{OFrP`5`ErV{Pq|+Z zTfo=n2nc+(F{b9{x6Tgpr;ypBdT#5StN-BAW?X*=AK7EN-$BOLlvqE}Pb_ch^{UI4!ypULW)oSw1Z=$HGe|nf{vt?)BQ92G};%q^XnZZiBwB_`o;j{X4SDx@K7v@HzL3-;B=8ml> z;iID1`K_xgFq$)8kIAb_Gi8*ial}V2ikWizYz^W)Q||25FwY&!8y`4ZWO?)c`&IoL z_TKo@>kTubsQjq|t~OH+c-pW6GJ2C7C@ZceQvR9|`A#D#6vWc-1}Gpq)k1x;Jl#4M z>xY*%3(i^3;eDKyJ+ zPFyAfut@UDX`wKD-}0agYRGR2oCIFQgBLm109!&<8vfw~?jhbx16M+X$?Wxi41L6pal%yfQ$ zr1E*wV*Ue^_L(0f;0H)wN!PjE%{~FD;d<3BHcZ8!_~w(jdZh^{kN#+MtAK5$GkHt- z;5eqEB4|<|xu-jb)8y^5`!1c|8psX`{y_vlxqWWe*vCQUgVJ)8v(wU)=F)loYT9Rt zC=-WeDzi8X=1}Yk#`Al?3z>7f*Y|_qT?ASH`XSL_%?Vf-yka|90PZt>>On)WA`0ETXG$5L2xN-%UQmNuTW7CWI+ySJKrIu5rI``d zm0)5&Aee(8jMTsj{Ay6UPwRU@M%wo>a#W!k6p9>pzK0d8O1+zG)`h{-G5xMGdLS0l zh4xGgVl-W1%o`Mr4e8wA{J#y&?F(T3|L-9G3wZ?bAsgC{X*Qbv<7Tj z2tSYjBmfCO0+0YC00}?>kN_kA3H;;`IQ7*zN37d)^6_a05%xyFVG!Z?f(=YY|NS}2 zR+D@P*XIv77=rQ>?1B;;3_#COwoDA@x}7K)@p!2~K=lzYL~EU=JTWufA0XmR%7fF4 z3Y;*oUbUzn+@3M%e&YDTXdE0aM|%B1>iJ+b!Sm*XYo0h_FHXA|BI1tHK^2&(zVfSc zlubtYXnizFd4f(XPC1lc0Xyvf@f>BNb%He-jwT$GE1pn-l?jWbdCGeEz(2_1aKszJ ze5kSwPJDUq9A$$=ek7nb=nu05;f-U;dxy`FN^4fLvvA9`eeOs&im?pgz8)+l(98*0 zZ#=koL#OkH$Y{WU;bB_wy21Sa4dg`%n~ucbo|h z#{btekAm;hUF5^aD-jmi0y_Zwj<%_Nu6AK~<3~#))P@8g0Z0H6fCL}`NB|Om1Rw!O z01`L`0;gVRP(QdhW5j-9>>IQI-+B4=2f!4AkDJt&7H6y(x9a2r?t>=l#XAQx1T?Rl zqiC5Jcm|4(|I0g6UNE)bH|HptE%H%%p!oSOzgM*Z+N#Mpil&Lh^FZ-4xBRv8lVDQ8 z&&*TQ%g5J&;wPQ2R=x^MD0u4}MWac+x*sThH1=I3*imuu#yN@x+Z3vQ_SjpMw}9dN zkIukN_kA37mWaT6TFsrCPsJV+OnPePZx?^$qJk4VFzL2ftUGtN)--tp3yB_p)>4 zXTX^LiwD1#YRfl+^$On^{9b%u@$Fy+hG0wFCgs6rvqaM2|xmn03-kjKmw2eBmfCO0+0YC00|t9fKH9wrF7sd4vEq0WgJa5($; z%*aAUZB<{IwU@uyeDELvgB^?DPR13$L)5}ty@PuO80=AsB|{0X%N12wKzOSUaAHN= zG3fMmy-sIH>TPGnm}W;57bw2yJnga@3r0D#@T`pe+^^gEC*BENz3kR);+ z@@(zrwXfFJwE^v&U>Cr@*ZisG4Vo8dY?_-lzO(WB8$Y`d*jQQr>iUP)e|tT%j;(L6 zeQoU>Yp+~uuRXA~y{1@w&(PrEvI0s$0+0YC00}?>kN_kA2@DCGzWdfi)%_aPM!$Ta zUa6EXiqO4cIp7 zROK0qDx*fV{`X@O)k>SMCk1c}xFBu(5fHd|%cANc5cu`6K(SH~ll7*+OB)~x$Ttii z3Ycm=bubE7YuEb^M;lg_*S`TDwRZhpOb4~>LYfnj zoKWDTH9gn}?a>jV)jt@^TxGQS7vOetlufIb4u(9vx^9+lf3_+B6N(X(Rqfcs{%l%( zCKxAt`AA^p-BScsKqNMvHHza(VIqzz9gxg7j^em-<00Z`vdH%nTR8(v+&wa}{Qkq3 zFdb~o=yc1U0QG?1xz5L#4i5P%)~CfN1<4L>nI|N)I>xU zqu{n{_a@p>};_+f!NQD0IMDyi_Obpz2#t=TBlR}B`~2Mo2VYfgjt?P zsyY}d|J@OM)pN!su0GkS+rjO~h>7yUMi;E`6nV#GoD z^zp69GG6&~g+los_kxo0L6zctW3hQ0tosXFn@y)vyboCT_Dze*iy9?2HgGiw+!}b> zEFtR(X#az`0`LO~Kmw2eBmfCO0+0YC00}?>kN_kA30$)TwidAcL;U+U z_8;~IF3AKY<^U|7U$`{m?1jl8fAu`!gB=kD`@$Sy)BaA&!Qj5?u7yj7o(V7*|6g4B z7w~=R7q!2vIlun?HD>tdHJi;aVUPeM00}?>kN_m`lSV-ESU2mtnvrfCx?;koI z44~jHw8I)YE!N=HadCUo=NbN%IFG%TaaByD3vD}#Xd9=AYQht1W;{Zfi^sY?Peb6T zN*i}!ZPp-^e4dPuDxh|s3kv|7J+=(woeq845bH97`z~FM^0=ERgQ0F2Cq3s}@?%=Mflrj+clqcUx8z~Fv%C&KH7=~dOhBs_Zd!o5e#F1wWgpfAkgRr`I zd>9t6Z7wDvAPyd?6J{z!9FrQ^vVMoeO4cA29m*y|AZsw^Gu~J?5pkA7$x-N_ll zWW$8fIY-&tup5e$rJac3HoYOj@OnyWJIrWBZ?Im_fq(R>Ujjq4o%y8Aw*58ilSMiT za`lKvyYjV&C!^2k91`bpdr6~%=c4T?q)}@#X3jIlR=^*UcwwGRVh9PmWJhU6MW`(r$Y&BBDAzD%Ilgd?e%Sv4(t^Ds`Jo%paq>Z|h zOB!z<8EI#Oj9?{+>_v5iK;RLqgVM!mq|u(iSxV-tRnnRca!nIabTqB5PRDC@mqWZS z8_3{jTufK54z}%P)M@n+4S0sv-#!4D(=2|xmn z03-kjKmw2eBmfCO0+0YC00~^X1P1f}Vf+8u&0m-@NB|Om1Rw!O01|)%AOT1K5`Y9C z0Z3p#0FM7dKY#=v0Z0H6fCL}`NB|Om1Rw!O01|)%u6+V<{Qug|W0*on01|)%AOT1K z5`Y9C0Z0H6fCL}`NC39~p$|X;kN_kA2|xmn03-kjKmw2eBmfCO0@pr)LHmCJi71di zM_z$Mkejdl6vGrk0+0YC00}?>kN_kA2|xmn03-kjKmrE|+f(nnIxs&DOBk2<48@-^J`E!Z2|xmn03-kjKmw2eBmfCO0+0YC012EB0;iwx zvLHBx@6|5@GB;$(J6{g714C2Au$!-ops!1bO{SLkh+Et6q8Wb2yjqje+_mw| zjrVL^-Uw}Mu0Ov1#uLH}MgtOn1Rw!O01|)%AOT1K5_l>QQ2uX|`qJWzF;>0BBpabq zmVfqZKd(?e6V%VcM)iZU6m1jZjYB=fYe2K&-3Ij~^^5?mHlr*+?lX@m7BQfvnG;W| zQLZ+M=i(}8wEX`U)vS8P^k>VI&~5`d-Z_C;Eb>9&QDEwqgSJt9PU0+vX~C&M^J?P- zbJWZ>`4sH|6E!#~ZGEnqNgmCC+QLUcL+$HxLNnRrD2Hl44_bD@JT;Si^mnKRnu{yV zIV??j`QZ16jf+no` z4f7Ny2kS?V&__Vq_sjDXb<@!(e;SN>e^_CB!i9_S`*T9m8>XjE@iCCTTjynjRX!PE zknyTVLG-LoIEnxX&`~4r2md?u73ANMFCmX1@BbmpKov*;5`Y9C0Z0H6fCL}`NB|Om z1Rw!O01`N60-E6Z!lG*2k@JQ@V5_F9SCp>+T`m6A1+{9>QJ^0D<=gv9<zP*xPPA+|M@l#8$UG%BnvhbaSoa+D4G&Wwa{*m>&kqc{Y(Y`}IW+jD!)Y!`p zk#Dps_0o*n7Y@X5hEQ^BV-U+3B3oye4w|h&!u)G=F zQSIJ4VmP>7=F2I*F69eVDLHi1p>(!V&s7=?UL5|lPev&oh_U?kG!sL(vY@uk4#OJ6 z^h)ej`gxGUR@LtIFm?k0DOnJc`;sgr>mVVWskfBn12LN_Gg2Q!XzJ~=SK@;8v&)+o zFRJ?1s}DWjY4CNC6F>~4fqHWP1!ZX*`o2eg|9$X?aLq*Dvgu% zroiuA$b+Z6Qtu{P1*woK0J7b&Brm4x-Rhul8eiaAps*Wd`T4C}lLw^(mrnE2o~446 z%<{k~fE0McY&l3Wh@r&!twyCSS3$jCQg`5bpxOZ5 z)8Mr|dAm_*0H5PqKzGXfe&5|>stW>{G_bGsu&;m}XBnsltoNnmP2(BWuJ5V~WeWl* z);CJvu5HOaSBv|R>hN`>>DQJw?*|@z@oIDi z6%ww|tnx$=gn|%=!37E299>#aFa(0Epl*--J zdxis%pJ!$Evji%7AWuhS)n08D6kuFE2cD!E=^Ee7@Z(yGT>DB458eQdd~3hFyouVC zyYZptOh$Q7pB>Z`rr*&|bDim;10w_9nSS@GTaPQc=db4RIamLumNw)1LzI!@hQxsa za(qph0ZkY%SN$kIoql6^^9(2k^dq+!6gZ$VQ!UiH!@_6$`}DUC`o%sm;0Ghgy)55X z@(ijV15@BVBzJR=b40&B47-2n>LWHTO@>}F{;hcAK-dRMm#woC<;!#rn-IHF_{d{+ zewJua0&j5#OJnGj2k(IBxkZpO%gATI_o*Af`u}eu|B8GK%mVnr@X}G{8nguoKmw2e zBmfCO0+0YC00}?>kN_kA2|xnJKtTKShfaYdD_`zYU_QXv{$P6mZZ{4_|C!xqfz1GV zS@6|0tB^M-7PbGW1RD?he_-za8SQ@}FG7F@{6GSb03-kjKmw2eBmfCO0+0YC00}?> z*9d`?U%z94Qf;Rw7gO1$p&%px2|xmn03-kj zKmw2eBmfCO0+0YC00}$=39Jp`4BP)t!JLCph6Er1NB|Om1Rw!O01|)%AOT1K5`Y8_ zPe7&pPsNh zKXwAk%Hh0sHph8FSe@g8&YG7h)C;0)-n-(z5D>S+&Zh^mtDhUr>E{D=vQ{9uls}E< zeEwqFDKm_M)AU|AjDt5(>s7;>FxG2_XNWCamaaI$R zE>@GtDt{FW(m#DD+X$BOv`FV%#Uk30(&18EF8fjthZhZHLpj1#%;$X$lnK9I0H1%yC+liLCZZDZ@<%88siA3c_p9_v?|I8et7PI`|`t4pM9T1%f zr#s~HRWfLSD$0zi{sUBoZzI$XQZo*jpNyzb;RH;Ib2Qto$D)KkSeMxZr;00vQ(*_5 zrFxS~jRCgFBubD(GPz^ge^$52U^1Jx6M>MwRd*)5xHp8AB-SOb-Y=|xq~AX;=@Un> zZmfJdsIN&Q&Hl5}EJ?x@a}kVI;Xxa3t9J zpUg9=H_24CeS*hdE%xfI7V2)cX*5@nNvNI=j9xd#sL48!^UazM!5E_p^N!H()Hr{)>87-ht^tI>!X6`Sp76BkkipXdt9B$U4c_HeV$GisEd4Zkh< z#U{m)-Fh=0tuiht$jXggRw$O{CEsA5$f$U}7URNU-qGCDE%?D}vnUMPM{B&!}M{`B~B*tcs~dE*6!y)T)BC5Ad*vyEDSU2JidWc3eAVqSF zyjidKDA=!$n`hL#zm&4sbh>RW-in1|oGUA^-L8iwGxB^={OVx+|GF}&(7sf2|N6Jq zeq$A1{?DcVzDO;6PxU5n8Ge2k0)1+IMH$$>8~~;D1L=GnoOUxjW^c!C+>yD8m!U9k zfKIr=xC`yDhE9t$xOH6I-t>8fza`FN?`2#S6X`Q57MPx zkgiH81CdX8@~yOyvXHJ^8%KwB4MV%$usQ9C=0Xuio;46a7a!Q~;_;#VX3AhLCL)7y zWAUIT9OB$|Ln4N@c_xoW3+zxfAp%*0IiK;yx`~LhTW6VO-a*=vsI!tu6@7xcNmB`T z8zovk?FDx(u23b z9wcI{P>p4DxhYK18e#VEbT`G>&<52=5?Cu_iV^|Fmv)I*mNgTEz9mI%;T>$ibajvE z+5x5(lbmUsa)caAEbQ|=xlCEzkumiq;-ubPj~N;}bTrW>3;H7GtW06*${GdAM3+C-Z5G$DQ6Oy0N&umjg^!_L#07U}`tWnL6n>;bC#Y?Rs*V zmbypA6!W{tcqdQC^AXBPG_!?HB4JEVW7-Q={N_xx=IiO|juzu&^FqqlvJ|m+&y?TM znaoCgq-hFD?(l%=@*dNb157Ow<*AFsBN4AVOgza<*&{4Z6Br$H$9az+MM|c2O7N2b z{iy7fm8arPyJx4{JH=wdRIfF-nAf9AyBQ;43|3ilN|z;rJj3V71bAQcsii%p%Lka6 zZE`W}&q*NyYuWXfZm4sjJ?J5p^f#kJr+!d|L7bdAbwoxC)I>C+M_=QI`w# zh(T*G6dy2E?=f9G$aLb_PoPZ9MY41(_9Qbsx@X@_RQaf0%*2UiL`Ufeeclllrat?$ zYsW6RV%1o*NMQ~(YmH`n28*H7qjsn&V-NVMbh%@S;*K6*>RDJ(qGvAO3mQ}pl)LFl zMHrkwCaap3yWnW%@!59A1hkv*Oskyg&8)%RN_k_w1mm-$JoX|R?^IGoLPr*JCJ>i_ zQ+0?@#a72N4q;HKW#dFK!4SP+CAsJ>a&an`D%+c^VNhvBO>D_7kYc__8e)Zrx73I; zglQ;SPV~5VrpYp(GF*=43_EGG+@tM5KH-nFcJLjiu#-Z&-JVql8*7#}+5!8;S-d=F z#shs!1xRiWNG?1uV7Sw)Rwqa@B!jYeI2iE!SUHaPH8PB&eRLchC`pG~{!*o$Hb#wP zRM#M*?#?uhOts?Ro2fu<$5%4*#a5%fQvcLqQg72+b@EnjAWV2bgCpi9jtQ=AY6}WDV7eR= zIfl>yxBxO}5fyY@&whrmSj37=&FVgNx<*dh)uw(IdVATG&8Hx;PI4SF|B4ZH}7ei{QkE&eP{=Q>0WZ3k&0PnKu()Fg&4p^R4m3_#C_ z$6aI%v0l18=vDE7-kw2kjk%R76T=r6QFco)S6-ircXq%mA1tlUwWC8>e>rUs#8la> z%P@YyM}$I6gCU)5#XV6m#TKZlt{%?nbx{L}Wr=VBZ+muX*=8fIYdUj%?rAGZcKh-r zh2nRVwwW+sdIDq&z7(~1jUJ=$y(1!E*A*6OsUYYsN~ zY^EN{xa_rVrq}VCQUu|YEaVOv=u_A2H3Y6d*buPG8?0W6bWr|8)b(UH1k6X)VD&mm znQp@nEV_eXh7ie^y_PBJrZldcIeo+}S!1m{@AoqKc&n|GD&3Bk_4llKYd+-6GB#(! zLJ}4IpcCNq9@Fa%FxBhiOhK8G@Pr~!H+ftRJGGiM(NT?oBKL?osyz}Yh6NKF_3z+r zRG)F0B)n{z!qnAGaJpO(t40I^w_~JIL9Uvl3zf9c(}kEs!fOnmu9%&OfjOC#e&p1O z65qazgDI5P@|EmqX;cY6a&$cr8uRKt`C!UR-b5AW)Z=;?Pf$apm#k7LFtXoz!B zTZb-nV$+`fowQZrLK$zm=n6J%f;C++;ApMn_Amvh*9c~;0jzEj<2hd%G!P=l9@SF^ zsOl&C0Nt$H7mu+NdR)z|9)2jQnBfSiw!`rZ8;W5%-fj1W1;Im!KKnGPyiSN`6JjKu zYosdWx&b#tyfM2mMtFC!cq^Z0eT@JqGOb)K4XA4Os3Hfc8qD$nfOWGZ>Bapa>dEz0 ztV=|W%vWPv&)-T5;ZDRw1X!I<2qn#Jq_B zA4yo!Si&%kYE2|vR!6gHHCtL{&S!SE%Xp=jXq8N@U^K(Hy4kL^XTWluunkb%*rTeE zQJpAPJ*XoQq~jRof3kh`2upx^M2{Q&1#>7a#DtUw6p(^IOrz@eCPQh5PtsXEgnOq~ zud+FR8!M(le$Ipvw656+l`*qN2-Vtw!K5dx;=DrpXPTEP&TlldP-u9TQOPf)kdFu2uFZ7?TzHC1>?_sCS67dpkKAw?4 zE46o2eH>g|Dw9S(OVeo!ZS)xef;-Gk>*H|k7?R1T)m_hUCYo^Au?*jdSUOdSDxuN3 z(df$3y^=ghcT4|f^=0$%!I@Sx9eFRCBTHc!4v*$cl&)goMAk@k*{Q`&A(0by zxJ)m=vYd;nCH2`Z)rxb$T1y{{W6^Azv56gTE7WXO0-M(a8b2jCs$sz`qrt24XbRkXEucYr87$02 zu!fiEg=@j0WRFGjQ_-~AE`65XsgkugjvJU_u#(qZgbAg< zP1gjPW9hcOv44~Lvi`WLL0xj{h^V`iiVMx7DufdS$&#S;@w7f|PPj4@?=2bP!>N9< zj%IJIL6=NS-nv7wDTk%G<0zzb9U`i$rR^b;FJhrhJBfONlx%)yPW9aWb?VD!j*Sd4 zjf^XIRNWR9!j&_{X}_6@g_)?QWTu!zU|JE=3id=@+pF7=vSPf-h1-#i-<#028+0s` zHzcfWbHI=zjC>_s2`K&m)SEA#Ikq>`bS6hO(O621l|f&GG3v|m6)E6;N+a=8dRv>1n*Axp)WLOr zGU9O;rXksJ<+zLvL!B%ajC;s7OIE_Za4r@ z9Nkne@wj^#Ppp@5>e^rmZ#NNl>k<(i=w0=JDaNG^Xr^?T9%jPnH0UKgq8?XQ&KMI) z=5RFZ2$an2Sd`Ug%hRR^Im9d&b8Fj)T(Fel3TeK|?hr;JX>_G~rIP5Zg|gkIq2rH; z993C(THmI=++01Tmen;q{y%8{Ym^#=QltGF&D%F#zMfgTZ}q0-e_wiZ@we3fZ6TpD zT|49Xi$A9XOGp%o{u%XUd|Ht_^TuFO{*OKp{~GBuM*7cDUk*&;cQlx$wL~XIEJ`PR zZvEOxpmgGAAupc@N=G*b_ITD5teyl)Cw}(z(n+9%pY*xE>Jvffq)&{v?nF>Jx^n4w z=H@S+1WG4@2>RYGJ`jziTFeV|Pn}ndn=?TomB;d&MPIC7V$SC6B%9_`E)uX7ymX7< zBT2Kglk{f5Oe$_`Z(jBG;Rs_6iVnC#e)8DX|32|~)knn`E%V;0BjYA25rf?mj04UL z5ueugT#5%`5t7tr!v+_DMMBP)CC{K{PlPFU1h>`M6n8ASI$EyPS-|+&dyKcHGq%d+ zuDjy_SB!{;;>^kMu{7=7g#JDWjtj38f1)txq{6|%v{46e>M>zB+}W5PGejuCDW~B zm~xbKenZdJ4dWKG)o-;*$%L=e&6#Rtr#ldcMu-Mjg~2^zkMT37GnQ{{6lYl%SfYs~ zj&1jVCte~)kIIj6+QXL9y*w7v=j#M!cBJ*0sdF+TaYHK>s0+!oG1c>Ox{6V!ui|`0 z>VjGBd@PJ+j9j;lhj5||7(acF@iV3~mTyQ#GA@VT;iO~1ljFzK5iv&fVV%GeI=c|{ z5`3D86*#cadzv3dyS;EEVXdT%Ox~YR|gs-*@xg3fNm_tbnDXKJbW8 zjQ6;fx&4?{z&eg)0$Tt6n*hMu21|%vU=0ua7a_O41*Y8i4@Tr@c zC0doJzupMNDjV^7?(zX{+|IcqkQUr+NkRPSUB%o2~X1?aO8egUC5sdfWwe+iGXaA-|0> zg6-ilckN-mFaxuEL?;kreBNjT1-sE7)0qWNd>1}Ue^i+1V!i{8W1u2&J4Q#VB{q)f zz#=2C2uij%5_2cBp6c<7yVsp$(v&>VMnW3x8|xiDmu%!uSPIo7GTca2{4}- zY<9Ra1G9Wk=f^Pw{6m1&9Tn)5DBwXlA4ZSS`op>R@+7H0V!ov_rCcoVH*rXJ)8u8Es&_jIZIT zB?g$!?qR-j24?xTk|>RP=@@vhj%@>}9|CjtsJuBW`gykJ*KxfVPq~O170vIYreO{_ zqeU|1lQ52Uo63BwY>HchdT&lwE+*W@3KI_H)AnphDCU^~<~#N3n4ShX7wJhGHc%Q?^qHm%c=l!wfTR|B&EL42<{23_F>);4&n5H^(|Ff*y@qtr2}9r4xmLH=o(V zeEST{^4-1Np;(0Sgk980^5*tYVGd)lYPg3Mac82DqN~Pu%uf>2>KY!#;V_hRAuF8$ zZ$T=Q!`&5!g$$MbiAXlW5&^oCW=j=1z!?DM^mF!Ro1K}CS$_E1kE5#b#0Nx9b}w0< zyKQfl&T|e&8TUk~pvw~oAKMG?No3q(qZx3ZNtF(i{SmzE=c1nJJM_d|ZZr#y3I)fS z^?HRwhqE-RjV4i%2!`Mz4x_C~?s(`5=pmH?{p78H@o9cT>}vgl=~wXfONBgw)P=`EI^mtou7j zo7?Nwmv;sCl1&NuhXA7 zalp(+)d)&qHcPM=H3xLQa-<|#*=VdmPU|@d6zVxop_vF4dzO3>HHwtb0!PVUSu2m1mCN7-NJ98IL95B;S0i#RpcDPrvgEMqVw%jlVM9dtV2FzP0I!1rXYelVQ z&Kks-j6UQqx$%Uq9AI?1D5}e_X+qC>6J2NS!ifWBJSt$UNVKDjJ06Va4Vhkehr|5B z)MmD?=o!2g~;~B;|HyHoFP4S3A@d)xO$OY~Dv|-JcG%d}_#%ngt zum8z9x%T*4ZB4cMi>qf={&2;y{L$t3(!VXeZ0Ux@*Dv0s{sT3-@aRHJ^|-32{HF4! zl`DsC!cGdlNU1IDE~-})LnA9IPw*7T=NMAY(V3pZR&5qK7i#D)>P1@8^(cZdWG|R1 z63^he0?L>rUCu@jk($3*s(R|Bi&e25x%8^9exz}m=D~fzPw#Bf%SXb;p+;;vyZJ)H z?&l=GH!kXYc-)EdM6IZ+F}9l2)!PbwHrKLOJc)`icaf`HEM2gGP`}W)^)R8z2ZqO? z($1o{!d5G!*AXeQ(Tf=-;0avFH=DFMozF&fEpNV)(FxrG9TgI$P&a##aFWANw+|Dl zeE4o0YD0{6^D$@3j>T+bD%bP{%SI3H?g{m%UA$0smch&Of~R@WTNClv1)UUaJADQa z>fbiD4il<;G;18Ho6y^gUcc4mD3*dI&_!VH#_AUY8}2Y$J-&7f$IG4&YVn0rW;aK7 zM28h`b0E~uH=cc%Q00R=<4`SlipI%$$#v0I@X)0m>#kOEyiFe=YCbw)>s5RDvYw7u za|TB~pBJiryDd8){JF-n4il<;tYaK1PhYH~rG&W=HS1h7-KOi!7{lkhVx>){ts?E` z-NsI*;;{;Zr_j}PVm^1HFbGw8riT52J}mG09|tPZr34+M+86MPc)1Cx;bm7>;?3<` zI}@-IOjIb6teta`CKnfBbAqeJWU(>`^s~}44iTulmv$UzI73t}Bv7z7JB0^b7K4@W z*lm1AWO8Z3UT>!8PE@R0eeQ(6;j`(|=|ToG20);HBR%~Pfy%cn7zgUi(se^5(kq7w zH5*%P7xW~}G+XVwwTMbwG|_XXa(uf`5Od9(!RaqjzCwW?271dO0+p}h9|s!ir0daG z-rfnM(lIJh=yWT7Q`77g4TfMj?yY9Ro(SIZNJ1DDs%bF6A;^R;fIvSZ-F%2ZXB0~T zecKml87k?bls8qZnGhWedM<=Z79s7Iif+L#h@k&HcQITs)0KJvbLB10fg^ody6F&s z&M1{iJ)ehR{a_=IoWtF#H$A?Nx69|uq$?%bss$@ncUJ5cn#UR!FM7(x9-;TAor6I6 zP0hXgy?4f<+%ZslChuk43Di=x+H(?~(}80mvdxm;nejUlp+?M94VGdKT^O7q>~}VG zAveXK1E7DMxlwb^{$Qq`+&T`$m@NnnfR7HOj&DYrosAaHn+{@ zYO`II$l(`rV!{<`f>1uyxZw~5z{K&U<52yzQox_*!np?O%W!V0YtLos&7zODlZC(5gi$mgvsl zHT{@y`v0)^CQy!RX_;W;p80ZlwNndKuU@`*wGc#Di)BmIP*v7qOR_9mwk275T|~>4 zY+05iTb5 zon~nm80bE;Tsb2%G9w)?tSIMFkgijHkrB4O|Ni&>|NZZO|K)hJ2RkJmpjEkOZZpvy zqDJFP__m0GAgz zn8*9HFpA{d-dKl6%FwbTf@Vg!a?Rq#A6XbXglQvDHeO$UPkIx%w}Bk$}MsJetO2&u-&EPWH9Mq&yMztmUh%X0jXp z>hRj@!9CEY>;TTmk-{9}B;IHhT}Xo#5Fd}LY!8%?*|XfLVrwYVp3S4ikR0pXprA*1 zFR`h9)xCQB!X%DEbIp2rHr=axQfVAcGgWB7Hq1RS$AbREa+@3|P`%+yNjs{_LMw|l zZ5+)es`FRfXI=*lKH4wvU~F`i530(n-76?atv-V-yup<$gqEhAP;U^D2DkCzpcZDE z8N5pB8K^v2D(YWxuN;q-J8t4sdziEAT%ATlMJ1Y9Np6^vYKbc$FfjUoqR#4+s!cT} zhWychit=n8PelA*x|fefBamapR-(K`Soedz&Hz1?3ENsW+t{1UTcByB%7f6+Hq;r$ z0}+BMm>f!u)M{I|(Jmd2_FzBD15WxhwODvgCd&97k%=U-w=jwpvnLKYwHIVaZqRfi zU?7u?UXf}MqdDkHk*tzmb}t@}hT+GJZ9J{#IwP~d`d(&Y69t`~EUblR*D!r#F<9GC zs&Qbk6?jbXu?XqH7%Ks^f8ky@9t}Ni-rVA#-0oWa+L%CPTCisIM$g$>#2{$M=;lWa z9);{4WSdQ5HmEyZC-$x7?(;9)^RI(;oCKhgqUkWm)SBC6fikCcq9^!!`CyNpqgD-6 zt0@XRk0Q1%Gec1}B1$$#iSqiT-nr|AdsXu=?)bi&>^etEWF1Udrr#`8^sK5+fVuYbd}U$|Dg`s<(*!0$aX2WS6px$?Hl-*@@zF8$=C4`2Mni}eft22=<9 zgY%2?=gJ_sK2XkH<+hHaHS?#RIvjjXMVU_a8WP!waPsI;nC z+Yz{m%l3Q-Y%v;6O-DW~E3HYeOv*PRM@^gL!y&Z} ziN_pJX7Fr8&u1e{nuOU-*BK2AvzM;y44&H<{j+huW=!{ z4(c%{6o~A}<0vPyd#DK#4Z0589JKaIc@(SXG!}9yv$kAqp=bl_xt%?$CAd)xRuDif ztb=;Y5rr>jJvWpWMK;ei2L#4dE6s{!Ee7yx2BF!a->58RReF|Vd1Fpy^suRzSH<}JN?{^8xuerTZ&?(};plDYV3T*GM zvEfJ7K|RKYA;DT>TZoAO>aA8~H1xfEx**{Y$SRcc6#d#zr^+=Gf;PaCSF)zPsr zKW`*6SPH*!olzZK=Laf|LZwB=9Fc+)Qd3!l3pj3-ti5tYuP$N}UM#A3P8<-jFyX4E zw#VnDtr4)y0O~iagL=$Z`wh^;SP{$Jg;8egNsjBmnXo#GkutZ}SKY2D+1)`WUn_ID zeq=Z$AWegWm&Ncet$~Uj<0Ns4{9ZQd?nN?LcfjT^)CZohH>O-is4+xbCC6;oszm~5 zrOOu=j8K`XDA<8tBf}4^gL;h0QlVB)Y>Y(Zrz)W!IJF0t9UF@HDvv1* zQ53Sdz7e(ZnnQNxiHzVOz7Fa!S|}e`aiI&bxV#`7xRlk4lUcMkXGY^}IZQf@>2(I- zaciM+;}YU6a@{48oH;|<}N;|K(%Uax<`24Jj-cfg)ONy zwq96_crE7u!yucVsEO{|39)rhkI_OwmQu~O9~5n8ZqoHZn<~$vDL006DJTbVWH*pz zCfl)ULCu;49M7Aceh2UCAn665z9&T1K|Mw{4RW(~I5qNlY$}ZLg}$KFA!D_03dIPi#%F8~ zpVgX^B5-?zE`&=wZ(#}QyF+*lROA@lRGf7ugUTW>_bN0}m!i-pNP=bc9HT{y)xI*L z9I@1jW*}g^MekL+SdHu_y6I~}XdTpJbQ3YM+}t1@6^l6SV#7!%l*-s19G50l4z$sO z;&~=t5{eGaVxW_9%|xsi8R~Q(?3U1tW-PnZQ@|+B?F%bx@DdO>A$W`%@M|rP{<+M{qVCOd8b;SsQXX z#gH9o$dsh4$rNeHEqB|r1=pa*!Xf;PJKCaW%ek2co z0|Z~RNvu_@X2qHxjY4Y?b*6gQ1MceAz9#(obx@DdO<@7^N^pU(af+Bq{-DqgTK1IB zcbT$pL-SlgF^pMxmY?S#OA2RQozTccH@z*~SqJqP-Gt|)79$juTs}u)ZA@vhX2eVp zvJm<0EGrCKcC%>=k)V=kb8x@P=JRSfnb$XjU$+kGF}evJ=%X$y624dG!v))cxX5&^ z-pC8ZJ;hO-mYWImEJeA{!s8|Z=qVd?k|V(z!Vj*4dW>#bh)t|bQ<0~dstVLR9(uArEisU>p*- zHm?U#VY)ZhGL_~y%xJ@2dvE9q#hlj$6^1jEv=|qi#9#8e;fw2_9;2J)PP``4YFv<#xso_xf=@6VBy`=gM|HM6<0~Z3+<<^w`pw z(WDu7^}NuOJJUQ@VAZb7$!}FfL$jw35U%H<-EdO0{{(lDC|KIqI>*udV zSAOTR4yylu_}q`4ePM<8FyN8Twz_mxiS&b?G@CyP^zK%!Gcu-Vi4>Gai9lgp5F~)$P!I_7dU4@ zX9B8sZ@%5yQ}XJ+ef7rW6J+G5+JAy%eXm}oWW zH6Q8I(l(Y{`A`!74pSx9o{hgcc=hVV*Po4}hQgCG8=FcJd^k0z4x6g1JsY3+z^hj; zy#8z)HK3lH*?6)`TphMwS$j6(FT8s7{OixgQN!}dnT?HA(0VA9OAlL!tUViZ_|>cD zUVk=@Is=}Z*?6)GqaHRJS$j67#;aG)zW!_+b&@s5 z|5tCk<@FCL$fHtl=@0YV#*%{6mQNTeJ`q5%-6}(%5+qvIs%&;4XV6>aBiAO79dUExC!165E41!0(j2$C)m8WK&1KCw$TT&~X6s*MFz$T{O;c2XZssUDj&Cu}uz zR$F^Q%s+ee#v4zVkfWCtPtb&HuAW4*$ajhiCNd1t9?oYCm}jh2sl_#_Gi+&bucHp# z8q#yT!5H0-_TjD(H>uhn+O5j1T{)`EcU7v_5UZ?I6022V?Fngo`>QwJaKeNKu)I*Bm4(HjDX!>c^F` zH*Qoam1%?Om06x<=qBCb)}D}$EMC3w>p>S1gvj;;c2~`AUBdqE*GI zx~gOw-KFh$uavXn3D@9vRWgTg8Wf`Wxu&)SgWau1>b}WmQ7WK%6^>O3qFAHpwI}2q zFTHx>#_LbW0|kgK{IF{B+7t4v{;M~xo-iR#uG!0M%I{;wB4cb+)=yJx-^{2|@zmC8l=Kl(se{6pG$ zVo?=GIz_@AM2TC7JK$)jC()ZTz=Q!GNJC6926{;^*%ML5YL%r$%)nZLS2AI}B|-_V z$?~wi$`+u;tuZ{Fg_FTh33#bep%v4b!sF6Dv}hTKn=R(*R_Mn5927afJ9}F{b;!;~ zWLh1^&W)K*x?3368uM1!!$vJ{096Mhd;2~;1v>?Vv?jLFwZjRnNv2^DG@qA?5{L+I z){EV)0_AgkLfCEWcJF29PrSlk%zWx&V0AvClS3?)RxQp=HToktLDk1HgA;Y8NVP%6 zqNFId+Zi<#$55r7jw)^km3p-X33Wii!K$vwxLze@^W1FC^ret6RoSE5u@I{xu9?s4 zV||*-Lj!1FWGhnnaB04JcWFKl^z~>Zo!M9&GSKjW)|w{L7|!ErOD93da!_A}dZbvI z#UeOUHSy`dfG|FcyR5`JO0&dBEJBG?Az!KYs+DR5ZOyoQm*y7_+4+brsgIF#W@A;# zM0JukTB2r9vmvMl%(sRkQClj<6zpV&ky&!|IZ>mTg#hwy+j_lSD6|xq3mW*OIhl-j zx`GV5#o4{={O=Fh`G{_rk7MV?YM3d*xm0VCErF5X&H%#_ zM!RFZ<_Fb=%+WM=pRoV%Av+(@Y3?|7ZtTRSH}s-dVuA`=?3yim(V@bHly)l;18N59 zS;Q5lFbX(hw}~(6pfMIvGslH!qOo*+&QFb*!Uy*mo&W72J0Fq9`#5%P%&T2RHAq5; zlFDG3#nyMLUY+Zt-HMnwF{YcaXBNlJu2p0!?6B(~W_)yC9)iT{9-tc|7!cLfs)|UQM%*P%rU%y$|!sy^H;q4%zv5bw_1m*MkaNH*^&b zt)h=MTT(0xF-uEpD3G0=LsiX|symU|F`%~In31_su3zmBT1~m&%_^jmhbgEDI+7jk zY+pKL=i~KGRW|0GYAC#4fq5SkiG~cCwChDtO7HB;6P1EQ7f0Z-uW!;Ux?k6F#YwQw zM}qGYJcajtj*%rE*}a#Yzj)U>^~hv>rFV)0op%UFQ|Epg6``ur%L0c^IdSTU!=*P1 zvOtP$;_d{YJPvSuE|y*;r?~VuwTHx1L*&Hc~!=(R!!2jd`c) zrMha-WLRm|4OKCX%ED&Nv?r{h#^4Ac^okQfhGz5e?y!bneOb!przNUDD2V9Qi5M_a z0AVzTlKu;a?0mf5DQ;ulDRXF+s{4-;Z zPQwsE@g%EhqqWwU?#q_k4L}FjLw5fBAv+(hcZ%DXcgl>hU7@edWtyyX8k4LchfFQ) z35)FYCpu&<79(w+%QhI>$aZ?!u28hu>bR@e@H{N)l}Ut@YxlDApB}RF@p`AYjd`a; zS6K*6*yi04RV~%MmIJe5T4Condf$U5y0~kSpjxs$q=`NZSva-Zv-J6x5g(ymSluc&>3q)4Y?;hgX0_lvGB zEL6*d;d#D{T2rsr9x2d(1kE%Kh5cs_+4*?AQ=9A3RcYu|BgQ;Kt$S@yM!hk_^mbaK zvrMW|tZJCu(@Brnoml9!+*i78jN5f#B-^B*kE>Ni%}=a*+4(bv?0mf5DQ;ulskTDo z>Q2bEkr36It|rhOM@uX0j9d4({scpusMZloL8&>xQ0>ramF`rXYvXde&4+L zq4#Fr^X3+${MED;_sy znC}k_yNgRcitQJcpC?VM$n^FHc~PkHiyEqnpt5n)&2pqwZG&RRj7S^BoV+NM3O7Fn z07iiootnVp0bx9{z|d|E_X0r8UdQl!uir7;eZ%b=lZ2+^_qQ2!^JK;*Uzk|H0T(0> zJ*L3LQR^l7Qpfi~z-I6*!1X0G`2Gthl%Vf+z}$q^kvY8i3Gg-38ih9aQWpHYTFiB(d@_4eXjS1v#FUKoOwYb5#VzG-!hshh|s1lG-6wfONn%EL2;iRfX; zUk~)jeWPAJ?(lDRe-ipZydU)^{dV8&hl!X1z{5`*)kgO}+lY+NNc0T0r13UAM~J+w z0S*aY(gyAe?vOd?cLsNU4lL8?B%@n?sP9Gnr6yTE{EnVM0dWse&)@`q`KxRljG?_{_FG;v7=*=BRkc+r>YCSV(PW=IHWeV(=i;*Z-Q~IE$ z`Mi{&d2i5U?N%QR7lWdyDSgmnP6!ux6={c2tBpi5{F3!GF28CdR!jNJHjB_sZVjCpRbmx zbhkfka8;?+r$F}!5^Sa5A*b2dW#7Sp-vd)=CCCB!<&0=ZWEVn5sF~ATjMM_VYDg`c zY~kI08yzfgyTwK7u-mWNUEepk?81Y*ksP($kl(S8h1=^UFX&69w!Gr-MM^6 z?KUl$_V5a2G7KTA&0Q^0Yc;w>RIB+|cKe-b-OCDQr3%{9Jum6D7>J`;wVho3gSI85 zBgf)(4phWu)$WNjB?|H@_6mIUsa*oeqQ|-oni$S3GW|BI)g9T-H*F;sDbReO;zY|X zRkB7I*%^oqLyb>w&QI&0#&02>ild4=(3~8$W9Ct(F)k@Nby4daGh~kE6Hpikip3XAzjOY)nTyZdC8C0SQ>lhcyp#!!1**9KFR{6K&&uq`Q z*#tr)qgtu$i=YrZE7o(>-7)Np7PL@U6iZBl9QU{aqeGi9F7O9e`B#AhFV~qFGtf^W zbmzVlO+b9HJ}5@;ey=$bS*4E@q7En|?<;+{xAXuWF^rbJGvHbR68GrO3wOX1SLy5f z?ml*}@sk{RPdEu2@sfcroYLSY;9M>TzP{2F>dbL7o^*&&L)y`xDFe6Q22HkIdWVFj zSyHTm_O_7QR^8m})TR&(XLlxSuCT~L<`S4v#3W6-lw z%m`=)mK+hOwdhKTQ^?+qS*qq?nQusQ&4x&Oe0qtCf?1<(En4(EENi+^;$VTbR@ic;OF6uXs5)3bM=Hq8=jKNo3ezgzRV`Eo8=M6|x6&|nWv ztcch5auL8W3EesWpV{5I_Jzya=f4Pkc=|e?1D`!}4XCG=z6Dqa5!sxMUA(c|*C~wa zO1qqnh)oV7;I7RyYLs5Z=hRqY=c5gsP2rVZ_!+eeci4z*6?CQ&bYzm&N?IP~Wxc4i zaJ6C-8;N*g(TT+T~;JA$P}8Rng_!)7@XMVj>v zAEUF?_wEtbhWu*3T90(yjcY8a#}sTf_8|)uB70X6Sd;)^$Z~f+S*4#=Y|VhIdoB1! zbI4vn$7&PdAuFsbSYXE~tSPdXc}xK<3#?4;z}*pV1$AW#ni;}f9l_A7PR`4PHtPi7 zs0to(8n?W&*qKSC;e3!`GyQ6DSj@nM3h3!FRqe*S+Mk$A4bJOD)>W){V#&pNg=ipy ztb`AsGOBlmwdp*nM=WW(g`omHQe0Pt2;Pm`xWbZD1Vj~<}Ki_*yez-9%1WMqvJJO&4GtLF>bjCPG$2rq^>;B|`@y z_qpyfiO3}}E-9AMhC~itWtKnkZ7l6RctnKm3-CtasPlH?z;#Z#7AQuA7!pC~W zi|Q=Xt)oVMiqc&!L_s~7nZCned48A`)lm(p_;XC27XLO zTy`8Y9*&gEWExG|J5p_AsCI#*#pX_*)SR9;jo`W0pv%O3f@gOSl^j9Sf;ihLk_CMh zv?Y4Iy*(NFa&NzKXKzm-a2rxI&{wKxlCAT|4gJmSG@5F~-n%-=<o zaGRCqe-jIWsSOflY9nk%oM;(Q842?eMu!W_C@q>^x8uhMyKubI-~W#?CW$JFFfVv2 zUUb4}60>5^Ky14eb~0ACFzonHjz`5pv!^&Y$mR#VTAv+yl^BGybm2A=H){G0)wldf zQ?Ovh-;vgfUtJFU?)m@cwyu5V%HO&8^=E$W_`~nhzdWHF_+0l5cg!;qefJ4RVxR_0Vb>YI1dG50(ySrb|Q)u+YU7lE0wv|9Gv3D9A0^G(QjyB zsOh2+(Ac6iFAt<#ST42ZEqDk{p@uJj=Dg6jj8NJ#!hCxYuyS_TWviYSR8>S6=OzPY z9B2g&w^@UPRa2-S7O}6m_-Zdh+6SaC&rW31MlSK6nrBbEd6v)TkBx}TDmimfXi%-7 z*B=zBP(z#yWNC`@M=?o)ObtASMc8gqlhWzUGdJM#LR~8v9mJeht1vrmQvB3{rdSvB zeyz#wj-F}uETzSDo0D3dXwM;BrF>RyWDpN@P98E|Rg=p3c_{ZgM0rPN>cN^9q{+~i z=Gn7%%(D~%w;@Hd@_HtXqFHI4rG3B=#j)Bvdo(KJFMxTLM~K9qy3#!B^H{!24C(Q} zv((DMS4hQR_?!vnN8?h(CrPiG36U0)9pI;Sz2t{gVr-YlavXO$O=RR&w7{0g1{sYs z9zhmlOJ-rblF^Z_3u*u|J6$o$1v7Wj4 z^p4G1`FYHvy-A?T{&?&uD1v88(5aVsya47F=8WS3)Gx~{C+McUBUYjzy&$OUH0Qe` ztgmZ}+RSTB0*_&8IBNBAj@CE_U$E>fW00AgK7xB9-xy|R%nrM=<|!l@da|?3t|!a< zTh5%>E-J&ORhr?1-RW!lxsJY2icq&$ispu+n7LAMH*jRRjy2}eh7SHK)96w-I+qt^ zY0mjTrETD1ZT$<#RTpc$sWl1rjZVj!JSMmPeG~xCK1gcYPh9=WW&DUDZm?|j*y}XD z*G5zZZIs446`{@#Ly@btg^n(b_3FU1YBM&PkqaAZ#izIF%GeYU4JlXd_DdbLVN#1; zLmNe%vO3M`*u3Hkt{?!fnh%*{w?lP>e5T=fY^T&L^ovN2YVQPU6pe@Ds7E?;m?XHaE`O{8V{5`Nf0 zT(3PWLS~D1IjTH>MW|jtWmjFWjcmWqWo<_(*UX`A6u7X>TS8Tjo4N%G=)7X)?y#9S zhRsJ4(q)q2%N$)U3QrK5H&%2xntOTv@B2KZu7qU3RP0+O7jUa=r-5Tr_JCw z!I7~t#0?vRNnVqcu(~X)cEoNj_k&)kKh*_ik+b&;s3*CRNnuPEGX{}83pI)+p&)D2 zbvQBT_U^D5K8DRli==|4RFXtD+1j#s zJq8n2v6&6TIm$^hRw;)I^K&I9aVT}bb!hp@Yci0Ra!{(!> zK&9T|s&!UpT2GGBU^aFN)I4z}w~C;?42*#K)Vsjj?TV|8CtOt2rr6lS3MA*Z?2uE% z0W%XSvDqsJPL3)<97_-SyM1Fw^^*Mma~Hl1#Q$Hp0{-*#^^^loIq;MNPdV^-IPjU- zo9|qNRH=~BXsrb;aMMM~1DDt*7yG+r11mRtnsxSP(*jL&&|-74nfpFuWdvl3kfP;9 zNQ!I9<>ymG60DB>2`rh2`Z)z(*a%Ui=AVfm z0d8jQ4b6TuHnPRCv>CD|$i00b8`A%LMOzl=PUSw-l6ULAt zo0*V8aQcgo>4eunp)^xf1_U$I0;@1Cb(5 zFPb%FM~dv~V9FCbCruY)zu#{3qBb*OKo5?!@`{q7gNu;Qo_W(!@<481iWRjP$%9uE zp1HaG#7o`;rKJIE?3#T-X}Q%3KVn<>uA+}x+1a2|Brs+kmBw5+rLxjm5JeG%0OP)7#l~CWQ``(~crD`EuA@ZI4C_23i4)fV& zokhD7xY0(1ni4aMY#<3ajTFP3NoS@N!1X@gq?=u-jx{y4D$eaiX08sL?9kR+TL@hp zE%sVv-p^v0*!5IDSo1P68F~^1FnVJmgGp{*inX;N8C+Qw=ZTlWGz6}Q)VZ5m_dpQ9 zquKlJ;y<3;K0J5)wpVs*6`h^-174#XsXC6rQD-L;3tsFfv@SvF#MgXWD+r6qsUvU5 zl8oSpSe(Zbd7LF8PtCwc*TZXWwdi1;9!*Chq-^_TXDB#g2={bvjQCV9He!Mo^3)_~ z@!b(x*J7&HGbAFfy7@J?y=3SI_y5mr-P(Hg*0WnTZr#|re(U@NRQg zh)LO~sf7XW;=Vkxvy>GM%H@vbPYta|fCgcF%bh^a3x?2PN=0Ghn6Bn(<+v&~>0WGNw<8PM#_!2+Gn%dqhetwB>AgTgU?Y;?f zezT*7a@o!2_mQa^O9dBnXmbj3$uJ3t=yTiMd>v&C!ey<6K0rOnbXbNf+Bm^Ac&^v8 zI=B=%ZYh`x((}Gqi2RvgvwUYbD7*9G(IOKs=ThV6j75Owc5zZ^g0>I{-!65hO=cLB z{dUkCa)H5@W5ZND8)e#Y z%M0Z-@Uu(&{mFi}+9JUUJQL!l9X&$tiUPex{NG;UrwBRPF#lgjjlWs`-%O3aS^hu2#7_}!vSI$eH#Po7`M-?+ zKmYcvXM#(g2mg8cddh*P9C*rst8EBd3T*QjGz_J$8Q?b(B8k>OZ}yr zY0Tc$f#NCb=2hXUt82ZT=-}8$dB{Kawcbz&fsZf6%lIRSGO^k`-l01!nXXFXY`MNu z?$pD{TyBDBJbUUavOSv{oH`WGAxXwm6p2v<(x}zqM%e1f%?S~dGCT_RJARYbW7Re5 zR6(jv*e29)$lk1uH#NGXbuz3)dl8TI+PQjhV1`~_Ux=f^I%W264;_T!ecF6$B63Ns zw-iftLn61Lv&C0Z>+R8q#aCUeH&Aandwj=fNeSC3qSs(U&-PnkB+2z+TrL;ASzc}K z40H8ab{IhAKFgfGuX@hSs~%M@3#Hk7(NZ#?IuNDWXba_tiVRWn5ri{FFK~F!(=DoS z8C}bwC0782M7`mH;pL2zcRFxY8|>5vZO-ZVVNa&k={R+J=wO+D{_0zjW&WKXJhPp` z>u~Z(cCDRdWjBcw2a40BIIZun*i1@Piqri1JKduR2)}=)JLvWT(n8GsWaJvLXAG?* zRNU})L2v51pGdxLjY1pzdyST=YbKi~ zP^WA3h(ZtsC4{W3YuZt3Rk>m0^c_;Gh;((%OWBc-^CF6=&8n!`jSD#=FgqG1pL(yu zri(eoA_W$g&1thHOKR3LLJf`7d6DX(gcsI8nhA{>h)^VpHMU2NEA|e$;O)k2+NJw5 zqC@qV?nE~c3MHl`v)CA{RZ95o&~H!d{!)QYLL*Ej^(7#|@n|`{!@cA$dv~(~me~o* z&+QMaI9UJ*`#ZiDT3!hL`SF`~KbrjUo+d%*+iQd6@&F-+w%>h39J5r8mQ6cK>-}py zp*;D-S}Q!4d~)RqKgB>lb`bx6{tUKt^}9j;zwZM-Jbj(afzR0QTpo@(XSP#ZoNmZc zTUl{G#r;XLg3=h6%g?7c1Fk-NrMTc+wIwa}J|O(w?A%9VW8T?unxJ0ld+R_eJ-Yg& zD3r~i$EE~A@nGJc4TTZEDA{zbKM8xjorO9Mm2cH%{kWFFxb}hz)zRrI1G7V3iNsD7 zo^_laL(Ne_Bigd(8lZI_LX~&sz3E8uav0jq38GeT3ze2Q<;YeLb#1dV0g2c0i~((f zWn|W?bp{iq-6hww7L%a^{q)&0??^-~X}OtV32jK!R@R(<;zcdRUG^$bTaga?SOjkc z9giU`7lqJ#F6sNd(pUqnfyX7!cllv$!bnBq~)eqTN{$W6-DT8Za?ud zn1;Z_&+@v4@1x)J?z5&>wB>%NEjNM4!MS74M+~RHe11NO5kVgJLWP4RuZIO_)7Hgy zi&huf*dKe0GP6$a3pB&A#gQ1+ZSzvp=8;JzAA6o@!$ub_hj!QCi_KW?&W!qwC-{;V zXnAI*l4o!#Lyo$T))m_}XXI&$CyIp$)oRpxwKdO2$! zg7_(1lAEy@)0&(|MCkQeZax~z@o7(|C%mT<@VsNhvHkyM!pP;a6T#<4a91c!NHNy) z3zg3{`UI3yv#pMuBbgG-e+Bvfpr&MN3RhSUk94@emE7`zvNQFas0#uO3w>`I_ZN1q zVFftREi~=&5Rqz`!Ga?CeNqt=X-1e)Azxbixc zo6di3>&xffckX-6h-ZHO?1#?&*t37-#?Rh(+x5Yt~QF-+cMEF6S&4%@C|vk~^Z)P3N9C<8_y0S1{o%7)XV4GATU%EypSf^>xqNLKxwlYe7Rh4Rf#YWg zx>6WD>-l>YTh5DyQP%myH_ZDnk3-#g>^^M8<-tJnce|7}*!Zvbm{o=JyFDy~t zki38WbS>2Xd<6CU_Jh-L(!8EOMBS;bh5BEPpq|^lue$A8V?DP-eKvXjx;L+d`X@(F zFK;9FmX%)%_3{#RIj_*C*FycjkDy-KM((Y$z831GL)1)rE!00cf_ioPzP=l4pZRU$VmZ+mI{@DMq zE}c8q6FNU~IMWxlQk>MD_$}+w`P?%J zo%cz3it-T$bl$!LJDHP9DgUAGOep-J|F|xN@4j+W%2U+LI3Vyq%1>lrLfH=}%lDsL zm%jI2PB{3%L+QD+JzKv3#sPtb+czfG!Q~LckXy#f89AGaBgS>9kw7k0#_U)zSAZY5qGuye_pbTui9FN5C&^r>LlKKijua{S2c+Joi%w+eQWt&K=My@=`>(A_=X2*0I`7j(DN^YV=seIx zCsMf7Mc?@|35Eald)B4!-Di*Lq7?N5?#y{$fe?w4r#hkRJ9^9aUtE*E>*qhe^|q}a z+&WX)`oRltKJ#DB{fBdfvw!>9`i&pI@uus+wO_gRfvc~8lRtXpuV4P7%hIJEzI5)Q zeCDri{o8d;{u}$uCtrNak>z-CJ4KAs0T=Hxi%yJYTI!*H0wv7+C%?NUGe5cW=7i3B zwC060#e@&&T%k2jq;{!~e)eA`)c&K#B%$(`|9D+0Uwr$KwRtX$%l?4M`&8wLbS_op zFKs1s{?fl+m(J(DCZY2_Rhh!Yen97esyvaxrKNMZ({XKBYk695H3Qf5>kBEHI;pf zN11*{M6?5H@6j_SQn}PKfB3&7RQ}QTuS?~N?@Oq>&m?Oya8Gtej94xyLa|Qz?LuJ^ z)-0Z_&2zBHP)d>5CBm8*-{I+fdgeqrmwM(8{&GU+e_Gpx@!X3?r049m?ZZ543(m03 zQJxjjy96_C*}Zn;HTOF)N#{eXQ%7256u!gF13hyhg-bp2d;cJz@b}-kE+^l;eRSVz z?zU_DeZAt9YD`Se=62H{!nRP#vz4fmcbtBgZWQ)CP}T!Pr7lBL4~~Z?5|~i-AK$-x zUwb{i`-OyqFC3l|E^SZCISVhAI{dDfE23dVa`W5*vAv4jEGuxtvuM-~!*BArY1^Y4B#q4VGWz`Bd`x%VV=-e=~dD6Vus=Yje;k;0|=`HkI#!ryp#T?*g* z?xX4_MMb9r0uR*Bi3BE;{hKq(_m8bj-`6G_yiffEEu({m_=qU$i{iYCw}ruEAB!Z` z4}xNjDNT08f+OS@+PR~^m+I$4GjXYYzWg5&`o8@8Yt#3xBlKNN6NP=iz$C2q_C5B6RhDz7^W%L~bs%&#!z}LhY~dYf_u!|DQkeCtKJ4 z;gw&zxDS4K`uYpNfzNf{b0@qkg%Uq`w_Ya)4_%pqmqGy7-(%nDg1&r5P9kMbnrJF_4p#EG5h1juNB z?dx-AOZH8Mz6>3I_npx36au#)MYFP>VH!n~ zL}rfyE4r@^McM}(Q5?5T%R~d*8`k}3RK{)WiQOM1S(3?-A)(_dOgy}kTAKt?E}3;V zpEF_KY4asdn_8O(QG@_?Wwr*d`fyX+AxCb%ZgQ&E7!mEU((W~}ab0z>O1nK@WQHXu z>~&5pa-okGToVb*z>RkNT5UG0#N;AB=vQkz1LwIOKjMag8L4`;IS+!|h$|GNW@D(^ z8aEp{s^5SshG<)ABUH}Evr_HZTL@p^Kkz%QCM&ytcjSN=^ z(Ta?R+XtG*BNj_RjI3bN_q{|gvI!YWg8U%)_nM~keTjka$ z1xP+7h5>>NYXVjF8d;?V_9O;h==!s|2 zH-d@6(bBMj!iTIo&8|TF(7VSkmXX(X)qWTcr?-VidYWHH+k1k zOA#E}C6G*)jI!SYMHHriH32%~<2PL&#Aw+5B>YfvFR}cUWg-{`qLd9#p1$BGnx)_9 z5If)sgb=2lKQH0rkSb(amE6v_HqCYd0}mTK#FOPg0Y=QCMJQ9DqtBXx(VJHqaV3M4 z490CrEf?wL2ok1LCe8iYhQw%P66g~zMk%iOR*F$_DS6b!c_ealGh9Lgo8Yk&8X;b% z5%PeNg+fTwb+@ZW#f4;cTu9`qmZ9>#!dB$DjSM>!)u_!*UzU!`YY)P{HUR^TCatRvsL(AMjozbM? zfhr7uoJ2iMy~H%L%%u$mvI_UZ#6p8HP%@TacYAid)NKYE0KMpCS;4awk&3aB^J^cq{*fsfp7Ors<|1{UWLZ}ef<$m5Z{yZFgz_|X>{ z!4de_3z|q>G+I$p%ysYzUVs=RU-9c@sD=>+u7stMZe~Uuxrx?;(_hdyc8|?cGF6XC zyzR6L5MOJ8;;d}Vr(qmn3bfOy7ch4~I)!%R4~tE-B@Y~%hjwyiC5#C^#O<-p0iQup z4Aq!Zrk1~+jR_?~KREwCzt!4$_SUmoH*Ve7x_;~W*0o#Lwyxf~y7kPhXSS}~y0Ufo zZ(N2h{o65-|OZ5WnY6z zzoJU0Ddv?zoG;DHQb3Fc^{mkBX#SYg$0gLrW_7Pq5yMm%UzJAjOspOW1bz9NmiWU!6c$!5r!9K4 zR_YZQWH6ZINzY;^Sb%2385uG0G~de){Oww|ONP4zc$QZL*}$3cEULwbEuwU2E#Rnb z!CG#>W?{kdpD*?2c1^BCVY#9D<$5og9mW4xYJ4-O7KjF68Tf+Lr=Sm&Q{p>svoEqOIJ>6tSx#4bHgeNm`Zg_T+F`>X8Hic?r$M_^KHw&PP z;%p84mzMZ;PBeq^u0?CDP^%WDQ4?+ZhMq^{6709Z;mT@Masm}mMQFqP*Hhzfmj7C6 z{LS*ebsK;EIo&Y-)ztW#<-fATPa)IW8^!O+=PMOy`_3r?&SeJ^CoUb)5{G}s;5Mit zryu$I^9!4rXdPo_DMelo#2L9b@`-LU9ZnYN^3g~+@{~1j8tHH_#8ef%kRGRg1PAT0 zTKQ;_=skQpEsm5D=d|-vONVpn`KjKIvzq_+@a?qY5!2zEdOVdQI4kKr9M5TSUKW;m zB1Lvenrw#WPs|T^xttn5MY6&g_RpIYLlxc=F2|7$WeehMGQ$*!4~3#su_xIEUt|L77w zh0Axt{QpR5{EhPecKrXBwywY9$`4)m#xq~K?-2j=A74cr_{`{icbumwcGHu0=yd|3 z&=qliH@8!4Dof{S3dOqodfkBcLkbgGri^(5m+>x{ zE0uslmhTiv1hTTq1jC|^JK=MAxiTd@vbAWs#<LYT2phPi?6l`{FgDPP2v0JzQD2D3S;s=L-lWp5m*`mNacp)0E_hsI zPzibthj$m&q~xx5F}fW3@?v!R&fcCv;5MXaRyG_;qiB+?^MN#)v=2CIRS0LhWXAEdDKCs+aCzrUs!FeIiPfC~O7wI} zYuC!RF}%ED{^4yK;%AczL1Dql17tascuN*Rx}=3Aw(-o($#4+k47xM`qF4% za0`wex3gpzmgBXqD_47lFKVrll+|ikwN}v#l0`fC!Xi-B^vh&$`bHyKk?Xezxz>$g zNtcNNXW|RjsumsEq*%(N?I1rg`?HdV7X_{0E!>(j4toMU$s#kqGMn11K*VZ?0UHI`c-|_4xZv5GWz4Pz8T)Z(o_x;!Z)A=8{ z?p=HB(m%cYrEA@-Z$A6Cu80?Nmp**v#?{}v`c3EF^USZGecQ#KentUx19F=DR@45l+>?8kmP+uerme+)%@IH6}kk$Y}xSoj;Err&X5Ae`p-+TAoqInpy|niT&Pa0@sZo9^M7_oLq!}^ z^2GoYdK!Y=dsZ5RO+)8~;cSfNa+KWKZ>msh#!_Tqxq7FRJaRHst0d<^rq9Wd@%ZWEYuMSlxraG;1|HyFU7M``F{4k?cYb{4QAC=r?9_0|M(YXvm zRIGlKOGWE8G0RpYx`;ByIGD7xth zv=zpNvQbtny(4E#Iy)kNHP4pn2+{n!x&4TGtuPwCf|w# zhtrmPq~xutLASa7lmTaA>DIJ!p2I&s;ZQNs79LKI2XoAbW-w082P4@@n9!X}qDe~# zIC!Iz#zMA|4d@L^?F#84Q7Mj;VZJ)C^4)MokmAiy9*t4`a#N&+cFD|zV@F0)$BIl1 zGo;M8W|7nB;2ejohfg@fXPY%3ztgS;32@Le}JAFq_9FW+WSIbOLNb6ZmEv<&{V}(FbMk?eu(Fl#rrS zqfn>Z7Rxn@s-_jCWF1YE@ysCmMX4odIbqtLWzTWg`Q{T2VO4DFy`)UBMogiUY88u{ zOw-8l-C$WB_X~V7&kyWTlHvnM)G1BJolsc=C$;56NOCS3#SD^^EBTQ;Ajn*pjK-^B zs?cj4mD?IPA2Db4t8JlD2E`Shcv7n(U6|Q7V&9DDHVi78;gX9kyvRaM}uCbN*8Bp zgbzs5B8G(fd6e%7{aLD2F1JzA?(_y6S$4wxRI)md2kpqDXV!e&i`VUbeC5qg#dGJ$ zjBzp#WLz+lEFaItxKx2RQ*$X~v9S)48&PO8f?I{glt3CuXG#Q$A``?TYTB_z(=tf! zDJAjJRz1oF?}=CmbSZ9~*$f9_IZU*r3&yBmAcbyjnh1eM=djw;XE{zR(rhtb zr)Z3%Qh~16YR>z^L8qz}V?mVSqEs$ga2T}~%sFy{CK`e~BIIVSFCQ_LLA~d+6pikX zXpUsW^ErnHx1DexI64`!g0*;BZDvV_Hd+;3<+>D`!@HI|pL4+|QW_fFcqpKhQ|%%? zYYc^XiUsWubEQFZB7_w>lPQRzq?DRurmeS9rL=WKm4%^V_cP! zE2}$CM$_LYwkP+Z*5KOBP%}jL!M-vqX^jM}v`yJo(_zrZb3_%!4ONOHlu0ZQO$p3E zNQaSdZ8(dRQ+8$&B|w7|Nhna#tXo6Sjv=72+PK$0qG^U2%z!sbGigAB?bY<$y;!^F zghM4&s-aqY3=U53bfR1YZRgY3IZ_&htyzjT1gzd<oBP6F06ZBD7FQx^uoSuht# zN;M@Rg1ln_OH7iyIB}F@R|Q=@6Z{cT?UXR^#c5V1=0iaxXy+V<^^crzD4XeGt=Qt3 z8a}To31rY6WccPF8LyYJ+Dr=K(;O|Ngcg|%u>1(^IC^)ABT7D8OT}x0zS+i-Bx~g3 zT4RXUG6|c-(cVZt>bDCdG6H)kTV^{*86C;zIIOBC98e@5;ipa}GRRQ&I3^54Axt@B z)3L)7B4r^|f>IQ0FiC2GuG7P!DFe}lwaHM@hRx0_Udtv2)sCFaXDfs9e3Hmckz~KB z9pMDi6j5yI3`&gwKg?5^a~#&B6Ar0h5Q`Vrld$PQe?o4 zrDmpDp9E$qk&GbdjF?Rgd^Tnnr_Sn<9nq^THa;uIJ7u9sbd792KRH749GNx9Iz{Gn zCRgJa@jQo*-dMf)jX#LCLh6y{bYdJ7vRNsxyO1!JwT^evXDm zgDw?o+2!V}k9Xx*VWOAnbtF&p6QSvp2^~>%r$kP(j*6Bu0*Q?a=eIDce{jMfMw|0u z%n4?YI4|c@rkzz0u8kc!np?QXeCTai&boe&asP%c`a3u1o| z1|Hl$s+2U9YL3U1tf=OYw%t1ada-){i5x=3NYN^cTBJGR%GHQE>aZ+1R10%MDI2Y! zkgA8vYNTXIJj{_17oQlgvA6#xE6-cqU)lN1%WpXR@}a$3y!1_%{%HHd`@;GQb~1aP z*lX^7Z2Og4AKHp-eqim6)pu+@u<^c)!1{Yu_t)O#{`-S39q3QR69drr|Ils@NI+YpgF2hNI~%(C=3S#F=&%~>5pCO{fKmnL-~td*iw zIz3f-L`Fxc!Vqj0`TC%gnG1?mZGs@j+6zdK`BKf6zQ|&o}jdXLrY2ah+@7s3I+OFGm;oY zV@ji5AB}`obTG|#r9q?En3pOgq!{V3VlA#2#io2@IiOUnO*6P%tIhQipRJyshtEIC z;cDUP>{zT_`Wh=t+ zfkC`n2{HLhLdnbJ!qgxq)QogAGs#(LBG{e}s}W?B3E>10I;w*dc}X)GS&o+FY|rcy z=jLJKdrvqx0ymF4^I>uV-tnS+mQC;sD@uBIC?q=g3?u|pi6ElZGo^r*%Z;%_$Hb&@ zl}s3IGL`3hM!eMFnn6@*bZBdYfd}6ho2QQWzLhT&6x=DW)L26?l|RQ}_6^n~`EaoM@~40@F2}m50a?LQ5Q_BFumt5j`WrQs?!s@_Q#7*hnxG9QT9~i%_K8&ErW)4F#!r zmsJocAu0(l(kN2{@q$2{Xz+7mR2fS8R7rJHi5R7gVswr#k!W3u_oE3k&QbF^SQU=I z?he(s4wIAinej=;5wK{7v~!{w<==IvNBY3DF}8n-P%FVr}? zG~ff_Fs)P_Wza%Ix>=-SY^gy9D>?A+sA+>)CPuc&2-i&Yx}(k!U*{xc$_Q1<&RW%c z6*>Ra{L;6b%tHgz*&yX6h`$#K5sM{@QfO|bYFs4Am?5-C_j>(a-04-AQXpLw`pJxz zG>fTWHVK;5^RYQjQmj)>V{bMvtO{u2&u zy<^N+Hf6LsbS>79q-xs)Z!?Ol4rXI3$H#Fqlr2$omVxukap}5shGbOrSAS&lIx%c1+2LG_q(``A zM5{Bfw;k9xCeug&B|+1MI@?o=-B6Z_4ZvfrzzXdGCe>rnbgk=XJxz8J4LzeBk)2|x zuk|Vl!{`RbI8Eu?z1aTM6AltOkB9}G!zG3W`*D6&P(wX3o(5m&^in4><4^^O4kjH? zfmoT@U8*#R#Ys4v*m}4SN)cAPB4qX)IV%!vJOWhFq|xy?HCFRrCB&6_Z;AQ<>0)ow3HbVf*}>_r|H`CL^_^ zd=Hz1L^d-GN5Pj^IZuvesdT4~4SJ>^)mjNF)mEE)z(&it6qZ*~#9Wa1nVp}G=XF~G zAJElysnMDg=EF=Q8jEr|e>CQdrclcv%1B|fe5;r_{}p8S`%mV9kIM$wcu{#O*9Bro z>DibnMR`4)#X!bjFo6eA7AseR+$2ybag^iqG6S_f7$+yw23xUhvz*K|@Jb_+=_MID zGeYuZ&H}G{NUn^sqx_g+#xy2RZn+n$Ti~nM+n ziNz4<98t2tK1$4Ivv#iCYLA%=VQAt|Kes*{+;PI8)ed#2L17j&TL__qB54VqTlon( z%NPY?l&|)&%tWAK&1owjOG-X12)Pn#MeJxPKLl~8Qa_(=+C13(gbK1|h>fl~0G(`( zCarN_aAeCs$YFtOp;+x4hqVu%;?RtSLHNEgWX7?Pl4zL;PEqyv(3HjK6=b_*sTGMR zv^1rEj>CG~&HsPN%KKI>|Ip=U9sb1Og9jf!;P$_`{~ee9=cVtz6x{ozy~gercln*K z?0nD8(e^KFA8-A})|Oy-_dX(C@eSoAg!ya}3sXPE9l2Bv#3SESyaXValqL2a zfbr4;@t%u|Wfn7gvsD#LOEUAt0OH{T@x{I&Ztckf)0Ihj5x~28Ail^q-lKg;ESSzS z$_oJw5akPf;jUJEh5XVbYjn!?@f4Z9#Nh zL^4e-lj{>v0ZT^a;KTW>*v>`x7Bb+K=Kvg_$>;dSIipF4CdM~*?*dRjlXv-sg3~0# z@s!-1E|mlEPTy1(nuIv<^v3QT-u2#bbm6Iv3sFAq^(N0fjA0I$RG#hK3DYP801EiYW#3Sb ztDD4m-B=^M6*DBfVw$509VreImX;lkItYAq`uB``D_Cn$^?>SLgp5L1R~w?4Y@?534X$YoS?a_Py02; z!nGNr<(zO=ZcfTLrA82kZ`PeYrG{l`6W{@nZu-VMCDNe(K(w|2aMB0jhHo66HT$Lt z>O#6VClw}=t*iquu&Y`34R*#ONK;1aa4!>eT2(y`($M1bA{xZ?)U@0%qRooHR)c!O z$xnsWjomc>1w3NSHx!&lph7&CmAhK9T&$4uYLqG0(iumJ5vW+fR6SJ!|(Tb|2jN_zt)I#qIChzJKf2 zw;tU5*ru}a<&E##*jfMK^~Bn*uQgV`xXQ151q}2g{%CnmQTd}@XFgJeo~SCuq>7Fw zMef`)Z{eKh6{TiSCuOzk;qWNt9G6f8D~h3iy!l4WX4Y&fFmZkBJGHAhUzKv7vwQCZ(mkEf_e^xRwo zreRjGW3!l4)~k*_Q58^tkWc3F5|OmJ)g(X4TUykvmvJ=2Wn3x;V#YU>g`^5hi0>=* z%&aE`J!a{cJ&+hBIHsFTCRrH|@X)ALYY(RhAvMGR23TXUi?fj)ca5#4={7hNW~Pkt zLTo-6I~5b_*|AzhwF_oXq9Q~T<>pudL~u+LK!PyNIDIT4$TOCh!wPA+ooD(vw)Un@eW(aNA)4n9x-;4iGoe) z!vG2_dk_1DdR&o)&x^DgN}&U^i`L{g)2oAk4ms(CbxxYn?WR63lan4-Nr8s@LoLg_ z+NE+JzS=jHg-AgRIvS-B6+{uE=pfr^b}}Vtf~plGpDY!V4YoW;bQPwFfL@xf0vJFs zuR0n}3o~PcGh(~m!I+7twa!srh;)f!L^ImN(uIB|-Vfsymab=Yq9sVt8d8=)3-egn z#wSc+%#bx+dL_UEig~4Pyiv zLH9O2B0lIF>TwklIydbBp&n{-ViSv4O_M1^>4phDI%j%#7M=By^st-nlM^-tFu-@x z)Wywb7mD$|lgey2+inswl5#+-k(mcW(V}2uO}3bHnpic5f`rdbC6w>eq1Pn=1o-Ne z^bO%2V;q>GgBYH!N0YHu1-w{<6Amtey#H|_4npx6BH4=IxRcc=q!}9~00LN>6TT6a z?|-K(EiqxCUZ`3+crzMDhjeMoRWf>}6>PVRsxd`lX*y+asAk3iB=EGjZ^$K{MvzRQ z7#YQqJti$seJ<5+*;!Dum8DIx(h<72CB?K>vp(!-QVie$PxH|vcyXUjd0I1D0vQg& zwm399Ot-7`#u-NKT5U94HJoaXiy@?rT81*r#k45E0sG&m@9R3hTcNWRkZB;yGseI~ zW0i2a9S@GkX^H0N8M9oFn;?N|B&EkQC8R_E7;x)|Z?H3NE%%&grl<|-BXzC`Xh?BN zk#Q+PlCsoNlll56PBrJHjHr$#H+I7S3b=LHHx!&(L$rkELN1j9G31-d!mS}%Nt;&z z29W2~i+dVB_JaVT&9w9ifCuFH3g39A0XJ+C2B0UJk~N9+Y10d+amHcy#pTEoQIX>)wG!O85F69Zn1<@Jl#xE_sSUbeWX`01qq^0pED11OUJy$=GHJpvGe1|Y)vJ_sCo1Rw?tKt%g}5IFD%Knxmy2=V(M zu)mlA|9Jxei1@z`0+&1j5TX76M40~K`0@2Uj{wA=0f_j$4+6Uj0{)W%0uW(+9|U$h z0uX}+AY%DG2yA-bN_x=AK_x!(`@BfE)9Q@<~y8p@j!KE)=GWY&sPv8Bs z-QLch?kL+|*p{~bpDkhYvzy$;A8)*F{SVi_dF>C@zG?N-tIWzL&!7IcvU2@l@4_JB zJfk#GyoB`_Ceqk9s!6!0}!EV z9|VFP0f<4L+9Q##Kj0D2(u@)tCo*LMWy>=(ld79sm1u$Hz=NSyO@*vhA9N96$CD}& zfCy|~9C5vV>=A$%GyoCt_Cer&j{wA=0f<1j4+8fs2z zoQu12a&TudZi%3$#QEf}N5iqp56-pO?rKeBz-)(5u&oBzjV zb>j~=-mvkU^>1H)@!HR?Wmo@r^$jaue4-uv)R%1EOjTNW*~NSp88v4zQXtOWy0Lc! zV1Znv75|AGaQP_^X$Jg}%jG~^@t@?eNLGP3k%YSkfPl2sl`9uVsTY$AUEkk-F~9)H zF)K&DF_vVI(r7$f8^E-@4$8G$G>sH}+ltuv}^S zZ(9H-O^Db(|9qDVxNiTg4HnXbDDr2Y2S7l&^UCut4xBEec{AS`B7)z)8$h_q^xsHe zi83J~_VRN9psP&(eFaV_6CyINJ_i7~%Jkn*;EXaMV(>Ti?gCh@GW|CdfKw(!Jf6SP zB3OT8?=rx0-Nk=zfyWikEeX*O z*#P{Z%LPoZ|K7$pVy}#Le$KLwxyLRv2`Q@GJ z_J7)*ZQr@|fvuNperU6@@tKX;`k$}gSby%?Ppm~(|HCS>^7|{wV-*7E`GsUk7e~$) z4tQQN5XJp!ZXubXZ?rR#fyi38v6o#)rr;av@g(!4%KQ1uLNcUpE(^&(q&|!=0OGEt z#KrOF$6ZSyG9&sZz;xGA+&AWuwG<)~LPh|fyO!pC1D;w-Au=A+*8m`Qb<6n%IwMtx z+=d%_4+AWBb<6t3dR(bKnIeDw)h-us-5K9p7E*;skm$b(fVi^3E)J6~q`E{l5Xlqr zD*>P@8`L-8DcL|INvID2AXhesZ=f@>fk=nAvG<^RtAK1?;~VR7W%Fbz{rR-Z1zh*T zzPT)91Cdj~r2vTg&Ggk5hjkaSfv)vulK{j0`1LB^7_i0uqXd9(7wT8~hFHGog-FKe z#{s76aS!>%T;g#Mc@lCA0JvGb{qx5z7jWGn z-(0@BoA6)w&)*L~K;i$&D=rRgKelWx$ZUAzJ^%tL|5t*(K~72KWbma)2(NuP7#x)T zuRP#;@H6swbok||2mi~x?p=7Kv~uiwcsNCz;4jTcpkLZnqi@WM+aV>%Fur) z7}#AfUhX?Wz2vC|k!2tU0HC{I-0K_glnNlS3EcPp7p_0-_WwUT{ISC{=>N~}|JD9? zf&Tyh>!quEAKs%u4*+rJA9lWf=VjZ!wEe)=Z-MTAzq8rf`1_3?*hsFwZ~fu5Ppqln ztI3b9-nsHqkM;KdRQ-PKd!MS2o*Czv8F&h2Kz`wt2o}_pr?y+b;^A?6>AkW40)TYy zBX>TvZ2=ZYP^+-J(6G;U*?{YY2>1KW4e{3G?(+b|O8^8!xF0Hyz_=R#JPZIJ!u!4f zZe9Mx=K{E^01hI^?;Gx9@t2wjqMHEDl_*3&-#6Te zM0*6(bq9zzy>BFVcUX1Vfa`_`==)~l zxr6uq|I;g*pWN(i?ri+n#$D?_efiy&??3$A!zwrn;9Upt{f~pQ06uley0o$P-o4v* z-@p5+o!{S)xBq7Q`?e!nAKiKl_yX{;wQpYi`&E5)edV1`!8?^V#OC{-vZ0+S&%F9l02M3YS4 zA2e#2He*2`Q5r>}2$P~SI$#VYYqpeuKkfC%IJ-h|#N05JgITme7? z^L@jeh{1jT-&l*T?7v|*zI6Dx3_3 zg10<4T_e74+F*Wx)$6Z#i>{@p19uR2xp4o5pjrc+ow=_N?ynzXX!l>T4r{Zc?s%kw ze`t<%{hDho4r~Sde+2w|b9OD8wcU{(ap(Ky|8zBR<_--(`=%%svvVSf_|k`tORYar zWpFtF>q^q4a(Wa5B<-VdG7s<>xLmwNp)-fU-W4M9mrW9W)69`ikkU_qdL{dhb zBTG?XXj#=MdQ4&ESp}(O7dWsVxB9&+lyi>h{_33Pcn@s%hCcnCa}yD$$5Q(@MHX>s+$7Y8a!B{KeAQvX;O7e* z#zk4~3CnNMiQ3^kAr_Cs0Nhef7zyKKD#TJIPdBhm-jK>k1{~x*zv_gnYDJ%*sZK4K zs9d#6Kzd-1C*lc2DS?v-s#FTq(us0z*g2l$qVXofkz!9#i11u#I9h!$wzUs^e zsZrsoT0v9e6c(T5(?~`s52u!cu<0z4?A7M1Qa}-3p5R_O@Py3Ex5e5c-|_s_07UDm zi+5+fydr$7S6qSV3FY2-m;;V({(Feez*m;|=oOev^UVRyd9*EBwoBx+KkI!~&ssFw z>`X>ft4tP0=bFg522b`$c-jpUV7y!1@QFT8VxhF_*Gs*qNj0)~L%7OT>#0Fu)|xTV z9!X=fs2oMOHi=K$F>TzEkCEfr6ZRs#HEvd=`Y=+U8kl5^^kG}-%~U!XX*VRZ-dFTd zyCz*VdJX>Aj*pJ(^?9>19`_qjIy4^VB;#60b@Blj;uv;I2J;-U7i!XJ# zGh0RX3ZAGBM#1jxv3xKlwIauqq*RY*dC6|X+r8>M)s7jmC8vtEnv?reGiszUQSCfo zAB+v+!=jO5r&MJY$&3@#pAZEN5ucvG=+jpju;Wz*fDe;# zGbB<%Mv?16I}u7X%C(`bH~L|WZ3`8pN{)ZfHu=4e<3Sggxihv!jB4Fj~UlH(3EkJha@)Ql0EJ-H30dk(DFTl zXEGReyc&R&uYU~y`}E}h2Ub3?a`}5MKX~|yhuy=CgZCc1djDtl$xENTWM6vL-dpxw zxck9fa_7@K>dwygk8fjJAKI#H{>kQS^RA8W*toL(uh)xfpIV!&{`u;+tUd<}^fmq? z;)}Ehwj99|a@S0aDurN0rpDt8+bPz1jEXdENAFbZ3{&INaei+3?uB=<9q8o&jGNg9 z(R9!^%=yf|9Kdq(aUi-5`o=n|>?}aK_Ysiy8Ds3desEX3z^2uCYB9d0O zn&d}$ON-j|GLD9PBR$^ME#_-}v#2sgjgTv34N zW(Yy_8T5^~WRXVzoSPv8(QVK-+=(m5uK_TwJRv#_`i40x&xZk$E6=C&3`QT{)gtE` z8M3c-nSg7C=r!p3niuJ0uL2ORSRlF!`p#5eB9>PII9DtXJqCTlof6AK0LB#yM1Miw zFlWW$)g6FaR1Y;dv57^jrpXkdbi;`z6(`#)ben3&8qxK6P-e%Z88$56NRPXgov%B1 zrsklhs&s8Io0Kcd-=ChTIZ(S+8%J6}*|1Nqb!hHJb4R*YG1F%n!B-9Qf;YpGzw%Am5IB}}8h*FMbY;hiT>SDN) z(C}EeDE574_&?s#e@ogV`%JAtpJ#QYZ`lu~QlZdduXQ4duW7Bp8@y73(}fI6vpz3Q zNA$5ryyf11W0V=V@&EO;7p(04@zz_xf1dgC%nUp_18-A;Zo=<(UptlX`$P+!e5E!f zZq3O$o#?;3ObO)woKD5o2?AG|YVEjJ)v~FwX*1>Va8iudI2P~lxoOqF(YQ#=6@ z{U5mJG{9q~%AN**t0XuJ@R(WYrvVbR(s3tlWTT3xOj`mYaA?QkThnAg=h$JbkxiD_ z$S9W%>7k2D6_%fBdv4C6PBuxENv_BCoCtnZ>qpI`D6koZ4>xO9!FM8kP|_39^tj0{ z5YMG&-uk}jW@VjD)|@Mt)#2HRNae_4u{=TN=Ux?DgD0D;`IP6CEzhW4?gQ=xJkdO~ zL^|#kMJ&w)6|aUPHK*4xCbO0m;#9F-AeeGvnyQO^)v$3+NsVm$`c03e6HXN*i9cML^w&tYKzS;;UG!4A= zlay7NMXS7A39GF<;;**Az4U9>-bO!gbE$dc^iuN#3m$zfHSxz^YW^v%1m~8Th(J>e za;uk}UTWYLOR!p=FTOavdVUR-XBY9@QggDm{Q4|0kK(&^6N53Qt6)t8G{0J_;Z{c6 znk0Tp#_D6NEK4Q-q*j(_G6fhdU0DXD@T@+tnU-J{bCK5UxYE$tJ-K+CXQx&Rm7Nlu zjOj977_^>nhHJCZ#+$KOD%(yt)G?wSHzTEdT-Cx6bx2YrTZnfgwG$WOTBj zRkO2vjNot((uwu5RAQnQ$dozBl!*%7U`0|#{AIW%z@=Zi_U$h?c2|~vCZ1YZmMit* z*92cHOY9qOWx2@lm%eoY)t+8z;CD6W9RKv!->;ABk3n6aSNZ2NJr^typz6|H9d4R- z1yN$hx?5ox0>8Vt1bE{u-ai+Gm2vlP7r#1l)i;Mb`MV{%u5xV?3F9%mRR1EteT{AyuQ)wCKnKs;y7^?PM&^v|Hm)OUX-EI$f{e>gb7=vK7tT zK%5kF>1sTl8uf~`QY(?irDP%6X!MGGYfhoWOkvXMa4Zosv^}xsZDW)HYhp5*iG>GB zu9ng0RL4>cNkmJP<0)fi{4I~}rC+HLh+(&Y->8RYOvA>B{#g;^(OF|pCP+wm;OxL_t(KAw$nsRD1N=2FOFV;v+nqR?gpw+f9Zfi#lNlz8?oG{(fU zs2FJr52we2Ic7vN7^migk?bT)=uRflq$MOgN_Wy&$X2okKkZpW&^|UGhlre*Q1QOW zMu>Q{q>Iz`lp(@KVHlf^t1?|l4m!{IIJCv@coumv5|u*sC_A4}NlPAS;|VqF&-z?U z>W0z;rX_P|Js+Ki_)E`+ws_uxRX7(GW_rTxwtLgGSZ$D6w1o>|L0~dXF%oPQBXJoG zj%&5&NzfMJO^ahipJs96xMk7}lB#jB**KE4fFB6+B7(ONJ`~R7%Ds}9pWJ>Iw8fnZ zR{rnD?|K#xvwOE^(Xe_mq};ShT%i!LSh6UE=4PtKMUspeLW^{-*YCxhUWF+=SMw|& zboG^*SkR#+NQE!0FsG!Gd!vU7SvhnsbpSt}u&=wCvTfEx44v71qdlo5r zMy0hOQX4a63%p0m!w_Sof-`P3LdG*@SkSRxkSJw>@*PcR48yaC`1dGzfx#mKd6J3h zA-Rl8Y^O)3g5_L)+PBlq0xR`NO|18mv3M|a_aAr`5Uc3avxvuINf~5X&FZ6KwHQga zGwn$-YBti95z3c?xwc%c56c8TlkZ%p1Y+IY_AnBi9-$_M(5P&)TrtUpGFYZ=R`8;! zkTrQ2%;vF)8Oa74o!k1*7N%!G=phZOM>M48utj{DQA!0N(Z(VnVkB0Y$W=V%OS`}U8x)htkyOunkbHON58XDbr=y@;p zFd!^m1a0v`ux*Mwx^?pPgNM;-hdLCfM-Q4Ugiu0}w1m&C`~;n4jDj)BSNm9IB2cmB zw6(RED+r5)t{^NHx&oWRa3g^?hRirNQW7mQ!6~X9A6m1XX|#p8m_r)H0UyrG+lw_3 z!eX&6sWrHEGt>;xeXwf^OIjmAD{WJ@)pVHZiX*BpZm3cup-f^MAM)l3!r~X8Eq>m! zD4XeGt=Qt38a}To31rY6WccPF8LyYJ+Dr=K(;O|Ngcg}yzXRIh+0YiZdlo2?kML6` z6B%SEdmIx6q7bGWvgz3236Zi8DnTgL9&kxZuHQ1+mTHsk@<5u%P1 zZ|SY|FGE}WcW8^h^(;biiqyL7B04$AKv}0-EZ2xf9K0vAK!Y9*?VF$2{PD}Z%c;v-hyQUC-7>dcdHBx{2Zygd+&}o# z!TS!JgUrG0`+v0mzwdnC4!0B9f5Sex|J_!p^)y*ZI(*5Ijx#)@U>))sfWYkqA(QdOe2+#Pvq6N?7eN}@~H=%ck2|SQ{%g~ zjiN5Rh zZCxIEmlcz(X}#_wdN%5Gdtx{~oTzlUXAiYZ79YouaIehE(mXqtpZx87SBCL2t&X(r0d1+q7X1%u3 zV@!=trix^IjE2=VXYIYrvw*mck!N9b5Sg$hWiCzXLRc$Bt8{v*^oWd(QiUOc=zM)p z%FKmCX=Cwv3SsdtJd7csNrQ-%qOoF*lm)3lDMcg{>9ggYptOQROG);KV!k&DUH(PS z!Xb6K$Tjjo+R*D+wla%1*jU1>+U6Lw(sV%=m9SL@CET6EUxl{#70&`qZqfDlG#s*w`Pjg96Gte%)oGWq&B@iyvp(fvfMrIvV#!>jZp*o9rBxBx zIFoSyWv)@L)yPH^shF{0m1$l6E@+GIgtmCQXEBRWam*0Ii5xNSn{s==>QfG%rwZ9x zS8b>vPRqsZvK3+Zy@#MJ9)z|?dlrts&Ew8|n4Ex@xoDqd6FkF;lHMH(i4H!CM`KkY zh^X~UX?4L0LM8w1CPpahlnVLiI7`~rbf`ygH7Fs~3QJCtJv;2A=^&xvJyUUdwL5>* zW7V%=WqgFs#7PZQ8A#nUGEAiDabIpbNG{5k#da#E8El!c*1XRJp#(PyYw6wuL&|Eg zBTiy;4~bOLS?Zco~UaQZxGTT6>X6PSxP0jVRx(uAj~sK zqRHs6ADbd~Y(iUXKwGRsTda8&5WA%}L0jAao3r0MwL$wQ`=fW|5FWMfS+J2{C^+s3 zBNm}Zxtqt6k{Sw9^De6(QbJS`l!}ZpH5Kb@EcT)hd$@aVVuYx1KNew#Zm|$^x@624 zOsjE{E{6t@`q;5CDKW$+8FQX9h}}Q(ENUZ52EN>zW#SfH()D~{8cz4P5Z4@Zv|zH+ zQ~^(+Y7?RDw?SKYuh}684DU5NRBBhMgU)2!qAP8ss7@2{N*SFb=s4Hx_LEIM+UI6O zdN7MxJKhIs7z;0G2V>!d5@9U7kH;axo<`ClSwOObFcwZFv2;-(#R%R<3b@#*)eM}d zhNFE);tv*A4q@SaE`Y)CJ{Q1Pc%KVGxp`Mgb&zhMVx)+m%A{ok6UWuKY^C#s$h4Q5 z4%@|O5N%G@ybo6}7C-0d3IgL_c@}6KEmavQ#wYQpSxh;WF;#gcny*q?uIylu1eNHI zD;*&tuX;Pn5QK%dvxKood8`t>u~SZ@b6`8cnEF(x7-&!~mBNFxs4Bo8t7SutSTq;Q z9{#jv0r8r<@IeTRx8Cgkzw$p;E`5CGlbaIw;r~y6-X?|J2BhEJU43c~N%4~>7GK(c zRPcXhiJSHUQvtQSFE2z1)v|`7XVQ}Zq0=efd3CB(TP4RXH0XYzE<2N3lTn|1%J(x# zS&MwDuYFS16U}Z<$Kq+AjiouLR=d-~W`YB!jlu@>VTM+Fhm-5D7!P6kHZ5eBB@Q> zii}6ePANK>I0L$IwL;31y2VOHKa&#Yw$CoB?xlk+t8W>EZZ-_JR$)B$;+jQ23r#G2 zYeMQ3n8T5lFB~u{m770$E}PRUvpk>2`SdIq9`-+p!!4e$%Y{O2xALVfXGHLkGVVu} zTHTz@k)q1Q8Y-{k`((A=Byz%d9yvZ9$^tuo;(3je43(K{38b7u#zR&}hlpe$WvB8| zDH$QGQubJj$8wp*aUKtq2F{={pJZnx=U7YI1Ar@x-R!^*H>Y*{uH@f>Qp2d2qZPQr2EO9og7UCj- zmy3E`RwN#)F+!n-wlGp{Rn1b1tf3QMUG?2d2f}~b@hfhYQmvmVcu2-Av0|g=_Fqp? z!9(&^v44%rg3eYt{Zp*fTCy6Pp0mfSFg#sRVPVA8w$ey(m1vC_i>XeXF7whjF$-0| zXP)Mm!qRpIL;F;uw7hC_*`XRwOP%I5u#9}QAYl2S8q3S0mdn^(l%D8vkxV}Ja)GJz zRV_ql2v?O^rkGQw#&PyIFJx2B#E=RJBVL^sM7xZ!AmJWxcp^vHq>m$mS(a^;Mr;A; zHPqaQRJCZKm#8#)&H0SSiyRvf?DU|Ki$`l|XWs1TjAaURduAmxy};+KPBTkO!{i*R zT}?@hJtKWC7w)BByXL?M zwD*aPF8C+Br7zqQ`^ZJ+)8AAbJD5B!w)Z+|v= z*Il1DtdZ}2!DsGk9ereT_CNo0XY`66CWB|kTJAmGSRc6Z@HgM_i~3)DO!~qvvUh&x z@bCZ6e~*3Wli&B3&8x=#Jqpc}51booX+@ql)@T0eq2|XQ`Of!!@e|iS_an2vpZ)%~ zh#zTS{}}ntO!1vR@mF6-lE-JqT3){5jrDzR+WywmJN_>Exo7{X{Bz%H{^Z(Mke`1m z`^&>uzwVc|{_Q=jUm@>5H`dacD{rhnU4J$I?l*ttIq&_od}sWD_6L70{O9j~r1zt@ zKl^8yPkr#($6rg{cXq7h3#d2Nd%~M{zoT=PNaTt8?kQ`+oBriHUi-P|kG_$5<^O#D zZ|*kkr(V7xg8K+@43anQzxYh<^S}7AU%TfL{UZz#_|hLg^vUP_^#{Lr*YM5syPMqc z{0FIfZ;lIb)}}Y^?N^87U;X*?ZtYF>7v}d}n&;mz{JYOxe(i0y6Vyk3{TG@s>SZ^_ zg*ZpW8#g)aZ4jUR@X!3(fBeWxKJ`C7{ky+v|G^{ZOTY43`oDkMf5Lw-DN!%IIc_r> zmRtHDTQ20OVy=$%IRvrnsz~D1X(B9SF)Ei1rtNBZR`bR!mR?t9GO_o5{{v0ry&qWl zncw|P!1}8{qcgj;Yk&XgN-{tNZjK9a(u6neTfUQc(e0mo_s^{T(4YOk?7erKWz#L$mV1*e%U-}pwp`_I%eH`JApt^7D1ktL0B^_}dVoL( zA%spSd8r8jLWhKe79h0Hev&M+vol^s+6*N5eVIS}nc4My?z!jQbL%aNI}&mMx(_dkc@E8Fs_C|@iSbr(=R>eAe3rh}xhM!8VP=4RtiHk(PBF;lFd zwNS-2uJ_LmeYV<<0nRB5vKOrwvv# z?ofI3R&CAgXCONVqzmqJ(1~kUhfT{;wN{0H5|6iWZ@T)zzx&VM<0WqC5ia3xFZXAboOM;WYh6i=GJsv z8>S*(kzt@b2{U6u}(&;Mm}+%gRe@G_lcK3o7pv(esBHv&OP#C2l@OS zSAFl2^SP7HJ3Mx@@zLdvrja{|B?eV>C4F2@-)#ub+ z^{)QqaoZmb7(jd{5!YY0qfmp3p*XHZDX%|Gg{x}Ri6q=e2rM%-qZ+EGA)*qG+S|DB zi$iZse7-}dvBUM>JMM-nAEu72-FDWWFUx=6dGmxDUMru0FB5Sq1R2-X3?-v8%2}I9 z1Lew9V<79d6)P31-hx(D9#ht(x0th$HttpI`OiML=RbZ@UApuW=9hba{=_F^y|u7#Wo1jT?l)AE~`VuQ|_LMVT%^&2yi zygB;&53hc)5m@8C`zqgMN4)jv)#t40Is25IPZ=D_Jm-6VH+%;XH&%@$y#!o$80l(S z=Oo<`&H<|lHipneAVO&4w8=-S^wwxbnjb!U=s9Dlu*VZuo^b4Wnp5r@w!HPp9zT8Z zxDU$DFFE4P&e9D>2x zFdZ~Q1_Nipt1gqZMJDh-QM=2hMAr_kMDRZTTNs>W|-j@HYI8+m^1r z0B<(7yyct=e~)h`;wGVh)l{oSr-#&4-k)5k|sZR4>ao5lBZADx?$yE$`l>x?sMo$=U zP-wCqwD9o*MXD0a+bg644Y`{v2Bams`0JIgy?f0qgTYV1{E1BJO_%+Oy^ep)Lrteo zKTEsp(WjQ<+laWvu!CaLT)t@I6MOQsxo(Z#P67r#s!c=r49-xQ4^fXbNDswGdU%go zd+SbrzNofE_=MkS5BpB8xbog-)*f`nSwFqx$j^sPErjr`MO-gZvqa1hLd_Wx4B*T$ z#u&`SR0#}pVnwqp8*9`}W@B7y4OZLwHB6fi*DrnSpylUKKR)NV+aLP;0_PcLT%>vF z^YQgdo`AO958q0}tosHk zk3RV1TSwzDd`iU4V3Ara67`T3GsqE5XU?y$IZ>iXX!2TJ zlq|Y@TD`jL1dxa%?}@8bHyrQI{_LaQ9qBlI>Z&#L+p)+mUO4-wdw=@RM=!Z?^i+IO z#I@r2ARnq|F!DJwBQf4jjl-?#q!neGWEM~8QLCzw4TS?iT;- z{1j+;yZv&S#}+e?0P%ef6cA>y)duwG^i82>tpfcB(cWsd@$pdB=7l4%R( zL!w?o6Dj*`&5|^JZ`tO}%VS&pb2a?(2@ijGJPQKG55g~Ox$=(0Tbi*S58v~Be7uco zr_@Dn#v5RxQKsdy!O<3?NnkNGt#z?!74EWWji{#-N;akOd&}|vxaif>tnWw8S$Xf^ z`N#hL@!vf1#sjzR^8LM!`gHrJf=4gI$3)x)qSC}|EKHc97I!5cg3S>$0%;tfs5L?u z!tR7MXjMgARcp@F?zh`!ZYUjb`yoSS;1}Br=!fFJw);;fVt1cX3*PJbq+#nq?7YuGB zVX^S@PEZwL!#NE-fl_{`Ob?LIyFnEfZ0 z6F2sKu(kD{+n!9|gCedq)R49)R$W$UE=_qHMTt3aaQ6Xme&To3DT!xFjhz= z(8gWe?D++8=d+H?76?@=0UrQmX>&5`b-P+Yw8+t|GS&e3G~r}AY48`d zU`)a-m`@K}yX8TJmzvN^hCAMIWwl4_b;h0NtvUCnvA4&dzx-V||Eqd<@A$<&|G(P% zyA$ekBH`I7EM=hkPr@MycA6G6C{aR;1Gf+?4B zW=G0aY!IZag=y@m8kc2kwzQ_wCfTpxXHU3KWjppy zFP&9>;Saa0aXo}bL|hKfhx1iu%Fb9*Wu$^Nuoz$>`8SO!XmZ&KDvF9=7EK~+th(E{ zzl)rG-W?b1=0E(Wdp&yl%=^7pP5k8l4&J-xXPjuiIKq=2#D%OeBh7BS6K{P~EChQ*oWkZI4Ux z9{OO9cfW^UKhmR){^*Q%4?F6K^x9{B_}R{HE`4KJ_1Fhj+==5Nt_}_)R{F zk1J3mJPvadc44_R;PTiEaL$zqV`+7Y=P(oP^QE(3-RkYYo+ZUQ_WtNG`{ehW4ja4o zBcBYx&qvnOzO_3Z5^;G#F}9S6gN_F8A+b*AXQMy+?RG0( zEgbOR(AgJXmpGroF%dUk4Ygtb!XG?h1#6*B^Edo>LytK_=@8Sv^37CBAAsn1E*(p? z^>E(Zz~_g5d&h_VvfU9Ej&2>g^T#Izw%z^qJ+J-NKBvF&msKZxhzCSm7e*Oqzeejb z11`Tm&0-cJYb+0YZcnY`@Z_CR4|{B=2g||Pg;|W{SP?VG*Yx#UV8t3?|hlQ z{PL%sUGAKK?|JOJJ#fE>ivXzte zX`GFx_h9~ZX{+y~+PC^9mfrL2wbTjl(HCD|IOFUmhqr59^9k+~acemv$mcIjHPZ$M z&)|e47{qM|#%*LP)v$6r_k$~p=7@U8(U9c5(c7YqKm49uM#ES3vA$E@hK6%%)Jt{G z{_%$6Pe0t)-jcmD0MgTp7Wz#i`%vi$`p?h$blMO!^vDn*?>0(ZH% zO1;dHsg%=8JG`_frT2lgirHhUdE*IPyWiMj-aPHR$eQOkp*A7Fh?AJE5!f7AP3iT%9qv+a&Tai@r@ zAO%Sq_XmNS>OWX^%+1y7wz%nkPxXfn8K8%nm*0SYf-b#a{MmN~afgVjAmKe{kI2>zxINc_TT50 zzus}$vs>&t?YI#7{U1-gy{NwVbt8_7xC%0Zv~fSa`K$;0tKPnA;@--^JL~?urR zrjK?~-+0M!=l=ek+q9qKRuNZ03XnGLr9Zx=@14sI-Dj_tp55u`oqPUq*z+R~zFo2& zaNE>*J00|B|7W;G#8r^(yM-m zrH;bQZT+eZa0!mV{uCI^28{lgqmk7xRI(W+f=sCZHL{e~z+tv>PP!L6$lLqlU2cb` zBB$=+`{ewUf54x+n||!w6hYu{}=bNFuCZqpb#eS7!% z2aoQd_9cI%zV*@cQ`~3UVZDFGVG&nBl8!bmchqC!kN$MYdEiceKHF@!`JedU;Qqr; z;hOqK{+_tv+}($9lZdMz8%G=0*SM})zvS?3S~>5J4*9!l@V*NlI`1HK=KdW>6Z70v zzkd=xSj1J3exr?hG!R2Cx&PhmZoVk8$JnC}T{ZpQmG%2?9#Xx#=EdtqZ~apoKd7x= z`N=lgxb@{*=U=|#sk{D?xatp%Pd`5X?X9Q(@gjBqAJBs?%D$NTLk>T%tp}e$*R;o7 zge?XVM8F+4A-W)DXrKlk7v=AU;8qq2mt(w-inI>-J~!NT!uTOae{^WgRqDCPJ)>TB z=etSGUBhcWj$U)mMd#xOh`1>-W+8(1mIG&76ddJLYOO0#LxQ9WVuOg!3iDys5w998 zhNbhc{Q}*oV(d-};to<=w-l-+A~OLl@`q{YBg+T+-Q$0WG8p zdHG|nM$g((Jl9Lz3G&gQW;+xDY&O=C!%abHy|`<~<}Qyn-a2mTw{O4qx3{0a{-LX< z&tA>|`(fn0x7fGsJrLiojZ21;QB5M?^i>)DFkaV|!|poeH8bXriU{R>`6jG0h7EZN z&b9S>_oY8dh5c(j-hG!H3(sD9$}g|~)>+_zhuXjU!!4Tc6<0Gm;QNZWc1P6d0xCKv z8#N~IBE?@A*DN(JV+j`=ybpBDR!!=1pq2_BIqw|qvOlIi zfBCfMpFH{FrNfW?`IZl#e!h<$y6@tj9)=r4+_1&VdW;FqoG%q%omXQdKvl)jbcf@d zx!`f8f^|e+NgC^|&OIK1TmwB(oyw+$zjecV>(*U)zAN(Jo##J#k>~lFull@j|8g7> zaZ_1VYj)*Ifl7wUC17=$bpv%b;Bcgpp$vouDj*)@+-5pck;d7v`+YF+%RQ|(U4Qz+ zwd3E_-}Ji^R}Vja^0lX5^L^~W+nzn?7F;jlx-;&uUz_kixTyu9jRaaTmU97}D;SRy z%W$-eYDt|gT8N{b&c0;jc1Xkhh7Wk1S-#_MvZwDn?!ITuPq)i1V{dxl+&^vq1P+S0 zV2I6mk{HatKcFTe0($`dVkzfPQyxZFbL676dfICUBvF4%8fULKUk3ki%$l*GUbYvC zfABZB#yr{l`MEpX_^RjarS~A8;W`mFXN;zGbv@6i36mZ^Q@J5fpnUd_ArdN;b3TK* z8Y;Sh0$FePq;uq%e;{9c^IN+0QD|4>?H!)p|G520NA`Q4>#lW7JjGsp+uw1mh?}o? zTpXR!N4OM!W0SI4fLyK$!3ElaXk6+>z5%nARvk(~Iq5y=yz8C6dv)Kqq5tso;MSQ{ z*Z(ZlJottg*XrAr-F5$)&%f!w0TDN?_arJ#70q9j)*@c3zZBF`MKTHjAyiYO>YRnf z+jl*B?U3$RN7ysmlCrvk8{HyD?eEF1De|F8tp}3}v z3m2jZTS=c+wX7OGSh-0S(m@N)GE~e5U0ycbGE|{Z(8MPgm&U`lcX+bKaX)6xX{|wC!V?YYcD+Tz5A zq}Ey2G$!vI5zhZJJy-QCxnc>vq<8Adsqj?aPQCcx{54;vmG`uR|7Xl(F?!NUi~2W}i72PXP|(NFbH^85hBzSX_Y z^q$r0?Cs;>7v~3?8R+%XGLn-harr~{;X&Oq3p+zC5wMj4 z=qeeYLHWK2*lq&o$`vls6}O;PS1FrdBK#zcB6E9Rfg=L8LIB-O2B-or1Z-CUbcGC1 z1uh8KE&}MTGC&phA7DERpu5NbRp5MpEf+v{mI123^8njP09`HvRDs(8woCxsX+<$% zVv7w|+UAJob36}-H8*!iDeyVKb`(ID$pBU0aDeR~fbJ**RDrhvw!HwlgA7mwt_Ijr z0d#vApbGp9uWG#SCUovBrNEN_n-D-JWq>MhBf!Q5 z&Pa z$pBU0B_N#t_vyKL@z4M9zNy|{_9l9l@Zev`4|b@aj>A?o8%4X<7ew<8I};?xTyx$T zz>u@&>ox=>YZMU$oIaa@Ks7!O?=h=3vtYB~tx~o`g9&FHA?y$V^iUa~NF$o%?+No( zUrl3e0_8x^l&gEH{%k$X>Oziq!Cq;lYpz(dS;W=~poho+Rp9=BMFr5cGC&>jeIKxh z02-A6s=)C9BLvXMifn|dWD;4P=OW+m!S#lVn!9Ex@Or?)0w^H^RDsI_h6|u!8K4UM z9k7r9ipv01;Ou|}1<;TTPz9b27$$%QWq>MhbHD-uC?*3`fsX^`7eE6tKovMRU_JrV zF9TG8cLU}XKz%Ym6}UEF9s$%V15|-u1LhV$Ju*NQI5l7{0n{x6RDnkW<`h6(GC&o$ zGhhw@)F}g0fiDAQ7eE~{KovMLU^W5NzQPl=mEdySW==Xx*$ml0=N&5*crjq80BVx~ zs=$Q-vkIW73{VCB3z$U!waNfh;JkpD1yG9&P@jCy1q=~D%`!k0xGi9?0E(=rH=V6~ z#TKQBKm>_4=v-*-YO26z0W%4punbTI4hz`90;ov_r~+>V>>vU3U>TqZ`K}7sfdc43 zGC)J}{S>eR1keLlRDEF_Eo4gBOeX9vwHhAp+>TP{x{$LMg#LTaLn>Om-O_~EB!q^qa(e8gKI~Il)U&$ zSylxml!afYhkEm;Ui;x|;^$j3Q@|dx^)xS^dyftB^=n3&Kv>PPr z-ym6cM=eRR*|e++%hu0Jb?xUTi=Tg%E8Fq|c-dypyCbU<962O8g@o4&Z#+i?t*Ls% zPqMpwfO^iM>X=>k7v$S>gYJe`HRYdPB>6}tLsseP!Xgd}!tK=*KBsq+E8_Hif{3$B zV`!CvvydcW7c*J##3>r`?t2N+?opE~?aF-xX=jjhC)B;<;^yFn0jBkNXh7L-KY$kG>7 zsi@4I5kbyfV7YSc0ts?%v@R<+UP^LGyO{+g6xZdQ>VkwjJuFwk<@&knvVx;(r_U=i zS(7mt36kybp76P2O16BUpuvsSWd+AxNkDOpUU1tWn)Y_nf_&SlyUVB1&z&o$b%K1e zCZfSr3Qo|Hd|f7@1*H{De5->5X}505m3F08kapHYG_Xp+(Oi;t)xp zqk;c_XyCX6C^A5=?Oml{IaqkH--vV_)CkO9w*+bb1p-$9;CW$qyzWHj;gX?dH0bpOD;CT}^B$-X zj<`zdWGMjZIBhOKXG^3GqS=Ohvk6@BfW;m!awdY~w01gZ_eZTZlb-NU?y$K?Ct9vZ zia$i8fKW9ESuKnwo2H!}g7X$h#FEycL9CkbHQkuag}|O_kfIF%2Pco{Tx`0KTfnRn za|T90kFCO9Op^@L%Cd?aq^DymMZ8VW;{+V znh&?!d3%+z0A_Utw`iaO;dg5F)3m*+YXLqh1xEd~Vg}X)!nJ6%sguWpEH<4tqZ4|y zf*GB%ZLgBO?;@EI@Ly?03U=My%xM0BK^X2|e+Hp0a`1~1A3j^56B{jMATQomxRj|u zNT{LfQs(kBXM(h~0x&aL$8;k{o3lP5OwbXYXs_<&EYmHzm}()(8qVhm;6XNri#)3* zg0|c=YXnR_y}y~t>67{vYlo7lT!=|#{bjY*U(cIBw?0vD;Nei3NE%8RAOX0Xbl46x zbS9UN3sy^IAQDZ$xwMtzA|_d>3jm?%!ctZO#HEZ+lcQj4Es_mt{yS|@{+Y6y4Q@CF z|9foji~5CqV5v^=REZmzobD?&IkisMpLR7l3QwjpK6BZt54g>&WhO;8{TRX8>m&y! z9B@IM2Ot7T>2r2Iy~1XioZkWBdOpc_A`W{A4V5vv>vjOgJP9Y=tQX5rou3U|amrgw zIUsW@?YB15E{(kw3YfeI;GRy*WU{SdCYZA~^ae-5l0jMtpP5fyBFIxO2$KLmg$X-* zGDXK~RsPq`dl10C3Tb$g6VCrLJ(u(>xpGN(N#E2}Q^ZvN!=u-Y9y&TQa{b6*BcsDVAC3)=4c)+J02m*B06XZ1OI-|xM+*VXd@5C1Rxw1fU<7PfLF>oz6?{nPu%097y%VFCdFt&#&O-)b>| z41nHS2B?B|58>MNlt5D5U}#Of=dDi8?(Rxa#b!9OR- zPM**v0N70is1m9IkpN(Y3{WLh1tI}}oLGG&tO6ndfSg!;C9DD>0f3xXeI={{A_0J$ zSbZg|0wMvxPBI7pl&}hj1OUrqfGS}X5D5U}#Of<10FV=_ub2QpPOQFS0suL&`icnv zs~`Z76RWS706|58>MJGykQ1w~m;gXdtiECbfT^#7 z064Wd=e!w{{IjeplG1ZRVvx2DX(!lTro32MPfz9rAk>?U!_>{(%GWZ@69#K zW-0!Etqf3{qS7qF1aU1wTJ$Yd0|i2=G!UV3u~r55Fkp=Er(2;0pDCyv{~whBYKuBq z93)|;RdrP~kvf>C0vcRIwd4OIGC*}?$%&|vxfF!8s$~mq zvXj~nR?HwC9hPjFVaV^W`c)2?VvFtge?kVSfD)nxG5$X+15^o7gBbsh%K%kE)F8(H zhh%^%A!-og|AR6>l@K+E@&9r{^_37ci1Gh&LiLppHHh*5azgc$5H*PL|8he0l@K+E z@&9r{^_37ci1Gh&LiLppHHh*5azgc$5H*PL|8he0l@K+E@&9r{^_37ci1Gh&LiLpp zHHh*5b{X;iN{AZ7_w@Rs6mYXx5xlhLewC}|C?oi zDj{kR|9`LyP$fhSV*LL>GC-9OHHh*5 z2g(3dLewC}{~sWOR6q$)gBbt6zYI_%L=9s6|9&z+l@K+E@&EhE098WNAjbb2Wq>Ln zY7pc9Wrga?&nD51|CbZ0uOMow9se&UR9``;Q#<}2T-fhbNpMm-{$D2pR6z_;JN{oQ z15`oCPdok}kO8V7IHw)|uaN<&Ad;pX|F4z-s=(b@^#AYOllT80Sv7Dz|I0T&-8FE0 z$=LBtQZQr}?yQa6Y&YdiZs_Klq+mP>&OJ(;;KlDN7@FPPY!z6cGT)@H#~Xej-=weD z9Z+Y`3r^QveUn^~nnq_Z=j$^8SJe{sGDh2sI?Mt!G>uSn)@^LkSuzDAyvXLeW&K8@2*O*Z&ce*w_S``TOJL@MfQ z8Z&xhGa2!ki+G4B5@0%Iz+}1u3QZSWat95fOYZVsB{7_fWOC4drO7Fo6AtA^*WBO} zvQR8!;v^FjBcJE`1b#hs*RGoP2V5D8nLJ)kET)COf&AUF)mtDtra{PF31oiFl5+lAHh%+c56z8G|7c^85;K_r#Vd~P^Mh8 zU8@d*Il`K+n}R_-N|v*(CS5ku=|olEQqOP>V-B+U=n&*;IXrH^#^fvIjRhuT$igXE zK7vBic{}`05)$n2yRqI?3dYwW*`dCh9V*!1EY4D#2Iy|8;e` z-{>22g7D{HlPp;yg;+OGl(8+(93k65W*Y<-WuwSK2P&6|=Q_XhzW!ufXr1__Oo7da z!D=FSC*(%UH|;C*|JUlp?O)dgUzhfwi2zZ6Gfh+jM_q7{Nm@;DB!gxBPD8~PA#(ww zT%Yj-ikm&rl&!eUU4ZNM;&eJ;(9}()Mxe&|J#Kw7==PYkyf;6X_7?+TeWRhpFdcx| zjG4MSN|#%~OtEFtspE*5gHx7(owlXpI-MI#!SbAug{F)C|N71zTYzlCqMO*B@oFnsw!oneH@q+?4!M_HQ=3+*1kK0n%&N7~%5H?797$*?z! zW6O3F(z>$QJXA?BSj|aKJ89NrhBXG(j~RS3l*3Z4f-{?MaybJ6qV+uF5Bc<-2CPP0 z4#e*c7Shu#&TF2A`S6+|JdNw^C7aLas6?}LI>XwhK`v!?Vj6R{Ji~ANXZYmeCWh4E z4x_V%-&59*7x=FuJ7&Pi*NSNx^)9I?O&zf26&Z%JJ^M9bk`jv z*012Qc#-QrQmkLW74Rwr_oWM8l&(8Mas3LeR%O)BDXw3^CA5tCTVnkRZp#;0|4qg9 zFRuO@itAro{j*~I3NRO0|8>RnFRuP;V*Ltide!3AUlr?DKta7~QMYF6DvIk@aQItz zYq+kgxc)^w1FtKI^(%M;T4enf71zJG`Y$N1e{uDnZ`UuM8gP;I&nT{carK{5T>s+g zKP%R+fcJ2b^`B8(|KjRDt+@Wh)qhH?U%`{sBI`e?xc9ZlDb}w5bCLCbxZ?U3RsZex|Id0RjvV>hAlLiZW*_9gDfnNgf#X{T zh%c;z{L7Af-=>9k)pDE~^j%4h6d0m#$N2R-H7MArcXMi}6-zwD2`~9J8FjN84X97d=WQTtr>%Hr07QEy{ZaL7alwE z`-_hq`CcW&>5F80UCECA^KDQ5DXp9BiEkGF7SrpzmIzk1tMJFJX}UshWN(JA*4`i; zAaE&leOyeI5ii9Cnqbs|dP&sJ1XB!>(^YJ(20?R(H54dn%OQO}UNdd><6;&}=UV`T za1O1J0c*ybw&7(6t-lmWxqLpK&5{nAtlDXazMcz9qU98sjH5k<5Gw0^MJ?bGy;PC8fktw~+ z9IDx^ZYz}m8+fba^@j^|F59pf0@-xBF;lg=BBo+O?d6vo9%OUq{x}Zr|D*qUA%7i(ew3hMZ3ts z?@PN-Zgyw6T{QM+P^S^r-L5uBGF~!)vv9?pwV@@8w`2&gd7IOcG$qQ(1|5astr?hb zuyowA**2JHRHHejoC&#$QKK^+z=3HSg{6XppxfzV+>MgUl_aeJAa4k=-c|*)CGdhe z(+ruT(Q3Zb$jum?p|YMz<_hKrq_wf88Nw`+|4(ST^!`84dqdCU!073NYxrNj`T5V+ z!0{!Mcy!=-QWZR0FZ>uT>)77aAydKot$1u#@P;XVU%~sRIPcB#!%`w(7eA~oVgagi zcQXs-VHOH3MEu>=sc9DRx9EKptJE36#q#UtN!6;gn|4hk!iIXnPzkAPQNlqu>@Zn$ z+jQDWEzR%sqfw~ZWHeefl+pbQEc-LGFPc)5wQ3piv3akK&#cQ9vc8#Yl6KUP!gM}I zT3S@fsdkvD8J?avmp8L1E|LmDS+13U{CT_IU79h4Oc;!NbOjAi3f1JfCW=kx=gIFR z4;D;L@PSq^lorY4y1FKQ?Ixtu1{L@~e?3O@jSsZs-u$2I18sy1o4gie7}Dhqg~J&g zqnQcX0~LgY$tDJ?DJRp+fI1ig3<;d1XJ&Gny&;3kMFz}*0NsSc@QfQ~0G$cTLTEA_ ziIgMB44SaeaZRqtQ zElVa0l}av>Q|C;$!_x2vQcg0+um}6in_U{uNBYP+cUG>ykl7uz@)n*e+(#JyF!xWET4*X+G6d zJE2pOWU4eTvUrW~-<3Ve0Bt(94+(jS2v;!!tqA0#>qRQ1rSe(Wg<6;hVWTrTd(vpl zQVu((-t6UuW~XaZNnbNWXDBZja3?}JT>vNrGNBx2tS~u?-c_1uV6CXZ!#Pr6#-0s$ zOPe$%dAH(1)4w?X|B`?IAN~H&ZoR+!&p-Tr zBcZ5)?<6gq1Brs*iG>3qHogN8!gS_mw}ZMqXDEov5YO}qVlKMxE0h8#IFNXvwh~;f z+ssLaDVrf1Xl@Rvq^wiDrc0sOr}_mCB=Z2i3CiRzK)%MHOsy8sZ!$xS1#~y#G;FC> zSjxuInU)tVnk#4}VfOjZxz_Gpa^5lroH1C*v@Uv{BQ{+;keE9M5(NggI1`P&>nn|tiKfe5icdIbH)^$EQ^pzyXyKA25{#JL z$p)P9=26J)f-urk(#O)#poxxjdqwGfAo+TxjDO)kq7mk-uP7*U-bBS$wV4m2TEL** zD>$S0jkJM%|#R|uPrg)wVXu?(&w&<+hjCV%kuY@;0rKhP?@RKwO77Mmq z6HvDZpR?$Onq?T$vc{sr0J07;9wHf5YgK#7PE9?O&eKG&rD41UD`D}$>LL+t)y>sN zDhh*npO?RTG|S3^CN^C-kR%Yn*D4sQi)3*JBo&dG?q$Y=vpD z6QcapX0Fu&0I=dc?@yFZf|+jTsBA%Ni6RVhe`&ElT7H->9mE`lxw;MlGNoQ3_Zgq zKSPp9%36hNGp(|=R&^HZkUvS8<*h72)A?b3LN6=~b0OuXg80`(I(K*F_fc@QFK%HK zTo;JnS8$`(eaoxhxv$&G)#crtAMmr^|G%F5gD!G(fB*k#IkvQFgLac%ao&bT=ZWae z&@@wNS%?N$3He-@Hh@O0(UK{kAgh%OoDU;a$nB-nz97NX?X)hcosODY8q2gJ!T=P-5~#jV1`HOB&Vd$W z?(T%9^ENov^GMHuJ(u<@d2-3sOPD3gr{0^oZ|b-yaB6b$g~y}S3Ao@XZhFmc61dSZw1cgF7>|MoaIJ~j5z*hORh{=fA7u78XE@ANyzb{u_T z^!(8SMxGrxdc-j@IsAv=Glz-c!J(fFWrkJ`zCL)_U}12JfyV~UABYU>ywQms+32tK zAJc={naREt8erCWig)S*L5XN)!|UwW$$fYCOmrZ;gW5d~<;!U|QLeq2>drI4`zXyS zB{+MHo!OCNQ+TuFIAagKZb9ZveYbsmH+DM0f6wwI-;-qgf(i?qy=I9@82g@LjM;oz zZH(`BbhRlRf4Q#ob9HrEN2UevzG#=uI18dJ)2SUDZVE%v;VFE7j|hDZ5Bd+{b#8)f zR5Fyv*$_vr>aLQZ+AL_HC#7LCe-;{hdf!36Fx2Ei%+x8Q?pepMy=x;J=un9g#f^-+ zy^G-X8q^3&`lcW@sBNsZny>9kzS?f%SW{}_>`jmCHtJ?i7D5|GAClU*0NfXC+{jr_ ztatQ5sg0Y0*wDt&-$`xU)K{gs;QypH&Ymn~w^2K?M;1G8vpA%?VQ(1(!%iNMm~3Nj2+!+=K^>e+Bt9Vj;-sob5j_z z+IeKBotyfu)Xt+M8`#v>zO zz^1+`nLu=G_<$au=gOYJGY8#+lYKAt{b=CPf$G3E{ZI6t*?&m?4omJ{l3X%8b^p{^ zQ-P`NCx110^rUg(qlr5vj-D`&zcPOOxV*+ncZ?xyF-Uo-6o-2p@h9-ypXYk?i zt;T*ocE(tEY-;qH(aPw4qXQ%Njhr%KADJ4yhOcxpexyYW_IX;)a=sBSp-nr5TBGP3 zLzJ{Je^DDpr4<`?Mjh3n)sxP+g64=P&cNLvzO08KV78eEJv_d1M?#tKT_jvCE&6gL z+%T-icapYma*NJB_>3=GRL#sDb)sf=ly(zxWor-5@f|w*m~6JxDuq-xabB5U^N6uivrLF~%_AVf-(oD` zH_|c%uB9`TM2tV>)>^SPZ?U#-qO+5ii7|U9^bK{kwK5BTnUQF8b63f}@kNE7yJ+?e zc6QA&rQcvceIo}A`i;U7MrP4D4Ap(Zos+W6@?B5LePf-zlX>kiyYEKn&Kw|fj+|Y1 z+|ro?WETFij)&&$0msdqIY1`7GzTDyD%ITaGH&ZkxH6^MFkZ&3Jw3hmZpJ$HO_BdB z4Rpq3(qgrn;aWPKYtZE+Kdp9OWZL6$;1Pwz+MX8!k9RCIo{J{a{!}I2%DB@OlgZJ& zw%HcizkkOTWETE13kB43H{$*K+4zevP2UPw89XreW~Be%j#0@3+F(@u2XyvOGK51jsNz(euGV5?JWf}u{RefrRc8I|(a^tdr%se71j6@K>FOW51>UDN4onbL3Q?7r(doh`HI{Lb#XzSCbaA=>@bceS+E z!*YvloEfL@T4}A9TfX~R|MUHXH>2K#^KEyzag81#JXOdq`(=Y>-eMgw3h!X@!8i2j zT9fbyC%@$UERrE-J^tLfF_x{?b-9{2qamE+`?a`OK{G~A25#WyQuJU4RA!? z+49P4`i9OLF$nyByYx^z|NqqIQ|l%_ojh{l;|Xs3qw(h02V?B$d!x0HcSkD2Zx5G- z-WnS|7(A`?=OAH-dB1PJumUazvO4#G0R1YLj?(*S1Cwbyl~3ib;sYsCUAau;l+Sb#6wL37-341H7GROP zV2NS@7P$+iL;*5pm^D<10A2TM^rk>V7flCEwW`rnh!SkWrit6An2FVelcD%xcfq7$ z0T#InCKL;>$Xzfl3ZMXVvAbYQu>gzQ1*4(>3RbvPi!8v1D1grxCVX(Jm9C%(9oML{ z-U5=X>HOYI%~{AMalo#HDGt#?=G>}9jec5p%&=ks7Bv=R-7!Oo1z6NT({;xTiUO#h za)|e3&Kcr@FBkEWjdnLBC=F7P$-h zL;*@9#+q1XiNJJaolzGDnmUuw3Z)P?*Nj^t#)e5x+RQP`6w@ts7xXF?V3E6^N3j5l z+XY99^((+!+#V45|NFkzGjZzZ=wO5Y<(r>xH1KcKz;OwCC-iYq7yONB4TPYdF0=++ z=#T!D(8p|qt7Ldyn@l?2@WJ(li<^~6>c;7MA!GtVGoHpwvGOmZU8PZn#aaq#LebPT zZuiC!*6OaLY%L6QO{1KtsY6&`x>j!aSt1OAwHjuQ;#qr!Ntf$+Clv+Z#B^o4sRb)| zOA8ka<}gnaCNIKQXu1&kxWp!gKJsMb3bvq&6X6SWp&Z&xNe0$-D5HWHdcLoftXkbe z9~JBqyM;b>_`rEtX2&~zJ>kin$~Pt>pX)!i+eT#MGN_L?jt*%DSRC zT;oAXWqG`NTA}G;Ff!T+MpiJ=7Re&JrovIMC;azVq=I8ZH;bJA@+UOo>o?9Wa{OC@ zk#$<*CWDc~E!q(wG~PsAm*q5ACEYOCZ4^qt)ht2fybZ6O(g$W*C10w#`N7EH88VY~ z<@_v=@WJ8Cv=ddMe6kv!o~eTopkSTRTT57=5Oox6Ca)tsJ)HzfY$l$W_NR2|kcILP zdR@V1_Jj4PB@?&Xff^!@h)*jtowvc`OIE=KPv&{86^yM#vcaw?asK0NP>FzDI6F2x z1^;_&?~D5XJ-pUho&}GebGmLs?F1b~ATq)P8iYAucVINyEISimDw9fsL^j{Zu`Y|B z&{+(dPc=j2L4()j1alrN2v1LwEo&95g$ibW(tr|Cx^8zmb36f6#Mx}dgEfsc#Q@cM z#Wxcu$D65~J3r0A6^$Ru2ZM1ZYiig72?~_S!67tVjQ{WHu?X@1_bxeYNr;dCe|hSP zo=c}HQ^u*D$$KYHo5UxVPrS^>`&TB66Fq#q|7qi)@tyd1|I5e9V}{YsNADRubu>7- zY~;m}%SK8g(C}x&cMqR3j1BKN^uo|jhl)e`!A}S88a#P0Fu23O^8^1oP#6IFKk2`- z|D=9@|Mq>)_5GwT->2*Sxc83U6MKEVOM9N}a`-e1&^z?4QUMyPCedQ_g&2`-8fhQq z$Z12i1no>j%|RMagm|DpU)%#fcmLd+XdA|q++j>omP{|4Hxu?J`)eb5ib!_ zRTE&b9M+@*mQpO8V``Ch!QOvJ1@+lF>(Lvn84`*Xm}bJ8hgB)JTT?Rfhq|T@^m+0b zpcQdEaAi(OgHJFWMX$@FzWWt_ot(-GkY;~PUll8)o2%-x>YKuZZUa(-RU&OxEhH={Bq*6U~xZW zjRrKDx+_sm1frM$1shV0JyY@ESq@y9Y)Mos0cV9iqn$auSrgL_E@*#yObELVmadIHfF zNm3t=l5~=Zl@t1itp#dzKD)}437FmOf;~U!6m&3Q7lom!aGnWRNN)?oNS(gms}<^S z)fjd%bXrpl@ch$K8%H{Atfxx`u!VY?C>w_<%#ILs7fG@uV;~9H>KR|viqyaG8sga3g;<Qx*$}uBDN-2tJ)c-p9;H6l5!?~-6`m^aay95^H}Q^z?!IL zlWNRc(#I1q6ltjRPDdUn!){l(kdp?$RZ_u3IOvRd^~RJNWUMtV6!+v=R|ao}ajFPW zOiRa8r)P+4!(eC^9QvVDFb<~j^|BpDachj%jDfM}GD&TmFg^aZ*(!5Ln$AJqrqSsR7>wmc3T-r7))>oh{K2u2$Wy+A$C=i(YwS5os*$w9W*1{{ z7a&(HnJUEabdX_-80P1^e%6NJ{Jl;Sjrd_iQqC08DF~?4s(Ln2A$k7mR1CF2$&6Zs zb1^b)O6uvfp68gTnEe?E^-{q|jnO#O2749srYvr*#kn~j(#jI)Y_yPzLk5=vtizfNK}pszxKb)e z)X9X7$yBNl6`F2V0iC{XD@Eb~E)53QW+oS_S@~nShJ&Sk>^q}Vkcs45Bx%SKk+5Hf zC6hj)z-EzjDpP9JtJXMZE2u4QJgl)x1K>cZU>I%TIitrLj+Cr6uhUr6$DJ)VQ1kdG zqm?eXR7P_pnb*;nq@1aaJ%$K%1TQ&TD*isto>l|Kq>Zbh4Y*~_JM$IPS47N>1g;A< zrCF|j2dTzTP^-c8Hmn@2>ndq|sGz6P83rs@TiHUMNN}{(gt$02ViuQVp|~QKvk%BpJzu z4Jwbd*evo{Kb>T<=*ZE5B7d!?lI(HnLa9c7*lLd2z15vfPyZ_1|vb+jOfI{y7m zK`>E($T(=xQ3hwcVBqJ|Do>?tvQ}_72-vDltZ6r45Zjan`Pi?df_6roLjpj~lx0bm zt!YlXoe6#$0jTX2+8cx_rL5T(cC|8qWXPkP$;wWJA$3*d2aTDa#leR2Aup4mVmN8F z#i}Weqf}axyPS`Kosn}}r$%qM$!f4H64rVN5VRVg1QyQ$XjU8ba6~Dt0wN^`?Wl&O z8V7gk6a-X}xW@>3FkGe2_-8FD|D3Zl)3tJN}i z>i|ypxpda)a_DVI%$)b}On_(tOglYkeN6+nrI~#ErcObcAEWA8#lkgksG6$e>J7YH zY!xY8GNv}#ksM@&1E`i$cSQY>R1h|yK)lJCfsz@o)pdyyozUQ{Bgob?4ZERg!u5DM zjg%TtXR^AkQ&7!DwIL>vuQ>B2XUy8vtJQ3x>WZgEQZ9&8 zn-O!M;I+95X@@^mR$2IUxd2#HooRJ+jZ_e{5-iV0mn_vPDuOPAihkM=%W<$d=uDf5 zVAhuoLlI9NDs>jC&a?`GIw%FkxmKX!^0@#bVI~@owtyME6*%ruS&5VGs>~)Zo9K?n{NeUx+$+wp37)p$R4nrEoH@OXF#u4*?5N-en{n zQ+ZPLqB9PmalZW!E{(W}RZ>B9%HYVw$cBp5GWt@^%^yQzl{lWJeCa%72s#q>P$X=o zDtw%;czo=|q;6EZ09Vyn2{{TC%Hsnvl?cg4AZMIhKF#mq$_Yb2#on+#z`ODx8PV)3n&~)1g8bbKrhT*Qx_h)jQoxa4wTA zWQwabR;wrLrcr66j_o?eAGh5-SM~HnX0o5}^YM6*Z&hh38Dmo_l~l8;q$-t4&(CtrC6!bf zs#Ge;6Nh>7l-OZ(=onV815Fba(1a;uf@XydD*@6C>8u5;gr->p0wD}ehxD!^UEg!{ z&U;t&e2qI<+&{GLdv|~J{dU#2_x`?bf5Xk<*&_@#E2P;po2}{nMjw0rr3P=<^PL=0&D|f_{o38u z?(23wx${d``~k1nhk+m9YI6Byk&D&my2)hr>)@Z^d=Y|jaIsKAO4ib%H&gppqYr+3 zV>F7Ooer(I8&|aJ(Lc5^x`-6TzD)P8AN`|`j?R~mJUwG7o$E*cz{Y44$(dEx8(ly8 z?`@1OB3Zo5VB_mYe_~^F0YMhH#BmqYH3x6x1rp^`k%foY4g#GRV!;yY;vH?v2rTINuJY{O0BSdh~Z~ zjLyM1Z@>?m*CYAPjnP>+TW9RDbN%S=cyu&qvIox_eq%H5bdBUg&lwF7R1fc6|LhNb z**KDIh1;H<5}f+7^>dN1g7>$9N6 z6lft=7}oJ2pNl8LknDEb<4L?ewf7Gm5Yn^!tS`2v{DjYzGlUnLdM`r!vEv968Jf~C zP|W&R9$FhiXa91I=xWr+RYZSh{a|F7Y4#^U*=U%>mMNG+8eH5GEXU>W`q1{3<*upZj)k7GISQ|v@AUsbuP)@c&fw#*0c0bgV>l3GH{MYnABcg9l%|oxl3v zcdZ#e-!b&gmDT`fqzt<#%bK8I9z8%u^4QJcR_HN|VU1>*S}k?!CfyzcYwiCPYrq*E zZ$p{sGzeh1U!^n#l$q~Qa;QecqMI!cjarw1adl>`&+vn_HwIdUZ7qia@3CYM4KY(6 zQ9S3>0#wo)Z57VI?8E|LEwUm%c=v0M6BK!3Jnc_M4UD$qaJm@MQqyxhAD=-c+d(94 zPCKz96aHE;y78tJRA}a7Y>r$urtO$xg~>=C$^-#CRzw{(9ms42Q?0ism1gmp@br^w z!q7*WCdzv#KSv0R<;7gd20q|^mbB;a6nLp|TdBrz8GWFqthGjHs#ikK>`x89&KEty z6_+^!%XR7sR2-Guxd?Y7woWZ+d2KeH@@tNhoEZjuJXqQ)G%#CqvCLXxFr;S4dJD8PE^VPBUMFUQfC}R$H|7%B$Pp(R%i~mMc^^b3DMdCc=Yi#$6>n(>nhw*WXdR5 zQb`_j(sUUR<_rN12s-F^ITZPdq-zhnqn$P3z$*B3!9>bvjjmFHjZs9Hb4Grcr<*0c zG@3P9jc(Yag0;SRB(Dj3ZAdpNGu(&BuJ1OR%}fXk8=h*l1F!FL7H4G0c?QFef%7}ruEHTxJ zVD}n+sZr4~oK&RD9#Gq+1n9XC)Ftq9sf+J@{{zA-(w|gX)p3uF`Gt-(gnpT0=mi;s z&~gc!R-=A{tcdPxtr{P{aZP9&Ub|kI)moefw#aJSLj4*%Y+F_Uvn_4}YqUocuFR_Go&2cjp4IhT|hxklTj!VJR>%xT3oN{xYErr7n@IC}k>P&KAh zNKjR(Tytt(+^I1F(&k}<7xBD-+qFX1N0jEgQ(7}Te!#Jf&57Pj>{f(P(X-2WX4vWT zmO`pg)Kdd=ILnrr11hH3^>%;y!)uNT3`&yHk*yB9NY9Z9_S7d0N5%6|z?jw6Qk}>( ziR8%W!Io2A6Siif9MYk>ER&16bZ$5X*LSqSLt~^di$dHkOvrh~Usi`}_s{-+T@%Uy z*SBptjIm_~b14O>2~eb(VH^pD)w|O(cG0QlNqJNTcWu3l>9-%;I_~Ax zgyJ-C24$qbsN@5M8w_ep443*kq)TJf%M?|q2i)GK5K`;2>c-z+6ADb;sSnBuRjAQA zKeUOCG8DzhyfX{LYSzkvhc0Q&%kCf8I!~jK%{i@+}<$75z%TBA;UVo1L2M00{aZD{cBj_-QCj*4+Wo%5SYjj-f zhj3Iw*kcfM!m z&O2`Z?CmeO<=y;eH(z!Zp8l&-=;Zy!zkf_0ef;R~&^-9Z2T$*R)s6r22D10HyT7%I z0fN7pKNnAFcXoEI*NQtkC&#-72hSYuos%6VYUQ=^U=grWSe@mEdIJVm^wG4>*I<27 z8xidsttc%6d!!!aWR377L3p0i#tfdZ zJ*U`Q1QVi#QCe1Rj(Ru`?p79+a#5tKVumj3(P#~DRl?%&025^2^~cHYcQ?1i_sVYa z24Da14IZBRjVj&AYr|Sri7+S6^#)~qmSI7|RfvUg6EfkBTdunCxT=2Y4F-YcSa;+3 zh^h-xZAeoCsjYeOrV(|Rfcv)DR#zU-fK)_Wnb( z`{($))2~>)VO0P@Ix#Y7NSHU{1F=7Iqnu0|J~dsqA=~Z555D-P0-u7%quftWe(0By z-@jpRi}KZPOHgJXqP%gwsCD$gazWYJ0_}CjlN`Zh%&ANm{l!!<>K0;g5U`@90Wn{{ z)2l^N9*^@zg75=I^7}z@Tbxflli+-0fzzf`j(c2#DVl@H*sIVEIhwaM5S|ZHPPpt3 zNYaI;^O3}%pAyfZZcQJL@bXK)@A7@VZ?rAKmv=T7__8%rh5_SHgQ_#Zr_Fh@i(neW zje|VKl72?l)g}R({ZhcK0j?MLW0z0z?!LKu`TLXG;(JQnc!QsFo)?@cMzT^T!&eGb zsWR)%#i3#}t2#A}*lD*#$#c;fs>}L=FTJX0^Y}6npOawwx__Pge*N!Wj}W=)B)DAP zPqjA?-Z^g@b4*%BVv&sNTy;qA3P7j&5~*tPqj7>-Z~HJi)h?4^7I_U&-){h5U~_}0A8 zWMo$!%$vj5Sri&Y%kW$PJKZ}xOAt;znfy+z=Rvqx%IV^%<_5ykbGE6YmI!Bs3c`w_ z8#Re`zcjHc`I<7Bgj0(I1`4Q5D-C-_N`$8gw&7nSzy7n&gYaqz_0?K#Y#=;2Pf^(^ zCBl;gTkjW>-~PSlL3q_kxbxzUax9pqLLk+r>L8=hIoxi_Sl@rO9k>B zmz$&ZP1~Y<6`!EJqLLk?sb955dqpLCEXqs8@!5AK@AU0&+7{*OxCCYCp-Q%Yo}voN z8s$dO;m3deOKqp~M+wwtKC&&;SF;J!N3_Qj)k@Y-AJHBk`${jh$M*LoIMt7Ci}NWa z!TE^xn4+f08t1mB;-&W3`soCr^15vizMS6F9#iBsKltWTOAD>Xf9eF81YiDMY>DqO z|9|)OApigPeE++_U;g^f^>5(o$J~S5q!iVw9{ZuERyC zm6JWI<+YJHbg$61Sdo%xln0m4_gZ1YXsGC23oN{o`;h*nsq!P%hRGS_Jh3cLsoU6_DclUHfIGW zuWi|mKAAdLhu>&3$vXT%cQ?(M^Mux6;oa|iTAPJ#*NQAZ43f|LbedXl$)`_KJ@5LX zEbUnsF2;!@K7EZQ{MlJlv%s1rFYldOpN4OGB@97HQJ728u4&oYY!HG+K;Zv<>SMg~ z!4#8zj^H2j$-iDV>&wSI{I}Ar8`ixth}|cOq>`_QPrv(8hWw!C z_?8J?DOk>q)xBKFt597in@y&Eng35e(6Li2xF@vHvNBMWW$(98c%&dxc;@)$WPt z{YIlwr6!854JUM^*pM8UaOD@hvpWhB|{l{B_o4@9!y7wK&#L+)G`ilKmAO3^G{K4Np z$lSEfe(UVn(_cT8PyXczcl?XHqusqT@$}!F&Ub!r?_%%JvP5uA4{Z^0xBrQq4_t3< ze^&nQL+sV9V?NH=KHJIS-GUUj1K22p&_tX}MpE9Y~95wa3srp@zXP_dKBt9iaYqx)r%(C}7EFgz_6 z5p91oy4x%h$M>8UjSOMd(Qv3!L&UA3c4yQraWt8r<2WG#}E}*jH zjqVN{s~;Nk!D0@gmfLf}d5%1U4>4?C>EfJWhJDh3T1rN+n!FTGo6BV@U}jZP7%&Z^ zv*aogKQ*LaLfvig=EMz06>q_fM3a-k%_Nyy(nAc}2R@!xS|E&WBKB3a3$reQ=f+mC zBKNFbyv&*;9V|e$bypK*4l?d8N0NxSwfV48kqMj$_04XRM?d(YFr)}6+ycW7JjAel z9OrqYm?5Ov-FO*D0)@3{S21TGq%@CFQef$-;$}t~TnD+t*xjfc4QQe*H$_HPL8OII ze?%rh)9iov5X1J7o#)zA<6T+Tz-gIViVj}SvNDlrGnQdBVsdWJltBfS6$YI{cLlM- zNiE63)d|;`dt|hke6&A!h++F!&-2=h9_GE~(iq`(8Lxr6+)^1C%?c4~HiNlbfeqcw zFfpX%#k<3RvzsPMVUY*2|1jU!O!nFRxrZ3G4;wwNhC_7D7L?g-pd)+W~Ht zO66h?AH&2fch?U=TCpj|_L8Suicz)aoLPUmvvcFSJ}XMYUo-vdH}KbQ;Dz46!|h@F zi0Jc?*+G`2SeEq`sDS`AJ=3Z{c9@@olc8$R-2j>Om0Z7DXi<0TI6-24quy5dg+dWc zYBM|Y>@^QDY#$?iKG*w2y4;Kyt_Y3UTE15Yij*qnX1(TEBs#D-oM?jBXTm;pciK_r zR*k@2FOI|I!mezlE$)2FLkuMq8iQWeRaws{-H9%hi*q(|KrO0?HCRZ|oE9?rz{&c= z{G3JW5C)1)$x=Vp(9K#OZjXczM>%}T%#Hj~sZ^|bkeg`}<8E`xiQ@`aZ4eBozroI* zt3K>p!LW5vv~v-!$YrVo%S1M84~d>=ka$}L`2u;*u2i~Ni0Sg#UZ&WsY;$D&%tH*@ z*H=47^@e5-q)yAk1gYQAZM49YdV_J1GO*bgLvp}st_{akO3vM_DHF}ge zr8kewqw*CDTbF7(CnLXG&bzEv;5dudv1&i>kCj;?-{?oRW^=*OGhAr3Fs}&RRma9q z<9Ke_k3vTcr-MxlXYYE5Vf(6Y=cU%%h0C+rk}sf{@gSF7f`mAocC#Sw3U%v{Ugi~; zE44&SysL~QH{^+yJC$|W#7%Cq$L#4#lKcOscC?*4@4fw(x8Hs1|Go9jo1eV7IQ#Rn z`RSjY&QAX1Bsl)#(2*!gWh{``O5 z`}TvxA8?)`Iq5vE8eJwIn7%QpF5g#t(jEtL zH{&^50C?L9K#GT+Pf82HDLYCntKpQP5Jyc}TpDe(6O=OauC`3el#*-mRzsKRVm ziQ4^Tre1)!LE9c8=&;|Qmra3F?`jHN4|+4Y&ex+pqY(W~ZT-ES6@U~EJ@1jWoEMDz zqAFzuMN#zXS*zzmBgt#!RDGb%s2L36g{^T9+5&*O0+8aN=ViLA0hdNdRcIzE*<~xH zNqEZJ@E9IL)pFUeT0`;0A&Rr#Y4{>QNr4tjGAwfOlN=vk{r7HG|o&4wJJHs^-f95`Sn(Z z2V3srWuUbJAjG9+V?gEW4XcC2ZMcZ4MGh2agJRVqM$Mt7R8+%vt9@TSr!oUrwF>&c zSKS zT})g2Am43^?n0g;jJE}V<_bWHho(uk+5$jh1t3KRSDIw1EdYos04Wln(qtk%J^|&u z!U{l&^rke)MOy%IAVqphnnaT=01ztxDY946(2h-Y1Tulv~yoexXMtCWXVr z*8vY8G5X_;-shK?6s{0k1N?x*Xj}8UA~7jE9_~8;A0RRB*qYBH5|hHUadFa3KGQQ+ zcWYKJC^4Jai)maZ`r8vIx2(5s4P}+fpT;}lKMQyOiFx+%7V7g$ObWk+{Wic4NX*-| z=66M6Qn)AFX8<1{G0$wx=MjlX;fJ_5=_H@&8LP83s~41*&4hw9ehyttpxm<5t)Z+X zCPiHszYTZ*iD^IHoP2(XN#SI$6~GTjjIuSqD-x5!m*BPlA0RQUt@%76F)5q|7bkM^ znVzxaty#UG#B3I=N#m5zr3A_?OWGRBT4GZ89WG9qtNAt`ulBA*<^|__wY+7Lw4SD> z$i+!xg{ARCupp(4E1fj%kc$&>g++WaST+lDr16YgoCqr{!eq-OltgXtR$T zzyy4Zo$=Nd=QUp=h0*TVfFbxAJEN@`Uilm;LLYh-UxlJJCLE>wiQ6p2=Chy|e6^vpj|Q?~s6Da+YuJ5xa7hr?&q;wsZEi zXD>hfp;PAMr%#%o?*F@w&p_q>FFO3;!^)lSxbueFKLH{Gz7u!`-gWjL4t^AP06wyh zfcyV9?fuH0xwpGJ-96d)*pt8hUp>)!<`8t5zqRvZgK^jA!^4;6bO4(yV3R_r-CBFR zs{UNuQ~{^gKJ(HvcTQV#dO=lSbJ-=c*V+jhFz*z%Q(L21sXs#nuy`wAkwTf>TElwo zIQYoX+?v;;tB$yQ$uCV)JZ@`F zFSP2O(?8*I)m0KSVBRUJxNePRwd%$=VDb9b@0<%Ir7e`5g*&a8x#fHo)qT7l=EQn0 zWcfxBP%wI^?Mp<aaJSE%A3?vbwi+(R~hM8=Q+nUXz0#i>uQKCs~-Lpg zzlM_{3*pJY+4K{v;Bad=DRL5?44h3*!3qw$hLgfle==}3eFZBx%o;*vfavO%8LIV`MKX9h)LPf_homVY=+XKtpSqSumR}zqYb;GDX47U^BquE6Gg5o zTbe>SPAYY#Q7SLTr;y3(>nW$$L#}HLusUiIC`ntZZZD}T`}E_bKOuN*TY$~YFCn_x z7GTBuMU_R(CC_@nvZy66;Kh|iy$<$^EQ^{$O3z>N+=_-3woqk}VK&t9$e0usBu!5G zLRbzbt$^!k#kif#;-I3}i!O`Gg)NPcJNK1k8MM%=87SmpjbY@FOOkI4EsJoc_vSju z!GVd1v^fpy`jm&7wpg8Lfq}<`MNsftg43VL0LtCYKs>=EjXZWqCk2LI+>*Wy_KRK8aOtwJ`L&gf zMRz1DI+lmaRD%#k_o8uhf0k`5bN5>TF!igFFH(V1nEJ|#?pPAJK+SPF+!LJRk+=& z6u2qARi)!xGWBFh->D}{`di`MC#y3)x}=k46xS?i=(Dk;Q`Atu{*+Fk>0Y;_*J^aK zLZ19Fy%{#Pxuzd5Px?i?KqHZ7uNEaQK7Oe_8bO>eebFm2TS!j(Yggn@ndN1n1iz)FtG;7FOrN3=%<2IFz6Rf`H#Q1Y!w1vcxWxzBWpB=n-!pjgf7^U2T(BfTA?Vo;6Z0n?J4naMl4 zZTCjO0*xX%*BSVbK!bv~AuQ!(*JyNf;eNf0x5WBH6Wv)JfoThiS_6S>U|aRr$|X|= zYj73+|D8K`K6K})+aJDta_c=e|IPJiL_%_qNp!XN+pffV}n@aILh&QOg-o zlR4;huJFx?NH-jaH!OFsXckKiW9y`Y7jjl+H{EfXn;-$cm=tvdwnnn{ zY&(EO;$2FSm9TZF{BxI^WdnAJcPT|~!q)5_dY2pvut_}IDe@AwX7h+=``AoA5fGLX zNe5eVdO=~?G`uwd0bU^5fgNj3Nj{7S2|_3 z>A48YlW5sv9i{0)&f*D$;%`Q0T^z3h#dgwUD?E`8^qky^i{n-30azKI?a+gZ<5k!J zh#?E)MHj(|TF~{Y{>;J~d~054GO{ZV=FMU3EDDXHWq5Aac`|QhGwk5vcolN6lcp-b zlYz2n<6IoC!VSP)j5xktpA0OKVN8m{D`mUpR>PhP(cO{G&%CkTDaW1Mv^#$?P&PH_ zi{n+O!OoMdx46Z=f@~hS|HFqFB=*0s^NF21zkKJTcf32LJ4d&F?e_QHp4@)x?OV70 z&8<(|n%^RBecsLAy7|A{eD_V}<`3vclR?l{>zPDyzy-}tQ*-IyP%@rNB6wF(%#YTukC&>s4Vza(46q! z0DzEi!`e+F5DZjLC{kQY(GZ_dr0{xE5SLJ-@XC=8n^2^Pl&wKbLXqN1zY5U_MGE&n z0Z|D>irCT$L?#p|LPc??mQbV!^(;fxgd#<>CI%4+MT)@7uY@X>^VmLs@~se_P^1WQ zd<#@gC{n~Meg%Xj6e+?E-wb_aLXjeZ@XMjMUe2QdNxCKBNFezgCaW}~Y|)JfiRnr` zHJB=mWyaBFv=0x1H$iW?L?xiPm@?Ql+0D1x4bH&uoYX|;HB?x3`Xo_a^oz=*TN}~y z67&@bg`;(ewnQ!HRLb$%yb;n3O9*Uw%oUDRu`o8;lSP$ap-9eDWRa~4#OhTT(RFNnXQqIj^vOC>jOgZ>AGrm$-vHevecNU zULGnX6cWLetMsI8O?>a?JYgC47m}9%s7Om z9b4`dYmI5XC4shuR&P2-ph7~y^*BY$ntZW^EsC|dS(}z7UL(g!hB}&*I}^}DB$w;> zMvXUMD4$T|<;dghW|_f8HLR_cc|^z-JQ~vZK`x}dPB?SxXv40F-5dnv5(=->adWdm zg{R6IzbIGGhOY~hjH>c@;t41e$RiH%wb@+2?nBvxVmRg5rR5aVrBhLBPL)7BFDRCM zi@;$Pnh)pYh*->uPSG3PhmeHAE%4@)$ygP&9jf)JIF_>dNb3|4xdxB)*^;oDT!F93p6Oo5;jnoc?Nl=ecm9S)N5T4l9(FcHz4rX|kBrJeHXV&y8>3zW zy)JnxlQ3V)Pv^t9iLtE$uZc}*o|B?ZEn?zg+yQ+=txl^(O|9|#)zE7b3Zhqvn-LjU z<-yePCEx0KtmT=gVNX5KXGSEvfwDBTY^x%^8hTAa!Drn`neh8_maW1Z)JLMM4xv1h zZG|{(56o8AqO(NL;1Ts4dUZk}5wIf<#stsQT5PpZ9_bhsgn~trLR^4N0&i7InNle5 z3-MLZc|zfiV44JFfI-t*+_%C?rbJBIuA@i3)HQ&d7sI^KK-x?eR-cAmb;%>63*BCa z^J@aJsLW$m(n_FDXRSzbkx}gTdv*on4ft6?AKitXPAFzf!9uGwgU;}UYBuD=L6KS_ zuFts=(}E+K%tm^+n38?pcqMcl=gV%b)ymfNy6Eak0M!`Kx(b)`lR;R5iZi#^o*EtMOQ0`F zD5h=A$cE^+#B`R`*hY2M3%dD8BuCR}wE#@J43G8M0#WI!UkrV5LZKl_3&QoD2~#7< zP(qm*6pLMCHdu(*U^KV-m|WA$l340dUkrUwLeUMncD-Gm(IqQzbBxBROS%lTqb_cZ z5Vf5(EKIO6g=(iKei8JA2}PTkH|GRQI!&-+3X(IVyWAbci>d>grAaQx883l8H=zi!714vWjM*!-+Jibr zkI6QgX?4c!+}ty&r92TwwFwo@aP@Pc&q*lC%jKBNvf5aT_!@0ljk+qwBIvwJuvP(H zO!Kl*Toj?{qy&8qbmx-Cl$3Kro2>i(w5}lRq!4(-WU68m>XKR+R!o*dtWvi%K<_}e z6N+9_4MVlVYtu@8*^U`cEQCz04SF06v$>8oV!*)-mRzH! zb_<{%aSMb#RcleK^;;(BvXq(jWx0`W#^NpLW63RL!VtQ1!4==p>;)BD%CRqDenb zK%ItfcafGJ!QzsE>Hefo<|s3ZNDYHtWalT)aY7+ka>WNd3{8Rnk|<@=ND+ieyHu|i z7xJKI)Ej<|HMu(9mXD#MOH|Z6fDloub3&IF;sJ;REz~m;xIbQ2SZrCX5zX?%p)jHY z9YKc)MZo~=&`o$54&sp*1*Es=c-067OKi@Qtsa6!hSZ2gtsd+gLI(*&24WF5V5Tzd zPjy~EdQ(-?=t+-R4qbjIPfWA{^F4$MY~ujhPbf$-pDQ)wVy0s-<_yx9r9~ADu~~Gc z^gx^-lY)kvjB2qd z-+=ZKin0lU51g)7pC6tW#&EL9>qN0qI*KIy;&U(u=sd5P7qz5Ba2tdn!yYc}L5 z(GZYg->TUCI>#=g+GuGlCe3*sL`-Y0jf$gezKbRHKezKwcJBO(JKue$e+RvDc>7mx zf9&@7_FHb>y!9KmK7K2@Rk`(&o4CVle@&wl6ZN6)_cOgMYR>F=NZ3kACUsJCEF>!qNWWzdHP$!@*(c@bKVQ z4*sWu;lY~@j`n|L|9kem{o?+O8^3(xyKXq3m%xA9`{#S#wr7Aj{FmKd*!`Qk-Q9aT ze+HmE?hkwc4x7F6#b~U~7g@B!wXmf(?I3F0f#nwM&yiWv3dC{0Pc#-4f*Ei-AAv8x zakFqkeW=tiWZhdhaK&F7`^}p&fdA&3T9+3 z4DxuX3wtV=tJ_7&6;!LNEL7i*rroF-p@SfrVmp5mz5qwg-np6M`)1BC^RzU!VxJYD zMU%G%B%Bp`Wn%&bWthq276rd3?)(k-0vtMf=j~-)%Ji#}*(-RCIMm|qXvX!F%5;kP zsvS?qs9PI_0#qn^J0FBEz_GJ;-prb@-$tt)5KdC3`YsgKYm7>`yia!Gs>{y!R%Hp* zN#8_wJ^){Ug9p%B81GcDneS_~{GR?bG;KeTP_f#QQ@rgzN*B8SAvGacT0vtVi z=LNNzWeWbVSd3_}4lCBQHR;v8(!c{6K!4sP5G~`)=|RqfcHReHfWv3+yn#)jypfqL z7`>K9ig96x*K8Er9H;;!Wt>v4(U8fa-U zcWQHRBy^*}vPeL4bEpd@&$zjrZ-Fns0R-M#zL~Fpp5i^EXKRfCSF~%HDl@LYJ{!%l z9$7&QGTSZceR00?&F}>{g7(hwSm9x*P$GqwOM>LFmS?Hc4uN77OI7kRPaNQs+6kPY>|mrV&@y-3vdhp zKE2AI(5~pbSn)^BTrU@Km=g*E56#sDxl`yMk*#A(l_#K`Z-6hrL9}-sv3;E-`DVcv zC(T~1x(rw%9`yT6FE490fsT|0R+t&E0%Ygw;R|pS?Va;&5Qg99Lc8EYUcS4GbBk7` zTM0S^%!7c1-VuB`%w`w8F0}J?@C7)Gz6$$*PmA=M|MUlA)lT zuYoVXfwXs?cL_U-PKUXCzMk(2!#SQ6U{syArbtM;c2BKLtXWo#GA()MtKkc9B<-Ce z^;tkl)x|iRXUCQcI<8t$}qsYJsK4A&9D;`z`On`=;HENva+3M+VMwOzEtAFw%X~{!YV>e zD7)RC8jDJv16rGcXS1>VUg+ZTTq1zAOt&je#c_)*KieJKBb%%$Jw#RT80fG;BeTrl zMq9^-_dpk`gGseSMYq^M?f0QVeYUiEO?YC@u!3EzqD5?OcgST1;fXBwZs_9jWa?(e z^-O(SS#&B$rZ+9Pjb#P#uztZ&i#3_*(OILUNhZF4#dkp$mq(LcY|50VH<3V>x^RyO zUAd`}YOdI95M5&|cw9jaBB9OAjdwy9muFLc;@}miT_f~)%i{SaKeWsk>5*Q(tWJW7 zDkEsOPTCo>z%8JQ%fqRlbrHs@gjFsFn&0-lP%@k{Ib{}opQ6>Wt`GIhWHQsbT`q<$ z!0EJmZcK8WfSxz=K(9ngcuks$fgW3ver7<8D$r<Syxnu z?3!_o<0UaufGkYcnI$)a-jh(6q{a(6rp`+Fd|@b{0#d{I2{-1gY-tqF(5zJ&WC?Z} z2UrNbJE2fWZPsl}qP&kbqrzxjZ`PI|2^qxec3YWgJ}lKCDwb4ShP?oKS3*%Tp;>Q| z12cG|G?Vt+zf1=NS;2}Rg0Slx1$T(-u7-KX7VB~+#P zV!5Ote-yj3U|1TfL8&DRxgiuK6f6iqu6Zs9c19M>YGfIOX{U`rAhjEE3ROmH6#O1; z2h-);8$h#!B1d$ia_sV*Qb8!>r}~^CLZhOCV>*Wx=N(L(4m^7>qDn<<0EG!fbs;*f zVLd+)=2OtzQ}Qb@<+h=js!-;v#5nHAWyYB{63bxU3=#^o+9F}to~by&Ys9h}W(qo- zDMrZ54Aq{`*a2uLnT}SBQ_)cm6hwdEh-Msy;Z-3vx$8QL?r?4VCx z|LfM@yA|K6-unE5=;(0gH*fy-%^yB|%gy)PWN&`)jjulY-QAa*{rK6}o;CKyhaWq8 z<&DFG-#Gn))1N;5=F|4x$4+0f8|}U27$~K z6gxgtW|e+_)Xzu#dWXC97b^-4v?ON-DCZa1(02T8%asPvsM!>kbul8Ft=W)?YpPhr zjoZ{E#XWZr6Xfuo>?=*WCF;4w3@HZnd$GA>F;d112(DK7J_r1Lr>|V`m|;S-8;Xpz zYyrjG;w3j@029A8Py~i3mtne{4NzsFb>(B@k^&+k4%7{r5p)>DlL12ZGBzgEH99W# zLpUla8L9`a5!|RR-dv%|Py@)r8;Pk9X)GC6T#hjdZ5)BX-Ar97PRIR=qqOb&VnQez2U!4(1)eQPJ)U1yShS>!V&lDJX zh)l{HJMq2a|9M3*p*(wxc&LK2RoXG_X3J$U2pMU>vEs^^N#GXy6kF?0q2s^1qFDOg zAjlP~NF#KrLS3#~S{~THEkmw08=^=zvsm5FO@xko{52~I7gI%}8;0dFYtNinLC2TX zMkb(2Q!*H4g#~Z{0<9T$6zuq2FS`NnonI{$mqUO-dy`o`sSHJ(tUNG*Yki0@-D+Hb zpvasE(qK^qhM%IbwMI+9=@xo>x}xaVvR&)rbI8&vF`UKVkyE3m3$&y*@NwTaZBZ@+ zXuBsFr(bnRai6)rxCeq~b&KRJkC&AsClCsT%h`||_+&CqwapRI{Jwg+dr5I`p(@No z=@~k2+xJ;30+4DgYtU?ZZF7(pCu6*T6Zv*rLr>p&NdXn~DeRT{a~$J)tZL13N*~MB z$7G9`N_B2bx0@qPUN+#FeDjA^^PuylU(}2|k<(F-Qiv{XD9~~$R-LxW5<&({Gp4X+ zF^Z|vpITACJ*z$F7oCyH9C zIQ!8R1wD??BHTbIwoYn`Y#SaA3`}eRZxp<63%O#ZfDN6-tme}<;=-~^BHn9t{ow4wD+;h1v`XFt6K5SBI3P)90(C%G#6ky7 z!P%lSVFq|6LvfXibEd5*dK%7>g@$JvL&_ebS(#=W8!hUS?#wl_&E~vVE@mgAT#TK) z`H}+4IYHcLM1_7>j=Fsd&QIn&h3|(GuR@Ts90vD!!?Bmd(mA`m;-Sr81>Fqj30gut9GRjo0bDOINSFlRN@+w&C#QXb)BdV)EHc`HBiHH6A%xq%h8;|>J_ zpLi~w364@Pj@4V=zM`-eTCELYKV&vT*X9b$#|~W!b5b#^;$)l;18TsCy26c(n?JLf z2MmmIAJkowi&PJ@r?W=YX!OguYBZ|Wsjv$iWaOf!EF^t?`=73OG!1O*%q!Uehz2zMx9uMK;*auPCT6MoJdVV8j^4VYG;s zSeLdCa)dFYrHbJCm=fUZD6);iKU`5%mt7lGu`W{|q7=AWDmfT#0SB5BlxhqT_tALL zX0w^HG~XSqDae}Tjyh%!c$6GJj(epCv@zbQ6#fIi$=jP?irnI zO=x*lKEjbgt1y~@NS%tav-%!TNrlFOgjZ!fqjV>_R4&fh$T17*#2PH5Xif_mec)t$ zVt(|)D;_0$N=+wB70YmCzknBdM#I4C?QSM8^P`CZyA5L8b-h8T?*7S&qR^j3e4$hY z0@U#bu~{FMGY-eW@T13yk_^5Wcb~i=xP)xPlvqqI`A4(gUvcE@{?WTA}c7Dl$w$`iJ-daVRsZX{9-~=8>o>@Xp_=5m_NKmDNtuDnoZTuY6{xs z@U-ZkG+Inp3l9i0XpiD;PJbj?i%Y^v&AU;*c8H-_8tM~(b$}f(3^`aSw;xeyV=tjFE@nypmU1<0v zVMH`rK9jJHuu^E1?^i_&KF5+hlqxoRiE^b&r;y-S-ddEQRhn{JiRbduRF3ZT3zcXn zkEZcpW@MU!QoV2DW;DZwk|ekG8Bj(oIK$M0R*l!nVj(}0r->%gVoAR>yJ=hrvq)`? z0IrWqQDo~$KtmBN23?Bs*Ev#)4rhYQcg$q7FcAyY9$nR@M6N|Qi>k=QLk&$JR`3FuszOvPfWdCA;2^c z%wLPojMz9h4R;cqK`h-1g~v*P$WH{%qZS%@9L-VjT)#Xb1`SH+Qq@LNBm0JRP~9mi zGc_8>C3P8%)jdA~G%~qrF^}XCv4`O`wARj1Vx!ot51W3}Vjc|&iBYau!>GoXaD&yk zH`VbQanwK3XQ2)rwiqTcFBTc2jAn+ZyjTmM*0G>JgbW*zEjK75<>NBJc5x(!>ZKB1 z#e&6{KQaq&1Jnxq7CTBPn=jYHJ}x+3U##c(u0NSc3UWCUpQRgAK(FXwIj_@XmW#Bf zHE(T`vr>KxntmH+cIF5&c#KyCM zRZL2pm}ssQ67f`#BrQln$;oXj*0P}ISy7k0SV?D9))3{5#{!L*YSj@k97Ja00&Ua> z?a{cUB(nDH5b zf(1!7Li(6)yWavdd|q#eOlC8PYEt=Bg^rDgNr~4+L1ebcyd+%M$nEvm(UqEJ>K& zaDbF8R!?^rX$HdKoLLy^>0qv|S4#?R)<{7U+}ofGr0!$e6>01>agEa3oV87<4CF$i znr?JxU!GMMp(KW@gJ8y$18zW&5*9_Uy-}AhiG7vJ46+vdGSP}mOgD2gu_90djqCP` zgPxBe+naaqy0+JfYwus%eDc-{w~8CzwsFViCpI70e4+a%-9vZC{UF!hx_-{}ob^9i z|DpB9I<>y;d9UY3JYCN-*4}^NU7pS8v2$Aw(j+IYF^5$?Y@eBZ|B z4&QwEqQlx@;P9~rpFeod!Ot9g*FojLckrP7kL~}T{nzY|_WAv%?62+p<=$KNUbLs| zJ#+8jyPw>B$L{NQXS=!G+jlp1KDhHMJ1^a7?gV!pv;EoaKi>Yy?eEwwT9F02TmN_K zfvsCMFSsA&`n2m0T|e%+*Hv&`wjB95ZP8UxnxK#B0U1=o!?t1pcij|Ex8+7=M#ur8 zoDvbb=WmQNXvIvXm>DY&yZriB)-GIgobX7Ck8m4$ZL}`f-@P6@xHe1#Jh&G;xHc~? zc<@~C;N({EIl#!8+%BfVqga7!vuPqaDfqfEv{O`715FmV9xAcz2rU?)YQX&;S2d)P z(kpN&trdwC*)GGOH7Q@Pps^@BObA-OZsFmR={OkK`PYR;x~C6xJ>D9RB8|4h7%@3d zpgF!#3oFUQG@Gh0-E?IAe3bR;0Sl9B#Dy%8E!LQ!H<8qoLAWOSvsyREB)P4p z0gaM8V1^dOjS0r;vG$}KOQf+PkIH-|M9QIBjH+}oF*h#z$J?L`6lX`|sFf)U_&$#Z zy^;1rsWwfltJmx4zJLr^)Ea?W0TSW%-UG@QbpuHvKFbb@6E2zV_o^Ks60s)yND5Wz zh*B^HQgiC}k0zPjF9MA|p$rXvqRnb0#3H4#bTFxAj1Dux#sh0tD$5g1@u66i^KCsJ zXmoR}dU`_kj8QL21J;vbQ(muY|*Q_lrYMo0l^T)p2sgVQZphW28~2Mnz5z{ zZ(Xd0;@NsGB5EWnvYfTKLUoo)n}Mx20F6#DhgM5yK@==@hnn{bI+q{JjEFMAaMNG* z>9bCTw@8VS;VJ_SO{KalrC>o{jEyqsTEIfjhhF!Rg=}S*vzQ@U0?=p{ zk*XXXg}N51+HK@=Iq%RKiFu|_G}^5Or*g74KK2GI4(|E`3yo-(j!j!3AANS`%j$> zB8eVnr1Cw^{Zk7K9BcK%g{a<4PWWjxkWIIOT!Nj(jUh6dS?h{kD;p16NmG2=cWho* zL$2xd*ZmcgAQ)V$R%3^gz|0$HFd`O9BW6h%PRz6{zk&w8Y=uKHExCfcGJ`EFWk;STc zU#l`G?LPp_ zfpMfc%2dMX9^I}5$wa5z;#DzTGEFNS#n&0({+z~4*kpZov4ay)P4Ba2t<kx-(AS*b6?@^LP1W_E+1jKZi#U}dI~Ob}*;B|5sbYhy(o(Xs5v9ENIC zKRyn3XVDI`{$;B&u04G|rEX6D6DTP+t}x+dchbhf8b6sw`evHRXj9{9q!Nj96D8m6 znVgk|Ufym4jVz`HlzuT4$J=@`;IAOodL!P-C#U5U85ep%>mak~Rl3EI;3l5zNm-K= zc{J(y5Q~V|VJ#TZM%dr@(6Sgo_-0zU7`BW3-Le=?&>Yu^*6d<`YZs&P*^#W<#Xh(! zh7e?CXb2Iz*k4~OM*IB)!6rnWwTu1LvY3y+r{O_sW*7U*Wigt-QW7`G*~R{RS&Ssm zbT!Cy>|*a*7Q+dI&o!H>UF=Vn#W38TO!gZ?yV!e{#ZcVGSA(U7UF_YD8F_7HB(Rag>5_bf|cbR;5hCk%VZvJ^^5oo4n#oxS+BrBJWa1J&+W7CJpn&1(}nHB9aH zWueof)GpbCP7P9f`m)gJF=}ADfa0|hnikXaxY{(z6^lVyNKn=mi|V989iEu<)17#w zQq=pC`ufJviUYUvY!>@vxIwNRA9TsBjmB5mtzsn_IU{rcJiN4p+~ zkSMT{N4lH)IsV{t~ zKZ>-&mj~pmHM8i%N-)r88wK48Jg{Oooh8(tyFSnLTge~w=?oNU3bs$rEBTZ@O@3YW zX$Y#=*Sb&Bw#A9PPuF@!DWz27Ub=j@@t%Kh299?co%T&T|!?+QVD9 zaa6ifRrXGXOiqLjEB?&1Az-RRKMLb)I^-1JgU zzFgH>DI}l_kyc`0p~%BhF_EkX8e^ZJQ;L;JV$h0B@J_mI`mLxmHXvx189!IivtpxI=SJOFn4YC$`X#g;n3|Dv$sedE`&I@8AC3zM zD${kN!BM`rGA$QpolBW+PAZq`p@|%Im@9nl`n-o{vRBvT5MM5A51$XCr}S|A>#~PK zP_e((J=~rE(B8wpidmbyexIJF-M_JQ`Du3FXrH#S;Lj(NQ~PwWX$Fd+XeX-$dg4&a zRww=vcbP9GP$@H`*nF3;V&=xZpd{XSpUzpAks5q0k&t4UX`qQt5^OPoHbXNummTH? z-Jub(paMk8<#~S_8C+sJt#r^L4U9`fyjvuwp(GFV9$lj{Y9i0)2O*rzI$W2}UAOx5 zH~9a{{Qtv?M`tKr2>w4{mBH};FM%>3`2P#c9D@J9VBkUU{{ef{3&H=#7k)VB|33m~ zK=A(;d~68*KfoTn5d8n|0aiiq|6j0z|G!{eL-7B92$T)M|9{bu1`PinL|wq}|JkE5 zVEF%k2>gKH{{w!r7lQu}&Nscz`TzR|z$ys-|GR(&1poh?3(uYN|9=K(K=A*68fZZ9 z|HnWBg8vUla4`J;1qlv<|G!}WLGb^775D+c|Np>|1_HtV{})gO1poh!fClE6|9|1( zKm&sR{|Vp+1pl7`8W8;dPXP@G{=dG^aL)h#d7uHo|9=_KfZ+fC1kixs|AXiw1cLwn zZ=ek4{Qm`S9)aNhfAFXb82b@c$nT$`G9M|6dO@Ao%|Z(176oFUa5!{C}__^Fr|dUj?j!;QxO!(176o zKMrU>@c$Rl6%hRYUj$`9@c(az<^RV)5fJ?U#b*V<|K9;+K=A+bu>AkcMG?;V|M&dQ z;{OBk!uo}ydVt~ommKo{e|;7I|COuw|KGog{|`d45a;}V&l+&6COhW;e`%rNod5r1 zpaH@E2T>{z{QpG+DFpwY07gOZ|EEV9F#P|If-;=*|MviC2!Y`LFV?jX{QoBatGdql z|6sI4Ao%|v>I;G3|AW;X0>S@Z#AG*}^Z)-Dcn-n;Uqn7Mob&(vH=X~#@Ir9T|NjE` zav}KtX`lhY|NqNX{QtL~75^W^>>&{Re{jNqK=A(;J2(jbKZt2XAo%}Zv1XL74gcQ+ z-#Y~VpS+fXx|;w0;bpN^{C``Rz$*U#i7sfmQtfzq!`7)%<^3n7}Ij{|A=e zwu=A%7t3O+`2X);7F)&t|FdPWRs8?=E{m<=|NqId*ed@2a+ttM{=Y3uU={!07ACNY z|Gyk2u#*4(>q{f7vn{e+B>F7A3HP|8I*DSi%3d zMG36n|G#9JMt?g0|MIfX>HL3tkie<@e_N2i>HL3tkie<@e_N2i>HPnvg9Q-;{~v6e zVfg>Q4$J=sD|s0H|Ki*ng8zRPSVKVY{~xlu{w6E`|L?E4pXB-s$p7cLfBo;)?_Jyk z_~VBI&l5I2aCpz*q35lhj~@K>#)H@X-F3!jRzAQV-PB#bwfU^|4><@)0FFCPUA6lkM;b8c@g4&-AOOyv1sV{^2mb(Q zK!Cx3e1N1AWId$?MTUy8RlZY;l*LXa(;t^dMkb1@xD~cLW2LsY6*&M$+Av@+IMRk8 zI6Uac4+w~Q=hMJ*2$*jHV?d-``z}xh1nl}epaB89W`G6+?CJ#?5HR>dfd&Mq4YtN! z2vB>GPzxdfAV@a@lcLfO{D1(1b!#X<9SjG@B8ws%z_bC!2!TMT;WO0z46q6Uq5dAA0fA62 zIury#4R9zJgc=O(h;xK`YjNrcflx0p)IqST7T+#JPP?aquN?x@{>g=gs(|L{T}?NLw$1kzpw8W2ePXMhF-2d@J( zAh7ub2M2gfA`RS;-CIQjHKp!o|PECibWa$pq%ntvH+ zK%n^x{wM^Rzi2@SG#@&f-{!vqKOoTj#q11$=EG<6D*>w@(EJ6c4T0vr7L);j<^yEF z3xVc84HnH`R4xRO87ySH5cvN>0|Niw22}-t|9=hg|2=cp^_aEH+CQzWzis{b>-qIt zJ)iQt-Sc8k+4D5_=hyz}zrGdF{R-<|z&GvRyPt6X?mlMa@_X0bTlZeN*WQcnUEKZB z?w{}e!tRT9)!n;yAG!0dJMY~2v7P7c2s?M|Y;FJD_W#~~`F49dy#2VX&uzVX>kV61 zw#r-h*8b){Y`%T-2R8LhX7kpK|F`jH8$Y-4{Ef)AiuBj~!L;sytXn50RR4B>ahMYp%)hM&cX4eXVK$v=jt% zm<0{*95OusAnQ5HatKCW1cK!NR*Nv0>0(a-ftfD$Ul5q-{{syOktOdH3k~Ne`R9QK zM1HkzzV^i4hpfUkmk`Oh*y;G@5+dodvW}jf-^_+cuF7v_OI)@J-?SwzTZM1h5|^#Q zH*JZ_R^gkr#AU1S&1FQ=zY5=6h8q2=@XckYaV5UFoRQ483g3L^wM|d^R^gj}uq3t; z-?RajtMJWbsF7ZUZ!SZP^eTLF8ERaKZ!TvfqgLUYzqsTnwF=*S%aYhieA5OsuEIBM zP~$3ma~WzRSK*t>8ObcM)A7xpSax9*zWL)zVk_}Y8`QW8-?TxE6nZ+oxePTDtMJX` zjAXMs@1-@y^NwxyteDN|?x&q&P(X!MEeDj6N z(BX3a;O0f2;$mL!lJH^2Sq?+U0a+5L!hp{qcRZZ%+cKiE6_Pk zy8})bU>MDJ1FIm|sUTtyfncXT7L)7fMBPVfd&LS6`WSWuv32(lmWp`1$#{d zf}Q$PpbQ9h>Jxy5bAtE{B#8g;JpbPl)}*xy-*Nbr!+Q_Dbnu-0FYZtGzOXmm{oL-z z!t0IgPjC0PKDE`e?gi*<{L4mr{bTD*&p&$V?vJ=t*WbJ3wGUhBtAD=z$}`qgH3M_m zCCn9+7p)WwOe&p^by!Br7p3+jWE*$C{mNa?20+|s>BN9Lp$&kzw$q6L{v!h*ZVx@{ zCh$QUaK=qQLmP0$O+XzP0P)hZZUPe8fHQ6a;>ZAq>t$gsmUYq#@FN2t?w-AEf=Z1c zF(qoemNvC3L9nhhpaBu`@tsE+ z5Wy9@t|JYYkdMXbF$Db|L@RkA!ec=EHcbA%+dy48&;PdvcmiICP>HvJG9W@FehFwm z1fndmF+l{PEEt~|BCg~&L3=>NN`MTm2t@wBPl7UTYIFq{!4Zh)ikAYbZfbPJ zn?V^6`Tsr(G$5iYJ_j`5qbnd{B^I;x&5V^;RC>q1F3NBo^#G=?bHG}T_dr^q|f8fLdbso%ecE9=#m9Vb*|t^jF-u`ItMmW;)wOS<{j1_*Y+*F3;$v)K zG^^rcY+*F3;$v)KG^^rcY+*F3;$v)KG^^rcY>EB+I5iuwxuU(XmZNP}#>d#AZC1s{ zEJxd{jE{L>sj*h(|Fb3bTNNL(oY-$=e9UsR&C2+gV{EZD zE8=5b{69NB1}umm!eBruZ*BAx#oVITXWy( z`I6_Io*(qoJcP%+9p8TV*57ZvW$Qb)_^rooer)q?o6q0OTXFfH+IaiMi#N&}PhJ1q z`tPj2d{vGB_n#b?2jPPU?|;b34fq55s+AiM-*@f3d+)XD-?o0o-t(+H0rUKT|8MvG zR-S+t@78yNyN}!X!p@)W{M^n9cBGxVb{@9<$?bP+zkYk>F+54nliXi%|GE3k?(cS2 z-FLYk>iW3r_gp{hnz%Bqr@K6Bf4%+Z+i%|f?nM~FqI0gTZM|-FV8S<^e04PdyIz0r z0zYxS!hbN}VnD3dcRmcPa=!0))b|1n=ZPV9Z->^n1g!y39f(U6*8U8XA=eEvIZiXf zZYCBpo3&VnGKp>#3yIMzB2YuM>Q5G%`6)AA`%`F*_a13L>`fmD5?aD&yaQO}{KVsM z0IdOVAc#8>+%EuSI6qZ!FV1=(G(dVvhHGyd$a4IgAa_$d-Ig1f86gLVa!N$#p1(29pcONjVrHyBJa~alL1^TG9}pTj zpy3?J+XOsci0lfR3q$~-jMl;`$G3$Y8uR%7-L=BnVQ>F~d)&?g+m|+fXk*7SaedVK z;|>4ZfB5+AXFpH6_=L#DJyN&V^M-c?8pgzAg5@I~3%?fk_FQm?bQVNTPql z#EyP9q?muEgnL9P92kdMfiaio^>VB*Qes+15(`r%Hq_KiZ_I@w;}WK|nV6zgn*l!) zjwGyi&Pt*s9}1!}R+tv&&%;PP6N`?j7%}8pfk`SKv2@uQm#pwKD4zyb?-NLKGJMRn_)2@8yo$-7=eHpofAY7f7yycVl{fCdcPJ>YA zw?VM8YLK$P5LHwe@{vqSiSeQ+jH%Nb#0;uhCB#x{A({?E!|7?vKkUUuqZx;yF{4zh zsX~41C94A@R8M5ZazspWiIn38`8)Rh!^d8yLCEvlAn2JjNP9RdYQaznFAP(~_ADoG zV=*xAO|~8qZAIfsNzYfCzFq{IA|#$@#D=Z7673c$eYKIPgcAdHCieScs2&Lzd~_l>sIw@V?P)kxi-hXAM64(TqsavCxIupAo%bI;>N*W_?VjMAHOTa=8l)-= z(`+X?mPJKP7IZ@>2t@X@Nj)k?5||?7Em$iRr<3}tfA;>vM_#8vuH8VLvj&-*Rf8zSGFEC8utKzv%*6R&P9W2n z)8>$vFX83eOg*Z{3^vZ=#uy>+uvU+FTg6%^&a}HZU$fpzQAW@jvT|`Q&F3@mkmCk< z;f4DTAMsx|2ySp~v^gY{Xrnxm4d8+%&f+kw7q+cibT6@85s;u zwj&fpm<`hU>QR4Mi#EHJI1|hU!wfUSD>&gVcfx_DSNDeMLNLN6m~bMRblf2Q&+qQ; zt?jSfzqbDH?GJ3dadYDS2iGsH-MT zy07G;bU^G)xkR_0<2Y`V3)RtcS7GC`nrkx}A5C&Pt&RvC_^P!hIcNKPK8_;Av%F59Q z$wwkswKEy#P&T5TwwgCRJj{2r;i(nIqd==uyekir>*;r;OsY2KpNRAc>-Q7fT-Ur?n43mmym z@n5axXPjQmnBT_o5Odi`fXSvx{Q0S7=A0jR->3MlR`Xpes@YDaIi`r5qiR0q$K><< zzsH?e+keW&7p?z&H*YUbH5Zo6)SxMS`57dAcT)?u%$b}LQg);n(@ ziQ8|v)ff&`sZ;3<6m`(-)~!GL{P#ZRR%tYBR8&*#4h`#1&%X6Gh?_EQwO)H0#MK^0 z@(|a<-TEz70lGEAEEx!K5!%rUhErX&T)VY%$Dr5i+|f}xRdp~oW&Tf4LAPGNLgxyG z+O;Zg{;m~+t9^elSCV_(p;fz<)^l#PTEOUa=PzJK#_*P*Y7FO1VMzh2j7C$RKdnmL zTkp00s;K&~Vf_K6tbZI;gfWzcb8ntKubQKORNCrvu5-FcxPA39FRV9c)|*FWXgyid z8a-op=X_;z{H$WA15+JTjy?*MoTmsya1ueOxgib7XsGkA6i;zc$?=I>S&j3iP2 zol+&G1bXu_E>AQiNelywPmCqPm#0{6(&o5uJd~oSOFpC7qGRz?>XOivbxb|;MRNRRXys?EmG&*#p)Dw^%U3dz1Q>N zr;GFM`pUZNHi*yg`d^o$?&|yR=&lfVn9V;Qn5zd*@7@r1+?}`Xz1)eLU9)NRRjX2z zrlD)nv@3Ov2BK4g`!N4pQZl8XG^mUQn)MxaN1C=Y3Xqn>4UGaP7T3o;ecrEZozE%- zj1Z@e0)!tU{g%k7qdV8(XK#F1}KGH>Xx}2R}va{*f6t1qSO-W0po^8 z0W-ml}N~3$JH}oe8BRa;K5?Kp~Tz)o< zXIg%u*3+b9www)2eP}Et6fiR6XcV~WI%wI-(fskB1G3eHmnRx|-p);He62`YQ!?gN zzBFLZ|8VW(0Hmzucw(gz;8T31;7fI5Sog{+V0gmtZ+#flw_G;935o?-tfI{#Mm zQB`ZoXK2Xre7saUr%z|t^=8=RE;zS*?w+F#h|eWY?SSy}{=MH~O%^XG6y}q~OYU~v z1~I6?P8RdNc*@$2{_mJ9ATGx|eej2Ufvw9Oe@kb-wzF@X?cI0z%hOcJRF8Vef3D%? zi|`v8y-zFp2G?-JJQJ*C$f=_@-OlEXnCWk|vbh2l7)fe}#xjzBB>StZ5tsB#>+-mo zONwUchDYyKLC%$olv0h1#DRHPR{d-=hK9lvO-n&$$jIc7uKK5GDy^sigUd|?vNz!L zzEt*y)R@6V#a_P~EnV)(zK)h<>uo`(hpi>$_1AD$T_32{WGt&jz1t-&;=Ln|DLUEkRG ze02Ki&v)gX2Oqltal`Ej+w37)4#mD27w9YZJm}a3h#O*8*k(ug9g2N5E>KtQ`KDtR zAZ}P)VVfO2cPRGNxIkXH=fbfIA*|l5rutU4k3>RYxKKD)i#4LdW&>d>y(&Ly>(z2_ zQYHqK@e14Q$h1STuf_%9$~}k2E)3g#veYZrCk-E4pvECZ3vs@@9G9l`adehfv7{Jl z7e@j+mRHzjUqw3<`)XXkuH18Q>;lA**9zP0%U*|KUyTd+m3#J&T}UIopdPFQGj+1u z%K9@EcHBa>YHcDG^P1kJ$E|A0H_OHQ?Z^t->}aJ!v5R~;t_S<0c(}#}T1{yjYukXL z)q^T}<(|D`7nCfLud4YFX5_o&1TN-GoNkzre4>(XmW+Xzoq6lhi0z^sV})(@vowcd z7e|xCv@?j8)nc|3Dv#B+;MfJ^%00WsE^tjw%-{-77Xni{HdbN-f4`k2l6kq*>j%sI z7%mY+xhu!`Rej+`T`~^QD^hQ39*Z@=A@ApICGfDiM>R&ZjPP0fKpe8?H$fq z4IM2ouAYaY4WHVUtOI9GpRzeb3f$%{O`|8*e0AohlRRs>86? zAL$nx^{Uy;SLA6hObjE^HpV5FxvWl^`nI zsArWTh4^xOra#njJv1a0t!z+fcF>8&Vu5KYES#|mNhv8uV$r1DQnZp*O2k4iE^HjT zP@}VWT=HvibyjZU-H_6XN0?~C=eHsZnR;E7eVJiPiw!2p%o)4TQvzmhoNUBdxzkQ) zlc?i?)cXE|>qjm;YQDhsi)__w%sRbzCQ~44R;m?>kuYCt;zz4#Uqa|7SYKr<@l|bA zD>z%7p6tT9CYzx+Uu}ijJjYGCdAkeXWbV`@HfpU;J;&`}4B`UOsl`*>?jR*%$v$PY zBmF_i0v8%_DTsz~PHzePrna<*HmV1=rQ zW@ZVLZS`Amf>CfRpOx7uzG88AqJkZIdNEUO@B>~Sr>f-`Th^6845l}@j$H^UWHeH* zv}rz>VrOZ*%xC;zvqmsMHC64G(Fh-wqLeTT#8xmTZ`g$yA7X}OzN)v&iP5N*Z9sHr zu>XJO+J(CgUwZJz2fO>Rz31)z*3MUU?%JMgy>;``o0m7V^`G+mz2^z;wCg2n?>xWT z|10-A^|&!=Yy@kdaHjh(@@LTKPgpF{Z($wNks) zB>3?}YzsQm7`IP%fpT_GaYF23ES49$Gqz@?Gr@YMA7&y>U7&ndl22Z^fO~K2NS#c2 zQtr{5PKKl5x{y%<#Z)R4E(+7J;!Tq^uiDI^f-J9CX;aG!B+S3UQ@hwj*}8Zpk!Zxt zgj5){gQ2E;iVNKImVw&o4b@xbaMmpi)%w>&zVq_sTCaB*Jh>8l(!!l5J>@p9^GlXj z*y(JtpzLCf-8sdNsx-KA&uzz_72^C>?G;Wq+eTA%v5SMUI96kkP@_GW3}@3?ICx5% zoI2l8#FgL^kJ};9=W!Dmu>#kf2&sHORBRUW?U}{t?iY&rVmYIlox#YfH;2p$7n~ht zPyI>t>+;Z%KnQT>) zaXv?NGyPmM7M&T%$V6+k5=~N&Y7t*g%m%2DjEeM%B`oFaEZ~IL#dfd=!)SKc=Vie+OLzm>lv1joD?TLL zPFFp>)0_~ys0dvl8BNB9b*Z3?%rp@^Y4-p=9HG56z+T@d5a zsFLlQWY?E7sopr1i_HY37HMLYf_Fd;{V9Z*&0O+Yc%b5Hm+QQ$dfq<3wIn3w!88%x-cv)>|(;V&3t~gY}?L+LSCE)46vHWel2byF2p*kqrZ+ZbD|}1r#%osA$%?mKUtybl;&mu? z(U7N6qbLgu+e|W69$v2G*ahpD`%%X(&@nYl1{u*DOh;N9_qEtWt(IfcY9>>i%|@kR zsoSXvT;0d4>dwxlHry^|b!RqZG)HWTPnoTR7-=~^+QY4>?UBbW#LEe@KuSSvY9y@; zg(I?4&7#q!WDZ(<46ROMy-1N@MX?~S8dU9b){0iJi(L$c1G!{FkrOp*fgj3;a&^b8 zU`=h0ICg+9_>TaWj~MJYbbp-g=^YiaR#109Ted2UsAb~d%)cCm|- zagRx*^RW)gX!)Ylo;co*;^>v9KK#gqhs?2B2paK<>UFk@J3+;{KG`|#l4uMd9uVB&hnLG<9E`ybwav!}iP?bc0xkKX&oy zY+7jh`IK1Fqw!S2-=pNjl*5L6y6TUrdeZ9;4{9Ak&6W0k6=(zzvux$TR62u1Cy=N_ zClrF1N~rqQaJ6s2S=TM$ER!MFETpkd`Y4JS1p*QJxgTD@B-H1e9+%jGh&RK|?w z3ZqV95-VkbA$jk`M;b^vLDo}RP-LhWTje{oNLlP;GW~IRWMrbaifch^#!78(E3#Jy zW#o+3G{EN#u9`?-hQuao$gJzjR57~TMS@AO)#|lKvz01yd(Q-A2nN@x)!5-AF!Kf) zjEKe3h*?sG6EiJKIZTKKD!ma?4zweC4+mvn!Cpvb`++(&E|9&hgfS$}Y3ignDKls% z(I+OObX`o*dTIAlKtr8Mv0_Q73P~>;8;dAEsk5;mk*9~zP<%oT4Mt03sv@V$yCJJ3 z-*L4yzn=fUs4FyzDpQT>R*Ke{v6{}YZCr>eYClpRx1T58m}1j*{tft16;pTA(8!^BMk(#R+Qa-C?U{HGf*3hvQatP z3KCJX7fmt4N>ifa)22AhSI7HXKtpZESg}S06A2%YL5dZ=A{y9$^iv8o^wyI`pq1?D zs4?;PT(=zEj{|YhU=0-E{1(UUmjeyw_q1=r-_zazWn>0M-Rd;0X_z!bPSawE3F>cE zk!oHqwY_vH-;C2WB|T&i&*GMtc~)V)G#&|Js@SkHrBh^z4~{93tmZ2{IxpkB>Xh;8 zQ{3?UCMbg$PB6d9CIS&14WT|duscI{kBU<66-d;pY9wMHgS`K{%VH414HCDjRf>l}yberr;i zq{%ejX<~+0B_f-zU1(GXXxrZ!hXQ;vSyIP@*a{FSJzj_m@+nSdE!MRp%(Cc6*rk95 zQ8y;ptVy+oL9^0QQL;a7ihOI>Z-*oC5fMOJyj-u%BD4DT-vA9{s&z7>M8aR04W$BI zthUpUoSdPfw6viizUD77i_{^7xWGc;E6REbN$&~Sd}xb~Wb2CIq5 zYA)urGBIW&Qff-(B!cRuhuu-o@QVpeZCE$WSg=`X^QVBusG*kC3WWaZwLy3h!uqm^MMq0S1WcuD2+4X-@nngvZvCnCWh$`*0M zCr+h`u=!=6VUz-O#u)QeKdUKdo5Rzhf6{0%VJ%ol^aBA+&gxuvrcj*wp+KY0v}-!n z_KBfnl&veZT%nN+AXrc0TS`Ol_KgPJVv@1e0NHv9(CB4Ce0&g}s^g5p_-hq^pilY5 zQLkP!15sS&)uJ!j?nr#ua4pKvDou-GAJ65dsT|$w7b?+E9!=xH%*ZqcrF!4Q&1i-V zB}r}#+;Qk_N}*(&%C>8AhpFj4TwoLRnvbsbE2D;j=W=5@NaOt;F$wJK0jnB;Qbw)} zhrwX7HmZ-PYG@YCdJV=uh#MU|J>@%sK8%OzVqj|%XsGG<3@;Abd~_7SqKeU~A-R-J z$x0;_8hfb(+rj!lF4^z4x4b|@5iRmZit^VvQi~2}g3Nc!WV0|43)LQ7)uu$QMK_DB zvCp^hk3d5v>zz@bDZ~oVie-{dixABkE^GcsC@AY*D(($RAwK1;j@Q8boL*_#pM@G~ zn@&yq2`=mN_s8|vcruMvOl?L-kg?LKrWHQXAl6<8%8)QG;vKb=ffOjP^-9bdeVd54 zFsn$7K#fuxRG}Fdgfq=h$?XOjrJ;r<5FcZZsd|K)O=sLJ9h$a^jaIqamT6y-B}YND zS*#lCi!zEMNg=f6z$kfRjfsH~)F`f(q>YGmJ-wz{37GE<}0Hcyw) zSltr^8ktShfP0PsHlTNVw7vvFsd;o+>ZnrsgB=>qyCXT z3w7{tsM?HqvB(%@G&4-)#aaLz_)Go}GHgUPF9Qv}>rZBqf?UqTXXyqN&?|aa&g(Rp z2xSW@ZIl1xU3sxKjTeX|;3NJJ?R9Tb9jEN&!n6DE+O%?Y#pYM{YrNIR6BANZM4 zGCm16xP(F#Lm0vg)Ks1x=&f*COir7ayfs~Dq{NAd=2{^UPZdcr>Z29wo((M4vQXDq zQJ1|~NoQ5o5aspzfktdnN=~P~)CemSGqXl3kz}cOEf?;jYq?HLAsY$4K@Cxi^X)Vj z8miR}$Z!yujSI9!@iU<|9Svq<_zaQP7yTm`h+*=?3}bq@FdPwz zt}1B3Wb@__9Wy>7P@oEpMo1siZFe1%VU3Ulf4d*ZjLN))1jOroq-^-S)9PRl7){Il zgckC887}Ot?;r~euQx;{vl&D+seGzJ$411Y#A~A*{JzCm% z>Ao;UBW<6Y;U+j6h_~{cuy0_>?Kl|^hp=8xETgq#fUEexvdc@AT1=BxkO4oGs!3Wx ztXpNW-Bdi;ZLpbo-y5;Upe`2i=1N;*U=$fGnlXu}>BE7ysCI>3MrAAQSumVWG{VJv zE8P(365p|Q46gSrG)RK3lk#ku9VF99%wG%_$28u}`BQ_S%zLB7I-l+HOnh43`7xkD zSde6rFumabDc57Z?l95}gu^+rFx1n*TwSl06x^(lf+o0r4``rGwWzh|dO_+CjY1=x z4w2om<-A<+^7T01>agEa3oHe~y26CZMO*cBUFV8BB zP!hw{K``UG2r3szVo56+RA9SH8faX%R~+q<=RC>BQt$HM1>+`aT4=w2Y^OHn?&LrD|S@Ov4u>Pn2s>oNL`vJvt%GH1Y`t_ zM)?+}#>U&ou8XoR`g`x1`}6MKb-%(bxi7oE?E1qEe&ez0A6tLh`t#TG>$iHI=J}N8 zH(cMp_RpS|ti9KDm*?58j%(dIF?i#J=Uu2=xbwop4nKML2iA$fy@!Rv%ZJ+sA3FHe zgI64A2a$uv?|**(Pxjxqf8YMI_lf<3y^rku#@-L^_4nAlC+>b}_kFuRzx#sSZ`<|n zK6vM!cYbH*wL9icYUimtuI&$O|Kj$Gx7F=uZ9ih`Q(J$q^`l$Q*~)Kut&@Yl-F(}| zcY40d^Q|7*^G)u5a{spb)$XC2b3fVj71#S+KfkuN;ug}~{S|kL?t)nw#n}-#YGn!o z-hvl{-bj0*RGTK&)$8?iUqA-d{d-}o6(A9A=Q~}`U)xwas%(gxQ`g=CEV%Xr=V%?h zd%I=#g=;p$K<>nPdv|x^JJ%j}jok9DNA^HmAHG=y8qTlwUMCkC(JmdEwn9RdpTrsp zJw>fCtDLEJgqm8Y*Y%p&Pp0`kqL zJ7)Ou)oihZdeLNG4VjoA3cbDGJt_mmX5L(@Zv^G6LKhV4CKYx(97d8HM}$(HT34KP zs6gAFU~|1aaN#HF{GQz1Anfh7j{`B0BAVBt9BdsfjGahbvp^laDJPr`@IVd$G53& zg0%+HDdx~>2`z{MBU5VLFX&u;Ff$^`2*XW(*{9Ds89tn^N`~vHR?=Nj+7OkE;<6Y*;TAa}J+_O9b}=kqQTR{1t*|79 zArw*%D3hlBZHXl@)KB^YwNA&hi?MbwUvrXc*g>HUW?9Tf($@YoU}xBD#Fxcrl2Th` zUbhz-TNa~8k`)4`9ZcJZE{P#N>sC%HHXv_*+q0I%Xc8;Mx&Da?d8S7OD~Hd1eqBcf}LfzL0&6H`~3vLCPbdKS1++F<|FWFc+fi07qDe9n!r*LH_6%G zhAfMb1e&e}nT}oTj%6{NK=@pJ z9gT5OJ7IU@DNABLKTgd?Y_4d3+mn~Yd^p)j2Bs(8_M~Mo8YjwnR5|fk-nJx06F8pg zbq8^Kp$}UY!*FbDuv5`4_TXhP6i0^^RC}V!JZM=A!I5gmG*8&}O-o{w-#WjZsVDow zg=H}x<|~(3@kA>eE{o9^ooNZIoq@G+up~xWsW#YAB3!n6y1p#t!w4nXX!q=5o@Fr_ z!$ZMD;6%@IFN;waHVx%@&i*!+T?`F}Eoz8eY;9SL#1K|d(rKI6`hP5m5j5%>Pqd`H z>DNEMEJmTUMN$phn|}Rsb}=e#%5i(oTK~6YF%l(<#(=kDzw4h}79&uit??auuV4Sn zvKWrysYESk|B%){y(ET{C_2t|vv$tZ`p4{INQ7yI>|gKtKRwCAxo-m@NT}7XtUO0@ zsbH_&e_WQreO!=_Dt4)lE=yr_BqDGp4ExBk6iP{*X7)s#{lje@&b6=$!BK0`V4<$| zapCDUA-oLFXq{o{ujyoS#@3 zKm+2$idbkkKe5t*Mon%PQ{hppz_qRMIXWr$x-qm_#;t^)h}rIx&^;YrsH5_>&F*mqJmI_yNIV_&%Tk z!DCp^6(M*GF92mY=P|hc@4@88zAFeG!!5un2p+?C01XHp!;dU9obwnCfrfJ)gKL4C zJLfTMfQ7Rcg2#{pRypS}Z2#>-!#R)P63~F)F+AE@n%&S6F6Nw-@Dso)2v)+~K*KpJ zVS6#Q`<$~9-g5I<2@84*1S{cJfa4GxgE!oK{6BnT6a@ai^?sn?9RJ_?{ayFLT4n7$ z8(SaRdehdu8&6;V!n(Zvd+Xo7@#&4f-1wf&J@=1oeDlU{y8G_C-H+b7WZ~~0+I(R1 zl@@cK-Mr295!Y|HF1q4t?^*kT>y@tFg}=P;mh}VAM?Amb`9V+L!+M_R{*wEB7hZTl zzVHl-Bk+mC-#`43!|7r6@Y3P>!QUMG^1=5WG!6m>kKX^x{vYlCgmtDM?pvhiy$|pG z+TQo=b@rlrPuTs!?t6BBdiVa_Z{8($4|hJg^P4-b+8OL5c5d7L^7i|;-?aU|wx#Vm zw;$sEF88;%Nz0YB<#WRSkG=1JcciTPP9|+quYyQt3yYLXcGEKfRyMuYNhSp$B$M8g zNnaLR1XNH!no=yZWnBao0mT=jh_Li7y$gs1@TIfB@=e;^o7_9j1XsTI{l1Mqe_r?b zpEBn;PnqYOd5(hUSnT4l6DoyDy7(d6CG_8q1BnjCem)K(I%fODIE?6|@Un3j(MjQ@ zwDAK9Zb{s}otr~d=h5zkOO4DRc9Q_~9YG4i%K&HrQxNYzlQ)D$DeTr!kKsW;m>Iop+ zEd(h=-0KLkow%nFBo}c{9W>1UAaRi8|NZepbAn^~bH`Bx$MolnqX>@a&mKn+9MhjQ zK12P_IHt$PgP6jY7(fOtMLq3>Gx*YMinJHb%L5rxq`h1|f3kp78VK^`QTt?&gf$i? zhEx!w#_Vx#U?R4`S~bp14XJ!c+CCWqVU2~8A(a|ZrgB;lq#2C| zry-&=Bj(@7o>W4RW;7n0hH%)TX-3Syj&mbOGh)6m4kJi2V*X_uMv!L2e0>~7kY>bu zZ4$O=fDJ8KYn@Ep5skFVxx7A?`DkuO%FrDKl73*C-273)CR zXr-z!+Qo@lz=-BLwIJ$F%92hAo?GKD7+bvxBrCc|F<8XxX=}lhuIg)W3y#XPF^RK+ zDYXtuNgR)dVWTvW(_qd%eu^7T7BA50PX}z3$NbGA*Pxp!D=7E5)Tz*BuY`IY{o zh`pl!N+rCK{PaD#ax%I??zthPWH` z$egZ{HjICX@2IFtVS6@cU*+t$ix#sBYGjHEMV2;ZQf~Sszuh`#ml38 zIqU9td9>at-1BZKh56*t9&*vbe50|{+hCE5C8`q@&gqM;?J~SdE>6DakkiX6DT&=* zDeGEs4St7dtkBh|tYxL7u?mxf^T8%6ZNT`4=>FChU)!y#+San$A5$R^;Xee7>T>SybDRpfA!z3@Lj;mM!`0 zfu;@CQ50|)EGcBo9plZyQ(0XmN-D*N= zl<6~YGbQoN2Bn>5wUbI&3x&L=iC+lgs;zQM-9djYhs_%(OEFj0YL#Fc8K*zze`-KnggN{ucdudV_vAosKIr{SPv6 zk76>x-jCw!7<=k*8tWV1qCrM7O3B1g^c5M1s{554&A3BMz-0lRHU0C-+D)xs!JSkIDTe zncPY0Ik~IJmB1W6?G z<*tVM)Lbs1TgnN&gS(#uQj@7YNj_2=Qjt+ksy(7ytsFcsvC5OT({6uY_AU-cK_+;T zd?fftc~3AMq!Zd|a@yD~7yv|voUvwQp8=OnXDmJkEGP%FodF0cXqPU9q$J8qCogs% zDGy1=D2XNzXcZYH(G-Bs=^XsOkc@Ep*K637ZmT(UrK_#+6Ih`#{OwuDHv>!<) z%VPwT>U~L`gUFZF`;ZV4IizH7u6^`WXr@+AV)foZomj3B6X+<}YjFyxgor}AC)c@{ z44GU`NV3P`BvKVI$?gOsG9{suuM?3d2uXG$A|X^v^0mbcrXi#d@5FUo{4UqXCLemc2-#N_r%^31)I6FQ0-6QS zJTIE@k_)=eTLu&;EIW;8%$g2koC|JBp*326Ss)985I{K(Q0X)T$?kDT$b)$dZ7 zM5@70n`Kccp0**Gob|U`M8aMC=@wdk^mGz%af*fgJ4eA;oPv0;N!W{1EbON_8LY)A z7TSD{f=NKJz~Uo{Sq4`-nrH|I!W_fQ+gKvX3fNMc5$l*M|IhzyaEMdRK-oP%f#jJN(*R#5;H7q*w4(9inHs)6tk1&4B zIGV8^@Dgx2kOK~(zfHe}UZ*Q)ALB|)^IN?hB-#3_C$}Eky_d6_A6m~LSq>M_k8HMj zJ)2DQLp`|#*{(DLUVS{-BNNY%;2s;Hb!2K!3ZHx8 z2+#wS@>D2QD`=K_O;7Rh4;3HZjUA&+a z;da4z7dOe&o*WR{saZ3JK@Bps37xu5MmZ^X^uVhzGLzg}XbeGFGD@PCV<*nv9Zg=%mOfi98rel2H=5Ae11ZB=jMajFQlY;v|#` zLLZ8eQ4;ykp~rIdqb(&G!=k-C=xQ0^vqj0|5PIA(WR!#+cQhF#p~roTjFQmfjv}KZ z^0?&@?y8YJ3Hv!r#+}gqAu>uLnX7{&Q|f|xpWCO@-nxg72wp%wn%-6i$OKQi=D1)# znczwCIl(ZQ;7MQIE*RWP4f)6fPm<3G_L2#nboK3md#3h~37#aM6YM4vJULRf3uf=~ zO~^$ic#?chu#-&i>v|7Nj@joP9}JAoX!`lg=}PkC&}joTge1ZE-d4MEo6cx z$>#){$plZ%9b z=qti^g?|$MR(OGM9S&bb|0+E!jNv!@A0*@p-V!`2_>JIv{0n|mU=fG~yYN5cKgYkF zzkz=mzr^?Q75qK$+x}nV{Q-~je@=gtemT7jyudq;m*pM7o8i8~y@C5Z?or&8_`Uy+ zgI9oE&IrO%)FoZb0)?# zG52Kr1HW_NEJl(6G5El>_-z0o+P*9~9RQX|=XY5DdsaYrm+&sSlh(U}W04;r9aDIp z@IJy;hVX7-udbb7#F7xn65cPopLQtA+fz@gv&2(@MBbve zs;B4T!1Q= z!=-cQoQ|wh*jyTS&gsa&Ebw!1ZXzH9Gr?`(oQINu8Te&QpHY240Q?mEbmXCAoWW1P zPxfI+dpoK5Cn5cP9z%GA@Cu?v0FQ26x;IPO+s`bZ6kZ~{1aAWuZM#@_@sR2K_spS0 zo{%?eoc}I3Y~%{L!^ZjVYyC!{;B~?4d*Kg4r!QO3D0t@`q9*>k?-Dif-g}RziTnQh zL`~oa9}qQhKKziViT%+>L`|%ZKPGBoZrw`M#Q0>?tqZIH;8n<98TQ6; z4!H0gMR=$1PQ0<_fr~W$QTWHv+r|91!l9epA-sdI6%gJkymcgX0j=;B;VmPn3mbng z{QZdU{5Q&h{qW1&enZ&G5Z)lXfv^=2UMswIq|*Y&;J)j+s9Gs2lg8s%!43UA~K`8;Nc2Gefiv#)3TkZ~;g zDE3M=lL0c6^u2%+1zzA8;3~RNbf@SiqCCAOWC)&Me{ZHkdy{q>=WvdQ^%VPM=GRzP zaxzRU_j$a^-*GdJnI)ok02r^v_c8qe(Kkh36~2m}8JsFSS||~$nAw1z3+VY@=bpsf z%)giaQ+|d1bIu<)=YkE;!uu=l_q;QCC{M-X&O8S0!+D$WUt9#wE%+3C2)qQZN4Sjn z9^)^}yO|f_3eWwx&kvUsojqd1$|hGP>+l=GF0?RN=!YpviBeR-3{g^)JOqD3$ERbR z0WZ{=Nuc(WtJT)~_3(nC7$`d8YmVEDMVk+?nF|qbPSq-{+CX;U^tEo)3Hi2f{&$@ra8O=vh z8E?9VcCyZlQLCLSF9b4PV2ob6b!1=;<9Wthmym(kjOQ573A|M8%t|CiI+NKMz$;cB zJTe9um~jvTufj%E3m_Jw4ravTLzT5;M;mP3pLu_#3TE+M;k`04M#W#XCW~0m6tPzQ zXxcVerHcXH4c<)^EC3$?9~pr?u8zH0Y@ro%+Zs2UB9kRh7@{*pXHo^TMQ4l7W*tvE zWp42PU;AUEGMVN0;KSglUWyN1`8%yq^p# zI$d=7!T7lzm6`6?V}~k`dNas-jrST=Fo*Xl@70mMWb?QP6^*7_QZ1Ogmw7K!wHhEg zLv+RnOr`}}lo4g9f?1-pC_Ms`^#@CImguYzm~7Suff6t}Lei0eIY1F8jyRLKJ^*e3 zw~YFP3=G}{-Zf%OrUehE0HfnHQkA(t87Pl9lWGAz06st!%mMEQ=axV+&TQ~L@II=X zS>V0ky;Q+W@E-6U+Q`miwgb|@oF9>5feertF(zAy8BZ{t7^z(DbPGXO-sdTy6+=#E znB4BL@CeNnRKZMe1Gs@Im;qi6UQQJZfR}-njh>H8XYf++(h*}a!}0OEu1AN%q#G$A z>qXX!BhF;IE&=O#*4*ZZObr&02l6A%WMC$qNt>ezW&l|rI|7sG%x68vdX6fX%X*6S z6xEFsJDD8uC3R26Q;esmf&s>pj3-B6vb`UdAz?_Uf;MK-t*+_*FTKk*teIx$+1H?TAQKU{7}a~5{S{|lKiRzIxwef`e(KN{O^ zjh*rTdB)fo|DR`!o$>$q#@HGE-#%yTjQ`J{L3hUg=UZcE{C{Wse>iXUW4=4%{|w>I z`2S%$#9#@HGEFO=ucp*!RMqf@<| z@&EaLu`~WZZ_?Nq|DR`!o$>#9#@HGEpFe5rjQ?*x;@lbkAM=cz@&9?V#(y~e-@E@$ zOgn)#vvI~F`bhK(kwy4V;ZKBX1aAn=#Bcq3iT^#mg!dHh+q{Fg4|ChxrFb%N1>A#k z8z;m03j0RZcM#z%}_FwDSPz`4K?^gq*2rAui~;jI7F4_OZ~lY2T= zB;}E2EmA7WT^fz69tuaY=DJB{sajK^9G;qPP&z{i+M%dgq-_AN$)EcSF{5`izZ{dczhnqz&ne8Y#)6l~g%p zF$J@-a8Tcto8y>I+!gE5P+V&qF#hU=r2{H2iNLkUy3a4)x@4cgB~PDx;+E=#*Dks4 zqXTy@(2@1*K4Vol=?%t{4lJC@NmTAeH<^W{)l@^PN~ukdTH8uREM2k8Wt2MyjQ{%G zGsC|p19&I#ucCJ{@`u5e_(#-%^&WHA?sOv#!-32Ep}H7orpmu z&el{JU)LbZYjdh}SZxkivo<-ZiTM-yj?_C~{CU^)(ckp;H#AnO@@#>#41Z;*t{ z#&`=!x?HfUm`t=~Sxa823sPz_D-`bJ>Jc-L2N-mU$ZPnG`Zmdtn0 zJ~OoG-k-qbBcax|)}_dLU|{1|RAx^a>L#bc*3jqjbv(aaXQ|mcxsufu*2Uw+xVTzv z#G=uWjj#FO;FsR~+JoF14|??m^-^Z{-g`B7AMov?N~vv|uDsy$cainT7JM-b>wkJFrI~G49F9Pzis%@0T(^|hUeR_-F(>o)`L#Zd`t8@hUa?A&)W+lX?$8o%uTfby-?R4(@%qVc z-kCn9m{KhHu=>&+u>P&<+w5m&<;d}Uz6ndr<%w&<4UJN%DtaJ~%&d1+ofTJ7t|_-0 zkUXg=$c>I#8`BQ-tv7$Tdh;GPz0mm^`stCe6IGWUtbFscOZUF{`L~nL>HdD#ZOFPl z-=HPdi2ABcWeI|ts&dN>o4bKT%9xc!5phjf*SICtwm+;7NQZU^HLw0`uWMdEVV7Gs zNi@r@^FH~<)5?F^dQ0)WH^Dnq031e+>+{tlB#mrU0jHa#BvelpwJlqsVKj6LCNqXc zs|s1m8fdAzHM?u*!|y-8^3TM931si~yQp;D^r?nbrN zG(NIjji|v~^!km-O5HN#dlB!#i`e(9%l_(vAK!IM>)peTylpvnu>Dq?mvcc z>nim6J#%;Ot=RK?#c`+qap|M4$6g243o>^j?LJ?tp_-l?Gij94LZ8_=%Y}3=<5<;rym%ib3+oUvELAz3>xr+m+L#cLnj zxdy@dd?6EJkxEmIu-@p38N1QCSQVFw)lFF=Q4eQJ_7s*Y1(G#QxG-Ea{uwYo8GP&5 zWx|Z(^WVO8;gfH@{mfsA@q_O@{_ekc|MpqjkJS2nvt2!uOqZ?JK-vM-t6jX3tQP5n z3@*6pwfWK#7n;lzI}tW?Upe;X%-dh_8p~hbeC^YB+J5Uoh+BNsE)0@k*ryqXEeOT#&XI)ooFS-LM_4!KBG}d&qJl1;2Yc@BtDRWWP zRFvW}ebD5S1XT58T;m93{dlRz|3+XW#t;q}b=%Hlb0w5=)fOvRRR~sI--WI^k~zyBVd)T8B`%)#x>}Ev|re*l&Lf z9yxpAzCXQ}{R(|6aQ@oH5y*r48JEAf&vWa)GV|33JRGFZ=W9lt3PV(4t^`5`x4aD} z^hTrH)OI$-=9Vq$$^}c7R0Rt}#rfg9v*#Vq%xCP&jdvV=oUgu&`P{c0x4yjjz<1F| z^~DW$9=X+m8GzP@+!#ASD$^wjH!7`f?u0gzZa`~9Q;+p(8FrAKmozIB-~p>vyj*`^vYDUfWf z<8453FIqKq&>~z&=9B?UtE{E0p5gra+t|L$m%XQM`*in{!MHmH2bheTYW9H>svp|?uDfKeAVWpN2#+0o3&1( zke5R_n?03+ZJwmp8}D`-;d;1Kj0c=)$U2;N{*gZ8)m^@R%gtAx=lt{sXZ?KLa(9Qh z_d|#OH?kZ4K-zSe0&H zGNNsVq-rRvcA9hsNjsd!XPsg!Xb7XFj0vAD2X;7eiO=NIXm&rJvD^D^-*vdeqkrt{ zD|R_)L-yFGYHj1`4Fv7;wTcVUa8ea7WSo&;RMm=fA_*ges>KaO4GZZ?I;F=MuuI|& z`*0lZ|JO$BtqafFb#j+P4!aoX7Q)XH^tGFP^H-T8LaFzmNI_rC7x8=nS>wiUPDarAGFyu`Qd z+xCa9KF!rVjJM{m(v629u|8iU9?vxO1wSI!$Be3uxMh|F#2HyQo{x5Fh^wHChP;_h zpe3^p=ik6@UcUKYI9R!0*I(UoTk)K&`~IZle&;GU`Iq+}{_X*ir+$eX+vjVE*qewl zrYm$XM@<@TD2s8Q)Rc`iYdM!$8t<6P#*o4m3?k@oecSh}Yl7#k|M4vU8f4|`sYf0U zU2w~d5A2P+v)B6FUpn`^z->sh&sPzv*!AL6KCOm3Sk;JVtuc83E@$;F1)S;_bZW27 z6J&pqxp{?Sa0Em4OK5gkfZy2p}e)&u;|UPdOQ@7**z90WR`kdm5f4SXe!$+Hx{=f zT7_WFHuST1bla|YKV4_<3;zf^1Z>Z>1NX3#R{NeB?TfTU;Ir;nxUwR)d^ozHa zOZ8-_SNt2VW}Ym@@!GDLY^BF!s-nlT9A+4-^~DYSz^5IKyW1tjTIt=+st2vu#M!Rkd_WAfdaRXsRw?|dy4`39RE>80 z;KLcL>oKg@L6qt+9B(*6)<#^W$`+7IO|hZv zGgz>4VHn;WJzc*1!sPLCqSITnEXi7OX%<^r&m@-?lYNDjqQfN)SFDJow`)CY?>qUZ z7-hIEqkRt+nO@6-izfjucX9C9GXlbJum1ET%H}TQU6jx6#Qe zE4bg|TJ@YhR&UgjJ#SyTd^~Nd|4+$;OsT{R!p5TRgtpwXMt)U9lJaGYadXR_2^-uk zjZbA)JKRP!BDc?4Tanr-*e)^I+li1*S#LC8Qy|t->KzC&o2VCR5R6#!E=bd?Cz}S3 zHHW&&Erpg^?eX5D_l6<;=>9O=jlNECvr|$Vnp>ok7ht*^CWHg`bkLunNe9W)mcOX7H0mK!rJIV?oRBUF&4vv=ufJyRLUy@H-n6$$sgSx^HOgUmzUb7M zGo_d`=+eOzwL7QNN_1LEOJVQP@iEwY|6fVFm^O3e3^GF({Yn%R0m7?P#pQs%29E|o&UKt)I9&Gi>?oVZx`A~pi_g4)c?^@o*vtqs z0N_f%PydX*f$pY#gfstle*X1*Cg4rdey}VNC4=vGEO#g<;~kwW1XQj^tfnN&7xs3sfoa8rx~{Ofsrwx>~GD|0t6i{I3#)LZgO zGLv!S^NOHXY>@iuRTmn7x^}VF=T3y9QmubIx6k%e3Tzu5B~)~xRh0s6mK0iD(bz2* z%XYuj>KEIqg=D#)R>JiNrZP(Wc$!4d2;W~83&zoi3(>_2wroLf#1fNt`%RNd;m4CC zdThT(f$bED6n;EKqQ~~T6xdFYM&ZX3BzkO5p}=;EBnm&C9?@faG6l9%q)_I zla}S%enZ0=!|>z3P(ACzvZ)1rOFebc!4axYc9YroeQH1PQp=W7=Bg$^_GHf4FP(_*&Yw&)bA2J8FvT2)NN>+N8jA ziqr_W-ecOJz;ued2pH=zty5q+MN$M@>oLVBFr6YJ0f4sfE!G)aEmzG8|z z2N>-!O;BJuMUn#??=eLwFr6a90gm;U#wjqJBE11Vw#PI^f$0=MNI2SKdMpK|Q-oRI zV|q-ZYBg4a(T?-%xPmxdualxOJln9>ZjhwC2B*|rB;p^* z#e&{+!cu5OV4RQHlncv{F52qJu;M!QWGI{QqN~v3mGGk~f6f>2OI-o4Us7`x6LNna zYm^T4kw!u;Ngr#mNh5t-ZADa8D8-8=Ng`w`Sd9)zw;QcO?kE~=NFwQK((HDc(+RW@ zvUdt9IGHKQkb1QiHR<$esZ706g8xTKd-Fsad(}xt_slu5WdFykXH`3kTcf zxZEXi=h6yXe$V=CSFw+mBtyEswZcJH4!1_P-;+60-_r4-rV*9}(jHjXg}WJh08tdt zKwS&xb^351gAIBe>0={uq=3r3J*)&RA%%WhG183%6OC}lCq;{abgh{*G<;Q!+K}|= zi+QEQP>E&T-BGp-M!Jz&4AF^0 zC7U!^v}*i%rFL3dDqR@tXa%*YA`(zW@l-UiDdEuhFwCiT7ZovyGTN=W8}6{QGvAiq zpR?tcHp&ZRBx`j#aca74q!%aC7X*n8yRasUV~y~YpoU+BPfRC^Ui$7 zTA4qQK0N2J^Q&$BN0+5V9Twd|4y&{Hw-PXveq!NWc7J|GwxvHMY8I6H? zw*29oE$5er`|@p>-chzxCFHK4Qx?ghC11xWFZkl-!qf{q8p}0;Rc{c9*dhv-LY0)D zVRxrp@G2A;m%dZWVNv{xzDmR!D9^Lyk4{9{N1uc)p)a1cCY8*uhxH#caW_2J6OHdk z%_cliMNxO=v~9QFi0?FQDW^Z(c6GHaNB{pywD2Rre#Fj4YyVDebbfOP(yF^GE(!`1Wab z;TCGqe>o9it51^Rri~?v+0vKvR^?u7GhjC5M>%W+PxrxJQiG@zRYnX6Z_<*N z1^Y9|#Ji}aTKDG_p^RGW4##z>hFF)4OWLl0q>5@h3M;JgE5wmn#En6=c}@w;^5_rK zLbT^;Gq26uGjr9ALJj(r}LiRU5Vcs5a1oc{ha$a_m|w` zxiI%2yb$c8c>S2;Kri?W&L^CQIhS*~95-hL`(yTl>`U2gwv)Y_^)X(v;tJOBtN>n{ zm(F~Gc_s7P%mDKc#^;R38OJkV#zDYmz@xwwz;S>VuQT`w{s2V3y_xdGD`?&)yz>yI z7jGlK%YKRIt5l);iM~P=x>U3aRp>I&u2iARMPH-Q=m3#9{{Q&*fw6RCbv5vMkRhtj zq$o%gnh+sWp{OV@gpw6GVMrMA0hHWd_ZRI(mFp^oj4E^`LrN7YW=N<)4`r;P3Y}%F zqzXNRA*KpFm~kjo=s}FxgP3?lQ1Ua~WP&0vfl>sbaf-lrj3O|0EJfh4(SdmYXdP%$ghykWN!&-f}c@^Rs}z$3atn(q6#ewE~E-A2`(5ygQ+-@)kZ3@ znl)E6#1Q|46XJ!C=~SUKI*lTf_Briys?ZNL6y^nKdS4$Pj z!Gp>dz^iwWG75WKRy%$r8 zzYl8W`CPw>fba_8e+`=EpMedEO}s8-4Vvc1%LYvs3T`KAx&<7c7+85obody@An-g-fYwTy@GujYS;AL5JofZ#IV9j1-5mgAw{NxuNx20kWu zh+d`}=z9WNfQ#^|t0rJ?<`T{UoKHEAa5l{NnfJ~7N#tXG!2J_p z?w|^_)9qBDHo9#HB`Z!0KFH-!g+9RL4xwblFY$`N&rsFXOZQTRdgvaiP&eI873!k9 zs6w4|=MYM2*L%3EA(X8AE?#}NKoyz=@>HQkpiC8-2Z}=|X~#ar7`Zkn^hw6EL$0Kw zz(@BDp`>=bm&>NA>obfOs6wA+JU@Wy&1l|ZNx)&$T1HJxpLTMK4*n8+jVkn4;2T2) z$cm}r<)~j8LP=+rr&yF9>v_g2L$0K~^&I16>d=v{q+Fk6yh!!2o@0%4CAGjatdRnw zv-a7dlc_530P8WT(EC}BQZ?2Kj6V;cdaXI;>-d_ra>n4(VYQ%TVl5CoDY|FKm2?!` z&jqPM@8fc)D)5ZxeyY%?MfXvKJ|(($2(`OXb-P_3bL){n2hR$d%sHPCh_(%&q@~kY zkK(n$`)Bav&<9zMQ&m7H8opBMNW0)+u3)GDDfA&OpQ^5pv!20sC&-RJYad#Gbb7lQ z{2Nv1Rp4KTk43t~m4FKWY0B}qAy?8#^Kx)BXOSvU1u&|*R)88+Xc?#up`<>rh0CA{ zy^9MBp`;er2);@cdIdO~h;*b}H-N8*HqvZ6#tDb|kxuFlvHnCA`Y`JWs!x3>I9h;6 zd+2U1bI6qx`UqDzgpw{mXNcBQg`O_@_7F-MeaeU;Lnx_TpJ$EwofP^aYq+)1k&dSf zkQ*vM3QYr9s?Y*Z8bHanVON5Gp$feQ{5$T-^WXXYm1hsRlCD*J(PvblJkh6ApyJd=p$a`ybmC9}(xqY(_&N`_WRdntFaCe;%r!IT%yedsnbFT2 zIKvmcEqYA!ThWE06GW)UESeR~2;UVxDZEMebK!S{X`x-XO87Ow$AT9GcL+8L&JZ*O zM+*#sS-~#+kMXJjTk!Y(v-xfQ(R>~MK)!(Y-*`U2O}t<58ax|s3HLqjJ=~vhF|LKX zFZd3)1-uBXf+lcp&ObSKaxUPMIR?(2?6=r=uz$iXvUTj;S^r?&&N`2kXRTrF#{3)e zR^~a(Ec2VpT^VmMe$P0Ik!Bph_$u%^a1(GQkOU3`X6Ucde@8!^j?y8zkT%?tlE<%J z5eZ#aLFLto$x-%GRLV-PDT#x zQV$<>sE3c*)Wb(D>fxg%_3%-Hdibb57d|4jD>mBYklVF3+T~~|1f_->B64U|Fx(K4 zLo0&H&<98rC<}&18RT3`g5e6H-5Hc3vjFXXKv0T4^Z~M%kdP*%QG^P%35IWxS~9MJ zPX)u3nH;)Ru$8I;9|=AhLMa5LhG(i;QYbij=O>49M(_OOQ1<9NjU37v9chw7nWMcl zIg~MaF(QWoqZcD`D4jdNLWuj$Np-V+esX~{CmQaQ6 zC)#fix>_tOcLhznl2GuLezAz@Zv$!{Os&O1dS);4VvbEW;AHx3b})(MWTa( z*NK{55xhdw^s?Y(qNbMwFYUpU;u&ZQWPS1pLBrOq1Pvd5OwjPrM+6NYen`;p!3P8l z@4rvb@ZNg_4e!27(D2SX1EVY!XuXiA=>ow8L`~-j&Le6%S8y&-(>a23hEdk}AMOqu z;eO%$gl+c;?;~uxTX;8N+g-xD2;1%y-bvW@$GIr-ViD#$hI9+tZWZ22*mjHX7Q(jQ z|BJ}F~gNny3jCphQh^L7b>5CWtN4B-KDl;f!!*VUt`f zQz?afA)m*@@c94j0Dmk05&lO034Ej%yQlwzUS!#TJ?U@J?*RVE`UQVhI2JSZQW8970bpn@Qf8hq; zPCT#RPxxuVMxLKf6LbU*iM9b30A;3y{So^C!DXx^%=Z8T<08)fj4JyQj!Sf4FQZ`Q zU`_{I3wpRd?t$Q^yn}oB2@DfsZ{|JBpRqn*Vyyf6hd_h1L`UJkEkhNW7Nw~|Q=$}A zXi}6MLP^uT4`Cca6?!n^V5-oS)KjR%)KjProlBu6J(a!!yke*;>5*fZdhEM2I&vhB zBRw~l)}GzNO*tx z&xIYyPpHopojrUk(um4gqO+)4;7P`lLn!Hhxf{HD2qhgbmx7lLp`;U0m3n~iF7Pg@ zy50lcLlycU_}~yqYS%MGXHtcpAv%L9^mNhbLnx`gKE)V?^htZ`VesK0S5oLh;6pGbv>Yh)}^=mV@#@P%}O|Ig&@Jpw*L^|2lw zseUOIqxz-TvBy&N(x<2=^gJ;a|0i{l{9M8$>9l?sc-c@_Qm%!$j7?I9{zBQMq*LWH zbNRKT(5D$sQ|*FhSfi|6(pmL>aI_O7h294aQ^m-0s_q5v9qLLtOcJ8R5K6kR{Ydm9 zsvVmFMz49KK9B}RuX&`<;#__$sj(gaN9zh{$390r75CY>R9w=z`gztc6PA3jc#-vD z@BaTKv>0vX1U$cf9sb_ERvv}k^ zjYr&*c%&W0lLun|V#|K~`lFc1q<6&>H~g-c;^JQN%XN=mAL%PF1=AH%T=Gjrf$-D{ zOmQJF6$L_l1*Uka6+2jgU|)eLE)QNY#Z9~9SL`0Y9+_H!DQ@MZqCjA31*W*bn2G}a zz5-KRwY;Od0G?Wb9oz-JsTJ74UEu92Fa^^c-36Yh71+UD;O;9h#n@i4gB5V~6`0~4 z>lIU6`b>V?^YQDQQ!6mV1<_O#a7?Yh6nAM;QNZ3;V2WG6cXSunrdD7FcY$?k1$J;3 zSo#V~!E{G=fq7~Lc5oM%`U*@jwpZ+61&n@c!L+_9Y{Zuv6TBhb1#;Oy@q=S_eb0)Z8KNQdJg;qya)U- zm;~j(sX&PR1b#!{&o~8!Z~OVe?hGlgVl3C9mr%h!NXke8E5zBD-eFPs@+q`vO;sRy zz-CXUAa%SNZDbO17;dR`YI(J+Xlr?g4=8NPL`fY(l}1GzvZE!ao04ELMObU9Av(Xz zg4yA=RST<|Z7cWQ0Y%bd>B>X(yvx)u%1j9?my*EliV96c&H9#DWw#Y&Rh`pOE#*0H z4JfMCe5xYLLqWa6WJ@PfFp_a-C9-nNl}ls-;wqL==d2EMt|sC9WkAs_=>n9~r4O5LJQ2eq6(4rBz)9VD;8@0bo(Ocu9YQ&yq&j#E~5dEy#Vvf>bH47F5I zk%Ys--3Amflg{8MK+cldQBS2yQ6%FpG|MpT#qURMT41D6w71P+uTGB&-t1FI;8xUX z$d=l9nW5lnXCH|sEE3of-JYP2}p34f{FbcSFB%DHlCicJHp8d{GjTKC$MT_kJ? zCtd!G7;QMSNFnI48+AIVEhNXFU?&z0a4sHDgbLweCl-T>p>WWvfKw??sMO5qGwE!l z)4)uq(o&Kbok&n><*e^htg^4_%qG37;z^4unRf+(K4n`f!HV5R(NZW`di@d(IgS3- zsxWu?Kn9FzE9sU&UXY|+mQ*a93qoR-soXBtHGX?6Rkr1mzOq`O6ib9dPqY>!dA(1T z*XEirhox=EIPD2lNH3FEYe~0XU902_o}ii6cQ<*jCl7bCIPYN!Y6UvO;l~L8DGAG2_Frcu;gK7yT_9``5ztPqV z76a}=HXcP{CQB4cce`=1T(~uB1%{(?POF6wOFMsrhN@_sl_|FPa#pHv9{0G zRORZrW;BTqf8aod zG@A?+a_Te^D=IQb#^cc|OA&w3fJVixbfauX)n>F(uc;A~&l<>3#cR^MCfCBRpUyhM z$(ACR(<8Nm;aAs9dP`eb$E*drHy(6W zQ2wa{tpb=cmrB{{ZEaVq%~*pnvq#blC+h8{2aUn9fJqxIH>?qD-Y94cwUU~o3A}Vm zDH_VPHAxR_%gY0nMADv!82m|@IN*>e(>ig(>lL;J6fl^2 zrxKpJRl*ofKF*NaQwg^kfuK8-ab(*nrQ4-zse;P5KU!A%QF%Guv1%$EImWnoAj4X3 z%QQ)wU2CjJ^-@T!*Elk^q|8#*+AYbj*ITq$JyLnOnL^ph0fng>O+jI8)b1`qR&h3> zNo(~+E3UB>64Ja=?g?tznVi*7&>{A!R$D-Q6{n$_P&t8p2NYGS(W+6#3OMI>*kTD+ znts$4cFIbwhOOI0rD{*WRFhi0Nhd=%peTFwE=$eQNw&NRt0ooIXsaqsuu)Ejnp%?_ zbtOZPDPN4aA{c$?kU|p+G!)q!)Ks*STDLUrs>XxCq+jMJOI4w4*&d0O)NYBcg|hD$ zP!ytBG@r1*{*D_~m#awBr1wiba-Fx)Y}Uf*YE#}cd2M=!(n)(_K#|gQWl4J>Zb?Og zh`)h$>{&_9rqtkD(t%?^#t-R6-n*)ans!SIfRV!jfOi_Fv3KfisY)Xz6G)BD+t%y|? zu`Z@}xyA7oog-l+=_|ysPPMDgB=c%_TVr?nAWgNEHn-XxQ?yy=s*RFXq8RriT=tCO z_<@L+Oe_&Ma*0|@i`ddpGd^)-C1Ru-jb*ecRWhT(Z?mi!yjgu17I^Y(ZJYI=NCYl;|c+TG&xR zrRFq4HK6dq@sMBQ$Los7&=gWDT)5;A~2!%Cr=~vH^v=sKG2! zO{3D)7v%YM}abI4qCLbRo0G z*ws2~Qmg|7iwV8YA4wY^r>@0#a6sWIX&hch0F8JpT~9`(twbvZ)L&66eNJP(lM-9n zreFd}1Z+lnZa`sm2ir|4oYM#8?vPSxg478Z&CAR=dBoKXRZy`kT(Ko>SP*4?e?VbQ z;!iB8*Noj3qQ=s-e4~X_%bjvuk%~$*R()P=()rBtt^{Rn9Z*1Gv0mx$c_qoF%I}V< z%?(96>?$;LlC~{rjih9iNF(Jp;Kgu(hX-z@t_C_%V;(}i&bHj9#aE}I0jf38yhUj- z=w;<{Fd|Jkw9Y8BdLTk+3N_6!MGD{Ch(pOzpzKZBqWP}Q;J0TCA%D)3399jB4&Ri} zUmQ>gOJ-BL?3V61)LcNtsO(vn(IYisS- z_u9i^3}!LA6HFi`0h18I9uoF7kQcUJAUK4Vki3LA3wgYd$C8(jU$v_59jPTv7kqbG}u5PIXnCQ>TU=O?Tg7)aHtIeXVU}jTTF|8*4WLrf{*5P3Ea?K^v_` zrRmu}@^rEH+Y>GTm--Wg5CE!mbSDBpw4PE#PKI2+qnyXu{qFBxw#-e4=V-)#1)38%f$ z4G-h#DBY@hYxP8{u6JrH%9je~|HF#2vgHOiQNOwLouvnsdP~-&Co29*@w;F@;J%&m zj(%rl`TOxKo&#b9z6A3BpWSqClABu_4{dyIwmER`t|C%b^V!ZKUw=axC7utYd5WF*0xmNSA9x#FF5n>Qe6qM{e5fo zy{j)->F`h=B#T>1RUTUWX({*`N2_D<_HVL3k^St#jB4p?7yf+gkN?6wFtP~su${*@OjSz{@ z8yUe;zFrPFEWuJozuJa13b{-&UyphH)w0!|aMhe%qL7Kij0UUC)pkd#ks|oI^ankY zb?r%FjX=H8YBU_>UdLLhWSqP`7ts)5U#Vcpd)$MNKkcjq(!E|ey8K#LgQ?hE@d9mf zyY-r&I+6CJ<2>2Y8t6RT-Ya;mc_M*Mg zPubl8GT|d~zMxr~GZ*Z1x<z{d*(9&$I+P(?b@pAJ;@YQSjW_`k_PV--B$p=HVuUWVYK~knY1Vdmcit8oCYw=v z#Nl;xgKIa#8nI4YL#CW*k6Y7CyEI$_Oi*2MpSnSYI^DLpKsOydOKoW5oXgzROMb;J zq1CC?8bVL0Y0}uUSESjIm7WPQ!k{&P+Yh;}9snrqi8fq z$!?q^(`jj@obolKWG|Q`qqSOB)Yz5Y*sQ!^f=sQ`l1YcR$x6BDq+~?NTkTCXfu1Cz zwdxj_lvSnNw3B37je&Ckzo3*%GfAe==!ex*!XhQ3Cdnv`&g)8A0iNSyQqk<+eLXrlxc@yVquyl3h7L zrZ-UZu;U6Rq-3Wj$@G+#^B4zH+^(1;(@~mK!4 zR)vkO# z5RhS*l3OKe%eid*Sl|bl1?0=8`cfw11kUqQGj% zXUj{e{&A9u)PZXsy)7x#KTJ{)bcSO?Q=RemyGvfV_*qj^1g)unjYO&KT_Yi)ijJh) zED?D1B$1ZPgDVz|5~8alL`1gg;!+Z#-AN*iy5Q$NwFviqU%sdMuiLCA`Tmjc~VBWh&g5l>u`@rL93ep@Ki3RW5ei^i33k=cm8 zoNgsIUJhAJ9f{uvz=CN9r++kKB=#fwo`ETv3B-*RR~1~C+bC(wOuOQC=a_mYle8Jq z{=6$u<<(Ccfl_xX!G5dUs)vlOLciS_aIQ$WQmoX>9d+K5A|sietJp6_t+k-yf5IA_ zn$e?i3`4C%-|Go9>sc;pwSwI!tDndh)%h6T%CG~2q00kOO2MHfXVj$pakZ{) zDD{Fwy6f>fhpwn*tpsZ{HTgF0>sN<`q`I1qxpXEE6XZE(hwQafGdMC=2G=Wsyo3GK)PKYo@`Djn^1+j*Is0qeRygOnG zf{l{>Vmxbhgot5a;se2VSuP^bxv z5!yJl{?GE8*FV1g3fUj7C)Te9CkQ^fcF)=^YtK>ro9audH>vU}QYDl9Z1t=1*R0;R zT3*$!uE;;L^39d|R~jofD1NZA30D0ND4($Wz2)~V-?r>pzHI65Cfmf zj{E<5$Oyx|8G$k|99JP&1H*9zH^ z9@3DoBznN~!UliKl=JP|5=CFA>2o>;#r5yN8W`@)`(ce>+-6BR{d}Fu>ZwrM2}Yel z++#{{oqb)qq8l&;L(t5|(lvhlC9npDLo_+6p$@pUMSs;4chYuO*2fjBsW=x5HhZZK zAGA>n1$)Dvn!_GdK{F25sm?ODE2Wz46X;)Lf(92M34P-qCEg#Ac^if7M(E|aSt)5x)|{^ z?}9Zj+_XQ2H89+?7FYwrP5Uue1H(;w0Bc~lX+=~vhn*?aQ+f@p+fVBIbxXOEh%|HH zj;)$4SM!?url=`ejIoK_@>NiVNo(>3QUy&;=b&mHeV6C@Y%WFF1F?$D*EN%LjIMY5 z$-Jc~yBpTPaO}#&8t9JQPrw=&j$IS1ff0xD1Xu&Z6%6VY*d;?3R; zYhbvvpAKtaL=1lad<|qL_$I7@;RL^2tby(X|17M5;ROE{tbq~7C_36P+~##C10#a- z2e1Z)OM3;@!0=Fu?;}-+7VKqnPt9a3eS$I)^`hO?8t4tlkhxK=|=PrVF=DXh8O%4M1|o6e}Py7-3u;G`Y^oUER^B(RlGV|D<4a9LvsjtDcX@t%E`5R zO^T+?(L_2DS0{Q7vq`fIgLd{YqL0Nf5hF_UR)`A2(+*d)`zE8E%=N*hOT!XtcQpA1 zlP6+bSESn}dQLrWjM1@D$=vAaDvAP>f#I9~8m@2tc~AsKu1OD|ta zEL|NBceRq-m1>Z;YBuYP;=eXB2D&8|Lo^+_u~Soz}0+gAE3 z!IfvPEGfUM{IK%X$|A`7|J0=_h)Q^-;wOr)E8eYmk>VzWMzJOTzWh@lQ(;r?lKs2f zHflSLGx-^y-qYtT1RysIxA7Cf5^HRqVjkbep8q2DfT#`VApx((b@0P6OP$fjbY%x+ z2pJm2d|y*?L_?ZTqTp=ryFJx>%TmZ0Le-kd>G7@%ASjF)FlrD-1`Kk>2|IWXM1>J{ zARhd{2s`)`lz|aMP=z%x0>azl?j=1j&$Tb{+3U3wm=!ss*JmrIeDBEA{XEyc#AiRxwJ-76 z&vWfdeD?EP`x2l1JlDR&XFt!iFY(#WbL~(1><#l=`x2l1JlDR&XFt!iFY(!HD9T^2 zwWQY?DkVPqd9Hnl&wie3f6`~Ko9Eh>`0VGo_9Z_1d9Hnl&)z_4T^+YY>Ht+reD?EP z`x2l1JlFn|&wjRRU*fZ;$(gQwsn1?V=zIODNBXj+efG0m`x2i$P0V)fOMLe8T>BEA z{XEzHl+T{%g=(QG5Bd+r$6noB*S^GQKgYE%aoW#u?Ms~Yb6oopr@clHO~c)?P-51l)djWC)E2bkWY|^mTk_Ao3g1{XSAK{S_f&0wubC!H(^e8 zn4S{GY2*0jzY8ZY7@l_V=+Ts?biSv(2xVY++P@8JV0hX;FV;Z!wEq;=;1XdXnP7@@hISroh?G}^a()CuJ zX|c5;ASBep*)yjzopveBbeUS42hVOgy^E0~L*R>(CSpX7k%Hp%%|Jk-#dIeCV&t+J zF=!7O3~1VE?z&dJ&RxebJfA5t0v6%_VW*v5+{vR(WNM)M1ZFp8YW3PasU{4BT1}=2 zuyj+a_o?xqEj?J42$gamP6u=^sCDKv#}|*_j3LR^^8uwUds@`IYY)#-(t>-e;Qk`#Ke;x%aOY=;t%e2y?mkop6 z)s)ttCJ0cyl;KN^pao}6^Q8=_r3Fqwke?z!8bj)G4xD_bG!u3z?uj?+CcmNHZ?Y^A zXYAH|INa$bPYVcnwph!wJ1vos1c<c z@dudvLEoRvvDvy&3j%yip(x=QR0}p;j2vnrA!n%*Yk2qCda_*`^a#x!F)ZjBDR0s3 zsq4c!V=ZOuojxnz>C^;7j9?i+*^A7Krk_2#^fE?v@Y{1*A@;b5*4%1mJNA$gJ zf1})FJ5@eVAXBYSqUZn)#YUrUDmS$jG$Rz64vg^5Miv<1O?UJ1T@2ki86z|pej#lT z=VL8=A&x#iqi}unzkB)$ zI3C+gdirW(*I;2u0dq8FFL8}xJsdX-iC!X-(G9xIR3aKCtX_}C7){iI2ZNxOWvf=VmL;;T zLbzKuWEo2k>ANR19hlr*g^XZwuSv^s=6xq*a{5^&hmrP7&{>>3ZL_D7xbsno2{r0H zFrTV|8FckgGNKNc)4^FA>ZDYd`>RKjBcW9ZO4wSL9nF|W%+C)r!kXz39B61gDKvlPw|y_*wdy?m zgQ2Am@mCVzR6mjhNfVqwkFi^FhhoE4hO(yu)r9T%1C3`uGVOlMV{;loU{BA)akb(> zlQ*`^kyxc`$u{avO{zrKR;f zThClX8fb*3gMmg^|6eA%O}3NR{=oL~R%Y`vo0o&!|KC`@a_tV)_f$8mzG>x$D<%Lh9$Ide@=CR(N@w8LlgcVufK!ZiWmZaYCUX;a!Uj5PvYl`yI`zP7qZ+uWHDU1QA!zBTMsF7)7N<-PbKffWJVcSECp-#-W9_!X_A zF5K%15MOWvQMs$Jrn{zo$(J(F0b1M2_Q0{3oF!{W6B@m)mof%h)k>jScN%m?5#sdN zwHU2Z!1>^T@b8~}4;Af|9RcmZ5!z*(n=3@L7p71AVwB4QzCV5H=-)4+qP(gtprnpa zF5%pnA)*}rRxHN3Bq02gHw*uM^AAyRUd{_RCk!0p5{e7zIWllxDPHtPl0z%V@M7$T zf?es!(UF0FuMb9}v97IU?vo&{y{*>9y;_i0DOd^ySl@owmAue{(7^fJz{V$XQKyUbIK92N zHz_#*Ix_GteNTY+<>$?TIDYaI8hAs1IAP!z7k!8jXBhZGv?Bxm!mkTxzxY?EXs@gb zXeSIDBM*m&cFe#Rq8u6c=UyS8{QSJ{&QzaXRTEH77&ykY5hBV717C=9WZ<7ZBjEhZ z2T*(Sa!$ZGVc_$moD%vYq0qul= zW85_%q8&5vg(yb`{(*-ClplO4D$1)$0?G*k$GDC`L^)yL3vrGN{C(hN?^k``{jWvE zd3jO5Ibq-!mnK|L&yj&IL^v|=_x_`R@OM3^2%k}q8aT#H4I)6%z!w4(kiF;H(Z6p& z#dnGkdT_$PF`SGSdJr18$pTUcmhw4w!Po1XBj&I)FUH8gAIJ$1ANax?h*D?R;a*;V zIAP!z-WCz!3!XnBE17C=9WZ-Z6rhxP9pGU+gtpBf+y<4{P+MVmR-?mL}eQoR3&Hvb} zZ7M+~zk~HZUVr}DH`h4T&sA?xZLYp!^?55_St%?3T)DA)pR%=VSo*CcqvAV?ib5qH z$R7vTJaRwg_7#wUeM`2RcbBWyZr5QmCTc`3!yBUgc$^z(tUMQvhCRt9RjKIW33A`5 zz9Jv3MLjl;ypr47Y%Knw-MXiL@trEhWIb^*UO9pHLqDnRIyy;zHE zyGjK%X=ySYD%stSMUlmfzs|$Wvp~`{H$8cm1W8gH5!;~A?okblbVxbDMw$v>T*8)^pxmlh%O5?;4|tvkt4) zx;x@@M)uv!DpRlOwBdG&r>*GS3x`+k3=r`GAcCP%-Bzkrr+fy3uboJySbN(>I-1Tx z0;B{1>7l&|8yPoKR6h#tlgj2&zL+Ufb+J{mmCv^%;OCA$%n*nZ6vmorcU%ce(NZkw zVl^t=DAWrMTbw16adV%tXZg_naNnFuWQ|s2aYw^PaR9^!3PUHEU>uz7=ac2Kn)Y{w z3>zkzg^rOf=s-eLW3P~M4EIT&mGUBsnN*m*@QQ%Kq^X8~-`}tdQWkQbZPBIB&?4L$ zKo^rbd(_FgsGK1a$(sj65Lwbi6-F=V^O3^z1T>)CSzS2l3}(Y553+I-thHT=hQUP) z8eKSKP3IbcikFV^-9Wn|w1FCG}~)qp%NBT_cN`RG60Vih#nz9hpu#)a+_&;5G-k-t&j;!)U%>Wyy4G zzol&({Ay=Ek~BvN^!~f3!suoxOjAGuy4R3bci5&TTZc$+xK!l($$;f_;=xPn=x;+<+nwwlC61P_p#IQ&?T#Y1^HWreM zh0cdc{NjDt`3h49;sk}M8JpRf5xDx|9B*h8?1@@o=pUwy&LXf*Ungy`bHRuega|p% z#f>RU4TupGCeo>cw5TBexK7_q>Vl~ZU(EVUT*~Jd`1@u{Sj#y$W8Lb_c#Fubnp7B0 zctt>AFm~aQC0$fuq#N&}5vD4j0qw@vaz{oZDoh245EKSuM|$yA>3oGL195`FU|fcb zEN)C;SRh7F7>p~Ak;P0ZOi6e}Kw&U;b&(}qRAHoUhLOS)1vH@D7+bi=Xhel601<-1 zVC--%zMOZy!Z1LbpfDI)l*r=76ebVE2nvI-$A~OuQekq!D*_6Gu}g?7>7oiF-C`Xn zOjbYx+KsU#h>S*5m<$jhC=AAq8JH$5*v6Z$Ft-A6g2G^Ic_E7%QAFm|1gC0$fuq}!k)g}GTk1KN$T#e|GTRG1e65rV>C?7%FJ`&rG- zRlX$Kc}Q;A`TEXBciz5p*G@zJXYx)9vqVe`))D+xKqYv0d5@Z0omAZL79^xb@)Hr$MfOd$#(D z>lLRJ>+*k*FD;#0dgsztmwvqb%&oxIwOg{y2RGlhdDrHxoBGYifgFOL-ne(8vEcw& z1b@2z_4NnV?*KUj)$6LY@2!0lWDzW_-MDs{>W3hY;M-MwRX}yEO1AnS$R2PPxII9> z`nZ*cRzAIQFUTY4SUIKqsq*U}kKi53gi@_s1GgJ~Z21k#9BAzH@0z8c;tHe5ku5~? z6^GeX;k|q&+LlaVw|*=f7`lR(%GtT~I^j;jD<)EQ5(d3?^C$Bvymnr}J+NScm2kb` z*67gH6~uJGtLN2t)x3gNj?j*80gTeOZrwfS`77o;fBBr}qjObP5aZSZ1&q#a%_?{s zggU+}32Xe?yn?sRE4Xi7!CPR#^viq0yn@%mg6Ver#=L@e%qtirj-A!=Uq@8EpCHox z6p`*{h;;vkNcaCh?hBA&@Ub6$55OL-8@pVs=dF~IrXZg+^U~`L)*nQq`!*uopCZzI z3z6=dh;)Af>B{Anub1G!ooKC&qYrMgkqowqAtK!Xk*<$O*F&W1BGPpb>Dq{NJR)5S zk**2plD1}x>-Wr6Tdkjy(BHcrXbl*p$`x`{MzlL;P59evJLzArnZn|-^*?>f^RH7a%r27v;L_$DIk&)erbKR4(3**VW|oAdn4==sDtPNPw4H-9|+ zTv+&R`sO_M&Ux;c^V~i4oYIlBcJu$PD{hn}6K5iud?(%!P>Zhu&s~%9@p-Ra6I}fg%+W6s`L;geg zmsa{~_pUXTA6owO>SZfGT>a?Ur&n)Wy=S$wd}`^ZOJ84+DIeVWy7GM+*KYlEL$?0l z`uo=J+ET52Y5i8YO!nYbVoSZQpFQUW%?iveIo{LJIBN>{2JR-;%QftMXVz{@NiS^K zt{~EtAzft{@`J11jRm)Jm~3zX>18Vl7Sa{0HmAkZE|n@hrdcSlQYX0bYl!^nh;%hX zx!+h)%dSP~QOM7pby+!ADCV+PQ?_6>CA~^$X&=(%Dyc##SOmcUthZz}8l`uBE&VB^ zbEjN<-R3K?o`@ys%T=Yhc($LrcJ)$FzIcBuJ>q9vxDNXc(6>HHdrz7MvHz}vbd5;U z$#8t!mU1>DxuQ!N2CDijBHd>Y={^nV7QQ#Qjx{Z$gX>t+LVY=aa_fU(HSVi~0<~Pj zQZ`wnQE{rbAkzIRq+4ij-$JB=>)ple{VwFUuwVWh(k**hdIxixf2a(Q=Nax(V3~n(k{&Zb~ z*3)@3vK$&&7Pe!do>n20W?HcLO5b^AYKu2k92-pC8QY9*uQ{ zI8H8nAL2NFeP>Glxmu0^DK4y0T7eG}(@3+cXt z$Pabi^i{}j;rHz;kZz%!mxcKM>ty%Kc7A&&wjMg5lD<4`(udIVC{kMV>0UudTFRPZ`zm!^9 zQoKhImH!+>5CmjD1I!=RFLJoCtOzo)yRYcFf=SNb_nSODn|qk7_@r0!?(Sy>&7rx( zRCNBn!R-y{4b5KB-s=x+>27tXv#NV}E*r>$yWTaC!}Sr{S6or46s^`+)HSFTJCUZb z9+zHgy_?X6wHBi@!jfzu6RLW31^d3u-LMcfPq#x=92PR?pwhZ*IoOLFu8r8f9D%L5 z!xlX*O*rZ8TXPZbuv5&sqSZ>p5z;vFzC7!xYHFO?72dZNBZsOH+q)3hdiKj%wZ=>p zJmz{eXSU{s=}xfUrfsw#Lf6_sr^R5SD4o0Ou|^J8M{Hk)z}C!Ks&vAV^#v`pda;)^ z>CA*TKvyezjXw}g(J8W9$Qp-9pH&?>Tp6*w6M=2lQ_gxEzA#-2Hp8slHsn3lezjb7 zv)X8;VAa>-AtT+%5pi8Ga;O}!eJKK4UxLl(%HDQ?uBj{9^suiP#4KqGp8^-asvTBS zDo~5IDQdu`0ovse+m|dpU9p=8v1%gV^)hZ>DCG|1ZIOPz(rA{{28&k1#mXIU{cfEb zvO|YHa=0{N`(gyP7-w1{hl&x~7a_34IKvV-l#kdRBCy3cuo5|xjo99Sz!u}=N%;0< zd62msgjOv$9fEV#Bz#U_dJcgp#)*>fS%K+U1g040N5Z!WOm9PAig8pVd`4h;27xKY ziI4D5U^-kJ$G=+}Izd8}0poUrZ1%oY$F^MILCe6sfQbuvMfTTVkVJP?=; z5SVJwk648J0@FSMQ#z9--HfGe^J-ija~a(7lq;GXE%%^YbGnkIY?jOJH=CVII_(Yj z1g1R%rn#mj#k+c8ZE48q4Q6|!1p-0(OtZXSb=EEFdR1EuIuhLomv@G{0@E%6Q;c&n z;f}zxgTNHybWFG{Fl{3+%{wx_K-QS-7_?fN3+Mx+-Rx_68$N~t=}bF%iZN6vtEbiH zjbUD3$|EqvIQ9~52~1lEOfgQcgqs4>=HfF6IOkQu4S{I`fhoqRlyF^OT1Q}taRwz^ z6PVTzm|`3^33CEd4uL7gsgZD1U|L;#769j*NVp;}tspSPI1Lgm3rx!hOfk-Wgjs

VMbueATY%^a}mx9O!Ej# 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

}5DBgemc=5KPRz4?Q4 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%0D6Dxo0D*3)|1$ zzG3UDTfeyV60q*See*k;pI-TmmDjGMR(vZf%b#EVx#brwi_5nxeS7J5m)^8gSbE0N z7RUqm{>6J2^~EPU{*UA1j<+~Uj%PZy7QVFb-i3<`^1>76zdipu^KY0>&wJ(<=Kg5z z9dot0XK!E|4_p7z`Y){Cz0R#aZtd^aKD74IHDm3z)qh+4?bX+kzwz#k-rN@Qm&nh7uDXdj$m?f>2qzrMk(V40ylxf&9CVTU z4hUX5ivW&U$PXP5yk-^wJVik6Js^1XECP5WM=l)@ylNH!92pUCDQEt|Jo3uv1QX@A zkc$WSSIodq2=&Fgp#3@IhiBm7!S_W6@R!ek!-M1t58yAG0f*&z_W}I=8E{ydFF1g| zbOs!j!|)GtjWbrbg)63^Xk7-ZqN>);sc=1A-r&MF6WFdG!IoTW1l#zKOi*fZ#2& z2qrZB$^(KQnME+6=~o;Oym=PEgr>_fqi6?r9S~c>Rm15;zd z345u|p8%5|Bib_qi& z<$4AkEO5FNOM{Xz5*={Grd`4aHF$d9%_Jg49E;|%Seaz`zT)-iKD3V-xt;t@zavVX ze$SH`ih2Kvm~%7443CxqZ_*q?8L=}|F2+2)s}&%z4qv2P3RwkJ`f_g5&Bqhdf(9w~ z+`WFz=Q7J`Dx~a3NRK5}sbtOX_i4>?EzwKky&x9rCj&z$jW>vw6sJwZGN-3JBcK+AGz2IJ9 zt0NUthK^EY9Y;Y~v}t8-vse_D^L0VCf;8?&>oQ#`k_o*Xh_U@hIN5XhI)SR#pd@e) z@t%}*Ma(nST@xkCm6#R^#TZ3DL-qg6Q=2y@(NHfoU}JG7+q9HcMj=AcsY&^SZp9nR zH&MAEq%*OM8PF)B$#<+O+pe2TL7>f2N5r^PyOSP@*}Nj=#EhT;&zo7ris%?6SZAm< zws~svW=pp+JQ-^0E^?sjrH)pz(&n@Y9lM_(aaD`O>aB!PWk699pXA)}HOs<&zT_&` z`ewCK6;(E=8$&V2u84Woz5u+7cz84V?5qOM`fc(l`TyQ}J^$a-&A9L9FYS|Ucu{yW z@R~W;Ri`x3di-gn=b_E6Tq!zJ#Z)twFyPNVzX@x(D-JxoGSlg}87kxxOX2;Z zubmGtY|aze8JF^3*=MHnK4e0^>U~-5)FwXHrXu=xFYi6+7(B~c%$j-vg1M2nMAUNmqEpiHbG@i-~NuVv4alus)&9o|Y?peUivuoNg=hDsXdQE<-`DHGXn&rK#X zc(FMsw^@ z3)d?D-!tsfJWRa7zh>II>iOYWe{X+y*5SJ|#bfgSeQ@EvOIuGrMj>Z+!k#>}3H!Bq z!+&@x;-m`sz&Ca;ZQWt7kcm?PXU9X%`dNU(^Ajgl$e&`Dwt~kf!$_| zPg{69E#EN;dC2C-SFeyWerDnDoX3e3a)0E~miHKiJY+lkSFeyWe!4;Y?un2S zE2R1DOIw~}6at?lK0~vtGky-@@a)Km74qEgT-tIUqmZ+^4ms;5CJs-ToLC{xdFAl^ zzl!`Zvi(Qf@7nHcGuuD7^^aR0-Fo#_V#~9&wE4NspV@rDCck;}#GeX`bolcixF1CW0!JzObof35_o1l2nb9z5I{dkl za9=HgC2G^*cR_Fuih3(nD;$~*e+~q9qo}7AV${KO_yq{=LQ!|QH7K;E!z~DoqNoeg zFe)}3ZbEP;z_;QCPEUu&j>6F(iaN7aMx61e#su6MtmkT4XL{>81P`FjfMN8iGt{I( za6jtwCo#U&p5D3&!F{OHmnm8_Hyy4(a4+ig3Xx2uJRL4Wa1ZMAP$ZX4Plrno+>JWj z7}KL?XjO#ZF4XC2O4ckj2@o7;2@Pj5@(juFlW=D{O$;K_`ySnK2xG2|I)g#29Ld8; zelB}9NThg(MgswFFhC?3V)}zFf%JGs(C(l=D(5Y6YEPCx3cDj{SI{TuMsNCKE`bzw zN6=`{+scImWqR8ng&lj+Kj4$~Y-D;!UjixBAhb7Az{1VxZG)6*<7i-1?iLv@O&?vBZk|B9Qh7zK zPjCAs2ptHZ@tjN<)6q|W(Efn4sC8>I+P)D&`}{$k$HU#}Z9jej9VqqLraB${xT9!K z(Co-PDDo+0I(i;L zyM4ZVglbM7^_S)*&|a%28Z*!cgm(En#g>|wKC&(TH-vWj+E8xEdI*`I*{)vx$IPVFMbn32fY4FvW!ii zsVx38g!X%V>4L>ipYbjJ6NL78y{v|Hgy}u`#!<90;B`mJ4Q9F*FMb_D`@JrciPmRG z@M{xjG#4#(XUOEM5ZdQ;Hrn;@bT3}~D+ulN1j}rQogu-mKxmIAFeq6uV!BMe458f~ ze>7UArpKzqFHN9*s@zV_(9RbjwA162T1}0d-jgpJL4%>#<6$EP&rV1G1%wWG+$0uKMbK=ZdbR-_NUJZ7C!``Q8(IZbgMJG z_*aJS{~hyhL^fW&y0~}|{NwuHbqV~>OW=nx7k^+mE$!cbVeb4+yk8&SxpdJl^n#J7 z90=CCS-RWnQ~RCLz)w26sd7S0@sXV8T!)p+vd|MkT$hRr%pzZaBFFP@X;q8q%6>j| z%N;kPPUq8ZzNHBYcgM^7)kZ4a$XBw%?>)i0pMOid-OBB!JLyWR3BJ2<7l76p=??T& zt6xilpKmGeHmcR~ZW6TBO5Xyo!~Z;BbIbAF8WJ?q70`oWTQjbe!7mSnM9FHU1$qdm zo`1lxGtB!?tqhy-i~z$9Rj;bqyFt6Dbgh*GjiBJ)qo;1RhR=0b4x|6rFQ)rLHbXAe zMjj;n9sXCTnv9on)n@CCK)~rd{B0kkGzERz9|`kdbIu9&t6c7(3UcvgEzPXQmd$iTq}5cfJg-Rth1IY-ZJcRGoP)*TkXc> zhM zbgUarbOdJ`kH|Te%&-Q_xU*iAj5IRQKuKoHx<|Ub(urqMWXXePZ@GKepLS)~qx8_w zfH7s$Qy%^9yBA)9Tzmwu^t*oehPm@Gui_=Ju$p(}GjX{T^=IfEDpDiR3fF17iwxmP zGVXnUq9Aq86^mJt)HSwW$+p!xRyE-H&Jjxw28RNgV(5WML%-!bo+nDa7?I#5qEgLv z6Cpp2Mnzw#81M+9X89~vCzHSiCQptOe4dZxPnslt0Ioru>Tj|7#p2kmTv)k?)8BYT(>pUQJ3Po?Z+l^rrWY5;Q5R<)K-4o!31M~@Zg zklx32T43f=SH{T8?14h53?fIQ*AWKA>WU4PcXdiS(-4(CP zDQW$5wX5&tNlUMW(W>I{dpx)=M~F$CFz_fU)b@fcre0<(wH0dl{R*l1KNEdeBKOdQu6UvMntGu#;By9Md10sT71%P}%hWLNe_jwXDYvJtfY*EjbAVR``bH!% z=oH&ME?2F^xMmZpx&~>93iZp8Jx=v;B9pF}aYFRh=v2e!71W|ddYdfm1l7lV8ADO` zsA|30bf0YKa}FOJINfml{}|*s$hNulGg~X0S@4$saqF$M-&uRY>fI}!UqP2&w)7WE zcPzfq@lOtV;Z5`ZHm}aTZ*Bz;oZ#=`lP^AH8<|T7B?MVrnOj`Eu;Msh_cpyskQ@Z7 z9-p~m>+l_gcYQmy2{s$xH0%Ez1MlGP?1iR740 zB(ArMAv~Xi=3I_p&HLZ{{P6Q*GZK9J{1DB>TQ5F&*jM*qUl+~?t({2Q6-jFiG2k)L zF34^XYpM|^Ux^obiJCV-nCzgLt0hZ;%YCg?yI_q|0;|T-2|Btk?Ba*-9e)1CtjstQ z9lhz|lZG9A(qTvE&+l|niGjB>sQbIV81AmdvkUv-W?zc6E6I?=@&FW$C|tX}RS*jKu+a=u7WZQN&8@Jw4|edQP% zpp%8NH)2E$T&z@8B+wc&^$O%oLG5D#M0lbkj76=Zd{vYtYXS@ILj@ zuya56f#K)R&!~4dku~k&?HA7vyZG!wH7}m;CJKHwq;w)tzq7{C-n<|yJErDo=arg7 zTBua6$3>|SX%)hkyEr+!Id^P#7l$4Fss9*$zI&EcdSUl@;>9NpySsPTUB~$-4%UlF zBA?4-%l>#Ykd%v_WGt$i9>!B7@~+5W-_XK&s#3qw-R}+u&EvZ{^73~*Fzn_}e07H6 zC%Sp-#ao8mJmTdA-t4;rxt)QT2xPOaMw{1@^|X^s2KQq<)Sn3WG7XpRQz{}h&C8FK z=g7-{{Mo~fzVk^Z?dVMx&p{SCe?C&@-5qC})uNT2Ca85hmXE2$0XS=3*JEnTlG2@W zqrFqhx30+ZvX>v*#gUi)*t3RR{IMB1xF-_MUA*z)&9Ij@Jbc*FBUzy+2JsdpQu06! z^@D2GZ#BIE3AdPxv)5;%5zcv8l$X8y*d7kM_M=}Je!gu++0Drg-hT0>2^XSE9!mCB z1Ie-&%GbSvo=@{)O1nchhX zUi;Z$cVGX%XQq@ zD*<`A>teyen|+Js6}uwJ5yCE6SB@Z*+0j7?%eUU1`NxMnSOv7Qz8?Nbw`WsWFK z=u`=1(SGpp*}t%ba&EBikZw}xq;WSrrQ`c`XS`Hn8kI;hKhCuWfm+Hgik4QUJQh1_ z)z$8YF~;1;KOpx)dteqWen56TPLG7o7R%ROD~tKs&qCg#IuH&P%jd~rDi+HaSsJ&} ztTS6kkHHqZiuFS-U&c`lw%pmVv+4)aydBJ)?pEp2jzeRyJoP+bX6dhbgDkGJp;z2H zq#IRlb$K^d@)Vh5Rq&hhje0SdwIsa3s;Ac&S8c6YCQR8|j%W`LO`443Xxxuk`l({h zEd3#uEaod0^on_>^juj?#bU{1oTQIwHO=OsHyCB?yqN6PLJdx6fS7uHlyeaI{l>^E zP^C*Vyu#8?6?A6l;}6J!PClSl(A%XOWI+{M;;2Q$=;Hoh;w!jl!fXpo9g#Y3CCSK` z@`XBSZ@&^D5@WVrxb!}cEd5jwXO=$v@3M%am*^GoR_S`Rr6ZhcDJ}g}31!jx-!uF7 zH}wj5n{=K04)46YfdiJV|2*PI!dW!?^oY#N$lE_DOW6Joh=l4$IxzBUWeF8Vj&Klg zB%y|ppBnj}8F~G@EUo})qdQkx6D;S?k;PT)h!MhC^G1HEm@^}i4nW*=e(eiuA6R?sT6pc2)z7a! zvHI#&|LTp#CynnjzS8J6UI#h{zISE3VqZB2ZvX%3^5dX$AiKQ0^s%KUmR`N&U%GMe z$;J0AzH-sMc-_Kh7T&usUa&8m141Hxi0JYQUSN=;2m7ifi2Do<`7;rL<6zo27!iiz z(3~a?%@H9u4y%pB5 zY;bzoIzpKVD;!5#M<|A{z;U#7grbR;!f~{9grbO-z;U#7gd&YEUf$U;U>NZtIIi}% zQ#kQLIF9zXQyB3AC=RJT?j%P%AC99v?j%caa2)NiCCvm2$I%{Jk|E4+9PP0sX@Y^{ zXpb#P5i}e}du&OPpx`*#_D>Q73CGda0}>|)IF7a+kQjkOaR_ZaAUFbp<7n#v!4d~> z9Bn-y%)|q59Bn-SsdM{q9Bn-yXySf2j!L{`O=ZL%DINExEv&5Zn9Bn&3vXTKIPLwXwi<9i;!uxW3uwEtS|_dCx&IOw&T{HV%{{6TX9!qUYD>Yd8| zjmsRV{Zgn)ccSdIUPs7JTKOh_-E)_3z17g`Oe#XY*Sqxpb8c9^`DOz~2yJld@aR9{ zdgus|c}+3NJa!%QU1%#Dfn5v5fT+&J=Pd8-8H!a=4u#D+vXk7t>eq|cs9|tS0Rw*B zJ-58Sub@Maqyk9(eRoFi_1IQ= zYj^X@n;+bKbThtre&Y)pAJ};9MtI|v_0O+AvHt3H|N4y}@BbG}W0Tdix%TI4kFV9& zsMT++{=w>xtiEjZF5{PtzhV4dknw;0%BNR;cBQkztt>A8zsql2e);lUOaHv|fhB3l zyL9d1rxt&Dv9)MkG%Wn#!jCL~E8m7M0+CMmP}0rIFM5&oEP_+o(-`5)x?1&IxFcaU;RF zZJ&S!(GuVWNn3CnET=l#2k$WjX;3GTa0h z?zI!pAUX*WR;ASw&>&hu04*_|fCkYL0%*y~322bvCctnnpMVBYE&?dm(g|pg;U>Uv zFT&Aq$hhHP+!jti)86ueYAcjvH~|f!T)3Hf z$S7kBH8=qaGF})(^-sWpym^eEdM98(Mj0b1@dPZ$c!7FG-4n1NvjBz@9jIr-X%8|e z3(|(;Xuo!jAzE-8?E&V@L=%pqZH^pG2yh(j7tWDH1CFEpx;c!f!*R4yqdlge ze87Wn9PRNjlSCGdqy4VUIFW(lXuoSSMx~ZtaOd5cAs)sYn4Fj6Nay7+RC8PeQK@pf zeNf1YR057gL$Qb?R6a)g-7yrAhT~|@2@Ffb;W*mwn<0r997o$87(({{_YJ>l*m?I( zbo)QH-?<$C=lyTra&LZR^DUb;;P3y}jTf)~{rVf$In!rN|Cfnc`>VC@UprX+_^Pye zpYcz?Y5tunA6*%(oL~OKWpVlDr4KJPmu^`6?ZxWiH4DG7P+Zsqq|g2nm)PaS2yghn zg;3t+DA_Hwe7`fw6!VpYx_;CyP7J1#YPv-D8*IJa87JtL$KNd^(?xqA8mUaMMpJaP zYXMic5|_*~vcIN-ti9oyB%S@HqwcQ;no-f`v#{Nihhq--c)mCea^6b9dYy;BLGqS&`gKToN zFJkFfHb~{&#dg)l^!#47n|C#WOGovbu&BJx0reR zDApYb@w$2J_BUBjIi@wS`g09?J1!A3vcIf@Y~3*a>u;%oL8i=_~VOApM*{=x;9 zE1qgiz2#vzVy#5Fc8|KE&2Gfj$Pq$Aq#~}sq~)*fwW7!rb)Le-fvIq zJ>Fxj)3sJS4Qe8|I-YTct`nAGTk!jUIovxV`)(a%GtRJ&s81X1Ty5kri&V8Wp5nuH z*^zK&SdTegXs%mfGU``mgJXLR4$TaFT zDmWUa8i|glIs#<< zbQrV~^ZIL)fCE!-`J1`&e#YJ9JZ&zQt9OWyBNCS`&d9z)2U(`;qGP3=&~CT60Y3I* z9N99~2qdOyCeY~AFza;R5^1rCMh$4fg&EnO)Ik;@fiNzepOJlB2U&z%zqoY!jO^QW zkVVMMi%Yl7$i7VnS%h@DxOD4`>`$DYQiYNO7ng3Ck$tNUvIu!=ad5IDZ|#0u2U&#t zvN$;Tk;%SA2U&!CusApwlF9y<4zdW@TXAr5B$Iu!4zdU-S8;H%B$NHo3#aCa?9zi& zC>UxL?RLBlP8(%n{<#xNWY4MlPQ_( zf7L-2Ax$a{POfCKZ`45+A+0G6PPSyS|F;gZ2uVnBaPlRS{Sh5x5i*D3;ABiD`@^T_ z02)Qq^|9{a? zHtdKy#_hLm-v`e8li<9+u<`Yc$2M*PxBBlgy%|*W|J}9l>hG?;%=l4bVdY;|esJZv z%kNmeZt44%&M*GR;#~{Bwh%CU5)grYL}}l+7`gXe-k@XGf0X$!OZMl%_ThLvv0Vg@ zpe@}Gn7(9-H|X#QpxGh22+l%9+5;R907o6}!d!yKE`t9cNcRCw9N?tGgE-<;lp|!9 z>nQBH_4^ey>0ZE&1nhKp7Dw!k=Y8!WI2+QsbPr&5_k+Aahre;e>@auhrP?t-bqD8N z1Rtd+-7O0Po?M5Qa_L3SY5?vcxGdnA?>c&B9bSu`XJ!=H#RP}q=>Tc&Jd#F-vvWn# zAUH^(vl zVs_%(p{{L$Qfj3r-6jhIo?M4Zcj-kR&K(H;9eCzjkDghFi>K$ADaG7@VJL24Te<}Z zBO8tm$M8rP4Z}h37%S4vfTL_UI()_>j>ilK!HKL%HvvwvChKq`k2onb8Nsszqx!s~ zQPtsG>KRpaEfkb;B1P#&Ss3u-I$Y8tVGb*M>>~K5;F+I$^vpV3R6WnEu4jPa*0!Y^ zfH1Nq>u_w3gwfDs1dq2OT@N_Qnyka;J>qywlM$TYnsgoDBx|w`H~5H?LX#0ZV=$`M z9*wFF=lF_76~Un{O3#sn0Z*>Or9KkoNRtu#>xy*E(Mam>u;+?opt#%>>D&<~9ZvT= zCj>9NE$skKvOVbV!;jvahCLuS=oM-E=-G6*=tmrn*#kmFjheItILY>)!)rg{q_77B zM;_S2CSWG-QFQq7N6b##qo~WF>>~RA%L^9_oBw6)w&iz$AFh6sZ{P=(q#Hq1(%fU9 zHZX$jo*t!9)aQm)qNho%vZM&=CfD^uDWN}uM^GT`0^JCKgX%ny498= zBOR~SJwJcRbxNQ1;=njbvvMU=%{rxt(B7~Zrkw*OHjU!ZUcXCL8qq+|%OooC1A9Fi zh>)dzy>H>EE3Q-ObTq78u~|64DhEl{>u=yK*O=ve$xK1Ox}>{Jv}_ZqpB0_yZoS{> z`-+x>P>v$TrjAKLxn~>I4DHbVp#eA4AW_@+5zB2jpqszsCMd`V+8Op_h5tQr9 z%#ir5G((hm0;xLi(_?<$Ud*47>cAjfX4W=A)9O5&tvBr_CW&6xGj!Jc`A)ihFv6nK za3EvvM*Fp7tW?gj0qavJjGsy-U8SzY%_UmH6k*5gr8wbAczT10g-Q5_c%Z?jGevKt z*2=SDeUfh!GHjX{W}Edv($_16hmmQ$6A35%zUrPU8pHf;v(GxxRTy7>bYOLlR;3$c zt9yNJVHZJ{&dlm?EvrMQn{_se8R}}Nsyvp{|V@Gmc=wWviNQ8WD9h~*X zkqdFpP;(`|oF52sml?TR%eQ;tJann+jeNrvd`ZjSYHW|eSBD#%qwh4ARRx49-l(FT zALjdcxup$_$ub_wqZ0D4Ma^ZwXR;OYa*vwXkBwTfap`AJ>OWtUpKkU|tu?I8THVZu zUn>7k0!H7#CLAC$47$QQYnZ=Ej{I&%H)QjcyWxo6wm0at%4DS-w6V_4FlBLuI?RD1 zam6cq*<&3C>rO}6X6K`}&X5PyJlnLj)~*fgT%_4ZjC~_otcc|%>Fj|r?Zn78h?G3j zj^ARgSr~Jv7O7`r^}ULDGKhqH4Y8gmS@bpGkRSbUg}?sN3XfoMXExVhPj#-57f@A$ zD72lo#tMAs_dPSS#v$)swblWaaZz52WjRWhYct4y);@6~WXMA+%?(5W0Ja8#AlvFC zb0RLiEO=ijy>k_ybj9tbSPI(uCKjpm@nJAu$dtuk)8R~wqd}k3>rb~!feyn~4p@iB z&rDf&_{v+~q(ZH%nLnVr_O38t1t(SrOv^D>CLHqFxcanL_9drbM=VtwHG4H%-WBEj z2_fGZw~9WlzZV?Y9HDGKp25owAD6AWtu@Zswd>nggLF-~^^JV~f4d=X*m-38TidVN z`sUU{oBzH!+4#oBX#MN!1Jl<`y|u5dbyvT<+BSa4*j)L-N@MvSmupL(U#cvAZjoPj za-nGWEI@~U9)1nG%<~bHGlX4)IQi)}q#k~ac_skDkpfC+T#o<@vH%ECb3Fpk$O4@4 z5KuD#5YAf8dI(5l0nT^`2xI}ycnI*B00>0SdI&IN0nT^`4rT%%+`~jUywYQa56lEW zI1$=KIN3P;hTy}m*+&)t;m|{m0QVycfN;{JM}WPV00>8AXFUY>Aq#NELvSy$0B1Y| z_sj%9AbQqAa5u65XFLRV%>+QO?cFmL;Le!<2#0?tM@pxAWDmb)7g+#=sE-~2?m!mc zOygemU+q5YA-D}$fHNL~Tag7g;~}_Z zCIAA_vmSz*kp(#8A-HKK07AeE(oKgFAW+>!$nn-A!SfC!K%lydkPfa#f*TJdKuFR+Nv}9PP52S% zxrY)UByFIiUFebEhC>Muk~vT^G4x1q{gDLv^E*)TGxSJs-Jt{sSsy4l8G0nR_D}+Z zOb?WN4LuS(=THKK4D(%t1clRcgddTvIg|i_DoVnF9tqAJN`R0Cfs)9eM}nP02@ujF zP&(A=kzo5!0)&hSl)kfiB-lEX03nY8CCfpN1e=EvASA=1G>p|F!N#Ek2u)s5x~-lb z{Ch-NKa>FF7y=<)u15mXp#%t2&-Ntd5ozsEf@drU{fM-BD8Vxp1b##^9!h``1x5&0 zo*n~zL|QqN0D9jceCmyZ&6$drjQhd)92Ai=WH*7e-;_f3AG*^4FGMxAd*0A6~k! z`2I!P!vD8WHv9!3a^j~h-M@O6V`0!yU{B)~8H&R$ZAg27o1A1}&{20!N0x;dQ)fjY z_}L}tK3N)&Ut-WvuJ1^iqqHLg7riIl3)sl1(uOBacTtYmXy-~JxbPk69>7&j3^M$R zZm#oJ08t_VRq1ZPQqGh!{IYJA-@Z-ULvi_=(p`X+oGoqmCEcu4*B4P7{JL}};3uad z8Q!Ox-|;J^D84)}lU=}VW+uO=o7;)WNKd8MLP@$qmPUSd9YqI~W`a^}VMDrj>8XGI zbmu|Ge_FcO9DXbR3I|o*K3*a`h+Mm_U?L@16 zDrF8z(#^6oveoJ@UUIa!q0~9pkZ!v4)IWW?yQ{})pGu*Fj`Tbr&CGt@saqNi`$32Y zR6!=D3JJL2_HPh7}JDn9=R}n%DRcY(e)4f?Y%VWBVkR)1{HUX=duKuWQRtjB3h#vr5 z-2mKXy80&F+)mV0b@moYh+-Z1?8(Kq8FoIt^S+%&cV4l>?ws5H{Pu^o-@M(~c5k2G z`o`ABw%)b%YS0G|+gjWF%;pC+->_NTeCg(m8(-e|gN?^G#-JPEJsXDgkFURP{n7PT zth4LqOrJM>2viO1nB1m|Yu^N20e=DX4EUZkW^H@*bF06#`li*^s&n-=uP=Xe`Dd0NUQRAQuxwoV^wRs6erTz@^kUFi;7g0Y zzxcMr;bIU}9sJh9pD+Bv!lMg?g%>VdXZV8Qo)g0lvo9BZ`E;185YL`BaQ9MVaTlk} zpz{&IFbvKx9&0B&QAJ{|xnmBFcH0}(jvDTwPPhv?;m#}JXwY7)mGL#}YQDGYgu6`% zM}bzY9*a9&Qq#Lt0Y{s08ZUa{p{yG27M*Z6>x8>WC*1Rta12gkiJm_?Rm*pyPPpgl zgu6ivM{(X@G8I+xyZ=9G!62DB(bJPjfBSs|#v+=k8nz zT(%Dy!$bZ_KcIjjxNCNFL2c`T+R_EJsgR%IaEkPf!xpPTG7P(BLnqw2PB@cJxHTml zi&OYG=8nwCx2hA)s1t5Q31`MBObqglviiL&>x5g<3Ad<(V{np7ru~_an(u;6IDV*56PPl(j!kKXr zn+lC?Sk3pVI^n*e6Yk4OI0n?{=vA$8Ud{KPb;5l~C)^j6a5PS^KCf-)SM&XXPPl(k z!cjN@njBe0_0HzpKPuq}(CD)e7){mvch3ETPPotOg!_A)aGz7c;W$CY{1caYy9-q_+_X(YFf2o8s<2W1gJ7Y=p^L|_>++Qf+7#uhE$_YzEP4CZ@a3qdX z9jiO8-f5ouGo5gMsuS){lyEpud`rUOOsnPlW1VpSzfQQ1Dd8}{xSvni=Jff0=!E;I zeE#XTdP42Z%uXWct| zX5~FAFJ1nV<;K!~EIqokyZGkC2Nr&3A!GOlKt|`MBVk4!9C;ab5jxYIegXX`@T7S* z)|3ta%a?2!b`iSP>1H_}m*u4g0KWsk522l%Zhn^oU3Jh}fd?mD29(?f-F#F>VinL* zfd`Q?1Ik?t-OP@M)TA}gOMwSrGQ-(sN$5Bhn!D+d2hA3MDB!6P?qcYE>chZq2Xt6C zvPsDY&>rE?A`q@!=oaPBBESrA@#uLGawyK` zI9z?5qXSwd99k#B4GZ0(K&qNL-p<9%1Tjxzj z&x?>#q5C-f#OGCC+h~GT35TYFaG654C|ag+Bj6{S3c`&F-TW?_N(Hn-IMiN*%M`l# zsI>QnqtQgjjL^;M#L-k=)yRV`2#4BxrsaQ*HhzcN+XVd&4mAeh_Jr=Ed@YT+7Vwib z2H^^YZhpr#<~e|mJl`SQp3u!lr7_pY!vy9#gqsn%S)Hgc>bn_v&;#L6W6rcpib7*} z(EZ?W1Q8N8PCwqCyOq%dJr52w6XCAJ>3oi!WnMF7|9^SmcMO}aGyRltAN+9j^WF6Z zK#AsCE|q9LQ=?b)C%{3O&0ZgLr)uWfVu=@PtqLgX3C`xV%BCXNpN!hBI^JpE)Dq2e1g@572Kx7OmS{G!v}^>L<=|ue zKARMrTw;V1jbbZa9ke6ywyP1Wr|s;3;^<6o+8zt(bgg&AC7Ky76tcLHdBOLRbJETSfb-C1Q|~=92tpXrthq$yCvT1=-`%Y+S}$& zDmG);p|jK!Z4S4mX1AB3L59y3OCsMTx^=Qpnbv^m>9acU=)mf}uM83&Kof=^d~o4x z>j3JsI(pvfH0(&jG7f#MXJ$Q}t>8n@_Mq05Oa5P^W(%&|T(d1V4!8&@E3?N0HOEtSo*`~{EIFIxKK9-Rq9FxwbApm+|i*ziHmYvZrH z1%NFkP`D@9jP%FdQaGlWF}pySZD|MDnIa#hh7iU#Z!$&cDh$+CFA~1 zEGm{To2Ax|A7I^~tC$o=6|9?&2Ce#90LYInuP<$oR3kU}X)f%Zshg$Jba_J2+RIY& zntlQfV(D(QUjVoSUJi~4v z>>rn7-h`X5l;ikhIBBp$d)75+Si{o}4%U=JI2|ebz4WMHsl=i_w?FKtJBJZJRw~yr zktPGAvexbH0ees>PlQO`uFvG;M?b3nzr6TUp#Ps~$@o0*!`08#H}LFw1EBTW`Ac5M znMS8otAf1vYOD&3W)mue*tGVnjY>e(RO)c&ub#2RY-+^29JXp>Jnl~?WxM(Us*G@I ztU?`;mywRgDs*`rfysHfE7bWaFY+=0g{yI~IW9i`;Go=Y9~3|%r{kL}c@$3BWI^_B zT4GFYhDN5@%RSzu_f6HxoT~8P!)wtt8ShI@@XPGuxAR%Fy}tF^1ax= z;fPdh2bM5nbFfoha1X7N!{2B{oL-_OP~&Ec>DbAtm2e+SDf59d-y2q1^j_BG#l(@- zUy4%jhV$Q6Gw^QB02pj5a3^RN5L$BlZOI`lj4Sjy z<}~-XL40Su4gzOsXbU9Vjt6}{pDh((!xrvr`BR%zw?1Cc*$HYc=;d7;|w^`FnO7zVa@4_)L-1Wn`I^Ygh-$FNYB{cmbMWE>CHS{hb`bk|eN7X(`#*xi+yVK(C*W?gY6VvE`G)_!n+*Y~W=;$FfV-Ww+{ zI@Pp!{H!}+_V^3&R5&_v5YA}C>%qBbteLP7qNN%3BrOx*&^U(G1A98(uj}mpCqKIE z{EMK5itPM@4!;P>b!Ne3_Pf#yQRWGG{ZwruFJB#aX3X!~{AH`_3_xSaqGC5F_ryG9 zHz*5!h1)#P1CN6q>4Zbmk`H6U?GO=8hIz~%6vGF6L9`DAZztx-COq|!b?W!APWFne zE-?*K*>Jrw73m~XrJPeSl?Wv)_OQ?^k)=?k+a7|R*;$;53Vlz8$<*z5$Tvt%iqr5Q zOAhvi_D&EVd&~kx?pZxuhp%S!=?uBakACF;`wSm8?0jVB%{!$XZ2Ozrf3W?=?N@By z4>JEhy!D2ymu=m%`L)dtZ9cY{-rNN_{~z3VbR)iTe*FvU9{|+>!|S(zZ2u=ruQvHj zH?BRo_P(`OuDRE)Tm8)HdsoM+_SJL7PZ{53>>FQd++6wi$~#v&D=%DGTmIAK$Crg= zc6oW}V@q#csxDDWhQ*J7Jc809w(!k`KUjFJ(tgBZ4-oRB;%B2oX#kYVN5Mb*vAtr)JamA0#UZtvK1x}={Eyx znsSvrxlukF%*Z&rj5Bdi;bEVd4lGm(@0$0EZYRr(g97Wq>})NVEHQjZA<{RB2-&#R8?1(s(GWeTCI|VL(-NJ@R+Movt%<$oFZX;o zJKo3Rjj}TwDRf5Rp0_rtHCVRcCakGZKNPnXWj+NNr%ac^dDh>snG2n6elm4B^82-3 zq+i^x5w%XRinS|ASGXM-w#>MhpJdr&v=EFtWIj0= zhp*CO|Fluf23=TvLV@)T3v^wHebLj1I0lKrs9AP;C%I&9ID4M&k#Q_+u0nVBM^!wC zWfDa?kZrh1Nzyxvk$yar&NUJvY)k|b^+rJE^Kuyn!zMyAGVuAh+_axhn$v{_=(|~r zn4Q@ujt~5$ZYi3Lu`YU4k#Sxo)fD(P{!?i=mpQ5-JtE4#WS0 zOIaCb$d6m*RJ$1-;-g@GSPIpgOgAtMGM1vl-r$K&u1uv(=X0x#aoyK?(H9~~9LQ;R#7v|M8Ps)vA)6*j(Wv`hkdg{I)9;(*6)~cHu0%VA`^W5%l5t`(jy*ovuUpAP)t%y^ zf;G_`H(H5uDxAWovCrb3w5bv6jbc`=VUcm7GEQ|+9dkW*D&0sCJtm2hy;QI;98|=# zW9YSJ!zAms<@lU8H>t=t5gBI?Xf!)@moLml)6IfCRuH>BM>i`LvI(1W+GB*Uy}{TA z8L}6Val$fABGOHPcje-&PPUpNBJS*dpx}0tlMY{zE=LJlaX(S9HRwq+A>)K(oOIPe zx|58{Mn{rvZ?+a27fYp3-0lKxW9#5$@2AUoi&)zaF(Vl#DC4vmW3cYD$3&=}b(yOX zA7@RKEV%@4jV2>lFXR*ZTr(Undm;fDCm`cc{XAPu*d5te&EqSJPHtSPdBZ}?)eMz_ zZlTVVY)(GyPiOI&Y3oK1d6^dDNXKUH%`4-$hfx<_ZO6-b z=e~uB_-m2sxabg>X=Y4&uwg3{vvZ!Pby%Qh@4zGD1P6Yw3S)spIol9R)^ehlBWkW1 z-5lhLgwt#*53O9eXB{wy} z^b&Cl@5%4YCF4Y*X`W7XJGGi^)M30+Kh+vonjt@ikNZQWnA;cVpl4teJ^tDAIAxrk zGw5J#WU?qk9U?uZ8jeu4nW;`DRddY8+S^5bgqgEp`>;IIQHPAvitwVvH5>=?al1Fk zjO+erhwQn6(iyRFYTWZCR8POfDygoU)|7!kopf9m0_LEwrHgVZRR zkF=fMPC;};x~^o&)|PQ>GERBImh5g1P8TZ{fSbsq*?n`v9Gz5BjZ{7w@D289A{njZ zQZrky$~Yi7KO=gZA<{dwrJJ!q$D3<}lY|{}4@QA;$KtZ@?+fjAxRjqgk447u^gCRd zb(3O482N2QSI@HFX*axnvp*DPiG6RQ)w7Mf;ACu+G5j}NdZ~;iZi2$%^9-h$Vr8YIeX1O-XEtDJ~m>H7=9g> zUMS<>#9%t9rb~ps!Pe`Yae{7n{M|w_U9<G7e5H zBY(Vzhng_n*?K+|sg&HpC>sMjT+VuiNKT6*nZF0fFY~8|g=SEY`D7;@Y`Dfb;%beK@%OMRY68*hOfv0OI~?~G zlPOyz^Q?Pe)LCg*M5vBaF0SrRX4AZ@(~G!RBCYkptYXkLrOO3h!)6IoE0(6i8=S;w zCduFvJXPJV?N5f`2sIf~Wn1MH&$bs<9G~3_3q}%+vEAP&wes;~EF`oCMT@*ZsOB&i zj}s}eW}S>i!-;%TIZh6QZab8 z^0V!QWu|BK!lF&wmB8s(Ad$fmsY)~Eq{)ERq;D_8qMl*7<*k`rSQFeF%BIR*dXT_` zP`8@P*&V&Mn2Qtk2nIClWzV`7HaV*ombKCG7||a_8l_YccQ%tYoC#`~q`zVb*rK&= zD4OM5HZ$Ihx`c+KkQ}gS@5JXRjxlD)w!@+`(y~<^eAd0NGn>PTxOJSKdeeE#+snrK z<(S!?Ni%<|8{XJ{H6RPA*(*a`uSUr0>geF^-)!_c51{ANm@^e&1eg z<@b4K+mmaLf==E}c%r2;zR%h!#b@0Mqt9#(OLnHkxV7eYSD8XRfO#8zQFE1aCi!Bd zPQ`?(ZOS%VBi}yHx7w|J0>e2R!-yQt;-H1VJ~K-KR9r-F4Fuu%Wm_HjMv&X*T4G<} zwIqUO;Au$}VZenPd^DaM@U}o@XdgHmLp$zwRL5039FL~CUMxIH+eq7$=e!cZzMrwh zS-X#Ok{v5^FphKfREPA{3$|D{!10cVeXcqjLcOLuY+bQRgEVL2a~nScmE-z&0egaEO&RijP| zs>^^HcVU6;9lw+N^xUG?kmH%SlS@z(1x-B6m1hS|ERFiX!0HI){f$V|8llM^PetO! zg4z$o^j>pR$xbu%x6TP#eL7pm5tyV&!lnp7*F_f5+VeYXQ=kA8S3_u{2Hxd_U2 zW@bog-N}8{n<0vTNOy9d9`k!<@8l9BZI*BSYTks$vFxDXpW;(bjbK89A6 z_%l-Z5pfC~!d+ZJ6VM|ME%x%H-bf=35)&HC~?4G%7Sa$zyNc<RKe#V;WC8nkZS zwDHxAKiYW5#&jdPao@(``X|<(SbuE2wEn_{*Dkzlfm-_1!lvO#c;I5`Q;TcxZ~xPN zbJtSnQgbli05df0O<0ENo=+=ZRDfYDP5PRmJFaSEvGR9HFf&bL+<|UQ4fdoO3@d|D^yUFb1nwO5-`%l)Z>RXaEN{G|FLh zio7YM=$#>l5V&r&lTdf9G9{I8ERM0M)VQtgQDRCc;mkN@E?R>9f$C{ZaU~psV@$b` zb*UfM6jQ>{I7Wwj<(9hVlPS8acrO&Fu@ftghA|ai7}ssv*hHWD3%)H-f7D#5^*ryAhmv6|YV5{zV+n$Ou&cg-;_sKMy$ zIGmaTGbq6bhHCWV1-0I;D7(Rd&NUb|;O$eNN;p&C7&!u10mmhpzFJ-l=0668vn~Icob#FpTA#{?JI>smXZ1 z0*v8UHrgoq3ToN*lwd4t?)eIlgc|HVC779IqREO+-ImIDuNsUl*DanoW4K2F21cIj zJMx0Mxt8&~8jL8_V)3q8w%e6pEQ{x>QD;gGcAFB+%wiystTRXLRwWo?=7K?+N4>Ne zZ&8D>!o=HCFT2K@6<`!+W_VX6p>7yyyjBUun(5RuKWVEU;Wr!fZr4o+BDW>GI73bE2hm>%jl@~p-PFiYjcF2< za1>55p+TjkHXc(~2}j^0-42u^>JEjbj$$+*y*5p4B{d8u$&srmI#sG?YAN8LeKbu? zB^<2prAbi2!TMC18cH}=UrAG42?y%~X{ssVVEr9U)j2pO(+oG%n+a1z2?zQ>(!!_| zQg=2smFM6z`$d{~B^<18qp75X!*GHqB;vf5V`zGX z5)RgP(3Dfc!TJfBzGn`O>G&*#x$$_p5{|-gx|QvF=6s2lDdAxK2~7_w;RqZjoBe5h zZtlw};b46SO&KK|tWTgRebxVmcoyINe^#vGQ2$@)ra=Aw&nc*3F#mrpOrY)mE5ig( z|Nnn0m;x|u|6dsqA z>i>VA!sCSb|F2b0gZuyAtAvC4|F4;Y)As*CUUlTsu?v_9q5l7?m2go1|B*R3ZU6sa zB^=cMf0Ysr>i;XFHWbwVe@HPJF#mt5d*`v1zP4GH!Cl~EfRPTT)iMr}x_|F4YN$Z*>Je@8JMz%3=nn1A9@2V_-o8xrdO zE8{kB|6dumk>Rxce`VZ;fcpQ+xD5gI|CMnYu&cn$y>h}bcUGs2+sJU*{=YJALqPq1 zW!#2<`v1zf4FUE4m2n#Yh7*>A#hF&e(Ufr;a1k2E`}x%TSzU2`d1&w$zOuORsf|Ab zC+rV_+J5^R#`V7j)%|{Wy}oV(_5J?U^ik8#f(m~b6Jgo_HU9qF+MCweYi`3=7H(W9 zFT7;oiG|lKMi%c|Tv~kl;&|sTmqW|(ZTI$hP-F08TkqO>^;UWd+gjWF%;pC+->_NTeCg(m8(&_1a`l6& zZ(J2X6~SAKUo(Eh_)g<1jdA1s#^seytvs>v*h+cj#Va=~e`)#mm*2KLT=?Xn!@h8z zfjH51SFDsYW7b}GpQi@Xj^f{>1cPlIl&d%m+d3$N{m^ZVYKH;a*4(Oe|1g^3nqI3{ z9im$KYsE7#upON8%mutR?Gv24mDE7vFwGO3yA)v1O{i)=2s?zjS23HxJ=2#e;h>)G zOXlFTPby!0>?uz0R9;z9m<$}OG6_wKf^Hh{h0}Jz1luiQx)Am%9XNxS%Q=t`4 z3iVVdgJJ29PqfmjUtWC!SKq*Q(Hl@0KGaWDoh-xr)N@K|Fh5lpI-|^HZ9i3c!v=Z{ zYD>u!c3)>x0S1lNtti1@@jB%iO~T@Jsv9r~h3ph5a? zDxVY=N-)@s%70RTK~Gea zcOaoBDxXzSGt=59Du1H{V`xkqbUo^;9V?$v>}=qEsWR3N_e;}CdQiVqd8ZQYmn!d6 z!u?X^ol3Z0s=QMP_e+&`D&c-0UmGq!~sq#)G+%Hw$(S-Y@0VQ9k zU+P!FLH$yn5)SH@dX;ccztp3EgP+y8)o_rzx-KOg^sLUQgoB>dIh1hFvpTyH4tiE+ zQ^G;d%&bZ{=$V;Cxnh`MhXH$6&;P%RHd$z*DpmyO`M+}QgRWL{2m4p=|0~xj`1!wb zt%9HbE7vOc`M+|lf}j6iz5jpIdF<-_e^q$cICrpr_5QzNwq#*ZwYh`+tM~tpXPH5x z$ErXsEPAXw*atyo?dY*G77Y!Qs}A;IfpXQsJ}ii@3cJIC_;UyQ+9wC9BLUbsgz`uL z8dp&r?8D+Ja|ioZ@Bd%D|9|!VKZaes{|`C;SDquo<1AP2|9_X>|Cj6k-EPPmb{^UO z*7mEmzPa_#=D%-FHombjTL1d`!1Og!Z|$pV-PJFzwvAshHdnr|(pdh-<=WEcmnw^& zTjUp>Tqqho3((=8hhLLg=J^OpmlBk29v57$c&at^mO*W4YbDaPdmO6G?!&K1A`5`f zwnUEriJ1ThJy%eApy&}Gjw}E|Llr#&#E=Cz;~|J13ZTD*%~=mY1X+MH9)d8k0B1Y| zp_u>(M9+E%g2)1#@el-N0wDA^I_sIxKNA3<@exWRq6@_~=$7iMx`)1ea?r5aYQ?#! z*oQ0tLgyeo0(g-HKxmPqM*z=E0E9kLDE*f72;fE*;EadBg)G1s4}o(g076@?vmOEm zvH)j11a@Qr&UgrHGXW5~oSpR$Sdj%d;~}sh3vk9m@Y0z82uFGLpLjECR_GXW4f3Zirp)Uz5sehG@5|in@}R4t8G^jFG^%tpbb;&JqL_u%?1umBVzIo!M*+CEP8=_XKw#TpTuS zc0QSiO}vG4Ctgz*wOjj&U<_O~L7AC0smrVwph>N=&{Zpe4rX6gzepTL>@qtVWII`! z!dUGr>R|WJbL?QmF0-Qu0vo1MPIX;09qhg|#|}p9GCOe9k#iR#={cJ_edNA4#|}p9 zGCS}tSzlzDR@Z6M@pNC9V+SLqU}tU(GU64C-9Kru(@WhS^ZDphU3yT*%lrqmiDMjrQ{hoHsV*t_ZFU&T6zrl!Svn7xxVw-UmmGtn zhxhy3qpsIcRZIdHw)T0oi9<+TW@QE!b^VQ)MO}?j2eZFdzeo&5>@qtB+#3zHYIE0- zb+G%~96K1X%j{@Sn#4ciGs@W#!*sCwyE%3+Vwc%b1jd05gw42WTy(H|a*iF0n1UTU z@z+vHMbY(if2+k#lh|c;B!QVH-QHYku#Ttu>>N87vCHhh?95m_nV!0&rVe(0Gsg}_ z>@qu$e@qv~m5@4tRtLM!%(24=jIxcBWpz!+OY9g>xf3JHZfAXN?3@K}tQKi!``Jd` zIhsx*Y8q>Qtzri!c9|VZU_^aj?TTv6Je}RA=h(rBDcIo+dwZ^y=!MhS{goCwO=6eX znF$PQijLqFJl&_}*ujV$V~2BGG?WO=ZTL=qx=+rtgAlvSj=?#$TWF2z=wtVZId(8& zm)X%cXCCs?_S`<@^r!pFId(8&m)TJ`D6s49rRFvcr?dO`96K1X%j`&yAyf_eT329q ze=)}n?C2U&d-@p&7O83k*E;1L3Y+8xnQ1k`B7 zRFwM&){1!{mltaJo-EVxc4(&_c*+Ppz6D+*c!w7uodeCPc|=A!-th2{HNd|km$L*z z!CY>iRHMiZNJ@OCUj=;R5$uTkFxTtn`#ku=gYuKje#tfXi7biC;tHD+f|Mgh?vkk8 zpQ&Bxcv6FqrS24x7CtdHr(KSg5Mq0os?Sy^N1C;ur4Vj4!>27Ou%pDlfLdSnjT@TMv<%V=82GP z9>gm?C)ai*hfW)47bU;KK}+r!#mkoP(8YX(&-HuY$HM>`M$I1mus_q?m-;gyDA<{q zBCBPJPrGTM+9pDOrl-fQPVLV$XP8&}GXZOOI{PzmAaV_xYYvLyC{9m?u{g%K2bq|) zAFI%|V#%M3*hxRlwft$nHyaH_yMZg7Ys#$->&N@HQDwkS%Edr6(833-e?oaX>6Ujg zEY)4%v4g{0fov_+?1j?QtE8mnC+>3$9dHKilRbJ%fuN6IqFE&A}`{1D5ZXXo#;!HO$E!}cE+EbQp zgiXX1uJ9oHeYOkKT;cP{X*^8x4I+e1Bt& z4x@d2(H%~tMot!MiD}2)sDY&ph(>$dH1|^t*5$|$Y0_Hi`1UZrGmvf&h09|9LrdQsYTZcKYMgwb>CN(?v$_OgT-MCAbo zO-=8GT7gE>7YPbx-r2CFon3Ri*oqC>bpwvH`S zCuSoFs%|G~E_yqUOOpwQfrc2J9W8 zW47~MYa%+TRBfP7u%5l_?9qXy&-{PGzZf?Ei)m@41%CL?|9NyI-M_|1q=VP*E$kxP zh1Fx-?Ll9t&Ah#GUg+^aw`BJ(*KXwsd^InWbM3Nh8bTZRl-2euKbhcU4c2hE&6?eh z5gZPgF3R*ZgGacNH(Qz&p6W2)&x^T%D98_ad6^UWX8UkfnqMamzf&%k8f)zFNr8o# zY=jij;Y`z3OVfb@ndHamV6jeSx@00tiF@Gyn*z(lsMk%~vDvb#y+9rg$l-Un9xs9^ zP2~qt=9vpTx2-|A)q6Qyq43hre%59bp@K`w1{R4+u>i=1LJLo|*c=YiOna}O&oa?Z z2I6kJr_mFp&fwJ1s=IkyFyq$1xQBCT^C;jM1cYg0RPyKJ)!0-#m?XS=!Q?RC^Dsm@ z-DExPNHUUj1~Il8_YE8M(Ma~=fpVzKOLpe@5}vY;3A zR4j$8dH1wL7GZs!0Y8obv+WODBkj$Cc=*CDg0kqDdGrMF%$l8@ztb}cg4=T0Dvo;# z2sgF0yoFi(jDIGBcYryF!krYqd-?JC%?wod6ffYohGUp)g4%Q0@$ahbq*8;49GSQo2mKnU=3Xj0a)N+2g8%VSPkhv5RdnTPj^k!IK>hscg%^ z2}GpIj3aoLw?$p?R+>)HO~zGE(}OS}*z;l1-J|dWTdmqmg=2|AvoNBuaVqVLk%gqk zGi5C_6SC{t?341N%SO2*-8VDJ>lbzrRPD@+5})7fU)+7x8zt&KLA7>NYdz3Ady*U0 zdbL7LsP!kZ5}*C@Hy6VLT#j#9G~@tlL$8__K@|A7QOYszQ;hPdm?1&JJCO_GBumGK zE*6hqEiT+BW%wlJ-xKgGH*(_RP|U_OBlN^d?q#mn422jqoe&L|(^v9_CahSW_Pe1% zFR^EC4TT~e8#4{35EN^jLM${$q$b&BH5qdp?6r!q18bM^=l49Jpg?<~!>X4|nF~Ef ze9xlG6CveC2WI&FqIB=2NEd=~otYV8-<4*Fa1^MuHp=m1c!!{!}uwyq;S^-Th4!dlVe|C92gANl_`8p?*9%=UY>ja$Xd-`%`@W4QiD z>vx#?Yad%XSbfa+w?^~I8;MF$k94PG*)c5eWW-=Ftu3sq zJh-;BOHlPeDphfb4q7Z0_lGSeR}IvXT$iolU57C2xocF~7KpR0O!EPGWq6>|9syrW z0DD8BccEVHRg!r2I1=>GGdf+wMOnC; zeo_AS=5u;Qyj6P1jsbM=oQY_X-{o#P>Ft-y`~69o?fQFkCzYv&GQBG3{Eht|_Pzwn zk*eB1Nhh6jdSek05QZ6g*m}~Ps_erulYQSS>i{wPzV87BYXm`-5fFW%D2RX{tAGL` z$f|&-peT!o=tJQlDkz{&#rfY#(o9z+l~fIl_`d(N-`C$r|LUA`?>*<#t$WWRc6hS7 zc-2%;HZ_W1Vr43O2ZiG$#6O!)k3zqfCcurDh-+PI`#3%~P0k zv(fJFr$}TJ4zBYFgmvaEKyba&PWfXXG*+I;lv*B1!DuN*y#A=h>T)LA!fd{xFQ%>f za`Ign^O1OEyofwZI4%SGJ@q_^h|r*j#}A08$?Dr)T`jN8SW?!!t70}QvnabkrQ=7L8Y|=tJ2b>LAs))xR=jZbHZLex4rRW z?DwA^rz+?=REi0Td-)_xS~vwob=i_uS}UrvRdD=oh*aWQTMgWJ$CFBjL+i|28)=Q0 zrc=URKDWJ&dC%Kk4^tKKP*g&E`80~jT3OOi=yExYS1UInflSB_nV@{CY>y(DMAo7- z=c`hsGP3e|?7w`*P9i4SA0EJd|M4)Y0v?Wvu{m7z^5HIoN;#oO&`-R*TD9&YbRAPk47O(Oq@$HP%rq?7nA6c#>(1t zNx!@cll27>GaA%@ji?Zl73Y#@RL1GcigQV01?{?|UpO&ApWjM#QrDr&n4mb9M5BOB zU(o(0WUPo?m-H;Q-MsDDyQqqID7r*+Ni^#AtSo!?;(UB5D@?SXzl{BUT0>R9b*O+a zhdes>j`%qo8|8v+LnI;+FL%I3ZWAUV&TG=Rpy-RZ;x)(0*!7w}I0%#R z@kJ6Di6?i!Yc66k;=Cq}^V+_QLpE|_#qD~{@BI}M_mQiqihBflEG90_YtlH^>WjN- zBR5veuGf5MCnn~@ZHi*@Ct?gI<0U3{dh(^odnP|OS)4RXuIK!n^#}eP{0sSMzKVYc zr~>%IscS$L0RI#;#pnGu?>oHD@(MgX?{Mz_a(}}8I(G**$b}g%O&mY*A?6HI%zB7* z8S^COnXDSi!dhVdllc_$hWTd*9RR;DSD7=-ZJ7Q0>`!LDKD%S~q*?Lo%*;zO-}NguRK)o_K!ZcF<=q z#(E7@7a-{=hVPj`)~kVg86F3spcLg)$OgM`xSiGbFyN}9DMIl(z^cKFXQ-<*7fIpiNRofj@EJ6Uhv)-c;9CLSMZ*! zWjH5`hmo{m<79}ip{>|BX*^6SgWZW{gP7~fN#S86t=Kq89E_|L8|Pzq7)dKOP6!Vp zX~o9bhPQgM4m{j1;&C7)9j-cq108s|U%=zYdXjQ49l#CkNy`2Fs$r1#B<23sDj0cB zQVsz|(vy^P37#5B_XN&o@Gz2|q@0V1u%SIkITzt!Bt1zv7vf-KJxMvA#=}S&CUHKA zhY95hoy9(o@yt032a}RCuH~GGhmrK&<$MAUBk7*N*+GO2?Vi9vaWJy(37j?&2FKEF zgqWAkY2jfc-4i%XJdC7!0;hq8No7O3CvfUS*w8M{9DEmcP^Ur$Hyn*foVcU=0nWC- z#zh7xoxyO2htg^tp36B#)Fu3DURR(PQ#sQo(kauJUZ2Sg14wU;5N36{b zu{J*U`dNiyLa$xi$=l6wa2;8Zr&`sd zcR6V21nqmC9+h@CQo@MH(kN-$?M~8f6REQr)#9QZ+$idN}qVP|A1lcP4x$E=--&?4`P7j*!yT`DyBN+NW~hy zVnv@_tU0P8e>o@hL5ljKrj|5Wj6qRbQck3$Qm-FG9AkcX57LqR)$F2WS#Gp9)iqD8 z-Evy%(WX`C%wU@GdPxT-0Re;r=X8*`Q^-eOYTQ1j) z5`_Wws&nQ<63$eujS47v(RM-kVmu~_r#;D9!X-^0@jZXTr}9CGQD;r2>oK!&xw$Nk z=3UN$v{8q}&7j!W>cBAwspf_HOhFuVI|{O8pG0lWs(ry~81^e{I_N;4Wb>cy*!*-{H+|pqi)}g8X|DJtnekl< z&M7rvyVaf*sbdz=o>#gE_#JkYP+M4py>e?aYf!djZB4RbX)Z2z4DLm_r5otQr-joz{)_xOLCislzm@-ysn@3-nYv=CHRYH( zdh*)Ik5Bq1g_Atai<~<-7jjZ7aR4i~|BQb!o&xv(5A;5*!H^tdKeA~6@MkHcXG|_U zj`WPliN}$iF*(3Ea8UBA=q-4h3=+zsN?Q$oTn3J}PQ>F#Lm^JU<47;Lj>qFjFS(Ax z<47;LHV@#2Uveo{o&S%x_D|{qoH2^2~hkDkX@x6 zkAsn3r5%TdkzJ+1I2c*Wb`_OrCq*>Qw|+JtQUNx#!|>WVPVA zpIUuREhDMC&OHZ5O-NS5oO?DNM^=2C`$;^GtZF#-tO4B6a^KuD@i?+N-P}*$abyLy zxjXPUvNGFTbTy8=q&4^BcpO=^X6_j{oQU+i-G;}Jp0`^EaKq2rEqEO1dAk{hBah88 z4d8~yW*PA~(%38m9!DCRrN`q)W3zMvIBChOj11gEXz@4@v?ea849LJ;o(7L2jrCIF zaipRod?cpO=6X>J>j!#;1@&O~A0F0_Ti$rZrYd$JAd!0liYkCQ`kMY!e24B#4g9O#TA zkHwsMVuTR4K7f-YBg%@Jc%O%l0FX-|xzyPz=ZT#mxK%to2_%=eQc?fFeO?8RgFxq= zVA|y())nWL@i-yax*@uDrA3slG=LM<>&BLzh%4f7G6f`41Wh34mxwFiao{vUZg6{w zMj|f1`mH6ENkl%C){P%6xLw6ZLdrnSu-M+x*9UGxvv{uH)hqH>TUC6-1K56M25`gU zG17P(X*@;>k0Xu8Na99I7LSp@QInF!S;Pl$!{aPscpPb*MHG)CjkAd0ainn;VH}P; z&f;Tu9BG_Ih=?PJvp5BhBaO2-8IL24vp5NlBaO4zhFe{-IEx^TnwT_xB7nz{#!vY1 zIMVnD-vDlS{Dc>eBaNT%;Bln!69{fr$l@p5*!}-UC$tRiK9g5W3OHZlDA?Drx6FTL zesS*ZIm7Hrv&hV=GpElSFn#qj%)gSq-_%W0hw*ObS-8)0TN5`=Xj#v&N-PHR0_HJ{ zOBt@-rwsSVHTk4r-LNKP_7d4U1Ml+;BvIL9+$ox6EW|{Hmac8oJ2h@$I$5@Rb1_+5 z9}>jlu4dHHQ5r&CjWyPEYpTZe%|a#Z==ifnYd(h9iY7C07tv1vT77+r&Y)7j>Zol8 zEXe9=OWFX7%0W<|ryYQ_nn+V*uXoC^jx!&3hP|!4&epEPvFxONk+1KMd`&OM`m9Ecd?0-j8+jN#91{nuIgJ~x@wWzjae5ijV71rv z$GoO>N~w%lid$`1fsMI@9tOlY<`~A>7BB3J)6X8nm$u8JhXR&cz%6<&EjUjmyWk8tz6Lrsg!CjB#221 ztzx9!X@K%LQOb7QQ=dF4?#~^FoeWe=dArEeQTy}mddU>ZI=rw(Xs9=>t&&o&5eRa{ zq9YbojxPx_V(vu!xii*E3cuVSs$_Fbi#n>WDg~ut)?|@c?1&r|n6ibcx@mHYV$A?$ ze!J%`iNb&n=5A?}5TeY7;BFyonSi^cQ3QxGt5Hs8pfpj>=?F1#z{qJ-{Glw)yH01h zKV}*QamJVKpx|_5#2Pr*n1K^q>dQl;`pw$NLo9fM$x}iFKpe~o(x`MpSsW54xCyYt zoFI+DHI!KnbApS2ALayURGy*CZ?_?5&|?7~1>i%YbPQ!agow^0dJJHOc?}x1Unnyp zc#We0E6i)qsNO=E)kv=)#J#qK!k*VS3KIv6oJOe@%Hs6AMhQI<@W8BsMzIyj+aVIG z*a%o+Rzah73T2kVtYQK1!>ocv@e|7YR$IjpfDdLBYa0CB_rCfk(OnNWu&wyYGvI?wPDXYQf!t8ctmz5Dn-g(r6~8QWETXea3P$RLeA2>Fb8M@D0()@`LbbP#O?>i>%{O+O#h|6kMh zBtg?jGJE7hYaG)!xxti>&Ur@{Pvf{|cnC3#*ZZ&<$NkL%Cd=yj;{TpV$ra}K`u_t- zJs(*8f3Z+1hQZ7Xjl0h-<~)wDJD0GP>MespSZ!IVRavaAR0Qk}tuWWot922*)*3MN zsy6S5QCgKnWwEYSB&vRUJuZ#uEILsGNhf6zX;>HY!}bKlwhN_L< z*-dlY|F4jUNK@_Nu5u}$b~#cKVKiNe>Wx}c zQWlRPBK5K;x>%0*l8ezq$`yoz#yx&F+DuY=q%92?Q%P^L6?YjeCRi1drLCTD2kxZh zvMh*u73XbMZ8MQDxr7mCxG1vdO_d1TN|=I+O1r(L4Z4eZgWg-D8hyg z%n;Q7hcGiduL`nWp;EN0lIMe!Ml}Pu<#0gJ$tkLN8(>jRh@p@(;t9s31z9|~W@b3F ztL}fk8B#BBIm`@qe|3H^=JziD52pUVTu%P-^BRodiZ!UV1dDP>I%u_*17=Hhxs*X_ z)s)MaHQE#zA0!Ox-Myw$d-C#28S)XmT(NAnBT&q(T((9eMyuPVRaBC;Sa><3F$YpA zx!5Wyx-=$d$<=7sePL-K9m{~1rotK%)~yb$M3{((&CM_Zc_XT7oMQS5HgsTh*!TZB zMwl^w#@xT=P6v_tr_TI+rVZlqoBX%=bx`@Q%KI~~%zd3(oc!Ztp7R1)y<>v$i)Z+jo?@0?@coS=+Ps?K_IJ1z6LwnC&|Xv;|nx zgZk||@?8OFTq3RY669zLu*OS}r7gf3FF~d&0F8^YwY@>vz9UUrfHhu%6m0?4cnOkS z0ccz#uJsZmXbZ5$OAx0mz#1<>tSbPGyVbQ`f+%eP)_4gbv;|n>B?xx~pmFWI)=Tg) z+5)Wc5`<_Au*OSpN>>0HMAv!=PNpru8ZW^~T>)ss2(0b(*!CUUx&qL+iCCaf|9BPoXZ^qTFW{76@6Y==13O}qC!ZNz#~L2`4Qd1v!iuaKWh(%;8Ua`*4Eq#y zqrUs^YFMx6#56^*U88KO!Q0h}*h)dYw;t2y@?oDBM8?;0ac8k<)o7amIF~i*W4TCL zsW57^WVW0>HwORLtOQ2i+>Ypk9n5b;^8MVY8V04QS9!VHF=87ZYEP_&=OG5BJh;{6^SthnT`A?cB)&?z?1}l+VISP7JF|L28WaLnJKarpx|ugtO&X0^iKHu9<*Hde zX51{}Bin_-|EO8ctkEp%akDJ?4}SYu*UB&JypXY@D%&(MA82Y^h-Pys7d*@_YYZiqJZQ+& zz_viD>8OGZp|#+FjgGpgEHkNrXP1~d$yOCE9WS_bLVvy zQ%({z3sZ4W(W`FHWrygp(k`>QJCdS4l-BD#t(-6`wMX5#WJV7*RN=fXm8!Vvu2?Z! zH?H=}3*%;)7+3foHOp(YlQZZ{PB<$y6-HeblJEK4%cj&xWF1!De_W5xsvd$-pL)YBRsk7%8S|1lxUVyDn)&^0pkdEf0HR`gq>!1Y3M#L|3f) z)HbKJi#6q=aIO%^>x41CDF-gJMNV}|rc;OzH*8S~BE_P|XAB2&bx}^yG0F8!xjP~A z8I+-dI{EHy85*O=a>akHQx@CT%3bJl0apeBb!a78yIa!mXLM$7VVEMT0s({06H~U7 z1%EnU%d55KxIV5hdpvbN$hp&GlPOunWKDOhwbkxo19tzvo^c~%zA`^Ew|#EE*{{wj zX1+h;nto_n%D@YL;7NAkYSb8uhawkBVl+&($axtb$l-^=z-JT`F(>rK`U)-3aD z%!2@l_x~r2+9p{bg(uGV(R#bvkg8?%o=nN3&ib3i6tRCvKi^?>V$OowoDX!$B6mlT zhq5_uQQ))qgIZ}r+Hn^m{!~QWib6;$Bo_ItC?B-~W=Cy{Gk!>!*~ru!qQWj1?X8Hl z&}o;!Hlz`DBr&@w?$DMqZYXDr=QZ)PIEq>@VZg{Arz}h_F=rv2G3J}$ zj3X)07mP+-xTG=WqsCO;CifK#`jRdsa%79(qgko&R8TYEa?CMt#t+sjeTj%StqhjJ z6=No^340NOz4hmJXll-axDm;Q6lp=UsW(&&hKS6S7pt0ObyDq8i0txQL=Q(Bsb)Ih z&=ye>V7dhi?J>$s2cz~D!c}o0*Jzi$zHF*ks6hdRFH+5`gkVuEQoB-~3Mq9WI0lz9 z$s}q79G3ydM=5iBH&Cg%kWm{YHFu+yuxGW1S0QxEgQdK`sBc1kZ!la`Y6WsQs?+-m zp+Fin08W^~K0=ujPGJSPP&%588Qe(Q?v@qH-jGg>Kn{`0S#LBdfn>QMYUrFMwMBv{ ztR67K6!!a+nT=Ff;&H@6T%`)+%vP<`9Vj#$u)@=hqy7n}KtN1C$c+GcOe?6*LQjw|I6%A@wQF=>xQOxhf(0-O|p z(*u+_5i-ga8u3;^<}2nM4X9}gH)8g*PFl8hY*J-ZtI5XUk}YF)#3g2HNS;QOfE{KZ z_fuwvvkw~eHh_I>1=Zf zxpQr#As#m@(1^_g(#ZPK+_5&&yvMnb_NDpO`y@?H6>3*pZUrKY%JqhF(kV+OVXIu+ zuu2>Xfi9&^XPaTUKqFHpE1;ElOzOc7eYPS~41&8AJ(3V0EaL%VOXEe#Y~R`N!t3n?DEC?mvG1 z!*j3CJvMh0I1TX6L38}s|DOHM>}O|-v&Pwv&irZS{+X}NoH^s0Id=LTP`m%D(~W7v z^uhes`1gX!{Sm&1&zX99>bj}Ulm*lYe4Y0I?-E`D^bFvGdVx1_KhAY=7bf4Hd}Q(q zliA7RCucb?aBk*&j+5lbIaBQC*w?eqVF%d9PW)ry@rf@@6ehMz?8Evs>sHn$SO{w) z^KHbtZV-4!z=^MS#?=DP2sm*k z$+${@jDQpGBN&$p+#}%lcZ*QS_=3PS0#1A}GAUs@Z4OA=|u7}4<;Ll)tL5p;%3+3_RbL#Q7|?iNHtmU#0B_`ni> zN}w14A6UOLH^~8<drXyR)6HZ7CO4HFmBvuSArnz)w8Ccy|aaV_1O zHjO|N*V46VaRi#UmVr&jjzAOF(z)rF5oqFC8U;tQw=qf95+<%COTFnR^5ubui+RYV zBS)Z#i+RANjU&*+#heo?kP8kG7n3L1bi@ea#MRujX~PIKaW!{sT0a6!T%TQ=4j+Lg zuKlh}>qel7tGR2_VI$DQ)!emsC_6xA$HcYVwfIqTFmWk&EgnJ+Ca&bJ#e>Pg#D(0o zcn~>wU>O%bLJl5S#X}Edp8#f~T52bA;cAs|&$R2KA7&p1{w#!Z#blb;xnb9)1IQ;{ z6eiMI{1CZZ1NySKKRI|n0T%XSn}L+kRHYWL*04Q{K%@{5`7i7{fP#brsEzxutpr-I zNHhqWAAmv9A+WhY7%Um2Hai3+9)!&dfekWUm>w9YVhD;qh=K-KE=&!eq=SMj@CHx{ zc#sx%05xRF7A6N!68Ru4&Hzd@L@xFqYH%jjO|X?<$@A@Kv6#cOhp0L0SY1F@vFfGL zAVADyXE8H@D)D5xS_Asm#S&VR63JrU|1QRl8S_uhUpZf$*UTR<_q(|}=FXW5%pE)X zkJ-m(zcgEz-7>q+%&%u|o%zHJGP7~|?dk7Ne||bOEe9R@o&hleExrNt>pOeuP7o>Z z)|7AR72d<(t3S$vc}(sT+{?KI?g`wP$zM!1im^_g4JI*&bXL8(}4eU4A_pv|C zK7}oq_~*nACcZF{o{&#)SwCl8&8o4~to@k3X5PX)gK1-mnFlle2M9U-=RovW_A>b9 zCwg<%-Am{(?32k(10X5UMYC=_5Iwp}is%+tw;hNc)!m*GeE{o*CG<%4$z)&3(7?AZ z>zXBW;|QrFME8zySQy2D=;B#dETKn?;y`rsm`EE&aUi;S))$u0^`kft-90AK;iDcH zSUbRB-KYl!R_M|t^srG63@qHmOX#5^20ie3%erU@{pctT1M0MM2|Z-Q1LA>o_~a6L z@Tdm{W~se|9yIEKfps`_3H`{ZK@Uu8VhKHP6o-LnEia)T9`(S$EESf}14cX`8nB1> z68fQ04-8D}wk34`Q4b6l&`C?^ej{c{G++;wC3N3W90u0GwS?|7io?K4tCrCDQ5*)= z!L)?Vje1~UrNJ0xM?5g_@yG%q&5Rnuz)EjkLZ?SOFz^YVbiU zJ7x*xje1}}myTLOxuYH!Sm{HT(8*B`46O8FODJd5EDbCie+gxedSE~`zylMbL>gGQ z{g+VIhzFp7b(mU0nWG*ce($hmmQcp12L?=(xpc-T{J^@gmQQ1!%p^I_h9Kf9aG5VG zpE~LR;wo^NFD{)vN+P0SFn_n)9>G93Fh$JIEOkaPAg&to+@0xu*k zGV_e3<|sUI$(Y-h8l&*UC1bXi>Z9-j6IxrUjlvUGjG0@ijxZY`Q6-qMrOGJ$!2HLS z%A@c^vt&jECGc5GqM{0-J}`rV;t2S_Y`O%65%7VB{*dS zd|=+0f|EzUi4)0qLvYdvIB}U6PYbq@4^>8-cg8(};0Wr(i3IS#2sm*f8TVq}|7_;I zp#DGaF!tHtAOFAqfHI`=l`^C>f^^0Qj1Z~;lNF8?kJ-%E6k-6$ptSjN!#Rmho|o8DS+~br&9yWpm&>Su(jB8o?~^rD zpiV!98r;~>u^Qa_qq1%dZV7X3djV0jI`RKWtD_NVH_Yk?L4w2z!2cf8>xO<}W_JX3 zZz3QZR||+QxUP=J;-y*&JcAicsSPUNSojhz)o2?F!*ia6b>QZsdAe7WgIkj1?NW{{)lBKPrRvI50Ixx#~Qm7O& z%TI@xblyR0_A(R=GfNt=mTU8Jqx*KOwM&>XZWxk*_`%p1b`gZ_Bu?EhJYRXRE6nD@ zA)&DAzOhF`v3Md}&(#14Auxz{cP6K<3=w2#VI!7ce;qdJF1g)*8;raL4+Dm~5k%L7 zryXvDYvD?lAtqx_9)gfPFR)wiu56Jo<_5a|%!0hSm48>_v2YX{UH32PLL=SvCfx~G z@iijA=za7xB9R#5FtiOvr6NHTqEu0tNw|b1lPFfGXQQ3GGPUeP>JCe78L>(`%67Sv z-}5#cZltvctG$7O-zl;>qN!vwoJi<3jY`6AGKi(tOcsh}qA`E9;t@rCF?&W{@&z?* zrMGPN+lq#~R*g9y$XNF3)46OXqgYnkm7p*>g)IO!^j`gc$;!r?hQ+O!uYupIufg6u zT@88HKETeMj|CRJsH#V*|ED#^i{ltEF7H-M9D+;X<)6@Y0qE zF=Wq5L-k4yJYT5ia+ofxm=o4>W{f#e^;Lkxm*DrA8K`RRo(zEYm`>lSa(Y@ z8KYjCG%q&{9(5ugF+19DG^k4;E^*Gf=T=vj>b?H9y$<{I^>hrbDq1;D!QfnO+YQS} z5!iFPRKc7uS}LfUFeptS4^%C7h0CBRi4u89)QLnoIX!6gU8*O&d9ObeF9$QrN+%_& z!-fv54y*s?W85>r{vP{Mc5eDWUV|^={ge0OnKKx#GVWn-VeiNLDzn5?Gru&!Wsefe}{T07(7{B$xnDc~mOzBhO2 zTyAa)s6_DF**j;?;~vMIoBZ|U?UQHo47@|Qf8^fJy@YvS|Bzx6$H`=|)~l8Ju&eEA zREjB$OI5A-41_3$iMeOzo}~(A%{?>s%p%7*1t@C@9s}q}1}Z7EZ264aTPhPMbM0LF z3SoUT>oCOJfvB+<)iqp7B6fmzJ@0y|U>5J|ysuLQvw1i0Zdd`^bj6ZK3reg-I+?ts zkxmmt=5YSN`2$rj8+3eljVgG8^Lx(kse)OYS2?ftr&&E#=KtaR&kEsj)6C`F%)6N? zc#?M$?G^4@ zVAec;p1(5B<7A$nnx9%BJkF>%bHAAT1ywM6?zy?=`e0S1TGd*tk#N=BL3AafKTnuB zHq*+qQUy;iEldklFpFtsnpb3Ym$kNXM;Y&gg9UxI7SR&KnBYImf0!zm#eazZ(8@HA zqs)Jh|KJK?r`h5J)!+i*Y^vqev z)MN>iIb1bYy#gL5GMoQB{`XeE<0x~vTCSEVc#^B(YWiSJJeN=@^$m3e@h5Gqyv<9L z8RVEnsDj!2Fh5KcJi-4M|6^3aEPjX|T6sr|lbL@C|CByq^|+biadliBRVn!VBtJurX^7*1JQ_-LWMa3*p;s%M$+K06dRqzCBo;6Pu%wo;4=2rA*oEWTG z*2;T(?D#k!I@R{>i+h}PPjHo7<%-PXz%1@o?$#BV$AP(9xLZ~Tj~gGCwLfcrs^Cf1 zeyshdf;p^xS^KVl$BE45s<^6^@r~PT%soB#^a|l|o5e}ao18bPg1MZxIB%^;G45T= zTE|+qLUF%#Z_yxEIpFQN+O&R#fs zAyx3??9SPpRKc9t3uZ5%3TDrqKYKn^@WkwSv*%F-vu4koJ-5GrW2bKRQ?sAy6IPE~ zz$w;ItfQ!cd8{K@M^XiISsPg!se&h23#}Y}N+WhP~ha#KVHU-~ZuU zs#vXUmdl00z2E<k!;cpQ%~KDuEZFQhZ!VT^n+i>_FI-nU zDoLfHhPa!SO4U`6L9%!|DEETyR*1Hm3d0hq_3(-*MudjJKcq$$j5` zO}G86KfHG78^|^ZAWWltd5`c22pVzUGWCPrl6aqM$h`AWWl5d5`e%7yfJOv)6y^oPS>XQSXNhC(gs*m4mNVpb^y|9B2AIK-YLgA&O8Y3@wW0hTe}j|DwI9Ke|+)0b56hG zx-b5=dZc6@>Fdw`{qo;s1)8K{6D9;oaf(Nf8fsh z&${x|gWvevjyjw1*{5IbTz>E&cOUns%fIGDyvsH8ahQu}Ao`@BE~-`KBf4`5Vp;HXr`ki@#rd-Q1Rc{J9HFdC&7l zJP3PHPnU2bSB~qY$#NwqXeY8!(Bn_!OqgWis4-)c zJ@&@SZ{4mxa>EawIPJ@SE1mP0%>9*}R}^+$S$Ns*MUgIHn;L?mL9s?$@;Z=m+HTi6 za%HVaCTW$j)kaDn(tC<-$kLKG#67}dLB0ItiQjzIZo77;T>aZ?Rj0muaO~Epa11W-Ce>Oq%Fv(M4ptvr*P#|o_s4?@FaW=pHSQ~D-G?UxG6Wegepb0zj(99 z-+TU{Pn}u6N9RDkc+gSKw?4hi1|7}(^HOt8rEJ!ggOy0oqt%ikSU-J4(uN?RJ7asDWjxJ%3v+M(! zpjW7L@{|vD8iE3A#Gp-hwdSZK;eypsfhVcXO4=?@kFb;fx#q*)dvo8@&dOhS=;i0< z%Igr-Wxsv1G4^MQxfUV8ALFS^hF&BXsUpJUy5?{PPHQCpXA zA=@e!8^soE&jj_dl+&zG`C`hDC$4gNox-Z!T&XGY4y(-Q?EB*{TQXPv9eU=KXAim4 zvG|0yHvd%XLmstx?%2Ce|NLK$TR-7NtzE+6T3DX)XtY6h+GtKxbc$9iZF1z?`n)}6 zGvqRgm|7S2gXS|(e=2_S^Y5Is^`ZFD=N$9*hyQwDWZ{2Z2S>joZb#pG;;eS%>7>Vt zTDpYWl~gU#lvZ*=6;doKLNc{EsY|P({&KqMGdgo3$XpexzzfUWC;Zr*`QqI5|FzY9 z<;`cjw&98oAAZs)*WThkbK|dEH@x_vlb_t^Ma^Bp#(+FuNI~%|?1b%sLe&L{;}$b$ zQ|QoZg%wFSAT5}r1*z8W?rX*RPapZ^Lr?ikOse?QYx+|T+w^tU);G@l$-Tkob&tNf z{mhdWyr`*5*wAq4l=hAv@cOSjr5 z>R7~ux-xIK2%%Q3A&rIvIf>0!S5=Ib{v0b0c;UC1>&`v=makn|UH;S`S<&-;dQ$$o zU)%EcQ>+_a%pQKb7u9tMS9Lb6TrKiOb1rQ~(h_I2_EbKQYPVA|w^^>HA+9^81!H_pF3{lfhh z{HSo;#)Bf?IPjEPKK{TRPkK>JmvE?{i8ylpina;coVAWIuCjS;o`y}_bcLNoov9S5 zXq{!fH>NK3=J;QwU!3Uq{rD-21t6=j&H^QDv7fjh+`h!W*7DJ#_zHZ!(rP9W{B&{B>8V%&*<{nZFc2 zb@8dJ`!2boIq5~Wb_vsHaM2_Di*x=MNKM{g%a(t9^W``H8s2#N^^erAH5PAa-Feax z`~SJ&MYnVb`_)#3&m)r8%)*F9)9^;E6>|faU?y4$fZma*Y|-bp$L;<=ukVh_!aQ`~)MX z^FVTf@lO10FM2|cu&!LrIIF=-A&pdQE=byJt95o&NYqY>-MVVt8nz%}k6G^2^eykC z;MXshK4@p_GdDa{|NYF4550KCua1@*wjBvw`O{ykzOz;AMUU?irqMK_C-KwS+a5dM zm(SdF{Lal~_oa_4UGuY(?|WR!gMVVY{@`=YKloZNdR&(oe_iISBNOvC{M_aK)wR)l<}25D(al}LG`cGE2>a%*f3kL|?2~6#&N03Ez@vY@ z=93pb3pR#_#h*L=i?zFjPA{tH5~k5Sp+{J;?Z;OtADlK9kN(rKcZ4nwJb0(`@vmO= z=#Sn$_vF$^nM}xw%DaSVq+$06f99|!wjFkh{E>hBW%=WOc=2`94I6DIzVXq!zH`&7 zUwdS~T^DZlqOvYw8d=Ld!spL?xP19M=ky1^Tz~GRmw&y_t~U;R=XTb;m;TLl@L#Ui zvL5lG(k@{diJ3jZmw#*D^KAETzhc+jXFYlIhkt&`^v%QVe zC0)Wa5@ULVPrFEZwvTi1MVr6%@vFYkSv>UHFYNORX?*y zTSSu86~l5MqNOh)W7m5Wkp^2jUG=J{xhElm@h{391BXwdrjz%zJB6qvA2SqnNULeK zxnXO?DKKkW@&trrZID(Pm+1V$Y6&#~u2{b*#yga`4rT)`1VSc9(yuEfUG6Lt*7@Xi zLsG0t*aAhF9WLtyd0jOa>R1hk!I(mgfF;(4h4D|yEZ@!duP)?;xvU@}6*zNwDO4MmWVm{O^u%?zps{IE_ejK5Rnhik<`r{Co?ssr4xRxFJF zrOa(qUy^}3EOZ)P#!xLL4L0__QI@9PjD<$W%POh?Y_JA`jK97|L#EO4vV^Ju*RGko zO_}QuGojJ*GKH!D%dVNcMVaL)GojJ+GJ`4stFGFM0JwJ5_Lr2o z4pCbgXUZw`c)+r&w$D>$xk_zmoHFOp;{dC!cmD-tRz&YkGN=Tw>MGFvlv&{vh{gc{P#`h3WZ3rLKFZuiDiEI>>I>e}H99N{!CBU-#7NXt?b(K-mIKarG z0>G;K7NXwgl=v2+ajt=V{~yM9h%vu??w@m=*}u&;X5O5sOus%|06qOOQ?E=VcrWoH z+!wj0Og=vupz)RTY))dd>Ypj0G)dlJx~x%XIKH8Ij2GzB#kBxo*3mD&I%wIR}n)}DxRdd$aw`VV( zHO#y*^Mx7p^lQ_foj#HOGXFxpV(K?j=S_)uzvO+Ax6FNpi*k>h{OROrlN&id;xs{( zzem{>_8}7wP2?v&%=#`X#oCwoZDy1?14ys^4}!+oEN7hY+wo_|3t?4C5y>^vf>yd< z%W6V)k-6#21re3Wma=l+M22*Q8kA&Pz~IHCv*;H zq||g&P3dMjE0<>tu*%o0*%4I~L8D!=|4o5xLK1U?<(8~k7Acj(?T$_x7No1rT2zpR z(IDL_H88RiZqMi>ULYV>10ze}hKx?y1p+cPFtQYG!{{VkARtczBTM1Zi%!Y~0s10(xW3S?=VRU#lS10(wr3S?;*VHw0v2U}S$lfh>(PH3Z~gU}PVoK$gZq83M8}FtU$QAWP%)3jsM87}-ZC zkfm{?g@6nUjO_O*kfm`>g@F7EjO_O)kfm|>gn;Y|jO@b{$kI4bLO|{XM)n~JWN91| zAt3VtBTFH~g3gH&0`e{}vJ}E7=o}LvAnO7nOCe-}&e;zFaxO5k6k;Rj9Oxh*;{qc~ zA;^KwDGdVhEikh8j{ihm@KZmgK|rjwFtPWv&4^qk#RM7JW~3sw zXOS9QZTfwwK&4j5dCW3{OlvO{qOioPDnzVlx73(QEsC{!+(;~wG9k-~MROSO`K!X^ z8Vs4z9i`qF2>MLgY_?NR!bM-Ak@v#8eF#-OD>-`2RFwGfe{__6|=D*r}2DS`HT6E$KeWlzOGW5ShQgd`1(hCN5x2CAA) z7Q(f9CEn}jMrde=`O|m|mPjDQ=5WX!Qx>uDsUlHRENyp*E#hT`1BSFsUqj`t7u6|w z%1~}v$`!A*lwB;E;FcjNbd+*ZZMFc*l@WN+tXlTPyv_s^&L?$JRlp`R)Dr%>bWsC( zdx3>n%^$2Lp1-GM)MNZ@D($j6Vim>ewpKw>_C(EMqb(U zHD4O!=`K1TXI|=zicE2#M(k-W!lj&E4NJvIRRdCorBGM_leHHG{1n}WqKAU;da#W+ z-^ir1%Nnl=glz;_8(-N6;YVm>hYsIH(6|U2wvFh=_F{^^_U1+GU5#_U9IiHZjZC!er@&NhWj;{xZCY;=#BtNnC;Q9GL>-U{87C% zT|&ZYk5iczdW!8hl+305foj|cjN2sM;~jyxYBZFpGH?l&4d=b0Wm8AxL-I~Js9;lC ztff7oX0ebiyJWd!GQa4}*i4o1`1X2H&gP8vVISBiri62c&u})%M3V{Cyeo)o% zQ}J!voCooaV}RSUoHn2|q%B%;MA|5M)22`$>&_VE;as7dsq4Z4xvea8)sT>Q`2I{m z+@F!aM1(dS>$Pod7zt`~_H4}KGDo!jA_38BG&{6|rUFK{9@ibj0oWj*O2*x^>!U(* z$LBNqomG3?m=3@iQv)%@Elqn{(n)#5fwIj}q$nLl*!V$UX)eQuAVQDb+#SM!JO*RI z>nZyx;#e~ubi4gVpHrOHz-47zRc{&KOv>f5$TUGHV$o-vLK4}C8vu&1^@vZ4_&v#l zE*MT|Vv(}NO<0FqJlu$*D#dQ4fh6E#JYPOejcpaNW} zxE%6E8fjRhP3LggGyZ z=qPQ7DYfdFMJKI$waIKNl+aWXCbM1)87<{fG@rExNMs}Cqf>;fkJZ)YdbnX#xf8Ak zqH|QEl!qWgRg&G-;F9N6S}QC`*P~Gc*2oIRbk-`cm8&tGNh+$!O67#uuV^bXo+L%t zNMKTefa8ZS!W!fe;*{T~O3yZmTiv zZ56B(rK1QNKL~PFlA>EhyN4jDh1v#}E?Y{+TaKt8mT{OZ`G8+j6)HvclGiVSWwKhe z(vY@E-U4!B&Nj&qh_LlYE^fBBmD*gTlx(*vN~e;rQshIBpj+YHL$DjPN{c6pKtXS` zEfF|U9jKgFWD})IRILpt@-YO0rDan*?IK$RV%9Lp5Qwn%G6XTY71})n;5emfV^drx zw-Pyv2`OqFzK9Gd$`y9E7LkYGTsRcc)Uz55nKcl13M4}y!qyx0UV}Lhb=O-RuOp`e zWh5!Df(YFz+&zSFMAxv}Av>HjOKcKf*pai_J7sG?(nuyUL91Ar5z9d|V599M8v=e$ zBqVud%Zae}G6b+ATHZYbQ&=N0$?ENtIMZ&pB~g#xt&jTDvUbr}GbhDKJzUp1g;9S~ zPG$?lS1-vb5Mk?EsYs)2(j;0|cQxoJH*zt`s{meZOS^|)w6`MGLMN26)^dVo$kl@M zZAc^RNMd$V+@URJ+)&OK&uijolAY;5ft}$)7=Vqhr$cJaGVpQ}>>ff~r3&QCR;?6l zF&Yk7;b}+mcCA^WN_0y0XsszMLuGxZElQS^B(hO^xygvI@wHXRhXCFSn|2R@M)75m zArQSKMcDX=Rq`PKr@FX%2sBbTNMs{=ONy}ZaU|qJ08aJT-9w;}2SFkm(M~DC#-AsV z4*{6-F}sIABl3VmHoUil6%=9bWe70#{eJ`_&X}*wy)t(u$p7Ctd*sZwW?a+Xn?8yE zC;r)>?*CO&TY0zhOx)jaE0en>zc@M1xtb$qKf(Uk#2+R)6I|9MEFtqM=4QqZ0h!@H zanwB-2lW#e3x0cDSPO`3dY7&e@JA%7nkHib)vYDDtkDvyxC8oJz$=1WZi#igqwSa5 zz!oS}spsO=4C(@W6o60FnHOll>2Sgwai){Wte|A}wgld6Lg=%qWXZN(k#4pO(o#Jl zrp$*>Wg&?=0W%R`W;Uv2nOLaMlFOQ*kkQ-7bRr&4)U7pUyzK)zR&<~~arH66$A z7Di2M9yI}$TY$dMsOd_+YMvMnOl)dlpT+P5XHH>O8QNXBa6m+C4%8IMe03MjDp;5l^y%}L1 zRRfloMbIeUNSWm@i%Pe%R#Q3k(zC{dEPbuJssV9xn5tRAuR?j3V0emox zp-~`$G9QA*h_Ph?8bjmwfHJF*8bds72%{oQ6fo*FEi}x0I5b!rS%_y0{Fyex#=MI$d(G^rvyRz~Gk>2lOdZPmGw=Je|D1i2 zz5m236ZcG9JdtEy!YlK1yhFIJb06k@fm`HixCc%Caq_{*&rjwjRa1{neR--ncNB;g zc#U&E=X0DaXDjCb_N(mg&i`!w>+@&KBlE}1{flqnUB+KG_2#r?dOiPb{tx(9@ar?S znIoqEHvPltE2oEEEVHe_Wxt=UEo~V z$~w`0rIX#ya}FSn>%iWJh6A*1*E6XCfs>?Cd8bm9R8^7#G?hvvl~m7)g zm(x4_U+eqUx7N4b-+$3-YgaGQWG?&xnPB_|qe zq}kV`u+&f0YZF3+|WOhrY}%H1B??edtTlWg2#SM!b7U$P5*_3kg8 zDxjpgbff5RrK4geKBy3iC094;uG)o$+_GIL;jY+)2Hj=5(12UA3-!B8cA@vX#Zzd7 z3-G+i#F~+i*lQ+4Ye%$p*V;XHq5Erh+lB6}-DMZLyS9G{&9pzt51-sDV*4s{@OJ{Fcch4>~ zd(6t!@9PA7qHA!e$9~iG9lOx4y8hNK^ee7!*oA)C z^*45*Uvho@6uPu0eDc~;>_YEdJ2-_doe@3PT{J88J>S=gbs&6FEMn>O@r}FhwhMj3 z?yuT~zJB*t>_T6+`%b&i*Y5tZUFd6e-?6p(&J}*;vn#9E>hDa}-K)Fiy8MquEA+|A zeC4x~eSw|-d$dO1PF&KbI|MuL*ct4k_U_yL?jE!8rqN1$vR`ncor3utg00S0Y_uyd z+C$iT*WOF^pT7H|y?@yIPy5-?zQc4+!1LD8KEmqjSH;o3!0tQu|8x~|zjIvGlhTH& z5@QHKvJi^}qm;s=Tc<>$sl@|KZ#|I+@YQ>Z^IFaxHmHD!Vc z=o%lfK==M)?=L3MfTruAM6xL8nPHY}MRRovbd_0UCeY=Jq^vt@7n*Tr?CSdIy-(ZC zm2-1;p{$#=tH7V{{kdJ}pY8pbUFaiwkJ#0fa#N?!rL&02*6Gq~`7ENmb-MIg4wbe} zmtM=ErLEJY*K(-1HC=iwolAdY>m#RjT?+m1)`w4_OGl8W+uwkAn*9xk`|NK(JaxJO z5nMVhk#6#|SWBTHcgQZ3a1(Z+L3hwDG~f={h5FroyU_dH_uGX&*Ztg6sEsws>1ral z6nc;S_5a=W*Z+6fU;po4TmLWBb#HCYt_612c2A*8ed`ljpRf!4qpd%(3;n~bKeP+| zgRMWX3;p=k$L&Htw)HW)(2s6?)GqY*w|;*DT|RK8-RaX@m-bmtT6@wdbm_eRTdr@} zg?`iZO}o&qy1r@``W4q#CeY>c)7S65-Y)cYyRWkgeeLdR?LuF(`x?8@pV<8gyUv``fbg3KOYkzO;%C)_DM%g4(_q)h1G8W*8~y*lvsq-Iz~2!2$GZ9H66*)y`fYnwS2x*7XwYA{we?e@@%mX za}pYWQE*V!=w7fDgXcSY#jvro$>!K&$d_7 zA)ZXoa+)YW5+Cu#sz?4Z)+E_the)2EdUxUX5maMJ~dm3P6i7vz_w)FV8lV(m)KRPK==fv~@m3*EuPI8?~-K zPKFSzu1f=|F7@jn)Xa9u|GzxjXrtfDt2JLC6yk&!F%Wa06s!hIiFVj%)Eb6>Ah1~D zFr4+D@&7N+)*FxqLA(#~@hF^Rn@PW~+b*m53XJ(6mFX6>lTtohiY2>V^T~!O|NruA z9R%Q<@&7N+)rh&1P0d3+cMqjZgXimuFii+bM5>t~X?fA@~L% z;79R7g%1r&|9^S54kA*{`2Uw@ z>mcytjQ@XmwhrP<&iMbAXX_xQnHDhtsIyyKA z2|45cU!JXlu#YqT|K-^_i0(M!|6iW1gJ6y`{{Q9KItbf1h$&cCzJSM#*<%49ZwLp`$H6FH?D4PL(16l4ccB(qj#OkH8CMb4fDc|Nouk zaR5%9Y#8(Zua;MKpi2?gr_5h|@bf*ez^mk|_m9XoVr)Ftze=;*JfU(J+y zoZZ>q<^$Et#HPSAge4-;lJ|r=e*EP~$B8}`B_{;PXNr$?D+-NiR!UV=roWr)t%5iz z-550CW;6daHo%n86*dKZ?-*TC1O`SD&LvhZG^2d7MJnZCgw3})S#RsO0!BJyKYf@& z!pRPs_RC$t3?wis`DPhi^|BZ)OH3&jlFO+>GRp(Zu@V$I70pj6T9&H-CFt-N>h#ni zT#W%lEg2OJ5P**xMCh)WTf%j3lF2VIdvy! zYOJxJn$GsX$b2)b<#zijGu=({D5Ee2jOf$P^}1oin+wE-@q8qIEaU_tT8&8jfNvZN zNXM5#B&bNI%26O62gIB9Iv#HnRB_l4qP|Gxuw=e3An|#a&)`K`Bbi<(kv~+EIzcq~ zyvZAyDv|9VsAp2b@i0^mB#dIfuMw4csGG%n3?4CPwK{AQ_PlOXbkpnpb@}RZMqc;o z2TZTC-&QZo>%e)hJI9YkUs))@C*iT*AI}&+63{ir)M|4yp4>Zb?qh|Tqvu=}J+v`5 z%3kjfpjpK4;rIeWi;4UDhip4S9A-78(rV{wD99KwKLQ8!B#+h`z!2}UhiK8?R>_+m zUlhroisV9ED@etFK-f_7jZn9&M8hJg6#RJuA_B)q6dy#YdKac@{al?g*K}3G-)iL( z^*$X>r7CPcYcy~{DzG)J-3Gvb-wtsGeiuhpD4wFTxI{=EkcU!JJ~zCgK6oOF;JqxFk?uF|1`<*o`3PYV9z5DHYi zm54VqYWWAvQVT zHBE}m(qQD}jQJAJ<X0qn-dB_MxhIMAZ_p835nwFXoHDMeJywPkm(uaK?ARYn* zo3q7H(M@mOa({9qy7I^CtD)5=y57F`#l8P??=5@AUUKiLyZ^ZR$nL*2BWK0kAKQJ* z&ewK6w)3!g-=Enz+}YUvf46^Y<&Rgswg0O9yY?U1f4{5mO1qxsT5-Q?|AqV5=5M=S z?#{UZw`cP+n{U{ZH}Btkoa^hZk8eD(@&1ihZM<*;+qlc*75LEl&#i0gvGse`zO(kJ zwRf*QxK>z0)^^OO{|~OdX;s|;c0Ajk+5TNNf3o);*TX9-uBSb9omx8pQ6B&xAcn!XFUeF3X-lJaz2Z6;K76*ffw-gSFk`?!> zPgzf1R}KKA5(8tF9v#9x@0!UC!9fI$_0xXq_M+!kW^ovZKzggw)2&k9Ifn!LnFu|X z-!ISNd>{g7DJfL8=J$?SoEL;~E|S~_1%;LNtgzIu3JJ+UuZWf1v5EhRGyH+3a z{Ol|a0U@761hj#*ylc%rio7wM@LW0m^(@Y;UW5!KDsy&ug?qMdfxX^poYbv-hATaj9}IyI zQcU)S*1&ep&&=XL5P~y_L3pmu_}^!700==vHY?3}$D3ww5Dr3MqtHx+t@iurIULY4 zIC8Gl|I{oF13|pp?+51ed*dt)Gz;Qq{bbH6_cgOP9|&Spg3MS$$USeE#d$%{r8KqPCeH_z*4aS#Z?bz@jFtmSy^3=YK2L8~pd*tuTl zRVyp&pZ^|gVE-V?A6VcA7PxIJFf%A%5bmWozHc3jTvumdAdVVxfU>@R6I9RIFS|N3FfWc_j3UOZ z%T$*!3&Sv<9xu|?yAm#a7Usje3|EO;2N#!Sg`wq|KWd$ky4o`^6vv=aHIubQrnqXe zFbo3)nIUU3-kJ*Ou5&cYnIQOg`qw$%2nvubp`;q z9+-i_m=~j&E;Yxy!p_1Fua8&EV|J@Sva{nX2m)ZVTxP7U<+(b8g8*{^VhDWD>NK98 zn8BIjHV9M-<@DUV`_;2J69#l7VraFU=jF3F*gQG%NBZHp`NBi9I0ytFoi5Dl_u$sr z*H)ryA6eUY$p*J^yy02@tMw18|Ma@L9$J6m+TZPjcAmKXciW%b{?+XVw;$L>ws*Ha zzx8`tZ{BK~Tz+@EzvMabVD1mQ-|9Bp3HQO~cQ^lJ^F5od*nHk5vbp2>oa=+GH@Q?7 z;rii?Z*F{I;~je++WUgJUx1mr1}|Lu=>Gfn-n`e^i|k$A{m$;EcHh1G;BH|T+TGgu z?9K;v-n64`ym%wK@gp0X>z^?-`l9wz|e#ZLU5B~kn#{zpFnrZFu!qwxOJ+EB3G%oi{Z(w!? z=XXJtz6JNvij5#pSocuxdXeSFC;V{WG;YT4SEScC@Ss z7p@&GYrTbQ!uhq{`D;SUS`Pt1v{5jkbA7yJt+#MZIJ?$^7On{`Yuklu;Mp-~@mOUU zgBFfeubQck#cNT^p2@1X^=JuvuF{|Igy%ozCxo|9fa5le% zW9jUiz^sl1a|g>9zHqF!Ot{SnCLu-$>rg+ln|*^#@43am`Lq1l={vS)mOndvU$_F9 zTUwmI0(sf~BdZ_z=QAGt%Ph%EXYK=WppalHbIZfo1)Xovy3ewpTeR-8Ea(=k`{ovO z=db(b7Id%?5>)FRrFB8KXx(R7&@Ec`Sr&AQ)_rpey7Sk4mIWQ|^Ge|gYd!q1F6b7m z`{ovO=db%L3p$_g{B_^+X8KXo+!955fkDICp?TglbMk>MIA5}y8!bFvnm;!>|9r`E zZe*TAoqxV$IX7B(zBGSsbpH90<=n_ThdTd!$#QPA@OjTW9SSjTW9S&7K=AK7W`!SAZ9uXUv{cm{YU`=OeS{ z9*fUgEaxZ-&#Pw7p+@UZ-H2?#viiH66&>p~bGmWU=o5&mAV=Im*Rh#ndPn$%F|tlr54hki~tKS~jOhu&gjh3qWHTtR3^| zSEyL)Um@FUS4@q}tW3>pEHE={F>@xH zp!4BKGQ-|RW*%OYndxw;l^lv_A`CKowF!tkqsGTA-_n}ny+SID7iG3cc`-2AiTSk- z+$BLWZa%lIgzHq5jkj@}7-Wr+nQK!s>kG_0`}7F_#6p>DK5srTN!>y{|2dsT(x`CRgb}NOLIw`SEA&xy5DX+*bqpe;em2_8b~{El zTT_tG$jsHLnY9IG1}&{Q$mfZ8Btm3Dw~?9q7iDG^l;a#Ew-YK3GpcOllonq+uhSs= z6p)E?Nij&p<2jtB=W zxf^(-)dAdF>Gqgl77Y%@eRE}M=GOUUo_mxs6w3xv{sI>vZXYv?=tY^iz_o^AHc(AO zWN)#S1Y(MzmCtR>oFvjUB%{jl9;b!Du+}4(NRT9?UPT6mO|x;sfk;Ql#h6iRE>F$e zTwrGFvx;OU9f_x@9Df^4=tTIU%$yvSfIO$daX3XtVOiq%7Im(flL|8I8?gk`(la6r+1stJU=qSW84BWjtFLnOT~exv{{^)(IVxN=8j)>|`K$8@1*Oo6r>o zfLEt}%YLFvXX7!o0tOOLD0Lzu2{aC=n3_K+H?mq$Yvu*9 z8n2DaT$-A>zQD}ZZ3;%4hlpp|Yd676sUPR_Sdd0}K`acX9DftjuI=A1tt2y=Np z9=wgrd|~tELaqtK;Z{b(C78d@dBQ~v0?oP2&+Oju0TfyAvc60iEKts zn!OY$5m1yVmBAw{SlKdH`1HzqS3H3GAKafXU-pyTUiZf4r#2tnY;6A6=9cRb*E?Ot z{vYqZb-!kw+;5t9?SIicwSUgu&hDS@zH7I;OYGjW^O>FZ?Ywv=w)42{&u{;xc~c<0 z{lu-WY<*zsRa@NF!D{EyH!gkX((5lh@6t0o-}Zde^CnNx6>#0P@s}I#S$XfqPj3u2 zgpKG1xc-mp|JV9k)~oBjb=TT|T6_CibM4t{+pEFVyI20}>YuItim52LF0CCv-X@C$ zfl#}rur%t|_-3}C;P61mRM_4iq9VCmUPFOum{Okb|6 z4@)Z(>4yV&STz4X9W(s%2QQ>c>^ zp@A=DNG+|PFf}Pll4?++)s$RGbqF$_9}*a^CQCvfj6^q|?Ks7cIZpAcNeTj|43=Ut zrK6Z!Vqy>|#LX?4Vm~eBaJ^AJ0nNCMcrqLD_4Z%jIK}fFr+D5ZMKJ8syKIz+Wy-@G z2>MU(%s@!+sWd2;ug z$F0LA9=9g=p*Jl0!%r^y!|Nx?BDD_BSHfBd)kN{A+wh*q$ts7pF&S)!)UF<>A|}y9 z7Hi~Jb;l{RNeYOsQDi=BAgEWNYlEi0&k5cVe}pqi6GzBI!>9P_K{f-hkN$nfDLyhz z0m|8KpDSmgC;~O4PNA5~HIcG^c*MZz02`-!R8h``eEs~ed1=xqIY}|;l)$oFJQ{@R z={6EcqFz}W8bYs91U+ey5EYRSA#%fMrH9jDlFoML;DB735xT1rgC&>)%g9&uf; z43TsvTB;T*34o0zK^=fdsRk&X`%hD_zH~k!g1+z|Np=lBN`=jz6^>Ips8FRGPaKs9_R*7J9_)2eV6CI6#T;K$ z(pXc{Wfg|$;*r{o)&z5ik2VF$C!ds?yMH&4;UI-`8Q+@7=q9{4P*Fgrn9<|0MkJF> zCnUb2HO>7~`iMfw032fB#z`!+`ZtbKeBE)1|8J6_lak?bM}dQ_p47{>g;XFY`hh|o zsAdwyBPx{Rh;l^}GeLUIaGYW?et?FN0gt*wsoF*>swBo^X&;sI)u4VbQ-o73`AFky z0F6aU^kd&Pk>Mc4&pS@>*3%Tq&}c-mUOt$rAzd)c^7STCRJ9|q7&!t>eq#>JXtV*d z&GpFy2Q=)bc&1|+_#}nS*JDV9tJVD!mC+Mg(Ht+sF>j?B7W2Jwg5|=BrZGemQFeaP zaf-8Lykj&>90)E2hJ_h@eXCOurUtD+&+SgNnbHlMu3$YL@th!ins5-*Jkb z;}qS~6tzr`G8fD7Aeuok};?o;FF*4h)S!1{3Oo zv|nv>21*6Mqk@l1umhB4bUs+D6cbuF)=IiQ?Ks7McAVl*Cn*{b#UKQBbdp1iI0nio zZ?cCNWh5IJWW-?B@2C45jrRwA|6~8maf(MAr#LOSdQ$Gec?@OsVfkd3&IVe=nuwlM zPXd0Rq{U%g*BBOsMStj?-<`;Ckm3W5Q~b^(MJ-AB;C!;}2h}pFaV^LPM+ybDRcMvE zSTRVVEf$B5n(btC?;gh~?slBwuC0~7TJf$txZ?R)&kNlD=ziEOx_@Nzi<|G;q&Dv{ zgXz(={@T}9|MS{!Y&^XF%Jo0k@NaCm{=)S(*8{H08=qczaR0;muidBiclQ3DJ$>&n zCSSnYcVDpkHHlb^ZMg;>x@x=@7fnlA>Wz^`WN&$wW&<9 zvR>~Chd#GrXDKbqMy5KR+-K0=V|IZsb{+AV+UE(n$^aM)=?$h zll(Ck4LeABJ%OGlaiNy69(8)+Ub8Vf&}~A#kPVo&7k&@&KPv>nnCR)zLEQ)fc1RBvwO{;_Od@U=uX6}2hee+0$Sy- zeQj#iMX!rmdZ#n9u6=o0sWy|&SL)hVrZrO_H6puTD*8Gx5Z9YhRek zEXPD%F4aoLmuTc)*PGyAjJSpWHHZP-jY zUmNRhn;O(+788T6|J>A|ywW5kQIT_MYEbJ$L)HM1a|l0OZy2!&XvmhL}!+%jc~DzcxleXs&Bo zPoMTN4zq`?gT^|0Y9+fFompv}Ikl4Ar1Mr<&zv?-yIEY{$E;^hUBYhSQ zYiRO8a)nMM7#6D5XFtwyi4FI(T5Tr2kxM|Rwasp9PHV$v_Gh)RacNo`Hj~cR#>Upv zpfnezHU$MuA1CuTG8jT!_D zSZBMg{b?s=GyAg!;ktX;iP=m#-ymG~Oa~pCSxg#)YxkN#LqfI?QYodxGVx+4XN~PY z$HP{>`+Rfu^uVYd?IpgEL2>IL{mS28WbU)I1>vhJ*J{h2?q{{N^1rUtmc69&wYBmO z4$XdjXS(u_*Q(l{{7F@>d}k`t<~;9u-&y<8g#Z7mD?2}NY2DRd{p!DQ>)*)UA;H^W8Bssspm+2|k#_c^1ZuI8Y>rE+;0ncvmY0YGNy&tD~~g5qHCw#>L73)9E0|qlx ztcKJ#Mkbl^uqG$etd|>|dQs9YLt{4XDevlt-g}-_`DS@ny(TS?nc=y7;ax~Xk*K*9 zn$1RtdOFZ3`+#68C8k7gfzRRvgjR+jqoVLc=%)F?`zgH9sP&{I)M!U#QK_?rq#*in znmOhPpPbYHrr!w)Ff4eFlVzpL4Ao;OCZG~5`a%d_70ey`a->70GhReufG!mfDVu!Z zqoR*kl-*Z@Q&)7*qHfU@;Zc2`=ZfAt(iKfg%)Az>Q*dk;%+)iUR_sJca}WVUI=y7S z;9j3KmDSy+hNVd>;EKVeXiH>+o$}EZx4@;d2*XF~b`9Q~E zvLnFgpj82brEs`Eh_;7BvJ>bX>p`-dim91YB8an42Po*ZMmr8D?H)_nBsBM$MIX81 z`qjXQy8pG0S#|Q*)lIqLdA^65KDx$17EJL7<6TSI#lMX4~6tiq)_o_$#*F6&MxWbVKvq8vn=*NAK1+ zXk5$NiRE2^MQ>ywccdFSnJ%C6`p4Pw_xt01*W`~}zSSwJ)yX3;(`hI2z4bZ-2S)FO zoICh29VHotLwXCZr@M%p1pPF@$|bd>F)6;&Y5CD^ELKl9%xJTl_BtBR_29z_0OE&Q zEpX@!fz?7rVbFn>^%V?nv{pVY>QR$SszkSmoWW>$tbYt3O{k9}8eBk0(I*$OIL5`Y z(SSETAaSB;bMPA#-SoQA`u}k&KeMu*-+TAo=5BH4KkPhVySw$NtslMg6Q0j|0`9kM zerJ<&{km&!qqhE0ldJ!wtADn+i ze`V>@nQN={*ZpSv9iz|0=J{6(eAcs*tN-e1Zhz&{sfw3IDjqm^f%8?(NqDnGK})6Z=9_ye^JXC$KCNz@{u7VQfqEXN^~u3;eqrJ{tOgjxny>eYI~ z=rtRTAM9A(%|3pl9r2Cv=eHN=Xx(kUdig46(eZHyU9~NRy_L3BI2x1{u^!c7x*YQ% zVzd@dfs|Zp2RgNOtb+oWQym{S(k}Sg@#jz3*O9g8_}GI~5Cp{nLLeRA0O^)z5qXyV| zUum#qKFtG2KkknVnn(|eF+c!U{7!ZJp^XC z4us8ni^Z<_T3#;HjRgv67)u0Cq?6dmpyE`=$BeXl&IiVyAGWU}W6|-^2c4u+Z`H!_ zG{)rWMZ&yO-HwOa1+7?MgC-lKS4|U&m+0hSop!3@qet33J2?J~+Sie`==i7umTvQ= zFNg5}iOD=fM+!&DVkA`NLU9o-Gf+@GVk5HWjl_kOjBU>{&pQBt^@N3m0%G-0Q<2@top82xz=PT{&NLh5e z`=Fup+s$sX4^g#3*eBC*JiwRyC#)1;I2zEYxMrBsfut`=C!Oke_eeYM-;O{3-oB10 zi;j04Acla+EDq(r=xq0`|IprJzE+lYA^-cT;%*su3S+JXOd{P_j@ zI>s$J?jA(COjwee*|I<$`Mq?aO3QsQqwPUdJpt?i{2#2|tkrl5Dx4m>cP3Ry={K;VN412qHxK%s1Bpcv%m0yw3Z_oso9O zm&Tv__U#z8=(v60H$rV@Krl(VplLO>kn<0M@PIT@HJuog>0CILz>7MY1wh@YUvH1J zI}*m9ihUg;79F<^XeKA2e7jYx274`UY?weA9en3AMNiJ*ap zoYu8xq}|hAI{tjMeH}v<9oTWpM*E&`A z+6IZfT+NMvo-v?C$t~h3oWS->ph*@s6sA`LgRZwJ$FXeXS+f!J?o)8Vo8O}UTp$^ z+o(crbCS9@ODe=lfp;5K z$Zbx1cSaUyyF&is4?VQ!zj1}kk>%Y!6>_^%Jd+w47v7RE`Mip|K)9#&7722+lNd2L`=PaoZ>kij#R3W!HLEjmv zq3sHJoadpvXD_Lc+jkuzUf4S1dqd@SMt^9#LhgCxX#M}B6=7xnWqbdy_mbW3?!IW} zJ3E8zZ*TXu{>N75(l;;Zo^N>C?ytL>n_t_kyT0O5Homk`UH`&*W$p88((30{#g)&R z$rt~;^r6RWND_SzrTXR#qoGOGWvEBJ6Q&P_2Ot3q(GnGt(xG-O1FBJ}PDoYcpjwO8 zdqOQ;LBow`loVP-v@S;FdJ5wkk#LKY;k459npFNcdxgqGWJ3wFwH6yxa=oZ*jUjvK zLysOSfOLz>30^%Cd*O&4i4=WI9SQW>!Ag+9VG2`=VMy%Cy=p!kI*q2UhhhlwX=mP=)sv*?<$x`{m8}ikYJ*qv-5Xl14f~3411@3XIz>#`V6_t|7@-v_^ z=AAC?gbZfEG#sEi-L96ew7Vu1I~@xq(4-v&?j9>3=tx8D4%!^A$jycU=5fBL*8_lD z3?{skACph~Bn*{I=E2HESHWG56}aFk*mtbJ1y{k|SbiuqZO)DjD;?`3RWE}aKTlu z;#h%;t%3(%Hddf3^&39EY2Gk4>7a#fiK>#`c5;~V`o$2TNKh+Pi>DV-3H_$zCYu?I_T5 ztbl`%kL3!C*8dx;pIX`Jc)sL%f%(f1e(rD!{J7_8WE5Ha+SOGj*WQ*lm2={#&Vu~0 zaWtNxNkZME_by3VwNWu+dd%0x@5rz!2Y2z#i*Bq>Lg~LhF;(-?!)A!nf^cK+OpGHq z3WmD4&|(b5;YHC^&9+Igzz>9mjF{IPvyzhK@gAOU;qgKoO7xC%xkx+eOARw&NF!Q#98fa70VTBOQ=BjAA*3gDWwd2zJ>> zk3IJJf{~`tQldSj5&>(0oEZe&vlY`gD*7n2*mE^J^+g9w>K1*`x5yWPJJJ^&+&R6Y zeG#4YMc`t65rP(m`RB~|d9cn_T6$T7s!UQ2RzfK6O|+z9CfH+3OxKLg$YD8@nFMF* z)k;G!I+~=LJ^YLAon-+Vei3x>ckJ<%_2gn1-9JfEFjiB+8I*9=PdYQO~g* zwG|x5|VTyacKbYoY< zF4h%cWFhMYm?I?x)mr^Qrp$6psXg?YPtM?jD%L>6bUR!V>*TNtRxxk$rd=_S872-j z^9d-fjWEqZ64QWubx?vz1&Zq@`?+Wn?I0B`f`&L1;hUvIKby+5i{X^cnZasGO(ujO ztN3G3wH^Y8iW#W}s&-w`EV}86TU&RT7yTbu*?ijO%B9Gq%dSVvxBA|EskQp`)sL?} z?D_Wg%GM)W@85dWYJWAo`n1)R^_Q>b)`4};+Go~&d+iMyZ{BEaL^dw3e|!Ctn_u7j zxcO>ee>1)E$lCpD^4jBezrFj(-FNLp_Aal#%k_TOt6VR1VdlGkU)cDN=aZgyd0y_x z?Z0aOh32b*ckO*)??ZcU-fMXPkH`HP_iwx3V7^0mzx#39U*G=t_QU&+?7x4zf9VV6 zyMj;Kefe%~>#nK%iLN8PJ>Q;Pw%@l-uU!dd+M$$LZ_-dGG{WW^*dfc&{MH92xD?rJ zu^~E37KgQZqFb$6aIP=g#C^#o?u#~YUzp&+qLPl3GI=rD6v-~*Z(H>9yu&8$mu%vG zae`ZFzu&Wo`}a0+|6zio;|ZE=W%7a|_c=l}f|l~GU9pL~Y!mk+o46-Va7)|EH*Mnn z)+X*N6I_K0@Vv;xnvsy$YbHc%J6PMF;Fj9&KAX6w+QdD@Chou{?#VWB_fBw1s!KPD z{#H6FcH)Byp;+3B+h-H!wTVM*;*be$X}#TJ6L+^w++7pg(thJ@6P!ORFaxHmHD!Vc z=o%lf*l+LO+Qhwof?L{e{H{&h2W;Yg$0qK#C%C2l@w73HkkoR(3?&U}*<2;nuctVR zUA77~ae14#oJ|~W6UW)au{Lp8o4Cvv7hLL>&$5YorcE4f6NgQ3@l@G}$AbbDi6bVsrTx|OY~mhRSy{dR zpQh&jc}lTaZwZ^Y_!t*lI*-_y;FgZR|IsGyLpJ5^*~IORaYTj;w3K$a(GAeWa5Krq zE&c9p-zKhS6W6hcGi>5?n>fuTu5A<7n&6i9L>nrdgH&0 zQvLYW)r7gN`m)!quR0-My2Wi(5ITQf)qFDPj@(yuKnr!J?5l#nsETg9u?n6~aD0<| z#$Et~Mj4)a$T8pqx*jkzv{wuU!weQLu|l}SC5EYVv6BkY1(NUK%~B_%-#oKw+bco+ zDqU3?P_cvdOxjk|2OSo<0F+D8LQSCP02V^a%^-^$vx#(C!bx1OE2Q~YbhxWh^q_{Y zXtyATn}K00k_uojOxQbb92b3b$Z_dveC&Q=pROdvl`GY$cFCcm)*0<_!C z8|NL)^oRwgkofH3|M|SfH|dNh?n4${#7U&OgStur+8{6WR51*uQ3xd!F4SoP!(3j@ z#|e^%Rt&Ox^A~ZvX)zB&Qrdi^K}FMQp_~h~dxNyxK29EH2Z#Z*^eXQQ>V8Np@C2zE z@f4NHYrcx%2eC##?}tJqmdnMZNJrKAfKF=Nw#|z;qoPloF*bEZ2Tkf$opIdnp6874 z9XT{QVEVi5oNhP|KP}XF7EgrhkXl2MpP4sQk)KzUY9_^0xEEd4F3xF#2W(#?f*81^Obe_$rQ~_?g2&y3N(I zFkv3RHxpPc7fR;>ea+iz^aD~-B2hyK*R%Rf`=Vbu3>-Ild9YdJlNE;$`J%;W*m?fL^%?2b&on+G`O)qMK!eUQNZ~97)l&1`U#gy;nC( z`v1{YoUM)btsJc6RyLly{>gP=|Bd_i?)}CdvHPdH<(i2L{5+~!|zzG!pL_4ZZo+Beo7T6^^BFRy&e1#NuT%;@x|u}RakN!kyh zJ^(;q5Jw>|E{nlo#j@3NhCm=MHdIqm!GgFVYcdfl)^pulpgN29p>sh}Iald92hnlM$Txk?*3rmLr5Q>QL7=9-jYsp15-}I}?XMFj&fGLN_7q+jHU;Wp*YG zhG2jon}dc$cN?YqJF|0n%(REO_WxNO%&ulpM{=z~akD}D3Ik%mTXk68)x393+@j3R z#4YNKY;-m6Q8&$dnb$f}$cG>x*!OGNEXxRlUo)`T*#fm7ZF%J!!mlX)eq4ysaxQCx zNj}3?G)w6(-K}mWuj{otP@o0W%sT3RHcIU(`%Ir*> zIUK=Zg|Bp+i`$tKwzVEzrm#i)ad$Fuqp!J!ou5&pUg%GTyHF~oM0Jy@-38|ForNiNH)@Vy@ zp+1ub09^9Sm2FXGXJtbm#Fr^&`-~+s8{*t^;ud9gCJu%WubAhm3FqQA=fo|_Y(^Za z*CW}vIp=a?y3P}KZf0lVAP7N*M4gtMmu_QD+@j3R#DNe3=R1^Qy*_E9?$+nTEz0an z8~`CE9~If3>r<8+bImFa!XX4?*-X{iA6^qTLI6U5>00N@%O?_-R?ic6Zf0lVFbKv2 zNh)CVDjVWf=EN<^Y(^YbGTPCAwW%zh1pecB;?B+NOq>sbeN|S?--Nh-m=m`svl($- zGZM3Ey^y`!n4bT2p15-}I}_)HV6-U6fu?g~es@mXqReK*A@!;u-NcOg@6Qu=Zf0lV z7Og0381p-m^Z$>p>}a0PxwzGjUz=6`;Fmkf0_NF&dV2Qn;4!dU+*1V?gf~LKof+Qf z;EA$3dQ%mgoaIelSp_dNz|rg-T>0k5(^5^Vbs^^a4F8@y|i<}F)sS(gk$R}HT6aZZR%FN zaY2M51l*Ysjt(BT2^e%|4~?~Xihbnxut9pjDTtBdoeku#_Nqgee5J8|@a=3&^Pu@MNd4KXPu zTZCV(n2+f#b_9Ff>uoHqjA#qs~KLRP?r z6>q7|@u(uhfh1nQ)LtTrMw@b$YcY*5&NpFV5V(0N4mjE_ReX%#Rf>k*@D_mH5XfmwuTkKV#MHd{wJM)4gksTb>0x&B_c$`rKY`w2E z*fO8y0i+-IM+QxERE{w~09X8XOs{I=h9h*LHyph%4v+3HpS%7~;w4$kMDm7?V+{HQ^jtlqUA!}SfG%Gu|)7hI*FYO zDtC-Gj=33cJja19^p2w!HktSqd1Jen%tMrjq=tC7Xg-HsjOm9|Q}Y=;0nemjy&B7< zBCX+Jle+mxoP60|!h2k^n3l?5IWq|QtEB2XQ=pR5hX5oXHw#J zl1jZ*3&+zKldBhrOi^pcL+yfAEU-axsC(5kp?HZ-9@gnQ#v8}S7fZbHLOF50FtB)R z)b)}eS32Q%C4H=xA=ZZ$f~rsow{*O$mW%P4bl5IO3?QQ4JP#+(GpI@&NG3{$=Ek4a z%qv5qkR4Q-NSF1Jp%^RVI+D+P9ShDe<^x^EVY#77gP4?Ub})fzbm*MtbUv4%*bMdgCHeT=32$#dUDTciVa6yu+E})1nRYSyo)=LRoRW z<`wYK(bu^%)u<2l=Np1>OjkQhks8J%LXBX)ZVou?QeGy>G;75|D%O>2FQWogj`w&$pWSGze`o!b>-Vg^ZS5JWzqJ}%`J|bN z^{0KWdqtvGo^o`MqEz$Mp!y)z=pnq&38uA*A2+g@UP9BEh7a^fg964e#Z)%5{MiW6 zd^=8R7(Y5{GRX&3NQ)|TsKXX} z^)eY7oNy;y@s>nF?aNbeYI~=<*))28M+I z?TJ&FU9~NRy_L3BI2x1{u^!c7x*YQ%Vzd@dfs|Zp2RgNOtb+pBEy)CN?O#o0Ug)02 z4ax~(+7C}=nUMBFQ<)cfs&XTlp!S%lOb6!?w^pA(`_U7bM;FVgaD#H< z?MF>TUTEz24Mbw?d!`~UG(z?UB7N<9~b#;3pGkK6B+wB^y7ix`aR!cFUw>JoqRNw?2RFDj**ONjb zi?DHwCg>}a5Db+Fe^oGAy}T$?*rMfH@b#8un$7y<)AxKXmz(Ak6V}YHj!wVQudZy3 zb+|8jkV7*l;Wr~np=w!R)TFN*J|g2S0&FC@9axDFkQjkae8n2wyP_zSP>5q=gGQy3 zZTp)%u|Nm&c&{pECEYL|RJuI=$5;zRle`^5RPqD+CV`8l4C$vQc87L5A!~ zqE{>A`~Z?NfQpxl3yil>4VHCn*i){gWWGXW^TSY@>G}H%J$H}(dMiXEO}bpwFOS4d zjdA+-7kj4CwaPFW3J1-70*q!^u1jjcJ|uE+13>vCN*xtxnNC+zN10guiY^XFJtwl! zSTzw%_V}cgdeg3S_f)z8g-QX6WU5^sMj~8Q8_*rBNQ-T9Al5UgR6|+ak80&;TE8Oj zF-os?VpNLi*5Z7qMlG=ZXo+#7vTshMr~7z@QKKluNCkgC2l}wQFG&Unpr2Pl%}ib3 ziM-g5$y$TG5+MAUgj^EioFeSTXcTJ@?7&2Bsg?bp~yZ#EAtg3-x=O~d0 zl0X6smVgpikOQRLZ;|;c{a!OlrL*CBEUIR6g3`A}huD-p+!#y0@7V_x;UtvHMh7vj zJ17PU!y^Drl#F0jNcg(hV44h|5h70k;U*7U(bGH^Zd6;fYL%y3gI;Qp<;P9kKW^i* zt$x(vo9mO3KmDK|Xdw}tfqe)|4bl40+rF|kDeRLD1{juQicqjpKFUCqJ{uKO^DQYTg+$ASKZ4Np2q}y3iJmn= zR}w-(428)=t5#C;YCaxbWa?{9ewi`F)>bE_d(wdb@f;8=R!d;CnH711jjKN23Di*s zv|nxZq?(@%3niS!tI;dXlE2&RCFDp z&$@+iC;i)G94n1?R!v?NAt2*LwO2jrR~spXIHBNpk5&uWKrAIw97CZUnd=A{($Dmk zZ$-?${5QVbefIJnwCL0kIyVNu!E>u4!WJhFj*jsT9y1+pjE*l8NnLY4Y4n=a1NR=} z8v{a5l=b)t0UBUdDU)fs*y^SAShZJGFieR+0p8q2&jv&XJFC5hR!t9cN;Fw7WmC;q z+!_fxW~Y%F=F5qr=d$h>8qLO$`FbJ_0swRZn(yWSz>yEN3{#^-r*1udcgxAns)bnP zLK(+=uo>^WXk(R0XYgj8EA$J70b_*WgVN%0AgoZa?ojS!B3ho7``rYMD*2nf0#F`k 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 z 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 z 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

oWR$Qq}a>lr0(RM?`P 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

Eed9)JkbMn$m`%vhuZE0{KeNCtvYq zeeyZlPJMQL@>XqT$FjJ{GoTCM^zGhC7MEG=q#z~Qr>xvmH{dsJFB=W`ZQ4W!kXH~v z5FXyR{IG42e7;!@1w@s-+GtBl(M_E)WtzTrpM0qa`sAfI>yyvXdj8q<$y>FXVjOU{ z8lV!yS2l=NH&|^cvv0I?4A(sqZ%U9UHr|CX#P_rT0Rgqr1J~#1WPBKTl4gwT*hpZl zw5HlnQ%%NluiYni|98+Q?|HL6`5Y}MpH-hoZ`E?n=9pZ_d%ocN&ay-t$nTjn@kV~4 zDSC=_7tqe2Ak#lgn8U zFK++vtw-Pg;9VC#0RHjYKmR_qz^m~UcOPiv|3|aiZ*Bb02VYv3Sy%u;vzOlgZUn-< z?%gjnYapkvoRu|5&zBSt_&tFlANt@+^UXS*fr$2E-GJYH;9J2*S2egl|Kwk-_p1>6 z`K2=p?nJeF34Hdb)NnHM(wm1h9RW>PfD#

(kv3y*Pm%QGQvhK@kD$gC~<)kdP!P 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 z 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]s@^s9jdA2gl4|_GI!MV96Iec0pXFzNxG6<%27B={bZj*AoIW-C(AyU2~ zLp6)CXlvdIDpjQYsF6okHS+e#_4iXFHmLo0ZEt#iFjltTCZH?qo z#8mQheH=%HnMGJ7wL_ssHg=|Irjs18ATas>tvfS6pjzzV9yCh{cT zeau{}&;P%5@9*s0{qUXtawoa{+qWZdN8h{oshiH}ubdj-4!(N)3&-OdpSy@^ zA}``D48CH+AcfR4DHL~MFxoIMOvRVGGZdT`q(r?bj=gzF43OdkEB1yptucVYFyS4j z7$Rx1E$+hL0~-c5NDbSUpm|Gjav0wv$zIaJ+*+m{$uit)_3||x8-=wB!MkY^Gw#CR z{Tl|gqB6^Jf)Dv&REnJ_slc`;mqt)^(3wI>dqKHTi3vL%VW!F9xC?_X-!Ld-SgcJt z8Cw|{b!R#oDqc>=Ea*{Dl5>+mJuw_U+aw7HsisNyxC?_nvSC0c4yPi9(kINiW#;*k z41!Y5WDtOJQta6b!DI$xUD4|FoJf-yau)`|MH}p%7&1+^$Ez+cVlSQ zwA9>SW@e3~VN&_IS@!yRe!=u*#TTje^S#XX@`Lt$F*==6s} zV(Vbf`LF?5ARh9l$v3>|WL7BE7IUsX?6U!y)FQjW_yd@vq3kRVa(yEg2_chaak`k+ z$WFs5%B%zeUv7)dI|3@p@uD;_s=}bm_8NViF8i&bpeCGR#Z{_X4jM>Zq(RpA+(kmj ztHq9cJe0#Bv<;W1u~OssVTEV`2ibTwtu&ilCgHRSL2g zQXVJyY7~#i2&8u7miN|Te`p7>6SER#hmF}-7ger2Vq696WCWJMHM`}uU9q}aqc>}{ zJ!(L3%lECt{@@N`yG0ut74)!e%^+_yYO8r|s)3lRnS6$B>-@Z1ALjbGj7~@xx6G`? z{^cFS))W#rutz?)i-yrgJ4?mocC!U#k&0jh;~Zm&J)Nkcc7q39_TIJFzjVe)qB|X` z4Yx*9k}>Qgwe4{Wm&ye{>Qy_nuA1-=M2zt6LSzO&^hBN{5Me}RlpPFXT;i55uEqYq z4r09kOJ<#cD1}`R?NOBKxNc12bD-O zV6#pQGJa*?6ZY6tO9Ueic?YR-%P(Gw{k|QShXU~gtwTOyy-=DSk7SimfO~zgn z+nuR{W>A>ZnMt1FmhW1N{oWnKLXByAreQj6qeP4}6uPKmL4=fpW}!(l14_Z5Je&`h zV$kN6?_7)h3s*Zy&b?}*)EyDcn@t^7GbW|JNPFrtm#+%gV8HbtC!ZuyqA*l)estCGe`#4X>v7W*wbh)v<7;g)Y&i~W;3h)v;n;g)Y) zi~Z&u#HMhkaLYHW#eUNcVpI4?xaDW9#eU-sVpBLixaHlo*pKcYHbnp<4n!L~Xe-yg z;c8b!nwUl0^7dNn*Y6-Uh3A7?-dc!fCloJ&pS^U9!|#d14Vg)mk*?lvgebLb$c;Uh(%82ejZS&*>$HOrN;4vnnOI zP5^2Eihc0y4PE6;%wam0GS$C{wD{>eI~6<7P>Se`iyBh)dI*8mn-1jiyT)eS40Zc1gX#u!+Ri~c!kocSd`4i z8w2@TGNP9fzt%$MMzd4lP-vFzyKFhC&zic*yUtALTVs4!8RG~c@kHJi>wI3KB@&h_ zlaI7|q$s zl@UE$e(8FWU+h2MeXrdnc@+P2t~(fU2I8yj$cqEA83 zt*7G^5n1=Ambkc3`L{p)J~WQ69^L5aDD*5KzzHpDH5y(~C>Z>Gg+iryb8tOf(^uO4 zcFUDSYvTIBmta^nyW+9=w-GRdk>YKQJ_K3You^Lo6Y@o|H{j+xHsnf4B&}U;OEOa9xDl#mzu@)PKvjxI#9f?nJoYC zO7XsSw4Y|UKCfvW#hz5Wr>N3Xlw%Xp?e|DI*7GMYbo1==2TyA!fAge!{7c6lKE4Cu|G#+j_m95h`cGe<9RBv*t@Tp;?|gO@K_FvVWy&N#1%ntAfz!vN}C@83I5vH!U<)U%>y z!rAS6R^o#Xt+;^ZO|faZGnb8d^&foY3IhT|X7?S`tM|1o2Iob6;qBNi{(}!*pr+Vh z{j^cF2MIu31z%0E|GG2ks}%9U0wApvF~yeb&PY!xqWxfgftO;h_S3*q9>f4|rT8f} zad*ahT=Aoe9!s&SyED#~;)eigrT8g!dUu9;w&HK!{SzMqD=wgUQ*00K%w?na_5&Ya ztcGZcTOC(#3|=VylS6d+zyo-z2+}Dwk$1*>Y6NNf!R(@nDK?mQhJ3P$%7ZDuTWLj# zo#&nL9%+T~zy&y~h}9{!qIbsG^37-u900a5nkhD@cLv*1K^s7=R4~P+_0CYwR>AF1 zbm9YR#RXJ5#ZLFmTsA5=eP9BNl}e4gejq7nvxTxorZE%BWppQaKzz+0(A zioNok@g7%+3BdVXOE9DXu$4-r*izrQQd=sa0;rWrq}XQP8S2?8u^q}#e4wnjfNH1M zo8OtsE3=XeFjnI;#chVGT@{zkO7;Wk0x!i5{-=SbJQxGKl|DSzjn0euJ<h$oAKv)p8}B{(&Limh-@o2F{Q1N9@FvK$`2`0*dhkwg z*1xs)ulKxNmjC+l-q8VQXyw`t=anzI>G74qc?zF^yL=zO`Z9pE!y)ENhkP2}fVRv4 z#Crg7hx5%(48k;C0e$)2i+0@M$nyo#|O6yuI+FT`U31(1M$hF zdm8V7w0zHs4QS*Y&QD*k*;vaoet~fL?iC88T-)I|^{ear#PU6juOM80@dfS<=dG{8 zP2nxjm+u0&E5+O$_f?9S!bhMl-+6(x!{P6XI=)sccli#0wK4)boCv?bdh)$2jbFlD zo?jsDa9I3u#1x(hZTUq2abTj)OG53u*bniVbMw9nPs=u(>chDZCQy@@)WRHC%T%wtj)~l;N7f zN8v5s3J~A@ZspnzC)zI%pSo5`RN_04f2F0tdB#9VN{b!(HCJK-}T7!R3f4d>GpD4nSOK z#}4-pE)XBnjuhSteR&%ot%mCkR}(IfUNu}(craG$|IgX`7khX6cYg5pf4kkh^_OlP z-~7PoUp+lJ86N-0@!cEt(LXqP+jZ~o|2X{oYvI99AH1;tLGbwR|IhumF8g=)PLy}G z_x4VXPp)5o|8bhVp^IkSyuMw%QM7M={ZD*Bef|BNPd}suXSZKH@1HGuclY*{cdr@l zuNa=5rwCbgLD2@s7{D3At3BzbD|&DF&h__qUq*6!GQR)bCAT8^WsgXnq}l(vAo--S zU(NSq#qh(0+iWhtz zne40E_4~uBLYb%cX_g3ErnVp%wuxzZDZtVTqnjWNCNn9}9yop`S@c^*< z*nhpV{{BllbA0QP*`AY9Y?i*#^Xu2Mt0}Iy{mu`pzW?*l&J53%^;IiAHYXjQ7j_R? z_plW=b5c_kTU5DjI6bn*iBaNOTNk5N=JR8}{#&c>-xyv-a{K)kOq{hXk~h+92w#wV z#e7~#a%Db0_NkApzW?KgcP9DXWpzdJv5E62%@*?ym`g|KqA%|H+T<%=4YgmsUJqrS~aTbX@3}L(^JT~Io!bZJ zw|{Q`*KdFS?f5oz`%Sn0pBq1Q>xZtjZhh!h{nqE*{I@rM^my;)SKn;keB0@-UVH25 zUqAiY)4}OGPJZL$e?R%glMn3wu)^#c36LWPWYFOoiEenAYA|nP`W1q$1`*35 z8gc8#HYMPOR6raC_0Yw*+{KE8$OxGx*Q`yI3EgkwDBa9XVrg1+Z~e%+#E>@;wSx8u zIxG2NJQr23UcfYvIYzL&I*M_nl28p`P^fq72iGO|nN$xF+eC4z1_D4A^3zeN*r*W{ z1xrE0X_aw|$rgdw^h;nGSP$qq2!X!9j&;?>>mxQiQ!B*A6=7Q^j=0=CNU*42J zro}9%ZCwuU$U;YA5J)SB#om~~u3C^ven;8aLRA3GBjqczH?OYMH8%r&UqVPR7OBQmg! zPSsIm)baaWynx4`xnA(mzS4px;edmb0NIx|CGwTQyo*$wA~DZ*^6fs&w5dU( zgeExLOzKFTuC#I80gD3a)W0Y}s?ww*SS(p<4=q|3gq9q!3q`Aq@@j7(g)AnQ8DG%a zr@yr+fn*CYH(FS9PIJY+C5nqF!FSrwOe(9ht|bWVWZKSGYb17Uu_*y*rZy{2NxOz; zn4A!fwQP4`V|~53XgIx6#~hc@UfrB<)XDUsgp9f4VJs7ZZDE2Ge;VR! zO)6u|Kqox`uV_T?7`-TAkm?kR$Bdp69ZYhve%|+#DB+Kv>?XM7`SZ+P7{> z_;R)@=<%-hY-@7PLDp#>u*_wk$_d$}kaiJ?wGo;yNx*4obwU{vP+S43mNQawIGPKxo zyBdXPm7Z0k0P72`mWzf{YlZlXu2A)<-avU$Bxn3og<*Exg-)ztLc?s#m`Bq1EPsbY!UCXwo z#aPEtsY4gb%4oDmq>??43wAsk*(}{Cd9j0>lA97^R_j7+Q^-wON-wl68%6qRqn~SzS_lNLPKJI(R9z-A z&C?s35~7a{yx8t#q+tur$&IRtD}pZN4AFBGKAU%{9mFiztr$7@6PpsFT1(?;SL<|5 zNTdfCThWlB+8nwC7M_(eHth?eq*382)xCeUDZv{#b-+7yQ*T=(%~A_ZCB8QqYiu2E zPlnSTHfs;;Tzx{hCvVx5==)&>34FAKk05hcL0Gj=owSBZaj+OVPUMXuI>IbJ@`$~! z&R$dY&xc&54@D?Ga?4V?SDM)zHz=DtlxVq1Z92@22z98CdAT$3^$kQz)k;iK=)u`h zz>RCTF&)<030B8P^q8Fr&DbU*(aRM})q{V2Q37retpz9X<6Nf-2OY-hR;IzsFvrty z!E;SvLE&Cl&KBH*4{l1Zy-wX`M>(a>NJS`*XTtV?Lj!HZ$F4W*`($kD+?YWL?eMEM zB^t#pmBb*Ja~I?_?E1q+mKp5wznq554%UOwm zO)HYXRo@z(J=#oa!|DR*b5VxoN8Pbuic|<@W`A~{6BNj@hjBTEa~ld09@h=Y%BO5mM3j88xY z!j>F#91zEco+u8kP??x;C{owF8qT$aA$S3~{;^F7EM!2c#BP*zZE_LwO<^8rxk>D~ zOkJ!^{D$3e@KAQpdGGLRHYEyZ$5{~7ik7LDY@&oXY{YtjgHNWJ9tAG?!_5pTu@2Xd z4?nsokrOp|Xhj(+$uqX@$km*jRdPho^6-+4^Ool+T@YY4SI%A+)+P7^=v*wNcKeoY$Y6h5p(2n1XyOf)cn|Orx@jln>DowoSCb=0Db_p_`Hcl># zXu$EY*&a@;N=Io~1FwL0nxHQ^LtPL9sNF>Kix@AHxKTa&yBiF@P*su=Bw`+w=zT0J zV$-mWv}G`-z&T4*r=zR)hvVt`tVK9yK%;2BorEA1W}xHJa3H;vW3$ri2S? zf=+9NlH5fcE=D}o3Z^n5v$+nB%2BJ(nsrU3M)}&wW*X#;Om|?yK9~uR5n;EuCd*Ce zF2nIjZori*>?|{F61v3K+ehLCLuXNPLC#$V7pqdQFiO~M zXN;Y^V^d;6FwT2A1wB zlFql|I+o))0zY8XU^1afh9MS6Qo;^?eI5VrfY`e=KmH5Xe;oYr`sei)NYer!N)h== zl%f>=;;U~tZAB?s#p!rjC_?nfU@31;bKUCcp#oF5m7f+WaN`Ew4#aY$%XwAcsxX0Y zc6&!;RqF9DfgAAG5~T>pUJ3imL@9bY@z)roD4#`ht8j-;joiW!g|at{a{UCet?J+^bCTS&4PeQHs`8-$W@Y zy%MD;#c+LYiibR@c%M#`qTeOucx_RNo-%fS7o!y6g#vupSm(8-V9BOa@S8B_=z6W> z2r``{b|)C#YnRHt<#5$uOD>H+jVMKlR;ysZ(FhkAHoyo@sK!t*jFdo8U#!6|N=mNF zDiex<%2T%aqS3);ErE`-c4G9}>^)>kCm^GUcn`(zapd@3*Dq&@9itSjst(5bvbij- z#`@R4xc_{wxo%A|m&LoyJvr9Xg_8x{mnM|h$_l5s9QyQ0EXCp0r%hr{?n16;{T_tt zd8+~r8s{FgfM+pK{Hb#fi-X0CB@!kc z=sMkI)LtA0t|!HXaInt2B8PaQZW-$$136G{~&RP=>{3^PbP=nlR-TCSX@CGIPCLv86Atd??+UWfuj+ z!d`nwYu2QJdk;aoDl#bY(md%$GDd1HSuIXHuAA5Ak;8PfUJ%IlV;Os`^Fzzqjp)dv{;H^WW}#==OiP{lQzmb8B() zw{OO$zjYd({N{;&{OiZF8=t!29{t*pef?Lin}`4YP`~!e*R+FQI#Bk1VPD$&H=y|C zpAUbuc&I4tG$)7C95B85803dPN~{T_xWAX?+~_U@UP?_M#i`I;2z*&;0x7Np?n2;8 z*91}oUwYnM@P|_qc+Oq$hf))G&Ry^a*91}secoO0C8-HK=Ptn41X4uGN^?ha7cIcn z1XA1qO>@)l>V^G>KUzpl;JJDhKK#*qY62-v%kI(w=$b%^OR>+p3y{!5Mq&Qmsyt@EOP2f3qK{hpk=iCMN)&x?Vnt$G1@V?Xpo^uyuQWJR2UGUyDffPcY zcNe^vn!t1Jg7>Tmq}Ve`v#qnsT=4ESffUE{)41fW_EdcMqhFkwK#J@cyAXI+Y62+^ zvF}3QoofOq9F@+p! z!}Ehbb?pz_whrEW>z8l*c}nz_`HKuv}p|G;4yXv&Pm3Lmdn>Qr(5oGAhFNZ zUBugkha4(I5V=!kfd~VrQ5#Q{YI#u?$Zjn{DJ9W0xzIp;aA=M;tLJX535|ycLV}Dw zjhrUWho!9SNzl>cjJzqY(jQkh!SWvQWD|LldT$ zseqH|&90r}SXpdktZr1E%RP{KzEll_=6Grf^)OUBlhRf@Tzl9IeR)G z7GcfoTu(^i^I^F2N#rwfHm7Z$Z%LL`o=v;4yx3}nlY5VG7#FG*%NHzmp-Cw3wHe@V z^UEILMIhZdRarFYO0TJvLhRfLa}kTxFeIan>tl`aU@^jMs?nx6gd0cfoJD1pE@%1d z@JweawXsk8NjUbL#h7d+TO3Zm_aTQ$eo}>`O3@uzlZK*AW(gq{3f`mnWaJfF( znIMLo3k!pI=!5G8SkMQ4$2chrH6t8T)jmk4GNsNYgUW*7;<_3pjt5RgF1GV09{%;m zICSKpUbM|}Z!tzlDGTb6tyk@OotnU=X2|UkF|pwo47_v1=ZHv|&gR-26LFVAoi2?V z^?Y59B|@K$r=wwE+(o*BfqO&HD3lE#xveV!H3_IZeb1B9BVj0(LXr*+H>Ok~Dc#~WLtPDRMbw1%im zpkpdsJ+u9W+Lf6q5t&xEOE{9g#bN&^9&zxMes5sFhT*|fVd6|=aEd<;QKsHtn0Cz4 z=+q;SxC)M-p7+^IZ5q|Pd}zvHX*Mt+qTVmZN*SDooblEmGn+8|(oE_GjWgL1`ipLj zDK~>&-S2ArV2i`Sryk>gl_^Shp}g&JM$#U-cz~A>s*x$ythnL_apF?Cm2~>%xrfhS z@Dw!nb4(~OMKLM(y>5+@It10OH!7lO4Qm0|!=}tLo3l!c&@S038Mgh}d_LXca6~-f zFzYszF%B;}N?eq^3ZoN{9!yP=prvjZLHkzC0Egg4DjuIt4RYk_nK~nvU`)tFHVRF- zVPCAUq$x9DCe$Ua%F+nhLC%~A95HvNdXb^P(TwUuscdn${*lKxP}z>iz)^tJAX}+w z^F?kba(yUK9l`>pJ1b`B!Mr~w8|PI6h0J-YZ3A5?&@&yJ{aEOB6iQPl=-^Sou5_!> zJV?qJ;%r*$RRd$lvAtTGuFe-azs2F?D;{#FK%+%f&O{U3_K0SmEhpJ(wPo^Jzh^6K zKpK5Nr$IU+bg}cUK1Z0k3T{O#vbx`KqX^455GR)*8*O%ISQzHo1=)hoLE~&}#tU7k z2ywSjQ^RgO+SZF3Kl6w~gLMks89C~=Gmmyb5#maSGd!Qi;}RvXJxFlC779pT ziqZkYbW~TW=cb`v3{;TnEK0H^ID5toD30%GDlLdADUWANcZQF5md6(=6V?dk;2802}j@%X;?>z;dze6lPR3zx@@b&@P3)%HlC%v#L2GO$~kzs+Ixhyz~FDpm;96}dBmd;J`>sI|jp#qT(V zB!*3B3QtLv7C9d~Pk4cx)C(j#E<;7z^`$`Sj0X0QrOm$ST1ri`XGYj2hK1@G>GUe@ z+*8WRylETV{&cp*;pn?w69)xBG`gL2D+25Ftb_2@f&^;@7#GhmX*OAm!L5ik*O2*QHlH*!I+=j+ppUyGMJR^R9f|P} z&ko~^)gUKbLX~QX4a^t`<`QTcId%7}+0+*aH>ouA$eT}GL*2Gf*H0gDfC&p8NCj}R zy*`yj2*b*yxX>69;BJW&gjs&rY38Rqj28U!#zWeZH=(>z7@8hXNJ=Osl@nZ~H0 zOFMArJmLUNrCAZP1rOXPfVvnw#1jkm?ItC3XDR}X+T)&}^<vUv-HOHW*SR;y0CLGqCbSkDOn&)*D+%fY= z@MbX~;KisT3@X({2CcfS1vBL=m-dE~5oLn>q;XaWeb!@wfxt0iyDiLZd#f*wAG6if zh@C3oI3%f@Td(6quj7F0casS;@Vipko{Q|bCEEN%$)*XDe09jUYuclBAY(GmgvDlu za}A0q3mBtFZZYXj>>AO@QgYYGGjP19!@cu@KviQ}mP;+1@0RLrwGp&RXfz%BTwusu zAs@D?r6`*vmFn4mu2hvS-Cl&DR#uAieEVJQ#@8KOgTea$kMHm8z2TqjfBXI$5B}nv z$?gB|_JiARx%J(*?%n(wH>;=r>{L4W)X9fVPLIFp#_!zlZrnK*E=98 zVCmYAU+dqU?|*xWwf-fK-u%^vErLX2bA;;|DK1z1N@ZN=*hn#+m9$a^&#{;_F2hnZ ziS=QNIv0I#Uo`^v83*PBWJ`yJ_+aeY#q4BUV)873O-2NrgJpPTXlKMg9rtxYn2KK2 zut~AcZHj~Mq}-~ zRU?09-B;mvBpmbs=|t8ZJ2 zy`Ka;zWyPPOjjbNT1@7wtk$fk;?NjinI`Sd-2mt6;7()BR61?}K{MHNnQ}yLVIX`L z>*Q@q4Z342%;o%H70VgS%pfAcTO_PKk=-*zFebx(a~jL8)+g(g()Q}__LuDK-Fm}A z9%yY0PTR2!1nJJINZc}lxev_=$s=+|c|0xlm=@iJz`_@C&j*8erV1)Csw)v>%8ORN z))oSK)YP$R2C*Rs%P!cik^ylH&RUejic(Y;i}fjQECPz&>X#$HWB-RA@&Ff~`URts z!#nttkmtyx((51{Uabb;?cS0-aHkyafZM@Urg5H8iwUgMt#L&a27NA+7Ii&q)KEN8 zvsPmWFUG|Qk(?bw!TE;R96$ERDo5{GQ#0HBey^Fhi&DQ8Nai+=TYq$K@5VoR z*d7IKfCW6wdp$j~$kTd6zyWOg^%fN0B(X;`CvW(4xqs>TN!~~YBafaCZAP^@wlEe&gF{OY>kOwh@)uUZecCBTx!l5 z)25Qc-Fb-R6x_;Dj5qg*Gjq<7VO8{7%_$$~9bZ?sn#=;@^lKirN4^bKOEG2R)eclk zs6n?&W$Qj+!r~waQD2u8!3?5Fb6CU9;ij0lCOUU#IPEgv0^78y6LpUk%Ib(D=Jiq6 zak8O3!>#sN*ehdfY;pYaZ;J-DeDqNH3HK>W~j z>}p;d+jM)(RfGmRsuYWff3Axy5Xy4_H+>eNs3};;R)Jr{7BpuD*G3J9pu}b|;t{t+ zoprrNdrCIw4pr|oC%V=-_WR;#e+gspy`9Z;@Qv4W_e+FfTF zAmi#e(qfDtPt?f_GE17kk3+N@wjGp|Varky5~fRp(x3M-a9=v>PI+n(EaC|_Ce&cj zFM3-%ZvBlrpV~!l4<5Xtx4FktaK1`!Q3%VSP|KYv+qFHLC81}uE4J?Gu_5*v6LyQo z&A$!$o#>z~c=-ZRw^)CXG^5<*=A_#<-R@2_@f>&x-Kypxyt*gZ+)Wc@I zda*T80$r_FNSk`1hKIii)|Zd`#941l2BvIylEXF&rZj0Mh(sU)I{?8$@JZZ;MiIkS zlUffsPccL8FzadHZ57NozaKt#{fDz2CgfV>nV6BXs&1pgWG)+!yvUF|(E_ocaA3%b zErrgsC%`9VQ&4vn)U%TsH}A!`qva=3zs1l}ZcuNB1GiGa%wnO2_rY6{5h?wa(B*4v zZP6r^aclc+ z_VLrda`Pu|Vuzo&@kft-`{cV$KKJ+=uf249eEROyzg>IjGti#be|c~D84#OZ?d5Awg&x-($HqvYSJeT|@vHk9X zvp`4|5by;e1WF)B-dookSQt7RUx>yTX$1&Wy9 zx-|%@2js?45f_(tySj5TK_MJ9$c|)dvNhgNJ;(Qb$2xHX#RE zZGjhSefLa^E5w8eDN~HNGEtIx+eY``D}Vuev^P#fU5NovW+cqZ&+N!qMa*!_AQ@DR zLO)ZVVPK1i<-^z)sdI0R4`vi-9onR%`e>Td$Wf0qt8mM%XhAbnnQ5CWM(Wtp&W52X zvMOPDAtxJ)UMb$zpKIT_w|DLH9=1mV>~E@0VBy6Xgh#qa@mj40Q>9hzKjO1`_ADU%7US1hrlqt3 zY5*E=Yfd=$hkJY1|IR}ml}4ZS3=JO$S+%dXLR2uCb`CbMHg49v{54hiFIY}pZx6!EOq!PjJ)HAQ7!AbSR582C@VuF@{ptd8qruQc#41XBD=!mLkc zheFV3M(`BwL3Oe0pA|jdrUXPNDg*T8`1y&ka z#Pm6fiqGb;TV-2Z*{)SB5EIUvZ=Wq<0gYdL*e|8JFwfcZB3&(_;@tOVExv^`J8F}$ z7sYmG>Yy1E=5%I~r_vk0Ey;wwjcwQr)o}PEAaQs}bHLL7*y`X7PVq!7)XT2m zDr}iftm>8>JNjbq9`dMP8m$623sBFxzMajBLoJ>R6R(W6W=2=yL6?~LSS`#L)24~@ zev>gJXIcfJ6rlmy&LaVu1p%QFIq3@|%=WOd++Wy{(2=AwN>@68SfYhmtyE>rPV0$z z?iei1|Iown!Dt1}igIM7-k$YbVu%;TIjv6UPJm_He3a{HMr)xdenLs-8QvP&3sJ;u zemH0&T~%%IPGpoOnm%2GJ}sdYCrdT@0|=b=DYmC&v8szQ71oQq%EnvmapOMVar+UE zB9j%Qno!JxSPUTcCZot%!>p4^1tJ zX!p@B3N0piq>#xIw$=ebuHrK}5*)SWI;1%V8_Oi$+xF6&ehsi6r!PFTA8aY0hUAdX zCYWr&b<@H}#blI`yJ)@Zk`-eVhXjdBIhym&!K(zA3-f&}nZP(*nis|1|IglcfJs(W z`FC}7nx5$%P!J3t10$}C<BV=oN5O$ns-OlM1ub5!d#NFK2ixn# z*cK7$VGvxW%67>Eh@6oSSU0l~E)@dTV)PDd!YZtz4hwf9kd#B{ph+7bbT-OX2Rf|r zN0>~~OZ5g>w}qryf|f39-h(Z+fZv{dmmd4MLcWi?&bwcr_P2q3{O&G2imhbN3=^%a z+_6L~oXYmGTz-^5$Yi8A5M@ii`U8uwbL&RGj6i6dEQ_C5ZE$9+$vDNoMo>a_AGlZLPcX$Y*1Hv^~I^MR3_i zREeU{gXnIwZU#D)_(%}FLZDQURi+ls9<38fKcqCx61e;`?3w)@2~HqOwZ7S^I2c=@ zduY=D5aK#mSsfuk} zj%p0XlTu*+UOS2>9EyRDNGD<9M#E( zLUQ#;nnqH+9^NX+g)DA^NZ;nWNxuIC^ZtqU$m&|EXkcZdS!S@I9aMR)lLn`^2+1YD zlB(pwt!M);v_et&XfcoE`{`V=U4x_uqbkfO;Pni#obHsZN_t4w$^hNTW%BiW^$1C0 z44YExc}9?jo~+z0?adrG6+E^p%fE)tlP|lxumn97 zeb585^OuBzo92TmIG`KAjx~UDL9~}Ji}}glI+{;-L`DwRGf6wmCGBdbrVR6lN#q3) z?ICV4ZwO>l23U<*djJc3F;55>n@kil*)os`0;qsKoq})}ar@)nGeNqUsoHgMJNZKn z?KNqlAlSn;kdx1G0~qmu6@oIrZKX*W5epM*!dO-tDalo+BokgMh-OkCu2^eU>4`qj zR3B*AuLZ}nM4IxtQ%XOK>1fuV+suNQ=cu2#!502>u5(NKH8J*m*V5xcK6v?n?N@7Z*C6#_{08 z!eNj{;{EDJpK{ve`K3#riXRy_|2P9{03OGWgF$!%#i3X*2>v^K&vc465Vz=*e9pw+ zuu?LV{nbZ@{EOtn$bGAn^jFt8XBH7bO9`y@?E;o zPB8vO;* za(B4|oVaINe;b!5MoyJWwDPSU38L23P78P_fmS`HKt zO266*s;Udq;I3i;c0iJoPO8&Ga*cYjzGs)X->-p7q)xcRA74WI3%CS`{GOVH6X8Yz z6?WisGpQ=x*28Gm052={xo}F+tUe49o~H~(#c(^zpkaLc3@gU~>IO~JJC6c?j1oYSJ5&*^jlq{Um5JI+%NIjD^dtyA+<%seixTQ;U6gf+`0DixS zh$WEjL11m9E9J6UvTjA)QNE{ zM=h3GMYjagseao#IhT0f?G$i{NA2bk_s|CKxpel5T~BTEc+M9#`)wYSyqCbT2xDtw^*IHTq4tUjdmox^)DEmiM^7 zd*8R7I>G;+`OVD1e_s6c+>^l{|7Sh`fwZuJK)Me+sNI1@HX)F12V9@n3W0PVcrX); z;C(={Oz8K4wz4&*@IGLhP7ZM|N8dG=+1kyh*mT)u_jBktW-Sw*A{MYNk2?soR zL*lJ!t=CCs5`2{!sAekCj;36VY{b&i;G}Wvvq~|V)=FI^#TT+hnH#FDp_6XcMWL0z zP0dTxLb0SC#40+Ml-)3RPHa_CFV08XLp#M*V}o9mW^}P?<;c>Y+`;47dNFAOgkJ#M zYDzi_Krsk_4}97AC{2zG9sD`HPzOz_4iHG6uCL|Ce)ufYcXS`tb;|rO{=d=>_krED zl^>qSfV&yf`yTSYyA&9MM=K^Ku=qU){2PTM@EBTnO9<~GLrFvwyh3S!7eE>~ENpnk z#f}G8*}Qt(YnMWjlrWl!D$@pb4up)295qah99#Eg=L<593XH597mxQ@ldnhujzplu z#ptL`B;|39EjHHghCQc>hRka8SfRk7Zqf8obmLgBVIZ#Bihy)x;Ce|i1p-*DdZL9B zNEnJTKaDxr&8)3b?1VCkXFJ)yDwbXRhTY(_g}ObRWia%G@sY zztZjYGe3{BVHmDLj{AF>(0NnV`M;3wp#qDJZT0*M3{p4l7SIoEj_3c0V~(?~{P_^M zqfjiiZC;mr795F`a_v?e(T#MT?-W!xnMw8&#`tLnf=I_}Jl>%&xtBd@$LzuFq*ugv zzL#xxvREPM9@8PdfnyjM$uxtkSZR^RG_Fu$b6rnpb}~Ub5&=-!ZHOeyUT%=e48Uc| zOdgh_e7C}LP`=Yn?k%s&xaz<$|B+kEjvey~+;?;z=61>)GrE;y-s9`c{Vr5DIocm* zPX7yenW>Z7`i!vMx!LBuE^tY3ur^sajd@1|Nuj1-fUn1SNl1&7CJU&`HbxT(6~}{1 zWc=$E8ROT*o%n6bB`+xO1jXjz=}%>^r-hmnIM#7ppG`_m@r7kAA&5+p}rx86KW42$r?1|x&qClJB}nmQN0W9Ja{4{DRBTTv&Ypp1gbv8|DT&RW)5At z^p*Laf`&zFR%T@%p?@tz1U6k8Mm$KBp>2IM6!HQR8uBzaotcq~&y3n!npi70`S@>&9J zDj-#Dktqcu!zMtPzvM^wR;TB5J4qoMv0*&2*&|> zL6SH4<5Z$rOa*f(dQYL2an-?K|1@T;Ja)Wij6l+*IL4T@SUul`)3s=oiPf<#UCtNm za*Zdmy%Zm4wHp$X3UK)DsSWn%cv~c08gG&&6Z~Dvn@4Z8Zvtt)z~TVV{&D)Td&oIX zHf7-5YQ zGy=N+xQ2!!1O22IHi%LUVhlhOWN4Kw^!mwW;zA1C^ja!gU}6*^C+vw z4*-x_3#uLKQsDu(QE9ayq6dd)kOo^p@t9RJLXxQNEoESIT=j8$V`i-cZ2gJPl6^;G z?8wRBJt^2u;U)_3SgWm8J`C^;!g{jZZfC;PFr%Y@tHygwTfrRE_Ae4y)CZ_CcAFJH<#BD`_~wqs=1K=;Rt&I9VvSo3M-2 zf;y@K1Vf2EW;UwG@tCbNkGn(z6TL_x5B52f4X{uFsBZ~MXxK5)Io?hr_84OvSAFb* z4_qsb=bL9&efzPblk&l>7J9+|l|HwR!^o|CZW7%Cv1nk+e>_>^c#-4tO53@7WNp+t1s{C_B7CWm&OBz2hvFMp9rL&yz_!+5b7 z3l<2GP8HOWOH|4Qu6xohYBh_;^pen!1O&V*Ye0ET!ZKo!iw^;KT`*{J1DlUw#X-H2 zjtwZeXW>S^rse2-luUP9E}crGsZygA;S2zoXeD#UScuuP%|pnwizO z@0qJJD<5BZ&PsRX0f)YO_~VDJow??ab|`UZ=HSN9~M9A`|#qk7u$>XTln_E>lWmN==|^JKQjNU`S$$% z=Dt1mI$v{6_Wf=y>i@m}BQxLfKg-|p-*@)r+1JiWho5s8IegCc+fI-;>%Ox`8!>B0 zAdoq11t^L$f=1G2+fpoN$GIF4SCX+>m=)n+W@v@MK`tDLuHH0dR9G?JXV9)E3wF`z ztF3mc1DDWDKEx&yoR)ZW%E$|)evgT?Lvf-XYb4uZ9Y7O=oU&Tt zEC7a@$Z*lNhBTqtM&q0@Wvut3%|ay`?we{KAo`FA@ZcdJbkh>r!Qx@C=!)td?IcZr z;XUb&l@CoBO-@NERy`fYwT6>3b562JJ4LI*AX3gC1Qx5-`dkkpjcJLC2R0ayWIGYY z(LyTEiz2~hY+Yc95N4*T-CQaRo8ZlAkw7EmX^FGVDWj1|_1t(%>qKg?VkrUc`ePV8 z7!=J=u0$~s6;b&ZpNo*HzOL&0aLTBg;O&H!Y6cP}cnwUsTsy9Lf~2s+day1@gyu9_ z^=eM?rmed0{wbp>wH>D>4bz>PSjPeWZ#^Ag8<{ZD^$K8-4IaAphB+Ew&Q1+rF1NuL zA+=;9q@WRqu6N7?UrRG;8;wNjdMqFnf@)L7^!%_Z+3Ttvdj6DA5_koQdO3*qhElfN zYox_cJE*phd?;E+t0Qm+zQs9G!r|8$XCJY_NQA*tu$IKLEiv1{O{AFamd$?0NhGA; zPz2Apyg;>Hj@75CE@Y>S;E9qe5vm#+LY4B!i&T4J0bubH?g-741l1&RK^qI$1$Szz zbJt86dA8d~2g-2XG5aB_*oMP)x!;4RFxrz+%{IUVcPMDocdfy?s=mvnj11E7qC6}O z0t&(p5tPmnSv6AP87gXFWP%OWhb|e;x18jZk=tM__On4tghrtuZ)Lb((`sd_MYUc6 z@23&~5?iWAh1v+g`KhY2=S>-bi*__kY-4g?7do+We_$}lx?T*_r9!OG+F zJ+;;4>p+sd`}&SHC@N(Hl8_xkDqS?5Rc))`!b~lcF-Q-xS|l`x@`W%|A?0;dgVO>@ zx|TB`$;}OAyVKzLVzbVKJG!e3qUClwg>}4P&vgh2o?6^&ZG*97TfI6Q%!bq`?7&n! zh;ng;2J5~mLWK)vc36}_(t^A?)pYhDQ%1bzkOVlZ_AJ^h#Q32H5!p%sBfso~sb$~aW(Zkut{7bAh0hSw$V_et~53Vx%n4P8GDs3T_f^FlY^yv z!HQEOPY4AaiVu>6ozF&wqftq1t5I*N>eAPyj7XtWW)eI^T5v_Bdc3L-rH~#N!J%5V z8S+X=v4%zXoQ%fT>p1g=DI>rJ5i*F_9Z{|vNO2{`2!-f&n}YRpHmPyxG;0Si3Sjh4 z=N#Y9Hy8t?hD3>Iy>1xZsFOiTBU(|bv}lYJ%_NdBF2zG>P9~;)ymBZl5yNF||X}MzPDs0_`T= zAM|4+uIZhT1+vdLc7V!vs8|q|i_=zJ{_zH5n&N<|@l>HM1oCKy#sZ^4whB=V5}ccO zDUzuaM;@O|OxF}r z&N<-QU=&f339>O1$^G!fq%KS4$ubJrT5meb`8S4+BB6|=w5(Q& z(V@tQxpG3QgN!#y4$EWpDrM!hCKPKXMSJQUbH+xANJ`F7oF_{)SLEwlKF?&RUZmE@ z)ly{-(IaMBq52h!np)1%@&;opnH?ZZTvsex^6DUiQZE}t(y@|Gh7>(JRH9f{E+zQR z)N&4fW6GGN*`gJ}QE<~w2_!}%Tm+dIhNWN#pn5*_;#46clSeV-Cp%+b6O(UF>Q9#a}N?v8CeqveUPXPZ`CN^?fIS!a#1#DVyGF_gl*bTpQkG8U!< zMpV5nw9Iz?x1-QE(ZmV`Q9- zXx(sGWCDq#J7qk$;m5F;&b1MYE%)LsnI)nFtQ#xARp7OBqnD-{F|1kC3wqy~j#cyB zX^CLiZD&cY&Oi+S@*gayRs_I%RcYIh;Z7C26ax7WV(}g%PyKlDzo(4BVK^$4g%l}k zq@0Te=utmhs754QD~-}>$F}k#wY;2_o4SCHBAEbGiuSEW-68s;YMTm0LY+=K zou8Wc5&)W+xqkbN-e1=*(^mBwWI-30tOfSjnI0MK<-0~-wdpRGGea%_Vav5h%d>_X z3+uUeOk34Sv|9prA=@H{Nv99R;)7TnEs z=w?j9G-@q?U2o<&LGO`hLYa2s>JP^G|5CHq%-oXyKmD)t^ZuycH~XpC7aqRx@Cy#3 zhabB7>(!5}Ua>k{ebCC!S8f1#0h%l49{TS?Z$0$HL#0D!g1ms&9kdQ64<1_n%JOTL z<>iZ)=MQ}Dz)KHM2cV_jEq!w7`AegvixzKR{J`R~7Lmmx3qM(S$HG$=>I?Uq|F8Ks z&tEp5n?HT->vON2)8-yIi_Hdnzw&+9_Z;8AcXZ}wGw=|K(dCJa_5* z?ABMHLcz^vWIpj39|_O+4(`AN5cD@a+*)Uv`QnaDn=ds^nf`M}rp?c|r%X5P$h7&P z>y+s~c4XSRA!fd?Bh%&!WYdy956?Y%-nVs=!&_Hr#|95=h8UQZ^SK?A3kA1U&bPb+ z6P(z(B;SD@nKnQ7URTezv?J5z2eDJ8#T}V8KN*}dE$qm&wYmD{cVybyTzzvpGHunwGqwSxg*oo1pq7iE^&OeE>DsUD)U{i;!=atZ32)Q2 zU)_;uo38!Jj!fHh?U#3C+Gc$C((<9t&h%$KI^$D)Ip6)~FPQt)+^6TRnzQFBb4UEY z^xp_#0;a#@f8gwGv!9%O`K&%$m_5h$i{;YtFPCpze&B)Id>>za<+6FmJybh%!NFf0 z{Pe-A4%(~#vHH3LpFHsL1Nwo&fpeCAvGnn!E0@%z+|vCQf4=z9#g{C~tB+r8tv-0= zcPpP?xn||EmBwOj@tlR*7H(X)YQbG-_^v$kn#0!~e&XTI;fJjL;b7(9k%b4%|7QNP zGe4jC=;8mIf6cr*Upr`z%bo6SFPdx4&K{{)$re&nN<*^Mbg(i>^*8Mk7xV>vucX?7 zwoY2c`v44(!YcScPnCy-qOwWq3bVrOm6Tqg_ex5wP*Yt|uWaCjp~jZIG)UEMBbyaS ztR$vVNh3YTIa-yl`RHpfyZS1UUPtumW)y)2x)aQ0qF&ssQZ zgES?8bOdY#0BRR2WxkWzq^tjQ|LJ=rJ?uZcSJD;#%3eti`Va1v^nm}sUP%}Ii+d%V z_uth8yEWFVe|9RBs1F8G-L#W#(^Ks(zZt-L{@(NV-Ye<7=I^yv($nWRdTUn&PMbe% zuTl@sZ*=UgQh~wlRqD$8h7Ig0_0asGy-GbezhN%BN?o2`UYA-;^(4F8vU8qfwUngi zXq&uX{+aX7+$-rb=AW@w(tn))$92-(M?!T$-7BfGpzM`YUXcH8uIcj~sh|}+o6l7g zjt~aIpq$?f2zdn7#n{5=vLIkHE>^Um8N;R7GIN5XT@-6P>S=d8D6*HN2QW05{` zs;g+WI#fPKmk7F;Nz%pMrgoiwB8#5hSc4vb^nF? z@0Ij^3-?7mt)`EOU!gR2|! z->#&~tJC?fyesK})w@e61@_2O}|>KTwS<1hG`>Y7V2vQtvTl$z7*te0QO-Fqc{*Xq0WO8U;#ckY$+9jouyE9u);-@Z<| zyJelWy0Hx1)d&x-PM4v(JNKhj9yQf<*Ky$QSN?vlq>o&AW=BRST= zLo((T6N($sw>b@kHivq)quU(iI+EiB1~Jn`iK4KKV@f@1lZ&qdT5JuO4}~vJU+zsR za^|=qJjhMsCdE`+sKrdZX9qJvK3hU#5XV~8sOq%Hy4MSGIG+seRl&W|?Ou^FRM$4u z2!U$2C#sPUqxBr5YW})Fso9EEYq$Ni=}lfKWdYmZVGR;Qf3e)+(ucjT0ta9bkVBgR`;sjc66IV zn!8tHN3()Kf7mDbgG`jl#T?g5B~@xjz)+u7A+z61IQeKO9IQn(kWV|wq=aa+xK{=D zO1FDOcIXcX^oMn#KUx)l#nh9FM!!}JGG(zMwA}(1PUU(MQ44iT%^X#52x#Ob`0do& z90lHnF1R_0dqk)Dn3crq-4f&F?Os+H?#Io8pg+tL{Q-4GT20|gLOhbrRigpKf+#rG zG>Zf~q$|O0u?p!yI7f29?J9CN1y5Z?w(pN%;&R{R#sLz7nrAMZK64aGc2%`p=w`S= zu97ftI)g?VCCTnZCAU0~WXMi+v4Pv^MXQ(G4NUCr-|U`dbF5luJY?!8+A>#5LISR& z>0lL4X#G)?7~qDH*J@qLKWRf*^yMgyImV?j=t}eZf^>2J!A&spe?l%ZP_d~ z0_AE4!XzR^aP2ZxDWrgv;MJ-ykb9+SCKF=HBrG+&;w7QYlhED2Il@v+ry$mnc)Oi1 zAXSdq94VZ%<#^myPqZZz0?#fQy++K&05JNfZt6%mT55!&Vm&R^!j1}ZqmD*>MRn-y z+Gz8k#9rz4rtmgx83S#poM=m=-%RxgEhEcNVI-#YP#bMlM6*~0PyXnb8}25Nb~|iP zyr^Fiy4yr^w{MPQlc^R*49tNkR+&oME~1-!f6|uYF;_m(mK}DSWG|N_3VI@v3pNPw zs?6?KiD4*2*|-*A`}ulP75LC_yU8QC`K)HUw%km&H-)!p%P43|=|o%BD5Yx-YIwN> zAo87f$D&#U-Ks|8bU#oB@Aas3r;6DF&f&9{gf`D&cmL){u2?rMPpL9;wq;7lV36FZ zEg{gB;)%ADl4_*{9!X`3LA3yt>O=}^1!x|1(Q+bIvr0*DEnSpxNM#4xwbABN+P%{4 zP2p|YG6LFCIMJ3-X&500+f6fEaoEe(yH$kE<@H8tNO>I+yhzQq^F}pXMstI^XSlli zH%Gi$N^zA^rd1VEL>;dPo5NfayByCw{E4<~r^NUu#+pq*g*1>-jf&C=-3~&5Xdu^Q z`wn>08bU$=Hi^f#Yok4`mG71hYHtc})0X35pF2^FeWZ-tt{Qh6MYkz=Gu`fKWykHl zov_PiE@vkuaTdredQ>Y`)9sNL3WORc@P>{#01sP<}|bgg{bGy#~cYnT}|m8Xd;X1IIys(kJ?}@4Gv2l?ez%c)n=aT)w|k@Q(4XT`pE^+uaMaVG!W@Yt`Le)A`)R_4<0>(T zcucA*R+5!KFHowwX zDNwcu0`4}tATx1%WR?oazPuGU$rc!-n{Z0*AyBU^6ak<>;B_NFCH)gjpg%AEe=G<^ z#{jHb0(CW9RhICU*c@XE$7+Px?11FiO&eqxtY8JaiMf67s@JZ0srX4Fkd(uWoT98_ za#MAs1{diJ^>Dds#I?;Q#N#m`<77MYh7``m1-l{V2E65Baux1aU+iVJxKy(c3T3_h09TUA;!7&dL` zDM2cDF@=t#V(iHSSJq6Z#1-@!BDM)j4F>DSnMRzfw|H>WRA|zJbe*j7@BpIBNCht^ z>^>bFwn54qkhQ&<0N_H$^N5P*CMDMr5x7u2?iEpUkB&L6I&e&Y|6d(D<{1BfALe$- z95cL?W8UNI%>6D@PXIlRLGK>66aN1_G{fzV|G#-B2c|**$3F(coACY-jQ)6uco#tK z-|+wc1PyS!uQ^%#KajzE+cZE-n_zWLPtcIl>UQB{b*Xo3KtlBjo9VGt-05I3>=+b3 z9+XZV{~v}T^dKH@#jQ9N3LY{GE57$<2U%hfQ zz4D`#=dHvKee2NE4+Re1bnvo+=P!S9Sy?{o!21r62M#U0WofwNTYTMOec_i2FI&jW z|9JkN=M!@`&#lcp3?K;nga3lrf1g!n&-T6FNBLHO;6K9$S#toyR^=#zCnF^*%Y>8N zVa{0#<(ZHB>h0Hl|4k40H)PG82;N8VdmRra@|(`Pp7NN#d%>eFc;LO?d)uX#-}~ZU z%>Uuxr=9i>M?WPmT=20ZvSv*Lhae;rt_G-PDPU#$kyx%}r7A^b#FVn3w#_>@+Cvku zSOyAB@p8V^wQv6HD=vA(n@)eiWoO?1gAaY?6QBCLSor{?Uj33g-uK}z8OWMB5uAv| zlYKFn)9pNfVH@@WwXzs0JK3O|w|TWviR)fh%;I{dI0cQm<4rS|JEH(N!3>W{h^nnUsila_TJ1(QlqPW=3V#J51zw(_0bPP z*7SA3#du9_Xu2JOYiLje=s$)Ua_dzM$^<=B6FUKTq|7&B<{q32^nl=#}u@j|AJ2^_Wv!Nn3ltKclY3W+N z4Up}IDWr;4@nWTf+5>co*!6?M#lU%w@*RmCc{}~h{&m-1kSoR>Mx0q;KECwO7rYsL zBeJGW1b5*)t#vB8IdX z4L)$w(s{%qhS$LF&lK2ap%Rznm7@>kE4?HrudQNnqP;P&;A67KBw_f z>>cwuRxa z=_$=-M2g0*W-@yke83PJd8HHfX1YsY;<%>jXk_BB>*>sxq2F(uvvIzx53_jlTYa-)UcZ z`3L^~P*u7%AHL}i-@M|Ie|_wyUi({QjazSuQY2FiB0#mEFF88f3+D##i&5zO!fGdQTK7>iP!{HP+6!*O;@{Wqp4+kQ{1bkCvW-uQ!bJ&d__gR@m1e%Eqn#9-1PJm`d=mg8LzwT z*y)c$*2szAeVn$f+i~H(@454xZ%hr}_NF^O_r%vdOucgxF*(n;#I!S zB5TBY8|+eIrq zAe4@z=YK2q-u?8Fow?aN1Q__y1aGS7d-ryptK}-aEE$KQaMY-zjOn`Krhji-d-(icN|${2Dd(PX_Iuv`hlRI(^V4U%?Wvc2 z+WXpd<%OG&wM!?0_i;|SuJQFxnJxeBj^FBEeHn7^cRlG|w|`TFi7Q|Hq7VEe$N%_a zKSvKBYmb=-j_&>Nab55aFZu3!zyAAQz4mi2df2tqcl-#lKT%)#_MP9j-TvCjmp=Vr zkOO_~(G$UmZqLZoBV0GpPS8l33&Ip8fkTU+U`4wqz~GC><$kCHfp%IKe0%l{XWW9f zZ+h?Lomc+{{k50h`i(C<`Zvw%KYYhamO+Hnzly94CxSarsHIdZfrK1OYvo!dm(XY= z+W}Xfuv9V=Q~|tZmg#id++b>M^-Dkhnh*Wqm(Tgo^{-B!_0^j@nXmrvmmi#YsSL7`wTf9HXrb; z&s_cEm%bByfS+n|j{h>8bW*Kl0Jp_upDM_Z7}%kGCV(WFmMUr@`x;_u%-e zU;gkl;WbaZ=o#Pm_;=p*r5Akpf_L2X;zMsve)$E@c+%i-6j|#|1e=v!y$LXS+YvQX z^N3tN3G+;1C>A(xl9G0Ew>>9qJiGjocf?}ZvsYfT_C@O4N8EX4 z{sox>-&tJj=71e{CW27~LIj>6iUtNpMA&dqTcfCv=>`R|mk+bWEZ1rbL}O5%E@Qcu zbze}p^!zXW^84Z&K6(AI$KQF&jm5|IrSs1#eEyNoc*EnaM%LOB!R?Hr4u_r5NJ2V# z5Yk7DRFw$C+?v@K@ZGpZDtbdxEw*k>2cg0vIP&>-{=sa&r7HaH=N0>wUqrwA<>%Fp zc`rTu#_PUw@pq85)rqdyy5s4?)$c%KB)AWjQv#V zm!G=mW%SwD#~`rUY)l02<8*f2DxQQ2^ZaQ=~vP>{qRNa`NMbe zJ`Y)|O$6^FpuR5n=vmi(J}P|i#y1^2@Uu@n{+c&@?)^7C?Rl?#%;!E)I=*zn^R5Tx zR-FjmM?i30@Xx;co=Xycc=6A^dy&VXKe~hd$JXeCcza`@ol;Jl_BN zW`8#W{{Oen-&)}RR13Vsw>UTR)`gj4CeOJZB@-&flM0E(Sxya-0*FG$Av_8|)Wa$x zF`CIUSa|LkzS$YyMItXM9L+0n%@Hv|$265ERZqh_6Q?{qDkuaWB~0+{MD^bf(EB8i z{bCOADT3>{L?Ue1JOep`E$S9y(Po4oML^7;M8cv_idSsh|ClkK3b5N|W=~UXBJR)$ zjEXZhtTG8+!P6q4?6;Gkz_<;4JHPmyh$>o3{Ef<9;|!lxy#2|uNt_i zx;QKwf*e-exsUq%GrlDYCZvRFn*KOY@ZRHj%cjFDOLCgxiiXRZe%wDZdmqvvSdld( zJEmintR*m4F{5$64CEg11dG15C4vj%y`2 zl$yJFT%CmkO@&2n?*EOeaXLy8G{IS_=s2E>3c5WX^^JK>Gf~#l0L-cZu+vG&v=^S_ z8~@<*;r=7@Grq$lL!&ChnLHbJ4b_}`J?N;}WhWA&Gz+AqSNsgnbC$;E-aam8Q6LBn zq78o-)M!a1=_svgp8s4>(pjbnn%0Y0B+EHrkJ2#Haao3*d*`^g10v^GV7G4SegYIQ zuZLrfJNM>^?lMJs7G@|WL&>VnP{J~@uHu;SbWM{S$)zz_09L4~mNx%{$*-r|E|5uD zEVw&PaJb1E^ZE53T%4jzgX3LJCs2Z7jDv5S))@$#@!5;-0Ch}*+FS&oEXx{(E)c8} zk#wG6!wJ;I88qT5cu2$JATb)x%Oa%Cy=_tpIK<$%ov`G&AAtX!T_srx7@8|uB8$n9 zxtEWL4u?V!Q;jBEMgid;O`m(^xRz%m$cSXSG{>?g9<)UZ*978WSAsqPdW`>> zz3ZP7-<|l&KcTxwVOUh=bysv<87D;oL>;q%v4JXFWD~aO06{SgXe9~^quDy<$`KhLouhH!&;|}V zkm7jRUj*iIj$yHz>x@(QyI-5sc;;XjTq_x z19WT;$8bg`<1o$6y&F)@tT;h%^^9~K%OqWD?wYYi_loEd0=NJR13Vs05MvbWUj`*F z0heTAp|Cply>TIDVoI1vXrWlxlwE~^q83Bzalaq@bOtVmT+76vC~lB4ZqQs<<>%fq z7XDxYx22#C9>t6kABrGT(R$)TRM&MPEUqU$6bago0^)fJMK#ucHt6ItRbbdUxNpcB znxsKd8w@Hk=06BZdmuxG7%;2g5!GTikJTNVAY_A}J;MO^dI3G=Is{VKB^V3j z0y0h3)g*`maU@kYg9^{n5^v)op&Pt9_p5R3=0Rc#p737?v^b4dq^L;A76oQR$!3+g zFOF++x&$6jcqEuYUCI`4)MOW4yx!#(8HNLnIb23yz`jVZwOB%vf(h8*D6k)cR4Ed5 z?F3CEICk#Fabf3JF_N@V8m3%{VZuSpA`^~?^CHJ`3^8}xcnrC(Ez+uj5*TC#Lk?w_ z5@~`IIa<&L;~A*nX)-G-1`SvQ!$HB>&HVht4^MMAMGPwx=YYW>4m;f3O_L$XwKRq$ zGy}Cfk_O`=1n%=N;km29k25~k|0q!88Bq!(C4>!F)OnLp3CR{jcJ6=1l|K-V8yW`V zH07Tiw=T#FujslJltsd|9mxY2P9etDWPwLHMi)g*N|=NMGKSB+bgb+H9GT!u$53$# zG%f{#aF~pNkOOV84nDUwE@2ks`~lE5iX~R5 zv}w%$U_iV|5e%V(!7L99ByRc(VB9{J$3<2TsT3Cr11I1mNM<+{1OCC8kzm|^E|3LA z-ur`*_X2?$hvThDI39I06@=qyO4S9=Q$0nEvK$fjlVjy1)b9bSTo5$Fjp%b90j)N> z=n}d>>Ae3zK$8m<$0QS~q%+_-SkQ4SE}{|yfrU*PCqeGV7y%XzyyGte+GSOTT`lgK zqCNMHN!2vQ!0f0d1TjNa6aF&+(Y&Ezp6#zK0n0NfRHjftGYx_R6NN1wRyK{f=eS`{ z7K1SCuwqD+$?=@%*}!aK8qMSW$AdZ?@K`IVP;f%=KXg0|d;Z6RV$OhwIC!NT3aNxe zg4GsyC>zw~o;z`g@tK9;Cjr&{qXJkY5-}TcTwW5{D2j=ciW!V2%5!&)t8gSH2_yy< zsWF_8NDPNz8r5BwG#EHcKpG>-b59@BU4+6WEy*^jYBJ~1qA7b4LqK2=!BBCECOIAl zUQHV!Y0f=+V&{TEu&(8CU?9V7urfRCxqmd!j++6v0&f_s1*TQZaVQgGPWzWlB))?f zc!0nv2Cs2E7|zCukeHodDU#>F1s(=u-B7#`7BNuFqbXXJP*V^kjdL)DN9X=|T*`yM zcyF;RC&G?vhzyuSummAExaBbnqqB@B6JQ>g`{6`5$uEXCQ3W9C>Sd1Zvpz# z8Bq(Pyo&Q-#)VXlHGxTjN*!V~$|5P4U{uNWbKslPB@8^);RsC>Fha%!!9GlEvK^o7 zrvT-$VW8k@mZ1f8e-Qll0n>{JV0`6u|KrA@Ek*`OOBvb&%N}sd4wQBjQKCR32OMVE zRwSyRjI4oSeiF&C#*rL}0FfLCjBI`k&>!|p#uC5<=ZMY*!QAQphq;;Y{{Mk9=*;0K zuKr>5@c@zUvO~W)H`J#5qv+@3V`T8P+%Xi^rQO- zkMI7B`1b95ia0ek5Ja4ND-1x|4nfYXzGDsYOs0GzS_1@>{0b*j4n zoW|@^fm7TC;M8WH3Y_9D0H-@Zfqk6$o$4+Cr$GBu;1qWOI4#uL`aEiMCoMr(9_966CcR^*}3Y_9DC{GmF#|(G$6f01gD6o%X z=A-+#lfV0GgeP24+_wVzc!jVR1q%CCU?0b?dr=@iQD7hE=BK&~a{E@`6n8;(-wK@K zF33z2*vAcnQ{4sWeJgNEyI^@?b>pYZvf>wB#4p)T?#LJacOq(bBixu6c#T&R6g{k#ihgC;VWk^T)pbh{boLK@Q1614!+&@p@UEL zpLeh|_vYDW9z1XPmiaT5-?RM8`LEC2Z@EA3_}{*KA&4e?X!bn^p6h?=5_RCQ2Ohfe z)x)1zvF5K`6hJ(ob?DoR7q8y5xa528!fO_^g-aGzS28R2p3g0Oar@xHiC-WPCOcZs z7OELL0joqm3<8e`PB&aAR~tzMD4uHzdMOwsTt>n&!T7;jCLFasi^jOBK}Dq?9~pU0 z*hMmKzz{aq@sOLX~-(^lAFL_%I*&P zxnZqECzay-r`O8>sbrOkjo3{-go4cNHBzk+XsKF=R}t0oy5)SRN{<4B;}qxD*2_Ry zF$h6Ms>Q}Nv{@>rLdAHF$Dy7Rh!(;cpWucyLNA8X)rB+G%K+(FX@KBZBx14=s#aQj zE`<>#C!>NpqqNTBHeVn|L_7@B%ei$9J|&oXv(gS?!KMzn1uSxp%^0a7>HUUSQsShkc+8N zWBh#UU};?`rOFZmg_J6ci&%+}7wMy;c)LX;k#q=2w5483G&MXRb+W!|*2_Rj;bK0U zB}Uq)RpZk`LW!V+W4MA$c7gZDgr+%i%XlRnJTR@en(u44*UW~P|IgmL$2+dw^`U!j z@7K)inFHk!9w9F#fiRenEI;I2Ajpzz%eHLGmSsuF!LcP-lC8Ha%T|(eno`C?LkRbN zXv-~>%d3|_p>WIN7U<>ChG&74M=8%zxKL<-@`g|-y-U_U$vpO)ZB6Fnko(F0V}Isz z&i=07S4-=+*3$Z|-}i^iyX%S$5_;%B(6L#oTO}8cS)}W2tx_C9(v@*yLtfEMIgjGm zVbxxQMpefV6rc6r8OF1rGAR2_+hzKgG0mU;yW}}Mq3+h{S>O$!hGUeRY$u5Du_p|2 zUQVyi8YtU|x}^omVf*h%YzV8KH>B}Ak?;4wg>FlxguFA7t84>Ycn@b4GMZ4R)}aLz z4kp;Ouot zUm-%)r5gF7H_&@EbFNi+DhF=FEUCODj7(pwRTsl5R2)<4*(W6kXu)PjZZI6$u+g`e zu^6?o2rT6qx|thE8dyMVhDN;whh6!~bHsGssMX8Tc#<0w@HWxpy3R1`0;Ps}xQ2CN z5PGnI%N?7%dhX{?6Di@t-ZVS5$kLi?^ss1cdZKCJjz^j_mhZDw1CeLGe{FDXBj;Fj zH|KNp>}<^vrqPEn5;ELCRDgmd#4^>H(g{XH3p)MfEutNuwpP~#n_lH8TMARgz}#RpnLbp(&JGfU-_rwd!)e$Em^G`a>%lA-NX5!r&kA{_HeI(Z37K}q zCORJK=j7Z7Vc_9pG|vr$ej8uu-@Jb_ZEe6?IyH^>OmIc2!fDYb~AiymEu4N8gyMVi5 zPA|K2URLugD8jt<+imb3`@?p_Th3fzKc67XYO7ailxFFP6;#yywD|c+5d*7NM2I5eh56Y&~lZSY?DE2gHSpZF! zMR3Wdt<*h+z?VIkrozmhB?xxdtu>vXC6yKoJqDL8EWgtQ8!!hbmreOT3};(ol<8=} zV1JPyI%U16mpip0DOh02SIzRwn9j&CG6Di`bk6CkHFDOKOSOFF4-y1D;g-u8;pjal zD21#3xU*PT?NYU0@APxTtRxNz5398&KC|~138GG7t-+jyMNV25vfcP{g;}0Y9Z$kZ zp=`=5oGZEwMeosj$AO%9;p$L_YTyt)eN0kWd@%n{k-zMs_Q5axESwgC;|1k+ESchQVt3W|j#N1hJ zf~w7N*~2RA)K_pu?Q1#;E-z6w4`m8xzrL4C=j4eEq{xn{v&N{VVTho%>kXaU2rN@u`w zLeE&YmZJ!yTf<|XXJKnP?s?#{?_h|y^IUi>Oc0S4actPG*`&>YE4ZTqk%b#Ye%bA; zWPU{Vr$_-|**;!FkH2H9|F{1IduLCd{^HRI_{T?nKGZ$1+1s!{@a5UJ+xs_Dq!V3! zz<7ZWA-GgM(&bol3(d#E8UDgk#^%WOWX01h75tAf3ST4ulj7KID>RtmkZSw)DbAKJ z7A8z_kbRLLnyL8ySXE*!v4G5ZCtcph+fGz0(0y^I?yBt>qa@o}DAoeN-u$YGEw5v| zlL(=K|7-f{2PAD4;QrcuB1~{B2;+Fla1eZVP%o8sIPkK zR!Rq4sa7o@>KOMkc*CQG5B>=kN)VKX;q9ZxYH00EKNekj$F2RFDMr%een0nQlO;NtrT%#ZSTVkr0dUo27fu=_nJS)x)7d9(-` zDph;+)vY=@uXs|ip$%cv0)iu4sC)YORuiffb;zvXGch-RY<0r&7*^;WZZ3`bz|Jpk z)7oq)IateHv#2>=PE@8`ZYp7}+Q|E>hF$M8*Brw2KtjGwli(b-E)PpQH8j`>FHb3S zVD^|+h1tm;&mTR$#yyr4|NZmZtI}xn0!taNs`j2+Ra1=9$MWcVR#jxXAUwRPmdYQN z9-bmLd69>|xU1^JYbbEr_b;^`xN5(+->uOL-THA?Rk)mgiuXT2nakGhc&!0 z_BCAzL8^=#1+@wXQc_62n_oz6wuWmI=MRXy)-gath4CB0tp+jkc9iywA3gTLmv5MO zRsCXm|7MDj^jJQ4!Kzw9KP-JNMKJmzpWCW?zG%9>&shgM(w*f+pN{uwvf?Z>urn)N z=!5qyt%eO0R5jaQybeVnD4uX1+eLE-0^k>_Z|jq;)EK~$J*m2yPWuQoY}nK0V$5L# z%G&eQZ+G=`AU@NvzZq9{p z?QucYQ`T$Fs}IX7b}=0x!x zE@5*sMND_Lxd99#@$>!J&W6=M@fV}91hzf?%@n$O&gMEW_WIb^pYCkz;Vth?Yj;IH zZF2@p-i}TF$<8L|_P zA2E-p*YTj5^34=gjpWUhm`>c!f4DQ9b3aKT{v_OW9xSbQGlhoV z+g`dk0hYnoRWiS~v*ioMZVDB?n<-@KW}9O`Z|leJ-I?Bfew;#V&)FO$eX*DMuR9yN zpf6H9pSC#!Cbxe4J3E`a$B$D8^UXGy=eqK@cQ$pOAEyxO1Acq}tZn`HzwB)7`Th98 z>Q^^YX#B~W{g_Vd$G^2RopV2K?5_AX+w48p$G>@bV%)hO-#1)Sr~o>5o(0CX%g+D1 zv#|??YYKfp?anj6MzCj{2EEAcPyZ0!PfPLo`g zA`8m7vrOpiWq#>{p_js>b|(NPx3kSJ?rid&*(ODT(d^FRxmW!QJDa+1wn>pn17;f^ zSliAv|8Zw)&p+EdSX(bmx{iFui|NF(&Cl;l=X|zFkrM+(xC`uTNBGZOe#CS>+gvch zQ^beT99sK#96;~2uLa%kGdt5eKW@4IL?q2wTO9xYguT_?Q*XTSFK=AEE}nhU+1@qp z>Th1HT=}ZgKRs=oeC6>UA9s(w`RL$K%zP72;CRFSKi@wFfDiKLZC5weQ+xZ$r!jkb zS5Ehjj^NX)X}%opTMRDWQeE9Q-|_w4OSixO-oqtT-}cIl8MArqb8MbIy>c^!Wcd5p zY=g*`)4388v3@fCZU5P$g2oSSIN$b}8zUz9`VWviy_w>}=aD1&a=xcAQ#1K3@67j= zoB0EsdUCV4yQ+EGPCbc9&VGCQ`ww;|`I^lvCW(BYQ;%<^_-H#PdGE4&d3PSi9Mvz{ z{{H=)nSSO*f8g~WrTLcqAYT7b%yjbF__rMH%=hL-i}~LEK+hiDO!2{Z&i4Zg>gD}< z7&9DwYW(|6pRqH~*KE`WMnL9fif_$xp7*ZCmov@89L2wgf4}kBoteI3GmV+Pf6jdR z^dQYw=sDA5CBK~GLClW(`S|znk+sSnnxtN~QJ&hna?bI}(b3bVH&cAUo>NR#_sc29 z-1_6~@88RQf<5_8JJHnK<8&y)83BBmS^2=L~Z+=J2wDpol zF@5lp4NUk_%=DfK|LA6l&*gKb4{u>E|EAk|d`*k_QpC=DZ*C?r-}^j1#W(jk-+Mg% z(th1~JhAm7@|Sn!`I^o6ff0~Nqfd0s^FEKilLawDMOwGkwM8O)=B^JU)ex z(K*v|kH3`T*5f~K+ozxRg*$V6*=F>h$EP?1IH!2-@t0DJ`|q>g8~^^CU)!1C)y?PM z*NgD!X`0i5`+M=yQtR7o67n}O$HM>maOHHc^L_dOHeg${dpU%ZY__{1_}gPPxtCqQ z<~5r)KHv+-X*7Dy+1xi9UfM(3+3?ognA2x{`lH_BgP)XOHhe?O^q$%9IE|FeInxVf z!yWlJ)s%2lyVP(S7+K_-<$K*Zg$M_qF>w^S!wdV!rpy-YL#7&iURmdtchG z+nMI(c5J?Sy))0(Z1@L8Y$lD8$T`pZX75XxZfEaT?!`U&%13^%Jv3rpu^Glp@0-0- zD1MwXJ)gZV<+z=_UuMM|U;Z(>_GWVb|GW0Cf9#d7ILhvS*9ZAo{}K3yqz6FFbUwaV z71vBp;i8wHu03#FIldfo!Nn@{!s5*-jv(VZJ8AT6wtt_(bGG-Qwik5YUp4g&uSzpX zTy(MO12bDp=iY0|m%3&Q@Gs30cyRuRQTJvEyq^HLCiPsZz`a+eUf9yirBbdKU(UMl zvQ*Tp-43isv2$eA6jqg`L+};M_62LcT)M>o6i!mT_3etQK7N(}(F}WIn-{3^6tP2? z=r7i}XgFWXp6WKRrOIfIzl^+~SFTnHQd>uyUR5t)AfpOZWMSF|8mYx*O#_mtaAhhv zgr|`DZl#&Gj}9(Nfzr&w*zMxd%qd3u6LY%@OEaf90)H}Yl-3VFkjJvU#&z$)`yx5g z<)xV)a>b97W(Jeawl?*17d7Lx;RVcrC?dHy$?3w`<`&k2JDdV37~UpW*!ISI!hT@PS+yro43NK<4o!{Mij?D%XxWAF9nsg zRbp}R7EVY-)CqaZ=zvP_w|QpVSxPk(l!p&%Y|b!oc3DQPIcsY)VfHL7AS!HdF zO(p)%kGKYyi~n>CQVz!6DDVvMmsVgH_b%VL=ep~@oWA>RKJU7#k8M5fo#;Zb7&E!h z@s_K3uGm8tD-qO0Wd=itqd7QNao2Ph=7x(ksEZ7A_Pb*sCiB>i=hL+yKWo|Za;4iX z7pK!%E{BQOWQkV`Q*w}JXArASBdJf#`JBK(%P_2zeMTk;o~H3m)N_|*r~~WDxI5-W zhr?9FA$GeH9X~p7yf4%?gV^!DXtbYZllsIQFZZzHUEoUbe|o?u|wfFi2e7R42Fk?NBK048^TQw_ORgf%HbeYBN0BKY}jvdsQ zrh`9_wYK0E(3c22fgqi={S0^QvD4C65f%#}I#wEr++TMs;d* z0-zqHyAlWV+y?lfvxetooh`^j3GNor88TmRh+5E4c-*S)m}nP2IvDF$&suwpy&pQ* z`-^L@xK2Fvl|a`Ied_qe&)xXi8_w06SN{0Q4_x_@D{s8=8K>_%{o&IuJDr?DCx3JH zNhd#X@=vb)&h@`@qMZ~^4v&B4TJQL)kFC8QI{uvFYuCODX#f4<(Kj3gSGgnn=%a6( z9sb(kw>zH<_vN~jrqpFmq)(G=os6Z8w%ELHnH`DEEaf`FM=0%^=5)_3<$}FBcV$$E1T4c zlN?Lg^R~8Rht`rODH>XcHG=ee>fV|}FRNLW3VMvS3QIYP7iizB zQ+9VKbakA*@{bZ5I>L}^f>b!Cm1V2lHo0_4GdQ~{PG@pM%?TCA8%_0IJ;L_o1fexd zm9=Lw9$MkJixRbgF(m|dw931BtE%EG6}mvWM=AP;Z%hzsH&>gq(ONceeU9on=s*P8 zX-sJA<9VfK1WEwpp4MytQ^!{mgn~L%0-Y2jR?>Q-eu0`~*Q4xcR%0>4uOix16DAP5 zqzv`SKS&T#RO6-~rODW`!n-&5%vQE;ZMT)q>? zXtSg>W>L3kVFt<#+)4{Y70WgKnxHGaJa+PX2?A70v8-^~D%vJ0R|0tILU2>k3!Xtm z0#aOtOmj_v^cjQ7d`W^B`|Z|};&r8A^=h-3zZ!F*FwnWhaE;fsk;t-f!4eh}EZO_% z1Th@f`n8fX6A`WAPY10rynWulz}Z;Kten6#79YO;^i- z;v_4lMv=Bndku{pM?rIetwGYhJ6zE;?v6 zLZu@&<|qeMJo|Wbjv)MX$W^Llp*sOengJ-@47pY>BtmGS&(`a(s7~?&1;Xe1dkLa3 zY$@M)Av2HXLN%_B|o(qCUtyzju7f>u2X_gb|$;*tf2_Y zoY)yVj>Zx=kuzPo+lN3+G)g%5i3Cxv5qS{^HF<>|W#VC@Dn`0hLQS7xMR`Dag&OFw zLC~5?2Y;O)YE{eD^5nGU?U>pcSp)1A3cD|km+PgPv$Ne!nQmBP*lI?W`AJYnHg)~qzTTnmiSJliBk!+|&` zkf6Zzx=6Q0a$T||RA(~3w3Ub&Axq(Y~=HEufc}A0w3rl1M_trueYJ1{OtBc-TJv@pA<14F?@|B^eOM z!agoJQ3YRW{k%6Y5Vt939kHc%@R)NVB0eNsdGi(_u4~ymW(-||Q*0hF zi)fCXtsD8qXwnj7SDAH2<^s1_w=~cEj|5?3MX|-O0@oWrK28LYUY@RkfXLMl&8Mmr zLN}ZBkXdl}1ulkb7kW51W=S9m8ckoE2MKP8f6=wtw)ypCih7 ze@%8u9@3wXYayC8YM@*fU&uv>Iu)bIxH_-T^;M%5o@=g&raZ%;PDwIr`kJV=1VY6s zB$cbI^o8l<8gmyl*NUbOX5^KxNNi~BsZgniq)gW2TnU{{hz^@wDpii~$Jk^54F_PS zE-=ezey)=!s{FPh6Fu$@8k2q=k1H~@OXRj96W-ZYWa4ewU|lMcC=!a(A~JP!EMK%J zCzr4H%N4@GEdp{vYO*khrHBRQeBkt!%>M5ttR!ciUk^LZqVJKaSf8tAuQ}tI4sYjh z+ZyHga#Xd5`czi0B${jD*evEdcou7RM_?JOt>G?gP-3*K*aQ#*HgXl(q^Il(wbi{p zPi%~<)B3=fW4MY}^JSsTWzqf!!z}?R2P)M=a^;p@o>4*;J$X%n*u6k^CeaZUd0YX? z!;-XC`(<(>5+zr4N~0bxB2#ghr+9wQZ_H~o-juE;OO(iIovx?XYa%EnH5!#yj>R>E1#`!6NIZG&84U_+3Hg552?PadX&mmZT0$734-hQRztHf3`d=4H4?wvspxduc%ZPnyB!ED6>6g%;eV~NgsfV2sQCTjh5tf8ADi_H-(_cYQweVU-uIP19gz0*S0^_57LXXNg*a6Zxpuua=P(X6hckk%X+krfS8RXSi&$tDs@Gq#MTi3> z1lx;|Rhk6e6mJbiv%V10pj_p`FL%SzievIEa!7iW`EfEch%6}Lrw{Y3Uav2ce02ql zt+9>fb+AFI(Y1bepsOgEZ>t6M=$|DvdRc0&f-=YwU4s2^o^?=cGKt1ozvyc9YR^IY zeSbI%!!@0GUxMhOGS)*RV=dG)$EzDYGVMEcEJ8h@Gbzf`x`Qc%Ad+?e=zmKPbiQAZ zM0+xo%Dm4>%WR`EuVbBlm2z_&uQiw<&4xfWn1_zPBSAC@6CPLB^a84gT&s#sP|S)9tEvX*@hm}5%O&aIQMOlI4QknN1{KI5W*dc$UTQCGi-Fs@7FHX( zn2`BbTf~T|_11KUY^Xf7)N_nnV8gtnl8u>B$_@%WE}uiA^(@OV%(b^B@&EDO+3NKF zJo@zg=d0xRx88IARubIcn>T{P#Lsz0X+O;^uU!rkFN(}xC^M8IRvDkBr8)H6{(Xuo z2p31@QzVUT4W>wJyC{eRVdu1+$JP<#~q49rw-bld? z8YvIo^ov`4%$NWQ70Qp4GLrIAgpukXY>-T7Am*gJhHi`WQkaCjX0?~snp1DaM{u_X z4&ecZSdMSJ2s|KB6HD^lpSraDPl}u?6yWmf#-omm+3{I2p)BL$8G)R*Mg)#TP}n4j z5<@~EUZkh}TVhyf1*$DoxdpT2t(-Eu<+Oq1^=)5bsezh9TRD90-lFJ9&uR=hT{U7m z_)fvN?W4!RxM=f+I41DLnf)}^BX@D*g*{i!RXKCHN8Px&P|E0q_Up8f{)d8nDY7ju z^3!{7mBz#OMYwZw9|bTHrLhG{Wq`ony-|uH;PwKB!pjgGTevVH*PXO_#1bNEv_OL0 zfKw#Hx*ryJdWT$Q98@IG+mze91?|nKWM5dtf6=XhzCw6hdl>|6!@l09El_uSq z57#iES~3ZtqV{Sst%O)hE*Wg+7N`i|aMg)h@FKyrdxel;V5rJrVgV0#k^_t%9e5v* z=M-b_i{&{}tQAkp`$`W-h+3s=^%KP0FbYMAqW-X74oNJEKA*$hx?((SJ( zrby<(Wvz`5iAcV-*vnI)%?zme&OirkDQ~=J~}=8xx=qMG!D_j!_2?Wd}T(<u*5(|F8Bv*EfbV`%jA@45z{hA%vqml>Zvcp^@{0VZGfOF^&hAXKNv$ z6V)P?`H%43PmD1`P@2V=_Nhs&BUd=5iBv{zt=%>wA71l=QOWdIXuI#PSM)sd^YGnI zh%qob^4iY4VW0)PN*A--5}xL&OhC!HsJ5U1MZ-Rnt12^Sp7{^(-Iv6eNfneN8hZRn zu}W2Z;wxd^Z7-tnw1qVBsA2^~a|98zzgE!9&%t*;KE})%B)OW|Vx%gH+pVqg=4i4A zEweI~yDX@zCD#mgku}N&EoFWdzWZ@oZrH$eoAm)Pt5;j)g`^VWVz*(1!=klr3YHCr zmBB<d25XKLg+W zm>9z-U=u+HEu`*4-I6_@l$jFQoegEZsju?cHtkLc&m@<%DjH+{PK?2peW|4)nuK~q z*IZhG)^x1-A{tV&5|?ed=!&#Dd=P>Xk}>9^V~n>d4_z{v4y$ugYK|O4B}RkJoXU00 zy1Hlw)0V>!mF0BeshOXK?|xK_S!t@`;mx+BHc8$^kz&8L2&=1^WEc9wCEBZ~N><4V zn%JLb{ylv6sTk9ki+rw&Y0|XT?2$;*9IFLIopLw_+v>#j7EG1yt4(`}`kD8@cW=a) zY8!(M$-z~KmdYMKqGVTXtU%zVIFW*IJ)I&ge^m(!6@HNUDfsU77_%nCZnZoXG=uCy zK%Klr>%47N8*m>3RX11WLaB=QY+0GFsLW5och6!>ASf$Br**bHEcuhA-78Pl&Y*~9 zCzYX99WNHOFuz)9U~XnIKLOvp7GoHyn{Co8Jh9}8JOhr|s#n0Um1x=!>|ui@2t;ct zIjb8{G3IKFnKY)|aPC(yvMEy4sMi=-++anRB}?dKYx$yRLXHJ)6^$xn=HJ10uf!N3 zm=RM44JR!f8C$4iL4MO4goagV+U%-4uGBQCXw{d4h7e;;V~n?0k#$V1tjbj2p~qri=&w$bUK=lX_+^$#@wd_3e zWANRh7}L&bu1c~Cq?I*Ggoc3l$%3oFtz32;6(OXmiUnG(S;nlXW_}dDdl+M!RgE0L zy-6NnS`5S!BS-3vrVKwC61stQ90SyLB`1yHLY!xQ1iqVzF)%U};O?+n3MaZRjyi$Q zHH(dIzG!xEvz=eLV0@28yyLFa%n!qN4`R%ykXN1N2n{>gp;KAQ1TkN^K~`gm>o(h1 zDE&y`3Jc27i%{l=;Jf=LY!F@`n#TFDki_32F;=B2FwLGotSg3NAPR%bMtPH7$(#&DjFV z{9AbQOpKAOvCEdZCgOVtJ@zTO-PXaZLF-jYn$lbqN2nZZfGDmRX zL9J6(*fQ=q3*TuJ+u-U(GaFI8DW_C(gvMB5=HI}Zx5OB@4I3!Do;ik%K`INk8E66O zZWbj|o306S(ge5kmG&UAb7+itbBxJ3PPNksDsV>%i|ArHp^=r6#RWy$VoUWl`ff6!50B_bY z1_bxT<`fEwO>59G-14H-*8>!q1SPnrwA?yd??FbFYlFB*=KJBzD#oxn#&CryCfEIP zp$c{7YC#FXWquJsrlI5&biUSMTw{)ibmsfuO&DWR2oz`jA9%BjF`9%G8cR~kV*|0h zC>Et|t>kArd=r$3>J%{$81`T{7%%HBD)X=5O%P*J=n==5MT|jU4$0HmDbvUNBrrH9z60LOV@wLY-54{AF)6gIGT#nw z^ca&uYAVKPTW-6{J!SqSyisFJ3iYKJGmSARG;cHi7raqoObRWc%(uZCImV>W1AV#duf*8oh?6n9;@DI z0ec9rmtr~I+1|ZTRpm|tSh@`?rC5-6w)B3@1Yf;Foj*~EMSN#F_hz-f?fRWMF!gCq ze_Wcie`iz4wUS4rf<7q77k9w;PA%pGx;TXt-_CrJtV8uq^}Ksi>_aXWqC0nudk2hp zhXj^lS4gpY+1b*=uAtl@fW3Htrr7!HZ13Iyil6sjirv!Ab{-g@6<{hJpeZ(4JDYm` z0s5fIVKP9mm=EaU6dSmm`6L5$b?5V98}LigZ1^sZH{L&YKXfO|P{vC|z4Q9>H=AM) zxU=c|edBY1rPw!8>>PKtbdhg-4zL&dMvBnn&i3y0jn6*s!4!9%cDD0?Z~QO7RO}lm zE@13z>iK=+!CdIXH(nR>0bQIzI$>u%iEo&9%D_fEY*L7u?<`XO@UU_2pupa1U%Qt^ zs(fdA7mA`QcZ$GdJW|bNR|d6=S6#_MP`*tNgQ$zzjcju%%eg^!P;BVvC}`9%-rm{d z2S)1bP65~{13Qh1;zF$G!+x*?*``ZyG;Vq_K7y%X&w(RmP1;Mm74$r8zOx;0{miy> zQ2uVTeMt3A9vF*vX({S+?`-S=w?KfY*ez0&>fYJZ^ScH7y8Ca5CT@|7`G7}HQEGf= zK8ag6cVJ*69;RI1WyuiSm8nP!o?pz!UaJElByGPvSXnc8Al5W$%obb7F#pQ-blfFt z&6zbI2JCnl*~SC(&zFn~hpBl70w!ZmtPfDnr?CYBMp11g1w%L-vO}oXvdu=TV-?0d zXx)fLvn7k}Z1Q1GjKBZ0d*8YD)HgoWyYW{yzWxS%{m-xeAJ^+=e{}Xw&hTr$ckLfu z``oL)ef3MPmaqKAmA7BXpZ@CUozvSVzi{%llg~K*+2gkyzxwE>j>4l?9RAp$clgPf zcW2DZ#~*zEfp+lG``@!K?Vo|BpZpJd_o>4JMmfCtN`1<#>Si00`u$p+qi0k7fxf=U z>>kZx@7~xpdu4}aQv|%&yVtkPp6<|WikKLC_iWqj$qvn?2%E8YuWg$>-l5qPku~=2 z)ors!J2aaj_{QG7vTgS8@+y)yN4v-8*t@6OW-~i9n<9kA-aXkid$2>ZDWZJr-Q#Vu z`#Ut7A`r;lJ=!+Aw?nfjVukG8!?@YZ|JjR<(*#J_ z&0Dw4?ojeIO$?RYyk*<$U+lm(MVOV{ym{N~pYPCYibyQGiMGw|P<}Q|u$JAdx6STQ zXf{nfS$4D9HoHSH*)$|HD}Wt&$AnA+w2b2Wzz)I*^Rkvc83bGX^!~Wjj?TZhib8D!t(59zHN4g zvao4R@7c|4+w2bIUDF)Gvm1Te><;Bz)5QPTjkaxehr+FC4%yj_x@~rc;;d=T(%H>) z+w2a7SkqiIU^mLP*&Qmbra2~OH}baG9V(}$IqhaQ(ze+h3Z6pnRdgZ;Aj+oDP`AGcL~kPZ5E- zC`b6-L&^v96ECeM6ay~T<$o|@6{GIW^S@k9{Gk>;A}0>g`agtn;%L59jJ0Gh%=F5^ zbfc;@seqcV>MctKN=LS|GI}J$)$*3#HgoIXc9-q)`Nx+N4~AYCf}5kc(k-bJ5(~_5 zkU_gzxE;Z61u<_|1BqC6fS#n;E#HF1IPG^R5TEdG8J@iAO^1AWW(%`QxdU`IMHi$D z-Kt{5PMKcYM~@W@g3TzNZQc&##8dd(&mJ)2`lZgD}-y3 zMazQ%JtQ;|gNIOonfmD9@vDZAXr9FS&eWLSo<%kech+)64{!N~u`aipGLS3eA&jXA zy`mG8Be$=Q5xEs~MWKWCD3Z^YZJo68EtPaiIFgSF(qJ{-O=201A00RzkQ4v>*zsaH z@f5@Li8&tpaH@0c2@i;MT;Lo@5bQ;Aj>nY~e`xH+xv5IKZn@6h zQtL%aSz>l|GMSg$fMbe!G;VUYRxFKM*2KJBSqt>IZxq(6j$d1lOsSlk!0(EYx`-wLN33q@a7A5u+YOp`-Ru#NOAZ3;x#L613Rr*X*2g9 zx~cTwuJ-=bX?u?;{*(LVtL}9!^tnmpdFoeOX!UWI78ph!S*Ze&ZELME8i{Kth z->&+@_RK1po)*?AH7w#KqG1!=$>ZyA@YPyNo`M`<%d1Ta-jbtQwV~oqXBD@mu+l0h zVOm65cD^@TBkj=aj3<@({B}R=uf~J*RN@h+MnVBL_3)h5jv{U%(Zo(V9O(I@U!cR0 zW^kS7|GzBG|9=K#{zo9^e+jbvJ&@;bfeil{$nBqkto{ke=l^_=$uEK&ehy^s_dwo$ z8)WP^K(2mu*DU=P?z8&Y&6OiXVNTOzAEwDVygZop?B+CKkRq~}CWmns1}6!F6hX)| z8Iij%I8GR(h*ze`uiS;fQNkcaXfsW`au)`N34;{*oM~c^m&XX7-DDC5DY8S;WQFd+ z;2>d;A_$r$$8;A4`w4>-@zOLw&|MhpB@9yJ!KR6q?!e&Av*!$U&#FxmTHS@gGYNwf zIl5`0u)8pLYr-H!#&4Q{?Jf-7a{d-}&mT?`+ueo1n-c~pGK>V>h#A>|M97E<8x2yNJ={qp{w91M@X@p=d{1jvI|AAQW>uOEKv;ahJwht0!J z%=~udJ2PL9VKOg2`2Be!&(C~!kvnJ zbR$6&G?7JGRMBfJN=1yejUh*hrY>VMYnA8GQJoZfZ3c6e_CZogWXKZ=j1{7`sdN2i zyEAC!d?RX;;}wSElOoftlOs^}iknvLGj3~R2#Hj?G#=yanx(e8-BmdC`gNQvNTE1F%*W9{btj41QsXf{kEt{c8J#yYHF(2zCA(M z?9dNMcO7Z%r2%OYF3`d%H)}{EtS%d%{D)LDjbf3X+t+_JLCj>B>K4ULGwQH~hLUxA zumP=v+%juo=0K(hy6Y0He#6dRYitpGfiGC~5|VebQH8`fy}M|F0*xaEALPuA?99tE z(qk4{hs92kYKeTIUm!4w)pPoE+%xFXw3e+l9Rjvm%K(|ziJVkNMy1J&3HQD%v4Loc z?h&~xjauvest+%TIggI62Ys9iC ziyjUO^)lf&OLQ1^yIqu0ARtT>OK|XzPYBq$0^_qR?7F&aErq% z7&4QUeeli%ktG|To(Zf~ZBWE0$h)$_F9$tuGH;Z+!X%(sX(~GALZ0WZe)SeHTz5?c z9AXyqT(=@mng*-zVv#mGb&(PlYOiP(unOK>Eb#eJQW|WyY7@bX(RwUd&7)X{En&Q* z&-kGzRV1xXqB)hyj!|NvGso{pY=n_Hu!f}?I4P~G4Ns~UNfGUmW6mF11IAI*MSVRl zAc#PneqMqI;A*u31$CY58|wzyfxQlCBc7Ls{ee2=n`qb98lzzg#rBN^;ZZOsO%i&G zcC}Ji&8y8>yON*XYVk|DV|i_NA-EfrALFjAJ|Y~O>5&T>xZ3%NYYwaS5n9{ z<9SV;<8D}pL<+?kK|Wl=)m6Ws(d2opy`jkpQ@rgoJ;R(#i{{#&j+UlWu<*9QO9h?+ zRSG7HLO2@QN9Q)?ytYIOBYMb)aMq*Tl_86>Qqc7(1EWH(JNlwCBouD!=Z})Ic0+C4 z%Il@Yz(Fc(sXv?(>@sTPoBgIvDR_Yn8aaI8Hu_T>x|)>Q8A?>PSvA%cK3IodU8qkc zWC_*Ew%8bzDsygNgWAfac@dN)%0$VNj~BAWE1RUfrA{>;cw> z29wIPSi@n;m-_xBZ$+g#KA!-}{lMWQby*se?en^wLqToVqIEc`LH5+=S=(@+4p&v@ z3$trRV5HZCrCV$+>cRZrw-OtIh!2pZiw))Ka@3~lPC2i@MMEyvq!vpt!!k2BJB5K; zLNmXZAb4s@G-#2m!K`w3zgx>IqAjhf05 zp3Cbl*I>j&b=1~TDw{XKxjwY+^p&hb*(c8-7A#r_+3t8QA%o@`0jY>#XNFE8R5U8x zQO>in48^sqNH{+4t3kJ~F=BS!%QA-AS#uTeyjpLmf{CQVwR?ynigms;#Wdz5NLY0> zP?~b?uj#eWkw{t|&!qf3ua;)S(C>C|+16FA6b?c8y_2NC8IQBQMrksf+aBrU9I`Mh z+kidpBLy}un?e9uH$uWs?>*k!|YnFFN3Ng-pFDULUC4ulOIkH4L4^q zLyKl)#4Ia00`Dx!`A|m}nA>#dH6a9itq6D2B6Iwm+v=6W`ZySYlHmml?JtXV*y-1z zfjlMk+=$D1g+;joQ7cx!2k`uv#6)dm@&+uhqNdvn5YG&BmL7x3Z#A~Ln06x?^76&j zs157%m3o4xk~s+-6xvh|EMCG&@DOroAw_nU?XnyZ zrejs|9k|RFvR-3F52c8(+D(-W@S-;@2Wt%~Cnku>usYN`%PJMg$jAj!rR5P*;R{XQ zi!>+8H^&o=&I9Sx@bEne0q z$#Vp$cIa-yc6uaQpQ0`?=Suh}1j@ji((Xo>S8wErm5*X{CMl0Ktibl7jWjVVZNrDh*~O=1HpjHVda4h>wWT_5Glxe<&yv@#Z(0~{_# zYNf5<8 z+Hb6@RLcn`g1MR$7}-bri-Ix1heEAnL37Qc2R&s7oqWnUVrSvv!ifzI`BHTYoO1lknBvsYOj$m@MUg;FF(>&@f^tzzh zU<*_1_QkdqmK}XL_m7gj(l7_)eWkFx?2L$FTXZX{zET1;%bX>?C`w~>nFad8piFVJ zQcsd1oI`l5>lC~2TZ9hwS^iwp^FSY0(#8eBkq}Qp^L4-3?9ecCd^dTHY@M(7)^)iQ zdcE$zV^xXfg>?^-Y@G%kHZ^DolnldBez`YTGhe~9lO0^jS*R$ zZ8jlG#SosImW+Wl^MMkye~=W&9(J()G7s5{frS_-)b)f=mSlBWMzb)7dnHI;OWeSp zX&7_k{Nn@cj_WP2S(zHpC?q&gXK}iqxe>-?F=3<$)kp)fuC@s#l^e+`=dtmcnFA$0 zgm9yE6Q5{pXHctFxi-Np1aZbA_Ou-a3`zS>-P}GoU*~yTE2}xnfNc@4gzZWzS5}3A zG7TmwUPe2phk-AzC6+%+idXYh*PSuGII}=W>vj(=hHJdqsf>`dn(tSAlOBVSIcTB` z!R&osVxu}`yiRqdAZP%ULozIs$IV9D%~qA7xGzhpY!?^InxIa9Ke0hdWN(gA0hiaUXcUkWyhjcxW#JhJ zQNkg$us|`XInWW|B-y(1pay8GRy62>T~o@-S(on^1boiQ_tS`n;UPv{@2&veD>eZ*4N&5Exh{YSA#2mdc{Be<5TzK4^Qml|8{I1 z{oc_WDF5l1-^r*4|K&j0|IK}A?>7Ma(Lc|8>E0m-&Zmj&JlPn`Gw*yhHGve;t(S*j zo_XgpTLRXL6$5(aop0R|NE3ibaX@|ff$KBxd`oHqPgJSwnRmW9HGvcdebTSMOikbkzXD@R zAcb!~*$D76@0@Q5qzM402ohc%etqVhv(yBhXzs`}@6=NhND-*rr3bVvfiw~DC;ST3 z)C8XJE10Gx@PuE1vL%q_3cwS71#)TvPxuu`sR=ycS1{QUNYUt%y#uk$|3CQ0duLyC zW&h|4z&}3n^N}8S@p}N|Ov=yYOg_=A#{SXHgpjbgqp>9+*cLfqCZRnWyTG^q?^ zNm?!@(NK_ks@@I?tQ&Q_oDOP1HrJT0x}4w@B(b8jdw!vVMXRw9kXWsSVW8G}+=b6y zf)$M)JxM#JawE!G2L{ZTR?7nVX!u^`2xFwM2N?cb-^M_imlnPT5~ zk!;AlqUkZAdy61{h-5?VqX5bu18wI~tZaN=0pu69lsQ_;6&{&FiTh2|y=~tLDs^~< zn0bS|75H>@<_J{Z6-5+WijtdsUe`$IvAwUBE8Mmj-hzv~WaDB54{&wL^d~{DrW6sR zIMLh*tL2qkxwir_DZGbIU388{o}_3|S5CoYGQd!QU$S_`spcG@Q@tD!d$wRyvY*yiD!G%p&2-bq*Hte=GHTxm_|FQQbV9umfy?Ceh z?r&pJ929{b8xX$E4@r@PbrrIU{ahUcMqp5~jbI`28>_nz}^ z^`3JKwW;tdu8f+we6uWw-Jr#`px`K{_Ujft4mqu1blj~I)lM6lZ~Ajp2X42$`sgJL z-2Zp);a}=sLN7gH^)FX{bN!FkfAP`-S3a@cTz}g7Hi-KFm9_5n?$$qFe#_<7?6Wb8JVaD9~Dj*UGh=2xD<79xQRHNyf2|o~rikAP5s^Jn{!76yPQ> zkxer~fup+?3k8@l!Vi$VA8pE#gH)19IBp=VV5%2cO;0HDgxQtDMTIGfiH=t3aNS&m z4z~*as%~0ji*G^YGCVGrfyUbM!W5GmpUoU!WH2I7tYpbD?ZnLfxR6FfCtMaBL~X?2 zS}^Q~a^);#-}E079$u*zm0u(>AuvC$K3d<2j#SoH8JH;l} z>}d!~i-Qzk`5sJyZxkdDN!bFb+`a_yf#AVh9#Nlci zE%qW3+ZmP%lHSW9)jqh2SX3Zt9DEPd&Sg_vfGak$ z^#BCtLUKXLcj6(K(h*3uqQIaBVIK8oDtW7$W}KP?E^Fm7qS^o#EOVJa%u-h- zKrKSy8Px(nJ$qrz{S%5nq{n~@U@EGWVyqsnboxvl`XYEeK6U(IOQvD!fIeeE+&I z#lK7_28EhjQd(@?Y1j(Zb-=yo1c@4ht}_zD@<6K#tpE?v*?wT{O%sX*tYi}818!%P zn^2jk7Az;(s-~r8b(|X)AzU{)F}hbo(-y<0v{pCcEPZ*O$08J8I-r=hiulHVPAHO) zl(xgpXdE5}$$Y6ZwEL-%0aZ~Z1Xil@f{ zR`L~3s`-}~lX!u{h0oc`n8|hX<^szYd?HmiTunx3gxBL_HU*;~W--jNv~4wrs@+S* z+HoGv6zZkINZ9l=Uw~qAg}S*we@p^C7NJ<1=pl7dY?G11P~3@>MWP;&F(c8_L#g_J zuAq<!!W*hO0U1j|vC{*FmalRVQj{Q(Q zn2HZpUOk~$U;+2<6N)TX6~p;L+L9bj%L5nB5)pq$pd@KrAcY|o0*lvrTz6pnrsq#6 z@R2iA?Tnd`69uEiNk!PHFghKi@TxyvVj5wkqeVMOy{CSkarrY#uRfTmA3Fa((IXkD z*4R8*NLCs;)3y}ZA9h%g!w4>o2F;ScY^Oz(9}s%v`|X7(niGlz`s07?Q!G#;aN~mu zQ{1&M#hnv99^4Y0Ju*x}SFBOL*X}cDAP9&1LX55w5?+#69E49K;AVSFmk7FrHUt(D zHt(D8h=i4VA=1m`iftm2btEEFK&^x$N8q6CNOjI0O^jPGf-BQp#*|9wvRsWuzR^oe9%Y!(Exi9dYeu_QgB zX($9~&1xX61}!+krwu_XbMZl`F)Z}*LoQuO<|>0B^{{tMcq~9M3Cv%F#~l+MRjCy1 zpnjOBL7`ERRSFq=X8$dbvT(LK^veE?uo_g`CrM z3S6z%AvC8Qg+S0*tRp{q65T|0>cMi>DRf|7%Qmf=-fV^|W(g_FAoLZZg06 zJCR4nCRjBh>0aC4_frubdVOgrYpmpnfh2 zWrS8;Nr52nZnqZkSLCYU$3m%ApQFc>XdHq$@~vjn zZ+D_if1%Q%n#n#|b`RUYL0{rgkDo||61hx6Y$(BWwAmPMgVsn^?4~j)x z?AOO-K~T9ANb(>y0;!q;rA&iGg5M72pbuYJ!I>d3sWc_#r$^l*At2bM$yH`-N^|KS+GD#nmov59sxNX0HU>i zIgXiu3ZwO{PS4*my0~w09z(X$LrLdrWE3CQ8hl3L`F6i% zz*1itSDm~K*Nbqo8;&Fdb#=1qT7<%yNVNcuN%9!d$QMBJi7L^xc}_?g4W?d2+Crli z(*;8d26J^)*0^>sQV(prZo*>$iq|eo@$(akT7nSqaV3vnVl@eb}RtJ$@9Gs~RDnp`HTB66PK-i{Iq7ug&sS$0|b3~_RZ+as&)##|Coe4`I zo|}hLqEVJYeGzhM7?B!{#6c;8#8c(CRH4>e6RGe^C);om zQo;{t1r-=0R_cT3}_FnDAJDBEGQZ@0(CqLLq_&;Eu_5 zv}8x|BZjV3(+LVMr@|GiTQn*q12O7YOMb*>7pC}=2}L~&QeaTMEVw9E)@iXeEcO_& zU?n=iXxHxdQ$5iM!2V_&mLDN4OfmUfNiM)+veRB<-5y<7^FN(XG~gC7P|1#zXqrW? z4x!arLazp#;SkrD@`yAW(P6-!ZV2-CTwa*s(l(fQip#HETCJ`=X?1($A6GuM@@p$U zyTYx+RvxwdAIqP+^1zi3UwPpb{Yv`EEmyWK|IOvQF8|u)`eo{J=<=0Ie}CzZF5PkI z#h0FQ3BR;`@oN|V7-SFp*^AG;__T|UySRGcix=*`@a7A{3#AJ;Us&7zyY2h7e|>vz z``O#+?ai(KzV*qi-vXHgtu1uRzxfZFpW1xS<|{Y*o80D)Y(8@1GaK*Sc*DkM%FjLuC=S|w`2p{aR_baiukaN5Y46XcDD*SlD=)kLD=+tSeyR7$ z5uJb9)9EGNqhmU~%+u+oyoa-N3MP}&I=$4>>BS!Tq_EV=OT0gS`S_pzo5$=+b1(}9 zL(|Ou)+7I$8zcXUhr?fc11dQ+nY`-YZ|Y{wsgu^}=6yuN>)xzxKHQ zrT6GqFMQF{>@U2Bvw9&E3{C^{1rP2&_sG3o_$#l~&(GEB348rZPvQqW@?&`Yg$Kr; zc@Iy)>(9McK6m|BKJVf6r`{_^@Or@G{#ozQF}(iF)9g>YhcodCoUq!@d3b%sBlqz7 z(;o&78eCra^zl~1(?K*VpY>$>WADXd$oz??_NTmury%ngPwh{7uN+bP(_YhmTkXWOnS9Dq`{N$Dr}ig3wLf;e)$oa-_m4cy{?L2z6wN;2iT_dWm1D?! z+@t@<_31z6A#=a?$`NG#&};fW@6j=2KI$QJulH~^GVy6-KH?#Bk4NqybH9ho-N#!E zpBS0$^ECUg_u?tY-0O+|A@7xA$lT-6fAISBcYDa(<-KwQnGbtS-|0O%hRlaNWIo_M zoQ+Iu8krAz$ozpv?jduRhs^IEZ#8^kXu8wW?DxDEPeJAbp7`(gUO9%$A9(cdyFUHz zd&vB*_sS7ue$Q+Az22i^$h_Y}<~`oS*~mnvk$Ino%)32u51HTfka^efR>LQT*Y|pw zz0-T~6lC7xiGPRp$}wc#?a}|v_37W`A@kebD@Txdr`Pm5yhq27xx+)|?cT%L$eb7+ zf5$`Sw>)wWncwzU{-*cmW7@pK)8;q4ho_+Nc2ApM_g*=o&2M>4|C;ycm^Q!ZY4bMk z;cRVAjC#M}Y4fWdxu?yqd;R_m17To z#RKn`yoaZN_g3$fw_N{~U-s~Qv-ipoe1FN~{)^tDWB9(s!}m?z!`b+r7(3tW;rk07 zxrgsBdIG=E`|~L}z3B;S%TIpt^2!^$SN50inq%5vJzuk(?o$(qKw#w;yoO$X{f6G? zY5qFzl_Q$J!P_vr)_XL)VR*fV%+Gtzk0J9q51H3^4`(BDV(fgahs>)FcuzJAKkupi zbKaj%SNkG75NNB}7|rv}JV0NEuPiWJI2HMN>1*;JUcN_te&t0E!>ZM|%Fx(r5fI%)S4 z&aD^r7{KgF&(c;tF*mc>08cN7b}%6-e6R%hO^EP z9)Ogug1VP?r2(pcd~Rtbmb^Pcl{^NrvDVB~J|MDUmNF#U!Uzy(B}%=a)zvTssrTW= z5by2fTrNRvnW-~Bf z?5GrF%65UEP&8a4wNNuZ#_c^C@O?Va?hohYd(t6y9E-z=w1t zN`X^&vrg*99^v75-`t#zj#0aWVzWSM}37F%k*SY@PQ0X57-u4R_dK&ng> zx=I7MBo3IlE_v_V%x1gf^h$g;7_Q2}d?6nnlnqBI=6a4u#)y0*X5|ozjttv*D^n~= zt@cn?_cE?D0OWh-md10*Vl<++$YNrU^A}q1sDh%6VY}zAF|iTN6I~5d1Q4TLyk*1! zy}h){1-RthXIF_lKF*)mC+vz60~abC)V70}qJ=mh{V|iZdN_rHFL3cuM1Vw&i*&0E z%dz(`z}I!j56{i_6r-&)OKm%TJCuRpYLyZ(B#eiGW*@fIgt3Ik*UWE!J1S{U1*guRD)+I?_t zUZ-fcKv7nG4*?uqm%M9kjz?T_f#R#i9_-ft&vN z-hTlcU6=gbxj7zj$pxy98hcOmaD4CFoQ}EV0(C}#OC|v`*CpRGH?!F;Ieqr9NE&T* zFX2i9Kz{e!(s(YpK(6fGUfksZT=HFKXDps_$?21kMT(T#dojS*b;)H!p#D zdsi;M_41Ei`uHXF;{O0O03Utfr5A4Ae*ZSH^@Xka*81jX^UB7rZp7C=zW%JWzg>I5 z>c6bMbai*-T`R=$1Iv}=C7jMCosyRray7dUX3Tb2VCe6-60xZnbZ z3v+MI4qR~h)ZW^=32=2?aDfAcxw)P+RDJLJ06*6S7dT{?o8NI4e3Ylz0*4E8b2_P+ zw)ee&pX-7P95Kwz@2CrYkB8s_#|v}wnR3BL0#>dI@&?3+Q4FeNoMaRW=q$IDL9i+Ixg63xIcl1BSU}IdH*;yF7pk($$vGlWCPC zmj`f(1xi!Ry%{=iiRqIAYi|p1bq#fa4gK6)Pa3Mew+Z;U zv(f^a`nmZXcfk!$vjsNub8|YWnYOnM__;2)z?Ob)en(w!%|md3?fl$)rd)6puyS2+ zf#M`{vzqOK)29Wcy%kp$0Pg}D`nhG9xZr;Nf8R|@7xy+Fz4~VGkMI8MBe1u&R|Dxt zOD_d!fzQ>MrqYp~o`4?QZV#T8k8}a^chh1nVmJHmFJSYhXXRdC9JsN(z~7=w`X|Ra zH$kaWD}j`c#}bsf{rAmFP?`(_LjW93&5qBq&3v=hAjVdUX(?<&(t|>ryILrPVo0eI z9Txh5&L|s&HQu@5{MpFWFoYxTp-;s{E$VjnJV&HF)13ozUe_Ij*Cqtp@CkE~gjRB=0c_dX~Sm|ms*`*8{ zGE2#N4l_|4m_A+l`72IIKZ!1H(`s_=5PpS8MVe=uKS;CycT3>5+Ck=XEXqzQV0}rF_Q}0j5^xkjo@Y$(T zD`0r|#?>FKIoqYf*Am3*V{6`ot`X&ZEQl0Cr>|!6{YbPQ z8P>9mTuC5hBV|Qye3l<;Ak#8a>GbeMMNacHnH=|rhB8W7-2f~=4tgzN5PaCIB})}- zg!PRA%o&)dMkAGWRyUG)J=YraM{2CY_69X!EOm!jWWHJ6Ro$KCdwbe`_WzOZd^cWm z&GN});u{@p+&OgLLLT&wXD+t=wUG}tV^@%h;k3-s$aDFd zPY=fpMBG;=ep>8xOwd5HXPU0J9ynC!0Iuuj^`CpV0!`gu{{Lhk7;q&y)$!PP=2{GD z;jQY`>@|qvuNopdh^88>Q4VL2wvI;=RuaaNv@GB7@;Xwjhw0Fu*={AfJ;3w z!&h?^oOY0rTF#ERUR}>2B-KmAV#k3M2+912v!)0@D2Il9IfMmDg=7y;@BiB=rIX_dYDaP?G(hUmfa2+v zk`{ZFv}u(J;s{T7O^vY!6q*{yA+v#}D`we%k~o?oL7QXQ0`DVmPz{MA$$gFnbS91U z%ArV?szbvZp8~W*mE!dwJ%$EZ@ZU7ra>^mPR!-OYYM6=TP<=o_`#eJXJTh!lO$&OS zgKKh#Hq|iY@)}WveO{pLXkFoe3`jJ{AhfBa2SFvsA}ZQSkzslNU8M=$4Ml5}j5ty_ zxSZnnl$sFaoJHpyD3?s+gAt_+;#62r-^mK}yO~HMt_GbVWCug>rv$+Nf`zj{o{p1z zD1l6k&k1DENlYUO%KeRY+lAJ)3UZV>a#|cH-6` z*{MhEYA{;KWexohWbrJJr>|W+q(H8p6Udl6!pDICC4iDPf;y1EhEw+Mx+axzWwLsf3r zJXs1M`h$bT=2><;oq^+^L>ll(dpj#!3|ri7XYYWy?a~6g!5D7K!G2 z0uq93qk7fsXO2vJ z%2^;!U-5e=flQy*w1);eg@!O>$8;e#ZiUNcw5z5gr&u!DO8C2DSfhp%HH=0g6r-3w=0}&m@0>OWw=S=18nd~)TQfKgjf6=yz6Q!byFnT1ky8%T$Ga zU3Y4Iab!+S>f6IZ8Db4a>0`~j>`%n&5s*n%jWc$p5$o2ZaA;KF^P*_P)KQme?D8>j zFmAQfm>l%u?R*|%TY)T=AXTD3$7}UQFhs#!+31QyV@DFoXfKZ%6fY49I)d`*vcurGqV)l?8t#I@W5X!LH`Wan8(S)BSNIm`X8& zD6M4%HOg=MlOrL3$J&9OQO*`vo!aTceUnH@0>_YoY&oPlos?ngosv zM=78~Gf_$_d`!q`1FH}*ilYIaVY{N4gmx7iB;xEKV?T=xg|vTA4@6T1$Tp)nBTo12 zCKIGXStwmtU@J6kuxN)E7^A^pJSD?c>QDwN+-Z`@GGW!=ksirlESa;b$#Dqeh3}_jUQAU= zR-ZH~+F&=B(9DE9j)t4LE|VC?b;N-JK^hrGdSsPv!D^HlQ>{!bkP&uhqSBBlr@;*H z7G5w5jpmdLmwxxcdJPMPK;jxSYmR1NRQEoz*Zl41R0 zhcaM_QN)95c&Gl=egcBEd$dt*!PTCmO@&eWGl3c+iS-n4)lHo@VVTf{sDsi2ZO@z`~CJ2@e zkr0gGNJuQ}OgEp^Stt`r`&%lt+i3d(k;tH`>n*i1LUM4ooXwP)&4j=g;)zkyVcCAT ztX2&xvID$*++y{%*2w3&L#HcF4-YFxhKGm>N8RG-Z3#+frGkN0)Mm~>9HS+Tqxqhx z_+dU{#JT}}mmPO4yUMky!;H#iQ-z{1%ImR$5>A$K1{X*+xuDXL^Hc#9b~3oaYcMkG zwEkHDOC${>1ZejQ83=>r~Q>I(nvCOPRgY)YRBSDB&#rpRYiM@Lkx$lDH+zY zhcX0Mf^3Cz5Zz0+&FrWXGB~(RX7dSk=pP5z=7}#8 zgo}{@4T?+2bJT7r4heoz3AMYyxS$ceK}Xg=0f-JsnnJq9 z2jUG0P5E0o5q5U+W4mqB>=4wyXdBs)$ca-pT<9Ij5M-sU6DQIFZlHx4W%~yT(_+nR zNfF~nUt>9qZx+-PU)Fbrai?D-EK-+~WIr|#>!3tbEXP{`JkiCto>Qi1Jb{H^$ZqTq zGAK0LsOM^mVp?UT&sV2p*f=s>&uVYJ`JEzmG^{D$>akF*VxDfPJyLi^o9eWdszi zjOwjIs$hUmm*sIL?04v4BO}RjtOwHiB&&s1r?DgEY7}H-tK^8!DP@aHPY(~PpE?|( z>>!b;W$RWMN19p}^(R3w^_pJpH~Auf1&w#3DjSl>a8Tam{pEboP^e}a)O|w-!x~=# zUxTvgc!uh$@p>I(;^3fSGn^{hJEmPSQ%<48%Y%+6&_uQ~g~MYHVQ4ZVt4&HhGiW!7 zeq7>Jy)dk`dA`vw0;y0aiH2GY(uq2$U6yLrh^XR}8xGVgSX3SnRSr-en}(U9v7B6ecLNcGBK zcMu*DMT4mIv!EzaS;&oWk{YTNoM_WdscQ{m%&@HYVgeSxcS_sg}I zoswbgxrZ_|GQEIIwaZ4S%BPE2TLUMqU9-;9T$-^mCZ|(EJI{2$a_M|Of3G-)GN@3L z=%;ECNKoi*ET-nuamSyNNH8Swa}`9`R$yzo#4P%lgBMx}Ty3ite! zLf5jA6RBrIu}UOC<>jo|4fu^LVseSy5V+Qu%#)EOE)KHcSh)iB>QWpsBSJAsT9FVa zkqePfmLn~B2X2hnTxN(uRT3Y|Eh|4gOfLWMp$t6Q%TeHzE5Z1!Ourq6qLOGvXuL|G zhLQ~p#+vS!iZV8-UArk{FozFvpo}dBs@1f}>L39QT%b}~C3)K)QOH_6kHoPww1e{% zi(^`Ks#(h08N57jrtD$$IfpXj!@~l@H4+83z|^o#Gf|OpF;Z#7s!pL|RjMP50Vn(e z1Pqf(V4NC|#ZsW6IO~|HoBd$A5oxPf{0g2{!vtj z#jx{ooU{H>8Fm zd9dc-iC|DO;?z*irIWCx85O^#sLf=≺IKmw+`=9}VMi#Ob7qU^*3Gm1W^xAXFKF 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%0A=3|AEA?E`!*<$pADTEtG|Rq9 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

?!0tq-9_Q|>b~~QS1oP$qHmZ3j~f1!OB=rE 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

g_#!9hbfuKmn_$ixF#_z0rstC-fl*t9L6Mz| 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%0A3U%r3R62PcQIKgg}Q!j+zw=^Qk3B% z2fW-K{z`lpl66JqG_>hBBfCxuR~ z=qp7sVxSj>l#rDGgZ7Kb(1^B2hkh29f&~Yas-jHMlg@i7dkYOUijD%|NK6XuQrsyp z`Cbl}V}TB2O?I3#iOi`M9j2i=|39%XSvY><@y(r=?AYP%J-zj1xJ$jZIojMh_VQzP zc&a}Q&+@0%-oN$;xKqxo{CwrfD;L47n%(7hLKJ|Tm)eWJU6dC$pb<;|uG^emv$~YJ z|08*jzlrtVwiy};glDxBr`*KT7c194GT@1j^^x1<8FjEO-m|F^U$Lcdd!&iSnETG z`!d6_yrS%;QUjvxqtP51^Hj<o`;KYg*ObTI2>-Cs4?dkNT04wMdu+e6qGc{8ark(xnOm*yU zv=%@k*lAjg23B(1bToRVT7a7nrYXIGslyQm^h7Vh;aI@5kf&i7jhb5K zqd6e8EFfdZQ&RUBGF|@P&!aFUphtB~#Y}rVqpPxjP9aZmy>vm-z78mz1vHA zuhxtC5YtW%D5eF3hT8OD(4(3o4>s-bv>dU3#2`%{3O)Itr>jhR+P8^h1ZGjqQzk~m8|111$#{cKCyS%P*F0!IQmdoz>;$BB#QfwfRLL=D9m6`%yV(R_m z$-W?o5!bL9iOTl#USwq;SFFD~EMi#PHORQLY*(ghy-qJ4Mm)YMS}2qzIkJxvlR>PC zj{SI_CL!XVJ&JWhBP3o)c}n9hrA#O+adL0cEO_^vC!3}gS?5Jp_c~T)$F&@V{C^f2 z-qGdgf%HeW`y93KT5?d1p8ZOtb{)4tx*iY++=+s>e*dWae=fVz<31-5XS|06$B5H~ z2Aq&vg-=a_CvypJtkq1h#WBN%d%;PrI)I=YXz{!ganE06hS^vQV!UR!PI@4uP$W7o zFwwl?j3z>L)yV zwUnn5Ru~_KiuU53B8__Rig`00DfK6=bY>D1OFoDM9UV>rIp?IFkK&$SD}h4br=YJ& zj@edTC*S!azx_~FQo!En?n?6=et36Nn}WyOXkJj&Wp zVFZQr;4WldbbbH-k_Ber_~UneyYsm1-)ui->sMRj&0lN|;l13POaM;H0a#eoH4}itasWsC2oNm*3mY9)R?p4UfZcKcNBju( zEC+DJkKjrz01G=mNBszFmIFBANAMua0UYroIH?6-VN2|&AHf4H2XMrX;0ntD9PuM~ zz`PCIR!H>M-f(}*0UYroxLgas!U%WN?*sSK0lB4z?Op#@-Jzv8GL!DW^MIO0cespSBU_z_&91z=%0`luhl#g+p&;zw{F%K;qm zBe=H~fQ5y~qkaVUvK+t>KZ1K&4&aC%!9`jC7S^PW`Vm}cIe;U51nU0(eHW4o$N%E^ z$(_I7A-6xj-P`)+*5kJBzWLXi4?gxU#~!xv<_&87ee1ck@2_3EcDL0xt=@O#T`MUF zE;a;f%ai2`mS#(rEWTrrSok6|vvA$UbZ?c1hbR_y8_{kT3%U`H%ZI{iONzIO(8RD1 z$7Gx-*<`|%%}E(1DE3*(^ojf5dECN$g(gL2fK^cNE25FNpgnxm9-dQHc2`VqS3)CN6W4LxzSf&-O{392MDu#i}7D?))o zG1*Io^U->y?DIN`h^$teLk$Ou?Wkv}PSFbw=oOcI& zZXr~3jRFFR6lrodpf&h{B#%kQ!_JplnkT#X~@$v^S*iwT44M%U9NqZc>=o9y7Z$Y3^j8U-L?Os z2s6D*jRjV(g}WT4i#^ZG+VoPrYAxL1Fnz-R%*J^M^rWVpg}WQ3Jss3IFNXfq#%bXO zhiQMOH_rR$HE7}XhG{?h8t1*CE46W2xS3(v)$i^__g@ZSruR}~fz@l_{)XvTjA7%R zdVww6$~b(5+DtZ{buk2vVHfGaSh$n%2MVS+y%5HtCXg4x&!^GSz%$t#|6m*2zRT8Ew%)Mj-g@j- zWa~nRTK{*OPgr~=M5@2|*msY;``GkYZtb_nb~k>!@xH~MY&>J_V;hgycp${+|M2>A z)+=jZ^;4@aUAo_DYxN-zQ~&cTuYxH0ft5Q!)bZDY$>L2A@$rJ?uP?u4`MTxg^2JNv zU3&M@bSZ!CLk0iOeu6CXOS`Oj{)e5fX&Rh)62F~S%zc<=@7v+$KFm|0ZU4x=%v&Os zyL#@zJfqy=Tjv_)spK~CxrTWTxz)Y7hI#V1g{RF6Zk~!QI9D*w!FKIDCi5JMn^(;> z%+s|k{Ycj^&C|C2qPd27g0}U$&Na+4v#q>-u3?^%?bwZT4f9-V%eoKqBy6icocl1( zzP6x8WuA6z;R|yg=K0o^uGBS56RoY*bq&+}qnlryYnbO`Tl21w7*D%l2xIOQ!*uYR8u_fFsq`U1QTR=S_AA<8X#8*uX_^jW~ zgm`Z%m?4~@?ZMoKw>pL_o@p;p8&;%(YnbX#V~PoQv%Wxx6Xa0Y#nVKk>vq_;ZkcPC zr=H#X>0HA+b?oMs<{H7YJuSJq6Kc{yome<&ZeKKEgs?QZFl za}Dz}cPl+z!!*C$qF2{2&HR1rb8`*zRCsGIookrqz*}3d+b?rjR0K#A@iw@C&!HZ$u8GQCzQHan$U$}I7Ew+gbi(UqLNP@uIPP9KrH2n zaNZLL7l^ctPR6p~KzbDSmm*ODqGBgW0UHdPJ)ezTn(02`BsQtV?K!uMBVxFi!%3=( z*C#?^5`Z-05?(yIM<_KR^rf|C==pEA#&s z&wW^!|Ci@JEX@CJxnursl8XI~`M+6a_B-bPcg+7O#6EEesVY8p*L{-(o*WX<4nExwm0&+wXafW0D65H$&ZLd7+ zv93UL$VeIVWM95x{(r~(|8UOxJLdnHJLdm~^4s4r|G#7Y|Np1?zk2@Pw(z!vdcexPHU6{>AmDtnaOTe(k!o z2d;j4^|7n>Tlx6PaOF}+g53r8TK?d26W$2;N60^a=f!s~vWr{L^wIyLrvg|?@j$q& zF)WN4L<(;(Jm13URHVD_tj#Whm3({)L+BDiHL7_Nq!M7l^%Yns`b z3A6tCm`r*z{#;$g^6h+QSnBtjtvn*P>6}EPy%Co2V3{_-cm4E~*Ua*lg{V3nOxB0ZRzBAfdxZkE zzi!ji7_YVLHtztprx@EXN>pgYgL&eP3{ngDLZrh>JCI0)K@@t*X=XVSX2ozP%rm}L zfy?lHZ(8KhuAHv&Y%4?%L$}EFI}RStM}%-FMNe7HENjB7T`1TqEY=UU>}0uB&tx*T zUW53D|3hn{Ah$a`Y& zwn-@`xi~hML`i6t(abU?%o0+BWy1K_IVc8_a#OBXqBMl18`L`aehUpt<#52+sSy2m zU8bjx*33TIgxQ!oFd{g6F6hX_hXtzKtaOrGC_;Cg7)MV6jasuFiiL}gSTajb%bMA; z3A30hT^$FMq|MP8F`^iZW&`6kg+$!JPBO*vd^atQNz|4dq+Rs%QJUFDnJ}B2BzaPi z>Y)KD_OLwf&J)>Qp);&yMZE7ECz~-m%?~Dn(g>!*BQ>*+Jp3Alm77WQ^bwlbN0>01 z2qH3Ga`pR}f>KusX=lA(6rzEm!q9%C7PBYa92@Sm{M8}MvTHQ6*O)MC;qDSWEoo*; zCd^v6s6+00wWVhcSD=vEWV9o5?OqdO~lD-t(%{Nm8Y}jDS z-Aaavl%kzPK^fYps7(?=%zze)j&zEiU8R}*I}>KDOeSYO&FouEn6)yQoOv~~Z!uxk z%4Blp(agTtgjp+-$(dU-`z8}+E#OwrGgLGCMiXW&Kv2*#muB`2hu?VLwc{kg*X5Ge z<@VT$-jR%n{Q})#bJ1iAXIoSt5y%FzwVY63>6ueA`?n^{TA8EH9GcnJn=l)hkU2&0 zqPCK!72^Zle$*4>;-N~=;UN4O2<+0UJ6ssjt;c10hG=I0#*A6>%%$|qu9r^z8nc*_W9xYXNM6o?Wh)eW?kvR%X<*`)OuhV#2Hi z90_{1tC{^P6J{+yLeR7OYGz-2_?>!7GwRt1&FqUzn6&`sK+i7I%)ZcsSqn%B^z2g8 zW-S0d(6dW4vo{`fv=?h;Utq$lm5KW7KAPF*n=oqutAU=~TQmDS6K1VU)Mxk7%>Jbb zvlj4Q>DfIsv(GhQ)&imeJ-bLV`<%n?&hO$eS1THdjmc1<-)hxH9+9F$ba6nnJM_5a z$TdS9IWQ)pQk5GCg3YN2Y8WH;>kLZ zWSg;xw^6a-oHz(fhJL@s|F`5=*cz|DYUL96%N_sjsDa;E15XxacT>4sX5~dI%VLM; z?mjLTl`==^b$O;)mb)C^6TrCCE`mya*)JusP0R-&M6(xz*!aJ&l1w z;~g9DT}6;vsvVd11vMhsn%&dsQIJ#RR(fj5ARnMwY)vRP__69obt89$nxJ_-lhpRs z&&mqlg~9FP2=Ss`H&m}PN}X4NTm=Om?M#{baK? z8Wz0SR4gGVjX;j^h0mKXtQT)0fm+sC+}or3XtX9xN`9mE;bx^rVx%M{0TnEiio>jtnmr|$l1;h#yB4rL$VVInj;KTi>?wEEzj<{&Zn+h zzFuCmG6)>rN$+dzURck)QL8Mdd@Uys#H&7=1Dn6&?B;)B*8(c8YWR=Ow`pOdJBUK8 z$tw!vuu)^b4G8?2O073;tNC@Jd1a3u_0%{jGN&cj0eoVJ9e%F$M`z^a`KF=A;ZzMC zn1)IwID|(^(vE(m;!5{B?c}5z7$y^JFIRLXD*33pMn*i@f_suZ?=;MXPYzvTw2eDS zDjiJw@|jF7O0wA&FW@cA1L-xZDZGeIYOTIInyt2nq5}zx`XrXG7t-y)gyw>Mp+PFc zQ2U^5a;c zUK<80zCt>dg|wAis29uy?0&iv&Ds^}yc4onY(#tckaN(>+d`-?p7C;D6kJ^Y2Omh35WRhDT(Ksl{Z`+a8 z7(~0mKz}zsd$2wIjaPk7Iy8$FR6&Z9y_Qbq4Q7VB%Sqj+|DSS9*c^*K=mgJ#@=nM5rXu#NODQFIJ@V$$;BU<|^2j?euchYT6_tu3B`R|L?*D@51p%?Yw*E zPTTdZcW&)$J__RbU9izx|HS&`YuB!Re06u_2@t8zwfwZDo0lAmPlrhV-s>)#J?Qwt zB7a3{VPRu^ab@L^>l^S)hv`ik12|KAuRDApePglt^!NWSss8-!BhN7hI}0rOx(Cfp zszF|TYmn?K*P)HOA{UtXyFytnBf zFP~kZ2I;)DQdg~<_0)qr9pCow>ReT0{MlvN&nK9U^g*)+?0ftxR*sMU9*=)Tjr7NN zRe%2E%ckSqo!wuJckkAk1y;_b>ha!+Bzkzg0yV-Pl+~XS}_~9`wsnLD=t?JM3+{<)~ z7teN&FKp;BZm2z}m2+n~s923?fJ$t@!`2L=j z@i(7PlkV8aEx|#j}g| zXS{{oVLe1W;|~o{jqMYt_VZb$BfNBWpMABsZe^$W%vwBjgJ&B{S=i!o~NN+0;N8si5aXgbCR&F-bfcv^q7YUNb99^(Q1(V=y!^`Gy# zL5=qxA9t>J_aoC^clqp|YP@H>+gaGN*W*2-r#Bhtv(-r7`!LgyK4^B)zM5UJasvMM zs9CM2f9I3bc;DSI9q;b!LN(qqdU^{x_IkXh_4J3cs`vDV#;Eo5Z+fj7@K{RQDU6c8^6XoyyVeaeEG0inMSNM_uEzvOBMR-@=g`Z6$Hvj{6BKSA`B*iadLm z+rQs%XbNa+yTA8d{Q-f%{RX)A-Q~G6_8HV&fU{R4k7}O*o^?RZx&N-jM_4h3MB`O= zKNn0^sZlnU!Tk+c2w-hTOdjMyioeHYrA+bsmnTOVr=Q5D+xAFAh(y^;znV(p;lYF@ zDR0dkhz}hBYIssA;?5kG>nD{4&%_haA>z(Q<6I7re3>d;OJ>pWUK%4?6FQG3%x(N> zMTg6iPwCC>p(f(>{Y5LS>*x~E{U@1-R(gs94$9p6mT?;;pb>z&<$k~hyXy~}cZgj* zS##iqdo3L6mpTKo(3mt6!GzB{=_(vs8+)_<>>$&o1H;swxaaX!>gS(#ft<5Y4GcRa z(c^Nv@@+BNq&#dPmP>f!a;lrCCC7Lt;%^O0LcEdR<4egs+O6b5(RNFc?0b0%@>j$w zBQMe+*r-CcDxnNxZhsF!V9{aTO&hZd)VzBp3kSdnEoOZ=h8RY@$w0Oo6BRP*Yq$bc zs-B2Cy?%e3O%o%BKj1yOyh9JlI}5K)M>C*V9T9ImC8C~Ex$^Y4;M*vZ`tV@a{OFe1 zH$s?l@MN31=cehFIljvDCG`-SdI*Idzyob_gZkTPi)pGSV^HUTJ#Gpt%V?GvFj z5VV7In(%m|dap80V*{iY?NpPjcZ76UwpGg&JYGNTt>8g! z*gYv#bAf*P{C6R}bs-dX20i{s8|@;|R;n^?Rp(xGY4*X#|m3SxC0qgI=)Dp@|Sy^z2n@T_0xZm>r@PT^;5Jv%71<{CLsI4eq1s zZd|`_ccYG?XT1+w>8}nLhUZh$+og7J`H`~^uA1(zpsDlS|Mz+9=RC-xP>D(h;`A&14wq<% z$#{>*F`bj+TqJBC#ENX7uS{x^oUg^sf1uBXQhlO}i!u@~poKzOZkFIH^}w4L4}BA_ zQxT*LcZDH*6AqJI3CuqXivA%*$J-?$*XIbS$>ek6 zFwv9vlsu%c(6H!`8exKyDuUk^#zIkOb5d-=?*{`2>&T4@>gSlfmLl+e#o_8vqJNa2 zLr^zTrRdryKJgD@bl4Hc+DgR{uUClrfOV$aaEdvk{F)Dk=7UV5ERn4xGl6>15tmpe zrTQ9EdCeEJjf$lVi~;9JR&Y79c{G#4(O@dXxw6RoyQLQVt^=)eWNuu_VC|uUK#L!|Nm6}nJTjgF^RasS;Swby(EYxo-w73yivJGKmYy$#W$ZQ6IfYPkuj}6TZq;yS^c+@2yy-jl)_2bN&UeoFw)34I zofWc>(A|OFiZPQL@>Vw?XR}E~#9~17pdJ^baA)o1#dBJ|dzF@VA?05sTjrzd(sDg9 zfu225BPGUx`*FQ58`&jVCQxlMZ%UC^+9ShE+6~CbM#F#ny zoR;rgrRDmoY|F|uX_+Arqsp6N5+wadYYii5r9{ppTE;eRI)J+)aqW?$P%UD#Mojpq zlt-J6SchywC}gvA2z`J)Z_Br@((-Ns#4BaX*A0P%L&sfr_e$RmMhQW8k6}-UKJ72hTKSq>1w6cL`Lxx|2bQ}^;zrA_x_Oe(c>4) ze$6kHDW6S9hfIETF@4d*YetqX`7V;W|CHOTx|0^n3`)~&bIrL9_ch1 zSZ`%>Yj2zk#rkB8`|r+CW)`T3xy% zToB%3)9*0`&gTfg6PqFs?@t>6+0K-#5i@v_hTs{E$5=90Ouet<=`o10QDmFcY6b){p82%8*f zyTVe$^!f8zzHyb7+pn-K3)i)bBWHbgGs8T_ohXgG)l11-pf6j-5w$`K9j4vUr4mjz z>n=4`Hk#%K=&-wpNGq7M3Ekox28x~!%Ewn}`4!%h)URzzg7bx}*4Iuni={Z53xqr4 zA@velCQ#h*J*DJ`g>ORx!y`zV5V}T(3kw)c6nNY-J2PZDCei1$d~{>G|3CiK?f({! zzvJQO9~k$3{q7Il`MO&_f0H`?)hB=GXmS1f-s9-{Y5q3F%QW!dPrclW5a>&`wmEp- zl?vh7=G}U0l2Cn9&s(DIKtmYAWUX_F7oonzlMZJo1F1WwQC;-Na5%`(5)%>+%dO6W zjbj}3DpP%aGxQU{dmg=9({T6IjaPj0+Ts-{wfPMua#IfBBR3uzqfzTMTHZ+HCeLq(ei*zq`a@S}y7LNa8o4%2Gwwpx z1z}Y2OJ}nrz#3IHlFKxW)hLB<_z}CXVw|%IBDG-)Th}`jJb>$|S_4?IkKESd;}m)TqVB5rtuEEbTkBlVZY-P)&fnT zbf6e6n}I?WoK$YYW_zLn#F;MheDnY_?EU5yntp{3VEJo2fYpYRWVCTqlXZE^3@ z2+P;6?GTpIYLgJ!^p&ORHtI81UsBxkvLh8NfR=e9mZh53yHOsxwu4x*o|O(-_dos9YxCS|71H3{@IkIKnihi}`-OVxPBZ8E9I%Leuc8dl<`a`0HBb z;dRxG)YPsc8+XbLM+v~-NiRE$#bGOAIvw7k*Ge3A~EZUkLw)0SuU7Azd3{b3s} z>O9E{dLk(zDh#ctum>}L)s7&%Ej_==`xIDDUlFH~`m1}&B7v*PYr0^dxurYJ7Q?ul zIglAia1&OI%T{^Bpya`Y4;1M^#ZU8YT)_)uJ|mlD+~~|zPLe!@@bOv7anEb|KiKB~ zKLGFlpCpg|hlj-dpTGAFcb?t)7dL``(y09@9cG9iHJa-h{prLBN}jzxJ)4 z24hlPH72cB_iPdpuIuuX!E{#@tWn6%hm^UH)nsL=m(96o4~1iPES5^6z%UPTK_ZQ* z^)*sMCEPaaRa)|7t34JI;5C}wHxIgyNZ(d&WC<^7F(@n(bEeew_GG#bW zMTHkdW$!TwKLN(1xN1yZ-E%Q8CfBuMVgyK}+`ed-o91D-ub~;n(~N3jY>(J`OdkGiFedA(#^lvKZ2)6( zUAI?sshWhwxsZ!GwA1Rvv(l|!Hshl~I=2aU<&V_mU>J9Mep?EsGYb+|Mz?Jamjipm zD*b`NlM;wss$vdg5oPutlM;-{>Z&n$b$81wbZxt3I^gYyZw~YrmZ@gRgVRkbzI3^&=Y&@&blfvI;HDNz6Z4x36=y8PkNdrHP{LtjHQg?1$s!1OuoPGn0)>p|1mHo z%d5uZ6;5!vzI`>-WId9rxKzs~;9sx7uO`uDx4gu#L9K`-Be;X0g44dyc`lZ{n6U&D+T_$HidDm5Z#dEh7+W`ueP&ZJSp(AA+ivo^OZ zGM&;1j+1-%-h0N+|0#OA|35k6k6!x&uYKE-Klg-r{Kp@E?W0dW{Fe{C2fz8?M<0CM z{Xc!5y7%Yrv3LLe-T2PGzVm&z|Hf_q*5A7o-TXHW zyk)fq=H4Yw;hr71XKw;--})KAZ4W5Eb8cskc3yNIv%B|ydH{Z(`|LZn;%qy= z2ZZ0bIA?_`o*sbb2fAH*uiJZI`t8;2-L2dq@bA&H?|5&__dwS>7w7r%H%|`$+?zdn z128*vpFJ@5&Y4}}J_msD1XW#ywD)+$XJ7@L$3)-g# z!1e(<`ZmDr)P44V?mOr9a`)NYejWlI?>_t1tvFlv*#q+LT%2?FIRO6;bo=A)b$bu6 zzP-A=yTdyKh5qetc=jzo9$?bmtv%rQ&gHqpq`CcZ`VOh^89jRn#5)b?d*JV#i}xY- zJ^+$$_UxO_ba8ZR4^+Mv>*4^ozTnyGfaR%s?}6BN&hk0;J^;cmdUo=jD)&I~J7@JF z_dWpF57_y~0Jl^3-UHn4oZHLYdw17-NS#pk*+;kHY~6bgP{4C>&fWU}guv+8H@&Ch zJwO7_*?h=cXM=V<=N56)%bOr8g>3REydKrW^}5UWu{NLj2*|&3mCzl{o_!-=b~+pF z0UdbG>=M^K08=n{_6>mLsk!a}D|pWGIdeUra%S}G>)%u59#DhltUhF}2cQoEbNxEN z?bKZNKp;Hl_HuLG-Ml_Mpva~B>?2!ow&uDAa^bl+=jM7qp~|;A&%XAo+up4`pbYov z_J#f7Ayp|4sSXK->}%c|vOVw;zcASk09gbx>F)sYoX(_s04<)&bBQ}10F`)1>9gqB zR|D}*XVN{870<={P-r@!%;fCZSDoqN=++*1i!av20YHqwv#$g!PeaolSd8Z^p9@U~ zlzNPweZ_mK+ykHSoYjXy(*YHG9>4Z&N8fn!<43Q3=i^^`Y&`n4N2^C){_yuc{2dSe z!~^vH-?;yM_x{zr@b15P?Wdmn)w@4_=l{9$(|7LO{v)?rw|?c;r*Hn|liJN6yZP}O zzj&j6{FjgQmm~{((fzxhS)^=~uyQfa_@=hJsQ;QdF3l;gj;82g1V z;lJ~}fDa7#9IzPPo6qwx!g^-}IDISNbin#^Z%$`x1^*r6yf+6dJFl*^ey+9WCy(?U z9dP;-=*e}@Fjwr+6% zu^ox-#`wKiz1%H! z6A#ba0^iC5y3R%QL>@Ef5?im)L`Fm&RDTG_)yQN$Lm2c{vw~yW?wp!Pd&_g?7PEIS zz+-Esn$jQ}>9RGnQI9nj1Y*v+D!S~g+uhoPQR~xgzK$1b9_#uiL>8Ll0(Gkyrt)Ub z^0=K`)^oSG)J)TNP{48;J0;)ISOiFMCp_bl(@MmvWPZRj*f$J;k4VMv0p^^O50rJOO=Px-G^XClv<)LeCg`R%gh7qmo>Yi-ppfly zw8{1!)#u}a_l^KKZSCiPlY_l`_JRF8e*F2PkKg)=qt|}-Yaf5|cc1)6Px_Dl@5hVB zuRr=*k3RdT@$er$eD@*t;5Q!p%!A>B8~1(#!~Fa$HwT;(~m{ zOX%%h+w4ORe=$8|w{F%+To`3cOH^!m-jq@#ZgD|A>?QPecS`%vLtjE~cZqpI@&Sbp zf+C&j+B8}k#xC$5|MvI1gx+or(?0Y*^%8o!$w4PHZB%{6)r@%{MT6nmaxUoayI(@@ zf`9Bm@4H???}C4@lhLT!^ch+7Dh?~L$@GGJzw0IRF8IeD^nT|{=v}nutAHK(x4SQ< z2Vbz4B3~N{XJjiaCQS#Y@{9h~c?rD>_HyMef`7aH5_%WLcP^`PzpqC`>CmCAnyzwT zJX$ZIcVRq+XC=(W*x}KxS>;gUH#q=5% z;=x4Z`m?FM^m{^CYQ%hHT%ZTPgx-bmSfeTmc7FI2RAuC8PgoTf=rvwK@4|SH6VA~C zQ0x&M+AC(Xn5!4))n7vI!g#EQL!lHZ+Rpln5X`0Z#dr$6gx&@J5GcJTt<9J%)}AvR zk|KUVzBgY?uYO@X*ujX$vBIeYb~ATFfTntZ-gmx)-UWZ#gI?_=^e&7qW6n3Tp+EQH zfw(O5rFJo%e#cAbU5L+n(0k)0^e)&76(@nV!rAF$ViVzrRf@@^MXC@g`2r8>n{*tq~5GI zyP)}{*gk?{$6Q0mK-`%g66Qy}W)% z9vL=zbXsLIT&4wO_#|N_ z#608P=uN+Hv?8dUiv-eqW6@YZCI?r1Vi_Phpq39?Z(1gAulu)167LusCVRM3YG!%{6>KoUm=zK_qxb{M^@_&hu}8NZ=;!fUT9# zI#fE%6>X4dxm@c3S8G$1*yLNlAgzrWMJ{`2BsdM(m(BC-?r%jTI3Qo*@oH%1Ie&LyzGd_LoPUWR0Gq7H7Ku9si!#@;6YoQdfGv9N6^^^*@r6$AjdpX3 zM0)Au{zgAuXXy~>14zM$&sN4{FocWA@XfilT#u|#Nsh>_T%T!E_6wfeThJ&N5s+SI z4Y0XJ-l%%MyU}tlbDbECoWNWYVS}kUBfRcRI%|yNB`i(R_&|3{a8{%ZTT z$eBMR0&ruHYcM{YxSz)f{Ng+C7Tfbu2WF)a(kZ1y+Q`t&YV=0j%GH6J*42Iw0)<;# zn-fb=`VDhXup9qBp%}FX_Fx zwtJ1#P#P*Y|K0ffQSB%@x{_=<%&b|HUKo z(d`>w`|#%d#?|$&`{+|aq0FUoIIsV8|cJD9VbMF47yWY`XyZdE#{_>s4 zoiD%ri?{vTUwP}V-U@Gh)$#P^FWro9e$9=~0~YUnlv|#3<0(0#L1pz^POD~0*v)x| zuabP#ciNeaavcRpTA9(p2~3aC6K8GA_>EHvLXZo~Ww~DNoUq*Z(KDh{oVFI301ArM zxH-U7^4e*_;D~_7foTqkF1-%M0XwHE(?3}ywzL7sV#a2*7P9%=-#sM0@y@xV>2&N4 z+)mfU>yi@ian_*G!J<#i5CKD);uskawM0(H7&)0KYnNEYezwk87j;WK-Ier*&xkVS z3KN31S}O>S@y#WOUIQ2z_f=Aocpp=$z!3Q&fY@==oEb)|5yX?h^gM zGorZA(0QC&Y?_b?3oaSP{bpZ^G-wKvrMy-gIF>ycj8V_eP8^Yvi#{l7ZIU?0ucK)9 z)L4s^Ss!uGi4}@@Q3RPej{+y! z*qhyh-W%x|QEV`tZ;W0V3_FE49dLC>o8|C4Tn1vMR0~3$rP*Y*PGCem2|QmN5GGEN zDF()$<954${C8ga*(3Em^SZYG%D_&KpE7P7>9ZjRP(Te6xW2Nc`o#*OdhOMGjN=t& zSpDgoRJ%%kver`Bb&_Jh&PRkjO$WR8a-H+Rcl`EB=36Bt*yBPUtU}fXhB$Y7t!B61 z#X+?@%dA0Ar9&sFlHh$)!qW+{l5(yrckgbFd*^~$2t1pE)B+&t<{0yNzn-6_~qV z`XO2mm$R5aDMzM}rj~F~<~id@w>X)uodguw46!~b_AoZ^cGC-vzxGU}bb?UBX4Y}i z0T#}io3f=gQF*nd7ekifieADt!(!C$5QF?=#VvcZp#ZO&;sZgXiE^j=Uwba-MC@`K zu_4ECqY;f8I2VK&-)>X$ik8AwrQ~yBD$Z0+l}=QiQpM?LxGs8yJcvgOwnOu{drs6V z^1^b+4HN4feke~Ns6OTEK0b9fIyvbzJu$@B#ca0BPZCQd%VoY%tqh-;Sw+rwiT;Ii zqOvoXa_Dfj8F)VFb;!u?I^}G@`3MzICNnlyX0snen_hl0aa>_U=~Pvw?yQH#pfc%) zC%>aVd``3nFZ5x6Omup(bn%RuW5oy{jm>d$&NMetQD26-Q1lE$I+0ak6Hh42pRI6I z5X+&wL-hFXofGXQbsXcu_M}xGX=qzpNf0jgYs9QJ63uX0n~Yg8$QngEI|=&?XXjMD zS=ozeWfQ_?m+1d;P86CqMr^o&^0e^U)hruU!&YxIQs`FOk7|yKruBJ4GOB@pf?|w4 zQC)z_#l<N96VsR=pBZ3qsfs5=|Td~Eg=$Xd0{vLR161#+ZTt=oPAY1OIUd`vKOGb8xQ+Lwc%9^j z%6Ozmm|2{#eCTZa7*`@MvZuCJ4L(E^g3NP4K{d{`*rIuZEXJfmHfOXMw87F*;x*c^ zqk#od;d8ksL?8$Es%zq-KYdP=#3`FY9J`^GOMZe*heGbLa~-b8NU?+-98kTDO1b!e zJYmh)*|4K3o|(itTB-W(;2;0{=e3Cy29+&v=zf&&ndjb5!r?6TI24HKQ)n zLkY`2ahT;iL@S&jK`Bwz#?$^z@A)&D#3t3N&D80qE|UQWf}$tL4SpfjdSuy|)RkDM zlP#}F8Bl&A%1p3~X*HUucx)7Gusi2{?40IIaJmi2Yz!B9T2HmjN+GyX9@oj08Z_7G zAcQ$kWWEO>{E4QIc&;SZAqG&nIDFMpcH}%Cc_L|TQWJBMxRK~M)Tio7*Io6CJPy0) zNM>8U$@WBA>>}*Rgfl`F*sYlKZAMQAe!fE!IU6Jbq~P}!J~U&Ubl$2)&RX@mO;?TQ zWpm?u>rl0{Vb_s@CUR2Q#u}gmmIEz_W<3UtcfJ1jADt6zTkCNTR=|7=F?cHpU6e>i zJb{C9uE=nl^h}&58Xe1yixb6LSpF)cGc)r&;$?$0!9h%F5 z8>-<@jyAPXEpJl|m@+rxwpa@#=>!n2LXR;Endc@exwqDKiJmW@@ljU+u+39LQ2Wzq z<#kqM<_-Cr??Bil!zDM5ktj8V!kb+@6I14B1G&>6?*wofW zak(KOL#vudEUqCd^|L{#x0G%jTdf=Q#MM}Q+Fjbz~<)q zH#!dYDCcmK`Z|L*R8`0y{> z4ez$^-oEp<@BGm_*`4pY^YHfnc>6zp@Xv0qZsWJV{MJ9X^>ep=;8yR}M{fR;n}6=+ z58WI-{Czh+cH`G?{NjVZa^v5@!B7L zEq<;0+CA{<;Ez37Jou3(-}B_j<6nLFPapru$JJx{@mD|kUmyLMM?d(82LfL4so(T& zZ2vuV)4Vw!trq-fu8D(gdt|jILb71fNv>ht#l31p^CstL}mj-N`#Pf2^Ff0P}~Irhz?!WYpeN6gQqDggw-hp4Mn} zCk<}00dcbjt~&1g@>z=zJl|@Ifsr(zX(q?~rbhVrtffkoClNi?nOb#gRU7JK=hhFO zweXEbGaHG#nZyJcO=fv$xJ@GKFs$ASLA8`Wlxv{BKYd7Z+Bofgu${a4Of*!od{I zQq3%8k?nU_a^!?P+hwL3rmx$ zof(3TS*7I|yfRop^JuneH{~vQ|Eteh2($LYE~`jw4xF0m4ctL%Jp%Z}4JKMALadCY zi&zr!xFT;qJ8RL45^}v}DAbtN9V_purLLzHLgwi>;y3P07vL(z~1-Dzpk z{rhJvXmMk%g^mJF6r(|-XA>iii%qGO&o{K$)R?xrXoV5An)x^W)AJUzZmr8nw~wyN zakyFLSyP32sgJai?raWQQshm$RdX@s?|$s81=UotwkZTDcr_RGVFK^2s##joh_ERQ zh+f!m<7JB=H&o}&Up#ApMofZOQ3?P=Htlw(A_G5xI&)ggrU_ND7RM{Xkj{uKzkU0x zMb}p;MuPKkIHjj@CusK9<8>8`l#(4|@ib0nLR}&(A{^cOy=N_23Ri}LI3%fIVbIei z*XDefvuJ!s4WRL~ZUm!xq?-Yp-}%O~7KjkJh~qXnW#*OB{s5}LaZJM0;X#Nwb128^ zy&6)rrfuoyC(c^HOi5KNEfF#l`g2iB+_p!LYo0eOoWTZV#u~x9g-qn^(O)`iQBRmg z10i`jn@NHw#^uyIPUjlw|8VjqvQ9KXAsQ77iLSA|GOP z61bCXPjrX}>oZn$7wa`wq=v_oNzrBuF zAuB~!s<-Q;I^&Gd9qFt^F{VZ~9!>`J44X1NOKvVT#b6j4HEf!+wZxx;3;14CNq5g8 ziIaDrDcf$$b4ScnmliC>S|?7rpl6+|SULy>?(7q}`Nk@{_Z4S678X3QrwjvL?Go;w z;^LKVgU7o7!EA00%2Ff)8TOe_nU3y$%c;{kbHvQKLT(0|wA00DWg+Eu>CK~B!e#`N zCCx-)jJ1-wY;ndSWBl=^$wA;HFst)my5uUHX!0db8IwTa^iI?z;GkXW@V9>Tti=>h zDGnAOY+ySPyRheN(-9<)4DOkO0Gyyt;8nw-qOpM7_?EL4sWqLVvyjo5GRASU+|(gm zreg)H;kNyCO10>zh9zNTi4I>0deMtFYffmo4XW1|x{27J!p@DKK5Jnwsw}YK zWCrjIO93my<%*EUQyS;|4k*v3;w8!?mTLqwI~+jjnK4hb40Yk6L?`puR-jcRzbNO zs#34ux-o)|8V065{J+jx7&z{(EDq_SPLM3PbdGkdb(0gp0cSf=#yN_3R%_gLpwYvB zdDeoQT8k<|47oFGrO9L|+np5*=OeZQNBwz(uFVD5SfzoRJv@&l({Y_yjO5j%Ribr& z$SA%i#cnHuNSPdqB)cw@MmktAYx3dGpYi~g@4{#v%f1!&X3a%x=-}LGQ)3y@FU?NV z5)qovA<-?n(8E7|CY2lt)PVH8*3#%M6uPgYQFDUPQ@)(f^t_u<0K`CX`Id-2{PbCi z+L#{!$d#tA+nR(EZI`7-34s2X@j2Ptm|cZJJ6*oY>;A*`X$udU=vtOc0|WD1CfbCR z)319D(i&rox#=%`ap-IPCe@s?5B?b#!~=_Jo$=^$J(h}e44qF*04{0x^ZLq=x+UF$ z5lCn)a&CzRSkh{-H-F)*MY~jefWy@7GS-yR8hD~Nleq*AD>urVoG)|?)Ot`!e`ZVf zK6TcjHCGqYT2^nmIJy|7XgsHFJaXE&x6!aXOo_A;iQIr?ox9Xoi)Jy!*Co8_giUr5 zEeE_d_gz(OPSsgHi2NanwB2xt2&3%gN6%Yy!sW&nxvuHf5Jh&FF}%#8#USl;VlcO7 zD&IGltUIXGqq7n8;G8vD8LNq=r}nVgY+8<(CJRo@95nR@d|20HYaou-8G85MJB^AS zY_1pPG=qh1S6CV&Go+VPgPF4vBU(5W2zLh6&~X?p)wj-sf+460ZZV4qG@kTZ_F_~J zzV1(23>1TyEXQfww;>_uEXf;x_^bth3*wq1$@OxF#=A;VW&}2J9a-Sa= z98n)i_s$oVCB~6P9>O!u&4sqB8*5e%dm#reR*lTUa-m;#qy~`=)Z=eD<>8hn)n$FM zVYe_oYqy+vDs)M3gjLizv@xa@b)Tq)bG_Lt?w%Q=}!xHCMV?4+i?0kk_c?bpv3vkUtAf5_vK<)199s=QD0j_xn_=5$w<{{{x3UEM8^Xndh-oXM~^AKBLcaAbd9VQ2S^@H{ zcZq`qxYp{MZ@r733UENF((4`q>|g<|c?i&h1-RxR_?}Y%4yfLG-9zxHg9W(eA^7ft z1-RxR_^wj{4j_8nL-4x}7T}tP;CG%1Z~$<_(`y#slcxd<)=O5O^o4fLw5e69y8_bg z=FKKEa$|F>z?d3#5vdlk26VmeFW-8%d$0i4TCDZ0cRL3QaKNkXtKXfy^=|uAfCFA) zUvELc2Y3Gb(Kj6Z@Qpi-`@P#g_dve?H*dZEWclzP-1^UMf8xR4yZ0-v{hmkvz@4P@sBn$E`4>7Yxc z@r+w8HEE)(z`ZQ7=&{_2z22I)x9)!+1D1Bi?QkU}^ZH^umL@HK=F3pM)~ZobXq0kq z1Z!|DM4b*eSs|J&ah#XPphHCxL>9Cp4C)ZVn9G_8}Z3Tz-HBqBZho} zxgt2*+RZY#x%fbao;T(SYg}q=k1SBSK~>X$L7;_3cEROwoN{5r5MybcNl#(9wP7;w znmyx|SlOrTWRwP&#;%tZ+8(iOyED&gm80vc?8FviLsl$Tx6KE4e8t`(WmbICxjKL`bf-wSl=J2{KTtaDoDlxXXy00URnaK0t8iE zmo~a9MpBB6>Mmis{)uXZOTvw5!YS6nJ}0ktGt=&T^9M3C#;XxJOw@pY<(i5>b7`(* z;1bxZ&BwmdUG{PlLP@PT89gNyL$O~HmA0nV_{fE6bJc3eQ>WRhi>M02d`&}H(a?}i zd{PM$+`{;Qw^=QUm>Z(TPAb!*fBL=*!<;Cwp&MG2+z4}W3Sg9DdBCR(90Y;Mvz5>3 z?Ap_ZI{Gv>S}q)ntg7d3An=xDJVq5EsB&)@O^60Hqc=`BZ>#mD=A6h2UYZd+H6(jO zp6nO)u5aFb`hg4uQPq)xT3X^}nzq!9Bosn7&XA}@6`(p;3~I{Fnpjc5#nUtl*R;&F z+H@5V=42MOnU!y;Q`^r&Q5{%}Js7p|ahuxgBg03YlR?GgzexZ8(h120dUVogH zeUr{~-|8%laTW#G904-uv{uc)8*I{LyGWL1Yne|Nd);>s8O55-P__-v>!c=0UY%BU zH~2t?YywZ*WNTS<`)JOxtnT zY^->J8e-HEo-{zsV@Ypxs}QH>0eW?{zgwUBKn9=FXfkZm!J<3J6r7jG(EwWN+-x|- z7Za`vGIBD{SS=v-DW4?LeB$zyJdjD#k4gABXn)aX#425>2|Q2;GPx;G*NVak{# z1#?SQAz5SdFiY=6_&_)_gf=X?&(WwL@U7~$Y+`!?iw{AuG%0o01ZMB%J5&kFT-%N1dqq$*`#Eg zVV~5;DtMyN(oMJvx+6o~sa!W_qXDlI>eIz+R#8dH_ak0s>=9WiqEqu_s>8-zs^x(< z4>8XL(`>~cCkzYlmodCt&v+2^=d`w4h46;`_yZy0ZrXuZ17sn1xtU`wH(s`@j)g09 z+^4aK5c-B)CsZ|NpMv`<+H%KcH8LrDiN!OJj4@n>O?L*ardLX93{im_l1n)0oM>uj zFBCR*hW2EU`L?v1cKhJ#KagSJ$152#*J^iDr^&&(*IB0+H&{m)We06y8Lno0M=A;| ze_GfnGp6&7T&?qtUQH4#iYJu3 zmcwqRV=g!{wL7Kj)~lRluTENj@RK0;zCyveCK3&0J@Fy5vx$j%y|&3(0w@3>^+T#R zblS6m)PtVDs@c;4u0#A>-IuXsWE%B3T+!;;SD5-GPtZ z9}Jl&=ODa{$5TzoK~n78=1Nlxfoyc)(07-FTbMdD1g@b%XY2JkR&Qs@za_AI;sgKak-7qx6}yY?v*M zt#oy;V$(R~*PU^RXK=M%IrWya^fE{BIb^=aGppOb{JspsJj^6hZt~NismUH)<0xuY zO1#hl3DRCZ+>Ch2!~@RP@~303Goy%(-E?y8YuBoAZ5+`Gn&cE5OoEXODQh+X z&%5nZaU}{37J&&~{q!%q$Gra|@5>-}!(!dGTSI5#1VM{f6=9w1&ZejvK%nm1%GfM1 zch+iwN;gkQ7fOme51e=@=!s`=n3zl*N?(_CM?@7*0W~%JA|PX5Ix&Pv0rrlAqyldv z`?$5*tqgc;^??jEkga0lz8@i+Uo>iv3p=Z2qI>nU-*p*v^yxU@QeVj; z7cZuTHRuqunoNk%d|V>)uuu88!WQ$e6?(1wgjL*BmSzz@wFVi(+Qsg3qFbNY-v2*5 z@{gYU!N;F?_+LLz?)}rdZ{7LITYvoKcOCyN@ayaPeAfT$-Fr7abMiyZ(b4gjy`b<~ zbGPK=fG0>WFA@gF6KFYVlDPt{z8TSan3pb6m znuS%WAbkrwtXjj(P#=`wRHI>~tw3m7MFv!T=Y3Vb_99h7J8LMWuY%`Jl%jz&wO2^h z#C55fi${vO8LAv4r|yug2bw^2E>SfhFa3%J&vaVwLa5ndV-4PCwyApG7pD<;SWu2< zBf%Dj){5}As=occs-L_-)fY;%D>yn7IG)19S4h>6cwJUaV1{B0SDMbxb+YXDjbWu~ z>LseiV4Ms#fo^$4F^9@>jibwKFqREeo=$>%)tvfVV}Jr^9!T1%`qulZe*7X;?-r1f zDRZc?ror&9l&Y`GstE`bph;Lw9qPiyT-Zss5|hiU8n0>~Kh6!?5_mcg5VeAmn>5|2 zdAJOl{vvKyFb9LO2%S27s_L8XtNPK4RJ~j1S=0NbLYZ7oeU(%#MAsBR7=Vhd3f3s( z=R?X|$ZE1ORrV6AMtdkoWgUy9(kL*@gWP=GHMKr?Er?3EZPu%_FD ziDa~kUQYqFQ&5d{46xO2z$Om)bQ5uUi^AkjRek)vsvo>a)w|`!F&;dAHZitGyi%&Z zu2{_okVv_G(J(!M;5J4B*%*m)nX1_#qS4ye1|~Ys@|HeD&{~IQRbCW^>xK+x>QX<<6Ou^9olL2;#uj|kxNvK z2I<@;;FUj8mxE#4?SU#^;dEv};>zfPm!9ruym%=t3Xa9-EMC{bpTo0#_G3! z;C)r!d6BAjcZ;ZD=<3iMa-dAbE3<`xu5GtS%qGKBsAOf<*Yr9#Fu?*hmM&2>DCn8t zol&YP1D$NF()k)@#K1S9l+kRN{+F+KGwAtXGfb^k!KMS5rBkgBk(Ns$-~4 zqMMXB6b+VDny1}#i0+4DaN6}P3PJq2k8b_$$DQM+2N=`Uko5cd9deo99gq-sJ$`hk zBkuUo^BA|kMV_?Ey~SCL-WRPbF8CtAc!8i($ct^;vFlB&RkFn)*#g3V3(V;$@&y1T z`Oa-s^zWQnsR#bp-2V0aS0C#4DY*~+c4=92!0>}_?BFo(&_2U1IQ`{T#xr?#gggb` zcvLrGx*oEt~ws3pe#HguJk_ zI=Hx!9VhSu@Dv?g@}`t_3GnuEqNIs7n9^m7?7E;j^CSvEDRUs^7gCrRAQx~Zi@E-Y zi>Lk8(i%9aFJ+zHn?{r@L7BCLC)7rdN|x|w(-B-=Z#uctQ#)O<)5o;FqVbGTUt+^d zu7Xt<)YWB7_j15$8=D8qDK`tc!0N6u77Tf;~@dw@1VN zkZ)%#OUV1;bk4pZgL>&@0(Ciy%h)^vC1pTGH+x{)Z7xwTP)4?!%|Youc&&o0-{MX5 z&3@pp)t&oblL=B$bOt-Z5|-Nas`Nz8-qYB( zT?fX#-T!~p(a#*c)_t;g{Iie#(W7sCs6F`c`+xoZ@x4#p&F}oF+yC3`Z@Q)5{D~X? z^No)j55X6|igey z^mNF|iw4-E7)nl43YLK^@F_{swE;Y$XKR{awXD%_L)X;UedXBqdsoZ5ox-eXHmx8)JZE2+-SV0O=5Pzvz9Z9>=c9H<01#D-j>t)r8DQ0FQje6H;B(R0bsP`@z zOvk&S8$u5OLVyrj2+1Q55&|R`V@LrKr@X++OMs6w9wCGzkbHL}Ew)G6(Tv#_c<-4% z7Tfzf=l$olTijs2%X^PP4ZP>@UEagt%+hl7=-)uu;yzF{RG=E4+Eood z7C>TZQtU4KeHDwpq6tVf{#wi8ix%^!JZenhMNcvk37UvytwI0jwOlpmxYc^tUo5ne z1`-cc<0uwN(hq|(SVIn~aoDbEklsK%QYuAK{;(yhu7f;>eq07)K{8v)MIxTE1C7Jzen_ancn>h(@d$rr-TxsD=zwRlOWN`o|i!xDQkfDX7LFyQ%>z+%1hSPdHLV zl|@r@7o=Xd-Ji0@ji!h)r;dAd@=7Wc&3YBfc1iRPARjsw-mRH8;d9!Q~vGV1dL z=%?BltRVr_ICxhzeBiYny)S96>5Z+ZELhCxLzaUZA})Hrw0u4NIHJ)R zq-u{;VhV>O<>jiua@>&9ajSLKgdTMy>e-wR+$pONgq1egW3UD_ZXB?y8d|w42A+O! z%t?#jRSu9M%!@P&R*6Z879GwANtRrxra>|1t+|$)v&!kGt(L1U9ruB%p#;^~e^)iq z=2A(EDkUnpMp0j1^OL zDXfd)JJ^JX(7WaXwL5<=c-&GAu$`X))+bEuLA|r_?E7Xn9VqTVRrK@_Pg4Nv` zaFST92s_-%b+~etqj(Q)wK3-RH(M5i+fFu3NCSz{-~C{)1~rO*Y*#fnB*Iy)m06DB zaysq4 z>+k)to^cw=Uh=FW4fz;^q#Al?0ima~r$eiyf>Vc~{v}E2^BN%YK%;DWFK<7DsT^4!&)aduagdWysE_CE$lnJ!7=`+H-VQee*%^iU zc-{`zKOh^UP#@Fou>KgbG79zaydCC4Aq%5WpBCS7{MnG1QK+9keBvg^#3f=Pu>QxeVl0fb!S5wMxj1VwEY?nq-GTA;}6@DzlKzd zLVe7%echFil2NFiKOFfWq+k*f_cQLN{|U(%h58xyurefL6zXT($>Sj@qfj5?KK+-s zAqk^UALBm#y`vy8qfj5?K7Abm%`*!1=^>sjtc8we6zbyB<`{+gc*N7ZJhYZk zsEf;gH_InyShEb@GM?CeL!=a-Y zh5C5JQ~xG|j$#z*;}K6?7>ABz6zbyqfj4@cxqz>TGKxN z&%0u1qAq%8WE}iq-+%jB;KOVIaPDzt>D(iS&B4K&d9-tn)B)DM*Bdmwr?1v<$-&+K z`T>V5;%x=2&OHv?hoz2S4ZeCn?@-zYxAM6PIMxgfZh=$HWHt=WbtRGo>Kqrfn@Azp zoqLp0(EqM;kNt@6<+(?xTqafls);4ucZFgFd#EUH8Y}ivq+YMKY$*Y-5Yryn>2(C}2L3&@i*bbf%gPn4(Rh&(G-62g(7juJG`MipF&hE!l+EQ6zNEeVg zo8$HEs#C}7H!ZHAoc&@xIOOY4Ny?^DI%O^^@)es-Va>{F{yBTCrZL3Rcrh0TukQJx zo@CTN_+txgn4ylB_8n$az0SUd%e^A1U*&Qna)gk#EkPyl^9xLOlz4{5uHpAvBU?>lxZm8ccV9y|7d<6XY6*9sCp69l1C<33MX zsVn9UC8JzbL}h`jw?@Rs|8%kZPB4#IEC z@Z#!r_SVTy^4O}*XN?yhZBT-h=5Q86E1M|tUahKo&Q(-_XeU^ zEnD`QnF^QsA~sm6aEtwR-+V=_&m&ElT`!vMFk7E*@m6$q=#E>f!M-Shtf=wn0USBMT4Tn_&5fBio&F*s&vV!+|SycL@T zYx>^1t3lIH4%lqzh&?C=P_{@T%~n;{bhOH zF>k8)T{R0xn;nf8#lxF*tf@V!*+tRxbSdT)tZ~^N|lf4v!l43%@?&@rM(HeTaN0 z69c7u`5C$b#;s)s3|ld$KZZLSZmC@tM(kdh-B{A+&4|O3hGpiuCg27uQqYWhbN@UW zcA2xWlHRE#+$mLFU$VyK`Gh)UEF0@$vq2h46u_(47+i_zBOw1;x)#Afx=gP*V+yDY zm?acR7KxJHk2<^-(|j{KAD^xGQFaSts_N7Nxw3c^6^K-t84ljHa)Bu6H$(r(2O<}O z)aDj#pO;#R220fp*n<3U0`PqT)V}AxKFthR*DgB%T1mUllrLFj8JwJ*_n;MntW}@4 zhCNjx5|bn%4s9OJ6lOK`WO}dT4sOx%RCg0ZYN z8ym9)OkV-{$ty(#l5MmSibBN|38}5FMAlO?G*O+)lLLcdB230|-W&qbP*D9qHAnRX zV1YyJsfXfZu$?qSq5u~zGaQ_0<>F4B?S=D9uMni7(()JCvP7TzfOj&tn#N){j-{mTcKz$NV*))7LWSnsx(OB$HlPYE~BY znfX+SB(+Gv5Kk7PZbVm2E2B1}&hBg}JrbR~CbODQb4Hz&rpuaih~@g9sygUy#>So* z@(%rK2>SKNwIf;HI^MKUJLQ@>8j3(_XmaRR{1f>f~r z^rq-3(T$>_@D1U2h1ZMpqCk@I8NZ?@diISC%jhpI9up7 z!tbz!UM+l#E%dX(Z?lDdN%%Zl=$*nJv4vhD{5Q7HD};}*g3!n@Q3TsJpGE zSk-N=;k2WIqU%LJW~=qf!WY>>?-IVi7J7s5yKJG?3!h{Qy-xT9Tj+hlpR$GCE8NKz zdXMlYY@v4xmrOt+gfkz@+hYwIQfZZ|6`Hk1yd$S}wGM>3NBnG|juE^IwYwXwN<~|V zWO5~6&XTXu2ngfCE7(F~!pqr0N#SK|p;6(bY@re1C2XN#;l*sBA>l=Ap{EN!!xkD8 zE_R_#t-Tb6(>d_9C*}<&;%=G=Mz0$E0bA&2M}OFb4toDnc$x4UY@wG5A7Tr=Nw|$I z^mD?e*g|gZQT?dY_H;9(Tf`J6C9Q}S*>%b1)H?n~()ISnr3q5rt#1^_? zBmyRcce9%ke)y5WFXfs3YWUK zfzaE8OT+O%=of@bqv1g4t-__za3J&+;nE;K5PGw4X^iqxVLr)G(Z=QO0 zsx|q}WNqTDiPHERk}OBUOaS zaClA^0H=TKVz0VjN-FbK)EF~nF_)hW15Jkk2dy(4QYEorpzbi>pmj#-H6e;z$ZrkR zVu%vQw3&iRg`y!i?~t|3=1N2BR%zV@j4ZKupZ%-_RfjgCGdV|DPqAIyMXx9R?h<&ZLY*BN@`t2)H&HOkarkx&^qI^TI;n8YL+^hs<^bG@I9tN=CC!x zIb}#~2_zkLHVkAP1{}1`h?9gk9MM+MoY)Y|CERZJT*X8hZAP3Zr&9?lIFvnSsL#PL zTxP>S+F`&!>rAek54B__kJphQ(>6=YWpf+!bye1f`Bau>O&$Q*FQXCEp)jywAn7pR zpmhezm?9ygz6nl%BaVDFp9q^h88cRk7k$;7!k7sf5m_;eRc(npn>G-47;w-!V^Jav zEGouPsoPd3tif!!=!oh}o{~%(^|X+5Je$y2jDD%b;bp^MzQcfn))@{7%h)hDzQcfn z))`03K^n@Lgs!GYRkS`*>BGDgqF9f6Q~Ii`>Bw62F0$6D1sZ8K4CXovIAk!JsisrS zmfjvMhRtL_2dj;#qL|FILM6KlRl+8<9K1kcC{@#juzYY@ud^1`b{KHbI%CW^G)_;+ zmvs`_dO8v;g~7&oz+AKw<@%h}JeNu8LN!myFLgQDFqrKy;GlKJYAshC5`U9afzUHK zBe=?_Ff~iHpjqsI342rLF=-`nX}}>>Qb>nIY~qn z+PvCe^qCXxphhi2iGtlnRIF?m9NS^QLFZ;_Rb|R<%pKhbV z6+IJv*21a|0}fheR`gix*wC9pilNOA?*@>6?`Y`P(3eK4BL@!uboka`a(Ip4Rl$P5 z43(zOo&GNLiRn*^J|ozl{}cXId_V7RLz{;l0s94yPHvp|=fvX^7fskE#>am!UL61U z*oz{oP$sMi|1k1`@GE1NjoHTz8hvr}`q2|a{}4Sj)qqk{Cqi;i@X(3FPT3@H@U5Oc zk0|k9R1Q0jri2Hjl?JSQ*{2&YQ_zVjX>`Ppw|c63WiLw)wGnnNxTMUX}g$R zy`9_9{j}xN$QZ;WP?em)uy(vK2`;*@N)YiCV$qp=qne3pF z(Q|r?+n3Z2dcKc^nWa6i_wip?vKLGByX?jP@shn*iuT%z|C6PLXQ^WMyAS-GOU7m? z+zqVfm&$Y}5M!erHf0;BR3MvoIR_hC@c5E7SPC;4TdJX_iV-}rWDS<;cUeR5?Imll z6z#Q!;9DG9W_N1{9$m5q%kn$c5Inq8Cd-K3BlJ#QvIa|GCTpl<^wcxs@QSLZr!5*k zX-R#}f`@a46xqzWl6qs>U+V9OOzai3tdvrXsJY}2EYdbq%G8NNBfI7TEXT#&D>nX2x8#ta37d3wchn4C;&7$x zwR(S(cS;@!bQ?2-pfxvgipCVCa2gvtz%@ z*aF`B#%3qlYit3ZT136dxRUzc@yjJ6vK*L}e54F+2vz35TWWj0twtMJ#-CfV1WWb3 zmKc9_$q!hn*yRV~JKFjGAnnjZOLWKZ5#SH|{^Q&NTYQUasViHToB*zDv456h@CFqv zmk^bzqF*i{P2XHX94?RTRmuPkmqPoe4B)Wy*zeAAC(izxu1V6na;lif3Z=x8QOX!o z;z_8dzX#+W>c8Oxa=Fnu7^<+n%!wxxE0xr%#C@+v6{W3QS=tEejHtp=N$XVeb_~-N z8}gjAX0c_J)r7oYG6o!Gv2m|&W17P@)NKxERj>vPWJ^t-ACE`C1FY(R#6FMZEoPTZ zmq9Zm;V#>4_OwJ3t;gYFOfg&c1QM;3(vPU?ey7J9ai?qLK+GXG>djhKw=vsQr_vD! z7H37I^Bn#~MQCL=;AycvkDxvJwH`x1TF8c2dcCObk+s?cDyWbo> z9rO?Oav5Bir5a`ToMHc>)F9b%eAoTb3fZVoJi_^C`%!LC)!qbR)FtNL0N%?ulxk#B zsz>M>hc*O_>TD`~OjAmUL#UJhZ#ZJ6Sj*!>(X_AWgmYwBo=hh_#NIzsjOKG$rPiRq ziP>nvK98CeQ9C#?S0G7GD``$Rnp&IGWj2*$O{{8ZMl2d-J|@OVUm<}-5_X?kE7#6u z$b<>8h6?k#W>(?Tr(kwzL8z)zacEyWu06!gf-q$Ny@>M=yIfXSjiTSJ=#PBVarm&+ zFY0>dnjcaC_8|g#$$Gw&i3Y)&=h09&L$*hmrMzKOcFW%QD_6kt=)pAEj$5iKdmE5y z@J#Y@0SSjHt&FZ#vIHX1q|@RpnT>hLd^CdxX0d`hZbo5oEt&VG%f#N_8!l_?QClMm zzOzP4DKcNgiyE2PT0^YLv}V?!b41EG5pgH92DjNmkeYclnl6+;cMwsfF^^j#t0Dw0 zmxT~4uOns6LfWI3;aYZi!@5;3k%deMq;U+k+msr!xO|rc2@&{Pc#Fq&hSn^!vfp(F z46S0y@_mO^`Txiu=kV>gUyx@(F3t~cXk8jbySe{>WqJqO-7|AiVb>ejdmYD8`LcO% zCGlcO>bCgPnA4>XMSOXwPSGqIP(#g7F8Y*s)0;&!v(<9$1Ed=Xt7KA#FCb|Y=PgL2 zrLnbQFzG{M3Y9-sH|1i4E14?bwN}>AOw87BJZbR6!WFOG5Z6?*Y1r$iBCt;CBkM{P zoHsko;FO{TWf{ky>L3cK_5a$T2ZyHbn6^#*dFsX~bn^AdD<;(wFHKxHF*p9g_@?nS zV^5D2#y&avtxYGUu-V! zKk2XB_r{6e@tr$!O7=J3T0Qde^~e4aUv#w<`$Cmy!c&u4QwEz8N5fgdtRUjB#F9f| zv4|05QgeCmTv?06yNdl^-f+PQZ%ZZ5!j2oRD}7_*kmC44@XW^#YQE*hH}OZN@I_}^ zF{W3QidD7JyPWK$ICSd z;fB(;`J2!C?ib^)C$DqiH-GK;~oiBz%=??zi0Zrvtya^^1T1;X(VqG`IThyAn^2{_zj*9eCiC z8hp{-R-DU4Q$7#o@SD}vy1i^EY314UoU!5{>PfB2oX*;@fW}A^%V@r%_@;WqFS+dW z`z9Xy?fBg%A9dxg=1w|e&GZAeUHc!`y>-Fs_aBEZ+B$8_{w3;;VnU1`kht`lE?NDo z*KfoheQ(J3>o3nfcgW_`ZolFD=MH=OoA{!&t+-V}o8eLb4ayWchs$EMNMw;Yt6ob; zqNGo&k>U=-ktl=FRH^GIe(wGUa$iUOxmkPC$9K$c_{BTlL4&@H#QK`&4f};VzI5d+ z_@br57&V~A6ygb0qt2SolPlCwv7(j_mIHdZF71d##0End)`tr@W0&#j$JdD6saGA3 zMqj;S=)qV1w*D(uG(L{LRED3rA3F8+6253|D>kuz&AOxbsD*FM{_5_FlaEQ(ymj8y zKUW=d{&)GmD@z`~^s%+C)JWeX-mFANcPt?L4duXCK|Z>cYGB!xs&0#T>2%b(&)C_UJK*-=;CbpK6Tw$6Pv!X;fnZgU0;9wG~FLwhY}b4 z{;%g>FBFts#TRvL#T@SEbQHg!JTq|KB}aUt@}*OcGiYDE%suhaFaMNzi+|AV?;ZM& z=brfrzNl?0=5Xz%qxg*Le|*``e|_K!-}~OWvzuT4$H%VOc-!yye{*a2_49bE-#qHz z1Mx+)t(e1YnvP=LVO!Qec{Y9sKk(VRzH`GpzpQ@k)+@c|-4i|V1p96C>d7R&h_n@R zxO&r3{8I#erFO04%P$GPsGkaK`rWGQdFTCQ$F27~DSu@0IDPAWd=YLd=5YI_qc}Qw z@7d2^&ATme$rTqnUWp%eQaINLzCj2szGFX6@k+4{Up%F)n8T%-j$-YO3(h_B5(XX9-g!YL1Cw}faHGL|+xW28J!(Ew<;y-V>Z|A|66>2v<{`}Q* zt1tJS^U3wMJoM7`H{aS1{^cRJ2SI?G+*Zut3QR}w#h+Pd-WT8S!cC7n8QFCGYr4PO zcq4q_6Lh2&)>N7 z`DdU0#^2J9|8(J9eDTD#Vh$HpI*NaEJ(<`@5}bOXI1N zkC;AtXdGWWp{tFiHy~;!IMNL~V zhg%~Z#q&285B~Q9#-o>fr@a2UhcBxj?)wV=ZWl+-O=#C%s9gJJd{Nz2%;7RfNAdLe z!EJ|5|2_4Ul=|^wPbuB@tnszuE`y)C>7Ik$JmT|LJ$g94sA?Q3w7kKFdDZ{dsbwqg!fJvxfll;4Bb{rI}!CmzSv-}B(bM{d1K zvq}8U%nWUz%tjEevchWuDb5OGDg9ncf9`ZH=KU%4WEb4^W%%+ zwqg#~C_0LdS*Q6U7P?dN+EEAp23~9X{m#(N(8kw}Xf5c~Pad@8#RYtEzQdUP?TC(I zywutmBJo+lo1SQ|%}|Ui#Bt%>T`{jePOBdwz7= zad%zv>*vfDZ2aN<=AC1*=T1FoExtJ0R?OiGWJmGQk{2JC*ZlRSSLa?%KXmWol^-5D zp1lw{O4{04d-r;<0VT95!@2iZ9jO`z`A)N3Z$KAur{g{Nlz_D%jSi_`#n+-lrdJ zKK|n;zcI#pXvhN{Gc@wX$hMK2M;arJkz+=Nz&-!_hZlxVA6D~D0c-g;pl#DTr|+L$ zm_B`4J-y%5Yg3O+T{~5nGEA)kHvoPxdFSNWlfP9IZ`?FX_7JT`jmXaQsfTqSx(^aIhIg4YC(39c0s1P0OB zB0?k~BZ&oxIilLL?iq+N`ZqtrGWFN|_|>W9!6yEOGa;#NERZcXt~{ zL_+39pzNrH0|^*2o1^s00irD|aTl`0UC_aK!`iAto3wfeQ$X8<9T8f)jQ^~S^JiUE zqc7Re7eYlHsdZWcG~DDEOWY{1?=fsF%2MArOWcI;Am|`A<^Es8blNXusXAL(li6+c zxb?bXihj>ys^eGwfw=Qo>N}4m?p&6*b2_+zemdF45d;1Hrz~+hS>k@e68GZ{&IJcd zWIli-Fw$T)r$Y2ZOcM}0-1uKu;@)P7`!h@2TP$&ZVu^dRgR{lzkrwI5H2r#?1r1f{ z{rIp5#Bbxnh~YLre>{Xabfpk!L5l`wvXsox`pJ=#S>jG$iCf1Kr(uaxv&5-b;*=en z6ZVATVT&<~YD3wi)_~I5Mf5OB+&5U_9%706H>Vw0pFlSPU()#4WJIb=MUG%k8c! z2I9Kwih;P!JdqfP>&z1e<2v)i!MM&maWL*|mhE>|2RCs2_8FGAu6+mUTcE!GA2f8@ z(DZ3j|2B2NWO3rDi6h4IW8WQHHM&Xk6OlrA#mK88>xVBB{6=sJ|8m}Mc^GsnSc9L& zW^Q!OszvSe5JVpB92y!GLBqovL_99}*7mr0JvjLq56WKhr1*I1r{ulmjz_C$k#Nr8 zi|F*wXqV$C#gR)wwl2laU8KPjDQ?G{Nc}waFRU4^UPP!?Ja$(rj?ZxTzVv=8wvYJ? z&XzPz(L40z_Rn80M^Z^|##M_j&D3LD(#pM0l6q-=Fvl^9-Tq&pejf1ozH&n3}bm*?&mzy~wsQykCxH1%`KFl(l(7Bv*p zJqAq)he0VNZ zAqXpNY@xVi-=cmNrdcywwWy-C;^+*AFSvWQB8Cx7iy?{=#e_5EaMeQe{cWnNe?tEz z_0zCytkd%)fbmF4aolZS7ja2D_&&$Zcr=hC)xSRhuWSE2dAWv^(;6R;RM1EYxnw5f zN|Nf|pV0iG{qsLrlRRosPLbTBf9LR#x=V78{(WF`w)=PSEX7oE3u~rp7iARF-G=vJ zE=dl%O#2M)1NpZ5_fi8aro z79}(f;B!fw*yXuL|2~juyMO=K^%T?nPhib-)uNbUx<~)cA;DsoX}5nL$g$nOPu)jx zoIaQ}$HN!r>HRy0V*_1^-Tr+bMT*<_i`37FMb->gFCI^|;%G__ z6gd;%aPW@}fkK-Ahx3msZGt>!0xN8S9A^S6Y=Uf?0Ed&JD{X=dX96p1f;49WD{O*P zn*ax;D{X=#X96p1f<&7Dhvn?bW`f_mHQpw`VI{b-3BWgRjd3QxVdgbB_x0wjBxeE~ z&bYE65N#9SusmDY{NtOqMmQ5#VH1Qo6IfvrgxUl+oVH$R6P(VOzzUln$eF+jn_y#` z0EeaYN}FHK+9tq3=}Ma*z?r}bo50^O0sE^4EBg?`=B-4V0EYwq zGaS;64bJ|ud8>~z0kr|qDgD-%0yg<2S_dj?8Ov!^GJ}R1I$b&!E@tLx4ZDfNwPH2| zaLxocWStpIpp*aa=Ant&=;FvUbTh+Be&4S?gcevxFN&$eN0&t)F8i5-=V7!YDT@9% zQ9$y_emPM%bVF3K6)yc|J4H!5!^iIF!#He^_RDdy`@SwkX!jgq1NSG}>0-!Slq^wc zZkF5pqklM|q;}63SS^#KGElIIy@qwGBw{g@{G=@#jFPcXHB$jPsAOR8x6Y9Y9_b4DwktR(E2 zV%(j{l8IzREjR02UU5}ra>eU@ZQUr3q?{E)86-9XG|S0&E>x+O$d1wXOi>N$wP_6* zl`0ex&AL#~9))}vFyHYscj%T*<< z(X6V=vfvPJBIhUPCAg^`t<^Qyybh%NkXmOo@(^zH_*w*l)_RGmL;RkmW|%M zj=UsX4;fT>*io4^ppinAa1#0kKC8!bnXJ|zK?<-$ZGknCsKsR?lg4N*WWsbBR}#Tw zu_773+*XBOuWd1_Qy)Asl_qdd3an(8B$h8YR?^MIhBs=L?D zE|xFr?BwjMS6o&(kZ@gYm6h~So8P02q*P?dmBzzlHV`ox5S#?h;qAS%&zj>&wG1)a z!>xo1t5_rHRx4p^dS^AWh}vn*Ai8ia8MdmWPQBRevCb+zxm*h~h-W3q6kamA{4sI9 zP}2}Z$RbWSP1$n27-5-KR|cw1<^CI4oa^NNt3q5p+YVmO(5AKYw7OEtZhdqAse9dj z%?yX77NvNG!`iTo|G;T=Ijo%f4{Yu4-xQbKV|U=E=5{L%oD$Q&hf`9!Pl^tmK&DvO)GzK&tSuG15I`t%D3WL}r31muAROQpj zbde(BZNeeHc&~>Y_qCM%u4Y^l=6XG|e?Yb5$e zAR5V=Y$!a7CE|{xqDdAr60hE25~D^mLLzd7c{Wg=)v`*gE2F9oB5@3?X)V0ukYJ-g zED-R2%KtL|Oun7Jn)fH(cc*_eecNzMG4_M8TgNJ6dhqJs>!aTsy>v7@DjyvY?G)W5 zIzwa=trET|{GRYyVM=(SaBAe`kq1W3AHheC8UEYwbHg_emxi^&2ZDS64+}2lUBgTA zPT);L{{ej!x&ZP)#}55{=-K5L;owCI=*H!Lz3(?4W(&;mH}Lw;BxOqar3~mwCN%wA zKeUwrEvBEkgnUeB`YIm6nb7pZG?14GP45mN4-=aHauDQZLen1tf?UV({ZRk*rt{c; zmVfGUPz~)ksAPE(QVkspIhoKsMt3ZamMQ7?B%sTg&^<=Kf(hMY^h=r0Jx0Hb2~BTo z=prU`kI^q?LiZT`lI1Pg^8lubL9?RAlAmEh_gHd)3EdOEi%jSqyKiAa_t^acCUlS8 zFJwaZ*!_GabdTN7V?y`X{TwEAkKNB@usgkyN(RmH9=o5-gzmBXSxo32yPwH~?y>u3 zCUlS8&tO9LSaK5+y2p|&CUlP_n+&$06Hzc|mi5@C!G!LyO`QqdW1AWiy2mzECUlQ& zDokj)yFz6qG`#_#5)-<|l0_zTk0lFC=$@F&GuWL@M9!dD+GF<|6S~LlStfLk-7`$+ z9=oTR&^>lfF`;|xo@7Gz*ge67rZ+bfXF~VbJ;sFYu{+6Nce-jBgJ$qyV9##`P?QPX zWA_LXy2tKeCUlS8Lrmx%yPwX4?y-B23EgAKjZA2Ib3+@L&^?wsjlq(1u#`cwxW|&G zGNF4c8DK*9Skljg?y)4XZ~gyaE{gvTmCfLm`_}(GpYCMhp3ZCE`acrNB*3olI<*>P zu-Ui%SJDR|Xy5vuK4;pu{-;l{_O1VWo=AoEt^et9vv2(`>luLet^es>vTyw_?HPmj zt^es>vTyxQZ;_AK`u`+eeMm6Ef0ln0|5W}O-v8s>KmFA7rPE&U-TyaJcTbh4)=fc^ z+a@oY#3w&B@#~2%PgEvOnBa{+J%0JPZ~XAF-;CWoRvkNWj6b@4^omhp^a#-_qI*OY z(Fr1f@EPG&p5RC?p$quz&~1!@bcb{3{2rs$Y4;$|EsTP6hqLRwIt+tN zgdVz;QIPI%!1Pw|5`o+6f^K3Iq&pme8Ze#3X?K~R&oc_r9gZ;D^mxF8x^&RZOb$nH zR2^(_S{+8S!45N(*K>lz=7;@=!{J9<(B~Kh=?>?21U$IjW3R@KHDBb(xINwTyyvhtuioHV5cc%?9W?MnSs6 zfq*i35ueZHgRWr|q&u9!Y4-SVr{3y-u4WXZI~-=PX|1px493t^jDmEB^EiD7=#P!C z5&A5X!_nI&fCb!sqeYLwr!bY*b9BY%@R&?G8|-&OS27CH9nS1`8~sL`&J}>RG78ci z4%NHdu+16pd!fr21?dil8ohq4$L6v-peq;!=?>>~TP-e!(_?_4%NPaee&ci)5SPK^ zv|6A`83pNn}GQ1GGj*FW!i(rFX4`tE@_J6k zXmz*))$6r(19TCiAl>|SuT5{Zn2m@Q`V6BW-TW|^OCb)w-=c@W8<)#ZZ)J4z+x0G& z(d=*<-OvJ~Al>{v9j3Ec40ekV+QKMEk14Z*(Av#@-0z1jWE7-ZBcOMK8-)(s;pj}Ygx2V^ z>v0r0nW?;xEC)qw{Pt;G$U!zf61IBmd;xjjzIYlqHe6r?+x&xu-`26MoGLT51w z(jCr^7zw|}VDfvQ&D8h*$)V)X7&H+dR*cI>5>wAkUO$xNeP4J3bfbVLdTxbQm*+|Z z=SO%}E!^-+8#TXFFAn)MRU_8^b2)@*T2XCYZr8$fGzHhK9$O)R;ngUX$%cG*(}-0v zq%&W)YISwLG?O-B(M&iAYm8c*8R%^JvjC4Gl86*<64dXt;b&JO;v8D>;u+)|aV~JWs=2Y6X@wydmmmzJJVQ?uTm^bclBMHCMmWjx$ zR9Q`{uU!_9c&;6uu?@LYQUQSn<>YW&3e3K z^Mp}2rz<3M;<*@7h+?QqYHl{l#z?bS3EC@8C!B{8AH62Cc~+>&v~V%1(>rY#E(zqK z6@R8+@OsPb=A|_m96@rXdZHXP`DAWXne<=*y{oAX1`EbwBo)%Mg8GbZ4kpcwdLBxB z^qS1tTA?P><8!g3xMlHFyiuv75cO5`rd-?mn|n4Hk`PzQ(zM$d4CE6qS`y2up0eNR zQfcf#cd}uP=xtHGr-4|~7H1wxe)O8mwo-wqPZ#kVp0#V!@<=A0jY!~XgmCupGE^qF z6pgBICW9snF~UwZN+Xb^O0*Zv2CTi3{#ZCGF!_o zZB)Zp)Q)>?Vsj?lugUzfIlqOdq^iDFEgi{gG)7t09M)oTgPP2i&|0#FMI%MAFCPO< z7XIipnSG`DGM$ESiwk*|%x1FZke1i0cX_??K24@A)tX*wO;>?6szy>~v4h@TuPM1z zNE8cKsy0K;6;Wa`tQ3yK^AP!wYch#VyFz^#7HwGcfqK9#*LXZg-XoJ&+^K$j8EVZo za=xl2iRvOmIckt;5QQ1b7MuxV4X-tnuDqGhJK_zkvjIlT^hdDCRw^)2MIwS)8)VW} z40*j-U#8Fm6Zk$}hQNpxMNR&qre>44nq6lzvn*K*}Dl7P5xU*?b*bGTn? zlo%>==CoCpAz`@^2@$FcPRz-zE`3g>PZZO+svgYCq945`Td_$7CNKGOg}I2dAoCdl zC65Oz=3C-EUIvqCvnrB}l|mT}szSvUu~h0YnR78xo(-sET8S1@q{(KoYAypWi+tpo zOk!QB5mR5l&9a2onW`AW(gNboL^X5eY^}!f$3^Wh0Ya1 zm=ENvi7DMtMM@b_hUPr7phQw}J3P`x#L*(dATZ-&kYmU@v~y_sem=n$^F_Ruhj$L& zKfE9k311eyBl>~p&S~}Zep9c3`~TNYpFUNXGEA+Sd}s0plXp&@JxNT8Cq)x4PdqfX zFm^is>|wR=ppiF5wh0OXgJ2c^9jGCENO-w0Av|UDTHfWn1n(5yK_X(bFy0v6Zz2I5 zGjTaIBzSFTr${_%7+nRu0d0eB<~_v!0sqe7(Sn9i%CGMV)p&`ef5&gmb)+u*j ze-LMh>%6xd8)&ywM_)OdPP>pm#v4I!JZ81(X!afMyq7-MUpnuH55{%g4I#k59usAxkH8GAiJ zmKyE~&HpAk?|l!(b>90PJPvmItAXRfg=0IpjF$uJ(RnX@a6P*5J5XQez4XEL?fSvM z{-xU=3~ZN6So&3`KZsQgZd0U{t6|Pu-k-NPd^G!x%y#q*#2v>HcPvZXF&*5%xa`iu z2I9K?^T2wn1@pm`A3x$mjBsf^1c}$|24^%Ba?;v)yyFLhacuqI(oXvg96wHCi93-c z?gW;&bu4k34sKw4bmMm*j_l|&5lL?$9JXgHVS_L0h?i;hfbL?6`w~msoh)&8u*BWY z68A-xxZ7CbzQ7W9D@)uhEO9rp#C^Vl!_r<|E?UG&NitKZhtUMhzT*OxI6g}pk0q`< z&mPFHJI@}78wRteW7y23+WZF3>$>ytf&Jth_5I(>+sd1IW9sp#tEOU8>ZviliGLXH z54^{^nf@G;YbM_6$@E8zA2;?c$no}nq@~KHxJ-nH2;Y#QSeITYh zMUo!fO!pKe^z&6ve)(F;deKa|hkDUWxrZe516xprL%x**8CZ+yC#)c*dy3+o)@I66 zB&MIGf|&9Y^(LQW%2U*fX3A3}?(uG>JVm`|lBorIVu305khllUlzT|r_s!>9uoKXD^u>FUi60Ly}Rcu;Q`CrrcahlhTDXF&K1Y_@$Nx0=0NRF`;`rO3Q@q@hFrD-Qyz&6S~JoU?y~rkDS8bBlPC}ID=+YkKNZZq3Noj zlbO&xwmFFj-P4n3n9x0zJdqRngrOlOe2;Y)G8al!J=S5!YuX3RkPWjBnj!OEAGCzQ zPw3|P*zzqRllHU-Qx4SLED@FEdGuVk{UigA-Yk)2CUj3fRhkq&J#;8fH8ghg=)a8~ zC3<{1KRrD4xv9CyCnlv6&rO^({``1)>R}v&BTf-Tt-=ZOKXI5$|i?qa=B>A=fNC) zv)Wp>mn|i&Je!^~RvbhQyT&94rGG8OQM6COyRhBE5&1hlmMygtv? zoin+m35q&;!vQlsE#lMrlJ=V3*ow-6#hgB*2|5u%VJ??iiNYLV4uum|c|=pHvo5Oh zy4Es@@)h)+jCy@5w537u0ab66&}O(4K!Y-c&f&6HEfQH|&Z^fEk|^oZYNWUWaU{yk zLRzU~&8PF25~y^&TWM21IcJ_Tms3~{wz@6ixFQ_3HIurA(@~DJ3}~thmlavLO_5Ts zPS=S>8UqJALnN8gdz_|H|Mq39bk9vns?zZ-)vmqEIR#NgoQgRVpYx~+h^Z1cphd)P z52Z6kZM50&67+th zwc7M9L|>7`q)4OX^hq_%B#IRiFzb?fA3Y?ultFp(pgf5-pz-DHHiIf7F5=~)Tj$B8 zBvz|TqiTg6PAz5*Yp|fo-c;#Wmq)c-uvE>E<@lCTw*_OSk}svHrS%mL)<_j$G8~@M z1;E==cClAoFeR0FD{71xvzW`zx}4n~dIZi>1VvF111FjWhuRI}iM-ZV@D=NUYDJdw zN%9379#=}z`c%bHK-F@sFU`6rX6K=gFrA1gP<%iWOT8vUkqh~)p;`=4;+Qs5P^nNf z1m_*Hmf2isXx%EUyMU1;_9lHlV)ETeyR1!pJSU!0*Ysj%Xs&L?!k)59tFlSmk~pH# z8Ki2DRALH;B<0mQYeJ7Y67_7(hv!lig0SvQ#L%aKRHd_9a)1xz$0=jcNQN{yBI+iq zYFCO>l=7|^8OecM0d-|6S}jURz0Z_1>YS|k^!oAG--rIXOlSSS`=3KwLS66T@G9-# zXY;y4(fi(Yde^%+ytp;^wX$v@%ppZzc6Gcf*!3Mj9C77kMx`T_sh~}ZOW7#r zveICx>}6r?Z9?G#&-6u&BbpIc+K8EElRi49g** zU@#wT7;3Px=#?TKBkS^Z@BialvfYxzNkSZsXsc*WYzXEOZnt}`Vj_(;BTkglsf3lZ zM&=ClIT(i7$MNnZfv_n2D?@|ak_I)SMj9+*iiC{%rmO~XV&t>=MA+=fn6X;C=&R-w z#!S$N$ckaCYD?r<_XxX>G^uV2T9ilwi;8hn>bBJhYcLxwI-)w0rzF!xJuM_1&n9#h zqhD%qcv+W28)=fDC~Bn9%3ZN0Qg+Nqi;Yk!g_^xcvtX5&lxWf6jF8~@!c^0snDf?L za@IvLhXsAP)){FM6d%yUj+TQolrsrkO_8c-eWcQdc`HP*9`~m7Raw)Kwdh@BtyK#& z(yaNs-xtKYl}?*WB`vCysN@<&p#_)KF^LDO6EMW?i%0BPP;qL9;dw zUPO*2idsY7l*nbA?uK0zO(dk4o>U?lWm4ytH;|-3;*YZ~hZZqmP!tt09G*01T@%HZHG1vy#y9sYpQ+tdi{MipPkP zt~f$8V%4yr|8%U^Rc49-nKP%B+L~k~rmmMVp3U48*r?oNTkH^ zg)=~j^Pt2A>k@Z4%S=7x&|vPY8p%{`j%X#3tlQExTMUa!OA>3-S5mt}GIhjHmP6{~ z!X{9X43tEzNZHkGcXyf5G*b#iwiS1HV)#QLF7onS+yx-$edYj&!Sm0r^bg(n&n3mMH>|wdPVl$LbG~d)&DzUja%v3OEDw>eoA4aQD zIZ+74L8TkrN>@NJ?6wz}D^1_on}HSX7FZY(j#N=)(G=YUsn>1yr|fZ~DWc4&<6fP- zk_tt$UPV2*Q19Bi%$m*a9qf!RREZ`$HK{dausLxwoF&W(A`VL|IV2W~82x0-<-v1h zEfSaK7iyp^DsD=w%jy$1m|j&XR@F+6TQ0Wy;s$~+2Vt2KJR^_il-X>+n+e4$ZbiWt zgyRcUP$CsKMb;(m7B>}rf{2>Py2qqy=}S#_Ay#u?h$m)oW94EtWkShdHR$sD!2Oow zLZ#b|1=c0)5jPxmrGN{SL19$fu-~C#b|Lz<5tp5-$U=!?0~$HUy6WAy;jkYCDqZYW zI=ezEfBKh`858@RtNcO%u%Uc8!j5`YaAsZrK!`a~*+J+b6M9F#~!YJ_!(dqgUSeT4i% ztXsP<>$3XPE?3TnS~8Qz>&TF4nQ9;xe>voVC2@<`p^Qe1#T*=~!^wppD3OZPQ(2d|N2GGtzsN6a?A9*8x~x96|#*se*yHjp@{|2KZnl-f7tim|B)8h z!dsM!Nas2Hi?Yy+G3U@YJtbe(NoecoNU#)kTTKBoNSRQs&soiLnWQdM^Q8PzmvgX7 zZdy~xr^#F}8C|n(l~k!%yJ}6P(f#WhiBz@*{6o1?B173=z7!=(U<(rfB~r2C%uQ=T z)k-2r*2r9?4F2)dH8UKXnNmE%;VV!Z&tV^K%?40Fp+wg9AjqNDYAdKT3nZx8n)g5c zl*^$wNpboA?7az`-uJ!t^xpfv*?8=4TJ2?5xAJK#ngM9PHM9HNV8AYZ^IKCH z;~)a<3&>vcGJQN)uIWQto0)_4U;WU!qhVw&qL;lE{3|$~j%)xV!SAOn5Jpjbr z;Kr2JuML>-T1EoZA+em7{JEH@cBgJF^D?4h`ZBI3Mh9-{Iw~lvr$n`z<75!j=nA`- zx6vMBjqyQ6xVdsImn+ZO>`e$l5{QC%rOYNTd+oZ#;$W>2Xw$W5(82dG{k5;%{`}WG zg=Wo$_C?#7M;>4xT|GSR;e_|}IqYAh&pp7reJ`JT=Bi^oMn43O_be5_P&Bllzg{jp z%i9Q`+uh!F>Uw>UDr!B1@3`3dzb=e=Kz^C~At<)?=X<)}#P(wiB5^y=REaLO36FX|JqXjC&ITevRhAiueh^6{SZ0e`xRJE zUXxh&9OGFNAojuaI|3|0?>7NbCT>((nBH0x*(q<%6|5TcERY#ms4u5vJ6;vs79&WF z#Ch)P&*6|KMZHyq{b5j?RF&Xnaaq!p9>@lFOCT$ARqx?}6<3F(zOqJox1OzdL&4$z<#E_ka5OXYYT- z_409HU*CsLzIp3q+4mhk|N8f4e=+;DAm<-(qz91+A=GGCLd!gs8=)jRh^)ylQoAL8(Fw+lzO`t3Ihwov zr>7K_;4hFKQ7$bNxgM8%*O%s0f5DE?l7MDwLa}}GQp2b#fb25UK+%8D%ZiFEV{9y z!jz|xKkP_}mk-QklEiJAR7Vu=t9`My9F)ZZBSlkl2yGi13YPPR42Rc(rR|Rd7OLc@ za5)(DdgH3Y_@gLpYOthfgW zi7zyokdSi<6SXV#ip#0QPFIcCOqSNL)$c3i9JTj*TN^{_jV_Y3q+J2z^Lp;ha0t;{gKWU&;r`?;$^?Z>uOY%XSJqa za=p1ORVHx2^CB;4?)I;qQ4}Vc&(zF33zbb684;rz-CRXf&jLw%XWiC>rkjqn;L%3r z=Qk9MWenjRlref74GkK3v#o><#*`>cfi#xUb6PV1pJnKhyZ6J;wwZavYP1=(m+Rs! zP-etq!nPdQ)!8C9CHSI{uzHVfU_q_ZtIC+UX)9N$P-wI$7D>AjCzFL^;Z;ptHVv*~ zj}l=p8aYL%nRE5-@f$Zv@$JPTr&PKt#EVxXs#Q=wKX;Z?JPl{k*a+J~ z8;XMD62u@76ar-i%gBsIS}Q$|6u8~LKck@Y z{V0&8MLx#!#d&=qU|K=b^8}jX5V9ZljFs%QAUDTk-nF5C&4$~qOuTLnRaV#-EiRV) zBy9FWrS0i{oviYOfPqV@BJJX56g_&#tB79_7Rj_faLs8grZhQ6f_vx+!VeR)iEzVG zERA=jr(Hf=m4{72tWXP$7#H1ujl}w_HHT+{T8XW0L@h^7X!$vmmp=Ve#Bh~YC{r~Y zxf5EAp3??jPge7iCFJoL0-+|aVsgufmtc3d{iY4Yk{T3*5{w%bQ}f zR(?Yfm=mOpB1V%mL~yUxShlL8;li!6^JUeBa`AWqg(AEh65RI5hQf)+1opf(1U|tD zG9<17Mud3WT0zRN3-`&KEOoiqpQ+aFr*0?)N)AO4y8!A_+Pd9gU`kug5tUy=%LRsx z8=!WTOQPMFg0@Q=3S&MoJ1&9@)mfJ%JCZDF9Ted^WffCLy<~>9Mgs~9tEYc0!+xzv=So zv6f!YsNK)sP;_c+&=VwS9uuUdP(!I%Xypu!nG&%UISN*^T1$#ZL@V6+-VKGU`BDK? z?;%l?uo&o%XKt_VwP@_?-yBoT;=3ej;OE104xNgsbDZOom4%EMd@W)7|E8y~b(4+~%iD|te!ccO+#z+N~D!JwH_ovPLEcgFpR zUe&uHwfl2tdJ(I}5^{ywxX^%uU>KuFQmR%~4L+e9N2+Mk0y#;hLQ6h*bYputQG&V4 zs8|;!7mMEOm0McJR@gRlZBL7B?9)1EpbVW<@F zmf2G}QKEuUMX1?>dTeR`FEbvcbiztK-{`tix`ZqT7RBP$$dOpj!X#!P_1(k2 z-cW>HvgHXPKVM-m$2AhXY6^)h<;FRBDMKY<3dN2tDosl|+(bAFYqjd*oC1kdc^q5A z4qR!6hKzK%Zaq(wL|=yrVr^ke#@OLkZ0tbs!cq?xNZ6Q6dAZ-HDh^XCa8MwKwo#7kUKJRLFZPKoNvMQ`C&_%6xv1dW}%VIxCu z^mz{%-I{lA>6#&ojb*H$pekLpgnFbH-6Z5;-VdstIzG&8C=`6@s*OQ=(z6CGsSc-V zp8m0;*VH%DMTy&6W4!4r*_irdj@XnA_JL4%CH;^Qr!36}Vs*p4RvD@JUNFDSm zohonDFv07EZ>(=gQqL-_4cubM?i8TRxol(#@_@oU5CFOj! z7>n9WKqUr-G-Sng#oT;`8uFx!@qVAEp#(zBl7o*srLf>0Kj`zt0ZNSfmSm|l0m4;E zp1VkB6L2!YW*WI%LCun}4}*O#a@F4V*?-??75BL8M_x}&~{}1>7 z5BLB7diVe2X7BMo+5Z1OoND!O|Np_+|Nr5p&mUm_|J@skhx`BkY4-nz%C*Y1pUZ6h z--n-Y@HhK|gP#Cd{QvF2r)<4t|Jv3UUVrWOn~s0y+CT39!~V}^e)8m1M}NBgdB@5A zS7zUy{rcwmcWySv|Y^f9}Ot?n+r z`=Xsc-g(2$=bTJ;q@9o4{_~?BJi2|!WnrpOc>DbyeIOI@rmFZ7BGlOtPgZ)t)&tYH75g*4d$r z1lEA=jqw&fZ#V;y>dMve;jJ@@nz!h;7Ai5|OtD*Zo#6y0Tnl`5YtM;QNW-KL!G(lZ zt?ly0jwMzec{Ok&WeIL;t(rDIGV7gu%;by}M~y;oP&i@h$pEBH+dtSoGh{fZTUDu~ zEDP;rvZ8A>v1Nhp&ybJQTdT^T*WY z?RLPMe6JgqVt6nZn-d4JPes^4W9d&TYV5(|pgbqlwqYp?d)jWht^;uhLKl#UTM6-y z+eiXd5GaaOGdDY>bPO2cxewExai3a5mqdaIcyawtgJ)ztfLvEQX3VmexCSiiFD;j+vTvl6E^ zwyUQUc72E>LT@TIacD@CM&m_Cro2Hn5CepVC;4Vq&JFODX=};h<|DNYH`OR4%e-82 z3W_a_a|yWN)AU5G*CE-t*=(8ZKry(sZtb5P8$=`KsY@2;j=Q}% zG_YfPL5G5(x(X5!9T*Ljsy>gcnaa(}`E@24k?ouPa(`)Ed&fpbOfKu7;NqYLvi7Z< zVhNlv@bbVJG)b28gTe}1)u&clZcOB@+J+*U1Tv(_$bi(ADlVyxYm#Vyj4Sa3O}bKC zD}dxkZA(?Lz4jReuk&q$tzoibvvy8W5ReqHSqWX5t`WVQfVEI+zVw@PBeS`>V^8OL zdDSsc2=3yN6LkVg<9lsFu9=CiU`~ITw1sBW@n~iT-pClo!b)o5=7@+Z;L|)_^$iDw zo%x(qD;zcx%Y=c)Lc;$u*Yy&u*H$Dm)O9J3gD{IRJARA9<=))7!}9}H=2lA zLE-Kr8xg))X|=}9)~xQ%WNf92`4O!bimOr3#(WT43^*y8HLLD4b~Z!T_ULlHBB`c_ zm<%V6Rt-|=;`3EFDPs90DD0MCV`|w@M}uS!*~oB(80tz>-O+|^zAui=UMnAYqjt5$ zH+;csS&|G=G|f1L*?#wi!eH?7oU<`T?o}g#FzP%R=4xss8Co@h>2#|zcp7VSK`d>b zX{F=58sREw7wtJfn%m=-J zFd_WhvIvJsb)f}zn;Ss0vELSJb9+gJq}*n_ZY4PzBNVWTwM%T>Z+q=B44V!qExI+H z&YPw)$eBGOph?Bz9BF5BWMGrwf@mu_gKe2=L-F!8CBz%^Y8ZCdZmte#0~um$q@3$o znRufVQ60=GIkV7Jqi(I(n6y!Sto2Al=Ham(uQ;Felm}<5;8;km_eR5F-Bri6Ia6~QNz{is zhE*;45;#vtNHaOwIG|Hm3e8o|pd+%=nh`U*Qi{Y}Qtu`Fq}1ox`m{8WbFEsAsadqb%obsub0cW1&4?rYwPS|;=)BbTy_)3Zt;vmUWzeSvvgg8iOz2=u-5`vrSmVo{CJ(?-b)y_C!tKR|V$vkQ ztkN$OXKpua;kntYJPaAC+6Wahgy*si(ZeQ5Oywpk1K`3koQYP(G(`$NX5WeY_Vcg!>zN0kBgakABUGq zzS~6u)RMGj$(5sUG3%~8gCqx{*@qu#oeo56G)D`fJ890Gl{?5o_FMuPS&?$t z8p;mIH)nXKy^;#1GfT+h7d)qKiVNw|S zutsCqA3becrw%fFmZ|Q%(|37F9N-)(mMM8c8HwXCBWMcl8kD*c;}50%P0)2D#xjYS zP;>FPr)yL2&O`}$1)1er3T|U|-(lKpN3Itt&=zwl!}ifxP@(m*Z|9d5CKQ{M+IUE> zklc7S=ez9=H_-ff#Tl5%$v12$7G*gy9k|>n4+v&y;8nW8&F#Uy4;t}Lc0 zTlGe!aYn&-BwMXeuFb~Fnlh6>24Bmi)kQB~?t>9(&|QZfF)Tsto@teU>r70M3CF!! zSfE?Wj#Ly`fi_!C+niuM8032c8Y3Fk*cr*PEMgkb&;>*W;5@Xjhb}* zbsLIe&lwh(7GLCxDm3)a2}3dqdC=6$i!PnBd|h4+JLn{!sq33%s5NW2`Fdm2ww6XW zVJNjV?F6h)8ZgCDAe9QaN>UCByb?8%qhC4G%5C)K#h~8JQRE1N;hI4<#)3;kwu*xD z^LD+{aR_4>N$%0+B7^O;S_KTPG?8%O5a7}d;&v@nM|xv$m#y4@8eyU1d)1oYT1WHa z|KmB0UAx2~op`B?*(qitOOnR(7G$FCqQ|t9k#E*G#Nu7C6$PckkJ#9OLA|OmnDA}W zOi)&q$}O83@oX4K1MmpN1=^ke^c^VSyPpV1j8TSkG=VQAjgEPn{876Cn! zAU5BNU8#_pjOTKBT3o0VA0vEh`vs?6?&R>GO64173lGZ!+@FqU!EaDtaaI-UMqaEa zbBJrU;F_0YzGX8=R!PV0^lM;St4(}y%8hf2dR*zvhS`0B08{4$+kDC)4+z3#LnkiSgdeSdY6L5h7?H+#4y8d5b>&@4mu^0dKlW#tm zoIG}N9pnQ1&g1!U0o>|;`ug`?|Lp7K^?m%KA2|AVNA;tRKK#qWA3l8Yuygpx!CxQz z*!kOl{h!|diuHSd?EB8&0c3af-m&*hdjYunf4KXbyWhSWfx3Smw)6Wt|L4wXN7#As z_Pe*=xc#}?RZw&Ay+DzNKMz|VMGKscbfYz-m0CRL5K4J6o?4Np+Y>d178pXd^O{2< zk>BjY^1yHpH=klavMjngYLBBh*KA_~NEj$GQzxc-5+ex}++2w>4ClcuACbvyt{56k zSuEjkkz5W{w^g*7Pz>%y!kyX_=7Ks@WV$mFD%x~~k`K_0R*gs<@^+^&^;^kYSarNX zFvi6xFZgYtL+RkU=OWBIfNJ-(rk0<nD^C5cvns_<)eIUVdzzzGxZao`x!B4)|BOOR3OR4$ zQ4356s@1*%netyf@*{Da8{t&#r_CH5>9hANemSyIT|jSIhY4}f>W3$enjVtv%C35zA6+}Ru$@i z7Y6VM)B`CB(yVB&oZ2d%^P65#hs$PN4^SifjT;%%a9LjXO^UY?-BYooF_o5*mZxMo z&kdY0*T=w|to!7!lpXGgKJxWP2(k=w%7P!a7<7WDu!#oJLMdf7PjC!a zFOT$Ef}kGQ3aG2PWfgj`UnqbN-}Fjv_z_W&OvN)>-QCT7S_9@=A;07+78jNDcv4L! zWuZnk=XkAC^x4X+tu{kCL1~oQee0=K`l>XjN6qq7#fC|oKxLsFmkU+esG*YyLKfyY zQ!XkdU!qEx%?DIHsj2>OrA$0XGujwaj_E}255rZ3^A&j7tCS}-b5<|qOW5{i8LB6_ zyr)UQs1fGc9D}ip1RmI?Wy?Jx$fcJSEeiI|u zPB$88G<2|uSNfFf_bRp|4+9j7s|d!6-3byv^mOc~(>`vKOP{Ye^L9|$hBs{mI#Y(E zVU<|T=mXP-kLl!)~9wbn&TiV;aE78ZTp)U-@fS%+yNjGccVkC45 zJOm3BEpSGGf-ZYpUc`m*&VSp;2q;$X6i4!eN5^2-tWZ6WkeHpJju8&4s8?qL@QsiG z-&N&Z%-CtF5Hc#9Q&y)+;tLT(mfK!!TcMO2~MsL_||#5kzBf zvr!QS%a}E!<`7#(1L^P!Hx#2~goB5EAbYY0E+MOJTFv!1vES&0hPx1^qu3-mMcj>A z*!6d9C~R*qtkZ!*dPY=++7%*ClzSr+va6#pA2LM2By+V6GZW;Uk2#|l`4mbgt~Z!= z%l@p_Drgavuw$la%=)>wjTL2YQL%&;bA9u5Q@4fK|bwW=&NSmMDaGn)7*+>yAT>=(UWEQ6UzVCvSQs!u0Ewc5_1 zW10y&q%`TDf3wqD;vB}nJD(B>(w2m+W!rO{q^wFX)8l%DK0oDpOcEJE*V;a7tEOGh zOXNUW81Mv92%;a&qI#69IwrVi59y5}PqY~D0DvoH{%&JOz1tcfyaBCBLxUxR5;~gp zm{P?Hwc4~d8j)mGgP_7pBHhf3HxyMja8}hK(hmC!*A_^oR9M2`vm(b1YLoV$q2}kz z96XSMlZ?BesBo&eG{6G}8^X$EU1h6M*U_PpEr2&U6ib&|a3!hNa7o^uY$(dZdg6w8 zhO6{S#Z|N14?6Oq;FCeOnd32{=R*?^37CGK+y2T81uqZDxY@1ZaCv|fr7pgL96H0}t@}yl*v@b4MwUmVIiGpNk+>c6G6sw@M9{ zoX(}|o9C4})8$>$s~~|$D}3KUkkW`S9in29T5C1YXqB3Es%>ad>t;4jYIO!GBuyHe zQyS(_nMz6D!je_Lphsw_ST4+2oX>+Nw(UGFA8h79om69)P^u8+fS@koWqVP#gQU@g zM5aFj@7k8K#jM%`w2K|QePaj7&B(}s!FNWoJt>zU6obaqpuLEg=4wixVvtJU@X z$cK^{y&A!mhV}ZTz0Kz_9UU#YoqVKq@L=9yI$EiNCDo*Cb#f7yI9^`v&mK!=HyOaCHlO{`@KGG+oOw^B{>>ug<=DAzjNlJKR0fmeKc0 z*YW_#TJN38??D+{NT;&NV0$r@%9S<7)&${|nfxv$z+F>4Gn3y&cxXG5buN|DRAK-z zaKeeFEd$Lz%{izA;~ z0O@`y2=Ar%b4(aWz!bICNoy2NF<*&iFJ>C3fmQAO~qQzBGU4U4!?TP640W z{j6k|AE@+vI0X~;%}&&ul4euohcOb~>=2y_4A~8~SF0NffitDy%~H&2brGDykDhyW zqWV%;K)vPQ76HxNN!|xH7lgKN8U}`dJO;^1@$)!g)rVr)Df?E3Ms;{)4Qvr7#apw4 z1_?UYVzt>_pVrQ2m2S(nFmRI{lM8V*LfE2In9uF z5BC278EMlOySn!@7P}+x-o7@N>J@XsF;rCLz zOY!G8jbUKky>H%I>Q<8{!fBAGH%&#d4ee&!GA5e6$~zJ&f&xKsFFy=LR?@lWJdH_J zd4Rd*yj@KeacQ(*<`tCaSaNGYP`*FV0!pNu5*v;2aiOM=&O`_ID`7n#^^tPZXbC>8 zRhun5Z%CDppQB!9Az@6qytix9*Pgxe6u!wmJC?b}PhTfKB)&d| z#jaQd>4&5;=!#BHg=M4M36PmuaQYBG9VJp8$`9G8ac^ILfXv(J-whXe`VcZQvpCljw|bbS}|D|!_x-{cXcSRQ~wtT8XM-P%oX zz&9?FT|a!ze%SP?K|!-?pl0JOW03d6Qj3P7}VxzsbnuxS=L>dC<$JOMtsJeVzaBdhMEg?Jb$>FFX2wj(+y& z>yCmW=IDioe+tg_zxeR84$;HC%uBAl_u5;wq3!LhcOLxK!S@_|?m_F|6ZZdp{}=ba zaeua7-2aH|UuNH${qn4trLsqRzrXi}t?$};ZHC?cg6&stzk92(^+@Lb1r-9nJ~P?- zyghaA=I;A;e|7g;caz=n_M30~+Kq3&vAiMP_?X>~-ubUPZ`=8*ozc#tJI_1$>yw{4 z`RWt*qhziT7T#)mf zUM)6@7iOmI<98gtBQ1FA_*ag9<>qeVAPwi^w;#X#jB#5KhMiWerGT>oa6yj=$``db zdBMpG&cUssp{ZaCF=LyY&=eN)3*f!u{p0<#;N9cwaW*Y@=XmdUFD-ccc=vcWEqLpA z=XmF=<=R&nc)WeQea`r*mfJdc{>k&t8D9lHdEUwM&KO@gF!qk$b^NZh;N9bQ9=|g! zc<1=nkAFR_eY+iJkpYUlp@14B( z~7JJ&TFwm!B z>q5irXYdT37M#ss87wV$FN0>#wBX$gl0njfcQSAWP7B`7Kp7}4cq?-&bL+gBuWGu? zD>AP*W87Mnly1#8c!z0KL0Z3d{z5Y!Zh@b);DfEqRwgZYf9u-TwY1=D=ASeFoEE&7 zd0*yzX~Dah|DO5pX~8?0f6Dw*TJUz}A2a`$7QB`Dhs-~mb>NkqlKK10-=8y{R}zj9 zyW{H2l;=~%n_TF?{hj&FJS{l86Ys=n!FxN=PLvkByA$q&X~8=?vz=L5@b=DhXPOqg zwKLh7oHz4TO}8`N8J{zbxS%jQM!LomxJ)bbI1f$tM%AQY{SAvo3A7;-d z%qzig_UY`?U}655;C~;KpR>kmyI$N`EliWur3mC%RxVDkdpAD(#)qc`@7{RHjhCba z@7#FtjTff{Z{K*)jrT9=uF`qy#tUz}@O&D)3Vh=QH_kn_?Uh-r>#=UF)>#z`p18Q+ zvD?{Ec66@tRp70xlXcFuxC#t%yxZrDuUstd{rTRx!?v#i@9zEC-nqkG3EtWJ)4g+t zy%M~=_ny6ThrJTKwf84`=MLMxYMH%vBA$rnt#K82_oQ-CNekXNDW8-Eh z2Iukhll|i#J6;?^*Z*wq&3lu*m+wBk`_;SqJ3qEt-&yQ@%=WvszjT}5`sb}TZH>1+ zG4ltRugcUi*=ujTmR$d)>+1Cn1NRF)_lP?DDYJ@}+#BbbKK0 z@zP~{NIDfClI9*4F6Bd}<3n|07-Rp|=5FRICV|C0Ubs}jbOV6|10jDIA0k}` z5}*T_OZiZ#I*_2a$JxvHkm&{{2?pk?F5^R7yp4MAfk5EgOq_<#W;D?%nSg z2(+JaDW7!X5Ch}z=4E`)bbf&Xzxd=!`J@YwC& zlux?;LP39h!liuD`2`C6;^QylgQV*(1oYPbd7QqKPrC7pgSbIm$|v1?frI&iyp&J6_=AJ^LtM%y zUHrj8{J}5flWyL@!MuZA#s^C`zhhv2M=#}*E*@ea9wL|WNjH8#jq%6fOZlXWs~Cu@ z(4~CRtq(DL{r>+|*S_c4jkn#neS^J`J^6)`F90_VPQZ=%FFS4=zv%jJUH@vZcmFp> zPal2bk#Y2ihwnN3wnOJIckq`7-+eGXK==Q8|Bd_e{a0rHKKn!2*JhdQwY|6Q-QMH( zj&^^3_X~E*yC*yE*s1M&+0F~V*MYCw?r(q8*6(e-e(STgJ|^?WneWI9GoO6z&w==K zKlBqXIe7KX&i?x6K06ZXt^xL&zkY4^6F&3WV^6&J;MKc32m1$^t^3g4wcE@1U-I79 zol@_A_#^ZaFS<(jT@1AM3xDC2!Y_`XJHq##Pt#Al;7Z|l4dpZ8^b^m&lJ=tTJBmN= zO2sb@*$aw)*pS$ZlFZbN?>8=%II7Z0qQyPe1m={warpOWOS6WORqa-V0UwiM=b;zo^2w`t%dK zSJGY-eop)76FXN8w!6MY+);eH^XL=XSJGc}@n_OM_Qckje*cmHM_hFCJM`HXeG&ac z<|^awGT_{J`iW~-(q2^l{j|3~=Std(il5UycKd(b7=8Z`L0k+kcZ}Z3e%xcXUwhi> zmkhVNzO~$8u>Ze2LErxDE49CS9r}Zhd?9^XyHfd!3Y;rX-~OyCX)g*tr+xHx|0=`p+O^+NeBI;!_A0}%i!OGD z{_vwegTDRhtCYV>fpg{Q+n;$Q?M30|w2$8Yj4KVl=-+n~UmM=L((sEbh&%L$AN`s1 z?e3MzUu1Bu{5t=CH*{U4eAtL#r@ zzcxG07P23)^|~!{>y=wKGVebAr{iBe{`TY79M_LO{`x;$|K;o7dVP7ldi~>${_g0P zj=uRQITDXvdib}8zi{|Xhw)+g@M8}C=HTZJzTqG|5Dq>Hd^7l&{nzhL_x`WFpWOTE zJ%2C1_mbVe-2L(0uiSNaKOIyHeDBU%cfMlB*?A0nHTbjbw`_m;wzd6f+t1s2&(@D* zekk)L86!ghl|S%5pS;sZ_2?fw?M@2?i3id`LE3?|P>^-t9D2ozz4e2=Gw4L+meDe? z+d4ChmYw|U!ja|)eL|;&QYTbeD0xDrg%T%3S}1;kpFyvBhPZx(m=+4&Af|)0 z;Qe7*D0qFC77E@TriFr+hiReU-C&1g@PA{X`xSNpH2(?gX|xqh5ml_ z_tQduFZ+9Gp}(8`-L%l($^K4S=x=9#J1z9LvcHuU`kUF`ObdNi_FZYA@65h4E%Z0C zzmXRD>)H2*;;SOzuVsHNE!THs&!aY|e=r7>gtSSu8cBX7XkQ4L$KQ1PO=n!Mgnr}k zH>QPt!|^wyg?|0<*QbTP{`mE2pE-iSF-P) zIIkK5znuN$v|QhwO+Cl{Qude9a{a~ZFQ$e5LiQKZLVrH{^J$?!m;Jf4(4WozY+C5g zWPc_t^ry2wofi62*`G=aeOvZzX`w%v{mHb@pUD11TIi2we>^Sp$Fe_`7W&rgThl^+ zH2b4zp>N5)B`x$nXa93r=#ON7BrWuZvp<{``a{_tN(+5+_RVRbZ_2(YE%ZNS|5IA% z4`zQbE%XPnKadvs{n_tN3w>kujcK88$i5*h^!u{kcLp7HBTcs1;yA1hK`ro(e_=KR zRo;TMP=C))3w8J0v`}ZyNei|1th7*b&rAz7_KY*ATv)loV&0m>EK{v5nym|sb@aNU z*QJGi>Cu;_g?`D=m!yS$@zEErx6v=V_8+d@c>RqVC*OAR=9kQks?{+OX%2kRatKXWI-s zvUNlo#7!}zs-e(^FsQw34Ww~uT{a1_fx)#igH5?-8M7J4siuoQm4Ko?-44p|ebb)> zhS3h|>~Ic>);0SL?xj7g=0@#O$81`8buo-};kg)m@r$VcWEJT0DqwIZDRhit{TS9N zes8Q+1*J}cLbioYQPM`mmWky$OJYQFxRhEK+X+GX?PY)NgpTfbPPFu&l+NVQVR6+( z$z>4~kkgdWiUs#9o>6J~B- zf)u+P5KdLa!-O2QYfx;qAnIQvuW}}D+Cv)wg;bYjG^CPQlZ1V=6mV^RLA&`5WD;{% z=)4a1Q~9TE~OLE_rz@Z#Uv9j^=X}tmd;m8<0834~y%9eak!-svXXf z(wlU7Jhqz!x-U$?Cn4M1(Z-iQ@K zR%!PN&m|sQJSec9b_dRRf~beAU_-$rLH+_wmU&s$Xi%y zU98v)qrPSsW*GWU7J#I6G?#N?4$pU$6Pl-Tj;X&C<+GTQ81ZCAH!#SwZ(;kW~KAifaGHdqZezU8%D>t>g zXL?HhtObK$FjCrly<4HAq!x;^0_MB9QdCzHr$AO*zsC3Ik~$69acVgiJcR+G)}Epi z@=8&0YV%@gH9-5*ps3OPMqb4$;cR73K!qVqwD2ReJ6uj&Vm+y)>(2Lk%B#){E{~mDC07YYe85O8 z5wdVyuPz;K(Wr;UN?=DJ6XHHr#Yfaj-9%lue52@UCaAgIZj>*67*DfQxa(xefATS) z%X<8-`pRNhT_n5+^?BWOa3q$h#b&-3p+h9USTz)cUOFs4wVBkq+&zA+Cm#j!))6=i za5c_1@EqI8waH1o99aT8&DUz0>y-+F^)PdZWNsbeivzwI#`=w(Oan`O32v9F}Lwh@QK}2u#PLDP!t-j_VD~Nez;z zN!cOB5?dOUe7@ODE$4z~(m>SOGZ}j+)TtKJvBw2vqD+%s9HD^%k0;fRMZj)x6xAmz zg%uPxwWv$yu)EGswI`{yE}(f4xtzxXtt)BsVV>yl1#d!=j!M&#snLNtXqhq99GUoR+#H%l!0~)Sj0W)(F^fvsDRGU|y4*c}ttW9H zZ|#|oqk=lGg_sQ>p&!_-Rx1~k%~G}0QA2lROQvMxge1o>OnTd&?U~q_WkERBR%p2` z*FDp%io<5LB@`3VS9paKmvgMw@vwox)^Q=dq`NIMpG1MYwP*5P+0aVGB|7is>tlax z>y@@w;pf&A6f49_UcZz_r#c^3?Q!ase5Pk2XDuia`2|0KIxNzvCaTo;^=W;;mD@A4 zFlevxuI1aDSn48TT~00Mf@i`&)Y>x(SaZ0{hxHsXU1*~bTaok2i3FDkc3N*3ra5bl zmI9uW=S?iNsLMU`t~2^4p|vicc^{xIS!!K2p1I!t?_{2L?dZ$)KVs)~;Ftfu|9t*y zp8B+P$)PXW&U|3~HD!G*7`;~+k_Q;~>l!!@GGI>e4=~v8U54ZV0?xh4kUTSrtra>g z*X0b>mF%t_&})LL3lClNt~J#&>*qa71t8bZg1gI)tP4fmU4G;PRq>0)aQwb?fvVja z*b+#%=l6)P#A?)F9xo>25+9H})0-w(vose`!<^Ngdp#0QoD|@yMYSLswgh@v;Fm}U z+d~^Oq0R_i@?@zsT{Oyzp;Yl&wb4>&Q&$g?wC;!et8~8yn1Ao(es@ppA0j8bha$igLtC4f zgVQQIU=beKAS|rQI(-lw5kl|ph_>%`9L#Db3@C?Js7Hk|GWKi%ZDZcT)hm%P#}Z^} z7v=8wITr>}D$Pn6Y8hmHFk>a^rf5*N0*k0g?W*Qgs)VQ*j+rMJUDOCHPy3}?H|2J3 zQNe^tP+_|Gd|qu~6cLVBaYOdHh!5qL`BVkE=(Xv<5kEhA>XmCp{K7c%fnD9Dbwv1I zr6WGbM17Cadw15nA0h{QzXI#2cM|KKV?4|6*5$H3cz#FW5JCb8_pI+KFL)(_t6Xga zbAHXKOy<1;SD|YI-|P`9L&-B7I( zEuuUv6P`MCmvF0Hjv%8jgi27Ubh8N$szZ8acXeA{@*Swru^ZwlniDrEoi+V*es^j* z@Vh77r|7lceW{UoQ+y~h_-FrRxpbOFqm6)Hor~43jQXGP6 zv%rg9^RoM#E@F4BNMHG}pe*yc!0ah)e_#%^*oy!U2>jdoYwNz|WoMr~&WbwY>%X4; z)3fY8Wq0Rq_ncdSz&i-A$T zAbT%tKwLDPs@xHQq^_wXbxTzxRhvynDwV2SEB8vWZ!~P6!(BEgw-H8t&NDir?5UA5@VUd*Al^t0^b`~LU;zW@9G zs{gwwV%1crD$~(JhA^&X_riSD5?i;rC9zpgAx6}S4x;6@n1|7jC50veZ%rV+Yu?f@ zsY}*NgxqPM@oaD})*kGIRXNpk*N4-YI@oUP_y1R}{Nl=uPuzF|$p80&zI$&5J;OGx zefHX0ucfYSZ~yxCJGU87yYIKIzUOKdbO!w0tsmUVfm(sTzxlz<2R5I#@kbj!yiowv z0{`U7k6cl&?0}{T|J!99R0jOZ^`8LI{4e$WweP1uI~xC`zrFM`mwK0iYk#-)b8FUG z6jTEK*y=;8uUPqi!QglO^NRgPeGlGTzk`8A7c({zsVsv~h=2>L&FQCuE3eppV zi$hUTI6N;XxHKn!_$=~8^UmTT#peYMmFMISokhNwmBpuIG!-XP@%Ws4au)ew=t2_B z6Dftv#8Y$f@mb`HfdB-Fq_cEVP9BXuI*WYKQJPU`DNUy^F+U&OI*WYK?Fc;K7?ETd zg`JZR&mv!RKCx+%if088=jY^uGssU>aX_UMlEH8;gUreMXOSr=JWKlstl+=;lJ79b`LHW$ls;i$xzPM}>0!OAX9A}S4uboA{xNOp59L*A0N+9Oot7nlf zE}IMwnzJzqFB~b(IE#F7vqG47oaYe?IWigjEb_%whsttF%rW3Gdk(&G7Wv|;ONk_t z$mTgIGbh*1B41o}Oq!DNlp^MjL{~nGd~wy0X^NL=5FdrjM_11xUtD!8CTApq;mISt zm(CzRl{1Ybgmg+kQRQf(E}licxat&MCOM7;Yj+O*b!U+;uDZCw;y9Ad2}j#S;Vkk+ zz2_-RmhiNQ9UT*|J&Sx%?-D`-&Es?`I|u*ZS>%g)=S4oxfMv%Y>HRflkuU0<&7hd9 zfIx<$Mf<>6%hW4$sg84>X@SI;65^kuR>gxSS9`QoZWP!dO(EYB+Q(b+S|1B+$XKvWS*QCaYOG$&`yB41o}2$hgoF3u#5 z?43S~d~wwgLMn}rqMV@Tqi4<{UtD$Zw8UXqiYJdmmp+Span&(0&r)E+!jHC#)LG<< zt1guWuaqcGrjAw}br$*Js#6GXLW-%Jc=T+OJd1pB)rn#r<3t)EkDe;Yv&a`$U4qJF zgd~>FadXKNXOSJ-nlyCU2|Ks(~Tz~KN*Ij?AklTW_zw0`v>`9p6WN4PVsfyY!ov zK77fz6uh*u_W8BU_AUAv^M?ew{RK2~JHdMTJL zVQs>K6SZQv8^@^OFjDY$x|wQnG>8(tTGIDn=&h?Z4I)x#FrJ%~+j*tPGITkORCYPJ z)QxqU>9m@tl-aSR=u}P!`aT4`b;YI;V;oiK2_Yycrs3T(9tNEL@8EE?>FCgXVBAIP_rNrK-s$hhwV z(DYH82G%Q;ytlN~y_}Vly|^yCSXB{b&Jd4VwA5>3cskeZ(0@<7mDz z;9|wxC|s-?um~eM+xI7}@*oEXQ@Nx#h?E#8#@20`hi#gQSZroc@bD7OHd;`UrrCBy z=mf+N-h+odD5Dxxp&tl@8OZPZA!z!LP2-maW)N1Ag>))7EKtE_d&GvQ9PMwz{9xEe zD&Y=kp{)ttO!|Hhnoew*Vz)>eA_(lt^@0;lN0ZJNXo4?`_Aqp_0#mgaSmOjqk!IB058ip}X6E|AH`2q=k1lD_vs(~(W1M=HZc z1&3o=ADLw0!EsHNhX~)o*>Jw&pA>L3%+<$irz(lQC!nci(?C(8P)4n)-^_OM0knvz z{-`eK@yP_Z`jRDNH^s%VvLz~7(Dxo_I<#qwrk2WPO+MRY>s>K8DWwy&SP8`^wU!WU zCJk7HEL>xBw#xZ_0GbYL8a|or!pRPo7kUIPBus9SYhf6%%LZhkJtWAaWWsQ{T2`c_ zP1Cn&m>i3bP(!G7I$^=2YSk`HbhSpO+OBAPYm)U70V=OSBcx*b-VIHAHVw-ru_y(y zVnkz%s%0J>MC;*^)y$ifz(`d_yEsTA=~n3one@F2ns#j(sY%DXdW*wx*o4$>In#phJ^`c{81^kv4hMa2g{DoL#t_f}lZn?dQEsOy=FUAv7)FMiWdnrC78QG zC1pfdB!Uxh-xQkGZ5kD9<0+g82GUj~GbZ|tc!PukZ2`-M!=u2koa|9Z6qlp~kNFOu zY0aj|?nY}&3DNZ$5UmC8`pW@z&~M~4sF{wbu&9$Qs}P9GNh$7o3pB0TGzEid>VjD- zBUZ~QvQd-}8x>UU6h^^LHI3_tkhD70a8(QUd~b%PhD|eQG*eWn66fgM$Ve}YLS~8? z7`u3*C!=k>)E&wxvpwA9Bg3fgP0&=gX_^5eLXX9<%A+x%QW7z0pzuY$!^W+N(P!fY zoG+P;qS%WkeUC%aicOPB)(694U{ok74Tx_HlgWxE)N8R$A{Gb9F&U=XvN9~+rwG;e zMrf)z<4}`S2$9nTj?GfWE{pe=REXrDHXT8^G>?|?zS8C)Ol~KAZ-Ay{XFgdV`C&GQ z497e>U^)`Gl#+Q8@6f0btT5@WSxHC+pG8&6w+~HKn}%uwzaOcx0W6VDeb^4sFcG|H7Q!_?fwmTu2RwD&9CngfB#Nq_zdkC7o#-{O*>1ESAVAFIe z&2ck6F@pYD+X@v&rV{cKS=BU(!jLNg*Ho@!hRudQ+|2kU&{VN$JY;y;G_o^}`z$Tr z7@Fp78V{LPz7aId*)$%qrhFDOm7H^Pw;*J+S`eD) zP9xfogIW?wQ3}iUJ7%QB2g9*;wAAzUpebk5C{Q_SmgGR7gG!oQ7g4FOq*>OaN`yruBlxQ>B#^baG(=0tMwZg?{TB)*@Plcv0w`n|Nf7moJo5n*3tgi%3 zqc)9)SWsVaYvn5|yDJ}9x!$OH;bFk-}tkQ$2VSh<;%e5U%&D_m;d_mJ1=vWpR@jZ>mOcEg6e%g zx%TkdE7sOmKe_sr)t9V%4`Z;B(|^CGiRo;77C!$ z0$nKVhK^sm1Yr2~8u#3fqV#$zznWMFxlHa!Pe%c*{8|{y%>#?W%#oE(zG|I|c_BL)U9|Em*gE%; zm$*mn#3X6Gh6w_c471mNc{J+xuYBg(J68Ye-7584bl+4Gc*QzMqXP?usy4En7B@`m zW4WEc$|0zj%@CPx*>ge|xTVm*{V6ef4+eT6al$ zTI;L-X|8pbEDp84`djm})h*e{vvu`%=Bvgf{nAyl`g^lgLw85qU{bZRxk-;4YnnnCh8&k+0{LVLs{zcwdw8cVrjG0aP;`mZ67pSKR;KnOZJDLw*LK%)XI7?G;AL{t=t!-?5Ae?z#mI~Tf3;3u^wK#tmqI`BbodMNEx{%6>4muU zF`&80Q=`54R_GG^ZTs8$Mce;>|BA43Lx*TwH{gW74jck(n3I51Sq9_lY#54DiyvNLe+AgUd(V7!1m-k-Z3@Ayjd;#u0? zK48FTFMbe=_J6q^?M$;dRngFk*Q$I#+i>j3f5|nR!@bRmqYF$sGoSXKff7(pMqNWM zPPF~!9i#!CS6$OI-=Da~b9#sH;!yMA6a)BDbA12#>%r&25$DC32JlgHe1G)y;Pc?X z+wo9dv3-y{gty}R!>--C@;T(TO@!}(GA0)tFV0T>c{kH4Dmh6rmT&%`H91Q80&u_UN@|gK} z@XXc@UOgA(Z@M1pg!y=I>4J6iDlnR3KL646XlJjZ#TREUwIIs}ue9OVs{IYuaAxM? z!NUxu{r&T4fBu51+%1L-d+|hr1M3w4k8|Mv2iJI(ENKswAH8^qYX{#4;5l3RXI#TO zVcs6x*zJRtAIf6I_iL^PTOtb&UU1_e28MLZ`&V5LdCa^$IL~Vb(YYvp#r04p%-e(4 z9jx;R7|k*7PrDxNO!Gcf&C!cTzkCq3;n?Q=Dc5jj=ItQ}08BeHpY|utuTybXH66VK z3jp&D0z8g+|B`DwOO~{UzyU9g{MtbPz;n#|+sM_gTz%iwS8n~q*1NZ2n}53bw#^{O_kZ)o-jy$2*}w9= zm%niN;ma>v|BZEP{eItPeO=#kFMaw_^U|}{ere5EdpgMPS68x=&prfuIEr}_Ts z{I^gqk*UJeblCo-3$`ADSA}WIVe8^9#LLx(Fl{<)UEF(kx#19|4Tr6Z>kTh=8N#&g zuyt{d;pIv~nARM&F76||TtNuas>9aB^@Eq21Yv48Z2uHIOr3M9;U!{MnCcE&7nc)W z?jVF|#bNtjT%h$3_bW^_hpmgN3NN=H!nEwL{bLtsJwy)+Q`KSX;^M-~oro|kIc!~A zIC#0r5T-?kt&2+qFJa2U^mPtf7Z(U#BAJD0!C~v-rohWfzc78R!`8(czn6GuVfvuM z_V>%=ElgkIuyt{7;N=QHm_FdJ{aqJmJp^70Q^jHXJ1*FIh|LzJvcuNJZH1Ss z31OOd*t)ov@DkxIOmhxf7k3X{g208TI+lK zVe8_$z{{JzFikpaUA*>t3HKMKq{H@;=fAdk$p{dpgu~Xw<$;%b0AY$dY=7AWS`T>z z!W47Zy0}{Kl6oLaQHSj>xw7_q;4 zSW~qy=r#a@)}Ya_E33|npF(zN)^n<#-l6nma2ay5Th#$Vfb?Vzg$?BJvjV7DI|Qxv z`gZMCC=~JggHXutj|OAlD@%(qmn%i8rCM~`oHV2D8r?Gna6g`};N&2h^lQ~*r)UJ~ zu~sH<_r=gPGAh*SeD{`wFcCj!(uMBnUF{aptYUQ1XpNAfKvOuXhU@)3$PAP*Qx5gW zU=(7S@g_Y8QJr{im?JAxdEA0jkTM=i4HMi89&sw|wt7{)1&Yhtg?fv+wq)vdR|4Pk zRT~<&f9Y+VRWCVao^9Li{?H$_3Wu#PAUJI6W~s4n)s+iXr7@ZH&AFcu31Ai!{`9cqTz zsI=u?qJ1laH2SIhz{;gd{jQkNVqwtLv&ju`tDIvjL%!>3#d?UYjyhvde56}Sf{^fb z*qV$8KOIyIS;>u#S1|;%Y zbEu1`?!pcar*7LJIen#b#D3SRm!))9X1LVqLf@2D=izy9nbnu!Q(yHDMjIzK*VEq;JKoIYibKNH(8Kc@}*Zco%Oc*44q9pmM$7K`MXMi~uA zpbZb6Fy%sOCh&iyJPJU80y5liE0XgNFs6lh_ zVl0kSK#jBdUN)kQC6uXBc-^3I5~VXq14?$%TGlKLg?3o1?jf>Vgc!ELnC{w*#O$fh z>i^%jQe3(5*!6$9{s@Twe`xz3w#Qe$cGcSY`>ny|S2ueb|81jl1%(!rmcQyRbBa0Kz`?+N1u3xPt!Or_v>Eni_iB%Jo?19c?RGiX3q_P zr#k>VL>+p$h<5|vX`TUi2;Xx9;D%=a7p#Ko4ge2KFIolHJOj936>K{IJahuTXfwR( z0HEAg1oCqKe!ffN(I>V%1Mm>#<_5r~X8<0e3*7+NZ~%A-OZ8A7$PIuio&j923NCvF zaKS2AcK~<@!1dzSaiapCX8;$hf=iwOT(An(8~`2yk}p~XtDXT|unJZ@1Grcfyyr0o zfQLZs3s%8<9`y|1VpZ^-M?3?#U==*<0Pw){qE+yaX8;$hf{6pb!wKib7w%#!e$Ute z;Gxc)m)dUU-;+H0o{?t&9^8X&09c*@c<^Vs0Wfp`c!}iq-~(|3VBi_Rg{t7t|NqgI z?cwJ1@{QGx-uXrTTc11?5&-RB(Rn*q4}10byU#*Uf$gbU=J9}+wqf?L5!;uoUQQ0@ ziqg5^JMK61ut_hw<2;_RwdlL3TlrC);P848c$RU2qq+;VOq}attpy@2>}&2jIoubU zjM!AiLj}%n2YZUcx7xu1fq&!L!G`=X@PBu@4o)WYywQ(S?fRrH^}_X7G8Bo0yEGOm zg-Qrq9gLv_n$nCx?66_>-MDp<7FcE-wv^u+Qe+p64EJhEHLCWoz0L?N)k4uEvjady>^$AQd=&h<4xs_IeOw_7${B+!1po2Yia2@>S;S{mA zo41Wuy%_DqZ8uB1PkY&J9s~+JwS9PHaNEsqhZ($Vd5qCFXyf>LVvM5pE_7y$k?qDj z?N2%3DD|@A-JK&Qsg+t#VVLikLBzTv}+miAkt1s;ytr^ zE90lIrl8>yF{1AcZ`}&jCIlBAMXXSp#Sk~44Tn<)miMOWG+|rbw6J=yuNf{6dMI|< z`MJcF>_077$6-eBQ*^94VL%{ctyeXj$hM$!?tH)#}EEAzl^th<*UF%&rW#DcGN9`4|G@`Dm z7K8O*kWkRbq(Q@cM)3zxsWcfWewH^TF{-ECqU*#s4`=Q=j;EdI41;Akk8mho9iw|i zzEs}}`%9>dFy%e9S`@5!vxB4?O*+#LR?4xc6l@{txZhv`alXN_!vU{G2jE}|)Z~N| zgK@}k`M_sS9a!Sl*85g+Yn@9s3ODY*{wG@>y8a{A-*mlpow)wJ*PpTW)7Sp^+NZ94 z^x7M*z3$r0Yg^l&+y1HTC$`($*{${#wDp|LKiK@m%^wB5|J2P_Z?10q)yBWxc;`lM zgWU*hJnhPFUHRCRhpxQ#3UcNCE0->R`tna+Zh;;F;mglh|C{w+U;l~q`ns_GOyAdh zU-W(4_dy?MsP22dZ}rljgSvu`U&1cEZ0-Nq4sSmf#3B42S0`81t2hE3q$JO_*J_Nc2e*elpt^Aq&e!lg@+I`30)3@HYw)MW%cOL)K zxBmT9O5m02YoH9f??Z0Ne1sP(tNK1NBWK)@2Nz3+`LZ+eceo)x)mq#41J45BudVss zJ|jPT^6|ZU#`SHkxGuit``$Gpf9u_mzw-s_YoGyyZ#pAC9VF>{$8*-#Kzj+_!R#xy zYvuUf{=D@yP*dOcmf1(gZ%TY`n<0C%D`bl)2fnw?#Py~b`Al5X7p|{?z7M{~XMbK= zd)jv}o7Nj=-&!)Qx6G#XhS^t+P3z6~t*?RJ6u$lW&?oC-``$F0_UmV#pP2UJvuQsz z`|$7$><+I%rz&dt-Z-1~qw}%tr%}!Kh8fpKTyZ_M^7m)*cz8y>L>{l7edVD$f90{+ zS0;D<%A>QdjPLxFM`nT_&AxI>@DI-fYt247A=rmzA{fp-JX^4*tamaK>|jPdTTkN| z*S;&Ri*@yUqZ#7fjQn^d9iKH5MtAn%5~&Yo!syJta!eS58SD1!qZ7jD&tQZ5g=3ct zcUonOW!!wdnJ`*2@|iHYGp@~X_pXY#7exN5Grp4#H7v-y^1$`#v3}j`qZ9Hf&t_JbeR#IK7Mo@G)S0|qJ0qXT zt2E>KpewG6mEwKH*?eC!BVQt~*WI}9+CQ#bURjv|AWs5F&;I=dN*|^G`$9?T= zT>13456)QUmyP?HnM88456@InaIr7C?}3?`q#5~4P0Ea`=!)y9Q(Ks4q1m$M24u zn#pBOzC(Gts}8k-z`$$UkiR|F>46p#R_Y7q6n5KfUq1%Ll%%UV7c? z=fNM(_<7^g-r(DK@NxC#ec1XPo>{_+%E+MckqnQ9GK~9+x!52{IREYyiP)r!nn4DQ zTSzT#Wdypd5YnJbG@8YnG){v4*cxYAphdkU!;N~9C^t&gcr1xxDZq0gH^KLj1f;?e zxQzjyqkf$u@IeU<_A|Bkh!HSAOI0$0KB6aaD=j494BGCMVA?1V#!wA#VFuN$3`nX1 zW5CWBBp*@-q#&?+{+9)VtIc=(O6RYEPA;9JlT+`s z_mEB?Mz|oI5ICBz*ZXRKr&OV_%L@If1aXf37IdO@C+QTKM>4%&7dKi#e@1V?1{N^| z6Rwj;1EiP}mPQt{VErKpA)OLXREfLRN%0AwleP17vUnqKPwC{M>;w;yLX%G`4Xlje7%(8=m~I$31JGEaU4nKgp_NY?0pF6WaT`aEIzZ{ zQ#!dQJ3%s5R)mIXtzGBpW`{=0?TlYsrjt0L>Y6a3(5_zcvw0202$YWHqg2%j#2Z4O zkW!-UV5@+|L7PkplO;SYVtLm(@qOWqPuqX&Je@2)@!m^1$y}73;F*X-;8HeFuSu{D zrJB)jwi;Zb69hPDhH@x_C)g2F2*QzmyDs+W`mWLJ=3=~(t(x&}Gni*5QQedfS|k~q zPG#Nd)UsuOr9n$>Yr2&RjSA`b(hYK|RI zIUFuF!v-BGS|$sg$z=sR(owE;^3Z<5(5t-NGgIuX`N}+ut0cJXv=ho;fn)QH_5b!#?!bU@$Pl9 z19bAxIXXFYBYh9qiT}dhPtZhO$nyg!m+9oNQ97OH$D*|Cj86m*%$OLtqGnkg|7bKs z;(e1XWb^TCt77y9p_Wd;Y&_N)_&HNR;yg`(vxjl5lkbVz@BiPig09?1%GR ztZJ{~R#rAQR@c`b+*tQgwR(1Sar$xuWR{;(CVd^e`F{HMAGd%1(Ldbb9wM}O9`5f< zAF%Q6-Nx(lQVx5D_x26e`8a(x!r%O5`}c2u!ZpqpO%)qw;5N=nUg~Sla2~%-J0IyK z8{_A`X8->6&$>o>|5Ub-zW6rMH7_N*Ph4J|k8#aL_nAMpe}A^>8sqb)`5P;nGmM+| zD)mzSdj|3FHuQXmHnvZ_%KrW7zi^H4<}_zZ@g=vVxaFmk_>mNyf-V5#Ieo~sY~>n9%`n01$o7W_hS?1_oJ>szG#}YK_1(J zE_9_pRUkRCdi&R?Ytx;Or%t@X$MgKLb>pVEtmkcT=iGl)lr z&^cnXu|4!B_U}jBBfL4y*it-p2wnA3Y5z!yhkNI_NitV$c&(pve*eN9#CR&20~|u> z1yyf(soQ@9(m90A#poQA+9zy``qS@(anW}O4x!Y7AUAhBGyyom=u(i*N%^|pw?P&^ zc_)yIt_JYvPT3%j?V1~2S_;fSF4;BD6{>S+D!*pqec)rR@!p&!7le9c$3w3IuR?VW z%`9Pq%%)v~e9@G&L7q4?Jyd*|fjn_&o-0o0(4>1dQU-F3^rcf`LH+Aqnkjr8>UR!J z)R8QC)ivIoDQ@FEacFv|A~M5!?9e<{uFi4!%JPr;DRo&67QD2&vWKb%PEM7VTwgn9%WG+YQ_001c6JXrSm&C%J=xyZU6$Z>b)5g+zkIA5|CV|XVZfVv`Tv30tCfSnWnQAsMp&Bu z7+|D8jjD9h%~AqxSjb+5r?Me*q~B^v>>dGWp&*hRR`$4X@UG2I4ogiz7yQAJ+z&81 zjK(5aB{UwY=~~mw5tSTKgo6Z?N$`0=4|hkhkZr~geUhVC7On&{X{9B!yUj?hrmLM~ zi-^^d4R=}h&eZKhR{cL~GR(UPbiRY^6NTrE2 zbrn+w>iLUgArJYE<#htf(vI(W^Yie?z0CYhzb-ySMz{nBFvVFd^5e&zZ6;5mAqM<+ z?(8uhFvz5xB$%k7GQ2?dwQ4q}HpZHo8etu26sWaRdz{3UM)kWkL@grMWwp&!Svvx< zmLcmVl^M6?zCxv0n#KaCAdq{hQi2>OjZwB&2?X^~D#r+ej@-c$aygmbgmqUD0J5iUpM(7=W*g$$KtTWxtLx$n5&ot$L@ztVgJ2CWl)~$SP z+!F%Wo_zPupKKGxh;So>8017yo8-8fxz&tR3?VB~j4{HimENsh!s-NRvoo%BQsZnZ zFd3zTNn8pmA)WM_0b`(IMuZldbP-EL$&wkjTy)e4LH5*v-P!N|yDLAta^uHu@Ynz6 z^&h*Qx%T(hK5~uR{@d*jZX;K}a`k;zU%B-cTkqbAZT{)z+ctw6f3)%DjlC;hyt04g zdoO?C^23*3xc(dK*82Uv&-%K)=U)2srRJq)t^LxPvG(-UkFBb!+ko}O{|M7(IlT~n z@FvSC89@>>MDAo1t4H1%aOG}3rKuV}BCX0G*FYgLmx@|`VfyV3 z+xNO)n;7C@sHJ8!b~3=yyiStoYCQ`FO-)L%pce!e$mC-Ll*A(d?K2#Jw&pA>L3%+<$irz(lU^xGV^@3~oT3v3Qm%861bpGuo8 zCABD9C`uH{s8#iw*-k!y7BSTy)df91nMl=OvV`oWxHwj}L`4e<)2BOZf4~J>KAG*p zAly4I^axx?m>j6Ogki)k8<2_ikRX$i3B%=TS&@Jcp60N9w+psRj>Si)A=EmZuwYWP zYL^DBtQwtayQ1-}N!CvUsJsS^kcufxZ#Zn<<$^8CCb1}GB0ISU@JAj>twkBGsAAfB!omcFKL876EaLXoy~W8wbCT3 z*DRACV8Zm8!}c96*djdCXrP70L@tE-DneV0PEeINX4EVMDIKe8!x|M7kmP^@>vr2= z`*s&>!?9?fpk_h|KZli?`C@^sXpvMUCI<4vu!Lh79T~=&g^?j-gy~g>?b~i9g>o~? zwQHGHku}L=$?7dGFr^g>_Qn_!=?1bTY%EVI6wxUS@*#K<&@0NMRK)9Ky*i=@OPFps zY~SjFts$TTCKIn^qTFti9Cjk{P+n>T>12gMt4bQSCYs)BD9WS(&~7?xr!Lr5Xyw8oSWU9SPfTkx*G98d@SMoxp8 z>6i+OI@z)cfw-KM0NTqA+c&#lTQI1mE||44VzsOy8$}7RQ99Q^-^~zr_AXz%$KqJ!(U?#vi5N9d_#)q7A@j-XtcN6UC$Y4Z>!w*lH!hwZ)#wu3BKk;7~d z8IE~&z;vXnl97252gwIUu)?IfW+fpRd=^!Km902zU+;n~)s_M!Qi1yv7wy3bOAHm# z>}brXLOP$9#@$`L-mC9+CVWj74jyyZKIVe0Oel#;RP$$ha;~42`!NNM$-_xHSMGpK zxSXs?9ighK1uCBr4jy&bKI(#PvP1P_sxhpR7EJYb4I>WKj1pWd5BPL1O7}=`P2m^D z*>c7d4jysXJ_3RS&WTUl(L_a8yIr*=m(tCsr3`6V3V;JaYj#_zD)d;SGE9XuzQAF^ z!NU&Qhh4DkWkCL9AXpec#Z*%<^E8=bBP?jGDB+o64bN)9U1=PyMX(SM!b1+*hg`6Y z_3()th>YjlWe*p6JVwQ6jP$Tp*i#(-waeWPm8^&wp$bhry;#<^}Z zTMR^1&{MMr&{_^#%LUumxE2aECps%s%+4ss4f=(CHIf)rG?*`7Y>Je)R=$rE8=55? z3>~&Z7i=vf*+>Kv1~;HXfy8bq$TX2A(u(Z*n?1bFO7J*=_3{#?X8_uP!*<|;ZM}km zvX)FKnC;VK7eUN1mhM+}cL}PUYV>2ppj<)*0P)Up9mxoT-sCCn4YoBo)f*aVXtNR4Qsx)+)RpwR&hm4+;lehpmg)LoYqcgoBR5 zw&Mb=hbS-MpzW}2yI|{~xs`BWI&4iBY&}F!2?s5QZOa8)4^dCTLDON|JpZ=GOAwH7 z&~Vr`T(I@fGD$e7J8bJN*m`gs3I{cZZOsK+4>#F|{(o|1JGAkWzTaLYFaBly{~sJd zMbq?AMbjKYBFS1pC=SZpFekLy7UB;L%|cJXgIJxdsD6^{#qm^gq@eE$yab`xjJ=d1cE100d($cR{o$6m7Vc2d^c&<& zpF7TWytTc zSTPKR{dWwO35ZoH+eT{>W~fCYG!hNqT0qF&(n0n)K^j#dkYFOcTWP(0_f5w!D4ODv zc{M{spi(d0Risos)EvdwQW(*LR*dOD4WXT=O+=Z1WliiUi=LNz`Y z7#sc}l@+m4a%46P)K!gqXX>_T`=-?4A=_E?5=!mWw!^l&{@5W4JBwtgu}8is-G&|> zXO|nh6O;ZVS^Hv)`l7WT9kN}`Wz}$7ou>`>Ztc(?2;MPj#vhE+>O^i)@`Eis9iRrrBm@ryUZZKt0 zSGv7?4J*@{AHrkZW&*>F0Mk^>Y=evtde+3VfiA>yp?p#b-l77NP}ZH(5Vogo+aWPc zI!EktSG^<)+?5$FwYu;(rPX-|vR!6%OM+Fvx_%Ol+w%-?36R;btCgKqJ@?+*we?Ti z-HGNhwsGI3Awtne?2ZRwZB*Y=6{B1WmHW!xq*e*}%}hs2j{3SGlZhd5SKj;C)VmsZ!cnze!I7!`l;b6e4UBZje*b^| z%BNRuJbL{fuTQRh?b>krtJ~eHe{;13>h0Gyzr0!5_|it{%73}?+RJ}kEC>UAv~Dpb=%K*PF&B)Hi<$L00dfpCbx1J@l7 z{wy~LvWEyfaNSAACcV@c^N&NFJ}0Z4b}Yfvj1iaZPDoJ5CrSWA95>?=Bj~TStx$1fDj`3SRZXKP47n2M$C>MxVYA^6H#2S! zu!jgdaNY6XDRhH?IYi)rtCzYpZV=Ej8Mt>9r?;L`W!>COS?qt>U1OY4Ox3 zLDu75e5C-vIiAvY6gzQ)06#?F!C~#i3+)C0c8I`3X_Org4r@0E z&_e_sxOyo#;syb7h`@u#e8+dkGUHJ@fia5zRHVrr#yD>>O%w`Jbo8@>HFBhs}2!(@O@nDeb-|L zuRKKH;X>9+xr_5}-0b)N7p@FeZoK!#i?4tDI(O|$*B-m}ob8|8rmz0R)z@GB&aEHc zdiCaSZoY2gZ#G`P@$@Snxw3ot7cQsP|6sl0`zPPKd^a!s{3T}XzpXv7_L9|)uW~D2 z0<6yeY3##SRy1zq#W#0`;b?g%M&M|$mSWRNw^bXGfk0AF%5Xp5>Za?Vl3y9|MuDu~ z%vR)nQ8WtREF$mgL%HJ9ha?yv~ed5tzpara}{ zLbU%^fM2t}du4?ItK-=_AW6mV4?ABjbRFjLHovJ1rS+0W5y`?n6I^vs<;+HDmpj7+3sX-H1vxsjG* zXaP!YXo6)8VtW%*!1 zqzjq584a{KZj|f$FP={hL~8f_A*I8T-LYx&wxs74E|Oi`zpPKDG1kx|gP zS*cWOLW|9mgf5yVJ50WE#^j2XzSh0pc1kz)Uo@Xwmk*=)OfE7o*p#73&0#KI&1Sfc zENX?3-$-ULIX@IJxw7l`Hd;xmm|e{ouDSoh!{J`I(-GxD4D`exA_L0GbaR7pcXu?F zx<<0h3vp?J5h1YyWm!KNylJJYeM%ncLz-qF2^(X zzx!~!m+Y``6T!1mshY|~#~M}+%Ox>F!5O`0=xh$Du#pm%?2*0v$hxT#bqQ}*%i~01 zB#wH$3E^_Q`vd0w3+5wgd9jw{Ldd8Wz^iNu77N9EK09G`0jn}&&>%q_luBfv?6>@<u4B8q=s& zV$WJN;3lcnzuz&8=Kg($!@Y2)MetBCnn(&U;43AHP0$p%ovAkh!&Zh5F~wSMn2rb9 zeg7`)ak!I<^!fAgaxj*}4UCbSQl^?t?hTuGeGvk&>VaoSaf)ay3?qBjM=HE@gmLLd{%) zBO6I$fadRXMBCfv$@x8RJ|1VLb^}_^k*2EH z1ne=rQoYV6l!mA#B&m|gES|K-x9j8MRra5IINq~&SuezS>=tvs!a`g*{y zx@J06q>Nss$+u#Iaw{|vZ)$qUs3tlLVKhu-Vz$k*jt6^=P;>t|^NE?Xj3Wq|i0h%E zLBR;5qiwRqR|PpY0I}cd2nmsiF)h{F>djP{k>wUMu;OEG(y_#P^$csDZJrISIMmAf z-+4IRGj?jVOe(8WDy8%?OuVn4RD&YIb$=&U1{*+3C=*0ujKoHG4q zH6JFPgdkA{#R)VlXkuBt#o;*cjH=yorUl`B0_0u>hJA~a!@={@zZQlm?LYHym>U=C zx;Q>eptS$(^Fch^;)6HubNnO62Z2lb&zKM5;hNmrAW&`p+YSf0&r3Dw^Y0Lj&*UiL zwYvZG!@<7mVy_QR3^t4W(f6NrIM#Pw>_y@6u?~xY)b?-8MR=jX-N#2dCc^7;5qh|p zIR9P$#9%WKZh^P;ZBWvGTfg?ftHF)m@U2}ou6$_iKW?qAhgLs){nuBj=fBRo{Kjg) zBot2xaIBld6rM)fJR_(}x{@G~26(oOC7a+~ zD~i;+GCstxVvd=BzDbqVILws-5Q~i~@nA;llgX|O$>8^T7%j3<4k|V?P9SGh7>b8xPpixaa^pXLnaW}^X@s3!57$P zC0WMJa2AV#CQ-?N3a%6dXqm|3c-Kn7;RzhJlt8q@Rpd-kO8BeLun;9D@QBm!VlmB& zuw+^0>UZ38A`8$L)qaiuZ2;?yAS*OLN19NI58#GInoA5bfzi=iy4&PnkR8so)2h@C zc2TVYtFT(@)OQ0y9VRt30UE4w=IVFddm_8A4ok=g&mw&!U!x~EyrCrFk(P&+i40}) zaWxmF=nhMPexp_Js6WZ}d-`s%YGp(9v1)K*9Ys(b2-IAC&OIoyi`xG21eUET#SGpi z)mp;L5V3lZ1cfv~gWZ!2S`z^^*K6kpiANMTAW=mEr4-;p#}ln|y5HEv{NS3Y3?eOI z3hZUydC!RqW-iPyrKW~w#+uN;%R;=V=F0|%U|k}zc#;%%hhaY(X;&c}+%{vA@~G6C zq`K7s+cR?2wvJbJYXK80fmQbGdroA53v1g*_$@v!CChbgEI>S`ib^9REo*&-q*Gd? zo8b9;ut+h5Q6WGE33Z}2a&kU|^OP0FLzTSVNa{k|Tz&36Co-76sD}(JkRjrc+}I9*VnsN;@>p>j{x%Q0B z53GOh>d$=Z-TPjb0H~D8&MT#Q2%$RvI`3qm0jDtgvgE74(vrmPKm~0HH98sx^Bqakn)^;G4j|p5TrquUR{Zguy3Q<%UuA6G8ZB6P35dlT4 z`oV#J(jOtN>Iyhh2bod_^(kZ9-MiG)iw#F~Yj;a46nh8?ycj$3YKRLJhmW_ku>l=V zU;GAH(x9h->|5KW?vS;A+7>}Q6~EuE2ky`o%31|PYWD#T+a?@$?;*ok-yS=X}^G6%wA)W=0he?;Wfjji2 zVr$N-URCS!e|NQVQMG61e5u+RR{QpJ90ANzI|E>#Coi=#;19!hEX>>!n|Za4A|VUI zBtEjIM@zw8w!<;E^md*{LIFO}Hukbm6BqA#yBW4I1|ev9FIO?kLzSe7(JeXx#TYhK zN@KwmuYgbsHc-vCi9s$Y+>cz$$yWMV}WjWSfwj28tb(YAHfjr>t++T7GA6EK!iZ+g~qs&Oi zDbsXjnGtv{u6Zu+1NQt+Bul&AuK?j)CRM|+0DQ-!>IR+cO(JO03<#qxofE_+8y^Ky zF%VCdj*Ns+DlD6fKdF<--B%e2?UgFYa*t0*aWoh!=OHc*H)=$_5-QS>C^Kq)o)p0Xz31}!lX%mI3R)d`un`nxlnsuNw7TNe*aU0z#_f98&I&vy2gj}D`A=Kk z5?fkg9fy5EpCapUPVyl*TC1jgs=Od*<8Tm}UHkucy?7r1bn|1zUW!lNF#|{7@EuEe zD6+_?{c0&*W9USJ(?=~9t-*4YYPb87kU;72EVxRg_Ba;1>tlvCBF##M=AsaZOQE&^ zUeKl5PG(Xjp2}4SO6>ghENUKVoW>Ps)*R)PB+QzF*LjcCQ4}1_*u} zssS#{pt_Zb+jfq@&YnQ>A(aqyHk&YHm~N@;okKBvG?AAj$`HB@bo^j^o0OR zE?kx}F=JbnEfbooVoSDUOR{Cz6r*ZOvTRjL*1`fyV7ZW`Eu;`gfRI2SBoIgfOD}95@XUk?tDC%11QPhNwA=aTH27mz1I`Gp~H z*GPh0$&=S{)C0lmm}nP3)^Von9!anUd(dt)t{16LaJ$yyg<%9v zFg)*(4fP~kh`kM?@`WbY-AP<8(Y*kbp==5CxN96i3Ud?No(GzOwnu%USXH26J5gAL z@Dewm&W`K6b51=wjrB_>Pj(Y(nv3Sy$uuvzHN-~9962#yGRNkC4-rB%zg3ts)(n*W z5}MMmq0}%7#)+sN@;t>Wq}@!rp2!Gke>Obp=DVI3RQE=nyc{~PxL$u?OIYclv;Xp@ zt?lApxKIys7uCaE>6GvEdbkFM)}2oef24BTwZ?%;+*UI$)WU_*+}06tsajYn!V9(D zRSUD6T>+#UR4^x#k}|gF(*Td zp)cI)*KF|%C*Lm~1n%{{R}Z_S%W&7b55BFF?}6*Y6)o?wHQbZS-(NR)=Qi&9HCW?z z)45>;NSOw*3Mi|C1)!fm^v|szfUO8ZYgSbRF4VR~?8$Al?N+|{1^i1@Ke!An;%9bO zKPMJuxKZMpld@85|;~fOcxrMCfJuwT;3s~7U$;A3G=zCoMl@e zvRI65zM~ddm4G6GaS1>wl&hPtJ#t%BQyj6hNg0qzClOMf;0-`jp6BN{??Hx9#Q}9r ztBCC_x@9r+#U*%ni(9JSmHRI*zu_)-;I8Ud_^DLCHR64DQ@>pT&3-aUxC;=ViraVL zg^IZF&fkHG2$hhF`u~&20?D8Zj~i&7?q}PrSz4Ut! zLmU%2*K>P~dY$88yF)hWJScjO=8~|QRC%=GOwP`nY+DnPeiF~U8QYGEXV0FqF-soW zJ{O&9#7uvp#4{N8P||is9ei(!xEMN6#Krgj1NO=Lu6Wv+|2p&3)89XR&8hF5nxFj6 z$@s*#PejN6{djQf8^^q(UpqQI^3@~G;jbLF58ZsoJoqmMhX?-ofWH5W`_+B_2%2B~ z=h_?dhX6VMI!y1Emy`Y48*^&|xU7`y*WQ?20{Dq>!e4vixg~&gDB;(jiw19^+o3$U z_Qpr74PcD|w0i-NSsTC_bk%zS@N-K5>oC%m47XVi- z0jy#FT*rI17XbHK8^C2=f_ttF;4&}4J(d90u&Z9)C0NA&v->`H;N)A6j!uY2i~IX$ zPF->I=%E7-+W(9DAA02DM_zeka`L((k3MqpioZPbnA4v+{K@@aJN()M*B`w9;b`Bt z4`YY#dg!w!Hcvd~&>K&`?$9-7UVZBKPR|Zer=D^e-~aAYpF4ErfzH8yI{21@&)E0D zlmC3;D+k_w>fWGUz?YAI7R$hvG6zf()xxM*ZAu(k>E>J?Dhv}GsrZoHJDo0D zm~Nxnn)b>?wj7c~kjW0K?X))&YEn-{`Rr_v@@g^i=n18se$J|ek*|+9L#3lY&X1UW zBz4N!)E>xj)o=R+By11s}@tKDRtQfsBNnnsB3wq)HAXMMG|#x(1(&f zrp&7a7ah3MBOh3eqmfqN8a5vXvzROZjtstyw;0F{wNb>UMP@SO>7GmuKryo`4zF62 z(tKFA2W;AC*jkrPnVMD}V=`Cj7~D+mbkZ zo6TgZo@%UMRT@w;g7a7;N#?i$QNGUd4NPr?!*)#e=YxTaU`=Oi#Kfukszq;Ln4(;) zmAc}jV$W-gWXsil56^M5>Dg_mKxwFjpF%UZTF$fwFaoEG(!uJJE|cZs%xMaDeG~C!kDOfZ49f`8*ovy5B&V9MJHj!?zmdS z8djQRC`Q1XFq0{(8Z8YcHG^?$CAw|q0k`tO-(0n5ryq4@H8ZSl-6Du74 z_NoQjn?uMbllBy3IL!MIE2^HJrPcC4N^&IyDH?uWEDDnry6@Gi7WJ4ar$s9>%Y^*~ zz>^g#gPx&UT3o3oY`s;Q^%Y+#Obqp4vT8vSL}CFB=%KEAt!h$m21*!DoQkZq+L{$; z)p0w_jLTz2J#?Q{3(BK(xjd0HL5~pL(qVtf8@f6ns@1q?X_1%L$u96$5Zs~ntXdFh zcUTUak{PLDe&S0UFn&6KcSjpASvf2@9g|JsEZ?0X2Vb+@Laj}`z{#6(nG(ElBbsqY zwd7Wea;T{+g3ft%n6-$UpFModc8f+AO^O4oY0lzWC7xJ{>tF@i1r=z~ELop)l8KNX zw$(m&@b^|NFbWQZS!gtEqAixQ0@8OKXdWh^W^g}Ed5UbZk(!L2)xS87|?nm=`|0XECU1GkC-W+O_<+cn7dgnY{; zd;33@J*e;BG*b-Ghf|8`3>!%3rJ$ey3L=C`Fp|sdnwFF2+}Vi}1eKcBFAdL5N+ZFV*ISy@LkcIh z54gapwuM5+atyX319eIQ3I%1r^JNpFs_IN?m*5#xZmTSMT3_|y4`m+7m;=|hN@))y z-)5X#rdBA;k|8!qGn8H_7D6J^?J@_pk9M~i07~{cr@IIe%muod4=Bm+Q`NlI#?0}^ zYF3G!NRKPS(@$CTF_jBU+wRxLZlB3a=~0f$p>U2IqogZ|P|+-Qn)bvF#S!Xl6q?);Gr0upA|IQRixW%??ytDo-n->Xdc0 z(M=lmq1Ufk?41h!@bOg(b1IZ8N`Kav6(lq-Ia;MVl5%NB5avOBMA@Lw06#C)$sTg- zcULVy%^s`}!eu)e56M|~-r`NnM(9RZA7nEy3CgmFLP(}893Ok&s)bf+Gy!*e!GmUI z9UhIXAkGd1u^H=qwQaEwh4GH%im0w0y?NE5pU!EwPtS*`%T$?clPl!~*XA%VwXlq1 zjqwo4c$y)*og>$+T9Box-zY@^GojlpJ_`=yW+6in%>qewd31=nY0z#|5G0|Fxyu%& zD$qP%r-x;O)@n1R46B^c<~V7bO&TeIOlDT4S0=-@OS$yBY13Y(=tiA{aBjN7%q z;^sm@^P!?n4x%R37F>n2#1?gI`vH(N2z8QJQQPVFkqMb8!<=k$eNbExgERAiSFdEc zjx^GfV86E-hsd?dwR%4XOBCbudMQ%@&xAUj0fiEroJovklxvxlPU=&~&MsTnXZvUM zu+1{5JWF9C^aQ<~ZCI76)9})1O)M72y3mKFiie(f=&FxY;>}`C2=sh#6zlCWCGx)Azfb zri_XsCJostk>pu_(qQAGUs(2GXC+T77LvFNi}U$_8aGmJIK-xTNXJt_$~qnJ=u5jS zR6&k?b%m;d8-y>ZG14iH%+Yi*Efr_NgT_od!1dTd7VvdbgE$HzWO+u9M zr&!6BTrbl^>($8i zxUKS`2G!eHHk?7Z()8%dR^!NrS`nF|QIGO`LaAkXxiqpUCO=Oy2<3OY3P}pc*daLN z=nuEYF%rRgx@}06#v-mGrM^CE@#5Ud_3}~&@{wNO$tP*9iyYn7sJb}r5wtb%vR!&y z!~$iK3Qb`IvdWuOoGBArPmj&Ejf}W6^r{akq-jBmC2*3ncF+?v8RsC4cG4=NHE<+I zp;liU)=hkP24A&+3aCryE<@z#VqQ>6(@}<gm5-wlGb) znHfPD49?@#(R|c&3Dg(3I2g>*;gFfCbf`_ZUNO%Vj=W&iM_%cwM6)^`=NTrT8cH{G z@)Lxt#Hm-C_>-n7`MN)(uqJYxU$(Fz1&{o)OjTQXn-1#?s!=dgwF*KA85N`A2f{qh z3+;x(o%ryo54y`oad*IK@PzD%cy-L=#+`fxf)Tk>_NJ6)4~i9IrXuRmbNg0J11TAM)$B$`IL#H`ng;qoj=1nz&`e{KA z^w_ZL&J{rP^rr(79EMf>PEa zMF)i5ImIdq4&ot>VkNyW>Jt;TX5lEN570JQVvcWr-DO)5-ExPPM>NG8N>dT#AmG*B zLWq1xs?W<~LT?5$i!T?B{^2tI{{X!2)YZpce)#vnAO7d(e`etJW&m&#Nk8Hyx~%@W z9no8hAi3S5w+g#*6Rlw%Uj|3rCb)530p|nh7jcY_xEMFl?F2_G0ts)Q9&l$m^`h0S z;`((*0o*I#@>s;zE~?EfenT?@cP!7h`N$r)U=hpYv$--L7t0TFNDfq2%;e!^g2o-i zO|%>EON^T=6wAnhC27~V$*4+Y8_kr3aF-AJw9<6ju-|AV3a;4a%3YVo!3URT&6RoY zlEaqER>+cVBVQZ#EI(IJxhZBfBEhaW1_zDPT()x#n_}f`CnqXxPEjij32in=kZCHj zjU<`om1$hndf}K8+C?V_f2?}f36cPuwi0fwQA#wL0JTU4uaaaI8Ybakj?Ko!w1=Ff6XIu}jS zvv9)+e5~oyFmlP8y`7dzkvp-yvr{})RI?h^n^D!K1iM@<*7C`00!)vF#d}y?;;gfB zo*qGRs+qOGID6J8j{t*F78n%+x>=J1P7qq4PQ>0Q@Rmbg1PLG65?7fr5n$p1tp8<` z)a#ZHKJL0$;HFRoZp%tKB&$vE#u_sdo=z$?8@+wU(~jM~Sjh`BWuWD*3w>R722&qo zT$z(0I979csQgnIHoS(9b+@qLpKQk3k1}O$DUY>SoFAVolUtB$-ZpIbQm-A5y>d}a zH1RH>ouit5kke){)Ek$J62w^3q-z(^b|*;ub4II;l>_e5qlCF~qLRocJ;$VIu0nOC zaa7LHQydLp@b!+UP_x-2n>0Ebsr@7?z#WG(%5A=ZaKnOwD_slbMWQ?H)bmDzt+jG_ z3y_!XDO;wn82UmJ#jVcb+PR^;e{+q^w6-EHZui|Dd=T=J(7QFl4R(J}u90QCyWTC{ zbc^kN2jt1@WR6*MbGaUr%S^)``=BiEs-U+@u(=3L2)rBcOV#Xb;o@YMR;p!rY36E7 z6@=t>g+xCWPvWyFU(aTl{}v zG4$>D|DW6U)_qsJ_6qjQx6Zu$40ihF(=R%WocipkXP?TP{Ogm~oP6MkKRe-`xbpbB zkL$-z9ec~M{;~Z>Uw5>5_YWI`EML-hq4X zf6snn|7jrj4)_tDyYG;GpT_1I1yez4NgghK>gZ=51V8zrE(SpKwCldfHMjtaB`HL|j?!CPmpEL_@BObn)uHbe(Sg1{H5R7_#|`P)BiJk(9y}`Cnuz5J>p;A@`wNaU2*H$rC_5Y zI}8q3Ad13xlA~C*cC`>p9MKa9hvPAf{M*tb|v_!;y70XL z`^LZi;^T|&{%FI$_lD>(Wl%U`)KTPIHz=gs5Nkq>4v~@Sx~lU@EjFU=7|(UObtK+KZ2i#<&muqdH#YLo zV+VfT|LeQ|(?{kXe$fY?@_@IS*xwM({pM+L>zbwD%8V@LCMu)1(uipA!&H^MV7V^ANhLvxACdu`(M4wTc3PC{~uqJdE2Z0iGK9x6c$|h?@sN? zKM=RlrQl)7n2ilGU)Dn`WzfRh>h)$AKOtL1$;+k#5-)Oq9o93&&T1;2`N#kE(y!0H z_2tT6ychXM@!s#;9Ny!4S2< z`q(pFL+61NJ6nZlNzWyiF78fRpappv5QFz{Ne_bfY(iiM5_beWJ|Se#E<&Mp|8E{ zz~^iJ=?`;f{%!Ni_q+N@*Z;?#e8~Gw>&Sn-?S`-T;#Rm6EIKUeYvX)#=8vb2#eqP)PmVz-pHy-1IaoQWep^CBb*ej?a*G!y2fgTdJKC|dT8>1q6TS1QBUw_GC z|LLPI`0gLR_lEcP-{!T?d}#WetHB4(cmC}2+2_3N72=k^6by_V&_HG2N|qxON0J9E zLod~hatD&CGle9WAvP-m_wK08tPp?sv!x$=pnMcRG|@lx{r~vPS6=%AA^Om}-i+t> z{rF$kA&YBR5d!jd>H zu#`dLX1|e-Q+*hW`+eZwUkQFz_N&Ny9|V2yF~4%n&0jgw6`yo^;~Uu4i@*JW(kuT! z?ER%6ZcUbgr;|>N4B9!N4C7=HIBwmD5qt`xa#gLjr

(J}R}a5?Mu8f?pjSz1Ii7 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 jCl!8V4jUZ_Z^PtG?-8chtF?$J z%&CaUk2sd+e0;pbZ~e5~b8)BTGCi`%Hbta6B7448nk=W-i#;#ocP{R^5pk#bD`~9L zfoBq~6}@?blb7Eku7}S%7pH|dY(R5^Nw8j3u{*<=hb@o3VwXc4BDahl?Y9jNb;rd~ zXSsf3sfak}1V?*?mV%3|hKiEydWE2uMxR!4lJBET)( zcRI`6%-TCo+zZVin1I46=h=QY*kfxgr+9r)G}sHH!CtV4|G#hTJJ&9_7j}={c?2H* z(BU^6{MUo~_kUp@-22d8fAX$UMr?O%7wT=WX0SZh-zGU+-Z57#iF$?mZQ>bR58FrL6&-Dz;`?SIgXr&mR(Tgr%hP7bitEdXzU3B!<>z(~Y9<3LffpLYs6MaKSW~W8^{Q1H1NWKJ z>eQXigV0!GF6jPrPyzly@BH(@s+~(8JM;g3DB8YLtC;R-S0V7K$6h z33TS88U#BiP$CE*vOXV<*>}Y&ho|Lqv+24S1d^8ILfelIt7AUHfS)J|^X07l0!&YSd zJyMQ*Wj`n<8R*R~wS-Keh=VAij!D263q>EPG@4~^+-GQ=!qKT+E_%aBGkQC`K1f`? zvUgfiqU-5k1;`O@nwBwgKAV&C8ZsMplwr@*2H9M-oE{fIwZprSHFsl4yQd||Qw?G< zYABM$wBmJ&eLdyX+ar!42SMhCHivgvy@%I@5+7Z6*OwHyl%1fcg?~fFX0rnU{KzQy zG@HkAmP^!qc&NVVvqMf6Zw6wm9c(WMuMQ*(+SuZBJCvINvDGpghFGWL3ps^O3gcVutVJq@`je( zcJvAk%LrOg{IudqbDGqf2$pd(L#SxX{WRVTwl`V2-Zpzhgh1+zw5w2Uq^R8|%Dkc$ zMl`PIR&Q?E*7B?{N-vpNKGYnLiJg)OI_r90O!5t?n)NDGbCk`GibJ)Jwkqs^Hrjd` zG8&ZCW3sI~L?(Z;eXX66!Ca>hoGPw(Ohw^)J}}aTO(cj@WW{0LaozF^s%L70Qazcm z?hu*fL$b!@2LmpP@dJUgcn2v4E4ZMURz|kcPt4n%Q9+uONfqe@ePD+OGNR%%PDbW( zM*n*GwLy7{{J@|TC&eI!rvPU1i80e|raige@hzvzx~Q+IO}?6j<;dk-*B@Pe zO;FCFKjOW#u5e6Op*buM75dOHNSD>>k7<_Xm6n#3D+yPeiBy^EMlR<@-+lSjr`4Gm z6s#ONo$)p~vIGOlBzrPp8wCg&8I58uJE-N6Y`rilrQyg$eN_*2Q$y!2Kd|5uy!K>J zV?IDux|^FV%||tANAyS=H>%o|3ApM zutHyTa@2h3sOKZD`7t`s_Ar6PY0uMB(AS?$TjWD{6wjum^O;+oO*2x37N~ObTnTW& zbQ2x{!CdAzTMRT7>AM?Tlz^yI5Y3(I|LHg<7^=6HTz8`W~tkA>x~Fi={U z)+)!#&&~<4!btEeOAkjZkCBy!g4lx~5avTjp;8G=t7f+CfO1!`W(uua(*ZA!T>`sws?m;?%*?UMC3hmJQ;zy@|om z-~jA)4PJ8vye?bGOT$XFfUCIJm?f=@B#Q#tZ034to%JEtg$f`KmvD0k2j_is6PS?s zwts0(N=*=%^Ry{j=C}YyI@W#q>S5LPPmU>Hb11I}?D(tY`MD{5PLzy1w~^~7@X#G9C{Kn0zhbKd-r^fE=JTj_d zPCXWmd4=iT*^YJ<&;F{QdY2ma&9m!88+Z&-@SH&;7-Iz)rGnHmCobk%QjA3svU9xW zKgTPkfb1fX&6(%_wM@Oxx+In%h^Ss_s?{K39_&$7q|&-XHH2m^i;Q#A_N5?VC``n4 zJ7X-rWOD)x)#xTYE2iwcn*!i^Yt%D>olnIx5V0#C!@_K;dx9cgf(kW5N>52DD?5bM zxI_(L2LsuPQWB@fcyl_eG+~BAqMiR=yn0}Wi~0Yv*T!oXzT?6Rj{f`6a}U4$;1dT= z-v2NA_w4;&d&S+~*hO~!+xFjXzh(P{TfevkZN6*s2|-kTZv7+c-xUAr_|^DRV?PuF z1M$^<9?D&L+;%+J{}RQ`7e7inNf%extiQOz#+QBNLO}gxhvM4irN}#SH@Ejy_V4X0 zN7sscQDkkMQRE6+`1X~yZ?U%0ZqptET9&yHMO;nvnF`})F___bp0 ziTu{5#jLQkFI73DbuMi=iTz+*> z706p6(R&4%3Yp@uPtsq@%+Wm+uF&$xI!?&#TEAY1MdDc?vH=8xUaxx zm38Nbo^*K*@TQu+=juJn z3|E){-N^7H)20E zT%N`~bl=sdFF3yN5zn}Nv6~xLgG5Rqi*KMf|;JS z9_Hw}!MzaYkGQuN^wvIe^7+|Yk-RBd{obozx1`9eRpxsSlQekIIiKTJ!0vVog_ zBYZbj2|+*Q`^W(9e4ZN%hF=;jKL1*X%=39ypR(Y2!*FPY3F;}&8-_#Yn)_mq@xlM~ zrp4zMe=ai9d#^rusg=f8nYymnN*A5u2fxrh`TR&^j`v)B(z0V;VcL62@w9V1SB(p9 zpL^}e=f@*6y!Yx8m$bOI%1ro1Emj1&+coL256_tS$Zu)OxhD1&?Edcu7N36v-HPM# zOe}DkPgrog&S~ymw5VzXO(4whe@6|&2u>}dY=#e-qGUo_n#G6kr%H% zZo%<7C$zmv0O={m>zvTJJQp3%2Y>UGi_hPB?yY#9q00+Py4+f2I=-SVPdcDaju#w1 z`CE}WzWD0Vg5wPx& z{y(;Uf2+6o+nemhCxexMkFQhlUyYYzzZ@$D!jJLKgIAuqDT>U_MRb%TGJV++a0IE* zBL+fgegYMJHlf4(Wssl~al8Jjk*<#~LFgRmk@PFD7)7 z%n+C*HLg2l?&E`V#znE%AbA99OelW3d`j`bD^Cd-tnk!}RanGFLYF05Y--fa5N*_K zB@&rIb)<|+#bKh0qnZw-Bul}fFnIDQgXjqpR*6A4f3Eo8l_!M^RtdaVAtd4aWu6DG zJTYXjLT-dr!Um$~;0Ymv72fOO3ef~nbny6)!3vQV7gq=}IDhN&!7Gmo8LW^|VU@^( zD7A1QWUzu}S6PFMQVT~RgB3J;afP+TD7A1HGFTx$!zycrQEK5JWU#{O?kaHzQEFj7 zWUzu}FRrlO7o`^VLIx|Od01sRFG?-!h749%YrVL_Vpx<~*a;b|pxKKnEEqhng+ctZ7NTcExU(4z9=s9@8LY6#w8}D16dkOE3|0vFxwGjF9=!Zu$Y7NagcTM* z&fm0s@bYUz1}i*umDP-@Ic65g`4oZy;y)s%xH%V09NvO2iWeC#Rx@Ji_5k0+PHpB$*U@lQ(!g~BE9<-6PnrxCPv$(GE_v2ES<@ycLctqyDPIvVM|>XTPr zoalSS)h8?(`?n*pRW>xv_fSiXeQ}=ljK&V-^EWs4M?Xoz3ccsqC;C(pnJeYwWt!29!i2Q5gfwRcBE)q8M9KxX5qIH8#tELj`WcxN zR%y8l^WM4o_#KgXX#Gt=5|kAh?aKDOIPvLC&Auuu(r$n$@T60@xp#x|xyQob|Jg@) zSJC?!M|chKWLI^el*i!QuHnj$j1)X^>MeNimM~k?6)#GbBi?(nAIP8S25#G$fz&On zxrLgrC1u5CI-^;9(F*QQ6gaos2<{LJE<(%>8^VYz5AcopkGY`Ql|gQn2SX_qQ;r8AivqZC#|DzIIi(?AyWJ;hCxAi}@o zkK|0rSD-18R?ujf406G%2ex>*boFtIuJ+B(irv{sVbrcR`&d}s3e&_zt6XKOcy`yk zN`Yru>LRq@Q8LuCy2tCs&^LC?iwJ@Rk!aZ-&d+I*JJX;erKvcJ`0aXvESOo02G1OH zTO7|cps5EBTTCIFl!M+^oT>9t;gaiCC=Wk2iJsTYlqNKJ+n!)tAGpU25`(+Oblh$+ zRo>2&Y%rs-oOBFNIz?71VWrMgWAh$4DIVJ+-b$LLq2&j&l0R-$7=L6PTSd}Tj;GL| z2||MQdGJAq! zt0XQsfA{4^m!4nroM+6QlF*s?P*&KvJ8`)y>>HinSJ;y}+eNRi%^rRBbZ$iVg8RA4 z95-Fvu#md7i@tf(bo+^;&Mf*iooq6n1#anN5eVqnRNK>B2gy!JlhK$i?H~Ko_J~2r zD#7pr^^ypWOU2tyLwB61BRC||vwDd|lE=l_bjYc-W3qefH+jt9k@kec)WsdL40H+g z^yUCWo19A?_gZygTn!SqOw3E2LYpN8d4^AnBvJvRIoWHkeuk4>rQt3nPn%a?w6N}P z3$h)p5Sw!T?(ULxFXr24T6glS0ow|Z0VmeFLK6HF{0e*CXIu9Q0VHQx_sy|>3tHFD zn(F7>{k9M|-_n;)z$~P$xAZfJJQmrGlG$A5mNO@3Qtu7SR?Dv(8}@{x({`bd(|KZ8 z>{V0cOA>~1RSI{|OX%&Ja=tbm2bYvPS<|4iom$?P{JO4DK3$}+%;;3%V5dVd`+`GsSZ@F7Qc}d00GD!}o>glmf%+Z!Y1bI-AwwcC`b9MHT zBsL5OMfuikPh5)GpzmzVFV&%H(gUlO$iw*@+O0Wb0B<<$I(ZDGYn583Th5XLd1lto zhAiWjlo{mnG@)=gx136Xq@8M8d`I#o4Cm*ONE4TZAp)TxzTlc6t`Mnseyq<@k9f=R zE_jw9t`H}7(!;F~rF4Q{Ap+{`9&v?m!rL>%bI)5w>k*evT;9DsVtTQbapnp_rNpQr zCyG{MRF|8RT(i!&v#ca#bNM6&r`t`VLXI1~!L*yYZA0v2eLA?ydIDC(vO2>qnaNh! zwcx3bAA6T_l`dDa2A8tq)PR_)WT$TEGoQ9bZpH69hL@J=R1mhFocFpa(*ZoWD+4Lr zikAN`FvN5&FvPuJmRgIyW$nVJF8s)a2QM@)yzuBBkA5S_1@M}q`qA?bzjFAA!~b~r zz+vt1xd;FI;A00rbTB)h4xY3B<^5mVf7iahU){fN?{D@#viFX?@m_iF*}Grb{qXJ& z?7F+9-DmB5apyxjZ{Hd16nCDn{a4!`-2UEeYa8AEx~Ah>xB0%! z@7mNhU%vU|jX&Rb&&FFf)QxZ4c;fnJ*MENf->=K--?0Ap_@BgoF8-Fd7=LN}LhLiK zpNak3*dX@O*yGneyY`-deC!je%(K6Sy~{Q5q75byf4-cY3KXQ$U)5EP(Xs1$! z9k*t9GS`Kvf^*Xatv?uhXH@79#NH7V`u(vVj0$~w><6Mkzc2RvQK8=(dwW#q+hX4r z75Y7~?~Mxm?%3O+Lcc5aJyD_G8T;<2(6`3ED}~e*Npqzmi6NJOQJsYQalkA`l9$Xm*I)< z6q^?Cvc%zpJw~R>P5X24zq@pa_XpRS zw&dYey{fSCoc8JBShANiu=Pt@zkf|x=RrTX^?OmFAK3ccsL=Os{Z3TqFK+#IROtJ* zek+6;6S-BwTnQhcT8(KSIAFYvVA6`57@q^c1c>6*1;OX1>{m;gqzy7WJ|6~94`}O^M z*Bi0^-skuJ=ib%r@89dhe}3-WX}-Qg2=zjWcFyYJuqciY--|H5!LvGeykAKv-y z9dUbm>r-3*VQaMYvaQ3--`o7*O@Fhv`K0)pHvVYi$2T6_AUB?|{%7ky75nRWcPF_M z+y2;v+=Z>9PaeJNXms?lqr=1BJN)5TV(stOJ{)_0{0oQvAr^7M3a*Rsi}ENjCc9#7 zs4)q!Gwvj3X+*-yShiu&dZ|{CG=tF^Ln}eMJKq>e*!4{CMIS9XTGwr)6OL1sTsTKD zZd-K0NlO9pQazg+HMIDjh7u+?o8vQ&Pq2Qnke(cOie&5m#x{d-GTg}$?Wt)RwLY{3WFBZs`Z9av8Ht_dIHlb<9mIO z9mrJCAbURNXGMSSm7#>B)JJj?U@MSS?F@0>}g(&aoVokD2~zjS}bOj%GkH3s+^{{&a6JbD$39=3B#dU1nLQm?;c2D8BQ87 z<|E#KgWKb!aXBsaV6Qu!`ct&w%>)$9619SsDPXhiP9rSC0*!fUl9afzflv*nZnb=< zM7DU`kOU{2w2Y1@_)U~_TKlO`qSM6sxJlzBUmA@~2Gi^kHfC@EES9m!h*2RFDVYWe zdcfX~gc1!LDI<+U%Ia4~g}k1$T$rS=T&n9U1E^|h72A|l*UO{Y-uH#?At`uX5Q-${ zLSB-@M?*p-Ej-`!21H&0DzavXjlOE9lW_b!p@fk2A+4g0_@U;IZKTVLFslJ`oG`3m z)w!g}l0;JJ9-f9{-xf*?bf>8(xRZdthCRU6i>#)@c&6GCP^^ zh2A^?B&&jlG`VVa(nM;-Lb`-@X#ls{LHy&1nRgqcHg=LGrjHB>B4}2@9i^r@(|p(U z*t9e1wJZznc*tN24JA5y$^_LEp_j%y>oH;m)LqA&HQ`B)rR08}wfZ(s@m)8!c~2;!G7-F9EEzdv92Vf)Hqz*Hs4=Cw&{UTPZh> z^9_eg3F!$tXqQR?g|1>h_aLn^by! zdah#IlT(Q?LrM zYP1T(U_Q1h$&S(wZiTZTg(B@i1K=dA4s`tWKM5reOqXCRO`9{t7?u#AQW$|{bJ*)y zlnNU|&jpHz)#%I^dF$6hiF`K+I`h5_@=jV56K=apRFbS9_4)At$j|2W8HpO@&cwuG z9||SXL%ESFh`gz$447~8c^HKyr9i7amnV~D$jL*EIbVQKY_oJKkuUT;lN=TCak@~P z)a+7XNhUBT+aLD^^OoKKv;-V`S119BjMl8!dbgKs&r?>iIGy3PTkpH=rp_C+ zEQJSg>mbo-lN;ZED$&bJcn2~nrKvya3z|5h$2qQ*$mZ(9HiS!lvJPPqF($3`FPu0Z zXKsAmV&i98z28q`plYBgjsqZA-9 zwrs$yf-@I{3nlWGiwT*{^9#QHPCGB`J%XpZgntlCH_InA$0 z9-VbblFfr|pRQA_CZRbHt)Q*-&xaCypxzbT*?a~_nus*UtxhT7I^%TO?$3&7Nw8BD zg2?#6xGVN&p+u()_pw?BX(ctLl}`7jZLLDd^I*(SPaC|#7m}uf1m?o!Ha-(dv~wM; zo68G!M}=xw&P~%ysh;Z0a~N6(25z;XlFFdzd6L$)UUVvvo@pIMXxcql(6SwA)Im|D zYjdeii71WS;UHaAp;00&``hcGM2j;>lti&iLk}DiMb&JnIZC$DMHia5AUROfrk6}+ zXb{*iLW!~y^lB5W=r!@_a0H7ypTK&-U28PZ%GqT#W043U5e8oGZsboTFh~IhMa{-C zo`q;`$8t)%BVv?S@ET&!bZQtAI93%+t-URjAQ)ua0eqvx)PYh$MeR

=kDt5*f{z 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

AU|MvCn@yJJuKYhRN2Untw{r_;_ zIr+|&ulvpUuYbj_+=p)EG;uv@2)2-pVl24wwYSJ;ANLQRW!T2OpZ=?xet-I@FMsrN zj{4OXpY^oA3B2OIZ;R^@Loj%6Ip1Ja1a`(9?q04Pr?YXoOg0^QQgXY!an2X)7wWYb z(cEYtmlfZ1+iwo7#690S>1XV}zRCxm_odzc@~x|c(0iNL{MGTNeNee3MeC2M5+igUVF|HXtaxjoUb9Sw2zG`bU_c+ zi{0{u;7ioi6OY@K%0{ny`iFo2$LGKEfooIS7ry$1o5)wHpZ;3v4p4c7A=pCxjj@k^ z^@jVO^|m`&_;+9TYW1!Mey#66bm_tu{_rjCBkyB+$(ye|PF%+g!4|S@j0L}z;@|nt zAAISneihrDKXlur!XtOJI_vNcZad|`-j(-X8JEO$%n)oLv&LBPi9e^;KXKfHUljj! zr*`N~3;d^hmw)5>dr$xQ%fI`p_;ZO^loAN=WQ=lqj+lDPgtL$HNx8DqgOd~Ls{sKd=g+v!)!SBDu7uc6M_+KCR<|Ekgsa+@B z^Xrq|@b4GD^cFw)x=-JE-{*fWu3t2EMJ-}?qp4D(;7KQ#0IswGNunE1ONAUvkOM*h zubk%!;UYS4j0M97?|2aV-aAh_<ggAW>(4g? zTga0!7OemHrmx-nFmfvLd;E2kUw_ac-+m@uzT=VlU+UN1ed&d#UL~$yXb85D6=N*; zs`IY+w47zgk6+jE><@loKlI^47oB+J4F`VlgSE>eZ+mx7T))5&Y#{^2SnwTl-?2Sy z_sK8Vck`8ZJbG<0fBPfS`@fR^k4Ko@FTCg_Z~Eu6#Py&d*g`6cvEY@@KJdwFe|Gcl z_5VEaxIY}XE<+#q`YXS^7?>7Wn$U-p|e9`&!6WMDn+4Y*&K2Q2` z>5PX?>r+m!-vZk0IDXa)q(rKYn%hr?3aV^_sKv8?nmgi8tIHRGrU$qqg{J z_gTju_p-OD;=0=qY#~d+Sn&J5^3$b{9r!kVm+g%2eW7*7(l_4iA|Fcp{12}?`Sgpr zXYBsCxb89pTS#{>7W~#xM6_sPP~?t10geBr_qayLNY zy3-JBA*sPw@Gp+7WnNLMfx zyzAZfJoLM>+m|1F{!edy!2P<9t99we@7Q{U33~}9V2)2-EU~KXEE7!c^UrunMy?5MrT_X79Km7dl`)(_plKA80 zmw*2?)zfc1ZnXYCbM7zacE5i2?p<%)b}@xKIR1~V zMpu5ka>??qmWRvFT>8+`*^759iVHtnI5_|7`RnJOWBY*Zxj@9$eqFt7=vmH!EJAa8 zEv)Ef&Qdoukwn0})dGxhZx~}?6}Uyln73~#Ll>~+;+=DQEi4>ow&lodCE1}9=-UtU zSy)fbtnYBDlKjvC^tgc@3(L!y^*k|838>Ef8`WuH!8x<4>D)4J#;U`8BU?bREv!*z zwq=~5q%yPv9V5zESfkCHuWvKToDcMkC}UyiHnYAbQsz9MXG9qbE4i8Vm?(4ZsG2}` zWMOSLv#P0-IpT;~9iB6?1r*!Da&Tr_9;3|JK*xwO78Yz(wGUfvjb+~tA3n;dQwc^aSJVu%4039RB zSXkZ7oRM!c%IpF9MwGFz=$l#J6De~h&@-Zpg*D*JdQ6l#V^mFmG8R^aGpm|T8S_z? zIy`-33n;dQh2+e(j41;~;?p(?Y++G3bAGPr1vVdV0%K0yFvh|{^v`F^DI3OESiEkL zG3GN$;F)J{7-M02#k%${IyW<913_=Jt>w6KPsS=IFZ$h_)Q zhtC+<0*Y;6**~)_;|_SV{L9i#6UHs*lxm>X%R9Pw|G!bz6&m?lDJS2Ow4Sy&j zgj}Jdmm+wAjeDYPf8XH?cgwLj;c!(tgwKP->6ly-h9D3Wi2cP3Vws{?up^~LhvMLZ zKdpEjEEh_}`{1z@Cn*K(4Sz~2Q_YB%0AfLqC=mPe8N@0jd%$g%LZ$8?OT$rru#2i? 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⪙RT6uI?czxZ8J_jHUFG7Kh72*UYvi zybTp}fpE==w)3G=L?i3BCzT#3))E`KMyElitL2EF$Xi&XnA2FKJ@MlW)N325oll%1 zqyblp3psz9O{RUUYNq8@s({GZRC-iXxN%yHm(fm%mwWQvZ#+eagAqqhheM3@BRyfv zsmZcG<_Xu)y+ol2M^(CNxf&Xc#CD-GM4QFvYC10T@@$SP`!epvik@DE9Vx-mFr7~L z1Xu3Cgtl{him0VLw2TNfY|CCR&^L&t-1r+&cdac6gi=$a`xo2_U>*X&U&3+jd^I<`(qxfZN#@h+nNh>&4nKVgaQq2uv>Zo$0&?-_p?8k{)w@(pWPYeM1-qqrMoB0)^R2RTQGxqR~{&JgiV1}WvH9v>Y>-Kf_b8&M$+ z+u5oH4Yfh0A~gtF^rC(9=4sD^a>y|w1GY=dKBdyw5G8fWl|VveTF7PLzG7%T3g=UW zskr}+(>9dpG>MH0WPd7>T9+irT(iPUQ8;5JBUB2h6LqKozAhyg-TV8e2)U1NWw@7% zN|szJ_b7^*7Di${TM{7OZG!|GaPyZ%)d-*5I~}{-RCnb>Ud=p;z0pPjMpa; zc>O3nutp`6QWJ_^)tzf!a@t0-%a+HIlLWI6p2anRRnU5=RKn}R#8)EacsrsR6XtTKxgasru1po#@5$x9AWh&3{q1bfFmL+~P5vU!lco@bI& zsa@trdfij&igHxyIpw^2G+{tJwQ`oe^{O*Ot~%)rx;~+h>6p`XMitu1w?;)d26B($ zaih|zt7@t%5yak?o+5;y+vtM@c9hm*?1&n6TtrngtWzK)vmehR5G|m3Jfc*@-FKZK z1~%hQ%0v-k=}|9f92P9g7PW3!?v`ptPJJW|JN5R_#5VW;(Zj{PH`bbLjk(3h2m5|)1?UvN+Bw18+ z5|Qg9gysl#_j^v;V0(O{YGfLjQL!y#WWJ%Iq8aV;iD*ZGr>R6W&WL?=EHTct!6~Ag zBJ)$=2w8I+N3y!BS5Ohe%vai6L=X}pnVxV?ao`VR;pX?BB1-ML1kvqmF;y2+iVWrI znY4sBRmV&>23_F&2oeIt4Tyof^Xw@CY?ZVN6A7Oxla5!T*~p|()KH8biylq%GNuW4 zN*qm=rPyxw6hXzgINIVHWjKb}X2TaOJ=ygVWX$SVW<6&^saOGJ1Pc-FJUBzdkcvh7 z*@)~!kZ#tsCo&poOI=0Pv9we!GzchFN|dvRb6r106sA-h6N?SDTq)?0a@H$R@jMIn zm?X{?Ib6vOq#g$oVvoH3yQc_}iFutEk%dH7GP=G-mW5iZh3310*>-#BHp{}PZkrg0 z^6k^vxtpK#*g~{AXtnsV=X+w+;uC3QY?SMC9j0(q&&jaKcNiYp{?^kr@<_3jNb-mi z>(CtxkquTJdsNXed-1+RGz(0w>Gt`2YmmD6GpC3gDDmn~>6B_0cy5$z_0WXZO7SFB zRm}Lf#SUnlhf^^Kj@^zy*VOHUZkCEElGf5%y{QMTOY6m8t_o^FNURid`_)>5kU`b` zk($y3>a+=h0o9HDmZw;{KFFvUqKi;Qp~X{a1m)TmqP!-sm={v;UAD<#pJtU72?zqo(8*++r zbF>6h4{4|-W}6gUu4M6#(#N|LOnc)_3~l^3m8zX&B23cR6W1a?O> zWMWesrsNxEE-_9w23{)7lm`^kuM@5II4K!Oq3(3#cAstH4NXS|5K?Oj+qu&=;^-(< z@>p?LZIN^wCYW(Di?D|34#9`d*s~`UwKnbH8Y%CgX9$F%qvf8O0c2@9MP{HO$#xBd z$Y)3z_#U}mhAUYKT!wJtY#K!LBr_2vGKZ)6c1s`%Ly2p0ZW;Bb`k;(9vRuotnu3Rt z*FSmMMs!q&L*)*~7Aw7x$7a!Su{*(hx=lw$UcG)1E3-29U>1WCGMn4=Y@SxTmIW2BKN+T{UMFGk|d zM6xl=kBky>u?F4zl~V*{nUPi^!dl$~?AV>uxXaiUE{UeuZp&pcU)Br_GBvIbZT+-mDMtNqu)p;+bOrE`Km?eL?%9B%Xzcl<|SwMpS=DW zx^rMT7AS|&lozXv#-g!!{O;OOsEx8L*qIvPjWn%|#bUn(Rie$6SOZ4n}xH;P28l^FG^Mxh~^l@6I| zTDIF6W}9M)aUpKNH)7kT3k0i;R8$-c{Z3x)ljEk6s#jwrub(0Y@e+wQO` zJyO8*d=xJ9xvF8{GE&KFsZP~M3Z|(HaKqXA&QpZkjW>`XQg-Ql4`V&g%~xtwp&oC< zTWxvRrkdR(L7F7N(t95}LzrfRa6A2U9>rU6k#P%XFP&{nDaOGDjk=5t5?&OFPYbDQ z{~|d5fBjZ->;4Dt{pGz6-2IEY@4xeBcP8L#-oN#yw}$(FvhVGEcF(=}M>n0_Kiuu! z_|y&a`tM&iuKnIMZRZm^>h|w!%Uiz<;1~Zq_g%NIt15e7MZi~D9OPJc6l9pJ`JzY3 zY-&(V#G?K*DQcZziy*6`=(tB5R0%?#iUtdE$f5(G+4DiotyGFNP#;ug_0p-a2a5eR zQ*18jKUf=~@!WUanh^+*-5(|+e0@IfbKkWenm~x`N zi(j7#|J?J_&;&wcy>CKb5}H7W8U~vX7|#fVDBy6_ufPvY;EG?tC^Ug9eg(rBfe=Ek z`V|a96S(46;LQkxsCjX4#RU2@0wLmiuQoQ}x#!)`1VTguu#|_81~D|N3WLX2mM0G3hJ!$$Fca76yn+yO#Hzf47YoB&n`jgCV0}hGdMV#K zKrLnztPjI{Lc%Lym@jb{W+ENCFzQ?Ll$eI}QoWiEjsXcta$-$d<;Wv8g{Ny^Q@-Go zYb1J@KD_*0&&@K3^(v=n39FQ%G?@_&CCr$_!A&n&OBwi_hKr8btq?#(#Q?%qCx@J{U$`)(J z$@gL6PM=f#khQ2S6(Xr`zK}X>inSViY{;xg*tuNhUr{s>yLQM-(-| zXiF0d1Vu&r+%(NHaX+5y4o5Nauq!atQFW9{j*i^2naHZ(Zc^y5jc;_zdLrmLusm=9 z4jx$EqXxK#VDl1tS7v#!^poY!SJ+bUS4-A$THY@hX^YtnFOhZ3njEQp&v5J@RVX%_ zcl&B{;Piw4e^C{zCkF2a0RP7|$2Yj$JZR>!gFT)|BLKc~uu~k8BYaJ6I+FxDhP#{t z%Tl&P&`i7@FZon_AiD#FDt20zuQ}eV`5qiKq%=mDSVhi2XctRi*;J0@Ct173nb(@7=d{`*Z8s!un5INyOq=X`#o1ljI{75Sev(qZZ%Xwt8OE(fh*TG;17vNwo zT!0gzTI2dJ+NEV}kY2lZ0nTiAtXk*BQcn*5zXtiXA+|17t&1LTnyX11w$TMRv#y`T|KHyFzO8%Tb2oPTmv8C&J2$^~H*x)cyVlvc4*vRO{`n?l z_t|fjU%#{3k0VfO>9dQ*(XkjEkHlyeYSz+2GB^9P*UY7-3hu;8IO<~^)Teo-C*`U` zC1-V;wd#b-4J?&)d>SWwF>0A)P67AUqiGT+C?InxcER&l29~4MNj>;?9MafacG!x> z26P7^D;i#+X@@SfL-GI}egk7R^RlyJ?b(ODbOSsQsSF6RWTT*>HS zl(A%l4%(W~wOLz0cDy5100)Arw7YVgoG_CjA@vo6tJ2Wvvyu&-jmC8)U!6!55ZXAk z8j*3o9J3r~>PFmbF7Ek$GrDoY!>C1a&X zh9)&WheCNcCbT#K_E4RmV0{WgSxeKL;Rpj?NX)Dcgqq_l~f?iEmWLCerOEytc_ zEnk&i#^rI_CSleVyR@Ada`kwGG1(Pb#_A;(f?T+W$_~m=a<0WUd+}(zP(u@g3=Ysj zWY|g6TX1P{EFZ1Y@^VGGr^#c`SLK)UJ(m%B2xTjAu*Ws#qXR~PS9%ODFO)r}s&}FZ zJ{2>vKGWznN-4oeMHIFU2?mG<@46h>=%5Q@`QbV(FW1_7sg7kF_uxP!wi~9Z5Z=YYDM(edd#N`1s&8KtuGUJiO)Xo*5NVHirkwHAs^xJm4DW#f7 zwL(R!RRJV*^jiL+$9UU1EiYGGd#bd2Wqw&qGzo9o!X&(0p&+= zv0%94$llaO>jH(0Ra<~cR>`Py7Mh6byoR&T;#fX>aV8i$ZmWxYS zhT=e(2->nZ)|5z^@K<<@j9lZOtVWHz6w4EcO;ufys-*^Pb6ionX|Y%}S7;f>L4L0TyCq65HM<(b)mxfh zx4Pi(x!Nc_m{1lN;AW&j#}>8x)>}8?TX(jMt$V+8_j_)~Zf)=V^7WmYAKTS;e&vp~ zWo&=%m2Cig6MX%f<(;R<6u>6Hl`XfcY6awo;KZ=jO(lhnLWz*L>Lu|#htRk*crVp% zL5^1}!>nH4P$z`p(w>@f51;s@5C%c&Am}ohK4TnIhseRC38yOp3R?_z*RLVaA?9m0i^$ zBP&gcRo$iQ`cN?~ORie^a(&fq6P`q>`9`dzP4rIO(^AR=;U}?%q&d7W&Q{n0q>Cuj z6dbwFI@{5wP-R!Oc+RpQx3*BLnByK@%@$e&iqOcaMFz@tD^sq@q&T_|74oE!E~%rW z!y?fplg*UJla5fdQZCl4vp{9=)2Fhl+ijCFf*oh2Zow1s1(&utygFT}vOdMPQKq>DW&8;=c9 zwvjG3TDULLGLtn770t>h^cte%Gx}-ulj4>HXi{|LA^f?>F~8w0Cs#*KQu){My}L-TlVhH{JN< z8~%-FuK&XI{`IfA_H);|*S>P+r+4(7zp?$3+w%4uAo}Wm_!Iy74y)d9AFRE4;($yP zlpNL~jfM|lllaK z%^X#(Yh@-T#4{10ClEe=GMI_AHxSE_t&Gng(_T^SmsDxYydffD*N zvDSmNS9u&XC2Ii1CAXaKPaFqoXBbd6he24xNHuJ_sTE3YJFg+haSSNIoryI!5X-9> zx0@}4xHY3StY_mm#ucYgw_4&k(X?TXEldapoK6>V4u8^{iS2A4w$CMSuoaOU>San- ztKCtpZd7R26$Q01hIFz*h;^`uC$=LHf8xx<8XJh6#M~}QnU0)yM!9;PZp2*G){}8i zC9qFdz!7+P6v=v2!y&=AwP#}W4a6d3K1ws-bO98qE9Y#5jfs3a4o51g41==conDr6 zo6K~?6@l39Osuwn*rL-=U9X;II<=J1PBkmtjx7JK- zdjqj7!{IDm^u=0Q6p_I&pRjw1&oy#wu{M%gsmPFy4n@{943a-FXJXY2#Hwx*Q+OIuH0Icc@g9G4J1T9Z4VI%a-YL~R1Mt2M>}#VPrd_Dt;i zHV|u@pb*%gGOY}fT0P=rl7!k~spP1bi-@J1*mMgciWm_r-v=W>or!I1AXeeEX`UcO zJxvNCnQx~{Sj%MnRL=1-KF*l*ZC>^ME8pf~|VqiVuSGAy|K zf)irw1euI2%SB5j)H0L9bb|pAV<+-V?6Vt)#bZ+)Mbbpep7aUTsZp-aw4DZ>z|ul9 zmMo48J8s31a(l?~C#{*-uU}sv=O98eCY{Xom1?6q#W+7_4)~@-b_c$TBodfVZ}2s! z?&q-#cxBC**!ONAwmiTywMmNW^Km9g7SbTht<{Q(A~{5|g|=?D=d| z#7Y~8o$@_w$P{PP(zxQN0<>;+9nlaB(5qXkvd@e4KuOz4a9~hd&HkSI}=;mKx~K# zN&Lyz&%{>O*Jd~SHkTzIP^8 z*g&jW7dj*x$HpF#1zR^!q0y|@E7P*Z6Gmw=N@wL^tCf!_elQZGnOJ@Uu^2Ogs)Ohx zTHZLD{UXB!n;tNfpNtKL>%4z|X`o8;A{2&4fRx%*4_ghz(JBg+JkDVvFljat`XXuI84TplG`Q zA>;NyU{rxI$v)&aY6ub+*hG4w_nb<;50t>p#8MlGwR0RCOJztt9g88kW*1}e9&K94 z$fmh?soC*HMHJ}`pa=uRmSBJ6Pw>aY>1jB{0Ti1o7+Hah~#Vjq&O3s-9T)JBuoB; znu#Sg5E~+c>@@%XlUsK_viDavyzNh3{m1$%5}yJMfRwAk3n^D29L4&8*k7Yr~v=oAPw}A>5oYWj$fE^HDwRevBDr;!4W1Juc)!9 zT%-n2f10j?-4ssu1PqOPG0*a$OXvDxX}w@4wehHop_2>+k{LS_qHCpK1|6}8fHLYN zquIqWB{0RDX@MR3)>M?5_0+fwCrt$IUU;_Vwg5NK>@2iGsD z3eeH}-_X=lP>vb=5srg9dt-27*7ei$NAd+*3^AU*U|Wn|Xp1YaKYHqIF~kz$>9fTr zZ4L~e#|Em7zzyZkv{Z0ixjGx^k8_iQd%~{F787YOGJ(LAsmO9k7~s(Y4Xy%dQN>~$ z-0yYh2#B685q4I?4>h^SC61KrD0lhmk38FgtFEOq{iA|IgPrJ}XL(t&!|{cwJ=Rls zcbrZo3dP!}ZFu=!+01dcE~}P^@VwLJ!QP@=Ol7z>*E5IxX%iujG!fhE;<2FXfi2!R z$p?A&-}mxzg^wokfL1GtrZatA=tm229zBHm*>cqK zu|yH|JGLZkn1>v69T?(=1}C{$>6sz8^MTm-LWZj|#OPm3Lk#07f>qm6@s1bn+4X{@ z!OLKPFAxZprKk7OpPTPufYy`q-E1M+FxA;&Hei0q#b_d#j9plZgJzN@qd)^TCgo9w zL;P{sAS7AhX%8v5m?7zh5}FYE27dWk9NCo7)Ik8;A;?l2j*ufhN}1D1j4@hAg<|Cp z7OUBe)S?D9mM;(LQSESQ3WYY$r~541*L#Jo0&cv))wTiWW4)okMwKCsZ?qVlbsgAU z@csW_>le4~fBXHfx%YSPMehFWUG~n;+(B;t5;*t&SD+ripYDI>{`TH??H%6yiJO(( zpWCh9_`@6Dc;nXf_h0+ewGZulZs(&rU%ma`ZNCSIc)5T2CtJHa>|5WeUjK{rXV=|_ z!DxTjw2Y>wdX^(ME#2`3Z%?Hl=m6w$%A;{q8kVb5PpY*?c1d+X&_upggzO9nPZ5QL zJY8wc~#*IEX>m2CLFh2#`bZRFtq(sw=GhJ{8By5aESe+uk#V>_~ z93O(beMQzDJqKDm0xka8#x1^JW?qEh+}Mkc>*Zn`2O(lR?o&FE&579STMl9FZI0zAg0qC^P==}B-(OE7=aS$SQ z!+E3vHi2pV`;FPG@XH~>I0vyQK}&&Y{kx4@I%irT!au-k zZJoW=|7+vMUfi^nt6zk<+DCrW4A_9K|K`SQ&P*#r6p8cbeV~lbdlW&dyO{rVn!^LY0+cHe&Ezq-e?j zcYbH*J=-7K`WygU_;dW=#Jaz=t-h7r+S=dSzH#H(y<1^Ix?gY@o(GQ5)~}#$ZFj%z ze}3H`&VGOPqK~uX_1y6@CuTtMEpw9lVZL2nAlYrH{q-F819o49&3^ylMI4vErGO$k zON!h(2oY`m0>=#%xfjrT^>@#Hf9@iZD->xgDe~q)h_9{}NN%Rcn*qD8{Lb0$&uq-` zZ6|ub@!j*6y?YSiJME0)lU{oLi{A~H{=e*f378{Sb#8a7N-ec|@q%G6V8&)_41<^| zNhPU_m(gC@O8ZjsqLHe!mrA9*B-_}A2Bv#h3}G?5Apr~+;_ObyViIBu0iIt1VORnV zA;ck&Ojr#Bc(<<9?io$bsLBk%e2?b)`foh<-1Fai&Z)Xpb?&(rJ}Ccw3a)~0Wri<$ zK*dwuFTfX-nXZ3F{(b(niuoScF3Wrm?anW9K*h7-l<)36Y5u$-GQ-tX`S)5>G0!Wu zOEORME>Fz?70;Pdo+sbY<}=mE9QEhJ-#=B%bpJLdGd=0@w4~+{bjoycJDksPNoJ@0 zGx_(jUopo^w%LsZ{gk6#zRn*|@%%caIJrm8rzmsNyj1>;tSDx9>2^_`#gpztS~c4q z9-qa@1$6F~3OU#x{K+Te-~V&vnMlq)9NzJQ?Sf45q>B@JK*dw}l;j>4r@4IPV1Mw3 zhvnZt`bWil4{RTi`JNK&DxU49d`}AYxjf}yfAIT@@b_OS=6S_-Ugmj9u&a14Fy(nl zu+L>G2m6EHd4~M^yJ^Kt_ix`MGd(5PRlG8I+)d#vbS_6Z*dIJ0-|Qbeag$<>mu%lS z8|*6HH%uu`gMBVVncKH(@b_ORW_aoL4e~6W66`8=wmd$IlVG2FtTYVvZ@gY6`Hi2S ziRA3VJP7vdWs;`^yNb6cQ<8fG`&_;-*#G$jGT(o>LNVV1+c}xtq z^U~w9I0<&v*GPNC8aHw}rIgg|j(s!ri{)p`2Oqv2{(i-oNY1`q0MBo!8It+~Dz-wY zB`H5-KKPk0z~BF(l;jF{j`u9RVqxPm8*kgF>+lWxhIak4>+fC{*RNe)UHjtNJ!^wC z_u2)kk1YRu^>IA$?+tOa6HK1QW--q^57kWPORpdj+&4?d)n&w|Mf1tTT z6VW_t=|7hK6j1ri&;NfjfNM%i2SkUX-EX!nM6@0eh?1>Pw6*$TlM~wG0uy(%7&=ff zxwv4X5Yv8nDuFj`Q6XI%**$T|Y)tYa&T8YicGbcMeZ`gyH#1?8sP%#l8~64vDqBAwCbIT7)!T?Zezff@K)n^ z0Pm2Qg5Tv?DNH4dl)aka`9eD0COG?WkfYs1wi5UD8_iHnAbYk%*k4OaLU?iiM1r6r zSkC9RR(%yZC^ZAa^ayNU7i`5Y={IJ_?WoDoabuLBkcZ9M8FZkwH@|n4Ecc*6cR2Ox0&>gvJEez+IAjcF|#U=7-MM%87{t zCb(E3On3P{VFp{m*<6`R`Tgc*-sNcfd>#)K2}jGsh!}dbH%=ss)pj}DuG?eg9M%=P zoL?ury;%+-3p9votV;B_04|oS z=-xQdQSW4ZmO!R$w>Gdb(~H`Y{;;t>bCBv7ix3WOf?XxXs(_})XH%)fer0W zf=)S|e0x|nCL)e%y2fyohzAS0i~(kZi;c{1`S3)d5+R&?oniXq;c^-Co|KefC76#E}KE?W||wu1bT?r%L#MEGMxMp3CFqKGiG?##L84 z7&4|-@100+In$VwQmJA(k|nT2gDbhQa*r4e_<)xUN4VQQG{>aa`fDc=j&OcRdFf%+;-+E=zt>)M zr*eI^m@o}nCM?d@q_V>kWSY2V{Y6kh=vNbcTY_^_lHDRSMmShDak;36qymF6(UOP` zF_3&sD<6%mlM@|G+vRpMc>0dc28qIZkrf!b|RnMB2&+vUtcC%h${n)+6@E zo=Pc@3yh<9z#UE2Oz9-sWc`t3Y-Ko+NV%;p|Ih-qkC*vSAldRnGeujnWhWyEPtu+@ z@r8We-EX+vmBsTX60vI6MKwhx>LIeYHAa{cK_5v+)0{2SAB}P`8_Ad=HNiGyb<=A} zEZ=Fn9r3i=Pv&!+v0;lEc_!!X)<>k*m&y`#p7S;XW248v9G&QhB&;Q;#qa1DL7AGY zvtGyrDx-19mCiCFOmI6emS_FWMsUPx%o7Qk@w&kLIx0k=8Zf!=id&?FcrD3f`mSP= zm0Sen9MS2z$f6q)i4dM>(g}?1`|2fUI^BY|3XFeI|r+JyWyS(0*hh z5v;bY9^Br}Go%H}S$p1;T=PGg^Cu;f*HmcP2KKP2lH!9N2 zPGVdb*Rh(|77@0XI|G4AjeUU;ZX+5i&Zz{&`+K$OP@t(+r^VK~#c>3b@k?`#f@H@$ z&3d=ylUUm*=rO!$BHnV&g?M!D zpGssxOvL8&2+@Yw*WpYpE0-f;Jrive@PVIb4Z^`FxIBcaF++7KLGs?J37iR(3Ud8g ztWah;qo_6QPRAlycPiA57h<(uF<}kL0Yi2;PBu~;bsI@KpD4eIvJs_HCHnFrVQx)q;DMX}n z)2Q_oG9IBe@Qky*4BZ%kbEi6_5sOAsjgrUNcSh@_+{hXAH#@9m>WMzuG)|d7{h49X zZj}6DACGn$t&-R-yT>4PU$hp}VW~1mCcB0=Pgr?HOWRjSCixV_1soDDT5a`8znblu zId_%s^K>dS$oB(8*|GGtiG+s&?}ei6j6Fm*7`%+da{VUO&3j61h76WkzT|+%gle?b zt}K0JBH8LH=#(hLC)Q@;vZK6|2 zID4GEjXSc5QA6wvtB!0wi`6jAb0!k@FkK2&trVBa2g4DMl@3@2J#PaG^@o)%$@_#5 z6{-f?ZC69ro=Dj0sRpQKUG}z|v@P3@xypH4+XxnM11jJi^@IIp&E&K=@n~xChN(o} zOcu!|%@&#gS4$)^8DFbe^o>Cg9*Zx_lv~D}C&%Y;(XS0mB&=<&C5;neuiVeodX)-0 zzy}yrSYEi#CN~Sq~-Pa12Z9%5iXRCR(v3mPN z0!yZu9uaO+{YV=u4}8H?w3&<4Mmdi$lQ3IESCHVkcwy{{El+PzSlAZGu)R=tV9OT< z4A*l6+H|7An~kxE(PZ~JlZ^;CV-0u4R)Z5A#(r)T&h^-gv6zaCD`KY;9_FNhU<$iN zy(llTF3L5EVC>kBym%_%3`@;%hhnSY6lL<|?5&a~!BD{w7LU+TM=21`l*cWeZdbIE zkqhIVlXRGeJZm?`)5Z#Bj8((s>Nr+Tm+9n)OESiYCnL0^2D3E14Gv91CT#(0VG9>a zfu&?`z)MXrTQGVeRgvyB`qgoVPOuFJW|%CXLDUMDWyxqfbSS>M!wYR1RaG) zwJb!2ZBXYh&|X;n)~ig7U-4)u5`8&h3&I1|70n)CG@~PUbq{)-vy@> z!@rcinUi;pV;J04F3aY*Qn}FQ<=$jt!5zYr4yW%4g+f!_oKMLAI{iG1f_zjn{IgMG8#{k@N?d`JM+g6< zCJbt@)&T5$XZN$bSL>Iz8_Z@go%v+uKrO(>dd0M->+M?(W)2Ytdtglw>}9iIwpMaQ z>sZlTIoJzl4nFZ?$OXn%24zDdE;beLI!z+(@;1EYc%_(Ym8zlGuu;v>Otljvi`7s9 zA5p=$%Yzfqq%Fa)hel;@KVduMHnoTTusMnkf;mq)W_K#u4F-oU@4_5xhv6>F5!en5 z$`gAU30tAm9?|_Y35o&^`jow%GpEUBp6P-*e^S^IsiiW*b9cGf^BiV1mm7JLBXp6+ z1LS&j*Q-5tNlwxzenl6KiSll>CW6?wO#YH46X9xIkuQvB?xra1vZGOnKan zgNhSi9fiqFCxM#pcX-f&(C2yilCl#~rk&|JrMAZ@`zsY+uRvE%a37}4Y_rLyuJ2ih zfMYYkL9QPUfTKC0k;*XQ8;b`)amGWoDBdlK%|g{2!6V^T!|}xTVFsLs4*BY#in-zs zj+49-PmC+2k;|L)7KB2gSLIR%&51GLr-LC=u4?s_d@R=JbCj=Fbl|Dx*c`_k=2*){ z71?CF<;_&mU6)H)}R^3Z{^ zzhZeiIH`a$TvSsHZ+=enDa%@L5+9i7ldDnLWH;S&oohYa{VzM9W_La4_9TwWCgt%_n-_!EA7~xo0Snt`=6W z3Bx$+^#{Q_ic;M}2uX{Wz*4DTnrXD^j)K?ZZbsZsyckumBnGXOexc&>Rp~@u>KE!c zd(X=S{Ka8E;3y6|g>tG!_&a=Qz{I@KVtCMMb;*V=6#%at@Qgn}6)YI;DIBs{g5U+3 zp9+*5iVEP^UeEryZV10e<@7G?tmRfuI>haaPR&Q9{ zZ}_U=od(wM+?8*vymzI(@;MR6hFL~l$9^) z7SF7Fc4N@4a*bx6-$`yJ=Bl6VVn_Y8oAJ40pMA*ORgZ4C$j#VX<+B~^C?_|g^JKBv zQ%!edui1=Di0kxcG8#6!H-4A6CVlNDJ#XOH?AC}~c{uR!T=KI7cF2>Pp}DfN%6DY1 z-3-npKD%9FhInAE^4Sh{lpo&o%gWE@t=Q~Y_q)oMmNsmENp4be)z5aZtKK(P{cNW5 z$?Df$cpd$aUxte)d9hvU)iBYv-z;?P6E`HFMOTeggB!>Nf~2 zxq0;*^{02StNulE)z4mSPFDX+?sv$|tLCbo?P6CwIamGc1?yz>8@BI}oA$ZtXS>){ zZ=0)r_L6n7dT4v=T=lbE?5Zc`s-L}Nou=M0SN&`kyXwtz)z4nCPEl|CIk{@iVS5Oyun4YL~dY#(2TpfzMuVcIEZYV9CvcbIH#V*db4DK7X$4 ztnwY%Yc`)ZZ{YUXh1o7~dEi&h8~AC3i+1Iq#h*Km{AmPs$dj8_%#}UUb?%zY%jXV! z_9C@QTyxj8n+Kr0{)~?jCj0DFW>;SStSxf$IdjR+64)L1Wpia`mG8)2yLstci_Kmj zc8O~*Aj!@BbCsXz0!MB>d#>!P@?GMW%q2d1ZI~hcthvf(JJ?Zv&E_-by36dvWmmZ- zea+@G=E~2;_sR0tZeBch@Ut<#E3e!40&?@|a|b^=fStjUoBQU<&XV7eT`@ei;9vOK z0&*>Kq2^J|Uua&Z0g0WO^OwG{aoa|6<9X{pTmOfpk1XAZehd95`bM;Y9!4+Hy=~(I zy0*@%d&csKjn6H7ZE|TDl_CK|MqrFSp)H=0KGrZd{G=vP7 zuKZx-?^f8}J`6lvL$m@~X z>S&c-eU9O=37?J6ZG1pxG~u}P4Vja&FZa(!VoOVxa_%-Aq`e@A#pM_Zqk<&P+$gnQ z(*96s(=Tfun>I}esgO71>s1PAZb-x2p(x$mJVbYZCW<1$)+LM;JL5&_%9NbK=I8A5KzgLs) z2Iaj@O)$S#OqWyIt5KIx+Vlmw^rR^_$O+{@OK>+NzSD7MQ;iuD7(Q(Hs?w$(GJHj8 z(|Zm7q_pV=4PTx%o$G{!wZ8JEJ*7?aYh9&HZ(8dpZF=KcTWQl9*2HPk`Y;5l%%+$~ zuv>}sI*rkcwRG>$eNk!Cx9h$zZJMb>l6}^hjyZ$5dRgG;nO-&T(7an|)3P5oHm_%H~fP3`%0T0 z*M3iF)6Z+atF-AqYQHmWIyVf9YlCUizAM--48qPtIbKY9qg{5!vlgGb_*|t;uUNc7 zY17LWFIU?1z~TX=O`o&)9HmV!Tf9tZ(@PgGRoZm_;{It0^t-=*8R z8O?2aw{GWVG`HzHbzhotox3J{X4Q>T@!{M~)+?)ItwLg&e z|C0+ZThM<2JcEaH-`3ry8|kdeKUsd;a(?xqg_o^7)9}ZJ(ejnrf7O0K8w76weiwe# z|1t7b$MzEbUwT+~QjnU$pSEg@-^k!Amx%_5WCZ^?H2$8EePa z-oAG2+T!XbS6{IjGF-g!Pb+U)0au{Mf1D_&dAM|`m>h8KUZc_)bp4zI?ChI;>YJuD zpPssGcmsig;*5)ziaa}TQFO8HYS7`ZBqV2_FQ#2glGfI*MnN@8ptuwW#{<2rbKtMJ zdOdLH^Ng?Tkx#9=_a>n7M&-h1t4&E*XJ2gTK6n)hYI-W~ez(So?n68Csc=}Q&!_Ig zR(U>^YoE-g?gKj$%JoadtdJZ@u}oU>_+7JaTlSjJTXrT?q3~HJ)H3^4TzA_!?SA&X zweIE}dnsgplD%%-v6n*8J?!=39eXM4VrsAPj=c)*CM|U%Ie&2E5Q|c_s~`YgzcZl< zh0kg)V)k9S?oH>k``Op+y1RDlrI7te_IksPy%dV>VXrst*h^s-Q+vH`$6l#~*CF(W zseYI**8GAyKHpwi^Uj1S6h521%+}e@09xxg?SA%ps5R}_OCkGRduc5@_EIQ%y1lf- zj=dCiF|n5x-?3L9!CetlRzzB8c;yWgGA ztH7 zkxGO@nYvqIeYIjL$0g@Gi{?+K&Z1QKY|dgb&t5Gx_wP8e!pS{(E1%)Fb{tuu@Y%T8 zPFs2!UbhpQ3cH`grs1_ye^EMPyZ)m2v#GZ#6+PWsH6NLhR@%jcwB}ECe8ewgV$mKQ zD?7cd^f=Xv&i4`hi+6lPq43%4U^UGyQS`U&OsK-{cPCVT+s=e46g_=H^|$X#sKPEL z6RN-EMe;M1@+YyA9s9f+z!mg*<+5j+PkeSk`IK|n`K+xrNcm|^IrCjpo$rtzew9m} zZmA1Y&UmF?0;QD-EDLsjTqy7Vzigqt0IL5P*59>Gu6<(7zWTmZm*Mvf)XF0(;>xQ2 zX8rlP_v^^z&n>&PU(|xLjnFo_hTMW&pm~qxnx#)KI;hmfv;~e0;0Z4uS40k25;OE&7OsMZt>O98NVf{YDJHEa}38zp3j&*}Tql+B!?Nbm8c&G9NH>HY6r<6jsC?Cb}L0MZDrpP6Bm1?6|j%St4{dFk<># zgsIgViup_e4-ER2>7dOczDSjxov7F@}u2FjK}-Hg8_C2aMiG-Nw2c>Bvy= zX>(^C^l2Gj@}R||Hv>M;d)|VY;}jM1+4GLyJURv{uLJX};ygvgDo>BR($NvncL?b7 zTLYZWRrIh#Ma+0IwQ;sr98+dvmzEk-mSL=Y!kHz@8L`t)tZ(-kQ9mjHJtWYh;y6Xc zdS(`1m7_zTOpa3(=P4>ycJk#OxaH z1&WWf+skd5K%eY3Dh?Y^tnXyE;ej66ZB!f>WnZSz?@T8Y<_S#iUvA&Z7>j=;Tm!tKjv0hCG)}a&#v8G@*Js_Md zXK|czW|CAj;)`(hkYpVCu&!b~Gl7+#n)IC3K2qdkv#;kTfpwG22bfev1ti5op9I#V z{$Frm^{DRW=;e#Svp&NA<{!^C1FuML$K?_}uOb)KoL@TMV5bx{F=HlB0%NZlCRX!a zHB3}g4V0^IsBtyeUd8dDd)M|;arxZ4hKYO<%4`Xfb5zxD_R24-Ik~FedKx)Qp(+kkj;qcY}{>%R@0?G36zue^ZwMx z((;w7wL+Y+6(v6IBt1{KUXi2A`#IMkcRAw949D?;9WSRshw4qq!!*Wi8(DGH`fY33 z?J!|My4f#UN_Af_7Lpo7J8We{E8a+pamHzlB-~{o>$G&~m{Va{JbCDHy`uDXY$vi+ zxU1^OmJ3hq8QG>^%g9!-5cyw^?AayMxr}UlZe&}Zcx3NI{E0@kTzBtGk=@I#t+h+nuqSCrDiS8`Gl^g znn{g!tZUqxiASl;e^&sKZW?*W_12A?j4Pk)+_BL>#;t8sF> z`BzR^;1{pTy9W`Vy<07SYP8U+cj{ykK;4{MvCQa+2BOuBow5EUIQwEaGR1nqK*l_% zAF?!K;oi_|H%HTHdo^jvrOSnsovc*K*(Y8SkPXm+HzcJ5*TEt&IMhth&T2d;M$*-i zrEn?v_)`Q^r)g|x^urT1LgIvqobikJA1BU!^NQ92pvalsA|?~lxbM9C2mljc+{Quy*C@_f{WRedTI)^?>1o;m-|64N1fPm2a)wzp}X!U3sSdQT-q4 zZ`X(P`*dFiy9aL3QM#urKeBx9vIHstoTvS=_8x6ddsw@Q9!Gx%7166v9r9V^ZiGkd z2%>pdbC2eBO;&?xmY2S=^ns-Ts0_Hi_{GKdE_N3Ei~ART2&x0VXW@!t&(K7c4&VeR zYiz-77AtORb$#yHU1;cYuQ+z`OhapQz*41|oB{NTD~>(=l!gROWV;=@INR{@D~|0u zrJ)74G}H2yQ?DWnX?r1Jsh1fX z>t=I_f|-bj(NG25j~{!AER&AJY@txMKyh_{+0+h7wRq1@jV(nonX?vxY>t%*g(g=l z7Q5)P@naXtGWL#TOpF}CiXe{rHhVXpi?_i^Hc^I7nVFoi=e89ZygSv&wfyKM_^}IQ znM^aulwGtK?C{NE+1ea+%lSc*B?K4EXIUcaA15P4aQ;?4(?XwxA3I-`DZ2)l0hOs( zIwap4cuC_(%o9nrOB4lH&0QwqeAd$}1Zz~=jy?lFcAhN5h`upbB~310z*M&9SZ2@| zWPSOFkYR(an7zjO{LXGbq(W9d`jjhjMGuy*XP<~>z-P0~VW;mst$>#B-!lw~@>gC5) zPHBf*3t*9an!jFzAJfaTWG+k2dZ@E~Xd0)@!Xn zU7D>XYeIUQc0`BeWIw`+iDYMths=DFv!LhU$52_uW$9a;(W0diXt?4+Kxhvf*<24B zkIchSyytZJXp1*&bz38n1srg743TBZ*?=SOiDq3Xb0d}_h@6yZ1nJ6{k)$}$ELMw7 zv(Rx>itd6PUB{1UWSL0MmlHeou8A|{%`rSv%r{IaCPBNT8W}Rx>O;Y0G_{6pit(Up z_^~Bf#$PY_212mHhN_u@)kjxSBHrceNsG!jOUwHfVU)PNsbkYyOb<@4JEK3>9TcW-QM zlDP!yXW?H_ z$s4zMQ>YH#zEzfSWtzd3IaRfc>Rg`^gUPX@=Qa)b0cZ53ja|{`t)xL#Ww07+q09L8 zEwW6|nW$!otZ&?KxybfNDu!xJv&r7$1TyCza^o=DYqYZg%0F_TT73IvS;ii*7AU)K zSg01#Wd~!*V`hHXW+aQrIPg%uLJSY|8#O!AYo|~Y-yX{{Nj3$twN0Xnv&4<#Oukx8 zgutO_PMQ>A;A|_>##gh|Krfxo3}u;-EMs<5a5Hbo^4S6(8_}*Ho9AuWT)pV##KN!; z!0=%rZpo+Ud>BRWZAq4?dswQ~?lw)e0_jh*`A~llv-@nKvrP6lmbN=aak_%lJ$MI` zWrng$BAaEwvbh*8P<$fYsn{a*Zoexub6K+>7Si5$Q)Wg-Coi|@!-;*937{-BUSTdf);cU-|j&@%G31F1$?_J%c#h=Lw=5LcVrnAIdRC( z@a?uNqawu(`6<3F$};_UmK{i@TtAu~cXOHYK!}vuguhBD)PFJAK}})ETbYl3waFRZpbn!@~n^_;@fpu zMnx_Z@&kOkCd(9Zd=DdX?I_h5wOUx&NlTp=ZH}31_Ph`-R05H9nW~s=l4Xc|AK$LZ zGAh!cWSNRA(}`Q%*>J|zuSSE_Qo1?FWNV3Vtj(lKc?q2Pm+&yzLCV9FjTrJhe7h{m zj4bV@BiLwxXdU>nR5)*GmxQW~s7I2WaH8v|Mh8Z3H<)kv8?sDEmQj%!g?ty^=46>r zD_?4Nvm{;5*lT5bzS6+jwXlHGc$;xH;~l)+@s$U7up4V3-@&(8Sw=-l6!PErc2Sm5 zk=}%yz_$ysjEX!ZV+g1G> zI8P=$_V|cY8&5-cs; zmWi60E^i`TtOYanQOUzunPSQoFoi4KY9*R+m7}&|%-^NDS!sy;3%(tfWlGsp-5(=d zQrGOQMT4ePzL3o(#?eaBQ;UoSq?hd%iJljk#jg#4s3>Wumf-io_LJCL+tI_}YYg4d13^85N&hkbjoH|LG`+=GjV|B|9R34I5wiT;<{EcZ>%|f%eMFCZ$ ztV&d@YGz}Annh}T%LnwqxNRxccS_uW&R>|ryz2}8~E6}NSbOh>+2u!`wSuNEpE$;7Rzff{&! zD=5y{-^0OGJe^gyUIbJ^4?3b)Xm&q_nkP+e%P!NAJ?KWobfzAp;^9=>vH>;FgRWPczQ2bDsd!GUZdrj!=s`Kf zDo^qt6_2;|Edr>59>geCb&3b6crFGWWRXoKdr(%fwzGTC?A8o5&)nRWS*9a6HKXpS<;{0r;tD47bzz5&q6F*0MX7lsZJi?1x7@z|qET)*w9ucPE z4S-z#FSwXn*!T>n08rnsZ)n#)yZ-KVasAr$)wM6K-2>hSxYsUNePs2+tGBEMR`(gc zVfaJCOAOJ)S1#rZmw+sQKU+Mp_?Ih3SJEp7^xs+f=F;&p&|u=m7$0&v>L)FDNi763Re0<9(cCj;4$^C@ zxWZ$J^)Ten0kSmQ`Gg~HS$N4rhYE>1R7>1Gk;o^S`4K4va&*6xum>}pK)BpZR!j+F zlpS(hiLG{1rT~}p`_Y%GmY9x1g^uYsGPtK3whEo3J)Fo3Ts6)Yv+;_(#}?axG!vqu z2@B>hTI?n}a`}Xn3W)=%C7v^ph$fh<J9%qJ2);LD*RW0FBE#aO> zM3Si(#!7g+m$&;wi?LRA&`!2w6k4@{KN}tAbJdcK5yuSnl<%mP_;1w`Cngf!IA6LVq3Mu)#YrZ*=uo@|QHN*3Tbi7S)_TeB$yNPTi?(H|4p_)MUY>BYHmX?I@;`b*K z1RcS0KDV{%tI$EI85pKVfk8vC6}zP0m>svHCP&AOQGP$RVp1)EtCqkf5|%;C7aylA z6e*SXmZg*-Iz__6)w5iK;&5DIyj_g0i^Fyd2C)eP6&Z_aJ|HV$5S=hkk^iXXld=*9kqHA8*_3LsD3vgv zCk#}4y;k#OSqX#ign^1oPc<2yN*IJD3{>QRs`<=3{~OtDTfqqf6&a;!GD(#%2uv8L z$WK-C`CJJD|Ac{xkKk&uT$M1OCJcT_pTw0g@J$$~`0A}D16K(H?}UMhkKtn03TWO1v>-d4ik+6e;{`Q2*ry61npy>0872?G^h z;nie`D`9Z;gn^2W^J;R=l`weGgn^1L_G-SSD`9Zegn^2@cs2R(N*It+KTw`AuO?G| z{^#r4w(JuID)R5uA>x6-dy#Zfv?S*WnzKdMWW6u(5w$tmo$OTbgQJ>PFB=qFERoCxP zBW052G+Q5~nxRIH#nlxFECyqY1-F9K%d`+>V118Ag-+w0`9hT|32$Aj^+bcSJ2N)k z7VF}{mNo8>iotTa?)Mgkbx>lgl5=p0s0EaH8b_#ugtK64+7CH%j;6U?^k)w_!zO#G z8zeKRm`xR(v@O&XpY^ssAfyr{LA%>x65V4dXkWzRL#N( z40>G;_(wC}>Tsn_O{f4j!NH!v)7}5F18S-V0ZpoFgZ{#G##Af~_O2kL5s#@wds4-$ zjkFDe<{aO!_7cTWI#i&2t_C=eC{zwt9A*XwyDTG#C|ht10`ahv^o7B{#PCB0%W;AO zC8ou;rJAKn4n?)a;Lzo8T-i=e!tvI{bGwE;dpKIiGlnDaYZ;Cz*2%vn;RuWHo;@7N za~X{`k~nk8O+K0ohwa><(ZO~+*A7I&Ii`}N`GgdSRx;r%RTdk)c84M>9P@-Lj!AB( zbATmV%^}zBMN2}V8*S#Ao`|KCJyhq){)*Exl*nG3Y>yp`7vpk4zAl<;1-lRHk6Dsy zRfnS#&T|K|<7m85NhON|mrGGJ!l6&1G4Xgbs_>{^^yX->pD`LOzn0Oc;+gT+BpPAi z+_OicT#5Re-5YIo(sah9Diibj5}8D*+qWefqSR}Q?8Ua5AbizKMR4{H@f~Z^LsT5` zgN-K~jbZ0#Wa>AW0l`&UO{r6C4=R=Hz&{X7M64hbOz{pS)PmV)yy)s=64qe7*=+O= zrfi9-&r4YjrpuzEUrb9*Z#3m}CUSTqDkxj3!l6&=|E(<87uNaJ;|90>KHW35N0IMo zjx2r({KvEWy!5=6T&BBmlfCrW9`p`~2gnA>;bcXk$Btnr=_kTTv0F6J)uOLDV9haW z#9bMN9Z-h}>Y(BT>q}Ll31^it(L8Go%eqG44Aces26^WRnM1>x<-NfiM(4P+H<2BQ zJ~|VFZF2%>Ya*%~l^%h%7H7Wc4kpNu81xdBz9W_=qxqtH$b?ziTdS4az9OhbM3f7n zqZNs|6H&_Njt6m*?S&ZlA091r`27d>)1}^3S2?;PfxYS~Dw&KP!d_^xEH+>D@tbT^wAN_$0-si*d`3FEPF79m$`4Xu%(t!yH3f~yI*3C{*o91|maQIE6J zAv#@t*viCHeh1^rI%vfFG!%@=H7^lbV z9yW1}1LbC~=(FRQPSZ4~HRFLAYverXxZC3vdbwuA(=t{GGHmJ$Th3r2;tIHu-lSri z95@baa>+cK%oZnjGTDS4pQCFRkpdwP`^HkJ4*>7A0OD$jF`s+GuXG3&c(dhWX_vbY?F&m0Gm8(o=s+VzCWpK zQa?vFafSOSykX7o@j6X~$B~R>oN-z9ijyYpV!}l+P;z?we4;&|Vj(`+q*)fEjbfz3 z*ld+Uk%&v^nue`}Hx-CylQB1=*e06GfK8q`&nCpotLP_{P0rQ*gz{K(jbXc#O=n`w z>Hwr_mqNm1jW)CHvL|{@?=YF})VpcB*=x5sX>7oDI+CYBng+fgZ?nX)1n&*Rhpl*! zaR(h#(BV*OlLvqLIbf4#%(Kbt{{APGP0rQ*#9Of>#$G1Zs3y{5zFVvZ!~(z9{lp2* z^!5UaP@JO|F*}CCRF&ppgS@pWimp!A<7G&^?=QtY7&C17-C18S2a203UJF0??F)fT zE}mzT*^eYoCYyxM)&0a*#bc(%FyrafxJ)&ktvfwja<8?p!%x-3-3S9Cwr)tCQid8GZ8_9fbj&<~;4AfH68(R@J@TKd{je`#&;?TgO`ot*2RJM>#= zcp~G1nj+4MOXZ!lp2lf!FT|}B;3A)AucFYiVlI2uuWN24flB#IPZf2d6{|eGa71Y< z0rbgdda9@~tyte~U6%S*9O#kHvsY1(H{*=!QLO6h?mGKor?3@~ zxqxA-sIRS<%OoiR449q{SVckZ`CAAS6@DRSR=MI<7;ur}Q^h76#azx1pCO=9j!zZ4 zZWOEBD?WojpB$em_Sh)acXE6NfF3zMRqUiutmm}&^aEw^fZy7P@(o6uhw9Z#p=veY zb~tR+kr{9Jg3)XxN?THd)0h%do}pr8Gx13QRdRf)*d3!-)mh`yKKnwcu;r7vfH_uC z@=-CD$K%sG9k7b6F!K}Y6~*W5+Zu4O@Jz|5*pu>0BGcS*11@sps@R>Pn9J#r+uU+Z z^{Lp4@(buIZ8?EHcse&7cgNhJA(J5)#@y>uRzc*3@w~6hWCppS-PjMC`b?}*tN9h{ z+r3g$wj4l@9K0&_mnhaV6TF9kDmi#nY$Q>v>g>Tg`l&a>b{Q30L=@{g*=4QUk!&X5GAed_ zC{}fLmzjOxQ`ovn<^qPTqU54tE;}wm0v)o;sMy^xf3LtfD=*A-($2T6{cxdB+jr%w5+qh@rZg48#of}6tZr_kL#Et64k&Wy|bc5PB3@Qa; z8&_`Z-`KZt-iB^tVg0f76YGzzKeB#&{b6u|-~;RTuiv+R&-&f#cdg&KesulzbqUlI ztb%g|v+L1yYW?uKeH~lBa((~$zV-9gb?XbD^1z9;N7o)%JHGbt+Cys(tlhtMAE-Wf z_u5_H48o&px35WS;#zg>$Xa$Sx<;)XUbC-ZYgexAU)#5K-kNT0VfC@q6RVGcI)%qq zA2wiyD-HV%`wZt9bcThM$5u|PJi7A8%JG$lR~}k<0GwNR-^x8Jcdy*Fawn);c>9X9 zBCb?dj;v(CX@=Cw;T8J|wsPgl{*`?z=dI{g7W9wlPk=fIkLZuKfj!zfXUU z{%-wU`aAVU!O4e`Ues6hNAy{JR8Q#->+O0>f2DrEexLq4y-vRXDj=TFJ*s;|cUN>^e+$rEb4&pYA-JPPef9 z*z$?xM?nRKp zdKA4KmB6WvRrCm&MWZN%9!BjbhF*#8NB5!Up*nN{c?>y$Jc>Mm9AAA1oFjSv>V2#C ztlqtP*Xo_CM^|rOl~%>o>go}2x@2^fT0OjKU&U6hT;0FAZ}q%Y-RgqjF;I)*QNtsK zsOAyPaZuah zAA-2#KL;)2bHqjKyQF}J$gOF9GZj3pbW$;nuVA_GZ52g8e$4f zK}@1ahzT?SF^n(rK_d`pl!h2a!w^Gg2x1ToLJXh*hW1h-T@an96QTokKs<~dhIk!%9mH$VYaw2PUIX!J^lFGNLSF>&D)cId zBuYZGqjrck)CSRtS|JiB0nvh5AevD#L=$R)h@&_}48i_wcAJ{^5J#C_;Kh)+YG2JxxrQz2f2UIg(e z=u;qGh+YWs0`vli=cDICJP$n&;s&|_aUETUxQ4DlTt!zQ8c+ko6?6rn9@RtCp*o1m z=rTkts)dN6C`1HBAZkzz#3ghI;v%{TaRFVB5&1dtbBI4feg^TU$WI~u5Ar_{e}eo3 z;*XIZL;Ml)BZ!Y7k3sw)@K`#P1^Ch4>xhI}raH`EQ6PkP{I9 z3;8dI|B3u3#BU?thWIVyTM+*P`45QSM7{~}-;sZZ_$cxy#D7En4dOSDZ$SK4VDWLHrx! zZy^3P^4AbQihLB}1IPmq{|fmlh<}OvCB(l#{sQ8kBYzI@BgjV}{u%OT5dReUQ;7E? z_e1;>M=D1Mz+6`yjp-eJ{lKpzneBJLvB~{B88NA-)@ZH^g_L z?}B(YdN;&(qVI(G4)h%m-;TZ=;@i--L3}IvR)}vw-vaT?=$j$lh2916P3W5-z7c&R z#5bUCfcSd!^$>px{Vj;ELth8+wdiXh-ih7`@ipjcAif%XHN;n;uY&kW^py~gp~oO@ zquUU-&@G5Z(W4MwfxZIb%h8uZd>Q&Oh@0pp#FwHkh4>QmB@pjG?|^tadOO71(A#80 zejoXLh`)zG`+$`ov=8#T2(%CKVFcO-`49r_1L}@L`yd}gpnZ@JAkaR@`w?g#u#SZG zLEeWz`ylT{pnZ_{AkaR@?;y}V$ZsRiKFGTfXdmQV2(%Aag+luv??j+|kar-^KFHe< zXdmQl2(%CKRs`Ayc?$yVgS;7m_5o{HXdmQF2(%CKMg-aic>@CNgS;Mr_CbCNf%ZXO zhd}!vuSKAJzzP`J2YC$w?Ss4;f%ZXOg+TituSB4IkYfn653-Fw`yg8gv=3N6L;E1F zK%jk)mm|HvsL7;t*I}m6eu=i3Q~bsM#>ONNC_f`a1dF9g;+$25DQ2F;t}Ks z#5|IRcoT9H#2b+tA>M%80P%X{|FQSp@orOh|9I}u-8UhOuty+JLNKu{%a&*<$Xm8$ zOSUE1reG{vmWMoKc|#Jy&JFZZXn?ZHC@qvip^UIY*?X538um!po3i(}sy{pR|-&(YCYIy#^C`{2%JtUuQIjP^%6pOOAZ=acK_I-hJm+xcYrna*dp zKiv5Y^@lp2!Tw<9lkTTGpHx59`3&?2I-mZ2f9I3zCp+=~J@aOb9XGUX|F6N9|NH|r za6B{-n~GLHaa$0-TeVxc78>eS7yQRnwv?09toiq?yr-@Th%zzszt&3tV#RjhxF-2FF2 z-EW{EAdVNri&uoye+kt^HW-bhR1;Mpo)M)Op>(*{F#1F2Xl)67`w zO@5KWQ4A$Yi^Q@Sb+*wcsyJFQfhF;#3xq4^e{gtGImg%#3Y=$^8%?JNB?39DQphf* zBV5a^7u^bor$>X)q^^01vynPmWy1fW)x-r;zg+doiv#l(oaR~ zAd+}WjG$JWitPvWx&Lm5?+${r;~fD80|N95Oa?PLb?G2H>A31dbgCn+to3x$x;9@& z)c>FCh~1178<4#CcIW+nsRL>Sn2Mh-*LW$b%0YaiU@;Z6H!})wv-LX|`R;z_^~Vxq z&cjRTOeF74l<-W}E%TVYT`y)5S*u7SX*ZQ53<3^vw)f6Be(-~vGBT93oR1DAy(d#=y^i z$kbn_|JQ%H?hf6zf7{^P3Nrlm7wer5T8}$XPgE0A+BZn2jB0t-E2KIQd|U8N+4#hi zf==gaKAX&c^Bqj-iR9GPb^ekq*0a^A%juARGu%uq)BXpC8zW-)&M0I*lfyG|EZwML zXvX9(h=xMhmGb*gTymHyOrjO=(zT?|n`#k1c(~!orkS$FvSEw8pm3N$5o*PBGN<TrYpcZZv)S#-vFxSR261Jkhjw)lT(;7~JHGTAcd4ZBZG z$WsMLWtFm&XifL|-M>qHYU-zIM~&Ai5+EqnBx!0$`KCu1zp1WHR|P-lC~y6j34D{s zD?s%_7uW{OJk_^1xWM*@!Y!kdFQef`fKu$%n3N1tZnMu;srduIq(HP9VgDk%1GW5c zsj9G$Zv-&Iq6lxXp^QDk3m!*0nGPq*o@_nSX!9PSkPBE?su+XwJ~M3d2RuQ+VRtwS zi+KwgD*JSFsQN>zbNpN{|Ebyi~5oMp4Wn!RNFi}Az9J{xO+ zx2}zmk4I|59}X)+?+?k~4RmSX?STUHCY0}gy*~@yl#0Et_6j{O_atY%2$FyIzoo18 z=?CKc3%WS-w|Ng$EnT&+>jG@n!)!}e?cFIr7bguj?-949tA5mV0XFNowxz4~>bd}% z`4;TiDL@y8FE{rsSkQF=HuEjmqw4}}=3B6PrvTk3p>NiMo=aEF@45h+`4;TfbpbZ> zE!eeFfG$WkcQ4qb>jG@%Td;Gd09`b!%{>|I)G0t0M|wB!+0~`1cI>(UoAtoz(p5Wj zU4YGcP8dTeEef3Q#z!seXba8fm zbKinFT^C?8--6j)7hp5rg7HoPx**-$w_vR60&Lb>F!la_-&t4Bn)ACk{@EYRK6|!p z{Ppo)jH6@EjU6|p8+~H*@X_5z?gc00w;jG^SQs7~x@ssk)C10+2L?VKIA_2Py#bv9 zVc>N4@%{Vt{iW}SzCC*H>y>-A>siy2?imNf{{#QnmHqv~_Pzx~PH@$WUNPy*a0#=^ zFEV9=BIUv+3!V&EQ*K?v5CQKc2_Ywfceh5wyu0PNTPo+3uVeG)pS0Xl`%~ocm#)|e z-PZN#67FZa4z*EsWnYIlS#pcWi#Vx+3X0}B%P6TChXzR9A<|z79C2e z#FgdGfBjS9@ij~5Cl|hEtqlFtcgLT(rCZ;7@I7D7X&(Oh?d;0l4so<-a}lV=B{lVa zd$nyYV6jlrA)EXmUr2|t4$4{D|P@rguG(eU9FIivRRNFMtVVydl!Eb=U?{kQ`bx4r zUE-Zbu6^>0_K}C%et-FzLssaT1HFUE$obFQdhc85op)ZO!0gId9byj~jK%^n!p399 zMAfbMT;(|J%V9b^kdhpRGEr+aErK&>aWZO&_Y6l;ueyiV)N_x%xo>XSLDzlxN#8lY zB(uA}dF5L#&~GyA#Ig>tjVpLdFq1L0a;ciT>R->M5Oz)hT72Q)JmT;{{?P zD)Df3;=Hyv>Hm zttRiw7GNVjVyxp}Iu@gshh{H+7Y`~0VTgnvF zwYhjE6Y|;kg4K@|X;&hQS)@!P%HivmTb_Dk z``>)|c7!|TEappzojANh4CA5!mP`>Tl91?@-{wumB}*ihe8?H6xxSA5eg2(W^pJmg;*MR| zVD`t4{eGvprWKdG5xi>qOWBD=hqwy`3zhiyAMbY4_4lWq{FvYS^v~LF|Kyz0*POfu z^TdkNPPYESeO%;ncA~D9c+=G?RN|lQxFm4w>(;xE;ZFYfslUB&JbWOEzIgR-_c`aU z!%yPx?lzyDsC9_D(56s{$-f*GxbnG+!|-0qPWZ_K1hs03^~(z$-{UWz@4EZyL;i8} zwd_Q-L)?YXgi2h#A(UFO;}hd2o%rhU+iY>jo!0w4pp%auebN;6es1BxJHLHJ;sf7F`-~)C{Oanvc74=;=hwRqe-gUzty_-_zsXKiI>cQlMX1Er zZ?ksI&cC|uBjw6fzrIB|_tI15@63OA_Lj)Q4fkXB9P*`!osc`kUFbom#K)~X{((DM z?Vn!0aK%0^KXvxXCHyV#p1!w#(Z1)N{pt&kEdMJzQSK0TA^e~c?{UE)+wFCSa>l}l z6797cP3Q@D_Q_ zJ@1d6s5`v8tVF$Se)X_xA7&?t9pWymP*vi)_C0=wFV5NGaQ<)CB^Kysy}i@wC-I2` znsY42VuPUMAjDLRjiR?tKL)?YB zgGx+3pF1P;&XNB(>w390w!r&vZWU&VoU(*I=$$4+EB#9jE8s>GM=a`eyc zyzz#u?mPCC^v%MH?mPBLxYpe5cDtS1-i&Xz>@9X8(;@ESb&yJY|HN-+U9xw%y?A^1 zfv+ddJ$IK+Pki|e!=hQoeE!wE+t!@7f}IdM#9h2)QHj?s|5(5F>U{3ypJbo09kEwu>EcmH411YzG-*R+4djLyZN*)K6}Z0 z{V@wBc4sG29pWzbc~#=}<+s)J*N%SW+j;K8p`+e=1XceX0`gzh~kT^!4lR=sAA%_Umkq3}0ik|MR2yrKOi%`^w4p-S={vo!~pfU2OKM z#OVWXdhV5@e*VR?rY%4J_`MxYJ)GDoc9Pp1l6)ukPM6f6Vc;4^p-$hA$0IKC+aZIIKh5#YV15{8h#K93q7V54-TM zYw}yIK3+Kc#FllJ7jHfLpmj_#GaF3*@eXkp+mkBsxwz-WC$925`+WS3o;O`*9d!9S z`q*z)_q&Pw}t;t?8KoR;x0DbRN@*6?_}NX_{X+BZ25CN&=q&gj^6k3{wosC znzL72-*11X!A=~~A?{)?OC`R0?R}5`!t{6XhO4fkjyZg8=osU3`wFL>H~UoQ%WuE4 z3<0anpLK}4*m+Zl=bZRL-!gmgh)d_bK5w5tj4z#h;2(>_`Jdl^%fe4jKj-14FS8Q| zcZj=i;Z%t~xjhA6nXkoPePBU(2X82Q%3iBSR=;uHFXJCR`REaYL%XmOu?}$;yJ{-& zPapa7!Hef!_EYlBqm~TJ+p(11ZuhtD-18H0*_pq28*jD2YBSm)?qaV-B~M z5B(9lv-Gs{#_i;T{<`Hm_x|Cdhwndl(f*Hbb5~^Q`G3#;@+=TJfAZKtAlCXk5Jx== zM93dDvIU5BFAwhqqSlWb(hoil^Z*Wl&ryRh{&{pKfp&5B%>eR;m!BAPC-oL^7%k6d2d2;cgmYxAvXhm){dtC>u` zA;{@WwlagS&<$TA5p2-0lpmL}t$NU1&M7naq>b>cuaP!C7Rp$OW`p+>nz>x8PzusB z`10NG`Tf~eJDhQo*?Og!Ddp1T8GN~o@U5?VHJ^57Jr&OB6v8-;Gq#+~F@rDL4WGAA z3x?A!r|3zEgdGDqbs<5vtj=*KIF2Byt9y*bN`u@-d>=tb~h< zkHG_qAkW}SZ-j4s5vcizcF^N@QbaK1#Fe~{@OfwO3El9KR<79a1{=Kq~ zgl~OCsrh`HvN;83h~#aKP~Fcrr5WW5Z-j3~d)WlO&_?)Xw3i^2PN(yQkb~z$(UB{b zh#BRhH^Migy%d|irqXOn*+ik8mMRYG%<-A}Za(vj_9BRUCTo=gu8dTQwM*sF%=!y# zgm1=tv|-M&4bYIo^A|n-7_BKl0@;#o1#iE6+!~zEUwZb37$B!Z)M;65&)KM3-}9F+l;t zC%!SWy`;XI51Y|H@|BE`W{b&^qml*+gEMy<#z8Uv7 zozCXMMG(>&uGk}PIS@(D;5&39d^5-A@8>&YBYZRZZ^M_ZdZKl)?)S74#Sl@SIUoIO zBYZRZ@An2wkMtcq%Q5S&ff2U%t==1ZSBzggwq4IYvoDxE2Gt?Q>?feD`d=75YIYfD zXDsRWj@$dUACP-)o%PAcn>|14WqTL&yx&LntsCER?1;W=pudmT2M(Nd*Wlp5Uq%)T zzYq2lJj1t+oG^Ad*i+bL&S1~a`*wjY9vC0EaQLL*gNGLaSAjc+P8~`NEgt-G@czMH z4T_^zkK8czUz|V+QWlMSP?1X{zyArv&?odR5OSn~{MVErTTQlbu}XpoBBhWLgX0OK zKi4pgDr$;=)TYYjbbv}lYz|!^P8I!LT<>;AlF67wBwHmto=XJFh>afZ@1#gIaVJKI z4kK^2k|m2dDHr^h12{G{3?AGVcUBCdy%m%>1ZfQXP0fS0m~99{3toG&RkPY#4zD#3 zjUdTZMe%W%KyYRYVs2KMcz)obPKvapn21^;Y)Q7%fGt<8Vz3*H{)nC|JJ>|l;nqh| zaWCt4i2-I%ujb*4*h{&*-dd`r(fHq=vf0TuBS6nDmipgYHp={~87ZHMS+R?9Gxe&_uY%ZE6g5El5L%dnO zB3C@=G9N890s)aS7)pf8WeMx5$>Cl#j}T5~n?B4~F-Re%8S;g}vOH!=OGQ?Qh77DD z5Dr_~Xg%kp`=3;kr0Rx*K20KZtdNKZW+H2Ec=%wpDOXylglu(W6eg?ZP-oqt^qtzt zBvln?Bo=4+h@pu^Od#qtXUsT#2FjAlIsD~%C?5*SZC|dyM|+Od_X#}$!6;GojH>F&_AO(Tbg_?qhHE4{$!%;Zl zi2LLIR9woCWI>5jk}C)fEGSOW9!dIhakBSzHHFn0mAs_G9Euj}lo4ZEJTQmzBGz=f z97t9hdf6bm+AUa=@mV)dr_dE)XM}>o=2|?Sk2ossGHqwe;U?vaFjf~C@;B3nr&uyG zy%(q{FprmT<~S=Zr(-GL3n`^^=_Q^hk@$!sS2YHA9kQMA>Ya1G5O zL^@ZcNLCh841*MzvO?v=Aa1gxvQ-l}EYthebP8b5&=vz8ESA!l!l9%)4p&RL3ROVS zJi_Qp4C#xwOBmH+W?j^2R{|WXS7OBy?8h)aXd1DstLikmWg;EPRgx`md??~HayA%l zGKZ^Kz>9{A5Z(td?nhji=3D?yY=}>0uJR1c^|L$XV9vnx6(DZl$Kk<%MJ{;wqFsD+vvK2hm=LCu3n6i7Sp)-{KuqJi-Hsd73j!wlk7jCI98CNtS^6^6|L zgT8+!HHD|a73+CCVu~6p&88<6O&G*#zDm&*PAP|+GU6qqS}0`9l7qw3DJonJtLL57 zu&bGqae?P`ju;3A4d@vZ#5$Twq>V@updCj0-k(n4FlG!%XTIz(S4+0M5-C;Od_{2h ztJZ2r5OIof&;_!dwVP&LtfnAnTP>n*6x?)B@787QuKa14HDHVhh;X7Mx8{MGxm>(z<{FADnK5E(#TFwi>Wx z_GRsYFWsnfkxJ6%%RBY{tS%V$Bl&2lS@F0UJ*b+8(-89&n^7=z8&SNKZ&>3_8&dK+ zn2^p)wSC1xIz`IvByOR5SE?y&b*}E_L{kNms&yx(Yn9_jIH@3f(U!HRk)RZD6cS#b zI)V28YNV4yXp*I?t7-4LsMAU|exh!duL=%ZO$#{OqucnYKwp7?= zw8at99duKS$yc?s^kz>zA7b2kV*&UCnCy%{6fO13>hM;C!#2!YvLhvfJCNXl4vAn= zX_&_SV$NMQ`GF=y%v+0j=;Q&Tojg+IR#5Plbxl_$(iRKeP|oI+xfsES{$R+07-Un` z6)<=*Ha|Hq-4t~WlS7np)ub7#2Ut68^iy)o!I)h+flF2qrB0-36sluQdgw#7vJwiW z3)jkA%NK$(Xud(Es{xBbk}L>gj|YN&CQRfj#z4l-^j)T2ITa^bzB-eRabXwRW+~cb zHD=|UoT<@{AQOtYiEJE)%@#o!R)>W&VhUkKV+}6TR>B++!#1-I^*Nb(t!C8uBD5R0 z1>@y*t_CxMpQ@#T>2M-bwY7>x%G|QsvmS57l-H#dztvd|`Qc20Hb--Hj}lP&?@>!- zmYGUXR{~bIHoOun;Cb1_+j2>ZO=z=LC0no;&|JVn5kzueotj6?MA|7@ESIuzLNF(+ zHo}N{d5LBMq~BUF=KT?{ZgJ&^pg6j0{OKRG#7H$N@wOk~`B>C$p-pvv*;S8vJjGU~ zSq(C@x8cF^CU6omVr53v0ZXX4DlqFPiMpIC7vX9}o=MPpZ?+kv^=Q-)G?~#v3@?gE zKpfsiz4Cb0SF5|trMQ8w!${C9Cd2v~lXA4EhOUOztG}bYt|v>Doo?EnaCF z6}l|Dm^kg{vUtGm&==i4OUn)y>SfgFMf8?ba&(QFhk_=uf-#b3?YfdXnJK!0Iw=P> z{79>b6|xzhr-HZ}`8wup4DUEysvw;)S_39qwcz*GQGY{krxhn>6q^BpqdYFKoWXpY zKAJKOZ>y$oN=n0)FURwxv=Bvn5?n7)b!%LPaRCAICLeEE3|zE^Ao_u`)f8eLu{D^6 zEyl$4iaA$f>s21Nv-)NjEIjHhJZc~@e*%FsPJ5&r zYe_f~(A#_#Ptju4D;3xi!3UPk{^<|8=Yl@Xu_dXPiCBe{JJ-&(l0Jg~)SKc~cdYHJ zih-cW`Vc2r8;l%32mN7rSelFo=LrmRCyGwiXp=ZZlIe`U(hkcGxx!fmyPk1~Wu6}V zi&|HDf7pXW>6D=OlW^XYFa>eRB*fCK>>@VAmrROCMk5}(AxIBCqNacqB4REPY#=5u zUaPJQ24OcsgcJTGUJxlVYXFNDDiE)i@Im9O&JdTj1YvgqliWf(mWPYBRL<=noEa1& zLLNdCg(ON>bCRpl^wCo~dG~C5%KyLdS5^P-+*xDQ;o}F!d(Qp8QR@G{loH4=9r}$7 zQy1rUzTY-vJzbP3`5C2J5fX)Xsge>Z*`f$EISjA`*^n+u7w2)Ngh09p-P^FPU>7Hg zHl!=~U*w2Df&Nud@OwAhrmjE{il9=abWpzI#WlMAM&4AaWT{vcic``;hwuLVGE6fV z{v*Q#YVO}D!-Sgk-zf$|2(rs1g+y~OWUeY{q!tMjWpS~MBHE1>oCr0YVAq7Mnte+2 z2g@)e6LzYaOL$rimqivRjI=Zm)?dd^BumMrq^HslL`*2#_`0(mu(~9tzg}+m6*xpT z8we7{aEFNMDQhNev@Hr$aJJd3HStXogPFQ?Rfg%1Z)BLd=yRKIxH0{Ahno$`Fm*Ft zZL$p0ziHt3@5nHj{wHOaW{g*0#QUK#OsL*6B?z@a8Kx4Z$07`PhQ@q>NJP(rz1s@y zX37biCP-J6C^|WVtH35LlH&*WJjRWXlrsinQb~`QT#K_+wtym0QI6$N2j^&(MKdiq z!PZleD@5RQ0<))USS)H@oI@hzsx=w(){It;CzEzVtelRrlqmDoP3(E#(m~HVHaqdN zsh+nY-m`h*yf<~ChiCS@4fLfA^bXZVYO1CEFHEbQEbBm}Fk6}u_uIYG;yWcObvIov z0pd{|~DDSxow=95&e4U9pm$Y3TFf;u@C?D&bPV8DZNsv4vAMHbEkuU@~tL z3b2(&e{f?b?7k49chwYMaB+z&GNPpSd(*5TW^&*NA3?l6Hc?|Ku#;f%m@P1DiaGJN z*ox?&6sFhStReK~aG6 z^7?)Ed*2y%YVmuQ#OuF{o=WwvOJcs1>Ih_K1W79GyIZ4Fky2B?DJbH%GOAUfS?#p+ zZ#t(L{9AO8;)2w47XB1I#eO` zjUO(s-Ad&-MO-XW&IIByH@qc%&BQAX*jmNgi}hfm7fV!~0>7BGF^012w}XQaqANr@ zZHuHz7K_x%ly7k*CuiGsE^oJFG3(;R$xU4HO=F;|&jdHt@#4+JOfLZW;I^ zh-3K0z)=IrKx%*)a1K}o_J-CBY(FppeGXI)pMf3%5f0}At%IYXDkSuO0kMz^INdFT zc7R6v-|Bz1|6vgOa3yg5z<>+KG5xjvbUz1t=Jx5|v45=Z%f7e!o&!-4V&9+p{?vD2 z-!J=)?W+SPyi3N;99ubd#8_eM(6IpUzB7#NKDO0Z|L8}fua5o|xZPbp`uowdMo%1F zGFlvsk5aSmoPF)=-vLL(6~Kj#=-a<5cY!dVk!zZ|}~%vwObodH0(gli4eJ8sB{X&*#6b24?p|y}dn!lkQot7P=9-QG?iz zpc|kYG-y5ox*ob-gYYmgTf9z#iw}lYL#s8okb|y;uGQe4Fmw%cjRxm$3tbIet-&47 zfv$qC(%|+S^e5;~8l3mwl$VhPw?;r?ze0m^7ec_37)xHP!H=JTfFqIy-=!g7jHJQ0 zUW9-@k_O)>LBJ+Sga23s0kku$o(%|hr2zV}O@YdzPMDq*{-h2}T z9GEnC!+Q`gV$$I12O;3cq`}K?f`BcP2G2YU0`5#2{Cx=wRi|k1cfWyvSCa-W+74KE zo~*$O7zj8wY4Du)Az~PvszB=cK`tkA{G&lLk*pLBQNegDWnEfX9;t zkGm5XhMu6oBW)0HeA3_%=Rm;tNrSDwL%{z@gY`ZL*g$DeiGY##XbqNs2?0|m4HjSs zctdG0PXU|KBQ==09s*8L8a%8$WfZBwLmLqAi`3x3XF$Lb+ zqz3gzL%@SlgZs?~maJtBE{a0Hky3+uD-bZI)Zm^cL%^R>gS)&90h>w9? z0b5KBzOXL@+%YxyY#9P3nHqdL2m!B54gPHrFujdv@GtWq;GC(!M~(!G1XhC&Jp}(<~o%fTARrNOg~ zhk#+H2G2ML0=}IZJmqBw*mr7ha!bet*)(|K`4BMk)ZmI&A>iq$!Q<}$w!#N#@R&6a zaQOTN-g(y_AYd%1!8=w!z+X~>Yt}-*W>SNzABKS2qz12k4p^q2;ZO+$}DkA4dU z;H~_K2ABU1dKh|GgNN@4R-X@Pu(2QXAoQRHmGhwopa(Qqc^rBcdRBwQUqbgo_iHeJ zEp#7rp9V97(7n*T8cf{+44&`NU}7C~H*~iK5A{KJL3e5J;Paq6p*uAgJq@}8x#bh8Ez z$UrwiH+=)|TDmI)TwgWVnuLJ)s|M?rK)?f5gUU`2u!7ZKd2a|f!fG)02?UH`HJH2y ztoKn3CO8P##A@(RJp|lhH5h#b0;aJV^;Az-ts z!50pPfZMJHpKU?FbXS9apA7-;T@60@Ghn&Cg$AFfLBNSugAaWQ0YhF5-g`d;e0fJe zo%?6q-q-U^&&|D|zL$n$V~6)$*|)6EKmJsIYvg!n&gk;--_81{|Cu=hkTUD`ISw8}t-MM#l@3H-^vCD?`8-8#0 z=;$l`+l)Qe6CESQ=YCJK=p6TuNKA~wj%XxG<7o^tT8!ZIYRN3Mw;Rn{ zEgpBX)&e*v!`K?o)@rE;ze&lN98P_dj%5oC zk)RmP#N+M)lTXWKEKSn}w2*dGqsfu8)I3^7wB&V^tpbcU)4q7i)wbFhvjmsfil<(6 z)(Mm@=S?~>XzEcXIcLTyTQAOyat9tdJ5i zHY`SY;to9!2Be&Dsqgi^&M_j%Yyn{hoW4a!vdx?KMXD@Q4129)3yCJYE?AQBNF`Pc z$6)=BJEf9L5-^cO@+1eWewun*=Qe>LL(7V#j-wj05!+u?Dec-VK~>T-Nls>->VSK$0SYjogbIRVpDTi`_)wN&g(@A2xDyIW=2 zV`Qs5AEuOs5@n^Z8A~;@RD_HeymleuN*eOvoIkAZiK}^dDMGTf!MQpUn{Wq8idk$L z$x_w;dqOF(#3V32oG8QuK9L`Eb(&Sd={2QdA}{jgwvw@#$|ggQH8dPCI%bk9EEjhq zBSpF{pdxdSS}3#(yKN4+S)mK@BKuBQ2 zh+=-K>h)J54nEzq#IoQxVJVkPSLlR2E@t4W9_A8E&wMq7UdrouvyPSuW<#l%Lz@ME zQL@LP`FuK^q@r$0%!=TBRy0BS5u_S zxGdznX)L1i*fMUT!BYvc*{nSYhuk8XW3v_}pHEfo+0vkT@>oh1kT_g}DVcPaZM34u z?hwZ^5hRZA>0~YKO%yB+B2PGIa_|i`k7Pa{cgVR4=Em(j67|a-yo@>HZLhy%tAZwF ztC|&~9%*~fzKEJ4TCE4kcCBWk8@e*n3~VajgW6{H$TyKM3)8Ol(KnQ0On4>dO&L8@$Uh`m2iQ-qXkJcap)fUlB~T~tGH z#SQjU8UZ6cIO|&ShXq!yl0~P%G;rW_3VRN&U{Nls&ln=PB5!TuFl*08_?+FM;C#Di zvf}A*I}nTZJk&{1U}|!_B3f}HnK99IZ!IUoLQ1sdJdRi%9HfQyB!g!VT`ClXj#E<@ zy-FziP5n0((g#RTiwEfs!Oj zSoJl6E@WYc+e(lii)4i|gq*Q_5r;f#9+rd{OIe5r8fLQIm=KOiriu@|r}Tne>C+kH zI;qdw+IE9C38{5eu=|Y(d&tgZ8}%e!k<3QAns>HC-iVdA6)BpFcv!F?$Tv!kq3PPq zrEqWyxO|=s2AakqPSZ9bT(`$DQfx+j6%O3gV!;KD5sO0$)Y>&9d{q}xiU+N!v{134 zwRV+7oH1XmiI|OXi-FC?b1Aw~sE9)YYKn{%ldx!&M2lY1B}eroAOKsD<)9pQCJkOO zQ!+APsgkZJ%&bq;6luv3#obw-!^E5GWj5=HNAsZ^fhMFP7`vQ_DC(E>!D={e>Y1k= z%`b2+wpFfzi7rK8rcwboNs^MAultgw5N<_~Y>1Bt0$PYU9D^A(lbk{{EpFO@xBUjJ z$r=(?oeZ>rO-iO^O4lR#02&KPDKN>PD>!xDge7WYOl2_BnoJ5V zw0*KzwFGIA35U@ko0@lr4+h(9s&p6C`4A_?s3dOlJkT<|}!dO0g9ZxOUkNJWmHt zR#T+fZHuk$Au8=CAr=UNa3wj2r0UTo7!0ilmMr5wvC=fS zEK#aX53BK;l38ig5z1C9_!<-@mQ&6IM)=^CF&nDmt(<2*CB1hQsQ0aN#Hs zD2B^Mi`#GU2GUfxo{1U=ytw5L6&jS29JZ^a;!v<|h8fP*;%l(6Xo=T! z&bG6P)Ld>h>J^bnHR*AKU=5lcJ9oC4iIE`WrXfhC{E2`Cbv5C**;k~h`iM`5lv=T( z&DPM>%}EeoF}PSw;f8&@*%F3JrW~2G!O1M0Y$`zUAzwx+F-OAVcO~RX*v(hb!Rc=6 zYBtM_Hi-&FPdHe&@MYYr@MxIuvZ*}MXRGX)gHNn^})R07r<(z(R zIm(*iemCOGqQievQ&f_KlviSPjHN}ZJ6nlkmNY7JVndJ|8HW|A)08{!k=?NpG`&Rb zB83`#B>4-t&l)g9^=RG6V!n9BPuEHzA?t}yt+oz4mzl#Doj|3we#G;CQvZKa)%50y zZYxq!8YNke>E(9BX3B_po7_O!F#^RsHB+Nya5h6ohIFW6h%x!kuddWzaM=Nm{o${p zCmeS2K3A+WyWj0CzBG2twp(9+%%9mwRnyyrH|kDZ#AY|e6XB}Ja9E=4^Eu+BxZ6=l zqhY5~_htg=OgjvwzbRvZ?Zy^w`^s%|CZG2IZR>-dylUSo54`ZbJ1+Zrt9kd|cg(sM z9{edgscL%bL6lG3Z^oS2ge6v#nyEm+suM}lPzLi(rb!``zmPR59B-N~@#7c2ebciy z&)e(9bW5(jw*0fcu_u3h=)_Kemp@3a%+D(=XD3xnZ=@P$pb!=()Ddv$me)GZ09-8=gVfM}AXWekJ`T3jP-XZ6h z?`J1fP4B$1nlhs$ycQ+oii3CSqK0-YN07!=Q-=~2Mz%VgAyg_jXjH9=Q}c(sao?=# zkJ{%C&bvQ7Nx8u|_V<04zB}>6#d96rd((60u#>8$cU=z`D5;VQnrmiS1`n>jM9C*m z0p4h8s1Q4{av0bWoYD3dgm-yqvvw zi*;q_)C&*V{_X3JdGFknrrWoC=i=Tk?!5S9=R@qIs_D%eTEV(G=&uI@Lc=RsGagwI z@QB$HX5yHqo+5k%ZL)yq`dU=2iqg-1vF11A&0C#&>%~3CoOw|b`fN|+zz@%QX170W z@zS-Q_P)was+!)yrpFzt#BFRBdDUGPJ@(Suh-vOa*W7rG=Z1yc0oTnv;6(FXug@9% zB|E8VdIM_(4#N;bRVY|2IU^i%*-2n!;YxXvLLkHm3J7V-CJaoZq^QLAS=fI*Tf649 z8{a-)b?UGOsI!l&{%#j+&vUOFg;rb^K6E8JscL!`xw3(*n~-!X?Ke<_-ni@xh___wT%8=ht4`weY(wTThn8?|wM* z!V&DGs_C8Jae_&MEuCnJvlpE-UgPaKeNnG-du`D=W6l)8o=#S>S54EF9@#5~=ueh^ zc|1E?rg~q!eUkX_lgd&2NBbXFYnd0?R-eF5s+!(S*aLPesv$+6NpnS%*5xXpFqIO> z2IJ?e`C?8G%l=9-C7GtH;_gcO=a&Yra>rxKw)kxS$9(@h6o$r&PyOhme=Ibw-uZM; z6{@B;h^nsy@vJk7wCm<{z17CcSvX`TjX^6GhNU1E^|&fV%m5x5)H>F`b*X*m*yP#o zy!QF~4~+27?DoJ@TPz8*=;%?-KbC4Sf}K<~y;C}wz$~e9!4P!EGto*~PZvU2tJ|d$ z^j@M?bFf7RVc=XQl2C~oub=SG)!DPcuCL}TT4vwNvzu$jbyx4b^76USJN96Xd;T_d zQq}YZdmYh`L&ya(2CCsT=-qN1DdnT2*r=9_VKRhy+BwC7a)#2hE5w1xh}(7H{yPV5 zy{x*+rJr8l96Ne6^Ax=0hiAWZ)tY%v9?edwn%>TIE-44mls@GYaym=FpUk-1RarF3 za7vCv!g)A~mQscq6PUhv+kCS2l3jOx^wC2n-+mRj$Mw+D=RJMdGncG-{M7nS&VQyz z{ezuUHNAZ%9f_u_zBDBiFeK&R6+9iV2-do}h8YS#Q6*8TmQ94&<|n0N7XDuMyQB7h{!(^Q)%2EKSy-RoY8I!Ea@CB1791?c2d>!w)k9D#afJ%Sh$GlJyeOp zFk8)^3FSHz9qgK(nhL(w z&DTBcz1J7p=9a~t`?vf>>5KE8$~`j{Kklnb4qm!8!A`20-ri6MX*l3`z{BI=Sjp(M z8%kNRRFT_ULvW!+Z^rCtXN#>WGF`_D7hHcU`89glt>l^Jy${Gtj4ZkoxuRJz@3YH_ zhZerS`d93vs_D(t1=?Y##@t0qQwp;BTAe3Tw4}87unt7Whnqw_LpF7uMs9kb5B~Gy ztqzXvvh=-G+f;A(_<`E6bnn$)ozOn@j+HOKyRNtm%+ji+ciI4UCQ^8-o=(`qyv-&E zX~|>`VOfl6`y-KLA`R?hYAHOdo37(!RQOkq7;pagX!44qTI*hU={Gm8{0ofK)@HxF z>DqM%1zu+-RZZ_)%Hj){3WQ09>sc?7k#b5y9}lsCh7e1F-9ebJ1q-<{iwo2CKPNqV z|BqLk@Zx3foN#&Kv>)pq|8Uih2Y)4_53W7(7iaFz^sHkiRZZ{0rZ1?~vEnR#=+d2O z!{f0ZuNk;|*=PGHZ#`Q1>eUl}b@pyoeD>AZx&k|?YI^&5)R%Vnj9{9pd#eG(X>jJF zwM?K$Dl~}HA4@T2zT|0%txSpblO3jVo?sxzzx1~bZo^3a#*A=U;IQ7}P zk`I_7OTNCUI)3XTAFTV+3t!G!_Rz;?zw#^niw}c!g{tWt^6SD?Um=%D62WL8OAAaa z63?459@ty(m3>u5sFVul3XOVex*fZpJN4@8FRa}*m(QHsa&lk!=l`hk+x<>?YwkWf z`>`w5%x5Q6O>d(onlqNgMoS_%9UqUxY`Gj?Vp3jrD&?$2fp?K!Gy>s56qz1`{&w0P zPoJT`FF5bhHGfU*{ZjGRo%e1WGVj&CzmM2DxlA&L zqD8+{P8f1RI$d*z?A0*tGe$_#NlrJnclX?9pTED>%iQ<#ZTI-#-Um-R?V6L;J$GI8 z=xg+&Pbp(t-^WfyI%46uXg(e?y2A~^Q76;xVx05oVqlXhkigjt*{=Fsj;7IHnR;$j zTg6E$4)i7-zjgUy!+Yz#c!s!#g>+68E*x zWR)x9)kYx{%jSdicuLW?BSz7kH6`LDw|#naTPEzdP2r6%Zn~-RmrHur?nRvO8|T;0 zT;==g(>*V~@W+4bWM(JX4sk5uOoP~-JZ=+hP7@ZzSRERQB}2>=RQ~RuKe3Q$#>XErbC?Y+e;_|W0o-3 z8B5FHB_7f?R!cC>wL&qV8Zl!+r)D?+PPk$L1F?~z}=g>8A#XE*F~i}Li-mLB&f z_nV*K-`y_s)slOw>}0q@98~O0YcQ2fw1C`+;wW0FWn8#GilE|3%9E!Ct#I6UUx9bYCyK`#(=}9L7$DI>buEoPsmaM9Z6M z>l-FRJdTI`nU*31b7{NLS$4IPm||fJ2s=GiT)Fg|XXov3&+nASXCq&~yOnvTKeV=c zZcZY*qve3Bu0dYm*vVjr7z=TR02PT@3`CHu;c`Z{ffp?>orm3Fwh#@ab+k~aH&_FZ zQ|mn3e6;5^=tobz6P#;&exKvcJ4JrxkyGWz+4PpnUf$*MFSdCSW+&+mF}UVUeMVP_ z&scJQ>GH>){rMh7{p!kso$k7AA!J}vX{_4An&#{wKhZrdkUdG@IifMz% z7vh6aOy{oX1R~}~Yc9l8Nyo%cRZ00n`RR82DnAmU>E&NGb$dOUeD3%^t~(+1+G83Xfex{3vl}e|C2n(rYHqq(_FyJ#ssjlj9@m52l%~B9pp8J?%O*{? zii>yq_{}#C9IF3m(cC|cZx?#;y*=k%VVZTFVRrK5r;j;sB+O3wJH+*LKHrRp&SF9- z+j4bXlZN%?h>3Hie59?)OHMg!ZVDbU>7VY>H-Ea|&L(uur3*)X{MDV~AHKG9-Zqat zF!JOp^De&O&KG^PyJHEViqXzcRVm;RjRtqwo-P;? z*z}xt!e_S}dC`8-fz8J9vD0mbd+#@o+ur)+i--N4er^2IYx2)_;{SV(n>BXA(ASUx zzWnFke`?_0UIQTfk^d(Au?r8F`QIz&xn4lnRP@jWbq=tZ@n8R)@W<~h*QSL~tuHsa zehAeM4S$@$`|k^X#9%#wfxEEb@JGCDG6xnHZ6RyDZ4TQRAsh$;@&9xa7jbhVZ*?sS zb^J3;KRo=gSWcz_G!mlRp~#{z(WsON8{!P6WE(B}0+z*f!J|kH+!InlQl{!{rITK+ zX>a?=wB8d|DAU{+Q6hAGi z?6kTKg+HP*oB!?$x)2ouSAM zqH1;9b*t8I>8HKcUR%=MYptMFLM(VD zWnwX@LFl+KVy(wcMNVAYr7ML@mWyqJ%-v%|wp+-<%^X|uMy0~tyG^G; zaHq$bisvpoh5ssN-X)rooj3CgGnjbexEGp%**9%-63+v|$Dw|}IYdCBICwi5YwAO3 z$2q`nJ?%J{$P=w+X^TAO`7Jzd9x^zvng;N57sWgWwk;;DLAszJ_o(TtFc#W1Hjj2^ zl?1W^u0X0rjv8lqi0_}A_U04u1Jm6* zm+-D9HxcvyRZhgqtkLHb?-+Wf+Z#`9E%-Q1z!wS33pDuCzHKTG&AWMFvcJuF2f_21 zeeS%I*fcg|1*(Ki{C;Ur5HRH~)3l+Sa95~0_Z(aBXwXZ%)k)VMdja&@CI6n$O77Wr zVTtucP#`$&%z}HUWi?tw0&TU1`C`qOj=E?$2`mX4rLAT`=7xHEXy$d1!{C@`%%)9yf|BW4e=hg>re!-0&zRq9$4@b{j`ILkIW&f$&p8|jV zq<$Xg58$hNPoI1lDCWQW)P^c*rPX!~n9!mbv|16xI)==o>@1YB6|i4Znv&JUp98sC zN>t2)8yFIyLnNy6jNh#aQ?+V4az}{C>crMqF9PL_BPnZJWL35MCQ>3XoCZ1*$pP|c zIiqBSSe*Ymme$#7W!lfelll;uHFz9gsX?9BXH0@bHJ;2di68W{jM1+ep#rh_2Bt@K zWO0q`;u-{(Q>tyf*(w`Sma!G6KEGC6?<}qbbmxZM7T|!Zgysr}nuUXoV0Q;pGBM zL((}zjew&A2`4Z`u1;m!pu%;jp@jvHg|aeJ0b3UFuwAo@BUBdI#ZaeT9O}}hpOntG&nwvQ2{> zNeO#J#p?Q#iq-gp?>MU0v3#b}p=J$;N*ElxL4XIthrJrJhq*536{m)DfClI*t#JqaKDq=aI zhwZ8A*fXKx`_k~d=W0yC!7i%X?R!za5fujDm>x*Ya5JE#5ZzWKHdk%p z!YgU0pV-?XQ2}k{d`cFONW;3A3Q=bB{I|sbw_PINv-xUAN7`f)Ri~lTt4)XC6-I02 zdLYYMko;cSg%EF9|NqrH$M3)TnuBlIo%c2P#Lo`c78ty+El~4%+9(i9%&=252gROl zrAKDn%`lmfRCc&=W;Cu2!VDd--V@u8E}m14_lGa~SG&P&c zWlC?8dkq!w<{xASMWx`^%lC#Le+XPDh36X&=gzx(p!ROlu|XTpwonT4AnQ7Z4jl{hIixH6 zNV5l479@F>VO@CqZ2>%g*AejcJwwq8{s={DHIRSV7A%f7y*%vu<667s3mC;1(|*A> zyoh2;9XSiiaQj+pP~CAccQ4{!Dt&cV&U?soFrE+VeIx z^DSyE1TAfrONjH<#Z9`hqk(_S&Hwf6uO7PZg}Rsi?cB8$T<|3cMRq;c1j|!TwoQ3{ zt(WVTLwO?4i~Hvm9G84nNG|u~Un0K1_vLwufW;pz6YQ0r{Tb}^%gX%j7XMF}Z=4+WC@`K`~F`S4U(Ec2itz$NC~lM6TLk2c)QSJA~r z^@|ha9hZ9mKW78r&yJ#&D9JY;RT(4smhh@zcBHOB=&cspcZ2}sA#h#bzpv4nR zX_>pls#3~T?m|vqlbVLu!1>~qGjD$DXZ!ze-TAJa+uwGZyY-j1{?V=a&5zvtmYc-! zAA;KS=#AgK@xdE!2mJuP=6dPcuV4GXHTdc;U48%6dq+P9YUzK$m7l)y-YfIkq|xCU z4*u1__~3Q>|8hUxf9u|F?0wx{ZEt7y+jgJcy#ka!xgY-Y^#^;bcKG@eHm-L6vuFqixFCP{bX7w2E8t*p#z#xOH3P*x~U0PkS!Z)kGmeppMKUt z?PqPFwgW1-3{b`gbpW0IAn(M%pcgty&P5qpRB|Gj@QwZ?7xg+GdHR_^ZR7AWPvnH7 zm`ao6S`{R)YGjRGWt+(r(rGwTfWUD`e^#dS6#@SAbqk8GJE@mDZbWlYC2>Q}N0Qli zMfA>K`xe6F>1Qk`e#T`e<}#UxbGg;)lBcg-=z8ttbft09M9r0ZAWlCW=&B!n`ek&% zrLe6?!kVhrEL6SbGOD0rz06e9HC3-(sCxBfRAmb|-mb3>7IFG%3ss+X8C99Q?3i4C zg%yALsX&!>_^DgCh6EDFRHKwdt5kMS?1JcKg<`eWa61Z(vPDy=lI4K%iCP{^iceXn z{gf@#LW5a(ShI$5MfE0XAM_OpLo*LMo;{VS4zA2%(VlYhOvx$y>8loMU$uqWhE=ww z0b6UO`)M{{AWZJZz3L>7SZ*UJYKRKMg;sS2HoYzW^ph8AKY0tawIFBe1HIVF3BxL( zr{bK4jRd8Rqh*D4vy@K?(wKGij*auDpR`c>Nn5DpBnINMoqUdPNdfe!HF(nn9j1|H zG~lNV#c%QBBvXTIM}_&*+Y7a~w@{11(|ozvhjq$AIhS%rAtH7s=_H1tn46T*Dg#&Q zc`VHLcLv*cqvua=F4W%KLTzJG?fPSolBQ;R z5S!`rdsenBwHS0}mTFmZ7`dR)x)+TI35@OWLhbPuYRREi4P9DSYDP!SR~fzSX99c3 z!v#cXlvN@nW<;@EB(fZ22XX0 z!x9D=g;cT5pI%?6y}pH7$we1=D)ubRV3Q>8l)5biAHhtk)=Cuxyjh#(J#gF$AA;#{ zZK3ws7HSE0l+A>*yiQE8(J0>&C2Imoo}y(?(ZN$8B-Szx0aE{!1hBojPT0UfDt|U2Yj-POK&tik3$4tY0%72qfgI zZmCN~Y68w=S|gDn$Qa>I4;N|=Pn>!%^JUUjVyiiAxL9njP6u9T=QE|OkE$@2N;*Bm zXw}fZQ)WsoTa`f#KHCrtY9jPH5r2BHP$pRB)ui1QQ-7>~2fu!`8@%IEHlme1=Y{a=@SNF4XRB zp>`BXdRTL7U8bLvOjAs`<7t!1ieU~J3Tlh8{W5B`&1#1R5o~9nc4rHHVR6I; z8ZP9}hLx9xn85ZL*wCEN(#Ta6Dq|55iQ@e8&(75z{Fg1%@@fa=3&dblVH3(tk>I@m733Y36R8sWSL<@CMuvLtd9;+O zr6zT0kZu<$y;+e;RT{&Bf+C>E1poYd7i#}%3$;^DRKi)dJ8cR{(CrLDx1o)|2@j%E zOS@$ zVXWfB#LU(VgTfG`V8l?ys?tqSZ!&N@S5CG4HV#Y{E!6(m7HT=NUyc|k@oO5Wk=dPb zRf?T?kY1(L7L1L#Tpe{(BHL#};J4vI?MJpyJDuqxCQ$>LjPgQ3>BKW_I!ZyMRDmrF zx@7_GGs8h!${Pt7gy}-Dgq4rO(nO94Li3uD1fjXojguK=(WnctWsH z`^Q_Tg-9Y>XX3P+9b3(+tFo}f4|17QOH(qncr^4Y+(>4UkgEZ;!oxP{tF?3B&-|M&MldFMEHd`skzL~j&|>ixjZJ;JB6vy zqzz4Q!j5RR>g-5rYrR?8AX}i}MA*iqK`Q;|X?ad9f|qA!L-UWO=jDikjqvxpQJzkW z-pju&UuKIJDE`%h3PmiLzM!GuIbyTbw?KW$Alo|U2OHlPu4KEo%(+9i0bGLeo!ygW) z70+XZ5}6SvqwZAFGqkEkTALKpj;qgHKJme>5aV-|f$#D~@ZCgkCZMqY-Mb?+2f<+` zZFo&{Vv&mO26R=)JG1hZo6z%H2j8YY@M)*Ud{^je3%hqN;Z2V<6VIJ^xsS)hyTsx> zcgf4_mz{rV>k`j4=TB6y`7oYSd3gf&$7r5Dyi0Yqdkd(jX5ia$e6~0J#`%tNDx1!L zwEuLrmnmkzw{}nnxiDva;XYLX2&_-(hw{ER?NhBDPn}=uabrQD1Z_MB#@usBS!{ET z-b!aDF;cS$dy)%V7&r+w(#w%m7~RvQ+{6un$BqTfILcdhiI!)LCh*->L>eHMZa5V0 zagF)_0|iPm9u_rg%^GD86K7CT`Kg#AI`@$JuzxSxc6`|%RRUA#^5tpALHsUA9!Iv@ zAwIkHrD=N$7h4QJ*i?K6SW#ntzFYso!Gjmi4K4DSA2QQ?_+Q?B{6*0@{AHQP!6O@+ zuWdkr2)oAQrM=sSSZs2ZHJvMLANKPXmZR;weDiMX6FDxKAP7? zMW#v<%_R-L2+`ubqU+m}+PzUGo%gZ>Jv)G99xeu|$ktJmDx>*)l*RQyvzsaP!&b3A z|I}7$aaF@^pxYhOD=cq)tbtWd(SqI!A@PlOF5_fp`~RQ4Bk$aP_SWCrdf&~zy7`{t zzc`L>{P~UO`bVx$ul>oj;OZY;9Uc9_k$2_yuQ-RlduSc}#{+Z!clPzY5ASKazqzaI z{5pVd{(0tuoXrDE=}MPUzWEaP+btX#8>b;m-q&~4S~v~ z2|QsF;Fl)wgiU}gePH_?gC}hQ^wI>LunEcw0+(>CCp`_31%XTKSKPV8XZ=Ti$bRO7 zUvg;zm-zO*4S_GdG=WQO6>LM`?F#~z*xR{tiA{xV2t0LZ0#DckUvy~#PuK)sxFB$e zeXu8Og123oz!Nq>>CyzAunCF_0+$FbPuc{9OA~m)Cdgl!z!Nq>Zb9G@`>Rjd1n{K^ zJYf?+mnQIpO^{s>xP;OtZGz0D2|QsFq!$D(vBKSX!UXOu2wY+({LUpl4nO*9%QGK* z>!k@?A_Z$30;x+AxWvBKHU!?XAaIG@h0Aq-;gX?d< z{_1PLf9=Pvee<>O8guPUSO4bf&tCmbka6&?tNE+9kACOqM~=S!C^)JeeeQ04_twsb zul&6$PXm3qD>uPyT=4UG`|RGyhy=yEn-hi|lq()McsOaUoTWPUx0Lg?lyetyKH=f6 z!AeGC_bG8iv-s+D`7PyITgq`;%CTF@HMf*&Eamu6#U_^Sg#oD|>I`*O?)TOYY$^Bs z3puY_<>E48G1wT_Owh!=wxaL&7q*o9`8VyeyC~* zmU2J4rQC3=2<$iigxu4on?*G|R?pL;y`{ga=ehEazKe!+|{+;`$#kf3r9{tdk za{qJ@HhfzNQG)3YQH!b9!TE>Pu<@zeiG9j;Vz1gx?31?>`=sr}Zf_@cYdLVBfe}Yd z#BvDowj@V>`kOCsBVT(BF8|mU5rF zrQDq@6ePTpJ{Dy3?pVt%F+9q>FJY<9GDiTgrWSOS#|LQtmgml>3b><$irBN43exP~sbH zimi~d*c4ap_d2nqTy;yiGyi+!b2B1vwf-zjYC;qfot68&3T-Ku-BK>SrQBP$ zlzYpTa&KNNgO7Yqc-?klpRt|TYqt~o^zFo6vz^$hm-YYtXy?XnUX>2*-9P#}_bm8{ z8(J9wP;13`q1MVJzI{G=cMqI_^DNEnnJPF^2c_qyq0c*2TROMa$|b(6o`1ONvIVX5)GG zyypfv?_QQ@HT9Z1Et%4+A%a6yd^xO6i!yQ7BC>2a>Vq~#k9}H;lFPNefch26r^dzp zUHM+SOlujj<*>|55m0GhtC?~4o}BMj8ZB{z3ro?*}EK4l;&IUaQ{VBJ-Zn$jPLT$;eC@R2Z8N;nbZUmIOyV=XFG7 zUF$9C!M#wwW^wEDGr~uw){A*ma@U@ubKOys7{{KM9+a8cm}MXqGK)R8F6&fdG$3@S zFoUB<@0qN4`+S^Vo=3HCkMkCu;<;TeFR`CA_q&gM9@Qnb2hTe-{PE4BT2Gh)WB5XH zl{}be<7IBuLsN5J3vhjIKJL1cP8a5nBIg$K%#rGFX5TF!x|?=#T|J?LiJ%#zPEwds za4AHd7EGhV&IaknUU&MfTno`jQNw0qiSUQUG>zsB0jkSs1+oivrKzD@(yzdUL5CRg zgn+OWDXJ)RoSEPW2V>J(W9p)Mk(S2R_%73^^4+a+CqRx1xOFfU|Nf_+4(IF6S3R|R znY8Om&%}AM%()Zq+$NWQ+Y|2++ob0|QC=eX^ZbeTx0n}oUUJdPvs*rzC(1`>Ma>&H zuTRje5?#(C{&+JxUn*swjjg0P7`5~8EG%oYwpzwfd;-&1-A=b{RAg5R5 z(-w4onnNc=uGS|VwsfzE+oU;|;@EasQD?V)&iVgK9AyB@`Tq4Q|8S?i^P_tQ^c8RS z5BJ)K|M+lvh#kJ_;MWem>)?I2|Ml%}ygj^KynW@?&)oVSZ~3?0zW>{|j&J_L&2PII z?SAD={N|?||Jwd%@BQHMcOAd)nBM!Goj*H%?Tz2wXZOEv_sx4>e&wkf-*@A=8|;nG zzW#gHfAISA*E`og_u3yH{`|Ecx%Sn&KfL#Q*Ph<_(Q9|F{^zSde)WS_m4nw_&0M*$ z`;Mdka`cl&-*9B^KX+6(I=u4JE3dET{cmPp+q>g>rr@@k7O(Z1g`qUzL3h85+-nz@ zk`PvgDRz*z3vyP$S@Nd8L^SvwTcOfDwMO@uAa93B2eeiSpnf|*6)RshG7zYN)UVaO zZ zZNA^0g!atwMLC5x44;`;QzIEOEyY68*VQG0$WhE>6k{dKfO)NNG=y@cP%mWUmSWR| zN>;}Qp_dWSx^?ZRmk1|G*gj?mc1x16yaU&)6b3p;j)vk4*9?WU0==QYV~vNeea`~n zSVg5$Y3aCQfu>2ULU8$^YmnV)hm=`DW3v3TKPy6d&sO&ThvhXG#iNj7dE$ZT)Mwsui=zQZ8{LMc%#FrO()KRPK;)yJ196g){)Dh zl11=#Hl6U%)xq)_0@iL0ZK~bHN9`og99Tfm!19JOBP3>^1k+F<2VFFR?c!BxXJ?72 zwFUyz>{J~wXlYG6Hft1|%C_sGm~?2+M=lG-Ygo{OGI_kbch~K-LeB7Ks9dbzr6!CD z;~1WT&e18-SBovouK77SpN1O6<5w-OLB_!hR0^{Rsn{|7me?x?DyuV@Iyp!cWY&VZ zV=Xr>=~LzSla>gs8mjm>Tl0-R3wPp>@6d>o@im4>p(8CTr4h9^%kouGKfb;|co{FE zgL35JsuQ+MyVEF`O%iLC1eF|Cz51X)OXgHDdWvRZ8CE99jWf?G!2^S9(w&lO>FmhqN+~GdgjR)WRk4gMr}|!1!1##L z;hXPSUSkS{jUrWqQ-eO;*K6af92OnC&f+}G_ru1lky9I$YFbQ7*Z=VX;h1JvZBh9` zH8!yBz&8uTRF~UsUk}ViyD1a>rc^+Cg-W6vy>^L+I))^7eQpR$-K|Ul$9Ib~W=4}B z79c6ohHXF1BTOw!?}bZ5*i;22Gixd?R1_HuYbVX3N2TacRkQ78J5PbviYY}PqPO2m z?`ykvoS+@|vM62+dTzy?f(I@}i1IDEZM5yE<;?Ur&lNPqGI~0(yhOlToJ`A&wrF5> zXhI^bw`gt#r=v1L_?7V}D9}(4!!En~|1K{vW@?G1=cPo)q^e%Qvz8SU%C3f^nOZt> zqH>7G(}G^Gytgbb;TsW!d!{u4@3!Njqag)2<(4>wb@EBNG&Nz)V^ewoN9%jg65&O% zC6%0X*Ob^c(en8TA);QwHd}GlEYk(GScj!J4o2z2uU{ZsE8m+KVD$+QG;EK&jMAqk zZDWud52b-x$Rp`;z9ai`LRdGIC1Pr{riGS@5wjxfT5)F9o~hj?ULvS4$$7KfI2X>U zPTp>^H?J)bLR9t|HN2H>_1xMpjg%W%sncza3)ymAN5P{nBbFnN>NUyZKVRO*kavh~ zGaM)-Y>=;eB^BdZu}tEzP@QJzd#!tFU4uGz$2t633B>#Cc`<+U-z~1;6?#yqUvd~l zb|}>IbDd7rbNbaJ7RGQ@lm=d^i+AlFsBU-oHA_T(qIXuCr-Ih(XJ7^A6XYz2vR z^oL7Cnir%=u0A5BjnSkws?zPI(`}e>7f*Fs5bUFLP7N}0g0fe>dWlF)yK%icQ97x9 zyOHSrcwCRVv(U}f(Rk9deFY`ZxRF*8a{sT-5as&JNyda?fP%r|&KB~*)qKCL~l&}NPZ4zf#xKdDVCVwx+|B!#w9K`Yg>GnD{UlT@QZ7%oXSV@ym)RNwp7 zGeo}4Xe_5w45asMgc{LOqiS%8GWLqxIIrgNAvbB~rXBt0FO~>b9d`I?Q=*IYs1el0 z80=Una?%td6D!b`)2Mb!q&Nyt?*LmOtl7v5y;-&~>{bf~OP9(W73sCLGAFR3qZavGI}6uf0`29C zR<(Zky=RDi8LWVr_E5D(#bk)i$W}(JXM#}$6Qse!SEYD59KnrAwEO-uL;)`KT)HPC zcv1*WBppMXTg_!7Zh&_bAxH(LkF`KqOl9xcB|^jc6ex^ZN_T8YcH9gP6QYb{N0m&? z$>y7UAcG>`N-Zh`N3T0Wpp9A8E*1u92g&-=RG$&-ESXX{l(TTk^c@ERb*xvCo^$^Yt;sKLsGMe10Rc2+#hmF$2pHaDt zQ)Fhvk_U^?(HAYR!D?fq8qOG*$0#8`8`d}<3aU)vzy*r4&45$2fW**A3E%nlC89~$ z6A9{d*`81Yof5{aNke5Izmpp1wnO{8kM#THq+_S8y)RrMn2J#@HHUe7SgJLPy_x5- zOioXG4$OzXF3L_9eDh=j7tybrJ;7~{IQ%cmYt$Q+R*Y3hcw*EtbjY=h#DSU$7cqFX zLN^Ebj#M6}&?F9yUb94yGl{BI*>H3m`Xq)+I8aRCl)O6P-@BY^0AyOGK5+z{N7jgANc-Rd!Go^Xrxkg)XR&*pP=o zSVJIxl1?$f!FMknGF)WTcac6d=r}>iA5=wmkTjAWLh9x?&rEw*)nR#D^Fa|;b9o75 zRHG~c#>w=E$WRTnoZ(WsCC4&dDlj31z(vRLl+4JK_TRTeln}A%fk&GJnq}ywJf3;r z!No)d7@I{LC=w-;wQ+-0GSPdXg=J zZuLYS-#J?h@?%uv=zL>Zts6rRd~QqNd`a`fsKB&(L5bv$RGmyv15O|Q#_}4uvabu} zR#_t{c-o_suBs%FCaGu*%asEwXtn}>?7Nj#w4YicV5r;dz|%&fH=^KLAW~Hfs;3kR z@r!{3gMJ!ZH(+_LHnH~Kv_wGApae50uNUYVUdG(E192SVITkhWrltI-z?X^6Q0~O- z{rgKq8lJL6KBZuqoac1N7-KVtZfCkwoGuzxtKcPwnjVh{g4~0bh!ixfJ26#hViHpA z5jJ?=WQb1LBl9IDL+e~wpm{x`Xc%(kw-yK|fy0{8mDMZ`>RWalQXpGag{f)vsqP?H zDTnpyX;bAL8sAw?4<`<%qmaeYnKBf%^UQz}L4g>KG#b5P*&QW}Off!YwK=4{|C{su z|LV>U@7(@ZxBr{l@3?*E*6-c=C%2xxMcn#i@Rt8S-L!A!j{oNP$B)0}SUP^=jsJAx zdvCn&M&-us>pyq>A6_3`hpzqAwI99qRo6P#-f;ClT>YM_@41Ryy?OMrN8fT}9A&Qj z#g%_?7w-3MjFg`3F-Z=P~gKs(*9NgRg^Zg&$KizNbf9BqA?S1E7wD%=@ z*LHtu_ZxQA-PF!U0G(}r;OCAHo_cEM-1Htz&1EnE(1Tevl#|&@DjKLKX~>6*amaMMfkxp z7r-`{8_vTI-gg0PgHOT*_`!QGfNd~0oP!^{=K|OUbHfn)Ah`gx!Q5~beh^;(+h8|1 z13#E;hCv(b1_8T87r-{y4FYxxFMw?@00ay$y#Ths+z>GL>o{ z2jdH18_W#>bB`{7Z7??k%vU>m#yv{-y#TmakPC0XDl`US8JUXle~ zGPnS?!Ar8hOSB7M8@wb7yhPm$%WUwH4Db@=0@wyG$pA0uUjW@8@wa~yrg#lY=f6%fS0`M0@wy~XMnk%z5uqt+!L8)4bf`CbD49)8fg0Cv8YWJ|zH8W+IM z_mXT0cnNa>?0hfDmVlSkFMyrzCD{`2lG+8Z^SuNtrtky$0@(Rpk}Ux*p)P=(?ftOS+fNk)SVsZZd|N5POx^w%zxBmQA zc=L~L`p3U}JiPHcHwM>#{rbDF{qnVUUj4LKJ6q3rA9;SbcqSeCU#xHS2O$1KOzd1R*#6@=_xsX zP|L!Nsam891~p8Yw2Rn9p*5-5$-rAOK&y(?B-ECqke*TrIb^jT;-)dk ztz_}svDG{>(uVY)B&I0TAhH>1%n7E6DNu{hi$lRIv@J`WV5YZZ;4T@Iy*RNujKwuw z#m9zaY&Bti69agaLAw|A#JE$w7=2+uFX-sfZnL=Y$lo&dJJqwjs z4KzfOmJIAAgT57kwoF7fYSpR1v_*%j%?LL!$gVO-3eE1A5Iwq$7MP@L^~zem71SXZ zZ|8zWd0N5vyXv*=aL>0k#8z-mQXJas2G8kes(wKn#>}p@){V2veE1EYkNUzA< z@rrdu15K<5wcVrZ4Lr|Ka5$-0>Dn~XgOXi!YedbUM3NRzMe?_{;-s&frr{l0a-4WHZr3u@+8ca7t=R25T{~ zrsz!(3V4qkO1V*yN$c#eQkP4~6i1*@zs*V`hD3rygmuto{MiTUnHxMVfwhoGA+A;P zXvWGH#etL}XAE7QHIQinWhT{hZcuGtxqR76C-m8}pez}5Dg>#uDr`KR>Q${=nQb{4 zi)$efO;uU8&rIt%8b>D_A3A)g#})Hhl^hNDs^gHc)PeG{Z=Esdf0PW?LSmtdlTk%g z(Pl|(p|nTVC!pwZ==TGDGE`gL{Ip4;VLFXzi;(!t19|BNY<+CCnl_77``#FdJCkkQ zU@at0ibx}iF@Yb8UN0NhE!KqsXha*5K41w1bcd#=P;4YKGQDtvcQ1$Fu?eh2#EKA_ zaRkal+g*wpjXUwwLYM|zq70O1#_4=U&u|l^7~rL|c+^`maLTM-l}S@p@hM#v2aRaE znNV1Zh{NiztmMo>53EXKri58UFl6Hvtd60)R_TFlKfdMnD?_1qwl2Nvqhzoa5$SYG zj)qpY*D51A%St^p(~^5|0pcu2h)E9tCmU*wj5(th@#y}}gQw>dUUM=NrTR<`z&S^x z-QcHHgxYct)Pa`!=V~gZ2LteGEHQd9%W6RloKuPV@8JzGalU&4KeQ|-Bb%z!C@MYlw~_RVuq~Yz~F$PvSiR*GGG;%8Ixu^ z==i)TR2%MOOEau7aJS!bpECfSAT2#%%xaR-z(Urn1r4&#j<@xMwYXd$nz=^6Im0+N z)4NePp4RK}_10-oQs}wjF<-<|Pd5*MrMgYFhUXFs{vJH?W zP?7wQawj-ybhmWJwbh}K7>U3~Ox43ZHn5dm4ohXIycjkcGga)>TLUNu@q7!d78j!c zIPhm2FeE8=Bs}JXGG~ub!q}3-+5$n$vhsi!r9h@#vEE3^U8)EUofJ$?g*yFAwlSb1 z3LF;qa}d7Za2>q6SN6Vs=k_<>c5Z*s?Hjj#;nug^n%%12diBlkz4_ct?&ity9~}Si z@mC!`ef^WK{pz*vy7tVq`nAuz`k$`;z}4rkidR4H=p#ozdi1qN%2D=c_sYM$@-0`~ zD^FcHKK#YQZwEX6#NlfWK78a^o*= z{9iZz`y0lM0;mA^q3i$XdT_mb``_IBtvz$ExOW692!8ACWEb6i)y}W&eD_PA7A?s>Al@A+Ykmbs}Cl-(Gu|7Ccx@*#BR6*JlF(SUG;aTOThh2fYmj6 zcd`V0=_bJHg0>qh0ncs%tj7M`@e=UNCctW>-W@Fg@7n}e4Qjjo67b$lfYk`J>n#EA z*#Jnd8O~h-l1+d$!#PVpya}*oIC}}0Z33(r&RPPZO@KAS4VQp$6JX77<`OX71Xx|) zc8w)qvI($eX?+O@HUZWwJy-(9n*eK;)|Ps39x2qWeM;$0oE+t zUjp1sfHh0YOMtTpux9CZF9G%@z?!9dOMtZrux9CZEdj$#fHg}$y#$z>0Be?h=MrFS z0<2m39ZP_|0kHPnY`41v3^oDQEG;bo+9tr7rNt#c-2_;(bY}@rHUZWwEi3{3O@K8^ zx0e8U6JX8K{1Wi)O@K8^x0Zn3Ccv7dxh3FTn*eK;W|x4cHvrbY2kbVNfUiCu06}Y4 zc(4R~)doOzEo$6f0={w+V9l4lbP4#1O@K9DdUgr;z$U<&FFmsaoNfZF`O^EAfaf;> z)_m!`OTgdX1X%N>_bdTlz6r4AOUV-O{!M^2Uy7H2zqbjnX6e}y@XqrA*}|IP8cV=C zHUZWQ$1DNeO@KAS)t3Ni6JX77wIx8@1Xwd1y##bN0oLq5EdjzNz?!AWC7`_tux1Bh z3HS%+10ZOP(w8j({P}=vVa+wFOF(M_Airj~$`ZhB0;~lAdneEjfA`Jfo@<|X^{1~sd;2@C z-Ua6Y-gfldm2bN8;VU~=$iweAR1R++{Mf;p_dm4%q2t%>-`_9p{pQ{`>{US@fN$TG zcW>@||Bk zC*UXniWW%%5x5EMU@m(i*Jk69KdM>%BqnE7-CwP4UU{&y-(c^YmH8^Bi#dpP#st6e zj(5^AMuN<7T6so^2!_NGwj#ma|Drj$E|Q(dsd>wC};B}*dHjT(}JAyl$h+=(3XXXC050Qi5JkL!Pe?{!1Jisd?R-PD!6=|b^ zVUAZM*!$1S$puEnRVlv~%w`hbEl40Gn^GeK_amdGhdn77)kPCAh6G8T$TAnPU6ny> zGY&aW;3-(qS-J1Mj9j4rLr}wW$Ha=*Nw;N`$ywB=oY`>Lbv%w;kznt;b8>A+E#^VO zrZJmZX^ZMOpe#tK6=b`hlAKa%W_yDkOr(+lNIpAZ7&A_IgQ%O~cxHIR!RnAy?hjv1 zE?3Its40!Degi*YD<(DWR4N*eAV?bp$=hq>tU0+%ib2J6YCH}Cki}aeWg4oaO|m_a zKo(&S%Hv!WWPqd6REth%2OJ|AR4Y2A>LdmV1$0()R_^OBCkGcY#b{>N)<({0jOlXI z&?e@9CNKxLSN(&%Z_LS!Ql-hTKBET>3GCQT$Nv8phhI_jA&V!N~cfQ zxYpM7*_e0RwKmcSw9y(l_2uNCTqYB7F1PAYCvw73Or=S3plWzv)yNvXqKLh(&B+bW zuoDs;9UhnLNmwbRV!xN;l_1x5Q4<^FI9tRWnKp@0@uXhvxDm}ol?3FK`baWcy()XZ zztAaCNDps{XdANFb}rYQbu60E6KOW%a!%jqmmznI>U~Tuo>*f;8wemV8XsHe#0`UZ zji)@zl3rv0XEW=dm6p&q=}s}$a5XRW+XX6H*8@XucF;ky&}Yej2*g=*qEszV z;l)(rav9X+2!f=#rt=+honpt)M@<zUJyxQXl4O>K z2%0aRj47kVB~GWz5;kc}F>!U7VDC$FovcTvAiO^u8L%QYiq&pV>=9vG=@^4DK_&!m ztM0&{Ogu@S41&I8apkVvR?31fsMpsckhmDn)-0;nXtfjNMF&;IOx4r#vmBU8qf|k2 zbT(bfiY*nUCGVu4G|`GyY1R8E-U^xi%w8MM&Ri!#<5{iNa8Y&wM=hRC8Rhm2;`^9c z@91TyYxeWfs9a>3GI_!^2ZMp-wD5jM2g{i2t(Jv9e%%iLq#qL4`>i=oD~9MG(o;Pz zsEC$0+JlXq| zIXNO})NLNc^Fe-?tvE@PtMb?^5gJ^_?N(3{$X%$cp!^cd|MaDaxh*FVA&miq)QqMP}M-jYJ5OaV1^Uc^yd-!Gy^^nxa_*SEF`UgHJ4< zZ+5V*TJHBny5_mv^+gDo>+HHEIEe8^VGOnUcq}CYsygaV#auRl^g%c2%N2{T%o$aU zPDoyLTC6{;yK>Vb$bPiyK zw%iU7@}z=DBR??-P3TT_v?`m!H5+~LT&LFV>dJVm45fa}DMsCpkpu`N)N9VzRTMsG zg5m>oPUCtke$tg2AWV~j#_45oHkkHTpC8!!Z=dT-@N%bQRy2ADs&ImnC4rwnbs`HS;GA~b6Htg`OAED`7 z==Fkb1;(xBKuHrsfK>XvOP*K}IImD;E0(N*$jYm_maEU| z6>(tjRHryBVZb#~#k%v>d5!0{8fE}WI>)`x49vc1o0Is_4<%Q2ov-}yaDVj%6(_oX=L7q5zV}|@dw553(JEP_s`f0+V3Q>8l)5biAHhtk)=Cuxyjh#(J;#Ue zp>oD|ewwB?p4wXQXr702hJiT$J^B2tdA|7rdvl(pmv|oBA=pti6V5Xy1>q$YJ`VvR(KAY9`pkS3-~7@j zUcb!C!Hc691fYocqhA%S&*{B8ng5pmY7@!TZ{PRdaQeO#Q(wEoEA4!yl=V>+=2A(g zXBe#-+IPxK$z`iDI5}n;qCriBUMG5y;fnu+s9O2j$|sGiCP@oYcstUX8Bp6??;ooI;Z!JubcmV z=O=9mcC@t6&$HB!Gy;NyW5iaur)FwkaFbs`3#*@<=~9(%hsVs@?5M#+)vDTwl-`N zfE>it_43W9vpLU))}c`->0!;Sb(wxvGEFh%j;Bp3D~35_D5x#U_RFZ%Hme;PKI6H% z4n30WVjXI}c22hWp{)n?O{dYE>_h92Se}%4SRAo|h6_2gVddo^Ca}E*HZ&)+G;&o1 zl!7KA5`Do59$JSU$#k&}(fFKc?a#Mn`i9eR&h(*mh*vu(Umymf3Y$=Fifqku)Vb0q%7)*N4dI$d3dl$y_XiF7}MWSL<@ zCMsN6oq_t8#Y|RrK#vOrMTEvmh<{{HlSTA=>PzPwpW3txt(}Jf zAwHOMeA!xgbZ5$mN;u1Qr%fRVx}8DjHndTWG>A?u?Uu<*0;yy=Fw`E;o(Sp#8|J69WQ}r+qnJ>}3`10;ad=## z@=9(>u)6xfQ}H>`^O1ZP(KGwibH2G>++g^1zIRSXbG{EnPcl!bRTNZ8%jtdx@0f`= z=_dq~3YeKih?10_=E`De(DBDF_~y%^=OcM8qGx(Bu9+{~n&+EO{W;HvqGzpbAvp+( zNA-5bi^Ex8%P_7oj8Qn6Xi&M=&tOw#+SMmP@r60G@)Z9_vWw{X)|2_5zV#QjCi|vS zZ%+21=s7cEq8i33PE5>f%`hkoK?+6;WvnXQ6!j(pw{ztb=#zxMFqg5{Ud z^N|$i+&=ef7QcUGTZW7Nf1u>vjel_TX9v&zf9$<^lp}dwADDM$-80NR12a86HLycj zm2v98TslKngcOQUNTHr?5w{`~ijWi$3Wa8-nl9$bEXFe&HJHl`_WG^8yDY5N2A*Lb zhJCDg`xcBB^9=TIEYA<~Y@h-201qteFQn|Qsw`z^M3Ft+Jdf(1tW5pB-|z4E{?70B z^8r8nH}Uzz?(rmKMETaYZQhJA!<~Pfa*CViOb$HkBY--|a|eBJ&b5~ONzXieGlrWj zsfc2Xq~-TxILCTMlo*riI!2VoSBB86CB4^@Gw|v2+czxm*XP??V+i08a|Qx{xLvY= z$N)lh0Qjv!ZmDyKPC|>GjKCxX;V^`P;%lji+{1xxOa!l9NzUehHEEb8-fAIMQmQ&? zOG-NKTxeO`h_bOKCNMeVMa7h+3(_f$nrTI1beBxu;#`s*b&5s_q|2I^V^1W%b=XPy zs=NpQdcSNH7);5G5keI#yfUMEW3D)x*Gop($SYn>i^LKZRvplfeB3=AhmLs9+q@ZL zbe%s?qB>k!R;)5;EYvh108xZpMjXlxSrAPcOB#L|${SL!ZXA%dcbXy6fE%Jdn-Y^%=su1(+WxHBUi`C6Z6h&WBR@TkSE6n#Ce6V@CGYyjiJJ z19lXqZ+B}+gPFHgU%!>nGrhrKy4!|zVbJa1(;4O%nZw+;Hm;Z2gR*STL_)7mQW_By zvXT5Qta`}K_ts;3xd3K1Z^jr*7w31#-$=iUvqC=$#_%bU*;5xFuaWaDPm!VHz0Xw~ zH-SKK28JM{52s;38JECL-~K$}nb@oAnP?_?re}IhtBG|?j#dseP0c#>B!naFTY%TV z8@aaYT8GkM8!qOc65!ARW`-=4g6mBcXUSA)+TaCbU_0*Dq|*?z{FNdjI1Vxig?>ZM z+F_Fz9>}InLt>$;7TAElWrL?bDm6r9s^!USOI^&7JU|yh$1`prXC@|>Bl2b2=3sf$ znZkZmu9!le?MYo<(FD@UXsN?uKALBSRS(JfhR0UunV*(6<7@~o&NJb4p7}-JYR28G zp7OMZ&!4Z6n|ThPtX&SXsAndnxY0-`4JWfx-vhI6Xf-DtkpTy;K<5xU%Pzkt+_s2Zk?g15hY0nK{&@ z;Y&Y*&9)$gwOf8kw`RH3`$f?uTv(_XSwX*#I_t+Z)>NYo{k z7*2$F^3cAOLgsiz%eZi1)R?zRm13h!4CG!LGP5PysWs?;wgL!1;zXqB4XYmV|6Tp9 zjrzvVZCvxOz7wGS{pgjCUTIx<>hkYj{>jV7m%Eq0^3oq)`k70gx%Aw%;-$ZJ@P&h) zJNWF?A32Z@kb}+rf3^Sp`@{Xr{{G%CZ~WZeKiHe>edFHs-Oq!}|M_ld_su)Mweur8 zcXsMK@7(^K?H}F#=yq%Ssjc7N`pK(zwvM;DTVJ{P$2Wdq^Jg|cv-#ZS-@01g_`;3v zzcIX#xv_u!m#_bW>yzu>aQ)h~e-C&D-wX_l@L6&t0DOf)AUbozp~n-V-KVLJo^2G0 zkX5dyf&x04jnkzX4y9!2>Q}Ey0N91J(v|c!2N2_WI*WHmW&%!p5lMLAo(rpT`TUA}&7>iX3K%t~l&9OG_BEDhK`}0+a>72>8P^pk> zHaiJuG8mYqC3-LgD8$TSpd~Sssr69sFb-}0>8gYchqMo(3?v&4`II>+AI25pERl8W% z==oEOG2}GnP=LJZ_Ns*7WD=}5?gVuPEW|^}x$B|7QukWXjiaaTv0#k7Mr=yD6|F4Dz94)k2p7+}c)zCatpy%l|o$~E#< zuS}9NFPRcL01mBHU=mU>J4t$it9P)H#sul4U)P+S53F>wg%MdG3&kX3T5yG;nq!rn zpl!V5ATtLhrGd`6D4J}*(9VjfMXmZBYc>QZdpw+)&#N*oz?wUBoAk8fjG58|uT@Ns zVGy0%Ul9PRARybw%!VbGCFaH?tB7Kv*aI_X18S$oRXu5w`HET@5iNe_YgSfOi^u?$ zO2ct6>Du|B)3v8%ajFz4H|H{nmTfkR^oX441#IJgT9v5Kc@Q1#kIHllDkt>97+@&% zO5oZ$KUvTqh&#|!rr+;W6Y0IHs}clX76TG-Mn1!3@(D!pGx&r7(`bH?%5rkks&-0k zHPL{x2mO;0;O-%x_w-t~t{0TL>d!jesZ_5IOAw_eT73#1^7ecRV$L>+Rf!VZt6Ouv z$O#B|WFGptekqUJRI$w@lFdck0&W60)}(BT-uls1iRi)AJHNXsk(;FwAXwIGWs7r< zb{0W9pUkiW2@;f%fVSiw?9>f`FPg>8+(`*<*6*j=NU4HPD#Q$)3sk->C!tPoG=<8pHdfs(Um49g%clF#ASF=oHR!$Rpoczxhj#e1U}W}>`BU_5#IxpM+ic7A#x1bTQ8QOuH3J2 z6rnZR`=4IY5~-;cS{5>9Sy}XksFjzEadX zT#DX)YE>dp!~kHI;Y!KOtib`oBdR&+728R|P&$%}JF|-HHPZqL zo})XpW{w6gWSI&Q$XYs&YlcqdO${TnbFx1?_~}&%kC>ud(bf_GNUhuPEU_)nP)(n= zXeiAWTNuDN?PVs0 z|I+uaN=#CDnav~lK2DZf za2qZ+giZ^Ph_+?W=2=QJ%43Xj%YLus^{TY=bt@I7hAygT)t2XhT3{L#3-UY4U`Rt! zzY37m+;P8UndT@VJNwgB31g^8y)H49>qg3GLq#bGdnvBw>1MYnYON;kBXiFNfPVDe zcdtqeI7T2S!6$oG)^fF^CAMaO_O`@eL8;)@GtFK_a_rn>lHR=f0 zQN|uObH!|dgXZi)Ys`Wy50e>6z|jGDc{Q^uay9F*&fLOjvVjz5(vARk~xJ2`=Ms(vt;n??gRTkXBoYOe5$QofW#=w>(VsKunnCr85+Bup%=T?fcp znNiRM%TXif@OvxrF{PWvVRSIjC)sveOOn23DD70C1E_!5QKRZ$8JwV$T9us*_g4gG zF=B3a8moFRgL)pIMpjz28JWjxx;ULotLj+fZNJgbnlNlv2qMKMT>#C8*EZY7fvdatp3W z6FEemfFuNuF1FfCuUyglSu~kaoJ-%aD$y_(&2ds~H05r-m>Bh&7FElF(A};i^->xc z*ihCGT4Ov4qhT^THCb5v$mWx5tPk8B>TNfbDkpXhA ze>J@-k<0OBjRbo{-kLR$Ogj)qjbaVYfeE1@xjr<3CxEaP)h5}Ef3Yl~)d1nBJ5o8) zHvzS9;QJm3SL_;TK!4q@Hv+j@FJwt~qNvdJn^z?Wc0NyZYWxB*y+sNI`FNuozz|J) z6K6`=k~09?(+WVVqwKBKkx0w%C_G*C$FMlX6(!+x_(~1sWVJ7P5|jxRt$-|ys@<7Y z-2ADPjuc?}Q%gujaq|l@3nFNFewJ@prDn1_ffoHzAxRH|=0xY(JIbmAZfHyzAX-~G zHp+?}F#}RX)j_VI_Z*Q#tH?BiFbgq<6pFj8lMnPg=B11{x-^oBwcC0%Mq_o#;UIbk+7Wm4j=nBefB}$TxGzN_`|kg?eKyjcx4U z!>baoLJ!&6*yMX?2S9wIrFlS%omSuJu%f^OGH zt=lL=csJdi4G{+f6#8RqDx`u1)T&5(zxYy#4B%}j$iKZ* zp$H0sVvrPQG2`Qy2+;grw~T2iq#F#NJ?7@LI>c+0xDdEwtaAB^LfM^RtVV?^=zh6DAs_? z1wR7c&*xwX;eu}X_X5CxCQ6=&U@mE7=MA<9%2rj9)#lV9>(^Nl_{rE25o#b3gPDZV z$woo*$?Q@GveZ#-;S$l-noMrMBa|tjWT-13J4;j zSSs%3u`8u*@s&BYtL)t)0iZ$r&gwn3mVI%9)JwT zy!yz-luibt%~k_MwykSrvE8xKeQgFZp!IwP4jPFZKTv5t$F*v~3^#C+1F;ItezopD z)XHCTUMrslqj_Dm^5TM8nHb|;v^U{9Z6BNy7?SI$%}!-qD|fA+)nQW-Zg8y+pD%CaH^ z{G>Hn*UI^Vo(lY7kEKCITz4`lmIh@7q?=46kjJQ2t3+4!^^#ZWW!#77<*&Od>^<{Q z;XRvYtbV6cNnW)^UM)y*7QcC8bk@S#102dAV7P4h%f*c-89c%*BtKjj0a5pI@x^E2 zh%&=PR_^u?-rx|fG^o>M%4OPklU^>O)scjNoAgl$lDk#2LZoX6kmRnF#)MNXQ>f!Z z7+Ns3V#4UE`Eq4wF=<`18%ZaZ*Ir)x-hSRdK8^eIs#Uf42s!Wt?p=btUy1s^rg>h<aTU^YOEsU??1 z!eD5El3Ft=-#6qej7jx8F3cYu%XoPHzr3@xv9q=Ry?cXQ{QA#d`<|=X73}in!7se_ zhyO2G@TC^`QVYDgEpQj!`KYw@`q>FoeCne1TcvSb7|!_)*B-TsnJI#)s*6&oJ7GvV(*$Yjxj}i522J+w zUx!V0QTxrj3)dN#mkO<^Xvjvdn4>Dlx>1(pTjd(mOSLjBQB|{*Ih|C=05e2w>UT5a z#CRS6{E37J>j-GFfA>0UvJ2ZhRx*ldasr#1F`aRlMre$&){V0KxZ;)ttDWwrJiIJp zRzF#)Pq|F4$5mZeGO$`%&nHa4Zv{^F8^8LxYqB|cQI1*8O_*VBC=YYZ7Ft{g?K!3t z*6jpyoL5RxIaenah**?!`W%qi&R|R_LIN+<0s3Jsrwtnb2(v=l8~^X?u*ojUG0SbE z9rT(k04$W*9$hu<1;9sIvw1EEXf3a%y%szcrF4y&nN|TCwy{MnmDDO|cP4V9zLshw zh-uc|_|4a0lU^@*hYR)DQ*GORHsli*U>P8CenK>Qhp)Kw|j z!h{*o0{9P-$~YOg!vT~xd*e4=hfQ`-n+!r;^Z@rLXSLV{+N1apKV^w^O_ufh4x7#A zAZ<$U6}p!LVG^TJyGpR>q2QxX)m}{VsjdQ03PF?o+Uu^#{0qA_C(8R0_L*Mhxz}98=u~|@sDq`uK)S<|Nc68?N6@#z_s$#KfL-qSMyhX_sab%Uw`>O zUjEGG%%$JDbng;&@M{MjKRDd~mHij?zk2VN_CC4yb-RDG`#wno2x8Az>e{J5`L^ggKh>!YV?|tRY7Om`l?tJ{^n+ZZeP!)2g#f8tM9Bn%E zd~RX~wuq^HC{xJ~Q>9#sKo^iyx%Yk`wz2yI?>!eP`Xz+()LCfLm3udrvi@EiSxGo$ zJ4E3$l4s@K`%B`^-y26324h2$ z8ZnQ_dTJ@_d*aAKVYE?GbJ{6c?7jB@S<3ErM;IQW&8VZYTDQQ?x=4yjBQ-b5T!Db# za!s&w!|I>`*sIs^vd!Lm_fqWNjUcueMt(~9rd2A5PS+<97clx#X`z7`AZ-G2x&WyQ z#T(;hU1RTk#Zv5dMG(t1=v2M#2p~GAq9T*pTyMFR@&XexY%eL)q@FYvgpn+Yfb9C- zmoLSBHiFpn6kFgywx(I*g_lY@{a$O*2a&Tp)kgVV!Y!nPp;>58dv=Ar_pYVb`w_%; zMl27~pvSeTITF?6C~(yNY$V}Uo-(@<-mNUyhUnq|Z5p)gJC|a=^L)_qI1uRUy>~3d zen$kc{FqqCgA9@k|Ej)sbp>I;~4YCFfMf<$AJGx%YM;wz2!|=fk$Y zJW-s5{9d{Dwxz6Zizf>zR`mR77~%50w*pz@?q}l2!ZH9#o99mXQ2E|lmZCl#M--YS zb5!XxMREDwo0p>gP8?B4s#t83r?G9z_ujM=^{F_b;G{6pTc;;4_THBPQOfSOMi?5X zI?oyPp&^uH+m%I|v}Q9siH&U2ih#MT{L*HkUd;G98}wM2m1g$XB- z#e7bpEu=cGaQu|Ew2oP1@7-96J&GW<#ZxGYbdq@4s&Eb^4Ora*anaePud-f48Z+#4 z0oPG;BxCHo>r1hpj373TdC6Q;#I))V1cO$s$t>ILxey3w^@En1%M%nurDRv$~Yz#nF_V^=9v3>-x zF`!e~;}0*z&LW77fpf|pe`qPziy$@zHYt0&Sc-Me$Igg@C(0fNOR-J_u`wV(+2i?A z>@sPnlf9>C0`$yMi*9zCZ?CNh`{o$)0xmvsWjw}E1%0IjEEmvs375ICXf8z4d z<<8~zU-~bXe)`g;0_?KHp2lo%819<f+L%|G1ysm)JqKC}6OjX&M^*$9^h+rJaxOJDlR6SlxI ztwc8}+Nd$0n5I|h2MsBaoh~rNQG9h+8`-#&U^~@*K5Ge>ef1lbB@8ZaSIksG>2vIi zWuaovP^%_j5#y}UXwqz(eJwD$d@qS$SHE^uqQ1!CWx$o<%={8&jmQ>TXp$h2O&)ly zMUn(D_Elae^#^nM>MCa!1cpkTB+~B|kTj)qi^F82Bg+|7X^RQg_nVcprt^9lU}vPS z{MThZjn43{%l9Z@3Ud6r^V*P3x;3IZ@4_P#>j`$fi6)^*4dDDh*H;y13WU%FS=P*G zGBjk~7!8Yy@r-6tI+d#Uo-4WJyfAR|ZhC)}NTf5kQAx^pu7>%&#sUX`?S7HSbyF## zgU$LK0TbFRXL~(HyYlTT9ZezOSJV8A>ko-^UB-)Oy_#;1(~__2Sp%9N`nbZA*^x@_ z{XeS`WL8s>({f{odp%K@3>QYnAv(x3$8_)kHt6eFHqW)2g&MT^C#w>*pxj#Yq>P*5 zm}zo407QWGgx~D>#JJKY^36$Q+)gkcF_YfRu1ZvpuE?}5G}AJF7^2gl$;oW zfc0xZ4{x`N$!Vq7rMI_NCCaMY)F`sj2>^bQzsNKibw%TvW}exv9ehNQ+_XBl=*Kr8R`Q6FG^^{SM9rLBC?w7ZXj(+X$)@7EBi! zwM&+~=ul`by-zN681s5jvgQUzh3)myRw753NG?B5TqKu_xb1gquBq~ZSiMho@r$cTTs}kdeBL{GeR>tYH6EIZ*aVp(uuRPz9d}wc#(GK#u9hsTniv(O0bV?AX z^St05k-FXV8TgZH5V)+MqH_ zHknqCs+IWk{>_zDnmNy7E}j{BrdFDDf@Vw457X7UTkAs!yyV(r4rDnmDBR!s$yEu# zw%ly3OVBW1(^fi zsudD|W^DxP#lfsHHuPM3F|F+V?5ae2PH`QRNapLt0wOCUlOMT>L`IUUUCk=1AQdE4 zWp#v^LEGh32{x?RD7{Kz;H0bHzkX#6x0q{QGpM(^ai+Fy>v2mDLz2cgT#Ut)0&1%J&Kbi3RIOc z{q7u2iRq*|MYtqmHM1M5w0WamP>a*9_B<>o*wyyYl~onDT4iLv3uX>#=^oGD|;vYQA!chddhQ1tI9T8kV#4nECFz+uu`eh z%4z_w!(r>Wbb4Od`^%M%==2~vzkE`n-pS6B{d_~8`Q@B%8RB@9$Esu5z_U=PX0_)Q zgJ`VOqqpgk5`_|yZ96?KBWS~*%TEYw&>$LJI-Bdj<0*=1^Flf|MTh+5lhK~9;4U+B z)yA;Mav6w8_r{~HP55w^1IvH5m`Ed1iRfB=eq#l$VB~YT*0?O;T-IjIKHfrEo$7OB zziLkte7=SgeT-`6rx;N@IPt+;9c_3x0}8{=C#jpzJi+O8_d>bkYSBqT0p}txBX8hzpVs zsRcmFkvr7|S(YB{b^1l07Z00PUH6dN3ztya9sC=HypnFd+f-CB?_i%P?p8LbSq_pxOOeZh=tBXp8t z4X8Y|$3{OtgZ=)P$mZ!{rHc&9=ybv$&0&e({Cle{!y=znG`mu1YxT4{SpYiEEJ(^N zP|Sp`wls*6lz|yAsZ2{+VJqn~J=JWPbTyyA#Ic*l8v3FP^5^plmLfbeQC`%h0@?`Z zJhb-{E2A7??6z3e>LtpeQK4|Oh$lL5&a#K&Yzsm)tIi6IDkIXvoqxJ2G0mI#Vt+vN z5(wRO8~J3fKbtH}GFhaug5`GcV9^;vJf_pz|7ulYT<n{Mzvp1<)73uQNuS(e6Mz=%NmA)aV*|9OvyMWR{t(1y2y5B?Ry}TF@*f`rC z+q)|l1e^AeFBnY?C#*`PB#aA{kyIIssC36F6XVhVZ4Paamz>SCH&*VU>(xeLfF|-d zQXmGwpfN6Hrwr{o{n$Wq{V%Qga zXq0BTM24b^>50JCv+cc)uS&?{9@-sXBfk$Vye5thrXW9gJ}A{#ej=(yGbv+Pk~9_! zzq#7<=^_JQ#}vb0fLO2v<^6<776wcfs9lT}Ev_)0%;7P(X|;D(STK5Dr4qk5&@wnMg$_#+WzLsDnPoxX3+i&s#FS|ErJeAA-I;zXn4B| zPX|+)nkZGX8FY%5|M^J?s?-^Q2nM4Cd#Y3f(Jn}Lu1zLA#L10(r!c5ahtoxVP{A%^ zs}j9@KGkewyY)_u>=M;MdNfkIvjp6S`lX6RpiM?W5hGDI+MB|nN0 z#p^Rzv?yB_X|OkzC98?a$-ZmdeM1qH+^*`m*A1%J>oGZI5h;dWKQ#c{2P zkP6ZP`?yJs_4l4z;{R{GYvbw%5AN>X2S0r2^Q9J8wZO+`$KDc~J=nY%c8h`Hw7WWBRchdfrig^Ja|UdU2kYdffA#eQ9vKj6?iP_^VUiu_Bxdd;eitk1vvUELF~c zf9Q4yIQ%pg+Of3Ao`(OAJa`{=3izdZFQJWrm}RwBILAXJ2<84Y20N5Vr$UmEwS%3c z1OZZ00d>nTx`PrcOw61JfGBaIma)?IfVve>t!n2m*M^>Wu#;_y>~tDU1)XC|t1?LC z<_=?w4@db!2xVMYHAfJj`f8}Rd}ewmFZdpjBAsmWu*@_|sTNn{0xdGW|EH&_E(>aCkENc`eiJ} z)6qH#koC*hxfE03P(ODWdyM{W>4z7yjHNT7uvc8h!o}msma(;lKjAV4Xs@A=EMcu@ zFD5nv_$E7bNTib-?C7!sB3bj4lB_75q+<%$be;uwy4E52#GWkzh@2GO5k;A)+p4q%hb0l1``h%8NN-C4&m zQpqPL0s=tQ?MD4peO_o*FtI8!c5h-L;|AWH99Hn5KPb%yral$&GQe|6SCC%1GZkdh zu1p;|U@OHkjs>?Igsovr-g|d{Y`3`c`JKBv8{41T z{>H6e-CAs2-~6FX0topceqOk8{Pf-y{Z-E@yYD_fVEGZQ$FsJ9r-6QM#t5F4j^7(v zb4}U(ipbWi_R2S7#8N+Z{1niFUTf@r`T1e8YZyTuge<3EWIWGRPsi8m$M0DxP42E{@WtUf0zX3K?T-_a6f&I^anoywWjgd7yG5}))t%FJZ%|KZ=slOqzvY4YWMzFhd{HD+vQ1u&;tvMN)F=F3A zrN8W?($~)qBaXC^p{G(l;>64YbKZDj&b3#|oEWk8)8jV)Yd-J+W%p`iYfko{k8Trh z#)!nXk8c2_p+8@VtaOb(#|YBbj;{lap+8@atnnd#juE$S9$y2RGC^g58V} z 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�u@&b%J(q0n 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�R>}U~`D} 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

Im4-#>JFJB5V7^rlXTC?(rco}>8})1 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≀qF9gHazJO}b2-)VLg@h84U^ zQZSAbeI*|g>mudYVIxKa_eaH6I9XyeRVZ|7k^ImnYXLkHl7sD3xQ}_LE)tC~3g)>~ zms589zeNa}+j#BjlNVnDz+*qId*Btst~JS7`gG{Di?AIhmW@6DAC|M!Mbr#C8MT|3 z6_dYr5j^AYZ1*k#ksOw@vk3E~c|%5I8Cw7@awj#z2~piagBfMLCU@GPD=>nKC{}p?W#_f3sy?9@e;= zKSLnF9G4WqJ^)VPC|j;2b7G6^YvoaX*vXe-BwlJ2%yg0V8^dQ7qNE^vn55fKqC54wXa$#uj*=d^pmly8xA1?R38{Y2Pn>^c4w!_ zZgr$S$h%qVNpkq)9pmu z-QD)&qt=7mM0PqkL-hWaG(FHqf2fw^D9XHtr zi9eIAR7J|?hJ0F**#kL_>;jh56s3+W?}#O4-@!E_yOdxyyHc|Yirs$09syfOtHq2=;ZVCX>6~92;aq4KNBb$E-pVu+okY=y1)5|{DRz)Z zuGopxkbJ{?*>=|pq*;~B6{!Z}iMSpdc=J}etg>h#GMnAmO$T=ON^Q4g+ubh;kek3~CuVm@ z_%OS35zx%mC^wPS4xb#lon0QVqn0_4I#pfjOZrdBl6scZna<&6wnZ=<@_Hw8CA(}@ zl;m1Zx9!pk`V_RKIhiuuesQp9)(m^>2-v@M#*c@!dm?WCcZYskL52H&A=H{?Qx$4@ zwWg~zoBXA+YyDccW)Cib3CAg)10<&(;Ch(rbd+Hb|$2NYg8J` z@uEa%mp$C@XBa+Ct00(l2tN~sSK))mYs=NgzdLJY<_jk7^RB;j~6NRYDG}SW>H+Iv(aAW)bH_W|Z?$ocIdgk_rw*6Zl z-KuRqxcTah?`^yeWcz#ndTH&$YcE)RX!U17w7Fkc{?FxKT0XP%2TQjs-m@53xPL*I ze`ubEz7HAD0uXYtKhpUpE=#$yTXUyDK-7d_khZSC2Q=M9zKWUo?CJ5A(`XmDE3WT#Q&uL|KDebY)#FXT$JS#PDKBQ`bC@1)fchrmaVk+v^5sC79~V8bR=b*_;~u5gq0?A_ z^~LCZ(=RposAv+And`e$kJmvxwjR4kj4^XP2lRN&d8IB=Wz1aF57y&@`xjF^UbXpv zrn|_nF*Bb%JzjAd?IPR8^(vDC$)?G6p2E6ykQZ5uU5*|fJV!Rw<7L3d)?*jR4QA$Z zm>w^I`fNRRk@H~Y`YzSuMNp5e$1YMJ%v{d_Jzj8Lsf(ltGgtM4_4r^CvZ)@=+k8ON zUF1@jna@;@jq?zw!&Vy?*$=KSQt@K8$>sYXZ&ZQSIO2$D#bIhQ2kNuc#zj(und`em zZSK4S)MKlSiyR3v*Yj|-x$~U;n!t3_Mb?CwtNMXz^N93u6ScYXr)@r<=`NBj%*@A8 zn>$|$>af+uMS6tml}c|QHT5`FYbEJub|kBZkA8=!&7HS{`fRny&z?15=K3yGn=z=z zRvQ;d6lSjHfZB}g*96qYMOuZKtNOuebMR#0RBeVfAJB9c*%)T#b5U&upblGYTx?q& zzbNN8F^8#5AJk{7jf<_Fnd{qEo9X`lyXUr^uy)tdEzrA9)`fpe+R66-*q9ky+?YAh zA?5ZPx~3*ItE#<4>-XF1@D{k5Uo)y^2ka`S%{usZkbER~*aESOL1(f+?7~tffA3{5@SjhT=yXfG5Wx>9OY%HVu+rf_!Lt~DsRQO1JF zv4A)5!KfV!t7fQ&mXMrL=ex*duas(dEqI0rk=b@fVFx{M(mH)6!%A#C930U)+m_p? z4pKYmdLo->Cki53r;9;)97&J05~>@N)C;HKW+05#a%`JU^W`&Y9LvpgGQn>8bYrG} zabw1XG9CY&nbqzBhuPha-HzNEqKl20ACn~=zcKR&TYPL|1`Pc_&W)L%KkPelxS`^F z>})=h<;aF!I@|6Sz^+NAY>csbrX!6S!@`gs;DOjUz+LumQ)kLeT4?l3Ws5|>xI+!K z0oECn3z*OF<$c{zG?fU%6H3}Af>m@SP$^TInva|b@*y6qr1$z+saGa?Og_m``Ekjj z5~WZyGShHlH{Cve(ckS&KJz2!WT)A)4mZdTJKWe`(aGN3-p=I=j%;L%4A2WojFFl-}HX1Y;R zI}?c}im|ZwOb#7X(9Y1`3Gku%FyezF;L8d)T+R{1NFW_h&z>m_bDc<06f@0%mg0su z6Kd5{!%(b^MN$c5+%4sG#eZfX<}EU6bmKj5zK8ah4nl+9;gOKqsU^i0$fsEh2^AQd z&GPJ~1Izn4b+>C<-m9vR8+ARo55519mUoCP+5dFl<2rR!e?rzVF~~kRx1gH#cK;2C z&11sI{DCKu9-+LK8ioB4IL0XKYA#pVkx<~s^R`}Jk4^_inlvPHqAqu-cn3)qXo7{| zF-SMuYS)v35)YvP^YUl4{b1gZ$0eOUYX&Rn;s_21Y1zmPjIkQ%_0usmo%Hi{<8~`>yIGmBHj4q zk+ddMZ+Ag*?EuM@(=Jk%yCu0|)BE=)+RyLOE=aCEf~1Qy=$A$E-~QAl`JIsqk}C&D zE}t&Xo>JW{$z_|~zi!#jZ-3kc$>m3obdjF@@<^^vs6Jt4lDEEk$tL-{{c&>1&2Fee z@{!BP#}AoHHq&ps-hMt9otf|HS1;Op&+hYGbhDf4@ZDdAKAz{I&G4VzZ9l*M<(YZj z^y&qhr+=U4f}7n~hv%icAICFYusMF^J@)gf@0*$Fb+4Ycnclq5bpEu9&D=dTwU2-u z&vD*n_oWw4p7+hn@tRjdr{>lij%)VU+|B;3LvcDuJ)WY??epMd;j8caLe2%lIlC8c z-0#KZ(+N2nPK~p0LDOKkQH8~*@Di|QwpC0Oa}GuOYT5DIij(2! zvsc;-Kl`L37(QZ~^JTa0-eEI*n5Dbez;+lOV(G{3Y)>rxQy;bYe){)k=6ibgoXz)P zmhNH$+~IqPr60?4V(FilK0iD&&zp9C+UEH%OLwt>?(lq=r60?5V(EWTv3v9{hch$1 zZug}&(}!8Qi~V

2xvTcrBe+`o}(DbNut#%p9-Tz5Sr2yV#OQx|M8B^_XCSZ_2@yr`(>wh zhc@4bS-Oi2Mu+bumVPYHiH-l!#MSttznq!pO}hh|=ff=B#fFT-^I?{LEYpdlzkf27 z|AW}fOt0JR+e{y3=`Qw99Hx$?AIouK>A!!y{nmf~O|x>e_y4b%`_SB}b0DAJ806>c zgM51(kn61pad78+z2iJJ zW#HmmtDDuRF0~bAe`h*z} zQwAh!T->^F zvsO0?ga0;V;Nm`pn>Dyu7(8dnz{Sd*nGo$MWrJI;ru3|y={o$RfQJI)8E3|y?*oa~*AJI)8D z3|y?roa`-*JI*5xgW2zfob3IM$@%~J56x{3*3K+H7l6lp9_xYsVm$zkO5T3)sHBVG z?D)0w14kw8P$7rihycgKZ@;0|>nhbtPdd6-FF*Xof?ELx4uc(jRPxxxc>6fj@wXMi z2d=+O&U#%EM(T$-DjBwg{iu#g9!C5!Pmdu!a5Df|zSiX&l@z2g!pRlBkCjULpipdP0%tHdn@|!NYwSJS zlvTZM#KDm@f1${DLHO6EqQg-=6e>|TKMDugNVcP9n4Awe6V4FDfteUtvmKS3G<|wh za(wZqqziRDG0Ou-*dAedKg?0dACq-l?or7{DDRK&s3hVIdF|M5hbnuy8Og_Md@LTX zC#aZzoTEDoTshMu&Kh}@u!`WkVuI~t6yMp)SN2>gUSKmuf+F)HL#BjCG)`E9MqgD? zIjw{SBr&A=HLcU-BaA_3+rB{OYy?fT1eiFR8wYA}BY@#nyhGHdUEH2(MKx#4Q^(G_PFZiMDyhDUF=sK{uy&IxjyW~ z|6yT%kKGEhnIHcVJGcXCm%8N2-KEB5kMoZ_hkThI4u2RykBlxlIupc(hHi+kx5)LC z7^c-F$~t?N>?2x|)5ywLCA-oHPI#@$K4ag*C3eZkUXy z2et&l{0?nfvcvqkP>_?erO+8zs0O*3;|3MSyZOck+dzjm%Rcj}Jq?{EL~*7vq<-~8_8XyZE@gY|E(_tw6(X02XWZLfTD z#a#Zzva$5_C2jF*i|WEx7V7g~nwOw2Lc-kVLGh!1&fWdEMX)*VYH!}f9`^CUkIvnF z#e{&Ht$7z)*s~Bg<(j~WhJQMD_qJ;SC;SSwCInmvJ=wDf=kDHgP2hxI!N!Du3&lFw zvk~X+UY`(fb2P%mM$PdX59jV)b4}nxgGZgad(|}o7aK*h^uWr5fSVmS7yCrB5Lk9i z;Dlenl4}Ad{0bH)1YBtL$!-9iyL-VkffIfO^R5Y;@GF2O1Y8I`=~po4n!w4vg1hdR z5O87JC%XrH?yhqa0&X^7Pjp@V++9EIn!t%JFP^*WrLGCM*s48#ee>L1w@(PT*?~Uc zS1@)>;Dlen$Tfizeg(q`0T)6~b_@R8T?5wyPWTn{Cj?w5*2x|LICocXLcqle(8eQNdh+(im)^IeZvXt!Qx@-E z+*u57KX>b2x8AWOY+bqe;m!W$%^RQIc-6+U*Z*n#jqB9f_t)OJCa*mmL?e=SrUM86Q2KwOKZyxg} zKapS0f8-}p?PHZb)&nQk1LRI`ZT>}1U0Pb2TZ8&X+z;{}%nxPCvaU8IaH;44<2Cm4 z>-MFs2aJD-@0^~BTb)~lo^iN17(E!beqY=@U*Ulu@Qm+Vzc1*Byj5`MXkPdH(z|Ry zzw|D0=eqp{UVj9m;K5CwOBk)+VGH_MyRr9dw#l7qXApOAOZF0Ro0o{&`YO5e^nG#H z9L4Tnl8$|GAO5?K+q_=Lfaiz*p-S#Nd0*U9juLk;#{4DX-tb$txa$7cf@6zq(t#NWk+7~AuCGKG4`+auz|Hl_?L4Es< z?)~3;Ho3#^ixZ9#cQA;}CE~tt-WDg2wzx0+1G)3UeQ_^3LL7XwRowq;FR;a3H3_Qo zYyXSfdBMK87ak?Jm4!19^a+J8E_3nQCy|$olJk5R@tK?2` zUmSasxT6*2{`f7nppR_YPvWZh4l@;Jk8wwe`vI?F{PegFEW@txehp!`w1jvhF$sun0#R*8Mj=(-!om@7d2ApGEFu_Qhq7;`IYmcjc|Npxv(hTzMzC zliU}VIzrqdLL%&|?tPz|G*c3Rxcfd=CwGW_aYsc;dc?i!OT^uqw0Y(J#uj%kP42|^ z#U+knceLu>_q+BT_4{vmz!vwrUm$m|eR23v;*M6```++PTig>dTihEikUQahanC+N zocCy5yZ4JlAn2p&FN5d3|M*&R2i+GJK3p6eK3e1MeTy9&8i$jhQcl4{cH)aQ{x%YqDuj1a=Z;(6QeQ~~{#2sz+ zANf7l7Ua7UJU{Y#KSl0%_QlJN};U*Z1f5-A5mOsU4T>!{2>}{XCbA?>u3j*Of*~#a+CBo%kkwamp;GryG!Sw?=6vF?cpnn?_K=4#lopDw0!D~r!==dkZ(bn zKV50&O$Hmwyb)3?t7;78F&vdLR$DQO-9jR7z+BlsA`#9^&VTw;c<#2@YnjOBQjIF! zjKiS>W;GOY93~s_P=qsLm4b!CdQwM+AdwwfZw0u8-lRlT(~+)%kbZtF#dWLhH(RPk zcZ6^vqhTJOr%-5-ZE9!`Wqd)Ilqi>TN>Al`_^9Ql=tO=zD4{$T($a`Zi5__{>?XN< zhU}`f!NNCi* zl2wW{xrN0^iE?HbZ4N6u)hLj8DhL8PSF}=5Q5zyRhC-9_RfE-i98&?|pmJWa z)l(U&=E>9(Ou8%6W6Ik>f@C%0Qz_{C(-K&;951WcemprA{nd6eUJF-qQ8ob1#Eq4f zhfUXbg+hdIX8=8US|WzmvlN{&TX`HMWVDTGrBJLoD7BI$DFedOcZd{%4J|Z6&3$fK z!b7kGNHZ#i2Aw=z>@anKV!2V?E0|QbQt%R5MbgHYo+R9KJ;9HN`?Km)C{ z^Ffg7n^Bo`+S{y0{467f^kTDN<*=wV_w&;dHI@yu%h_n6L<%9zs`*1wB-hakgEpCE z`~reBgML1)lyzk8*QO=dT&>E-`B9vVfl$Z6cC}L%_~0net5p(Bw%HoP?Wkx~@j3#% zVp5_emXdjYht|?E*XXvbYOKNw*_sb7d39X(cl}XhGzgVpzMY$Y!Q=(j_;^2?^=09D zI*SN zjO!PJO0yKms5;g~>d?EUC5nln2-nqiHP{cO1vR6l;d-BhgL*Gl=z0wAkg9-Fk3)v6 zLvNjyptyRn;Zcc(o=z8oQlSx!>x^9M#`*)lo&tTC@r~;~Bwiapk!guEtY-RjGZXb2 zJ$6JvF#3uJv1;}ryi zPS4VURqOPJ#Gn?46`@T#!3*7Fvo;8D49+)=0j@VF9aj=mtBgb8X$jL)DxmqMMCv(- z!6MP%fROttn~mkQ22WU)+3V#|vrxqAb3ZpNq2@Wh&d5T)0gnRKu;EGe^10@45Q38| zM_C;rY0_nmR%6sWHBsHF>d)aL43|nR(!<2*4Bl^r8a~f3C^y7Z9p<9mPE}U(#5hu& zR-vSfI%77(*q}M6(I}QE>N3W)t3#x%lJR2x!qqorayN}{OL zY#UaBQ>8o-fj&DeQK~6?JE)?3x~;%^M;f*gN!&E4PD){F<6gQO8P%m~yWB;f^V1Sp zl{OFsEl@@m<-%;b-AIf>ddSDO(IS}|NLewZp(Ss$V?uXMOQbl0fwLhm$~43_(@05N zAFgC;t(L-IF$^}^6kYJDH8aW0ePCLGNMJ|>=fa&pK54`TsW>`^;GB|pYsEz$_qxe!QPsiyowxWDj zwn`B$*B@*1*=Y%(r6Hqow$&|k@Op*N>P9}@D5YDh3CGN46$Ta=Z(@Wj*5{}5g-Qtw zWlgMaRk4O20l$+5T)5unkanH(R}iCE6orviBNJ2-TAS8EqhciQ7xWPaPLOs8-SW$Y z77-bv9WKiUMVV+tB#f)z1encEOHfE5AXH3V7Q(Dm68fH0R3jqULOj`qORSR0l__Rm zRT8WhntnVg5h`QV!(=L@3S9+_u*evR!+uGwul;>xL_EnCp+pve-6bh?rDMZAKM@ zYKo9mK#*g>L-t6n(g4Z|IV7bAVoxDg@q9z`sRsye8o8o4f$!Rye6r*-&9NgKsG zARV^ZXcvrpG86L;H3Eyv86P&z)$0ip&W_rpd~*J6lM-y3=L{SVAe~MY!)ko7+Eul& zM_?1FI@!)dKn8?rC~HJxJ#&6KU0`KY?Gj?7PPGgj_Vp6Ymdb*eXT8=)wWIx9vxMQ^ zrcw;)*8D3coy!V?2s7^ILc}mj#oMyh&q%T;n&mLeSCvpN?(y;hLQ+YAy2>fgQ_?Mv zWFAz5$GT)Z*el_6ZCI7Dj+{y-5MPpPw**7Wa=~dUgi5&8sJ4|@SyxR-P}$rdjry5< zg)NhES5fd-qfrZCn1Igx*|Z9#*WgvTCHfkySc`av15$0ZqTz4$olkRuKXhUdm ztPxN&v&?3tEX!4>GgDFlrNa2hWU!O!hJq2w)XO0ByrGJbMstX;XqCV#`Aja->YH=F zI4$8kM>u2W035$KPnC45{x%9M!)Kep;+_Vm^)tW35&nEe~j#Mw60v?hBI=<-yn}Hp`(z%EX9zsv{6Vyn@nrtz?y( z#av6OcZrb}@cX$W^r1<^%ROH_9`*JJ6))(c6rS~UvbgE*bP%6atjlZ~r7a~@Et6;@ zKdqu-79$yNk&nv3L<|fmLc!vZ<~T&eG`=h(3%yoJsP{v~un8>zckx$dcNu?7e>v@2 zf@Nb$xDGc#7|cp6I>#tfbuRmk$p|uaJ{o>k-)^1(>?&`f8*Dii;@g0j} z=+)2*=5K@^T>i`DUtex6KWBLzhPvG7p~p1NUVNm`^~HOtp4&Uzv`R& zu+3v-@z~CTo;3#_`$rG^gGomj6%qAG5zz02sav8 zZST?xrzMKrW@a#=aWa;pz+6>5lhXAjS2t+E$d=EVY`@ZLu=3eaQ(O6erzIG2obRKA zSLs)R9X~315?OzuFSoN&d=w%@CFvK~q&Jelb4$N6KP}Ry#ggC~(n8aZvKb63j%n0P zdD4ARgo_|OTS`T%y%Z5)&4u8^VhyV}0`ZyggjH?En>{I(&y)tiLb|9F!Tn}qSRV!R zAxY4bYCSWp!eX?1EnUkNSq0Pks-I2Q3+en2@eZO9q80D7tx(!;jgV#zx@S_NiTEpY z8R=!S)piQJIyRNS{FFyXMOvuEA))#xUjY-#JlWM2=B9O2m4Mjn6oS!K$5*0MuaQn- zkwPq17x0rXNhBpCc==N+9B)27U(f5F%}K6FEy^WASX3C<$7Y zX0wDhXb4bw+VD<>982{eH?Ct`q8`U#O|wGLri90Qq<3HpqJ4bS3)MrK>AdomGDzX3 zswLqaB`-rKg&3oZ zDsVFtp}KlM8%aw+Q7I|r{9V%$H6urZMXV6j3Se=pUd)LRHk-|2Y;iOYJ#;|!NNT}X z6a;+kO_LIOrJFAYiiyEE9tsa)B`qxQD41=>(Qv;k=7(V}Kt~!`MraA^cTelc%523; z$#D{8ITW}x2{sz8_|1~3g{xh|iqLq{8v~AASAZ@|OOV~NoB~t(u{dg2Wh&_yCt_1BD95tK)RWjCHH55bFjl*c%w9(1qz6)N5!UPE=AV%*7-gtV5K`Q5Heq zDvSkrp>9d0SFA)e%9IeOF|DKKMe{+}P%J;{GdgA_s*9bFp2vhvddf&N`qb zn1V18uVhQnc!&!5Y6U@0hvIM@m;2xnVT{sN;f>t1Oc;@sJe@9wMXjD|8x_vuxALVz zJXJ_wKE3AQ%|r=f2B|u-asQ-*;mz=zKU&rkY(CRVD{^E6w#y%lS6A2EZHPjKG(65ZN8mhgNZ__7^-?ICA7ZrmPrY{Er3x^ z3keOd(iRF1Ll#9gIUG(TDw#G7dN_>_P(GROHFFy~(-I9UISdgwJTlJs!o!lU7WDup zgF`H3gvfnX%c7NnE;9ZSx`9nu5u;Wv1(&)!&(PMu;z~Lb42wgRW+~Q}#v~;n_%x=G zVZ0DAEs+=wTZ1tLmP1lZz8~USk)Xw+Ofp>(wZM?ibt#4Mp@`RqtiFCaRL9eZSgG@M zzBRU#7}7#~`I2vdS8^4k-6@r;xWG1ZeF?1>|w~VB#y#w2nZek7lhL*DLW! zD&WI2!%*CxQ>Au~G}A*1%;TltK@6>HXRv^ zl`b8r#`C`c<>HYcX^dh6N~S!0@FG&Nre%yWSv15g zZBI*hbMPchHvMC=#PFJzd5J$>l$o)=|%S5jARQKCP3Ms8}zORMPDVY(~Sy zMBLXA)%qafBhb{CTbVk7Mrxc6#5l4FCR8cKL&vRbIzZ%ntxO2Zk$FsvcPp*D59eCR z#kK7>&$ADkYCiTKkM+O_^uT2BX-3+7Gs36jvSEc=a)NEj;7>SEariIR5e%!?)ThpPfG+lF)5!8W_t-rX_(1+a*PDS zlF1H2nL^bHC-Yz;oE*m##Je4A;JiYSc13V|3Su>i14i!NBL zPOBL&3>KD8Enl8J#CX3_BH^BL37QnJ|#2iqYgN|YP5@~F7LOb4Ga0%k3hnh4`O zE>w(UDOu3s8B(Hpbt8g>16Y;vsv={g@wu-|ON{!xc0Y%CeNjKy)XK;yHd$lyNhMoF zqs{g>Q%hx9m{!b3iVM>XL1R!>xEdId8kS~O;$6#VHPJMt4!YK;kJHLjk z{_W~3R?n_{dgWy+w=DnF@=KSWvGlQ}!P0e$A6@J$K6T+k3&z6L^M5?A%s&o#KO{h# zK=jG~FuUQ!`JA*IzP>4U!5S{9lI)d0}qhO6eZk9%JEQg6YEbb}~S< zBMmYXWV@Lhvm2U-4b31n9ZGw8#X!BB3ui+aZ=l0h!?Io|6V`}S2@{J&$sTQp(Lx)r z4Nk-cukX}iPBrjy)gR#-csDn+#)NN}(%^I;l{KvHsLczb z7FJ9*!{HId>;@)c12c#fe0-|GX9uuf4@(7hOxNpJ#b;m`#_HiQpEJ6_G$oaFslYM2 z$V4nMgV=zY#B07ts@}sCpK7#fgI+A2h{mvvCsxeLQWzoBKHCxbn#Jt;Cu03Gh*g7? zuv8V4NAW1Cb^8L)1m8rg?|Kc)zWJ_M$H%cY#}`>yIrz$IHByR_ zNv+=ENw0*)d~~!_sdW3PVgW9It&AFQ?2>J*-wO|zUGGG!cLuSetQ2GQN|vsAheMw} z=BkAhl@=pM7F&cII$D1rI1h~p=41;)P+nE ztbwBuF%<%0Jrl8>8N?=N%Zytv(ae=2Dpjh4LIyZop(1)D$@Arrrly*7YTy~glSyXx z)`{3#uTNFX9udtCV|iW}a;YZYJow6*vwsMg-7^!hXJ!zK)bmOy*tM$GNbVV#a)Te0 z>pB@xh+YdD>)v#n^Yp>u1vs(7?A|gFd&>-B)7&tY>I>blAtst(v0D*q3>a(%NG}9t z7Q>J{WU`v>QJW3Wx6hi0ebx+OO-~e@VH$S`L7|ESoT#9tgcmp>J@V$W89E}R@@08! z>X|kW`^<^hXU-rNk6Wx3v)#i9GZ}2rq+~(92rKL-J2(3 zZ@!+$(j%op3u&#>FBn*|&mMebFfEmQX-}ug2is(t^zyk9jI?Pq?$L2Nl*$E*M4qk^ zjXDn4-ZT+=(+pxk-l>FOkSMy9eUEnk<)x434&9EO^nP>MJ7Xpt&{vv*~%AU z6%ydkOE8kn5K;v3$~3Hc5+Y$xM5b7X4FpAr!lA|}NTr9M6Rw?zy>#N z6Y}WkK)h=VJE?w}q4lze);X#Y6FjL95;tOi?KKmz*UTU`QVnMHkx0jMtzq^IZ!<3j zG%KrxhE1j2iw;e&5mAg%4ITwzpFR=$^clpKTdcp;>Sn5BB`1#iQY}#!OZ}Rs(D4Ra zeGOB)Vz>d5;hq3G;b{}GPdh%Y(rJICN3%U7NYq1~R(OP)Qq2mY-BygN2}EYt>MBJ~ zum1jr+yY;R zr%c2?Wd^Y>;!iQVPo9W<@(f~K#8hH-pEMEsq#4Az2vo%EK5-)Ui8F|G5mbrUy?P?{ z>f;03oOTh_h}perBKE2o#JY&o!|Yx;5qsqfVqHX^VRoM|5&MK0#JY&6!t6eNBKGk! zh;KnVK;1<4nEbs9B|AJYu#50RhXpe|EHLnoCw}*jEHE&FpaB@Xi$jBK%i$r57W}2y zkZ757#Xo2#y|mFRqH-gdj19a@5B5_$n8Ie0mmL^bWBOe*o=;DrS8cq@5icv+8?@IFc1eJ?uQYN_3U>lOO1 zUfufby}z&a{)Vc(e|t`dAj#b%m{|Z3bSp>my#jGH^cCBE`o1q+Z)&sS7u$Vn-%YLb z2^olo?7Ck9Vr;8v%d6e9x0$(rj{@+q0dE3vH=a3z{-$Mv2R-B(5Ld&es7zScc{u|4PZjXUT` zad!ak+>1FJ9(uamilf(dcmI8gq*>xm6A%w?e+s8j`2rCJ_%T#0u3UGG?Rb9Io()p) z&qeTWk9$kK{Y?)W!;kEt5B{^V`rjvXwcm()_5Yr@w*rN){=&6K@u=_GgN{Au+biQ= zD|^$DY5k8Y8#nfB8M#>qUNwA;ckIz4QBmKG9{qjld$zk8-TFuU5h%@A4QD+8kCvO& zCj38exha*aPd#rJyJlG*Va*vlW6edk6<6mrk+r5&F$Lr63WHIW5UO4c5cs*58;~qm ztK3iSnVfG|T70_+70dUGWxtWd8rP1qL2Z#{7#iSibk7>8xYRAT-C9k6YROV+%_K>2 z?AB~T4bp{V_)U2h(h0s-BMx{qbgfL{Q_q&e&^Cmz4dg=Pn zI!FIQcd|S9olm;`uWtX~?RVT}Z-3gYe{=2IuZ7p@xBmLAciob1edf*Iy!m4{zv`xT z^X$fN-T28HUjwrF@2x5Y)>Q;oum0n!-*t6-_4BXZzVd%u`OuZ@3V!92F8`~`KX~~| zFE>Hfz;9go(Mw-(seS464iorB~8=Jw|tjCjii(ZWFLpM~|yRkKna&-I+PhB1XRoTO@`R%y*+vPe^m6{&AH z-yp;~E>7WENyu%kjT0kKcb&;Cs6q(!d8Q2}p{Ash8CzlUTmN!{5c;!Z)FL^av}1os zf|NsE99v6J1_p6?JL7DwC2#^%Vc~~K&iMg|O&Rzhh%ek&#UVM?p0vn-l-eUv@WR3f zvb}M80F8PTvBj_~B!d)Cok=dg@nFQK9gKLxI^!&7@&uF$+Q2J1f;1n{LaQnkCQvwD z;!R{pQbZe_1PrJrD!z0m;O?P-JDWi;CA(Kn-4RuU&Hg0OdgPSIS*I(|h{yAGV4x%$ zMN4Y#UirYmh;KX?@eLaUt$J0Iq#zvY&-mhS=8-wZwG}p58dzp#20MF zL0FR2%m!YeXO()6gL(z%je!NZ*8)RKGq&mWA~WDJ7*>b-S*`y(Bs zMvfC^pt><&3Lq9fNQSIcOHg~zK}y^b?ODcb9+p(|?xxTO=-3x==XW+bid{#m(D5Y9 z+k>S)?Wcn&U(Ap}r50ojS*j~eS<^9IilR4u)4_-j9*o#{do~>?Ax(^Q%cG2bFCBs` zDlu#TvXNNxR5*!kDfdB9hFcEaxM5p41aWC?)gg#YHP|7u``!d}&In@^$0wU2wfq3Yhc}B4R)AbOLk+|t_^GR+p!~|LL9A+l zdu3hcJyIbHW-}opNVmooF|oN@s(lh^jnyf>~81i_C5RV1#!t!rdT1b#u~E69>%j z!t(*kMS*GOX1Te<>)XkRZh-@rd(}_c5Y`He|*zT-53GrH`%Cmlt zXRRb|w(O81Zd_T9Bb#UF2pUxi`hu^`dbOfQ#pDtYA7ry+$}VJQ2x_vHr5W$bpR`%o zvs@RVRyj!;Vq`EVF`f3ugu?a45qdds)Bzk&TW65P$z*#HOGon`lXC z+R#S4UkC2dq^O|bW4J(TNGD;vj@n3Bi|Akjp?JK>P(K8*scJg}@ozTc80s)c-EM1& z-7e{{Icz1Q1X#4#SwnQyR;4pWYX&I)i(ulV4I5E*0OG$KtYZ@&Szju|fYwYI*bsOf z*q>?Dv4{wII%(j|pc?gPkl5vAw8<<5V&{C$_-3f;NHIPtFLk*e`Y48ch$EX4Hv0xK}C#Esn>9c2=OZa>K zNsjS)QS{>O6t62}Cxn+$R}owJycB~-b$ZPXbL#fJ#cbh6+LEQ|Y!MHgt<1ukZ+&b7 zzR37?uCie=CG8qd@<>|B8#6oNsCr(T1mSQ1O6|plZxUw>ml8=Cs{1L;QhJ^2byAwI z&y6aUPw6sPc?88wvTCE)VoswlR1-eVlYZQm!Hv!jwrsfdX^(9HN#nhOC+CtCKn>Y9 zD|k0uvOH4*Cu~~^n(joLmEnG~OUY*x-UlCLd2%@N#-m9fFchE=>8EX$wnny1kIEPZ z8=S?oIFNro*32eIMsrC4EV$&-2)Asw@nx^tK(Z4AlperZ0hGH}WRS03)KqP%24xZv z3sOKhBs6GtLAlFW%N~s)z8bUBX5QxPa#->BE=sgWEu+&+Y?st-J(Kc!l*y+#mf#_W zrZd%<4y_M!H6UDqixmni0} z9hZ9nPUd+-N?Rfz3hbt>nIJ=*CFw-dvBE=MI8tU3#6_nWnhjXO0Ay^OY7P_56}RW% z@4jloq|8)1LF`c&26{`&C0ul}5}9j-{`z2tr552x0-g>Ne<2S2`H-lXXAEFWGY4S}tje0r;tSV+ ze5{Xo%?Xu`!cl6&aM=yDdP7A{=RGHzuy}XQw-Za6fVh#V9?6O}P#^=c)|lX;CYoKUg{Q8j6ELRdVbs)8lvSV8m@8ZQrAAFoc9 z^^Kk7`9vrXt&EqqoGAjC-hxdY( zXH<*zwjfW^K|6*p+wjDm#7>7KMIj3jSI^`bL)G$Shtjk%<%n|6(UT&@VIGgEmB}D& z6jw)BW9nMWY13}HAl;xj4go>iz~m@w%ZAr%S#VgaI>U+!NTrqm?JqoPJkN(%g6N#y z#VXB0C2SNq1h<2;aw&nMZUvM^Ml?3Arl=kUaMWHffEPE>D=vp9({W)gDH?NX(+dP}NVPo(A0nc%F5fU>%aLDjy-P*`D zYlaq2YBRZ%&!!MwDhWoBAA_a4PtZlf>u{Pt1){n~SmdwTP$+{;GFs7=>7)wa8B0ih z4Bj==El|@`G)mxNpHh}K_W851jv$a2h1k49>w?9ERxnQJgj&vqxthSUid_xH;*{lv z`KcqcjkaQYBHv@C=9pj7Tk~-BpFOq#Uk>%fa^yCL9vzd)Jm-XViJ!^^j+ocGkP-B9@hISiascmW;%?2u(eE5RHiBTM0@~6NYtU^d6Jkv2i-9!GVW&HqE7K9$%Pq9Y zFUWvT&WwrVusos)5Cjh^CQmH0ROni2qT3^<$#g6tAM25VPP6z6lU|yjdTp+cL!d`sXJ9k2bQkxyfH}+e{}Oa+aT1$N zF^lGDiHH)wTD@(<=Yl)Rm%oU6%Euw&@igGmme`!*3k~o;Vy0K7tXZ|%9=R0`5)riw z_^31la8GJ*JvuvppZjznbTJ#{C4V`a<`LUMw(H$)zxEhK=yn7vDv{P|Nr=v)DehN7 z*#lp?nSS6+S_5-h5PCB@Im2nTkhGmj+8K6Xh6@YJAg(e?cP2X66de~(88A0v^~EBs zpRyx_GsNitkcg8iAQYCi*U6i|_IMtMQi5~?gO!H`y)`KV*XjcvVib^1r{E-AFrj!b zizaem)Md`PhC+0xkx0>bcLGRah-gd`Gnw)rk|nZ<+%Mr6JsRRrT0HGHY1T=|WI1!D zqlJSD%Pn`f`Bxv?fR6>YQIf4vYnlw9p|+eUI6H55AY!KWD%nu#ms!;dxgyM;<@JFmPK*x$r5>gu8teGcKzk!%-rW#lbCsJYqX3i)JDzyX_>y(`G zWR^R5s#^gU4bg=Gh{=h5mN|u}X4dsGaSFHs`s&zm#6i=c^`R6bTk~+dTz&tCj^1<(<#H{Y$ssbGv%$*KU2|E$rrR-Tcm*(v9D_@q>W>@AmZ{ zzaCzH&c@vs2)9uc*@u&_K+E1?9v|TIfyWY`Qh`NB`SSf!lp0P6ZNh|72Fg6 zcp;AaU3pFq1;lN)DO%&hZh!lM{EeLgFT^o{pe@h_WMZFy3K*UE%2}I6nG(y71xf(W|xmy zl5j(_wPd#i{?JZ=7vdOoBdgn!gQ>gd^F0DVE(rXAodPe!F=z1V5^0Wkp64d94Bo14 zTloDu1zw0dp=*O82QCCjkE&v4fG}cP;9uDx5W3LW>=ax(28h}nEQC2R1*C)4Y+K-8 z-YM`xq@AId^k~+DV54|ymM57BE)Q5Hoyx`cp zjy2o5PI|uAwwE80yp{S{q zfgUp#=J30B3cTRhq~ywO&lh|r^T=!{jxL-GzjK$s`h_hZ1B^&k=A`2p-Hj*h9(f^B zzGJ7r3yw{Io*=1>krvBhj2Hnr>+Of~=XMIb;Mjs3*x7X0$3P0Ft%XUl*cSM0I|W|Y z0-pJb`siDB3cL_0-pJAxZMI1Xsmdkoz__qtfAda(7a}EIjw~A+i^5F6?WR4AF1XD% z?G$(+Qv7BzMTyarjt4m=1QB_`!Vm5gcp*}lrlV?>*p;N*wpvq;z3|E913Lv?h?E>z zII&0ui3mFM3OaG~k5!T*D_bV_V=yR`vh4j)J4RAGq_@TR(AAx&9m1-g)(tFMsIL=N_r#_e zOAn7>5eLcBA(HcoFt#Xd=23&8t!oR0N&)R-obx!5o25a-P~O1lBMk;8KtchsG>72# z7z(*1ZDFkbA1?+nMb4ZOobn+u7Zn0v*)FdHb76_(O@&|b$~bXKLeHUknOl~tA|B*C zvep5Ahpq+SrP_rnP2Hz@Sr^n&;?{yLaPmIz##BqNgX;6y!1wZIc?BO zjG?>!tu+F_Eme9Rtu3rJR<=XBUgrUCP7Ig0ur*hZ8c|+yxoadd)C)7&EK>c#FzzEU zm}=$mR6niZb9sATQb>yBX+}|BUsH|0ps6MX13MZ+@+h8}fR9)R*$S^mPnc>v1us40 zjt<(J=_Qr$io|qeCRPvUqR|4W4l8zrTS&dG;idU%s@spJ`l-#Zw|8ug<0Z{P@n-vV z+w{7(Nh+X3o9UQaa3E8K`Xr1)l|PIuO^X-ybxr5DKC9W;CP}p za)#y;rW%D5wBUC7c5SRdPNUpe)sXf`Z zcQ0MWnljB

L$YGL_;77%Jo`l=jCDi5iIhH9mGViD>SrWzqbrof>U8lOsVVF1g4 zl7?Y#>7;3)t+iTavMg0b7Tgeu^2)2F$5U-S&8xT1za|R$5GpilDzB@l4qwnzWAlNc zCQD~fZ|EWT#srausXoc8k$Ev+WMZ@0z$eX;OIJYRg-L{DvxF0BFj(q63oWUoCL|Zv z+oJe*s*R^j_4dsG!*zk;Kw{tPYONMt*jjCAK0cl{>0qL1La|M0mSa5W1cG8ix6=dM zvz7V4bAfl0yDKFNt>$0UT1|jNo6xej(i|2?TBI3X*rMPk zt<^F|cS`zsRKOiSwLPPN5tcViS{X{L2s<-di7;n)U zrXj}bYOUs9*jio8jfjl}&>q=c4s-#Fb2mz!G}S`jRk(0Y72B0wxIlt(U`bLc4~G)u zAXQ_4m!x)HaxKhUpR2jYQ>{F0s<&^8xC2;2{PcM9kDzasKA1drF0MI*4fAfFu6MNlpM-gGoa>dstNL2bMyVO4$(d1C_Kj}UD&^ffWl`N<<< zrURVm#l>CiNpbgk9!G};;sL}=zekbuOk$>svG@nXOuxs(O!YFn;-z}xa#OGO<1Q() zzMIyUm5G|$3>=Q{F`gV&d%`RxsjkLl>3t1No_o1zfF>WHVM`KKSP1VsX|11)jQ+UB znId9%c@VZ|HLMq_AY)g9QJIEkV}#OslcIrP^8Gk#Q7Hkr&3mcfL;X^ZEiY*YmG|Ob zTn&B2znGmLuebTd@!0_}_u`hD>c_j>tai~gch_U)vM+km*&*xI6Sm=@KXkx83M5bC zc-ZNCt1b1}V}3DP@J4yk>yKAjR~zz5uGde-1AFn1jO|S;3bjJ12!UOrSn5D^s8%c% z%BAA!ryB;f&TKjW0!FjJV8x^Mi0W)bMD`4-v!~sJo^*D2UIxZm4Sp&mnrBp|)ID;P z8H6(fulUU>Hm=z=()Xvt)a;uKfl0iUMwJEFS;26ghk>!@iIi@tiXNvVsGcsdSscu; zWi)IEQ=cD{VpxV22+cQ(z5D3YyU(!q5YK|qe5h3?71zDl)rtE!LX|7K9@E zMc!)YAo9MVe}1%zysz#YAL6dh_QHJ+*p-Ws_k>t_LUg#`v<^7*z|8+XM8{es=td1{ zn^$yyZ-Q5LtSyQUJNUjfD4=Ht#Y)ZdI(xNVs{{Cxx0wZXe>4!e5W$Rk;LID4J+kkW zdN6%oucD=7!TXgo>w!K*&(HstV||Ecp!hyAABzdoYus;`1~czjtUU!DoLB3RTWlFp zXwH32HDv`&SXe%5ksXu9{*t^zX$DYAjXv<^Nj^eZ~KG`ClFpM?Z6P z`3H{Qdh`Fj@pYHq{?gA~X0HE#M?Z7<4VQlH(hpyHa7j8jyY!haT|N2D8*e-Lv6HVl z{;lJmJpP*NA3Xlzm%ja_@TL0Q?*d$bUvTTQ?%uic%Xhx-#(%n#-y!aN^7W70{*~K5 zbo)zhbGP4k>)+n`ky~GWOTPKuo5s<9J<+av8(zT;gW$C-c0m8+Y^+f-O*a}Xzg1c& zW0G+(^I0#GLJ(* zsKruIZ$1Sii)JS%z%tHj6-!3V%kSRkV3N39>-1II;YYGKD+rcbD7Gv?CY<&hG=D6E z2J6An84Ja`Ke#~*ClL!sr&t{)aAUk6p?)O@`V|ElI=m|8HJgKcT3vce=F*0zWgO~G zFn|iSA&eTbR%rWd&n8;)`Un$Cm0_0}8)-KY)PBi5+4SrjhZ@CTvcnL`5Q$aZbO%}t zf$q0drC#D(zd{XQufy~$n>p4uR)u&c?iaYGhn4D9AsbepG3fJb(;^;u%~@gAcfq@T z60Wr1s~gspvB#qY$6b~)FKE@0sV1o263y))Xaua4xL}qKmP*RNOjW+w+RQE*7ILDI zfm%w4##O^;cV`SBQ1Q96+o^kzqSB%$@iy7YufA<#Rlh>T>8Ryo4!jtMMRc4(i-iw5 zEc#9x!|Du;XswhR;v@OWuWb-Pi$Q?iW`;u@Z&Hwky&8vR6{&$pP|}@BhCA;p#(KFe zAOHLY(Zx7jnUu>jZ4OHlf#8;%s2B&CLZ>p)M&qI|o99ig;xZ>2E|{@jH%na4!)k@v zn3$k=6-*rgxD#A5uDQB`BWx_rwPC0dSBh(rsIlG7kc=A}ZFmAVU>CfIMjB)-!1(1H z5Lq<v}as1TWL@Mp;`72|*Tvd4DvWH=~pl9ji35!U{i=uT?gP{R=Fw%^xBlBH^o} zsZ!_LosrjT4IHBt)Kk})uz*BDklJt&DZuqo{p3v>M0?TDEW7~767WH>pDJOqu&}#j zmA4m#2|1X_LV++^VWF>Hxwb(_H7H@HQOG5gI2DKrKP+cvLCmrS9Yoq>6u<G}TML10O|(LbmP7c`?HhmM=pQ_L`S=9* z#d_!yo^B5PVtta^aA`P~QA!0_fnJsyW6*d3;)mT@vq29GKZ{S^`4B-uV$1I_p~E^d^#Qrl@d6^fkx@Y8;uR3$xp*-v8H-uhr~LNB9pal9F??5H5QvA z-Alb_f%ua!jBoZHA}B{rL?1K*$DKID{MvlZCt!{81r7#eETlKBqJ2Xubn7>NdxPLW ztzGI=@g7k&kQT_wD;ABhV2VLW5L+^q_stnTaR)Usy7gxsB21ieW3sw1jRIOj>nP>* zm3-XtXQmYAg=#NjDbv((HNI4Mh-k?}9_7%G=b2Hz$IM7q)m+d5t9ePSI0=f)QqyktrC?TEdUUg3g@V>o98%YDqsEk`@ZCT45V1GC#EqL9 z1l2Et1h!d?^gU`;5*KA_xBzseYOJ^n(ba6Y(?IGu(buDE|NBFPiK5zw8#dfpD@7J) z=maEI>04Po8(?#^l(kWfHFa1)u7(d07+oQ?#-M;Mf>bo1(ZaN=9wFdXBZRSzIE9+U z*;I`a^V)_MbV4phg{fCJL@^$+0Yd=BbD3Hi5NNF4YzR<)ly_k8$kaDdAu~b2WjgY_jBBdyxMXTbUvi@M zl1=UmH;5&U@?H_nI!Ie^<9gf#gpaVYY_sDULdBA8Rx+)Q7HgIK4zxk2l3fPrc!?YP zssmVJ>oVDBk~N&J&f_*WRlA9XL7ZA1$hSXw&?xvfHZmYPwwz>u!p>~_msS{2K{E$;rYn?L?s zIwlC}_WhzWh%kPtQiDiCTWBHhBOQ_pp=KlYgs7ElY%pamegAsroHQ0vfCf5MwlT)l zi4v#>Z)$dJDCd}kA=AEAo-62BwIV*c`;VW?sIjQ1a?zbbt+Z38#&DtEMFiCK3!}Ji zGlm3uocH{iIbNE@TN~=baYk#5I!&UHq*n@!dPSUvl`bF$L z$hD%m+>wV9LlW7psbabc>m^Y*dCQEd#az z*wVS7X=MXz&}mkzTpM)os%v%&(WMO;+@we@RK0^Z&VUeQr5nO?GHwA`78#zT7~>`w&ic3GhW4_0`4nesW{8MU9t*v~JF|oMTLtCg-4l z+ArZ_GO5p9ypRc=FG%`WElNau^3OLq78NBf<3^!O7aLj;Z;x_16pL`5mDt2X*%{bE zc(OY~ROa&O1~I1y6>2$fS#HWj1MiGosneB>j*j;Sb#Xj!@FE}t)Ke?Ee0hV&Qy9?c z#4x1bP;T_HVpSh8nS>NNRM@O~z$KBLO@|lv%n)8MlYBd=!G+8w?LxhpMEY_>ecl9@b^=yri=8hxqW4smc|9x(%hx&Gfbh*%OElIEmJ zpOl++w~?WFl)D|Y0*)H_qNFyY40QA5i~v6Q)`ti{Oqz8lkH~-zuFT43^H$HUZ)35*N^|xO8 z$!o^dU%&e9EC276^DA$@{3Dm;OFwg3;&fR(03o`JD?Z+-ufPYb4`L*7C=uK=sBP{{oZ`{+htMqE7O%NVAzzD0B*AqJ(6{# z&udn#Hsn*Q4-XK=ar8yhXGap77ZxM~h%s|EaMBp?OJWn#QH}04`QF3F`N(A^K*!2u zM%5|ix45b}qxfDJGXb-Ns|`Ok@|w`@Bn_s86nJKd_2A;Oo6PTUnK96}av2!#R%z8V z=_HHpY$)iVv(SS<6Y0^jF%H^6aim$u$a1{o_t*DYmx+O%mCJDE9Ah&#Z45nX)bLWv zvz1Pxo5Qo(xJ%7z-MJ!0gF=54278Zd%VkEZ#{^s^E};B;xSY~%k5}muWz}P!Xqz;* zD7HjFLVUKRcR~HNFxg+#ZkO3E9k5>+u51CrCcyQaD4?iMb!U1#pVz_Lbv207S;JIg zbTI996?^2*&1M4-4O#JwMq6_Xe5#$B?8wnUdsd5LcLvoa&^>HH(g{ zmEeg~Ckq2YsgTI_3$IiM`; zGQ*^iRJt9rU?onW8SSm_wJs9^Ju8>70i9NTGBL%)(r_#6l&Ko27t1aWOaFealh2q#jbRV3k`U#Zst+6?(#PiyyP-C6p09AR_isMFbO6XBc`fk)S z2ErnoF~l~v?L(j0J>cy^gyAb)V9UyX4H+>&m)Bez1K##-qqD?DNTD_#>K(I7M0`pM z<3gSj;?QyTw&h6&D<9}vAFQZpHm@S0K>G+>>`JA6f%51fPYY@r%R#3Pv{VZh;8X3q zzPG;D`mYD{to#?#%hQB~yg;ZG23&;%?EOEFmOU=g2-G6!?`>*|Qz-4PD88f&_2erbu zu^?jKPO9~EjG4Ae3vPN)UN1w_g?v$2qmSz4^MvWlpI#fbxN+*(N^ zFE`5dy={4t|C&JG%70&Ij`zc{zv{m${=eD@dvy8crJud@-b?JInf9CpEUMH_#z4kNLzVaG*?fTV^T>Yx6^wsNEK62%&uFzMmU;f$4@4d{P zuqQWdIxj{nZ9fB%EOePYeP7hL(0(<@KmaD<`l{Qw;FMMgJS zO*x+ ze)~K|5saotgr$pX{Er@oyYbrx0EsXa2A4$zUE%-iFCPcH@!Rt!k}3h%yjg=>@)9>oYcMz$GYt-W~T_#1ZPw_lPN9L`xL-y&B}|6}(~kH3C5e%t$73WYRz z3DA43@ShqUzkfG=+k0gVHvu&mORc=;W0#JOziu~v+k0hF)7qqD;4AO>#ow78f9-Dk zw)gU6Q#Zl$$CwrVU;p^=*X+h`KYapKF_mXTeKr1H{HMQqe7+mM{rF{C=o^T52KSV~w=Jf9as< z8XtefZv6JzMG-7dak3?>)~;Xtv|m1c_ip^Q_k#8UZCN7Z=KA?xe#h~b@5XO?F9W8| z!dOst0QCRDZ!*X4+UdR9?t_W4iV>3!{_aZE)v=amdUyuIl|K9QcvKzncy|RLF7Av9a zwc{7Q^p79Eb2on5do5ZZ5xmt_*7i${<9F=FZ+oxG8L~#S!Ar)~_@&P~{?gs}ZSTb- z3ZcLt*p>JF!W+xSZ{LmI_Fk@yB9bLA*lP9og*V+`)FVMh&lo>(b z+l08TZ1R{cF&nCUv`ab%n z-*G(MjoIPw)Y}9rL|B>7FOQ-(a-wzc=TlaXh(|flcAJ@_?LT~ciO8tlb75f=GwO6+f z9uf~u?4U<&Yqj?N{6Cr>4|n6Yy;n2{RIo^-y|(_Z{p;hwrvCp|k8bDJ|HGC3@vnYj z-8uij+Go$edoG<{yY9;mF1->TpXrbdni(l+(!~2lWt2p{KHRWPw`W((v0I+Hb(l^# zPUIo^*)Ol2P|uXkM-Pkgpz6Bh?MVy*wbI1`r#GD)P*c4s`8lLCYyJBJiYTAVAP~^Q zdC97>%tBYC88eKAd{*zwvADd*W-e59Q$Kc9J-K!~1X*L|* z>-9~0T;wyIF$1ts-sjj^U&e__X$H#pfRN{@Dg>R5T8bH!>a%e~*`jT!HNu_3%!sWJ z9yS|;K;UXvnUq~g z2ZdH346nW#hI*>hE4d=sky}=|MY%muaxtuTi*;*&N}bYlRFE01Ia-<@Zx~h@t7rYh zFoZrK!|;Ij@2~eTgq|ISE7FY@7lu%!xT>gpV!h-2YSk@MB`95*8p?9!lSM9&?DcuWN8Y(SPlA!mu>O}N4FGrv}qO83nPr}c}PLbdLVoT(tE z-AcWlxARtBHftOpEp3B}jNHT9?LqI}80=PJIf`+q7U6r<*jFGh^i>#MIlsKBv47ti zj$iBt)hC=upS1T?8=sItcfhycC+&R~KQpfurQZjxfF~FN&ZbGvp8DSJI)$zdjTg7S zBn31FoycFGXjemc&SNR?Mfj<#R#9HUebppe2s+LvPaddR-gn1r>Ap%QG^D`Gbf8vk zq)8Ib9f;m+P?Fp7WI0!uwCR{0Uxu-CFf>(&a~fj5I~T_unWvdfQujp7Kn$Dl?nN`u z*sFnI_fwE!0Z}z!cpoJyYFxj!py?RfYd>5M9R%Vg|9^b+(g$98>)oHYtK9jGJMX;n z$+tgr>%ZN4<<=W-e(%l7jlX}xx&Gg-fAjV7wf9{8XIBSTe&@>9gZ%%$d6~QPFD^|k zT|W8X$>$vZ*m3jdUmT5}eH)D)sMn5s`RL83XDZUD1YQ>{2fbcvSf?tswS@3RY07~1 z-rQ6wE*jun<*=DZ@v}R=KBP+fldF}T-`uobhAPnm<&m~B-)i&`BgcKcV{?I};d0cB zX9dGxB_1Ak**WRX>Lr6MAkV1{jvllhX~XNP&v$#J>Y`kx2PViGB%LK>)fy9hG4nw0 z*d>v2^Hmoe{yDXkqX(@=+CX-*CAi+0QhYxzgD+!jyH^h>EE(}$S!|Zhk@~XV?GlwLH(utFW?&+0EkU;ml!3_h2!o{_=u)5iS-J4M&kc$mNRPC2 zK#CUBuCrsM-X3Jch$#0!hi7zIbP3!-x|ohCvk`-%2K2nzis1usqwTZKV32TE8Y~ii zfr}NqI2}Swt(*(hwv{bNF)z-$MicZf9;Jil^pZySKzO7qY=T_!elS)*w(LN|{d&#v zGpVI5LdYx`YI#~Wtzl`tg!KCJJ+^xIfPbXSMnMZ#-K};r7be*RjKBuxu_<((+ zjIsGvTbn}NB2(k4Gq1@KR0*A@3HYQPET$87GVj*QWMEnH^NkD+A25%VHCc9=d%=RY z?6mHrW>rohtF3#re9ZUpXi26SV}phknEHIm%Hae0k+MSEu+VDW2eKkiwO|b zVCM6(Sap==j5FxLTOW|u*E`LF&p2!QSR1WRbGWMzVbDp%sx?7tkz-4-I&EQ|VU`M$ zFheFza5~LZ&?jixO(_~j#Q|XrxAI?KyVW2sKYXqCavHy!+x_(Ac=WR4yEY&hd^zp; zFaNG&!JaUk+#JDqV5#ZZhl0FV=z1UAiZ@FNr^8nvG z#+S~7w7Ov2VF00%fiRFjdIS;T$O4?Oe7&ntqoCNr?yrhbCqaaObqt_n0ylz-%qKAVTgHW(}m=ef&fIdXAj3G|00YgiU<57|`(2uVGBBQ#?!w*f76vqvZ5VXD5J1 zK_Bt5R~|M%(K05c4LI-DQ)I%8!Nz9J45|u=K~;WsrQU13hhIE$_V~dUY_xsuSwIMIBURA%U@$(;PY%Hu1 zL=7V)92-j}6+wrkUS4c0CN>tbgBIZQ@SH+-=*smy>XmkB{JcjRK?U|eCUR`h#uim; zFyi{MJ}XSzIcD`|bE7%Sbh|wEqqc88yTs(R8kN_K@a0nc;LkkLC@e*dSwL~7n=$O< zLEJ4?X3Ny)a12idfQ|&|2@}>01Z$6)p{Htu9=zqjpMJRNyy1+gqCG<_VLa5$W2;@R zrXAf;IImft+=W@LnH4@%%Eh_9FrQuL@mihwYFX^Y)0e?T4)7qm{>$KV(2m>}g>tW@5F}hjM5Trp zZa*}1%SGK@o2ZupreN3cw7)j$(xz-&S&lGvK-(}6QFxO%Ns19?3KQ%*`IIC{>8 zdLe%vRmI+e>LXQcZ01IMFOud_$;aH$L{>XlfOX;(Ele><@qd)_FSvXn z`&WPX*&Em)ya3Ai=dZ<>MAyFO&|SI+;`TI>(*A$CF4CVkOysYWX;d!w%6WY)_WSn`djN}oa$Z}D{VRKjJ%BVoIj^q8{^dQy9zYPFoLAOj zzi$t*2h{p2=kQwWhn`)*eRjZEPdP8I#eVM|Vh>;qP|izhv43d~u?LU?DCf{x>|fkN z>;YT=%6V}u_IvgadqCa4a(;g;_Ph5Gdq4@la{edRV!!Lzr{=Q*&fv;F)VL3aX#9ul8?6uf$-9zjFw<_iQ&1Rs1ziAJ#2bA_J=Wkkz{oo#A4=CeT&QI53Kd^_`11j>B^Ox6R zzwy~+)@KLQ;49~!wHEsgdx$-toL)Kq%(d9B-$Sgjf7QHl{uyhr@83i00Y&o4`5#-0 z{klEG9#9akoPYXS?APug_JG=U<@}GX#eU7Rud!zbRI@ARZ(NH#-$U#HFDsPuPupAU z0k2z>^Ea%;zHbj}4|w09od1!%#U3;XpSl+N${yAp@XACv|CGJO9yAG`ycYZ37i;Y% z|6e(}&0PQb%fEcAy!gxd|9lcbs_(7GIfe%~-3zdO%)gruL6NjO!f6B1*hv? z(`u@5w_U}`RFUc`la@;~+?g^=OkJ1m0Z#b9F`qlNDb2%{0BBsncXFs+R_@Iv1KwEN ztGiXc*uHnY=G9Y|G`Rrv`In)Ljx3ZiMZ7eI7JCFA~#vXOge`}s2x z+q7IMi((Izl(%;WwrsTOBF9NDW-W;OIp*xq;?wg>D0A9vWUi=v*md%~ZP$-@5=hwUC2 z{VzOhpIYCxpt^RY3HZ?(%eV$8__XeF<^^SKR8AHygJX0w=DT*Fd&B3?=&x}@D7ZJ0 zQ%f$F;Rzm4J<3V$`vWW=YyHs42YI74vSBQn=5yo!XYWnGT*vBr!CGwBUPV}Yp}TK2 zmu|X?s*+^MOS4+CEbp>x%NsNmS(atlvSnMAH=sLR4Fu?0FT4y%NW-v&5FQ~3;UxqJ zVNW1o4NHKmWO2>XJZiTc zgy?hK*8W53wkE##4rbf5@sH6@yxDff9`mXuAo;D8TNf(2?P>0s4spKWVSM}u}Z_Ve(*;1v|H=ZS^vQ3>TpOm_8l z?AeHFua-TbdQ|1ePN9(?T7a2UAnSdEPKG?ba-c5(#z$|5S5i<>jRP)G+-q!+jDW5W z@BiO0ckkTROE*8V`ScBW{WI%NU8}EtY4z@vA6x#`@^h9RwEdaw1=gRn{0|GY_%nd* zKQjM|^Gl$Bkv|XIarEr1xp~3P%+0MV&s(kcE-#v)wjN%dJj;SP`wP?}phoz7`-}R| z&i5t}-H+08JaFmgvX1o5Q=|)Kn6a-%QpZ>G)UkoGMKDu2W#k)>Nammb=yGKTG@KS#Ex1c2*URh@n`F#0h zlR%DAa6GVmv^@gScG<*tK(ioiIy{jW{Cu}zkk383q=S6X=}215FqaQOj=!^-J*ZY4 zY5phmpM}>O#(VkbHXZN&DPD^i=JO%mQ(n^9I4wHDdpr8im%Yz0&O457)p1@GkxYF5 z9OAqxBKhf9uat}Np-QuzsConK%*e&&K}5>FO-K5YxM8H1j&9MBUKNo{d{TYHO!b7> zvqy6fky3BeF{W=YjPcf^XO2cB6Q5^?5QhBYCcZ)sL7o$kX5t+LB)471`@-7{~7ItWO+|Ewe3{{zEFFC9HyM|xF2GVuxi5i=bG zq?s580qL$m(7bzMKpL~sTaTVL8jwtU&L2V?2Bet~_3^voul1jI`b>=CQ%6S8X69xJf-NZeEA;xojeJ0+4uiyA29q&yOC-qTI zxCbsDJz2+lm9Lw)z%ayn#@A=!9QgXv7#-)+C$2olaNcqBBRbBjeBH#2h#}6ae0?U; zfv^9_zyhB9_meDO4C$q#CyiL?!etYeEKFMIz}L5K)kkxyXBgwHM_Z%5ZsNkm5aQ6+ zXAYx|ZT;@S&sQ5pciLv6lXqa9KbHv2|>GdWbkQ>;#$cOk9MD6Ui(g{~B&SFt-cqiq~ z*Zr+)C$;oZ(Pk)BbwOD_>+iK=^ENskv^Ftw^ zrSqm4r|a?f#AlX;Yit@baVJo>Pctu`jZb?s*OPkF;Jx;YX>W8vsh7H_X>agpcj~kU z>y|OY#V5^s=OMq=P=MLplZiE9q0%m+*|boIO8r8$hVDacyR%d3keIib5j=%%iebx% zTqttQx9u1%5u!cLKnCsRIY&t#dnzL~8ZMBDvf|9fc@gH)1xzMWk!Ua#<>DZfYB0cR zd=ZB1cD2oC+>JukUGwDBV6xpM&;IRS%RVuvV!if*AyMIGvA2aCp=-(Fl+9}=Oi#Py;D(dA+S?nb}V+jsd}2wO|Kf`SW< z&_tGXP=Jb@3Oezc&}eC1L;Qaj3c5c0{{Pgu=G>Nhv$^q$>wmR=%UWvnCssbUGQWJ! zl4^UO^}ktfwWJr{u<+T1xp_AbeAIs)ARb&^5$?U0dD`*|<}SO$T{oR+chO9kDu*GA z^j97AYF%MzYPFS35$(=|IF63SaienEF_Nb_z8&K5P(4%bwMgH{hdX_f zcK(hGg+Vu#PP;+U2a+62B)jcmG}P}PK5w11R}1;9JL3_WetRHVk0;2OqgO>Iz#B_4 zWmIlE%9WW~BU`0<$#S>k%W}=Dx`DVScf;-m-LRc@Bb8yegS;=_^MwhpDRndsnj)Wf z^e~OfQMF7B+&gLD1@(XwCv{^a(;DKQ(2X&jLhN=1kG1#uOi<7?77}dr#bVI7OrIv8MSq=1IIn_ro?aEZL zE_b1xk&qbaQZbtG$I(pRMFPe{q!TNbP=^=m+v&=Lakh`905K}Jtp;lv1G0%XwGvy; zc-3(BDzk#1DQ4vYX0>?Q4X=}HAB615P|gea35P5dJl<+H;w30Qi^dP+P>zUr5f_)Q zOmcGj=!Kn0-58bIP6|y<)FevDDFRLT`f|W{H=Ljw3#Z)(a0e=cXOa3Y(x>r4)0uJ? zn@Wd=s35qVRmHJhI$z7NfDcog1aI^*icz^o)eQ&e#{6kFiUEeuGEk@LXX225SMt^~ zAiK7&*N6~}KBEbpo>)vA9HfK0u1WAlzZe;no6XNQ#i;C{8{qoW$;?5dB)qPv4Zt4M zjUz!g%hU=;$0rvH0uxconWVVuqDWuTO?W238$Gf!DmR;)ZAv$g2Z+P_PrK3LB!;7~ zPTbYFnO2mUG|Ho? zmW;}6*8o>zSO#ROVxR8NKCL*)$2UbA5a`A$PrG4)e`f+b#61#UjLOZX@tNEW1kB@y zuQ=_736_^h@C?oy$f(?GvY08|0Q1J-%TK#uf}&(nH%8|JqjIw^NKWYnm^Tjlr`<4d zYiklbgYyP5DmVM$*OYF6dE>Bm+6@y78I!s(diBYu+-%l{Dc!(8H@c_YFmYvN61>rC zCr0HSRX0%3jm~K|OwdV8f@g3RM@Hplf2)|H4KRxzwokiZ;^X9`ZWx@!kx{uv)eU_X z*G{`(g3@79H%4b?qjIwk(57euUbU9zLi2abSr)9%w{BX#WBGvfGt2(1uWr3-)3))g zjrW0U0I`kR*Z*n#ch=vuUS3$A|I_*RFTQ8~a51`g3A_gQ%;MMQlYs5-rx)&7e1+vT z>(5``T>H-22i9J-mRftx>c6gjVfC$++-iOGsjK+PU$4A=p#rjVf|^_rLFdsf9ob|dGqf!KeqXX&HN@h z_x-uwn{)sHI`Yyow(`6@uS}C3b+7p^ zPm>;tWnY>mJvM@0oF+Y{%P&lm97Q zH0d$Te|CyAJi0fZ|D9>lW4iq9Y0_hM@|kJUW19c;H0d$Te`=ca*m!?(n)I0FKQT>u zO!FU~COzi!ADbpUrpw=&A`Ok{@}twF$F%dC)1=2V|B-3ZW4iqCH0d$zd}x~VnC5?D zn)H}1KR8W#Ogq0mO?pi8zcx*JOqc(4vb5bbHiADtO?qqu-!)BoYy^L9n)KKRzH^%N z*a*I3n)KKRzI~eX*a*ID$|`BpVBJ${c8+<@Tc=5ndCvbnH#d(PUCCURJ8uLgk2gB@ zrEC7%lcnv>QN_={Wt#MupFMni|9_r-M*nT{n4;JB|Ks=7ukZiQ8I!T-e|`V2sI7Zk zE(p55{~rlB)8ft8>gW3Y-#)fhy1xHM#_Z(!{{Q;^e{5bt;jtah_5D9IwyM6q|A)rb zFW2|~uIu~%k%i0k|1~rM)7E!mvF8W6|JUFDKihKa+!nvJwfQ@n&CRDEtKYu1}A zzXLJ=-n#bQ#ot?OFW$cJrG@^&9rItEzi%Y4`0@p|2@fiW; zkXjU&%O>vc&%UV69QyP+FEn*Hb4coSUT6|;=FqFx=~4SCkLELKD1dob%!`zLon4no zNBRe7Wj7g17UO+f$+lTfWcGb(=8(|q>_kc$7lwO&h}p#;oW-(^7KZnV5Q*VpEY-0) z;zAw|9h7ULkveg`P9p10YI&rViL&8ZttYrNQW4@2HR6KszCthswStxv*-!08W?z_Q z4n2CEF;&|o#5T_Cx-}v!6?^4`tJ#-pbty+fbgfosc4Kim;;Z$c*|(RO!x!syMp@>d z-yk`%9C26Jl)MX7)BT(;(#5=VJDUf|(3^E1rau%Bjnw%fz0M0wLe3n%P_NTO>dD!6 zg_*+_=yjS%VmSNaE_3*Ny-pLia%W#ZWe)Gr>ojqjarW&)=FqLzNfl{#uumh2N(Dj6 z2M7@?NlL`&^fh`EBRP6Wwc)MNbvDQ{+ib+g(zR^7-3fIPu}apDk6Z#`4qbYkr5e+# zf;X6&3@f!@+nE6g64TvMBblsJB#1;1w-*5q5>!kjw@W##Bh+M{m#YV2INd8sV|6<9 zI%5eB6^W+#XoqWwc89D~LS)4u!POA#z+$eVrbI-LJsTK5za4IS;;BGAm=oemHy;v; znXx(@dYx^DtI%ehhzl);V&Pn~R&M+4_CPXMKw6nvGhD&)P_7#<9r!ETiDI_Ju_eN% zsL`Uo$fl}ebz*v*S_Unbs)#)w@!A{FFqNhPI8sWM4&qgrmC<|yuVUpif`qfob~IKf zR|7>APXz*%b{g+{$LhRWud@!-K#uN6RFyT%-awOlu2^6L0ekg;@H8ljB>i+GR>XQ( zhum)W2w#y+21D6k!pjwWrQ}$hcj|RABI0+n>zPtIz-g+zNkko4ByvD^QH0y5`pb+)Zp+MR$x08`}qvGTmavCks;_N4UzQboM(#&NT12jW`(ZhwgU0&Yr z9Ub=VtjJL@vdIZyqTy#G@Wg%$+|zY%)to)KnAvdMG69(yfzKR1PX}k><;UzN4*LE-H20agtuJi7X{)dWZT{Ql=QiKC`J>0Rc4_tRRzI}*s@2%)Ggtm*<=0kz zY$d#M!}4D(zkm4^%l_r3E`4X|eM{XX@6wZOe{6e?tz~<$ZQc4U>$|LV@XBD>@^#DG zEwaUBu`GUN@hyv`MRaj);R_3IS|}_)^Zz#gx%oHF|LFX4!Cn8)7~A#0N2x_&;c}CT zhCLB?Jja)Vcs>QIvYXK~XRpvtQ3e3aD7n0R<i$hbe?*MwNgmyQ+F9BcEyM{&K3iCu#2#qbfMmoc$h(aXg2GOFC;d}T=B z+iWr(fJ;bMkg-@T(I?b;5UESMf5Tey-X%eb_4#noy78>0hJtFH zQkgo_kHv&;rk$}T>Y-+^kQdOl>=}$fP%p#tU^yivJ8gf`@8WiOCW5(}yr&Q6!W0D+ z{Kzhy%LSm0pB`Wf=w))O(21qO2Q_D2YZgJYJt!x#ok$Jqwp4el;{;4VXqZfsO}wIG z12g`@Q2D5f+SM9rDiEu?*c@QDWjcz)Rd7ny1g(v!y`%;Q^fF+_4#rId>|cEX z=3Gs(FQr@%=c;QVlqm#ULBuW>i*+JSGP@#+MQksEkHC%`v=c0*T3QP&HxnLj#nENx zR(Y4FT?Z(WP@802j%h6%E!Kk7fbE6w5!i7rTsHA~+4chX2<)f<6XE+I~-lH2lo^0PEqX92Tgmg z+E2oMiVAc+Rd*-hz`{zr)=WaG?H>3D>}VG*OG<{$q@!Wspv)IN^>RbXWQ$NS;3GrK zLC)UI5&0g4);$Mx#pZ^Oz>am{vWe%?HWz#ZcBBiJL(b5y#$sZcansHygf;UA?xLJX z`_*1p@+)2f4{Nb%0Z%j(n-e|)JI)0&J9wJ|J_0++1v5Kzy$qO>K&q55oTx?<5U){X z0Z*kgCgOAli`ih*;j$wKnTnPT!-W=7ZB_I#V3)aYxfb=6aCfZikjw6H%yrOJYD^Rp zA*G5Y+jTL+RdcZj%BVGusFwk|$c4-0ekLxdVoG!S9das_C@D1KPLtKHhJ?C)JDlKs z&3H1(I;24vuuELHEHXU}!cuk*pCjE+fx(4jT9r^wz{RB@j_~Anxp)omN4Wz7Y+x6- za9M8h0lA{)+U^u1;(n=l;C2>WjkMSiWU@r!SeuFYE5U|8r|8(gE^pzokazOcAjje! zYBxyjHUk-FuGEjax_PfvCSG8tMOU<122Fa|BGp3TQ1t%DG3k09=PtW1yv zmKIWSC+3U!Nf%2w@^sz749cK-8LmgDM5&ty?h4v27Rb>Gk*Wlgb}Jc8N_eUe2#E+B z&8ajR(XrX}GQD7Bx9`IOkkq6hRyyc&rJC$P5MM&_W@V_940z&kG~en$kX{DS%cO%1 zL}Ajk2ve|E-2&nz=$0GpNUoNr5voUe-mI3P955|97`+Uvmx%?-WctAF$(P9%(^dOm zZ5gZ}AuLE&)mE>O=X-T88>+e8y#Y2zFT=z6U^!2BKwNSQiA+BxirySt^?JP-+1<}G z)s`a^5z?}d$l5UYXkRabNdc0BJwZ|PmSCw`=Quc}}&V z@X?-L#>Crxn;kyd)yw!xZpr1bi-Nx$raIwFyF&U(U;~ovmXml+PQrPd#9ftEn`dkY zeDqwsjEU1I8w?-q=w*78vmw!Wo^r>PT{NEOnp(p{l4PnbWPIg%pYImig<_>ptMYmo zuv-Uqz=Z`o9H@x(d>9g31RTRtxm>FqWD`h&@uaw7FA-=|+M2tgm$_@u4lNKFIY-A1 zpl%nI``Lp;FcKElGogqE?Nj%lTefl*68qU#oljG8FP93gNYC<;~cDroPfsby}%a}NcwA~3G-Kv){ zkt0bjbBkUEi3uq=f;M6<->yu?oI%FH`s1+!e_13&z05Q9GT;!8 z;=v9cgNylm)SgV^kd$(F^Fg#2sH$!iD;{Kbb83D!T(R8&AKk2%F_FT@_H6hFoFXk; zHbKi^y9^(JQ=)~-CbHYuo&_I)Qy{P&4Lbb0P(ANS3*AafQsGu4UeDGrrR%KiI)nMD zjI$9orzmdGb~}9Z482UiftNBgLPX$VN697Zj@Cgsp9~ekyA^i&E^g*^rRw6Bbw7%QvkaRWA>t&v%mobqM z#dZlkda7Q=M0OI}ZSWB|!~y$~0|zbl3Nb>ilohQC)#3%LqS6&-w^nLscvs1Symok` zoOkiITj8T8>t#$N?y%hgAN`14#>C#(_DuNbNqQL*nJsKL!$(_s857whY&XG2n|c`& znI3F6!bji;$5hS*+YRv1x?aWv%ew6u$Nc|KpId*;@-r-Nn}52o9ryq0Qa^4zx=rUb zzVeKDGa+pDN1G86Z$0_sdA!DM$9dGHCW7368Pr6u8pxXni09Mun~2=!QIU=(jL~78 zrRtxVBTO$ao3nWI8H`@)ENAgKKmaAVTH=~Q;VL##U4Jv!hxNLzkyY5~#_SUb#sPb< zkBaQ$y-+5d@a@}u_PV!IQ=K_jfb#Kh#@UOJUZxmN#o247C#_YBAx{BpbrN{LpX<2d zy#!kchj{0XY7Z6mW6?Ic19>COZa?Y3MbRk*JpPOvq_Ty6cE98i%DGfij=?oHm<_w4 zn5PwHNx~34>7eU?p7b@Xqg!=Hd`O!&vo5$eN5p<89nnmrI42W;P8f60)MP<@oE-2G z0D3Bbx|WwE{wlxINqc{Qe&<5$6WP2|wQ5GIQRyaFmBDQ-K`_`(#Fb5kf=-7!+3I7} z{czmBPdaJWHTzwMR+>z&%@EyvBo$S>zO>Zz1%-s{%hvotCy9}LRV-(BIsvs*P%`bE z9u@{P!Y8gGYsyqGNxpOOM@Kl1=xp5_)lMgsq;&kw5>po5g zF!)Cu<1wBQkjH6nPJs`~=Q%yG$l$LPiLade9ZcyQulH{7n`3fb@PC5=CF$mnfg}RoukG+mO2G1ASq5h+D0I!? zh7RK%JTAJtq?+r7+|-UzsE}!`R(1Hvf)Z6dj#Q4UXWe3?8VLFEMo#fDjyOcg&2VC; z7$miX#0t%vyRL#yDn4H$n{I_XhO&@5^{(sFeEaB`gJ~Y5t1{tp7Z+~qk2c)sv*^G? z9&r$muF6cjI%gU_&KUJL1&&9PuF9cfo|?DgQKYLHi&vnnYh7SFP{+gtHt&iQ-CYOI z`sE#_heK*J>;i}QK0)hgSdVtMJVZklLXHfIUbE-(BH1WtjGaflWYcpo} zNqn5=0l&t0iTOwZbf@QlAa}UoP;( zQ%=F#KouxJ@yVRfPBbeuDa&H2$Iym<&~*@xz{%T_bjiYF^baZe!`Wja3?6h>L^YXrhIbUU$i^DHRHQq8abHvo=F!u; z4*U+BygfnCrI z?a3SFa=Io?^Hc)I(izMdNv80EkEn;WtS1(BXJQV%A*!Kb57Js8K_PqawIB3nEA>u3 z?yHC(f0P4BwUuTt=5jmmMrj|ft9?%+O(S{9-z)dS!6F3(3-J^V`GpmEWn=j(%RjsP(&ZbM{&ea7rAxNIw7t(J+4ihow*KV8hv(lm z{}=P&h1|j|n_rpx!u-m{zic)(9^OQ)FR|WW`4h{1mfIHpV)5q}Yl}Mz-`#o#cxQ0i z58{~N`Buihv9PV;e5V&vG9j^+pi;?lG(Pf+C)gO-&4p8Zj4Yhr{_&3X#RmtTC>=dG*oDtBZK4Q@VJp9Vg34f( zdL222Gn5Dvq5*|Q!fLfaj)@qhof zZ=zer$<0?jaWWdSlG_b!8Cx!iSzmn-^o6Q(_)`VA-()m_-R*B0mI8ma(!qb$9jj(Sa?&TL>X!U2^ zM`smWe*THF4Wd6)_VNo)9M53Uab+*R=tS8Dt2kEn^7DrJYIayHg*QQJGrt$7WimJV z4)~mJZPtGu4ye(>)AVIWM|py6^TPo(TK(yOTK{7>phk<152$T!IG{$W7<$jYoeXF_ zl?{WPEbogJsJ^eC8GU|v&VVlc-zNiVu<*12Mcr3hDB@oFmkX_alsRbWpHB2;5dEpX zmcDnQFM~zL^|kcRCI|lPe7yAi6J;B0|5({e|9H|SN2F6}2IQ>GS1UrdPK>_rJxAHr zpFh!t!NSv&Z6C`rVSU$$HVjsOstxPAPqbmM=(sklzi=|32CEo)&(ECdP2CF#M*4$5?Xe(W9w<%n#`XYr?n=i&HBi%bkTg#s~F@J-Fr)djw zj0U6SkDm;v!Rk*3borAf18T77_<$~d>SRC-R&hL_%O87WK&eh5ArzarK9eP~KL6;` z@^c2%_FdGb*{s zKHr}P7nTH7R>1A0(h0k@<7B*ME#J?vA?s&O#>-&gX=Y#_O9pQJw0{5pwmEKY>;BDu z-+bl9ziqsH{a@F6Yu{h%tbT7*TluFIb@?Bb8%y6^Qfz;3t6KlgTCx0hi?sMRi{ipx zF9`F0HP6re1t>oG&wUR=76Gspn0P^D=0($N?xp)4-Zu@v#M3P!0QLp|OjK&-9heaS zyQTq{cvNNtz;g!xOuTNpXdBoW05I_!?PBkt-S_a_rU96^JvRFy+7U70bH;O?l2ADf>rSB0RR(~UbG4>n+9;fDtOiafQdV%W-f6W@q*h2 z08G3IHSp&cuLxNE@_^6pWq`>q2RQsvfW0pc zc>AJ&u`dX?`g}%N`X(O{Jlfyf|Bzw@M-0nmSdPrF8_njSzW<@xF#;2uNoKf`j3B5U zBQU|hWQKXk2m<*SfeAh*Gdxa45LAv4m|%x8!wO{tLHQVg39czK98*RRNXH0Fuuz#{ zpW3c;f?~d~w7ASevSz&IC}84fTb2>4?JCRoDEu!GG$_`Lt2{4oL(oMUEA z%8el4juDtSe>ZVbZ3MxO9wRVO>&2db-~Z6TF@neK{Q3TeUUrPY1n-&|zO~s0Pxn7` z?=b=sEN&M|eRTgrFFi(J<`CY*X|a(pc*!vW6SZC}`^Eha<&F_NZbz{9Ka@R2@VFg; z-v3bM7=f8HP!oK3vp3%NKa@U3V4_wt?0QBJq>d4oIfOKET4n?RdyK$Dt!9}2j37uJ zBY517pzePtag5+`I|91@q4+TZGiN|14l8CKKn~pW+*)k)TdO|`P5|o5-(UW*<>jR}EZtyxyX`sF_gh_-Pgwlm3?Q@corTi;cjvqF zbD)Yx^+$VfW#irltd`~D%P-3lURz_M*NQG9`O6S`IfkWkzE~D6c5_YH#fl*{&-;Zq z)FopTMi6iY!*hTXP1TZ60Tt>le{N3{?tSC`+|eFfHq`FbS2rPO*ls>$FF70gxV@0~ zYP}vG>i~{=Azjn(y*%3PibQ}UYJ^V>%8Znh%dG08=iIiEt7C)2~Gw)*!+H>=Wb^G9iIVf&O079<&*1cl2h>u-#C| zlWjy@tjo<+>Mq&GMau*xgu-5|L#TADBqq`j(QSqjO-C|WaL1(3UWh(!*J`NU(ThsM zb}^aM6gyA#Gu~!8QxFot0Gr*DB)@~=rEa_|H+sCsPPEBBnV~5`+44T#98RA(>j z=t6BT-o&L)R*2BKMl?}LAbVk7i>+n?Zf8of^RkekQ+xyBfFFJL|IiL!Zm8V}aihTm z+OC>E(1>USwgys6pz=N@1Z6GK5IhbtpG`6xOW8?^aB;0@GTv=elOS$<_dD8Q-%z^~ z;zqBD6cRO%7E6YTO{|ht3u+T2F`?Op=AmW9>#r*$6o!y!oGp6;i6o!F9298x8`@#d zP`eZSH`m!~u_)Hu-}5K4nna^y10?)&Rzi`!MxvE2A7rw*w!%faY}OIsC0^XGBtW}g z&4lj7|V!EG&32&!?GFnA$MPg+%PxU(WP=<+X4$kQVDSeuG zz7#9!{r#MF*fG@Z1pm$RHDBG{CGBJ|)l0QKVmaMm1-u@rd7{K#!k_)#_*BZ)3mBb$;}-f!iRewlJI0{ z5J>@5?Da!QoLBRyAg9L-(5`Bz-3jZQy>cc7xqVy$4s|_+{W#&pIlJJig}uQxZr7*; zDh3MGh7So>dix3Qek&NN==%MjcGxu3?u57z4%M(w)JJA2GNO7_go^h(94!>{#az*c zY0i`*ry%*XfCiW_j^j`tGl2hYdsRPKX=nblsQgkVHPf+kI+%4^O*6f~A6- z>(U%rb+y~=e7X}5Lk%k7smDDIz}3E|&qwdk4(o>6oe(!zXA~`!C0D%IsnYu{G?}XQ zA{uH(Ds?~CcGdSY?SLm3fdENq)Hd6v=aeGb1?L;F^IRK}!0kDq!?(4O~s-bo##H%i# zZA`lTd(J&)Cye^=fIVCm1Pb%^IZ@#V#o?_~$VNgTIyheJ)jjc2v8Ko8S80c`p>`+4 zD}qh<)Ibx3a!qHUfM<|?$<@@dXgDOvaJ9^P8hj>z@AVX4Gnw62f{t*M)BWf^?XY5~ z-3jq3P_H(ks8nf|>}eWKhZ`s)g@iEdL#k9jE$8C}$jfx4M4J|5Jl_c-VJ5r>+HGrx zWkc;wh*#=9nP9Y5TJIubON{^R8wxUMM@OUAb3{ z=yp{(=Kue|+{RN@e%5;T{0AP@CjNTi$6y2iG1D_nh?z_r-pxLc8{t0GNpH{PK6IT& z5M$yHZ$J=Z;wslb-oz2z`3z}h0Gc9ZBGVPcC-9Y0MvnH%T7VpV8L8u+os(eHvGr1C z`41m?fc3Z#GdY|nG?AltCkr!TBbj0A`yE*JD2__qStSZY(az#6hY)kic890v?d_-3 zYbHcv=`msmDXOF=j%&>&L`QPd_1MGGR;?=XcKY{O&RLBeRZZ zf|%(s@}$|sOk=)yotWvdC1!E}7Q%@uK0IC^>S|~?*RwJEs%p1&T)}ogRw&_?I^VcuNFllpP0)w z%cmDA_>EP8FesY>g|jS4&4v)jjWZ2lE$u z;os;2`<$FZ=lIe&i`3)S?2nUo3>>lzvW-ZJo@nFtfyECHDNX(VCw$RG#*Fusx>F4^ zZ|IZ23|?-%yVq>&h(o7%Ki}8^&2|qCtECi3If^yGXIvi?-thz~%}O#sw<}sma(8`>hPNnV zonmU=74Y#CkGqrmZKeZxYlT)JN8rVJk1$5_i-N8LW7qfpx6i$GZmY3n-F)-rT^kQ? zpzA-s?gkuy_pbi)YH8))R(^8j((*@^ca}cB!~)j8S6ctxdI(tk-femD;;$}}3xBjA zEiBHr=9fVw7yd`QLf94neVt%?(RQV*2}Hq3maEoV*$3!Z4 z>Bm3wJSOtl zD_8CTt?0(%nEeD|NTYI&@tAlUBVKWX7Iovn47Yg3cuc&3k*>HvOSc#^ZZt;xqn0SgJU9p3fbmQ4K+|pIXW8!%TFdjr#7cibZ!;MWhp0Tf0Ci2Gf zS703t==!cfbl#> zR~Imz%Z3}9Zaib(jZM6V;IG`NqXAuimSHqQ<1xWz13G=ju+z8CKGhpq&-6}@eY7_5 z1Vg#-zxeQv=ZTXVn7SGtSiEpFQm1lvLbX&g7a7$O&vWf3O zK&`hA)q1Pp#-^)v^aRDsdGV_4S#zPePtPrU))E9P{r|N1yNhpKR2Ti$cUn8vuyuPy zUGcBnvZ}2H*PpfaearWjZUj7lpSHcpR<+?vzYX{Se|P1rp!nZ!{`=Nl%f98Cm)`=| z0v`sPfuG%Y`I2|_?d!eu=#sMZ-Hq7BvjMZ@u6PcD4YsOTpaK4Db!;|m`*D*Ca7j~NyHt%ct*D*Dldj}Aq{ z-DacS2v_p`UZ6{f^-+Ra>o=_5Fe>_O>$i_Zb96__=j%ytjieMW)9ucP=<+j{pJ`O|=H;7*01_oU(Cy@d)o!_*t15A3q*m*1SbxK) z=m)JIG%EV*)?Xir&K!X&n^%mAKD7CeQPJ0LzTT+l>o#9^EXvJP!EMX885O;C`BtN% zw=CZ>#F`DY%XK*y?*-EBekIDJ`tTY1Ag0b z5-8i@R35xQ5qj0Q+f5B->B$UEMFOlrajTRUzYm4aFdn5W&hDk99uVUoy7l{qBm{bWVF^Bw{ASa zI%@>}*z(6lMgPe1M@B{e(DH}JqTI~c`j4z9way%YTeeQZ?@ZBWZVjj4+)Us4y5;Le zv3}F~&5d`@FHDYT|9|++&<19z;I^&d6r7tWdh6D33ZAX4Kd}74i4o57*l$>d>%iR1 zn6mzf)toZt8mOneu)-2KQSiS=^^}Zh#ZLI>J?zvCRZGC#{ z%2r@&bMsd=o14$u`1g(9-1xDL?e$Nszj{5ozP9$2wO6j)y87p_~u1!(X#Nl zh5x(|U3kj;ug!PnUp)8!&VA}m{p!KoNm3^!9S-KI!A3cqE2rpkA?q7ikqi&Gj?dB7 zzC28e5KHxji7c2(rBLg)3(c~h^HG(4q;-A;rzb=!?;55RFnn*oVc)Agj3qUhQL&}R>uY|60jMhj0pJaUen3EHYB18NZ4>44fACj)A* z==gxz)RO@WA!zqHd^(r%Rc&!a_uW8ZetMr8AX5f z#BB@~9oO{Q*G}BVU==4}^h95gW+f4m884Mrnx0;|HF|C}jJ{{}Wqb9>fEp}(qObD? z)b1L+!DM^wg;qam4z^dF=*uAbQ+?T9bD}SUMaT7J`|%Tf8LVO$<{muJmrn{d(>|X7 zvi*8kqD+vp!<^-pPX^Rr;c5Cp+@pcc@=GTJYOwm#0k!<<$$%OxIzFJ5_nRD)dY6KeJ} zD%6{;qt)<v|9^MgTJdBB zA(5ecS{^}Lk+P>tCrMB?Sd1jaZnW>|gp-keq+D0?NVJrvN^RcGIB2ifiFgKWAcHpi zag3)}F_yqfStKg+_K4nAFOnX#1?tAixdgyKM7lY$6^sw8 zLX@;yRV0r>&~!A)W_)zms7^lr*O!4#{Ihg2x;J@3>BM_MIw55lP)5Q`P0_ODI1?=g zu}B*5g8`qq+E2W#=5ACuNDEOWjj&_^BAd0Wnk#ujm0Hi^sA3!z>cvXMOrh0Ll0GUJ z@duKuah-heBS0tA44sT!Ykvaiq(@(vpVYkxIT)jTc$@b6*aRXIX`kmDop|_41+F5M zPQ~sf1w_n+LNQO$DY6&~$Iw!V_msV)m@GPTeo+a9BC%8s_l6UO{p4)|(1~xBPDbbJ zCzMVu%1?YAjOj(0NT2BoR5TPJOSL?B(mL)ZIGn_H`2&e_)>ClaZ9nL{Xr9RObefQS zUZP4kU9n6QqW$TVsCaWBDv@L;mNu-D*KPovkh63$x}tp|=_J~}px8y#eFDprV{8Ow z$rPSKx@wMO&(Vp;R}SokaLn&*#?W0EWyEBwTrTzVt=4WL6_01tK7!S9s5=TVN}3_U zj6am~6NYtC`#R8xca~1hZrxoj2zo;4#?ELrx25iiM#az9zUe&z!gJ!ig$>PztgMtSc9I;4i*X$*oqB0Xha) z(cFcI&za4^e2;92qC$AnQ7YsQQpV>K|1*J3@L4(;y*K@Y(#b{5C;nhEmdvSaIo?nE zN=3SY_G0XLb375Gl2u%7XJi)??D(LjFD>{3*)VEv?0SVh6bZypSM{Lk#j+kS$9s~2 z42?&9hW+G@2Z2sJvve}bi}^&-N#vsD6Ee~j%DG$!>7sNkRY_$EA za6J8S&?~rCcaT?(@!2qMidBlNkWSqe4(eRr1=~%Cn zi}9gKvz@4V1MSSnftStv_qS0zv^JH4{6UQfv-@|t-~d~&l;C^bU?&aGf%E$hdUDbe9B zb~`agxLoCH7!#ngLSMmHJCQ5luJ;N6rI&jf_Q3#Oa>MSQIVp z2hm=ilj0x`%1$U5CjdiUzQOOiIA<z*52SF1hZS2u81i=mG74unwklSRW7gtK1J-;>>(kOrMhL@TbGh`ySL%&v)=uvPe9c$jf=v5 zKSld|1atc<_28)0R7tD}1;7^d^4mF4IB2QhuP_EK^PsSM&~@FjJx4b#3(NzHS1R+D zP1vhx*VYMeTix1$qyxGgb~tn!Id@`%9uHqPK?-=jubX&^d%mx$Do~O>D31phpDLcl zo|e#U={WLAp{MIA%}K3GMWuNOI4oEI@WLg&#MMMWy~J~X{j+c>-@DW*373jeQ*B++ zzolGiDVJJJiLb~4_)SyM#L~b;PI$;1p!RX^zg8!P5p3f09B1wINR;LUB-W_)B7Cvw zuDbg!PoUTd*W-{Y*Ca!+&W=;@c&^!r`B=agA{4v54pLW@WxpK;JmgI@)>T6|i=mDt zSrX)(Myxx$($L zk8aSX=PS>czu3%rBh&Ll5^MJlrOTR~-%njcchSA^kIs*i_nrd;xUE{^njre0T_@{% ziE+PETU<*xs0agR*Sm0yj_7c?(TS|ql%2rn3SCWuoIRCHQWb$SU`@$Mq{3{M%GF2_ zB3iAo2C^Alvm+J*J5?XbhMAZhrwe$sT!n%}zt({(1ufafoE4|N6pGfIP=u4xwW2+o z%tmFpQOb$D*s(XXOe!K$sbIZC_GO2A-yatC1Gpha9CRHxqJID1x4_PAy~Xm3#s9YW zUl$)(q!(E5CjW02KD_1HvTc5S^PQXZ%@=O2Z+&Uw4?%Xo_J()k$?M-)|K;^ptOwU` zSo`a>Ut2reDz7nXx2^tPtG~JWy4CdRvseCc<&!Hvv2yRq&ho!3e|Gt&mic9P=|7gf zxb)^FY00_p<4acC*KF^wDYh5b)~w&Q{(@Dr64oEF{F&vKEHAeN=KtsX$LEjcUowB! z+&|C#_SRbrY`YgRjJn*~kw{D5*qIQR}s0xs7^jaJEHsW5qCB?e!hsvk^aT zquTE?d2cBtC$gN^QANX*U2o9SYm_rxQYi6(iYn4P+7y!L%C8PlbsI`s=^}JFM>$JO z$c?89o&%7(DV|~=l@2vq?rJRJ3^0+o4-8BALTD?|Pqdt3W>>|Xq>yJq&UTpE&C*ns zYXsXNvQ09{X29~v;}RuLfKGbBtrZs<(Gspqz7i1aRf%@QJHbw+M}`nFOmkX>6Kx+o zE+HmqT*iYvkVPmQbvGM$rQLDTTD_jjwbDe@rIko;xtw=9A`7=1mnh01Uk&c#d@xIf z(GU~vNqs!ykg5gO|Hs~&2Rd$6cf)s`d*{xQ21rQ=OEQ@R$YA14wiH%L-ep-{Bw13R zt}MxtEZMR)+cKm;SQ40A${Gm!4hc&k>;wWW5cWMlXjwxcFI!p4TDJGGYre&;-o^n13W^PEFNOgvdm7VuUIHuB`=o9Bph5^42`mWXC2>7kBBgG9Zb zhvK7pC)<=*H{$BxdvLE%m)8C!L@1edsV`Ybv}y8UuOFk61x`fcb}m-##Zy(<70FaZ zDv#3c);(qj%b+U|8y#d2nHCGVZa_Bbb;ulmqJkq@FN>{0)?ll8dt4jszbr&_9WI|N zHElR$L?^sgP1D8t07Lq09V&FwZs3(|S!-$~N?rSUh|oLr1QZ>?7*t^}%(O?HNP|K( zR_A4v%0>x1QsRTyz-W&4y?ANFi$a7-L73U)dB3HU#CS|XRL>`~*_7MMgx##=6Zqt{NsFRBVWJ=x7iVTCt8_=!wILhTB9r!W?*Nh>(T&1Q|0- zo257?+Aw4#6YY4=XlRylQmN&}{cbbaZKc@af%E5xNL#k#J{<2dji>_Q3X;;p@LHOV)C^8sYqL$;Q6n(g=TPgf~Ydv#hF?2t3tdONiNx$`v#ZL$!Rpno|nNZoLlMRVdQqcrGql=^;$URmLQ; z8#^JQREf3uWHJNX4rD@=$2=aDom5=1ATpV7hW!FF$(Mpy8Z!2G=ZJz?^mJ|1uM{lD zl61E{p>j2cl~HjFN3*@ItLMj3Iv}z;&khmAVM$GLWuhq20}IWvQZFb{I4BUH=GAm6 ztrr;B$;O$eR^0l<9MLTER2s|nt41R4$Z;zr3sB9AS5p1hun1X0lBj~|p{&(*%sB!v zb6Q*z5)2{xdQI%`PEZ9^&Da{nWtg&E7i+rLZX^cofyac1g4-y>CiPLPjHprp?I!i4 z5O_Q!r{PGm(s29!nGfN`lYa)6J5Fu+?{D6Xh8(ql zc`|IrP_7Xe18=0ET#Jk9(Y23;2$-xWWi3^ZnHE7(*;J*J9C|q;TJ}e(ipk^~~fil38fOV32j_+6zAuE`z*r2%iw;>{B%hq_(7=SvIf)2jkOT*YOT4>7+ zEMnILrcB`7T9%ImKELmuLPR{zA}pizuyU#pBLtUg71Ol_VKz(Ex>+=oWQHhJ1H+vd zTk|O~W{n6J8}+ktZ`jbyCY9E!#Y`1p%OG5B;$Y@+KAFD??I$p@F^Q? zDDnhT?Fp0~2-!%pkw^}30+QXBIKgZC=8JQbOqZGxi0d7uTjG$HJ+uUh#;Rr_Qllbq zIET>&1>%fjSnWVMv=Q-zfD}Bb8EG@rL>)K|ITFUhew@SuuWAk?ynqK(RGJjm=hIaL zBgegoQ;;;0Da7*vY$@3WUm${LovKt(rBrWrCYD*T)y=QXZ1g7{hM z5}b^5+C*C|J8@9Cv6m%lSvpC#ht!yYWGa`Ba#4QgRiO9vShf{Y`BD!9QW zGnB%bqiU{N$ihrM)uslin!5Sp5aC+lBvGQ7s;L;2a;IKt6mp|N2KTE9qCgA6w*;_BMVLPTHgHtBqo zCtz|!xn(Dl>=}`wSLH@}CQTSUDlZ^K&USHY1D_+BjbU9IjBydpWszzUXPqGFw<$DI z9LR7ZW0nUHk%}fL_~7X?goTo=Q6X0uamh@f)+pv+EyD=D)NaH_RyMJ-k&YN($6YZh#fgfpCSTkOX$aTDKWJ~&>*Govvpabj7>U^W`YYji$kTZob zoCJHR*q9m1LN$vNO2Y|cjUfjbj`O{gS*;z~4)gy{R!-YJ{M*A<9o~27>O&7ac>O{8 zzy}UAcfP+9?A&GlW&6+AzIHpk_35pLgHyY<`LoTZZv1ZJ=^GaSZ$WwO-D}k9Kd;)W zJ1djrPM426c;)8SEUD+pzAv6DXY2|ndXs9IJ zktnIEan*hSLF46W!fiK9tWYfYN|ntutDUCE46a-UW^@WR&a=uS`~vG;z&(IpBg$j2WuZO*jw zA6mFMa|W@Q@g!D?D=6fG`pq^}74t?~E{$-H&>){D2oXzX+}uEgt--t)eFIbaTC0{V*bpLt;^nY*{8C_Y#-I-@Z1Oe? zTB4Rh_xPiMsTqUbzp$x4V$d?h6b>GL)by228T1beTf1Wh?Y+%$bct#T^6|;EIa3Dx z{ld+eGiZr&3WMVTFf(J&TNX~de-Z{QQBOfTJ_aUd40`jzCU3)_B?>BZkB@+<8H27{ z*wh~}Xqk!%2agY@uWZVoH!W=K4jFX2yC+AND5)SH_ovO7GU$y9H)qbEC2A@Rj(fn& zj6ttoIQ#xd7_>xD1?{*COwJhex`j>NhCxeIRp=gffTBbEXWsX5r?{8MH)Ug~4$Pn3*x?)eEQKKM8}DsH~tJ z?*o%F2EA%wleb~e5~UTo$9urkj6ttl*wh~}Xqnmy2alW6S2ktPD;BnP#|+wgujl9z z#TDe^#( zv_yS{?r{y6nlb1_3!C~Q1}#%y;oxy~`pTvZdf~#>?wCP)<6n<1QDH$o-kCOM%Agl4 z+?+XsmI!Tr+`;3@iEcmt)bU}996@gv?6*wx>EO5wnloec)eAT0B#d4nvTJ(&f56IH zR!)2MX_do2JN$yf_@VC}dio*w;MWd5>0so*O$Q!z;Kl>GEJaGN<>yKVPZ|$RN_S%`?3w~$y5K#PQ_{Z*M zHk7l<%hZAcp(SxB6)QgE);C{Ud>H$+FTLprZ^U!;Q9M|kj34uG^t-oy@r?VN#_r-X z#Y-GlLd6ey!*l4%^DDIAYOXb_b7th#b%0^IGTit zpYp^{K2jnc^~*0mdPlnU{F@(>|M1?mm2`c!^U*sQB1*H@vg* z*ZgUf?|%h({d<4=H}qBa{`m1LChOaG`Im>PXz5yZ_hB=|Ro9Q;>JY0%5Fan=ouJ!* zor&2W=mHg``hD3MSL+m+>rbMg;)j0hhMfa{kN>d0{f-xZH2LnoL)2GYQ2F$CZXLdx z%6xP4#SdY3A39T9VQQ_3ofWfP5-}Loj6{3QiI5}5P=*@vZSW1x8RQ@#6D_ErVkq;l zPrmdXcS$_t=~?C1#j8H?^zw(E_R04=>1Y1)zH|4h@K2A~-G|H+FYyU0RQ!_n)Sr9P zTbysae0&D;&h-cV^f@;l{p!2FW_|G)-#+m9e;PjTOYH80XNvokoQ5IQZp!Q;4qC-#IYnXI{ebl&0@d`8^*+&90={q_OBf7p#r{&f1e$(Qx-dc$+? z-tAC{Cp_Q<+wAUxW{SHLuCBW(>m!Af$+8X2qH0Jp+Z+3FVhp1G8dA=w(-kJA&lUeI z^qHr8_>Aj6^KZ{)>Q8$7(|#siaN`f2@_|c#R{VP-o1njYKX&)xnPRmT_)&;WppuY9 zqH+yW=%%YD(IPB1qau#BBVxVpISK*-FlHm~aR=YN78Sto7}uzvv&o`qL+~yU0wj=}1M>a6!#qwx(hQ-Dx4|4s18okz!`GECvm0)q+v7 z3xX9YzUuAUR}p8u_w&`qo_@>U{r*vp`RG-T-+JX;PG9}@Pfxoy*1Z=PAn;7F(oRdg zlE`3aK2pj@oFbPNv`HNvmw<=c4H5<#n83NqQSSUv1UG#7xmP`FWA%ok)@#rGn=f6S zeDnvdHvaA7Z-3gsSN`;gU%dFk>~4CdxWBkBJ8bcbzN(9F{`lK}^42eZ;ythUxtl#(6H~jh;XI=c*XFu&J7riynxZ;dQp7Wa2OnFdHgHb z-Na1s5`KnI@q?fHflGe-zJ4r%pPjhRBZ|*#zw_vKUibD3pYL9XSFic)FV16k<1@ue zcojm$F8h()H^1?pf=h$*|Mj8U>ni{FgV48rk^1!WzWS8dw_X`s!0yIoikI*pgo?j? zWW%WLzwGPZ_`?0KF%DdIx4*^?zxT50tzZ4c<8FD+GG;ZHdI`mN`^?qf0NGq+~(Dd&%ZIV^^wSXKEUoiaHd$*nj<%8D8qaT5qglV71@zN z$OWt|@mS59phHxP6{^Z;EQN|MJL8RSKKi0}_utd$cc1XY!P8!y-+cR-n+M#rADw%X z`_=c~o!yPh6bJdVty66&kJPP5vfdUQ-$t|9EIUxRTz4>$#+{+u0a<~2bHyL-fA@-S zfBEd2v|m5rlJ51_uAm$2bDr~;7rXA=f9U_}UN2JF-3QDRFX5C3TdcqAs~guHz3i%w zeddR2H~olFo^PF>d0TPi4E;CHIC9NJ*MFJaJr=f@QgF@`sA?-Z9>>fH-)fXdLLN;V zzeiQQW-l7Yn8-vQL-Qg1;ae~JE4K2m-^M<~yV6ge9DViRlh?lr{x$o2@^Ovt-}0en zvbz_~6ffa!2wVJ>PaXUDUqABvzdY+{=U;U7`ES14#g{+&j9cIJH*b9AjZeMnkN)cY zSFpSH4_hqexniqX^Be}bT_Y2FfE%>rKli77?n%u`Ly|wmpvcnMEI*y1yPeZRl_#N{vC zeC!n;`8U(-7L3F6leIrlFf8olx# zF5hK$&z~t?!mSW0zGXvfzSjKR$6x#|iQRqoRhQM@_k>4Xa~k5^kA3L7ANhYi{t0&X z=uGhvE`(6={jbC>_(kOBYVO5f>?vP(=I_7wqg(#vJC8OW^_aVT@)@m{y$EJ^&zmV; z!WR%Ke)-qSH=5tO*N4m1OP{2E^xmgmuyrhQI{dl!ji1Fn=Hl~HuVHtO%oHzi`VSTV z#meXJ`gL@y{>vpFkNxZE&#&)9uK&W_UUHUk=}-Uhh2Q5c`Wm}??o4s1>6x|c2saye zkP^&3q1T#z#0G&%g6@yGvOjE!iGgA?6h2gZzM(w4y!PyKuKUi_>V-%D^7`+--~$)5 zuRr5s@A}#EzWSV36u-^xo-UYr3JnYBcd~AW;z4uJ<5|ef<-M!aL@e=-qP_g!-m;U71 z=RW8GUv!^gocGEf44-h%eGl1%?*5!Z)|;rWesk?z?Cw2hikEO4go>|@|7-5|Tc17q zOW%FZZ${sE>X(hLz2?>@e(buhJYo0CkJw!~GsEtlJyX1ds~}YTtIt34*uTE(=j+I` zE^%w`@4WjR&;Fot$;;pNt&ct7<^OX2H{bs$aOrfWc!?XQq2dR;=<;8eE?HII^o*Nd z_l3`${>mr+>gD&o_1D*3^1Lb?%V{sancY2Wrg(|_ZlU7yULro?2bU$9KYHo2-u~UM zeW4eB;YHtg!AEv~Ej{x*>eARH7wl{hE5(%$t!&@8{jBXa$k%`CCbs_8^~bN*cCOfA zcaE;#XYB`T*RMVKv`?P)g44QxxDl{=Y&*L3yRDB;ZwBn!+4RD@0sDWl|9$(PI=vyV z{}KBy-2Ua>djc!JTKUlG->?4d>La(Fx20{Rwl?;CcHc|(_4hq^t+{so>c1YSf+_>w z-+9;O&6}@U%5g9YuHd3>hDQS|hIb<|+t%g&kS~}Bbn6*Q_v6^ohzmmmDh4$pofW{P%t=7tZLKn3^0^#a$<#bv=yD0GQyKZS zEa~3kaZ4i}yEGzbGG!z$mFZ%wiPS(C4#e1LcX*8$-AAs zG$IVZZ7$I&JG7CCM#0_*g+QPRBj##UU^GFZM~SK!Fb$Vuc#ZBey$L?Rteur*5&M6? zG~#z50@4IJQ7)#Ok_WCDk(SwHvqDdo(Qbd7WUencX^XX~dDG5$A>o3T+INQOjfc36Ni^>hL33lGP3ZM`A4Qq@#Kuv5ec* z(At?{v~qI^#C&O90`Y;cRbanRC_4n!F{7I2 z=F*7pvP+Z8C@~b8RC+qwASx6?#9ebz$%8Nosd`D5{UPQ{a9inI_GXs?J0OV1L^T9 zCbtQbRyZL=_NanT8=QG3GSNli6ydjw5{B8-;&Z7hL! z)6$6f`!G0d2tzHhSrcO5JBbZ4Gz#6cUv)6X#K?fsJ*o+cG>C5Uzel_YR`-2r-<$S5d7ros*?0QpcQ*fi z^XkLrY}%W}&2u+?wegvaYc?LY!EeMi_OE|q{hjMiU)R^s^|RJ~y!Mf`m#%zot+sam zwbj)xt-fjXDXY@zgICYk`tjCBPP_TEH=cIIX@3p84}Wp^{~Uhb;paM_aQmM70jEAp z+;_dUb}mO&>PE+vYFxdm@+Z^t-mMQ`uwGj|ms71asbbxVl&P2cb*{X(p7SM(i3y8| z@r#LZVJ{?+7z=K7*cLQ2PZ08W&(m1_d??4XGk!IrlQdTnG67nZ_Qx&WHL=`POa-<%)K1Qp~lDLcUfl4uX-fN8ifj3(7rdLAfV}a(T4Z?}+TMoAGKT z-|vWf?5t@tl=@y1>eFmitk)H6T;fnrVSwK| zc2^&iPkXjvi+f9euibw^xo~_mwNvQ}$48S7^cu+C8P*gBFy#IFxG&>>wyBLMP}9nz~%xJHMe z&nzhS>9D_)MxuJTLj*J>021U!d!FBFaY4C4D2F%jiqf-nqa^5cZ|wZ>v>QiOt`A=? zn$4)05^MLyoDhIy7JF%>EheUhy$#A%k?T#^+$fNG7^7%=da?5MP!43P%g9DPBaMdj za;Y)g`;=E77s}BnRnIt;aS_}q%wqw%S8nZX3(AFqh&iRcH;4Mj97FZ_`fx;y8dZZY zV0-$p>V|T8Hs{y#LABPy+IcsdvG&USXhFFjE-3dev)4hJ3PA;E)^rAH5IATJ-D5Yg zm{@#fl{QPb$eXx9QS2lKZt(7%`c_}MpxjFqlzZ`laxYp??u84=y&#lR98vZ4yd_u) zmCsGg;-06s`uGLqE?Zeyy)zBhsm~#QR*CRk8K%!p#)^WsNw1x2HgC7Bc4l&HMYc81 z$`}D@)ntbX#qgi=cd@*wZrwe2?7fS3`ZLl>163muI@H%*q3)X621~ zzqxnDTicq+aeYS6`BJ9IR`MMe>*ptX?5@q1laAj_+)xSIR3l zu1quW9s0+$@2|0k4~7}}){buC+rQd8egCn;51nS_TePmft6o|5d#O_a`>h&bKv%e!rXz| z(!ocsK6UMi1DBon;=gmn*7Ns$ZW?g_VhCor16O}C%@DZQV43+njAOzv1w_Y^6Od~Q z>UefTp`+z4mhYAAiB(`zDN|_(39D_RJE6DI5F2^EXX0`a?+$Zpqm{P`J}(#a7)fYB zfmGYP8mlEMeFxnR{j^2`9H<>cEat0mqvR1JDinw7-Tvz>g&z4Yt*Df&qJjW8%?nAHW?y(#&L8=!-cf3xUFt0 zTW^hMcU-LXkyfQv0!43(S}qloQqi4PgotuFk(1m;ne6sk%#aZZX?>7|M`OhAw1)hI zr{s!9Mh9J3+WzbeVZv=w>_`?b8r@1%t3*a)JDY;5Bt4moz{HW0;&5x?P)dOA5B>jL z5IR-1@?#Zs2Msan!{Fmcr%YGJqkN+U)yhQR#|lln)*MB*?>V#4Rn0M;^dQ9WO*J=c z2GzQb8*r)Y5v^zh%6Zm+ZFsp!1sZLKU%QN9EnZCVWv?p5{9LM^hCL1$gV2hh7Kf{f zXEFhmNayppvaufe?2Nc4r(g^9`J(M)+l2@poA})#84t!0xacr8mdiC@vzM*GTjBSy zuADAY$zCyOCx$u!X=c|QMOzJ)8YzNAcPoh^Uhj1WEUHLb;g>;B89nY(g_I$FG)BJ+1`jrEU)*`8FNFIt{+*I(VXl`uGH1d1Z*eKzvI@1<{a|WLydy zQucw1+pa+EE>*KrDXA|Ga&^mB%Es!PS7;*IuhX=RRN8Pt$QB%D~%|;@#R_KBLfk%q@T)EikMglunb)9x2r`4=9z(QRN<)0SY*5}`lBkE3>yV? z!A`b^HOxxdcL8}aWu^MG4V-Rz!WU&X(u+k3$@Fln z;@TuqRIpUi1IdG$kTqD8wYL--h> z<$T@@aAb_2{N{Fu5GFxsAd~`iG9dG0l&YoUje$|a!5D|c3|=->IAY5kalq^gyfVE@Gm0lu`=GRwvaUZIo%Wwm%ml5P}{{k;ypHZF{C?cYQ)>R@!Vli;@E|QL9dr zwwi#`PKH_gVu*-GMx}8>FQ|iFPLJCWL2o4mzEeRMXn}0{GsrJnt`#k zJT=O8jcP^6(a}6dIvCbq^}Hc>4k7^!vBCn)Tu34JK3lUdQnTQvZyZK~WOk^Cb}f-KVsoU~+{E^dtZ5%_bLiQz(5sf;Nr9fIcr3GQxz{CDuet`O@d^S$-H{_jG2w@fO6#^M%0u641-Gw zrF`5ia*j~U$7LGVO|47ivpiceG4+rXBHV7CZX}s*uTx>fWUn=Li)bu|#1TDF%u7R# zPD8^4LQ56&PsYvHz~(!++)j0Q#1#tEa$ zc`VbX3GlfN;zc5#Fl`j{%Ah_028xcCeLUk?Kl@ortIpSRcMD}}ZDj6S5(!ARB+u0&J zjE@J|%{keF1mTXotpS5F_8M&a1w(pE2}3i>Y_wp-bWTI0B0c)#I3lTku$xw2S^Yav#h{&W^@32ZPp3!Rk z(faR0glU)+#fota))P`nzdy_{c)o14{B~PaQWGl;zWn6GP8C77=35`|u$nNNuqqzi2zPK@(PZUOw-_6%nuyoLSW8XC3Lt8v0;jTiT$GGfP3t=Oq>w@$mLbL&VU}>XT!hFCuh7 zj)A!1WBs1%Vg*@9tH=6jthSKU z9E}QtLMj5bctmajPJwvWK~PS2t3lplz}%wJ{DvO(Tx5zMK0PiuVkYUJ6^|pHY$i(Fp$&c%IIJIWlmO)E%tJEKYPIuXl93!01MTeah*FSfA={JIe|6{i|IYLOo#+2M z&;NIx|L;8iPZLph{}-PB=Nsxf&;Ngx^Z(qby~O!HJU1Ilod4$swI$B~r-fVBCC>k1 zh*;wMUz#EAJpbQ${{J6${-66w?mYkhFFOB+ar1Yc|NmE=|EKx?&RcoR%4ttK?E#0M ze>isNBZv5dKRYNN__qVk0Ga=8*oo|a|Ncw1zqft)*3Y-D+&a4NE&HmQ@7-iJez0*V z$QE$L`n}iQww7DHWwo|)D^RoSKjrwv`xKB+bLD8DCCoAK5xbotJpvb$dC|o*R?0w$ z!KBxS)P*!ej*D6>r;3=HXKMUI=1uiD0_-G!odJs4e$P%q)h^YCl(g7RTmGOY z_w9+u*HMeFl+-vb6NXq@*v@U!Za(hNaTu7o?<3DR+9}p>pNo!qg<3sYi0GLC$CE-X zuu0sW)O=+WsGUZu)htEeg-zXI&do$(PyEUB0f@)b)AoR_gJN*N>W=58xI3EibUK`3KK?>s?kU;F}D3N2}&w~ zaD0a7XWD2hpt+7(%hvPO!bKKtpW#wD4gq^p+Bo@286}arAazSvn~k|9QuF~scrJzI;Y3`8z$@3yqL2-um@ zrd8^&p6?~QP6N#tsd0_+x)F(jTgh6(b+UD{>U&tS(=YXk3)|U4n+Hx`6QIo!g(?;{ zb;q>XTPq`+Z6ee5fUYl5(PH8DoS@AEfQ>0_mMCGda5;sO(dHPiH>J%IiD4JEcU#(A z2<%L0vqYZQh3)L2&HbmZ3D9PV%&`lbx?|czPdeM&Z`vNv^(7L{F5I3Iw7D;^F{RBC z`6L&vp>Q(VTmbA%X|qI{$%XCRmNw@DJF`VfFJ{FUT2lRt7SnqJapdEv_0SvFOj}-;r5)M%@JT@ zN}DC}LM~i5;bgQq7ucK9W{HXw3){ObZO#F9rnFfid*s4)_R!`&)7J!OvqT+=g-zWt zZT6PJ2x)WgX?sA|mq zmKoef9tkuL9A)^QQs4yx1(-haNA*RH0L_kN^};?JIdz|>09WaQ?oD+)T46@#a}jU?iG=d~$HvictAK7c*yNH{CHnm(9nna+As30ZdsrWo|a-mIbjk?yIe zI21k6Y59%`6!d-5ymX3J2A&4~KMsDLJucT1y{W28rVnuTKP^KErY4tZxaH_7XdpnD zmL!^*<$4#xa5Oq?NBFmvt2jf&Y0chk3{M7`qCeDButh%)pRVK zs9?EaJSRt^Mikt%!P6dJm+~$NvL2N)JrL?zPT7={7mG)xU2}D{FM7T+SG1=gQ{9Rj zhoBg8vDl(y)SBAJYPkVyX-2A^JJv5vVtx!d=Jzfdj!UERFgJp#fk$TZsMk<@g38O8 z5eY6_2%usH%}0kqXTYTVM!CzV`mxcdcPvv#)h8T^9=UYd9=|_rkuoJd;I^uqr0bVn zy8c9c_sDeQJ-M~I4Bt=9!o@~j3PEl^@}^YSGnS^m+A})BWpet4{~Jcf%p~BGt6MYL zC8zC}wa8j-)J@;ii9rzqKc8Yy{DII@`t0EtfZq|>q-7cs-UMD*~3t3S2dwL&R#f-!bxfhEYoS!k9!$wN$a_`PwjT2|)=i;ER!c+I2wo z%XfCq-BxOkTR(p3>d_@KfuDL?wTJBxcop7wzU&-{rQqp%JITV6Lg3re|H#oL5@=5q zA6+7S^-O+=JlZE;*p^6kK2xw~==K!X1vApLEw>%J1*#foLRBIRXV~avibHOXgJ|{? zPr0(~tv5DB-F9VbTk>pyp%vFVXWo=^;!~-ftNx*-o-+1kTt6{9(?H_(!}CvFdSX!G zKU{h&(r5IJa8Yi|_jAY4bTvmQ8O|&p%Ydn#tg*dnMlG94&ZqwLrKeFH^mz~!UP8*9 z7@jB+M8WV_-jnKNtj&{&MuMd)Zj-7G@t*I}0m(BWH_Rm@#@CH>vFH!--5j5Z2ZLS_ zEs3((AhW2t(9$#QdN>--`S;Pd1f%}bqcJ_>(e1Z^DJ1Qyv8e^v(jnl3C zf8A(|rV|KIc+#~o6HDgCUyBUOOC~Sp*NfaX?vn#|>1G3&_i;`WpiK_G`uZ_8Z z5r734Vj+tON{EEIPR`UlYyjD`n}zYN)^VI-4CgdW1NXR0YHT}dbX21!;$URhYJPy$ z(MqkxAx;M}x>lk;9AIFnUSK0Q?K&8Zv-AJzbt{MNy8r6U3&9U}{`>#<6L^xh+nODk z{nh{4pou@;%s|t#+DY~<>C{ON%}az;Ob7Wgu?REyC1NQ~zIR!sfhRe3{J{Y_ZO(1C zFQ*JfnF3CEFuz^s#uRni?aRM6V0r==_-#`Ur=1hqlq8V9{lxf3N9(D|KWX0*k0z2( ze7d_m=}f6M8K#;cAdycNyR_fc(RMUW}(8Q}>6z-QFt?Gy{WwkN_8Tt-cOiTrJVm#cAVjt2=bL&3-v;W7wWOjSMG( zKQw%XZ;Mke#_UH!my8oXJBlny)85VgqW3}i$*Hey_5e;yHwj?q&p6#A(zEsFq|=Rz zrip}nky{$E!?x)U>=L54xlY3Yg&L~)M4z$Rg&Gpn%acFdX>qLC#JEhAnlJ>B@anN~ z1Jnc>DxU6GUJc8ZrCx<7C!?ii65R1rlw+=%A^DM4Er^o=tGD|ZD4V?q6w%3Ky2ETn z>NkT_XOWE=m~N(B2h$DkjX&~)Z+wX{cj~~sMW>sSx+spJmfJ|ME?Y>k(dF3+f4xsu@EW1tqv zxXuoJEtSkV?V>R@{^W(86OlnF!D_OQ#gCa6$s^5hP$5 z{@0DXleiTFNEyl?#WU(PP_Q(Jys6gc-lP=?H;06e|PAf2fumn+Jp8%_`vTE!v}6W zuzT8v4$ueA-nr$ly>rdZU?;u*cl$rMdgcBr_cu;^>Y?B7zsL4BwqLV7*iM6bfFA^R z1R87Ut$XadW#2XX2K(U6-*4Wyxw}bkp1pC)#x<)S+!$=6*MGPE!SyTG8|(L2yXEk; zr!i~StPNHhtM@qkP4M5f|Kai5`49i{XZgdlZur_!7XcNcvS=zwiL#8-nbOFk4Gyu3 zbx`*cg+>WbK1LVWX4V~r-w+SnZw^fLJxqvB2sa9kux{SQc_s6G8zImk;zHX&Q*Dsf-)*a|Tai?hW6jZ@g5_nUsMKd+*FpgIRS zwAl0#mN!UM#;tT@R8K&HDD%Z=(rF@4B}QiqE>fd4W?m2Zzy%@DcAP@pCzxcRj}5pa zCqNSn%XC<5m>A?32JU1g*|=S)NR@dF)1BYVEeci#2PIT{OwvNqoY$66UdvTzkRy>R z#syL=bQ5u$gOy-j&v@r+A<%_Nak=XbhV2?&ba&nd=LX{yG)@t1%^r-aN`T?H zbe4lzh2w;Yq;+(Y>Cy34p)g?Q<%HMHoIjh%1bEc7#2`^0^+DO3646X^be!jE1p>CV?M^my)sb$uq<-s_X7|-9v#?A>KJhAIdQSuvHDm#(-J&;q+A+b`- ziQ+oS7Amst<J_Hp&E=fJ@r$eBH| z+$EcZHlH2SloU}LIk;#bxfCdTNE>8H>sx*y83NZ{KL=VAHppet_M~UkNNU0gNTQo* zxC~@VIX2RRJL3-2Y7kv78Ui;~=RgzgN91A2$0kHZU@}~)pJgbwD0)4wgI8jCq&OZY zs8pLiLDj(=sA~k~v`TTIFXDb+4*DRu&mhKi=mL0)BZ+YH1dds8kdG#`xN_?WV6iZg zk(5h<5`-O))3MQrF_m5-igpBA<5ZjmiFGJY?z1~5!20v%KrNn^Cc}h2Vlc9r@|{$? z9ZxkuMkU);Be-JvWyhnEYS5cMoYfzMqsmby#cUpjMGCGl3_Hv@_&6@HgcpUHBt%8p zHLX_FE0IQNKB`W1QI$J2NGqf$bP}bsTFWGKv)Zd!KG{bydKA=RGlPj2O`-F_vH6{O zOFOQfFmk=I?$8F91Qd59juSjV*vUydp+`-%N+)6gOy=jn?VrwDsxZhkJt8V5NwCL?yzP0lCByUY!?yG5Myq9EaVRYi%qFUNanSFclC9kW^y z#No>%?f1*A-h7H%J28z5W-=MkTq?(O@@|WbyPc}Qdz|l={RrKcO9;=D#!WQVV8XY! z`MtSCL3U6zHm;JBDyd520i49KAqm%oC{-ktUdGqz5rnaG%ADd`FFpYzYEDTkwq>@^ zwBdMzPRLymWxMTCw-4TrnHofWq~0tU^S8Luo&$MHZI+`cqz@HFLd`72H3RC7GfX$8 z4XAt{6t<1Nci?u}a9hx@R5+qL4`Lo$L@fypW zbO!U_LOq^|@swyxy1hJC9MyT-B#~rgIOjn*U)NmRu?%hNuO zHV_%|$X-M030)Kv`*l)qd&Jk~eO&*_31F6kT1lEv@j?zukJE%_kE?05sql%mT52eP z$f9Yt11cVePj%msIk4bJNe>~$M0Qwg=$U*WuuZ3v*6K=*X24yiOkK-Q@T?EEF`5{M zQfueWfs5bTn_Ikj4g@PkCtWF}tUxR$8ASA$#vshWsT>L6@h=|?L%1ee%30k%Fk z2cijK`dsTtIfX7Yd!|I4F@sc}}^N z6U<52WV1p+nP`-N5@k8cI$m5RL9V;H9Vw_H0*F`wpBUB7{Ne12%`HZd3@y4@qQ?4C z&w#jImY)ojsF2FaDZHu`m^@{~0{h?lxIN$G+LMx$^E?PlW)rflI<1I>s8rRqJHwPq zx1!7_mhR}@uytbL*tj&bXb*XHSnijFN;}T$Iv43XAdPXH8$u|^yxu680}MA9J>Mke zB;WkM?7az?>sWavT>Gu{UI-Zo!w+&Wy`jt45?_dY-{l% z+p-OGr-1-TcLhj7!Zd^s!X$)bvJuv1C;1^EED6aZGht@3Jz)u9NQRJ&$^ReOzPInK z>uO8Y?(WN^>pqX4x~^B}yyxhA-`T#!x%?m*1PhtWumvKe5==UaHozRE=}gwq4{6rO z+Ib<&8_5wpogCMu@3GwtD}!T18nkV_77L}zjY@#hqitQ6X;Tx)EXsn&k{Br|Q$@b~ zD-&JS?xG~xam+z6nkk2}B2#1?gzXDtKRpN!gh4H#3S~0cQCrg*FZWHGGs|&gL?0s+ zzMZHzFwqzk31isrx4CKpi?Ohnl2topO@(6q8^I^ zd3ibZE0YI142b-!*Gsh8SJD)Wv=3Q(npr9Cp{I55^5zjP-Ca7zqSwApdJM zh3DdW4jptG4JySXO)%^`g*1gt7e_m9nLKC*>&bFQ34pkXvd|_l71*TUNPyD`y&Ix3 z$vzH(2_tY83QT9mi?5hINT{K7GLec1Hd&L7+rv&#Zigz_0eFcKJr+&D;ns0w0G8*| zDQE9LO&)CPeLVmN63s%c&f7?~YNTVuL|G;?0TqsDaiT@E&0dU*ji#?-Z@T2P5e{vJ z1hyy0IlL7**0F4agq>ktt)`>0V%X4ewTaS2Av~P{_PA;1C3GqS9v zlIc;8lf%aQ|L2w?OPBue(mO8Qb&0+7?2G?;@$(noe(|Lj3m2cc_pQC(+xxk_ z7w_fwzGwHJcR#!Pw%y@wcK5q?{&DBiJ8#`_b}~CRZ+~<9Q`>LZ?ro>GpT6~tt>4~y z^On6uZ9R4K?>0ZN`KHbGW@7Ux7rqWo2JXAiyg*!d^2XOTKECmW4Ra&5@uc;yg8YC# zv97P*vHtJYzOwe=wI5%ruf1^X39DaP{ov}4u4=2#Uw!<_U#;A~^4b-3<+hbe%YV82 zzU3bQnE@jY%a-3qq0d=kR#tAVk#^p!a8h$12;H1v5BO82Ydas=`CG5h5AS^4EA&G< z|H~`%gF9dI3jM&&-*|=Izw=eE(D(2BwO8o-cD~{j`re%{dxgGd=Sx#)!l`QtFX#@R zEjp@EpiaN7>dsGkh4MQ;;T6j5yxuFcvh(9!q2-+)^9n8P{HRwbyYo7)(BjT(y+R8+ zubD!L=qOg<$q3w^Vefuq0d{7dWGJ$euq~mwto8*s`nKhB*0*D z2|UW^BmLS$g%Gs4yBYEd-PuIFLbo?BkF2=_wl*)1thvz5&C4TeF7(3Y<&jmbH&`oA zOKgYWsCJI4o)YeLW4$wN)?BE*-u4P@thc;E>+4Og(Av7?6{@Y9UZK@>!z)x>*S$iO z^@dldyk4I|mH5y!4Y|~BfZbZZJTy+dt(~{+{GnIq&+hzzSLmB|KJ69y#+~2w3cYXV zQ(mF}ap!luLf^3S+fyjWX#w&>Cx!!4X{DpObvoU&o2_X>UY>ifMy-?jQauh8FE zeXm#OJ6GT175eL|@AeA)wbkj&U7g!mzq&e|x#vTFWpz4p&xii<>U8F==2DrKC3Uql z*bA2laIA7_Xs=mo_Ow}Zq2}6Uoi!I~tX*SfP2h!7Be?V*~SOL=1*+z>SCA&E|Yk&-C*su+< zt)+}Y4dWH*uxf&=GTOPkUmx%XG>6CCj%eAywi39Hx_;fj^kkNWJ%r5iNAeMF!!6j4 zTkja}!vTM`X`hccCqDdjTHuXL6SkD7)!EFsi$!-&h-wQ=l6cuJ1}}v_bUHD zx9kV^4qU81QW!zbG``D;lEk;kKtZhAb!iwVClcv$xm;xM;z%~qT#`uylW={I6e8!g zM^dD4-(P zl)8yb1J_E4I*ErcUaUbm1mvk9ym<$@O$YW!uUbEOhHHFx?~|4n8#w1$v)8jjLIl4p zWzR>p{u-zL34VjS!l@OPbv}@FR%7XWbmzV8M zJ&%F_ULgP+J|1Z|Gaz>;8gV^KX3E_tCYEzzm8yr@!F)Sh4AX60AUk*!57b+AXw*0I zaJ;HZ=_-G2xiiR4yN`iLG6O4%^_m(UjL4YIK> zYCauetW;a7B1pO)c3LbUwU1O9O4gYYs&s1EyjjcTC144g2X=ATFxPB1(ow>j|G(RG zpl;m#|C5$JzjTQMcmAt;;_e4`pRl8E-@pCjt?uUUZ{B+06&qjJc<%bE*8XJeX!YeQ zf3$M2d>4S<$Uk>K>*V>DmX_5pv$V9exxBvqqRs97hWD)D;;p8rsJHf7w*vESL+#w^ z>X!LIW4D*hlTUqM;C_AxIm5H`x*)cI>+?==-P+ffVGlyf!Kf_6F-T!zDqFGZNHiG{ zE0Bmsr9-h`fjA~uQa>lxEtlo}w~ZOU#0S^guH$NS@p(ynOW zem!1`$AD31p3+loN@zY5uJ8L>m+SlA;)5%8itFaSDurb;C6;1Q@-Rgnc9L9JsErDd zLCWYH+QXn89U54Y<8rokPOh6S%l9VT&-X3+;2OD(tJOtYyp*TA$LT zzsqLXdaSG@(;6Je3U&^m&)KgRT$aBv?%j92)d$zmbzFxj{cw~(lTf{rgG%+|kyx{g zC=4fn`6_QfH8vS76cDL9grJASb$!fu*E83fPf(ZZvmek|8~c@18*VZci)NIdiK#Gw z=eii;)Fa(!JKds(1mEMxP#~v;m@A-YHc`Hs8um+o=-{_?tKzWXOZm+#R7eAoQMVxRK8vL*5mG=0rw_zRzLKi}~; zo_Rjw1a^4_AKgJ|o1y{2pC(IsW{C`}y`~duDpmNx)_L%mt&u#a_LZuxSmtGt?rHk`$mwHs^oGy64Bzm1&kUb?a_BO=MjiQx z(Le3R8R}?mkB-&RkKgR_ef>8*^W8sruFLltb>t()|CH}l>S!*{u{wI)54t>mRPfC6 z87BuW&ui3?j~oG0o>yKBgMcKK^m*0xxG17 zNB4Z$<#_Tm&m5n2^6b;<$Vb+JDaENenoH5;_Ud=KpFeb)XNEVQJj?CHtJKkkpKH-8 zy*N=vb8ia9!hUzxC3*MYERv`1>+XK;$unJ&*9g0h8^b|2^HQ@&RT`&^!5VILj2JV*cPm8ZM^fBKTRbm=Z&_y1C0@Bd<8=RXAY z{SL6}?*V&$8`$x;f&G3H*zKFZUS9`x`gLHRuK~M!71-m;OMea?Kl|s-`*Lfl%J@mu zwAh?PcixxvO<(cWwVA(FxbwcaZvqQVYjx*+gl_^qwxM2n z03Q?Zkq>RruOQ}|z=B^v)Hi_zzk)l)1bh^_=vQ#NZvqQ`1uq;E@R7mJ&-sBDDR{w{ zfRC-}V)LfmdEfJW6Y#NRnZMb)^SV?2E z^uU5&0dTJe1bh^_=vNT(O<=*V0G+%75rp?RuH<_GHPeO&z7oX56cTQbwOoW%3^|)r zu|DtsPldI`<}1AOK4eV5$2NMgX}a#bFX)?q4@;E!HZgbJ2m2??RI{{PuW7dPf#fMMprWy4G#+I#+_c*;WBHH?{Qm^m{run#ppHW2fnNym5}r@FJf z6Z{bLWtchlOxEQ;H~v3-`u@dz>PCtGAHc8xdbUAyDW4N?+yRkMvY1^9#Hu5_AvVlv zkP3<@n$-D_Pz3vpaIJX$yhSvKmlA>;T?~;(dXy{1Wd6vELd_#2P-?U*<pi93&`l6& zw5Jw_Jz7ffdRT5ZNxmD(V(EiQ&MBkaMm5$yfBb(q#MViLigQW&Ky8l%122~nL%UK> zNZp3RRP!Q69VkW>KXL{gkXJO_!*WQyS5WGBPYOy6zNJSy90m|ex_}A7CjZS+@HRhF%8d-|NjUnj)xHcA0DTI ze3;_@&s;m4EB-&2?H~~A>@3MMBqfWn0WJn%XkP^JWou5O5oNVdpKo-_X@)j&o(qMJ z_~H3o5OWzFiPkBpkuv43!}1_bbcTXc?V#H0!xWz|)Ow|cV4;x0D0(iQp%Z*D+|$)a z!fGmpVTXqYqfA#rd$D|)tB__?&GiWnAJU2J4kMX`Z$wRM~ycx1I zLpo;1|9^y}1qQu+fXXIfs8T}+!kwf|pRdF#@y zvvsik8t}_wKacgm4b=lc9Uom*#|ym(I(6O=0cvN~c}HM|BkDs@#~l%`lq(EragwR4 zgBG4WS3m=7##2|H7o1leUsvuUr;afc9cLVyx$19~sZ<3v0xj(*#uTE!J-R|hxRz>m z2_YJa_j08+7B05i#s2xdu5)}+Omt;2lOsui5)Czu)k`uROAcv89L7dcI$jSY62Rxh z2qaQ?%_s!3p#%mLb&46lbgL|?lo~~AN(pC;9@A+B2^sgc>UWzC)G-K6a_g1QBtA@n z&$akCM`)73Oz%)1$06fG2p_Ae1sz&=w_BbS9^s6{<&V(9>iPgAKvSI&ujFcBU1;KR z3<)4-?hBd?lp^vSvuIih9ErB$M!63uq!dfjX0n4Lu?p7-gvnwebbeRKbQEz8qYILWn( zAVG>5wkrJ~tXI4(mBvkXcjVN`;dn>>RInrWKA_GenlmS&nTdCAc-iw|<}ge4Tn7SI z30xfu;gJ!%t7U1SVScb60-qV>K*>bya+l;XiJZgsdLSWBP_MPn0pF?#2`)rfc)f?B zy{O}y|8z^tS2QHoVLCtx@CpPCs{t0cm841*o6iM%fzd%f!5Ui9A~86Ps!pR=%Ziww z7m`RqM8l&(A)8NiT6wG40EvjA5fyPTkFw-8-JR#wPoC?_k{jm7#{gNFEI~7q>E7_N z1k)-5xLJQ`tsFc>~Nt98zSID0^WU|UB*AW*Cg2!vL7;FT3gXbl2K z3&~NZ(ve$3_Wa>S=$L>Txfa%Bdn}ShIWb=o54uPeE>{wEx7KVQFg71c){cgQaFi@6 zm_rsy{bo#2nS;`RF5@7TRF%(0dIydb%?+xfRy!E=cG%@M9SEWu|9^Ys!qUb2_K=;= zY}d9{F1%?2Tl@T~3BLT98)754T)iP^{9jGtWy;2jULiky1Q&hX;Kg zPC#M>>m|wY@3u%{BZX)pIU?%dji^Jj*_J?-dkSfoVx>Gxl3hb(nhs6G9UeCHB&iru zJ&GlXcp8*B;!S|ZV*wd14>|Ym1f(%!q9?&fmu^G{IW`WSO;u@D8)QdOC&SVKy3w*F zI9rp*T3-$_p&X$(G{}Jh>VU^}Pz6*bOIp4dtMMSbqa38&x`rTEj$0Qf8*KvD@rJ^2uswbw z*Y+}j@g2N^#;Bk^bjVRHp4BmO6gloR%NR(ZS-0C-IO`k>jGWi5>gDg9*UQk2+{?s* zdYRDMWe5}5My)NHHM2yGg0(K`%xHB&OA$52@SErXbNRGV}(E_ z8a!Mc$&RZ`l0f0P3JdK^X4Y^OlQisFDp_W#W~)Y1a!97il&HB_aCZe8mSiR%g-aR9 zIMgqvdTMY8!NXDw8x;FSs9D61LCTo6l+V;SDJ30KhH_XmaqWt}c*>lCjGR8IyMdH4 zlv|X#h+|w4YBj-0TPkP8Mx|1sP%0ra1~Sp<4@-1E-i>16K|Pqs0UeycI>m4doJ4ec zS;{KNAu>pIB>8G}!O!dE)5ah-R4*?|T~JC)B@+q3X!YrGd{nTEfB?vY^uqev(wa$%#)9U^ogVFeSlwr=$jSE~zdUN4{a_q~yNIXhaAQU-bCq+oWG zg9kl{RC47~7D^lXjIoUK!9za7bsN3PsISXS3`+*hv_y~y6;aI&-mmvbMQ%ftW|g^0 zDOcw8a_EuL%Kn#ePr+7S&5593e< zf?+n5wyG*(3gI9TAV^eKhxHza^LSM+m*@2|dL#GpqV#ei)h-uX&8pE#uvDzd5Z#)> zmS*%a5lz>t=|ah@H%K$x&B-VoOofzEB+PTopjezWmT`?2&1Of0t%6C_jYMgvV~q$gavEr(n&SJRm}M!) zeADjAS8EJ*V*mfdrQKI3Eg$23ctp;&vG?KEd%okzN9gYSMme4Y@k!k{=QHoa3wnd5JKd@pXyTcHst#zr z5rDb@+Abc1Dm{?Kw3jNC6O!s)K)a?5b%`%Zjy|L zFZw?g4u{W7!@+1x2`_Xc%t_Kb-s>`<5S^u=j!BurW)CZ54(fIx53=qS&Tke-v{|%3 zq>$oJka{33eS_qUBFbwKvc}{(okW<5;WXMO2U4Za^p9E!4~Ni1fwOHi$eKbjPKBT_ z+D#}!niqLmm6{+-o2RhA(v&V{B6?;Y(d<}u`)V^wUCsW8YM3p6)D){5ra%=k4HSHc3 z2cA4L0@tL%2N1GoH9?AV(Kr^3y6Dz*b=eae`tg@8#|fWPB<-%)Rj>MP{JbPumy6Qt zvVQq@kXQsHWV-yny4A0D-R6ut;Ig`LFY6ypbpxGkg`DH7x}EG+DK1RVmYwAXbVmtQ zYFv#gMNMeXiY0?b3|uZZ&VLJ8!vsbwgge5pg=FK@QKC$PWcPY+C@DS5=qPM2GFApxD>qr@mh9+i$TF=im5(+Kv(s<)&ESQs?jo#y)|(0G~$ z&MrP=ZgJ{nX4W4+>c*W#uXA?sTfiyxG27YY5t@P@in9ymD!Yfv+2uNY1?oDlvr8x# zKGUeJsK#q;L>~4d$#mq%=q1E@RcXZ(DiC`D^-HgZUW@Mdu8HXTUbc>llhYGD7r`OWnn_~o&m$9mvV=mB8mFS|2<>vYvmM3@Yd z??ZYj->wjmdWNVg#a<%;)sCq3>SR-BsyQ+-GLol`1+P7E zLEZiIEaS@1j0NWmo$h{hN=wgnx|++&RgrU}0t8PV*t<_Hz{>yna^1Dm<>k)9>2w7{ zNC4OXg4$-DYQ*&dnnjgH8c!&xBLoU0%`_e3BX+FSs8UUe>&rc@5zg20=ReiZiE@VR znMNa6)0rV5;X_L@I%&ZSqK!(el_@fHA)I84pg@#ku~ziLXGYqVw#b4 zq&5sNse0*f2v%Y25VdbEiS;g_=p-RK{X!;H?6)3#w7dQMfR%p$L&u{Iqz)0y<7P)u zJ9Utw4lK~ap_vPyRy5k~nQa8kH*yxLhD9md?6pkAYL7&|5N-0=lm_EUS{E6&;QfeB z_7nui=stRk;el9F$CFsHA>=CcQa{pWN2&0DqE#-|PLA=_B!l-j$T2CV08xi^qQ&vF zkQlaUygg!isx#E8#pB~j>M%0m|bZY z;bfFV_*n;zXV~@6?d5SV&PzEOJbgjoK6SP1UN+%}-9KD!%^bd?y{%M*-THHwvhV08x(MZ&>*?qp57k>YA*GhMN{ zLajt%F%MZrr%j&-pm;?9eGH<9EP%*(=*$wp8D3_$?BDPLSdI1i$H$QpoJgYWB-sxP z#4KJ?LQti`j4Dd5!I&{Vpb(Ox-M9i6m@j|}wEqXAFboIJ6u?YgkPC8U)M?=~l1N$g z1e9qxbeKyX;ta zmeQy~;7BbeW?E60(?f}PpjT~14}ydxN1;}y1NA7=B1t?a^wK)o89`or?59l!0thbH zzTaK$x%U4)#?ZOsZ{~7urXSl4FK)wH&=|-DYgd!3Q#fRJ(T>&1GJytLBeQr|5W*-u zNY(;`9i1g^Gw!v(JaDyV%_B7R%^>o~+y8^nhz3Hg+waVoP$|etxj>X0qe5ldHHPjP zaa?6G$vT~*BkIV~D5qXLKBZ3;v2z1*6GKrFkA}s!Z1Yz zWcf&;`WTr=H4p@2owAG`G?GcXdxQlg&LApEz<{bus}M|MrGe}vs@1gV)BXS9(z})} z{lcZf#ecc@wu|)MH}`&ekJ|m)-TQX&ov-e^e&>bTU)+A}Hn#QWTlZ`soBw_Dm77Nw z{`kV(7oM~6`Hh!se9!u4)}8g6)_!}fv-XtL-&{3UpSbejmDbDoq{{sgFa!$J;DKtOd8;_W?N$nI z)iJYF7utG8w4&^ZJ}&n6Jro-W3ht{0BeW+F0Zw<~5erW1;h730O@ z42iHO4bR2Gh8+Q>(IYA~=;k42ID~Pv57q3tDUa$zs+fyPT0Rrc+cla!sgH~OJr8Vs z6E7F+2~N|98gZ$!ZWQ^v)9#sgbl#meJiKkcDdSuQ7&b=K)q zEEN$CWrjwvjvO5YgbYbS3I!kL1R)7IR1Wm5G%ogcJrpaHR3~4~99v0C#JI};<(sPc_{W+%fqCUjgW`Ewp~ud4~$&1Fte!@esWnwT2 zq+k%Qz%IqeHkubnJ*=c0yqm6}JwBZlV`0Khw6sW*J^2sgV*lTpt!kId7@1mG8|9o_ zqE|hA0I`qr7zSMW$Ho4ZhhpiH zj-laPyU1c7KC(HeWnx6ZNCjz2kEvGen2A$u6qC!86Jbw;ak0PYp;#X&QP>lHTP`hUFU}qk(1ek>f$rVzQ_PHBE%=w=_;Pg*=TgwiN}P zz<4g!S107h#eQ(nY;&HA^^wz=J;{!X{eTCyKGH9TerNV1HZJzv9@zRSHaaf$T^@?{k>Qy=xno@HZ+IxyhspZn{{N{< zyZx=z_1CUEb@7|^$4dOR?SWS|Pr{RH5^{OphaQ+eM6bP-N8CfWtL`bo<14XQ=eF?7 z7)U-WRNbR~Kk1ytcpoNdv+pT=sE=#TZD*#ScPU+cX8Q;kgjMT?p?1J+aq#M!N%zfN zZyI?ndn_1&0)c>g(=zis7EMDbI~*CsDivUKC19qgR%#+lb4-LNS38|TBL;#kv|2s% z{K1EbGG|m}C6iQwkj7W@6-W-Ig@YQ4WQFcgsg1&MNJ-ZFVhReET3SKq(+XJ+XDq%K ztBd&_tirw0uvHvkMmc4vNF~FByxmNWo9^CBHcvuVM9#;I{*Xm(d`ZH%0yuL{W>XQFEt^IwMTVO}v|2`iO+_x0ALO%Ur<+vi zblxP+Zvq){9IX_Ix5I3TP&ufJ$Qlu@Af0?t77i*X0&4=DZsvw6rDj1Qk+w_UZp;tuCho*kWA_uvee5T0FOtJHr z$UOpKbFa*DfrnG%B4|ru)NUc$!)UpKvmxj{$ z?Qep@sMgNrEX*cbWko2NglN)5IV9rZw7Ec)jHSxA}}EWnW+jmY2X-8%m;^qc-bTn5CJqbN@mS?*ojrLY2>Jam&}mSleISZft#NtGfA@o6?0pz3_b-U)T`V z|9HK;_6KW))z7VFSAKUTwfxED1bFyS{<-Vky>*aEbaUUwo$0=hi_rO(X?NYbJ7M6X z)O{a!rd}BAOc?kmb>GMJsuu>^69zs?-S=@5>xIGAgn^Gz_kCR2dSS3R?E~*u!TUb$ zcfByUFk#@M)O{aU!Cn|_Oc?mM3EuZ{y*vLx^R9c>Ck%X)y6@vA*b9TT2?HOc?)$hj z_QGIw!oWwV`#$cMy)amrFz`|8zK^SDFASC^41C-~@B6p{oxd8r>)s{*41C<}dSGzR zT@waAE~57r%-~M{418Rcdg+6gP8j%jxBClb@Dl$FeB48O>4O(f82EU%`wM0;^3TA> zeX*B57)}`Yc(?ltW-##2z{f4Lmp*VN41B!X{RK1V`)9Dw%cr~U=}j2;c(;qa{<`a) zu73s#y$!hQ9(%&T$GhEMFoTYN1`9o&z3ZO#gn^IIwZC8ntqB7kJ-gWRvPt|udujJY zo9|zL(z5+1Z{`0jRR5}*PM$YD{Ji(s%ZpuR%(+z$ops*;&v1VKHi&ZOLv@X>@qMWK zYYt0q$XodE)%OztH#F1YuHiu(z%%azBf$<#26cjnWN9PUX{4Bx)TpvHYGj2X*#&04 zD3Te*bD=hLZvR9!sD%PFALK`c&S=yFxlqd-!!koHf>eZB9kCI|tYP3eU z!8hXv8qevYAV@#alq%Ly5`s;Y(3*Tmci15t^AP_G+zGl(cP}WOesbGHMxVL7*k$k> z89mcOAEZrs@hxJnTRXlIg6n&~4P?~E)H;??KQjFyFz7D_tS+6j3;M zW{AS5l_1NJ`~j0qn#jP>K_m+k&2!Q)EX6pQH$#0#I_MJ76iT04M7chT2-SWu7-~Q@ zt5-j;=x*y^R94_JVQIQ0C?cySbHeDzVUJj>)KtTonrkOi4CDfAjoKhtikhi8Im9Um zxE>Iw&DcC2gEPr$bC|~SvTPkOok1s;pt4RV8`FZE zVH8V>3d{r!JS7)%?f5{`Cn6fTBBDMt`$HDd;F&AknSTE_yombPbdE*Tk4WF}B8prs zq6@Y+MIvX${|iM~-bxKJ1+h|hS^+6=P)~~q1kz!?*gqm6q>B#ioWLu#eePLQZp#jc zfW-59`{1C{PA7`pW}uWGcCq9^O^KR$m25*up_vSJJK64l?a7K7Di@eavD3Bf7Ke&- zpi(F5{RHKJut_)+I~w8M7PaV1)xyH#J@#sUF`L7>2yCit0Z zkmyvMkbW?da5Y#W22rln$Aq92N*G*!P^cY6nPDiC2H|I1is#8WkO2t{%E2sHEm)~g z&M{kUxd)R)HCkzwj>=4(D#WQut60RDnixR)jU%n9@QreF(9+UmH;hryBU%a%TAg4y zX7!VKRL2V*jT7CbyQ`L)PhNOMNPS$JK2#wMpJ|*3e|s7y`nYDBY%IJxHGdlj=`|6= z1G8glp4U$XL1FjAZ>EsioFYI2(rnPOlb7;lGt4v(NUNPMg}XTgOEnL)ObkSjL=VoN zKwPd@@vJcl^bKU#XshM{o|1}H3(GOr$s zwCclNBa-iz=?qg%50pTNOLT)CcmHnFT_L^cy+vhR;5ss}7=4{2-gbTEPP=NM}GC6;$f?Td2t(sbm_qHW-Dj|I?BW_w$?3nOPSh`NT;&l zLAp$pnfYKqYlCsx-lBWnHivzf>z^1OyH6|I&BTg z{cwb_k%M8qTZzGuXt)?lhjKxJl-dn8t2kNB3NmGn`+v9T)BXRGmOi<3iM{yty>IR1 zcYk?zd*}PN-@CmDEdPIV^QH^^jn8a6ZT%%{pIh5s9RQpEo0hu(e#8CT{j8JXrKM#x z%q%T!Z7#2`zi4yEPwc;$_eb-Mop+Yali%Gj+|S+E9d6>9Ll~TXf&@D7aUc>1L(pL` zcsi{+;95Avb$j1OK*4iz-F8`SzQ_IC{6ZteRB zCU{P+TQ1AB7r37rclh9%JH>T#-$y*fb8_7TEI;*s{i!)c6Yv}+Hkr3 z^G)N=XM1M&+>^A+@b(87uKV#CIy2|je7@^0)4wO&&wuc)S@%y;F5jaE_^$c!F`Dwd zl77&h&vVUX_;;^%KmT{lbH6_0B{zApW}+l?kjua&wD*{eA)?lX=!W9am$_i{kSwuDNgdZ%%|vb z`yxI5e5VhF3AY!ok#ryaQB#UzNuL{nb1dn9cHlDnv%pylp;K{^?|$w{++}!;r2Fun znlijTu=jj7ow1~U;ZrW(|8e4(@BRtl^1VjVeRx|<`CcXIbNh8H>Hn2;dHy%nGtXz7 z;4aT=B;AKk)|BTpl0KK|SknLCt1i?3@>I`EZ#s#&Os|o2AHG{trc+6u%W*8}zyBnc zN*KI#Ob?4;j?(aVJp|KwQ$eLHK+dFSvsw{nUX~)^g?A*L&U;4vK z@3?f=C3ZWt{q(JGZ2k7uo44#OYU!hwo_+COFMj^w+b_QKV&UR5!AZgI?fu-|i}&(- z-?RJAtADrl)HQ1L6RU69Olro}nLVd=3cCHB+u!LG`h@K#c!hq)_IG%NK7RZ0UZIcM zew(m6D1R-Gtr#tIa+pqEpedYEmy+VI*`v)h`eyne# z;#{f7q)WVMr{&yf0`K2`zgOt{w%_NKz&+ddOreIvj0Q@c8}<6xX1lGGP7%2Hm5X2T zO5g+AAMgtO==Mjwa(&tM%e+GG-oD!_^v>-&y+ZHWzRN502eyA;3Qcv>BnOTy;})LJ z=E&J_T2DPE!Sn(P%Stx3;XW0se1?hVpIvqK3gSKx8M zQT5Y98^=8YV=<4wXw)O{jypU8Z@+!g?p`-#XPKU)CP9u!HchrpYpRVO-}rH_&>!3Q zF|W`c-S|p!&pLtde;-hQ=L=*zcX zK84Pmg!i`hyh3-kcfCS)ws*X8eZ}@Gyh49?`-i8{$+F}@mLVV7{g7Aa2X{Z{75ahQ z>AW&s7d;4p`*$yofw^4YzdM~-=a>4v-ODrU+*04W``&4@=FY5NzW8OY&@Wy5l2_;# zFMiQ0^sg@d)f7s$ic*u;wLv+XYiHFGd1|uQd&Axvyh49+?pXg?@AWn^WlA1@2ET{j^u;o0s0~75Y<4Kjjtr zrlmJ+14-Upe$&#$Pw(BgtL-4$%bTCM@WzeWI=Z&9@>%eQ`Wd3`|Lf(T|7ZS9Kk>4_ z+AHK0nurc~c+ltJ1SD3lUXmRDZi^%~0=~{jj)*#VBkIs>wk43|o0ci}G0J*!6F5QR@a%>zto2t^R zHpq^mPKKq2S+-?MaJD9qwZ0r=LODWn=;*i(cw7fnKxML|<%_Wz4`&TINV|0n>B_h+ zP&V2Sihu(ylvSeD$xvFmT+Tv8vMflPJjMzZz`|%vp~^#{7{g8ZxRxpv(Sf1JF_cQw zYM8CF$y`1k#xR9Wgn1z#593`;YcA(*z%C;e)MZqP&9j1CY7|D5M6H_Bkzu|RozZ1@ zie$~0-RF!5lyVYKCs9$UB%eWo?L$HxL3FZ!M9gE8K>28MIeX)E89iE1m&NIU+7N^^ zJV0_*Sub-+qd+1vx-811sbe^m%POUegQ=)U<_`%;?TQL_h{pw`V;A~VK297LL@>(e z8?eh73+gf)ayUKJ%r=a)JTfX-yy?igJ*&&Inu4Yq0}?8twZ<^21vMd`?=q&D4@l7v z$V&_7iw-OcIPWx*l{36iAmvioDnbIA#b=GO z7@IGZ#7L-xz!iv*N5{kC5IgE~Ifrqghn&Sp;RKyZ%8soA!A#w7T?S^+1qo(68%P^* zL^ivKKq;y?D|QY5E1^+YcrU|rB< z$s4fC7B$KeLQE+JQuN3evQ~>_$wEHE@iV$C)~C%*T0agJnko{If>6ABtXW!tO%gSc z5Zgqt*&KpdGat%>8I!v4x-7e>8Ix?GTu^B9m6DSS7)81)fFwAxMp=xrhJ^|nmeEGJ zkSIp;kzuTqDCg-Gm7sE=6i8GbttKhBqL84=$Q!WB7Byoggpf)iy^PsJ)Cz_$2Jn=y znzKfkRH|5!W37)$RvpIz!`xwvuT>d7RxYH6eYAnn<#LzKAEz=v{U>g~E?bx`)4OsY z8cMVlkmFTUl9r6Ty>Ev=qp)a9{{{pBzK zZZxp#qX5`lv8x{I>d<}4E%Lk`#~6m38E>rAF)HPFyHGYsx*0_~LtR(UHWvw}R59fA z55m2?*g1;hadSfXJYfNt0-4TGc&Ct&iDA}On?!(+;}rp^u<&4%RKwL`Ka$P{^MQk? zQ^Dd5CoeFN6${l6>_99ADg#rC!|fh~&lGeZ|W;&S9y5HAPln_hv-%BAa$ zcty*+_?BrSEzwlLA5r(Ov)Vl_?t#BuZ!`@a1{FSlkX_HRx-1&UqEQ!J@>iEF_QkH~ zX68i494CBEk(?-k2Z)cKmqhDwQF>k0FaO?A6;XCuHwM+Me!c59=bC^9Zj6wJQQbr$ zfiokJN-?u;4QpAAjV8iLrKL23W=>%f87$imNnF?LCAuk$${kqAb4MX5Nk**d(0w)^ zH3oxnNk{9WOh_8>oIo`pM}smDkDCeg$ci`GTqvJO_Utw!QKngec?wTQ>5rb{%X9dT&9-qJY}4LBN^%;Z51vPQfvq@e<>;1W^lLR>s&R?Z(-)PV}2 zNHB`%dAglRG)N`hCNWv-YSu7SMaUrAr6D5NtRO}pDA7%)q7Z#T6WQ7z*%}a5HXehH zQVbI1yh6*Hxtf6u9XkAZ&r#bRc;@SULI1 z50}rM{5E8w$7g={LD#4Gh+I+dWJIGl}{Ne=5uLLn;{O(M)f z-9}F2m^dOe!bm(T+M|PFp=+0_P+0Of&A3gU`u~6T62ElmO_wfR{7)BOu=nY``tBEY zJ0J?ceLJD;U)a8V>yNgyt<}x`=I({JT|hQIw}G$!*Y)=L@>+N8ajP$1ee%k$twfjq zXgRg?SD=^&e(v77_h}m{^UN15?StUm;<1#FhKURg6lJJ=+|gtyG(>x%Kw6yD8o}+D zVjssfC^#Hxp{a063#LR8FC_BNu@&HIe4qiMj0bJcTqa@%8vOrE8w@480~k|N>1eza zzypymj%8{o6{eceG^O$kV^Ya{AlU3zWzUs7XoG{NU3;{9p8~i9L4zG|JH>?xftZp? z9XpyB$fVS4B9~59f<~XE`)xfewv7r>%V{^;DXYiuxnZYQE44Vn&hXcCJ_JoXQ+lmi zT~EYY$6{#S`|m+bw}YA}I%kE8ZQ{7h9LlyLRhzYDkL!Y1ur)iH5Xu#?AFI{nKHs+I z*ED_2*DsCLd!G#II|B8MLdOk$&`4)z9PQb+-zge+vLV-XxR;WihB9ltTg2_lim%-V=VTeLHI@S=fik(Do+B26~ zlE*OoXZ6?yPt?K87AL{ z^i;lGAtLn*QCEt+N(iZoaf)RfK3+P6(h;^Td*(73zvjJ{KppOosm6N!lF0|QF#|ZeeRGM zf@`&bY_N7U$vTBYh8OKvtt=C0ur)G^hXo;w(t~6zK-f{w^<6z=_CP)EkZDDu?Vj02 z(0n6jp=wx^!p&aGRIK($-5J4%HI6s>ZxcG5GK%R^=x)Zq>p5Z$eZ2&0lL=b^(yaM11zXh`Xh zOum%12q}6zWVx!w#0zBeW`$^H)7gZQY~WGH8j1rSLuL!q=MI?;naxCLtRTpZR7@%d zYy)F@z3@VN?~Bq3s68 zYKIXro?tON>ba^1A2RTDLuSL}0-B8oVz8*uY*o(5`5KqEC@czR@;V#za?%R^=z)Zq>p9|@J`hp%`LEwcvdbBBzN^vRyywrV7V95Bm=yPE9>?nGBijm1iz7OJ7{t`q`})Zxy!=z)t^9Hs85<&!)Wjyv-+E`0EQFzwpKj z_J#C?@80;Ajn8lV;>OE2zIP+Iv9tacmwxrqA76U)rSHEKx%9Y;U%L2_i*LBtx=3BT zY40DG|37dZ@GHx&T0UM5FJD^v;`;sTKen!~GyTKn0(&+WZ^?+5m{J!o%p z_fL1^x^@b^8n3@7n&6ZEgFG?I&;j-PWf-G=P6t z`|R4!uidp)Svy+0u=+n&-?RFfRc-b5)hDg|ua!?caP%vKGN}|;-mh$m| zkSWXgxN=IZ@4nw7?tLC{@AZg#k4N0QJ>uTw5%(J&aqnDOT6xKgZhh>l$9mvl?}5n} zjmge1-{jJo1)N3LMz(NDb}zowBkpHB;@;vB_tPG6Z}y1$DUY}}dBnYOjO(<@e1=Vt zaw4mcqr`|i9YbMT_lRqF#MM3GY94Xg1SjH^a)klM#*(S4gBG4WH4ZjD>=E}NkGKzd z#C^ac?tYKB$y_uy^3%p-E}D;<%tiBYleuU76AmF7Hz@CJLnH`w#M!EW;g!@R*F-e6&Gu#h(x zI)15h?^`y$<`MTd9&umwi2G}gxUYD`eR+bT3RIID@!%@8XjEc)^SaPmf9VnT7annc z?h*HA9&vx_5%)hm;{Jz6+<*6o`+`T@pNw%Gr_?LQI@K;o5wV;R7tB*i>H_W&7xRdV zdc@t~5qG;s+zUP8Uf>b;{0VOEqN?Q)*Yt?9JmL(GICFb_xI`~~a(Ve1%O6{Q?eh1n zaH}`1d~M|etMA?;Hdiiu?!ucd7#D84u(@{UW_9DgTzb>S#f`UZboRbu_fI#$et+-n zAYZ^sR|{)@zMI~C^3E5R{(0wJo5<>yc3uwl@HbujgG-H#J1)L(>64ct+h4o5yZVZ? zf4}{K?H^wG;cb5Vnd|Sk^qIZ>)~)MbSj%kw&N{jL+^xUg{MPz+ZhidH#@erLy>8_h zd;e|g`!Bu~5cKd9SXEc{Z7kZb;?WS46BPw#iImf2bq;GM3%PPKiaJ5Mldg;9O5Aof z|6%+@r^AYsZm`*Hg|dU=@S#%(qJpUKNeJmwFf6{QF5Af>yDj_HjL< z=H&`s1h=!`s<~TIEVX72tYomr4HX8?U@>I#%i|~7eKHu4Dw$!c-Qx%*okbfUjGU%3 zSw}ylStD!bg)naYJaQJkjRNavT}a z$4G^5Cn^q1GzLY&820;ZuA0DNEG(vE6--gu#-EO#u$w%b!%^KN&yK3ARCW zfgDOeP#O(p6sX+k!V29Pa)tIGrHL4}WQ6V8rXoSH9stOH;DLS2jmR>)3Vt`fJ@lmys z(BpFYxE0ih<5Esf!g-U@y4hYe(_)BNzbbXK)t{TNqCv!oAadLbh-niTL}mYf_P#vq zajI;8-Yw_dUls*q5e8pn8Ox+?+NQ{sw&|MgY1)RRr0KpSZPOhXKoMb@;vl#%AS$x& ziy$BfC?IZ#2(tL8h$wvG1|sq)%I_rYunfaT3!^{3>$3debG^>F&vSClbLX6sXK_XY zrhtX8F?KIku!qx_hUpj!Ceq-Y_6=IZH2IRIo-eR)@0cj%^=4ihj=TIt3)X6AP(o;@ z9C)r6?Q-bc*9Ka7bZoLl7?Tc{Y8;)rZqNt(+*ixT;&7qoJk_?p({`a=1Ez0xGlV%~ z@$p4}vP869);56*_^QVE!WOY&mueB4Jz|YDgYiZ>X3XMFs}QpW0wJf#sW&>@^@eNm zV*{h_}ty;p|Eynd-cPAC2?Rvdvw0aWVtT?^bpoPn84JW#KjXuB$Oq_1SiLg0ksC&y{ zzRVT~6Uo+$c(N4_7N?FMwjg7M1V($ZR;S%!YMVUuYCB-cM$>V9GV2bfI~+rpq>R%S zoH=gL!dc_7y4lnAkWE)OZ*w?1)lw~E%7xN)Iui8PZK0f_Yb6`W?8Ja~$M}Ma%nkY= z^lZjtvxG3ZBVrl7phq#*BRPznPQ;ltVOFt{a9eRhw<&>-4_cV3x^j)ywk18)U?~gN zHA#`Mo906V8T5Fa(6Hj}x{emeB7zQD)bVaL6y^9vIA$cfyijIoXU%JIYip=APcTkj zDqnXH9Zd_LI(g87Qgo{0&XCTgjYC`Q7Rrv8s}+h@OI|bPYr%7JNotk_BQ|@?pats* z*4&&cnPQyLDy|FD`c#1hUr3~r8jB!y>h3A}{MH@{~UR~OpU`qJPpbH_IwF;co4{beP zY(*;$yy2p3qB)#{t{KT9RneLx8)IOru8F{)MNx2tQn6GZYl@ZQn8Vogl~T!^FM>8J z0@@UODU2z#VgX5%rVbgjNON4y>ByKmiKI8~%0nkptwt<*LulHGHH{R?;AIP$!#Xx` z@`^!=U|C~>F=?4_GMI-O7)`~)v^7jIgGRj;Q?}zU=UZ*35eBDWYaSd7+}~+d^1Q*_ zGG`K{Xd=U_jY0`#ZiU_AVelIlKHdksyhQsi?%b^@fM9f7!2|Ymwwvr_bL>i5Y z6@QWO5c#s%9q?KiZDi^r1Eu^9u3XJy0lwa}O8N|z)HIU?A51W03^0rlPZJf+QBI}X z5;;W;T98JsRE(CWD$jLt$u4Jup?S_=z-tOdGL(bYx_x-XQupO*=oC5_hTaw|YB^0R zZ)Rd8zt>%H#&8Y8!kt#D$lJ{ZvfXr4U3EyuI}Wx?Qg}nbTMFp_X7IS+^%@iqX1wEa>zYY-KmA_cV$f zv&GGblZinK$%%zbo~*r_2_ju&16y(lepgEvPwQCqf6wlB8N|2HfT|Gn!4#S z%@ekyqk(78oRhD2^D*A9eY-rGFLGP<98Z#ku12Z zu{v6ESOQtX6KbS%Ws{T-`C}oAn9!JOamq_K1S(PXP7L#Q+6-SN&2-^|q#mvqD9m z8kE#ED}{91mPKwDXyu^w8WW@*qGhQO(XgLxW=wexX3a^ihLt5dXoAbxX*TJIAXg7s z7=^lpMY|pe#aotwL#h|70Y^Knq5YkBIY+>6_635-3s#*tbM$Z=B@EN#B)>h9aYzY9 zCuIU`ydG;bsD_4y+b(lF;%-L?Z8$Px8}w0+RV_()CL&FCB5W&k1(-Y`rK@|3LM`ey znab^KCJ>8fZ6%6RGEMB2}Sd^^&7(*Aa2r$!TEDd%eXF zpFeES2OW<&D2^z&ivnuR_?lU>sh(=1gw-d7;2A0Fa`5$Bv{vW5^Y2&U|21oFT(ff7 zN@)3S%NH(tmtI`@!jg0Gxy8+k)`j0Le0t%q`KRYM&70?*oa@bDvp=0Zb#^^?6r2nW zg#G}>BOjc(f2K9F56sG6o8DvU?y2(BE|Ygm<|cQXxOIY`ScK5q|3}N2>B(Rk$*e8Y zB`d`St>uiZ5tQ8RH+jgd^3EtM03L?k%3>rH3E&uEqiVXOJ?pI2bk!&ov1d?LYwfUp zFD<84*z^dn4hg!Lk$F*TgxjVYi?A!*0SDaP39dM9of>lN==)Qj%EmL%^Wvd(Rz-g<)jLm z909fzmhy>|nR5B5TCVB{v?NDDYfEJeH0rQmjOe4&OttP6>{^VLITe;00TvpNMS$fn zy@fCp@*3O87F%Q@xqKlNZ`m~tNmtZZl7=GD42raTtO|SV2(W~wrB5e>aIut5B+F^D zuj7O^Ay^Hje1mkhsxgC3&qQ@L!HUvyLWNDN4UoK_WXyLo*^?* z64mE}1#=}3_7+)+M!ANGZR^4rH;wW^qXzdxa~;2nmXA?kj~M|rSrNSv)SK}cyESv& zC$KqZ6NOobyG*QT3Iy^dTged+S|}Tbut%$~M~?t&a3{GW+`?d3xaz`SKta7NY1;%} zBlMUfS+PW@fIX;9dXgA?wnwS3M~wg*jkc;8HbW#l>87@r>I4{fkgeMjzC7;IU}dcq zM?3y-T2paB*dtZgBS(OBg{m>i8i>WZw8onf>S5Yh(n+|-QKb?zg$r~fg*mZGm-NMH znN?xg5n$t9OPK7oL^2d6+wj6E-a%~%JW;Ug^bXz-w#D_9xHo0X`f;9?<0@=?1Xw() zG5cf9HWs5C1qwBg&a$ymuH=p0E@bMaq1)1w%k?;8ZMJARrozTXfTh?7RcmQ-u5`m+ zvC)mL7Vdl8*1Cr;Cab)V5e)Xa&zXs2DM&R@6*l_zVMVK?KanrF!*siu@p-x}o!wT6 z=*y{O+#Dm|epv7FRp8?Z7&~QXyo?IVynUd@IPpfb98qB-BfyRkTtv&X3QLawJ4Or< zEr(Uu@CdMDT;|boNQDiJ06Ru_5G@B)*x(4TV?_GUazKR*ynV>T>KL&-wCq=5{UgAR z5vD`SJ{8tC0_+&kIJE3lVMho98YlRMmMImM8bRAJ0&Qs7qr!SdfE^=dhL+tbto!XF zA;t-lp=Fl}J3@fPIMFb)>{MZ$BWOED@Cz+FR9MFduww+e(6U{HwT}QhM$8H=lPWCv z_K^tVgsIT7O@+0M06WHACM{c4SnCL|V+5bjvPFfpi~u`Epb0G#D(namC*#D7(DD%~ z>=ADtl`u}22rVD3!X7>X>=@A?w0xKfd)NrDV+4QD@}Vm1p(DVK5$Hk7hp4cJi~u`E z%myugM1}px+k4`V6Q)7S2dl6Lj{rMHG=`f0Z}RRni> zWH7}-BW?9K%W*EA&X8m!xV3+%(%|j06>c5frnGu}w!%T@?<`f4Rss8GrApeC;_onf zj26@Bl>9U~PRgYR3FV|5r!yFpY&TKXUdgxf@pUz8gQtVeXfe>>>z-tZ%G*LIr?yt7 zcz3EEluXh)PEuw}uaD@H8P=T+k`6N43DB9e=vvPjoX%jRiebra*_`vCseq9|;pb&` zeOWA3ayH3Vj+)IiF%Zg-#utq}3a=Z=Dy6)8nZalOZM2YI6lj8MK+w2DKDSJzq9g zYFUab8V&2s*@UO1d@Ap@`R&|-l1#mh~vrJ2w3-Wtn= za*`Njy@Ef$IC-NF>-xK9q1CWuU71EsOtq`}JXZ^)b)_`uZdM_ z+)Tr2HbTr7p#-6m#v7!V4tXUnj#|1RjH;K4SV1f~9UAZ3@7Lb=n!(;O@hy66@HF$Hn%Xj+X(3-Kgw53kk zSI$tKFcZp!D7%(eUy%Zs8RY^*EK7AGV#Lj^lUnYU5N*BV7e_eGmTZ+|+MYB;I!S?V zr7eE=%jb?o+rc7q;PP|+q*TEKno4!;q_rDmf@Uff zaZt9djR=nP;!uVTZRry;eVbxS@~#u(9D=r=E$RPH+R_*YyKULho3j!9|FNunx1k8! zGr`>xOg;OCtk#BY+THfb|EEQ9-CG)Bt>Eo&Fc+7^<+Yg4Px>oKvek{ZvN^07xA=o} zJ6kuxId^hgmkp1r(an z@_EbFrQa-_xr8tNeDTyp&B8+q$1UtT|Lyt8{H}9fpG(gz!rTDt>;$+J_>mWp&m*>( zXJ5HiOj?j1pX)dn0{qC{XS}(Tz-R%dF#S& z-}?a{q^i5Bmppa;w?9^6cUvV7_doq`JN9G4IiEO=>5D4x82S4Kzz_7QKY8Tz{U;81 zNOSye*LzO+>`m`KX@0L??_lBXJN)QpZvP9@7gXRelJO0I?_8D>7dB=dbiFdW`$N|s zgM~M)E&u9v{rby&k6#|S`IKjvepv+`XR0*~>5YykX&YktY^aSwN%?F%3M_4=zV7yRzUkAL}=6OQ*} zCw~9SD;~MyTBcuAfyeknHUPeM^S8pEx83l&ORu-=anIuypYzkd9(n&&dp(zSuifMH z-Ok$er%b<~0*~>LYyfh0wAcH0{lQ&RGbdbo7~X#7FzLVk zo9X9O;4wav4S+?f_}e??&Zc*|DYf6jb@y=}fB4`Xzczi&Pe1#&#lP+sx#}jSpHqRy z_)sT5B*~AUmdVXUJ-W}D5jrPfyek%HUKtU zdhw5rF3;?C+f^6N?Z4N3e~j<>aB0Ic8`hom=wpVT%s%k}rk_!P$M{$_0RGF$-~W7j zF8<~3@8r1K^;O|w|Gtm*{*D>H_}GIp{N`r^>zIC81s)?;-2nK}!u{9lZr-DRxjg^g zb8md{zc1T$`hf@KOP{~&mOmUf_5AOC!t_%r@E9M=2Ec2t`RxO{7%%zV4fw9b-z5@P z%X>bMWOtrB`RNay`}t2#e&jT!&#S;=d@>sV-NJE@Apf71&odc3gZptF=^1zEU8;=DanG^91=Kmg{JP?tDbk5lS_> zV~_^G#-Clk_S;{4X!nbZ;a4&{ulvOXn`*wb-=*hH_g{ZL`=jrk%k()FIBdjxFs?xm zrL3*1?>bOCrU}%Xv0S*qd!sSdB1H=&lZ$X*@d2>s>ZJ?6w%-N!P5t2bdw+D#KMuR@ zk&hpEls5esa?h7Pd;OX{PG$PXs=#Ap!y5n}zsEJNyz-t$L*`3QzUGl#`g4x%57V2z zpY`?UPUfT5@}o>Yp#qPQ_HF?DoxOf_&$q+ZJ-_o^dvD^dnD}^f>gld@&iXU6U#IRq zt+9M3(?3Q99^+Hr0QkZ+2i{t^`Kg!g|H5a#mjC)Qx1RI)^RIlI+hPA>A3XNa{oH3> z$n=jMJc`k?+6^DYDU-LHfPSvN;{zvNu>9#qwP)5Yz4P+#FP#6){rC2~^5gyf!1Rw& zfyemVHvoQ~xVm-H?Ye)Q_=1MG;E2;+IPEcW?(93hbJq9He{RojeHOWZ=^v>Ad(6H> zz!(ZdYFW$~WdhJ2yq0Sc5lQTJbZ(n5CnoA)jm>U=Nl?{Wu5~{;@$d`%p_}o&(*DyP zD4zPq_gpVO|LCT_p8Cn(d7ilE=>pSdRbV6C@RloVdOGiI<`ERKoK7Q{Jf1W?jc;_ptk8GHpwn2?!D)1N|DF?t; z@}4z62&V7*_RNiq>l1-nfB$@I=8*56w5jo#YrlLC{k6xLepCe><1^&|_^G3gabCS= z{66F(SGT`>Lhkb0_c^!Jsm1pH#8uB;^4yDOWtl#s0*~>basWIRJNe>g51c`-b0izw z+)sZpkv`#(O}hIY@vYpp8xekCAEqBsfyYR1Hvs;{XTIv5+WCZUo_E-Le{kN3M?8AN zU#|PgMbkBK%P${(FaPuSMNFSofm_~OgQK+21uv*Epq7XsZmb(DoeWAEEZG3l(i=nR zq{Wjc2-yMfo%ad3+iQE<4tx3t`d4RM_0*nUc)&-T^ykXYFFdBS(;a8DnSNLWCZw*! zW67B0aT$n+!KN$NTN+&Qd%Fp&sDUMPGoh|Uj5i~McK|&3z28cwK632^<_Vwmac$*q z-(A)p{?b`b{lNQzgq*rQFvavkDll{p=IjMD%{L=-$feEc+Ub%ZAcVVCsp8gE^5$|R z&sK{R2ctt(1G)aF&AS}>{@R~+w)MH6?0V5t|MTGY)*Y~4A^Bo@-^^tTg$mOTs=&B0 z5R`0HDbQg;@qjG`UD*?!vK?<%tI;xHDcVvcA4i50lmi<8|LMyQTzl}9hkr15>>nSw ze)Yryx#RD>{4<|hlUjPro3q{VVS(ufRA606!Xv>#*XeH818A_*(OJ_?n2b4J=DT^D zJM1Tt*#Jcbq}=dPJbvUM!P!6Ud0zQ&o9?^Eo_~k4v@xqcO&7wECl0x8<@%kOzF!57 zMaj0y8Oqy*1a0#=GyYP*U^3)uEYmGDNGTFbqOM{%K*B^61C5X7wENVPyPWu=#k+of zaQJ`z@VP6}NB!z%&wGg5XV3h=-Pg64zE1_FQbN*O%5|+&ixugNi>XrDMlEKFVEQgc z8AX&c)QdzTiB^X1Jk4W2(Z!_M+urvj(|ybZf4=p`zlHmM%6yIw~e*M{Nm_DTf^E~5n<>^+_Njq^)!w0xXnxogfl;{Fq^x>Nt~r|#Xu;rPif&q?*sJ)dU! z9u=64z?{4cl@LtHMlEBdB8fsc-tr2Dc3mC6c_*>bN_wEp*I%4bNBrpJpPB*9B}j1 zJJdfp_mfUy!=^KLT=`=7ET-=qXzb1~^+p{l)WU?VXl%nExq^oG;|VMnmTF|E6ll~f zSVnM{T?62kKl|`a&$iF_%1O(<>yCJq|DSK(_R1+cU4Pw{kpA6q}`z1#` zym0>U=Rg08{X085e^qseJ$6dkwc{<;3NGET6FS^3rjOe_iY> zytL4oe{sGs_rjbs`}}Mb{1J%A?~(G%Z)b|rznRWYJu{V^{Pkpd;;9LK&6BYCTmLp} zhDYCYaGY${<2cQ|y;IwU&G3i}6&T}au{y?SVH5@6QF=@T#yC}sq5wQ{Lj}ed)%MMv zy^FlYPTxcm)C# z7~|-;eY1^k*bJ{q##CU8GuYevv~Ji8uT;iVV2oqyC{ z6u_&TF%{U(Qvk1gpaNrnZtp38S3_eeu$`v>ULipR#<({b$7_2OD}Yx`P=PTHW7{`} z{f5o(N@`36#yHiyy@%I^&G71KOa;d986QOfc*O-37~|Nwy{7d^)jc1aj$!^;*aEy1~ww#%^ zBUPwVLY9cqmp1#iJzGVym>)TQ!?3As=rbMsu=NhCXl%bZ>DC9AklOH(P>5QMVDx z@o#TSV{Br!WlP)e$J@%~_FrRJ|2B?+Ce*BZD?{vbeM~+F>oXFz^=&Cs;L_`Ysr6-T zH;~LkY$z=B8mP6hHr^I`7mk6fj&;HeP^MC}vYrlct)wj)A&q%b;7Zm;D_v>oQZO5$ zBaRz0zCf~`3JSJtM_aUXtesj-Yt&gddyC72U|Ks}+fGLvby`27Au2;xyxr@4m-$RF!TYKjIiwy8p1(YBrn8~k{AB1eA(v^A+{tbgV zzu6IfyE)_<^>1x=l%Z-jw{VYG_tLI*nTW3o#Jos%;agbcSn`>d@*?__tSMOOM-k zSRhZ}%4zt2s^@~?pWZNT6%Mw1%ldt=G3@ZQf5CHtDCBvrl;BEvPEr2#*n^Nv^^b~< z1AVqqazF&HDXz9pa>5pdSboR84Q_RycdQln<7qXcnb&os6zi5-mYj^SWWO#^PFwqtIZ?1`2a{h;TUw>lYWRnRWRvX0%AjXhbXN%@!(`tg~1>=QikU zFfK@4LxQ3ajHgh#=CbKTU71j=!Gxli4%I3{hjcSPZ(MWOnxC$j@K5YAdC^p0YS-zX zPhUJ;o)_nO%hkEi-1}yqSpMb0?z5N7{e4!LIBn^bxw~f#D|=4rr~bD1BzSrLQdl8y zBM44oQ{PySCx5nB0lOhTU-{P5>5HZ{KV8O=i;*&dEre(OwsOYIHgNBIK|eI_PX3)xcJiq#F~ zVxyF`H=~_EC~qy|ZmRA{pcm(#HAuQ* zx}sBGtA=%fqJ8LLJMqAv z+{C7vfe_OvXydwAL|?<>EUf+8)kg6_kDxVd*ldvb?z(z|Gt=1w;X=m~NX6p#@Tt!4IBcj(MtRcjD3x?xujHbV(Rx`Y;C4%^TXhOG!Jm)S(zUG1 z>L2zv=NLAOCo*}4GdfLCMz9n+PH(KpIoP;O@uIF4=@{mM>AJfKqbV{_brBghG!!V0 zJMU@7Xo1QYU<|3FTCvyAOgyNE8RjJ}WJ+lYR#Y?$pDMCt<>)b@LZ&NTQtLMA!mxUF zPUCA5POnh(ay67J2wXRuh!z{+;XE!~G3=2mF{xI;rEO<(oHfX46Ir9yX|fr7xRK23 zIKG$&=7d_-;~IM5E`NI1kggt|trkPW%xoIR0i568It$d(>;eAK8* z#B4g;myW>@Uw<@}GV9Zkq=s&_0xmq8kLU5W!91Lf2ufqIs1+1XAa!Qobaxs+*XsBtMB6*=*XwYSr#Ra>@ zYzT72AWW_9v>G&>MAc)_M9Z-pOlLiKHWQU$!&;UL;hKD+QY)CcaaT%$S%fuC%+zdW zt+}qom9`kMI;n|9x46CDIBY0#n3pcHo>GPNYD9Mq25u7ut-VCpD=|iI%o`(IolLjl zk)f(cVA!yjmQvxWp{vo7T;9heYi%o6$1FMA5{bLB-l(?G=*9%Cr7>J_Q(H)`7;Q9* z`m~kJlpJ+)qwQhRL0B28DDk$KS+p>G*_`I7Xqy^(HcnqQ?6KhWgtKnm%}QQBQD<%L zlG|Y{qSa{1U~QY}l*3W#Sj}xSF`UMk*wOkc@4@gR{-@odgl4Psb{bW~}p)#;%OMrNq$ z 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.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 xM`J`~$3LtQqkP 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 kQKcRk9{jmB0 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

oU6$Ht%jP}2HcwQig|u+27zCp+ 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

UJZADbcun6Xd_R?`5r>_ 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

  • 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%0DQQH22_4~LD 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%0D^YyFYQVotcyrt+N#NRvN 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

    AV;~H+iV~UK3l8Ma5$~76UF^- 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 n!7sPt?1Me;h+tH!ookLxjQKv; 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 A7!Ma^1>s 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

    >Ss`*f% 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 z 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

    @y4^$(8VAAu3pKln%R9x%fChn{&}4@Ox3kXzjA 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Ⓢ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 _vvekaC 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

    FR91EQEA3Qc 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

  • |{Ls8)xROJI{aUC%%&X!4I9ge$;!l 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 A66pg? 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�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