async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict: time_now = self._clock.time_msec() try: return await self.transport_layer.send_leave_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, 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() logger.debug("Couldn't send_leave with the v2 API, falling back to the v1 API") resp = await self.transport_layer.send_leave_v1( 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]
def check_event_content_hash(event: EventBase, hash_algorithm: Hasher = hashlib.sha256) -> bool: """Check whether the hash for this PDU matches the contents""" name, expected_hash = compute_content_hash(event.get_pdu_json(), hash_algorithm) logger.debug( "Verifying content hash on %s (expecting: %s)", event.event_id, encode_base64(expected_hash), ) # some malformed events lack a 'hashes'. Protect against it being missing # or a weird type by basically treating it the same as an unhashed event. hashes = event.get("hashes") # nb it might be a frozendict or a dict if not isinstance(hashes, collections.abc.Mapping): raise SynapseError(400, "Malformed 'hashes': %s" % (type(hashes), ), Codes.UNAUTHORIZED) if name not in hashes: raise SynapseError( 400, "Algorithm %s not in hashes %s" % (name, list(hashes)), Codes.UNAUTHORIZED, ) message_hash_base64 = hashes[name] try: message_hash_bytes = decode_base64(message_hash_base64) except Exception: raise SynapseError(400, "Invalid base64: %s" % (message_hash_base64, ), Codes.UNAUTHORIZED) return message_hash_bytes == expected_hash
async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict: time_now = self._clock.time_msec() try: return await self.transport_layer.send_leave_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) except HttpResponseException as e: # 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") resp = await self.transport_layer.send_leave_v1( 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 check_event_allowed(self, event: EventBase, context: EventContext) -> Union[bool, 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. Args: event: The event to be checked. context: The context of the event. Returns: The result from the ThirdPartyRules module, as above """ if self.third_party_rules is None: return True prev_state_ids = await context.get_prev_state_ids() # Retrieve the state events from the database. events = await self.store.get_events(prev_state_ids.values()) state_events = {(ev.type, ev.state_key): ev for ev in events.values()} # Ensure that the event is frozen, to make sure that the module is not tempted # to try to modify it. Any attempt to modify it at this point will invalidate # the hashes and signatures. event.freeze() return await self.third_party_rules.check_event_allowed( event, state_events)
def _should_count_as_unread(event: EventBase, context: EventContext) -> bool: # Exclude rejected and soft-failed events. if context.rejected or event.internal_metadata.is_soft_failed(): return False # Exclude notices. if (not event.is_state() and event.type == EventTypes.Message and event.content.get("msgtype") == "m.notice"): return False # Exclude edits. relates_to = event.content.get("m.relates_to", {}) if relates_to.get("rel_type") == RelationTypes.REPLACE: return False # Mark events that have a non-empty string body as unread. body = event.content.get("body") if isinstance(body, str) and body: return True # Mark some state events as unread. if event.is_state() and event.type in STATE_EVENT_TYPES_TO_MARK_UNREAD: return True # Mark encrypted events as unread. if not event.is_state() and event.type == EventTypes.Encrypted: return True return False
async def serialize(self, event: EventBase, store: "DataStore") -> JsonDict: """Converts self to a type that can be serialized as JSON, and then deserialized by `deserialize` Args: event: The event that this context relates to Returns: The serialized event. """ # We don't serialize the full state dicts, instead they get pulled out # of the DB on the other side. However, the other side can't figure out # the prev_state_ids, so if we're a state event we include the event # id that we replaced in the state. if event.is_state(): prev_state_ids = await self.get_prev_state_ids() prev_state_id = prev_state_ids.get((event.type, event.state_key)) else: prev_state_id = None return { "prev_state_id": prev_state_id, "event_type": event.type, "event_state_key": event.get_state_key(), "state_group": self._state_group, "state_group_before_event": self.state_group_before_event, "rejected": self.rejected, "prev_group": self.prev_group, "delta_ids": _encode_state_dict(self.delta_ids), "app_service_id": self.app_service.id if self.app_service else None, }
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, content=pdu.get_pdu_json(time_now), ) except HttpResponseException as e: # 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") 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), )
async def check_event_allowed( self, event: EventBase, context: EventContext ) -> 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. 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. """ # 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() # Retrieve the state events from the database. events = await self.store.get_events(prev_state_ids.values()) state_events = {(ev.type, ev.state_key): ev for ev in events.values()} # Ensure that the event is frozen, to make sure that the module is not tempted # to try to modify it. Any attempt to modify it at this point will invalidate # the hashes and signatures. event.freeze() for callback in self._check_event_allowed_callbacks: try: res, replacement_data = await callback(event, state_events) except SynapseError as e: # FIXME: Being able to throw SynapseErrors is relied upon by # some modules. PR #10386 accidentally broke this ability. # That said, we aren't keen on exposing this implementation detail # to modules and we should one day have a proper way to do what # is wanted. # This module callback needs a rework so that hacks such as # this one are not necessary. raise e except Exception: raise ModuleFailedException( "Failed to run `check_event_allowed` module API callback" ) # 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 _do_send_invite(self, destination: str, pdu: EventBase, room_version: RoomVersion) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. Returns: The event as a dict as returned by the remote server """ time_now = self._clock.time_msec() try: content = await self.transport_layer.send_invite_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content={ "event": pdu.get_pdu_json(time_now), "room_version": room_version.identifier, "invite_room_state": pdu.unsigned.get("invite_room_state", []), }, ) return content 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 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 == 403: raise e.to_synapse_error() else: raise # Didn't work, try v1 API. # Note the v1 API returns a tuple of `(200, content)` _, content = await self.transport_layer.send_invite_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) return content
async def _check_sigs_and_hash(self, room_version: RoomVersion, pdu: EventBase) -> EventBase: """Checks that event is correctly signed by the sending server. Args: room_version: The room version of the PDU pdu: the event to be checked Returns: * 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.""" 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, ) else: logger.warning( "Event %s content has been tampered, redacting", pdu.event_id, ) return redacted_event 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 pdu
async def _do_send_invite(self, destination: str, pdu: EventBase, room_version: RoomVersion) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. 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() try: return await self.transport_layer.send_invite_v2( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content={ "event": pdu.get_pdu_json(time_now), "room_version": room_version.identifier, "invite_room_state": pdu.unsigned.get("invite_room_state", []), }, ) except HttpResponseException as e: # 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, ) else: raise err # Didn't work, try v1 API. # Note the v1 API returns a tuple of `(200, content)` _, content = await self.transport_layer.send_invite_v1( destination=destination, room_id=pdu.room_id, event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) return content
def _check_size_limits(event: EventBase) -> None: if len(event.user_id) > 255: raise EventSizeError("'user_id' too large") if len(event.room_id) > 255: raise EventSizeError("'room_id' too large") if event.is_state() and len(event.state_key) > 255: raise EventSizeError("'state_key' too large") if len(event.type) > 255: raise EventSizeError("'type' too large") if len(event.event_id) > 255: raise EventSizeError("'event_id' too large") if len(encode_canonical_json(event.get_pdu_json())) > MAX_PDU_SIZE: raise EventSizeError("event too large")
async def check_event_allowed( self, event: EventBase, context: EventContext) -> 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. 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. """ # 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() # Retrieve the state events from the database. events = await self.store.get_events(prev_state_ids.values()) state_events = {(ev.type, ev.state_key): ev for ev in events.values()} # Ensure that the event is frozen, to make sure that the module is not tempted # to try to modify it. Any attempt to modify it at this point will invalidate # the hashes and signatures. event.freeze() 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 _serialize_payload( # type: ignore[override] event_id: str, store: "DataStore", event: EventBase, context: EventContext, requester: Requester, ratelimit: bool, extra_users: List[UserID], ) -> JsonDict: """ Args: event_id store requester event context ratelimit extra_users: Any extra users to notify about event """ serialized_context = await context.serialize(event, store) payload = { "event": event.get_pdu_json(), "room_version": event.room_version.identifier, "event_format_version": event.format_version, "internal_metadata": event.internal_metadata.get_dict(), "outlier": event.internal_metadata.is_outlier(), "rejected_reason": event.rejected_reason, "context": serialized_context, "requester": requester.serialize(), "ratelimit": ratelimit, "extra_users": [u.to_string() for u in extra_users], } return payload
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
async def check(ev: EventBase, state): d = ev.get_dict() d["content"] = { "msgtype": "m.text", "body": d["content"]["body"].upper(), } return True, d
async def _check_sigs_and_hash( self, room_version: RoomVersion, pdu: EventBase ) -> EventBase: """Checks that event is correctly signed by the sending server. Also checks the content hash, and redacts the event if there is a mismatch. Also runs the event through the spam checker; if it fails, redacts the event and flags it as soft-failed. Args: room_version: The room version of the PDU pdu: the event to be checked Returns: * the original event if the checks pass * a redacted version of the event (if the signature matched but the hash did not). In this case a warning will be logged. Raises: InvalidEventSignatureError if the signature check failed. Nothing will be logged in this case. """ await _check_sigs_on_pdu(self.keyring, room_version, pdu) 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.debug( "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 spam_check = await self.spam_checker.check_event_for_spam(pdu) if spam_check != self.spam_checker.NOT_SPAM: 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
def _check_size_limits(event: EventBase) -> None: def too_big(field): raise EventSizeError("%s too large" % (field, )) if len(event.user_id) > 255: too_big("user_id") if len(event.room_id) > 255: too_big("room_id") if event.is_state() and len(event.state_key) > 255: too_big("state_key") if len(event.type) > 255: too_big("type") if len(event.event_id) > 255: too_big("event_id") if len(encode_canonical_json(event.get_pdu_json())) > 65536: too_big("event")
async def check( ev: EventBase, state: StateMap[EventBase]) -> Tuple[bool, Optional[JsonDict]]: d = ev.get_dict() d["content"] = { "msgtype": "m.text", "body": d["content"]["body"].upper(), } return True, d
async def _locally_reject_invite( self, invite_event: EventBase, txn_id: Optional[str], requester: Requester, content: JsonDict, ) -> Tuple[str, int]: """Generate a local invite rejection This is called after we fail to reject an invite via a remote server. It generates an out-of-band membership event locally. Args: invite_event: the invite to be rejected txn_id: optional transaction ID supplied by the client requester: user making the rejection request, according to the access token content: additional content to include in the rejection event. Normally an empty dict. """ room_id = invite_event.room_id target_user = invite_event.state_key content["membership"] = Membership.LEAVE event_dict = { "type": EventTypes.Member, "room_id": room_id, "sender": target_user, "content": content, "state_key": target_user, } # the auth events for the new event are the same as that of the invite, plus # the invite itself. # # the prev_events are just the invite. prev_event_ids = [invite_event.event_id] auth_event_ids = invite_event.auth_event_ids() + prev_event_ids event, context = await self.event_creation_handler.create_event( requester, event_dict, txn_id=txn_id, prev_event_ids=prev_event_ids, auth_event_ids=auth_event_ids, ) event.internal_metadata.outlier = True event.internal_metadata.out_of_band_membership = True result_event = await self.event_creation_handler.handle_new_client_event( requester, event, context, extra_users=[UserID.from_string(target_user)], ) # we know it was persisted, so must have a stream ordering assert result_event.internal_metadata.stream_ordering return result_event.event_id, result_event.internal_metadata.stream_ordering
async def check_auth_rules_from_context( self, room_version_obj: RoomVersion, event: EventBase, context: EventContext, ) -> None: """Check an event passes the auth rules at its own auth events""" auth_event_ids = event.auth_event_ids() auth_events_by_id = await self._store.get_events(auth_event_ids) check_auth_rules_for_event(room_version_obj, event, auth_events_by_id.values())
async def check_auth_rules_from_context( self, event: EventBase, context: EventContext, ) -> None: """Check an event passes the auth rules at its own auth events""" await check_state_independent_auth_rules(self._store, event) auth_event_ids = event.auth_event_ids() auth_events_by_id = await self._store.get_events(auth_event_ids) check_state_dependent_auth_rules(event, auth_events_by_id.values())
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)) if result: logger.warning( "Event contains spam, redacting %s: %s", pdu.event_id, pdu.get_pdu_json(), ) return prune_event(pdu) return pdu
async def check_event_allowed(self, event: EventBase, state: StateMap[EventBase]): if event.is_state() and event.type == EventTypes.Member: await self.api.create_and_send_event_into_room({ "room_id": event.room_id, "sender": event.sender, "type": "bzh.abolivier.test3", "content": { "now": int(time.time()) }, "state_key": "", }) return True, None
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 if not event.internal_metadata.should_proactively_send(): return try: # Get the state from before the event. # We need to make sure that this is the state from before # the event and not from after it. # Otherwise if the last member on a server in a room is # banned then it won't receive the event because it won't # be in the room after the ban. destinations = await self.state.get_hosts_in_room_at_events( event.room_id, event_ids=event.prev_event_ids()) except Exception: logger.exception( "Failed to calculate hosts in room for event: %s", event.event_id, ) return destinations = { d for d in destinations if self._federation_shard_config.should_handle( self._instance_name, d) } 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: self._send_pdu(event, destinations) now = self.clock.time_msec() ts = await self.store.get_received_ts(event.event_id) synapse.metrics.event_processing_lag_by_event.labels( "federation_sender").observe((now - ts) / 1000)
def on_new_room_event( self, event: EventBase, event_pos: PersistedEventPosition, max_room_stream_token: RoomStreamToken, extra_users: Collection[UserID] = [], ): """Unwraps event and calls `on_new_room_event_args`.""" self.on_new_room_event_args( event_pos=event_pos, room_id=event.room_id, event_type=event.type, state_key=event.get("state_key"), membership=event.content.get("membership"), max_room_stream_token=max_room_stream_token, extra_users=extra_users, )
def from_event( server_name: str, event: EventBase, minimum_valid_until_ms: int, ) -> "VerifyJsonRequest": """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, key_ids=key_ids, )
def maybe_schedule_expiry(self, event: EventBase): """Schedule the expiry of an event if there's not already one scheduled, or if the one running is for an event that will expire after the provided timestamp. This function needs to invalidate the event cache, which is only possible on the master process, and therefore needs to be run on there. Args: event: The event to schedule the expiry of. """ expiry_ts = event.content.get(EventContentFields.SELF_DESTRUCT_AFTER) if not isinstance(expiry_ts, int) or event.is_state(): return # _schedule_expiry_for_event won't actually schedule anything if there's already # a task scheduled for a timestamp that's sooner than the provided one. self._schedule_expiry_for_event(event.event_id, expiry_ts)
def _can_send_event(event: EventBase, auth_events: StateMap[EventBase]) -> bool: 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) if user_level < send_level: raise AuthError( 403, "You don't have permission to post that to the room. " + "user_level (%d) < send_level (%d)" % (user_level, send_level), ) # Check state_key if hasattr(event, "state_key"): if event.state_key.startswith("@"): if event.state_key != event.user_id: raise AuthError(403, "You are not allowed to set others state") return True
async def send_nonmember_event( self, requester: Requester, event: EventBase, context: EventContext, ratelimit: bool = True, ) -> int: """ Persists and notifies local clients and federation of an event. Args: requester event the event to send. context: the context of the event. ratelimit: Whether to rate limit this send. Return: The stream_id of the persisted event. """ if event.type == EventTypes.Member: raise SynapseError( 500, "Tried to send member event through non-member codepath") user = UserID.from_string(event.sender) assert self.hs.is_mine(user), "User must be our own: %s" % (user, ) if event.is_state(): prev_state = await self.deduplicate_state_event(event, context) if prev_state is not None: logger.info( "Not bothering to persist state event %s duplicated by %s", event.event_id, prev_state.event_id, ) return prev_state return await self.handle_new_client_event(requester=requester, event=event, context=context, ratelimit=ratelimit)