def __init__( self, private_key: PrivateKey, chain_id: ChainID, device_id: DeviceIDs, message_received_callback: Callable[[Message], None], servers: Optional[List[str]] = None, ) -> None: super().__init__() self.chain_id = chain_id self.device_id = device_id self.message_received_callback = message_received_callback self._displayname_cache = DisplayNameCache() self.startup_finished = AsyncResult() self._client_manager = ClientManager( available_servers=servers, device_id=self.device_id, chain_id=self.chain_id, private_key=private_key, handle_matrix_sync=self._handle_matrix_sync, ) self.base_url = self._client.api.base_url self.user_manager = MultiClientUserAddressManager( client=self._client, displayname_cache=self._displayname_cache, ) self._rate_limiter = RateLimiter( allowed_bytes=MATRIX_RATE_LIMIT_ALLOWED_BYTES, reset_interval=MATRIX_RATE_LIMIT_RESET_INTERVAL, )
def __init__( self, private_key: str, chain_id: ChainID, service_room_suffix: str, message_received_callback: Callable[[Message], None], address_reachability_changed_callback: Callable[ [Address, AddressReachability], None] = None, servers: List[str] = None, ) -> None: super().__init__() self.private_key = private_key self.chain_id = chain_id self.service_room_suffix = service_room_suffix self.message_received_callback = message_received_callback if servers: self.available_servers = servers else: self.available_servers = get_matrix_servers( DEFAULT_MATRIX_KNOWN_SERVERS[Environment. PRODUCTION] if chain_id == 1 else DEFAULT_MATRIX_KNOWN_SERVERS[Environment.DEVELOPMENT]) self.client = make_client( servers=self.available_servers, http_pool_maxsize=4, http_retry_timeout=40, http_retry_delay=matrix_http_retry_delay, ) self.broadcast_rooms: List[Room] = [] self._displayname_cache = DisplayNameCache() self._user_manager: Optional[UserAddressManager] = None if address_reachability_changed_callback is not None: self._user_manager = UserAddressManager( client=self.client, displayname_cache=self._displayname_cache, address_reachability_changed_callback= address_reachability_changed_callback, ) self.startup_finished = Event() self.rate_limiter = RateLimiter( allowed_bytes=MATRIX_RATE_LIMIT_ALLOWED_BYTES, reset_interval=MATRIX_RATE_LIMIT_RESET_INTERVAL, )
def user_addr_mgr(dummy_matrix_client, address_reachability_callback, user_presence_callback): address_manager = NonValidatingUserAddressManager( client=dummy_matrix_client, displayname_cache=DisplayNameCache(), address_reachability_changed_callback=address_reachability_callback, user_presence_changed_callback=user_presence_callback, ) def fetch_user_presence(user_id): if user_id in address_manager._userid_to_presence.keys(): return address_manager.get_userid_presence(user_id) else: presence = UserPresence( dummy_matrix_client.get_user_presence(user_id)) address_manager._userid_to_presence[user_id] = presence return address_manager._userid_to_presence[user_id] address_manager._fetch_user_presence = fetch_user_presence address_manager.start() yield address_manager address_manager.stop()
def __init__(self, reachabilities: Dict[Address, AddressReachability]) -> None: self.reachabilities = reachabilities self.times = {address: datetime.utcnow() for address in reachabilities} self._userid_to_presence: dict = mock.MagicMock() self._address_to_userids: dict = mock.MagicMock() self._address_to_userids.__getitem__ = lambda self, key: { get_user_id_from_address(key) } self._displayname_cache = DisplayNameCache()
def test_client_manager_start(get_accounts, get_private_key): server_urls = [f"https://example0{i}.com" for i in range(5)] (c1, ) = get_accounts(1) private_key = get_private_key(c1) client_mock = Mock() client_mock.api.base_url = "https://example00.com" client_mock.user_id = "1" client_mock.sync_worker = AsyncResult() start_client_counter = 0 def mock_start_client(server_url: str): # pylint: disable=unused-argument nonlocal start_client_counter client_mock.sync_worker = AsyncResult() start_client_counter += 1 return client_mock with patch.multiple( "raiden_libs.matrix", make_client=Mock(return_value=client_mock), get_matrix_servers=Mock(return_value=server_urls), login=Mock(), join_broadcast_room=Mock(), ): client_manager = ClientManager( available_servers=[f"https://example0{i}.com" for i in range(5)], broadcast_room_alias_prefix="_service", chain_id=ChainID(1), device_id=DeviceIDs.PFS, private_key=private_key, handle_matrix_sync=lambda s: True, ) client_manager._start_client = mock_start_client # pylint: disable=protected-access assert client_manager.known_servers == server_urls uam = MultiClientUserAddressManager( client=client_manager.main_client, displayname_cache=DisplayNameCache(), ) uam.start() client_manager.user_manager = uam client_manager.stop_event.clear() gevent.spawn(client_manager.connect_client_forever, client_mock.api.base_url) gevent.sleep(2) client_mock.sync_worker.set(True) gevent.sleep(2) client_manager.stop_event.set() client_mock.sync_worker.set(True) assert start_client_counter == 2
def test_filter_presences_by_client(presence_event, server_index): server_urls = [f"https://example0{i}.com" for i in range(5)] uuids_to_callbacks = {} server_url_to_clients = {} server_url_to_processed_presence = [] def _mock_add_presence_listener(listener): uuid = uuid1() uuids_to_callbacks[uuid] = listener return uuid def _mock_remove_presence_listener(listener_id): uuids_to_callbacks.pop(listener_id) def _mock_presence_listener( self, event: Dict[str, Any], presence_update_id: int # pylint: disable=unused-argument ) -> None: server_url_to_processed_presence.append(event) for server_url in server_urls: client = Mock() client.api.base_url = server_url client.add_presence_listener = _mock_add_presence_listener client.remove_presence_listener = _mock_remove_presence_listener server_url_to_clients[server_url] = client main_client = server_url_to_clients.pop(server_urls[0]) with mock.patch.object(MultiClientUserAddressManager, "_presence_listener", new=_mock_presence_listener): uam = MultiClientUserAddressManager( client=main_client, displayname_cache=DisplayNameCache(), ) uam.start() for client in server_url_to_clients.values(): uam.add_client(client) # call presence listener on all clients for listener in uuids_to_callbacks.values(): listener(presence_event, 0) # only the respective client should forward the presence to the uam expected_presences = 1 assert len(server_url_to_processed_presence) == expected_presences # drop the client of the last homeserver in the list # and remove listener client = server_url_to_clients[server_urls[-1]] uam.remove_client(client) # only call the listener of the main client # if the presence event comes from the server with the dropped client # it should also consume the presence, otherwise not uuids_to_callbacks[uam._listener_id](presence_event, 0) # pylint: disable=protected-access if server_index in [0, len(server_urls) - 1]: expected_presences += 1 assert len(server_url_to_processed_presence) == expected_presences
class MatrixListener(gevent.Greenlet): # pylint: disable=too-many-instance-attributes def __init__( self, private_key: PrivateKey, chain_id: ChainID, device_id: DeviceIDs, message_received_callback: Callable[[Message], None], servers: Optional[List[str]] = None, ) -> None: super().__init__() self.chain_id = chain_id self.device_id = device_id self.message_received_callback = message_received_callback self._displayname_cache = DisplayNameCache() self.startup_finished = AsyncResult() self._client_manager = ClientManager( available_servers=servers, device_id=self.device_id, chain_id=self.chain_id, private_key=private_key, handle_matrix_sync=self._handle_matrix_sync, ) self.base_url = self._client.api.base_url self.user_manager = MultiClientUserAddressManager( client=self._client, displayname_cache=self._displayname_cache, ) self._rate_limiter = RateLimiter( allowed_bytes=MATRIX_RATE_LIMIT_ALLOWED_BYTES, reset_interval=MATRIX_RATE_LIMIT_RESET_INTERVAL, ) @property def _client(self) -> GMatrixClient: return self._client_manager.main_client @property def server_url_to_other_clients(self) -> Dict[str, GMatrixClient]: return self._client_manager.server_url_to_other_clients def _run(self) -> None: # pylint: disable=method-hidden self.user_manager.start() self._client_manager.start(self.user_manager) def set_startup_finished() -> None: self._client.processed.wait() self.startup_finished.set() startup_finished_greenlet = gevent.spawn(set_startup_finished) try: assert self._client.sync_worker self._client.sync_worker.get() finally: self._client_manager.stop() gevent.joinall({startup_finished_greenlet}, raise_error=True, timeout=0) def _handle_matrix_sync(self, messages: MatrixSyncMessages) -> bool: all_messages: List[Message] = list() for room, room_messages in messages: if room is not None: # Ignore room messages # This will only handle to-device messages continue for text in room_messages: all_messages.extend(self._handle_message(room, text)) log.debug("Incoming messages", messages=all_messages) for message in all_messages: self.message_received_callback(message) return True def _handle_message(self, room: Optional[Room], message: MatrixMessage) -> List[SignedMessage]: """Handle a single Matrix message. The matrix message is expected to be a NDJSON, and each entry should be a valid JSON encoded Raiden message. If `room` is None this means we are processing a `to_device` message """ is_valid_type = (message["type"] == "m.room.message" and message["content"]["msgtype"] == MatrixMessageType.TEXT.value) if not is_valid_type: return [] sender_id = message["sender"] self._displayname_cache.warm_users([User(self._client.api, sender_id)]) # handles the "Could not get 'display_name' for user" case try: displayname = self._displayname_cache.userid_to_displayname[ sender_id] except KeyError: log.exception("Could not warm display cache", peer_user=sender_id) return [] peer_address = validate_user_id_signature(sender_id, displayname) if not peer_address: log.debug( "Message from invalid user displayName signature", peer_user=sender_id, room=room, ) return [] data = message["content"]["body"] if not isinstance(data, str): log.warning( "Received message body not a string", peer_user=sender_id, peer_address=to_checksum_address(peer_address), room=room, ) return [] messages = deserialize_messages(data=data, peer_address=peer_address, rate_limiter=self._rate_limiter) if not messages: return [] return messages
class MatrixListener(gevent.Greenlet): # pylint: disable=too-many-instance-attributes def __init__( self, private_key: str, chain_id: ChainID, service_room_suffix: str, message_received_callback: Callable[[Message], None], address_reachability_changed_callback: Callable[ [Address, AddressReachability], None] = None, servers: List[str] = None, ) -> None: super().__init__() self.private_key = private_key self.chain_id = chain_id self.service_room_suffix = service_room_suffix self.message_received_callback = message_received_callback if servers: self.available_servers = servers else: self.available_servers = get_matrix_servers( DEFAULT_MATRIX_KNOWN_SERVERS[Environment. PRODUCTION] if chain_id == 1 else DEFAULT_MATRIX_KNOWN_SERVERS[Environment.DEVELOPMENT]) self.client = make_client( servers=self.available_servers, http_pool_maxsize=4, http_retry_timeout=40, http_retry_delay=matrix_http_retry_delay, ) self.broadcast_rooms: List[Room] = [] self._displayname_cache = DisplayNameCache() self._user_manager: Optional[UserAddressManager] = None if address_reachability_changed_callback is not None: self._user_manager = UserAddressManager( client=self.client, displayname_cache=self._displayname_cache, address_reachability_changed_callback= address_reachability_changed_callback, ) self.startup_finished = Event() self.rate_limiter = RateLimiter( allowed_bytes=MATRIX_RATE_LIMIT_ALLOWED_BYTES, reset_interval=MATRIX_RATE_LIMIT_RESET_INTERVAL, ) def listen_forever(self) -> None: self.startup_finished.wait() self.client.listen_forever() def _run(self) -> None: # pylint: disable=method-hidden self._start_client() self.client.start_listener_thread() assert self.client.sync_thread self.client.sync_thread.get() def stop(self) -> None: if self._user_manager: self._user_manager.stop() self.client.stop_listener_thread() def _start_client(self) -> None: try: if self._user_manager: self._user_manager.start() login(self.client, signer=LocalSigner(private_key=decode_hex(self.private_key))) except (MatrixRequestError, ValueError): raise ConnectionError("Could not login/register to matrix.") try: self.join_global_rooms(client=self.client, available_servers=self.available_servers) except (MatrixRequestError, TransportError): raise ConnectionError( "Could not join monitoring broadcasting room.") # Add listener for global rooms for broadcast_room in self.broadcast_rooms: broadcast_room.add_listener(self._handle_message, "m.room.message") # Signal that startup is finished self.startup_finished.set() def follow_address_presence(self, address: Address, refresh: bool = False) -> None: if self._user_manager: self._user_manager.add_address(address) if refresh: self._user_manager.populate_userids_for_address(address) self._user_manager.track_address_presence( address=address, user_ids=self._user_manager.get_userids_for_address( address)) log.debug( "Tracking address", address=to_checksum_address(address), current_presence=self._user_manager.get_address_reachability( address), refresh=refresh, ) def _get_user_from_user_id(self, user_id: str) -> User: """Creates an User from an user_id, if none, or fetch a cached User """ for broadcast_room in self.broadcast_rooms: if user_id in broadcast_room._members: # pylint: disable=protected-access user: User = broadcast_room._members[user_id] # pylint: disable=protected-access break else: user = self.client.get_user(user_id) return user def _handle_message(self, room: Any, event: dict) -> bool: """ Handle text messages sent to listening rooms """ if event["type"] != "m.room.message" or event["content"][ "msgtype"] != "m.text": # Ignore non-messages and non-text messages return False sender_id = event["sender"] user = self._get_user_from_user_id(sender_id) self._displayname_cache.warm_users([user]) peer_address = validate_userid_signature(user) if not peer_address: log.debug( "Message from invalid user displayName signature", peer_user=user.user_id, room=room, ) return False data = event["content"]["body"] if not isinstance(data, str): log.warning( "Received message body not a string", peer_user=user.user_id, peer_address=to_checksum_address(peer_address), room=room, ) return False messages = deserialize_messages(data, peer_address, self.rate_limiter) if not messages: return False for message in messages: assert message.sender, "Message has no sender" self.message_received_callback(message) return True def join_global_rooms( self, client: GMatrixClient, available_servers: Sequence[str] = ()) -> None: """Join or create a global public room with given name on all available servers. If global rooms are not found, create a public room with the name on each server. Params: client: matrix-python-sdk client instance servers: optional: sequence of known/available servers to try to find the room in """ suffix = self.service_room_suffix room_alias_prefix = make_room_alias(self.chain_id, suffix) parsed_servers = [ urlparse(s).netloc for s in available_servers if urlparse(s).netloc not in {None, ""} ] for server in parsed_servers: room_alias_full = f"#{room_alias_prefix}:{server}" log.debug(f"Trying to join {suffix} room", room_alias_full=room_alias_full) try: broadcast_room = client.join_room(room_alias_full) log.debug(f"Joined {suffix} room", room=broadcast_room) self.broadcast_rooms.append(broadcast_room) except MatrixRequestError as ex: if ex.code != 404: log.debug( f"Could not join {suffix} room, trying to create one", room_alias_full=room_alias_full, ) try: broadcast_room = client.create_room(room_alias_full, is_public=True) log.debug(f"Created {suffix} room", room=broadcast_room) self.broadcast_rooms.append(broadcast_room) except MatrixRequestError: log.debug( f"Could neither join nor create a {suffix} room", room_alias_full=room_alias_full, ) raise TransportError( f"Could neither join nor create a {suffix} room") else: log.debug( f"Could not join {suffix} room", room_alias_full=room_alias_full, _exception=ex, ) raise