def _select_server(self, config): server = config['server'] if server.startswith('http'): return server elif server != 'auto': raise TransportError( 'Invalid matrix server specified (valid values: "auto" or a URL)' ) def _get_rtt(server_name): return server_name, get_http_rtt(server_name) get_rtt_jobs = [ gevent.spawn(_get_rtt, server_name) for server_name in config['available_servers'] ] gevent.joinall(get_rtt_jobs) sorted_servers = sorted( (job.value for job in get_rtt_jobs if job.value[1] is not None), key=itemgetter(1), ) self.log.debug('Matrix homeserver RTT times', rtt_times=sorted_servers) if not sorted_servers: raise TransportError( 'Could not select a Matrix server. No candidates remaining. ' 'Please check your network connectivity.', ) best_server, rtt = sorted_servers[0] self.log.info( 'Automatically selecting matrix homeserver based on RTT', homeserver=best_server, rtt=rtt, ) return best_server
def make_client( handle_messages_callback: Callable[[MatrixSyncMessages], bool], servers: List[str], *args: Any, **kwargs: Any, ) -> GMatrixClient: """Given a list of possible servers, chooses the closest available and create a GMatrixClient Params: servers: list of servers urls, with scheme (http or https) Rest of args and kwargs are forwarded to GMatrixClient constructor Returns: GMatrixClient instance for one of the available servers """ if len(servers) > 1: sorted_servers = sort_servers_closest(servers) log.debug("Selecting best matrix server", sorted_servers=sorted_servers) elif len(servers) == 1: sorted_servers = {servers[0]: 0} else: raise TransportError("No valid servers list given") last_ex = None for server_url, rtt in sorted_servers.items(): client = GMatrixClient(handle_messages_callback, server_url, *args, **kwargs) retries = 3 while retries: retries -= 1 try: client.api._send("GET", "/versions", api_path="/_matrix/client") except MatrixRequestError as ex: log.warning( "Matrix server returned an error, retrying", server_url=server_url, _exception=ex, ) last_ex = ex except MatrixError as ex: log.warning("Selected server not usable", server_url=server_url, _exception=ex) last_ex = ex retries = 0 else: log.info( "Using Matrix server", server_url=server_url, server_ident=client.api.server_ident, average_rtt=rtt, ) return client raise TransportError( "Unable to find a reachable Matrix server. Please check your network connectivity." ) from last_ex
def __init__(self, config: dict): super().__init__() self._config = config self._raiden_service: RaidenService = None def _http_retry_delay() -> Iterable[float]: # below constants are defined in raiden.app.App.DEFAULT_CONFIG return udp_utils.timeout_exponential_backoff( self._config['retries_before_backoff'], self._config['retry_interval'] / 5, self._config['retry_interval'], ) while True: self._server_url: str = self._select_server(config) self._server_name = config.get('server_name', urlparse(self._server_url).hostname) client_class = config.get('client_class', GMatrixClient) self._client: GMatrixClient = client_class( self._server_url, http_pool_maxsize=4, http_retry_timeout=40, http_retry_delay=_http_retry_delay, ) try: self._client.api._send('GET', '/versions', api_path='/_matrix/client') break except MatrixError as ex: if config['server'] != 'auto': raise TransportError( f"Could not connect to requested server '{config['server']}'", ) from ex config['available_servers'].remove(self._server_url) if len(config['available_servers']) == 0: raise TransportError( f"Unable to find a reachable Matrix server. " f"Please check your network connectivity.", ) from ex log.warning(f"Selected server '{self._server_url}' not usable. Retrying.") self.greenlets = list() self._discovery_room: Room = None # partner need to be in this dict to be listened on self._address_to_userids: Dict[Address, Set[str]] = defaultdict(set) self._address_to_presence: Dict[Address, UserPresence] = dict() self._userid_to_presence: Dict[str, UserPresence] = dict() self._discovery_room_alias = None self._stop_event = gevent.event.Event() self._stop_event.set() self._client.add_invite_listener(self._handle_invite) self._client.add_presence_listener(self._handle_presence_change) self._messages_cache = TTLCache(32, 4) self._health_lock = Semaphore() self._getroom_lock = Semaphore()
def __init__(self, config: dict): self._bound_logger = None self._raiden_service: RaidenService = None while True: self._server_url: str = self._select_server(config) self._server_name = config.get('server_name', urlparse(self._server_url).hostname) client_class = config.get('client_class', GMatrixClient) self._client: GMatrixClient = client_class( self._server_url, max_retries=5, pool_maxsize=4, ) try: self._client.api._send('GET', '/versions', api_path='/_matrix/client') break except MatrixError as ex: if config['server'] != 'auto': raise TransportError( f"Could not connect to requested server '{config['server']}'", ) from ex config['available_servers'].remove(self._server_url) if len(config['available_servers']) == 0: raise TransportError( f"Unable to find a reachable Matrix server. " f"Please check your network connectivity.", ) from ex log.warning( f"Selected server '{self._server_url}' not usable. Retrying." ) self.greenlets = list() self._discovery_room: Room = None self._messageids_to_asyncresult: Dict[Address, AsyncResult] = dict() # partner need to be in this dict to be listened on self._address_to_userids: Dict[Address, Set[str]] = defaultdict(set) self._address_to_presence: Dict[Address, UserPresence] = dict() self._userid_to_presence: Dict[str, UserPresence] = dict() self._discovery_room_alias = None self._discovery_room_alias_full = None self._login_retry_wait = config.get('login_retry_wait', 0.5) self._logout_timeout = config.get('logout_timeout', 10) self._running = False self._health_semaphore = gevent.lock.Semaphore() self._client.add_invite_listener(self._handle_invite) self._client.add_presence_listener(self._handle_presence_change)
def sort_servers_closest( servers: Sequence[str], max_timeout: float = 3.0, samples_per_server: int = 3, sample_delay: float = 0.125, ) -> Dict[str, float]: """Sorts a list of servers by http round-trip time Params: servers: sequence of http server urls Returns: sequence of pairs of url,rtt in seconds, sorted by rtt, excluding failed and excessively slow servers (possibly empty) The default timeout was chosen after measuring the long tail of the development matrix servers. Under no stress, servers will have a very long tail of up to 2.5 seconds (measured 15/01/2020), which can lead to failure during startup if the timeout is too low. This increases the timeout so that the network hiccups won't cause Raiden startup failures. """ if not {urlparse(url).scheme for url in servers}.issubset({"http", "https"}): raise TransportError("Invalid server urls") rtt_greenlets = set( spawn_named( "get_average_http_response_time", get_average_http_response_time, url=server_url, samples=samples_per_server, sample_delay=sample_delay, ) for server_url in servers ) total_timeout = samples_per_server * (max_timeout + sample_delay) results = [] for greenlet in gevent.iwait(rtt_greenlets, timeout=total_timeout): result = greenlet.get() if result is not None: results.append(result) gevent.killall(rtt_greenlets) if not results: raise TransportError( f"No Matrix server available with good latency, requests takes more " f"than {max_timeout} seconds." ) server_url_to_rtt = dict(sorted(results, key=itemgetter(1))) log.debug("Available Matrix homeservers", servers=server_url_to_rtt) return server_url_to_rtt
def sort_servers_closest( servers: Sequence[str]) -> Sequence[Tuple[str, float]]: """Sorts a list of servers by http round-trip time Params: servers: sequence of http server urls Returns: sequence of pairs of url,rtt in seconds, sorted by rtt, excluding failed servers (possibly empty) """ if not {urlparse(url).scheme for url in servers}.issubset({'http', 'https'}): raise TransportError('Invalid server urls') get_rtt_jobs = [ gevent.spawn(lambda url: (url, get_http_rtt(url)), server_url) for server_url in servers ] # these tasks should never raise, returns None on errors gevent.joinall(get_rtt_jobs, raise_error=False) # block and wait tasks sorted_servers: List[Tuple[str, float]] = sorted( (job.value for job in get_rtt_jobs if job.value[1] is not None), key=itemgetter(1), ) log.debug('Matrix homeserver RTT times', rtt_times=sorted_servers) return sorted_servers
def warm_users(self, users: List[User]) -> None: for user in users: user_id = user.user_id cached_displayname = self.userid_to_displayname.get(user_id) if cached_displayname is None: # The cache is cold, query and warm it. if not user.displayname: # Handles an edge case where the Matrix federation does not # have the profile for a given userid. The server response # is roughly: # # {"errcode":"M_NOT_FOUND","error":"Profile was not found"} try: user.get_display_name() except MatrixRequestError: raise TransportError( "Could not get 'display_name' for user") if user.displayname is not None: self.userid_to_displayname[user.user_id] = user.displayname elif user.displayname is None: user.displayname = cached_displayname elif user.displayname != cached_displayname: log.debug( "User displayname changed!", cached=cached_displayname, current=user.displayname, ) self.userid_to_displayname[user.user_id] = user.displayname
def _client_exception_handler(self, greenlet): self._running = False try: greenlet.get() except MatrixError as ex: gevent.get_hub().handle_system_error( TransportError, TransportError( f'Unexpected error while communicating with Matrix homeserver: {ex}', ), )
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
def make_client(servers: Sequence[str], *args, **kwargs) -> GMatrixClient: """Given a list of possible servers, chooses the closest available and create a GMatrixClient Params: servers: list of servers urls, with scheme (http or https) Rest of args and kwargs are forwarded to GMatrixClient constructor Returns: GMatrixClient instance for one of the available servers """ if len(servers) > 1: sorted_servers = [ server_url for (server_url, _) in sort_servers_closest(servers) ] log.info( 'Automatically selecting matrix homeserver based on RTT', sorted_servers=sorted_servers, ) elif len(servers) == 1: sorted_servers = servers else: raise TransportError('No valid servers list given') last_ex = None for server_url in sorted_servers: server_url: str = server_url client = GMatrixClient(server_url, *args, **kwargs) try: client.api._send('GET', '/versions', api_path='/_matrix/client') except MatrixError as ex: log.warning('Selected server not usable', server_url=server_url, _exception=ex) last_ex = ex else: break else: raise TransportError( 'Unable to find a reachable Matrix server. Please check your network connectivity.', ) from last_ex return client
def make_message_batches( message_texts: Iterable[str], _max_batch_size: int = MATRIX_MAX_BATCH_SIZE ) -> Generator[str, None, None]: """ Group messages into newline separated batches not exceeding ``_max_batch_size``. """ current_batch: List[str] = [] size = 0 for message_text in message_texts: if size + len(message_text) > _max_batch_size: if size == 0: # A single message exceeds the maximum batch size. This should not happen. raise TransportError( f"Message exceeds batch size. Size: {len(message_text)}, " f"Max: {MATRIX_MAX_BATCH_SIZE}, Message: {message_text}") yield "\n".join(current_batch) current_batch = [] size = 0 current_batch.append(message_text) size += len(message_text) if current_batch: yield "\n".join(current_batch)
def _start_client(self, server_url: str) -> GMatrixClient: assert self.user_manager if self.stop_event.is_set(): raise TransportError() if server_url == self.main_client.api.base_url: client = self.main_client else: # Also handle messages on the other clients, # since to-device communication to the PFS only happens via the local user # on each homeserver client = make_client( handle_messages_callback=self.main_client. handle_messages_callback, servers=[server_url], http_pool_maxsize=4, http_retry_timeout=40, http_retry_delay=matrix_http_retry_delay, ) self.server_url_to_other_clients[server_url] = client log.debug("Created client for other server", server_url=server_url) try: login(client, signer=self.local_signer, device_id=self.device_id) log.debug("Matrix login successful", server_url=server_url) except (MatrixRequestError, ValueError): raise ConnectionError("Could not login/register to matrix.") client.start_listener_thread( DEFAULT_TRANSPORT_MATRIX_SYNC_TIMEOUT, DEFAULT_TRANSPORT_MATRIX_SYNC_LATENCY, ) # main client is already added upon MultiClientUserAddressManager.start() if server_url != self.main_client.api.base_url: self.user_manager.add_client(client) return client
def join_global_room(client: GMatrixClient, name: str, servers: Sequence[str] = ()) -> Room: """Join or create a global public room with given name First, try to join room on own server (client-configured one) If can't, try to join on each one of servers, and if able, alias it in our server If still can't, create a public room with name in our server Params: client: matrix-python-sdk client instance name: name or alias of the room (without #-prefix or server name suffix) servers: optional: sequence of known/available servers to try to find the room in Returns: matrix's Room instance linked to client """ our_server_name = urlparse(client.api.base_url).netloc assert our_server_name, 'Invalid client\'s homeserver url' servers = [our_server_name] + [ # client's own server first urlparse(s).netloc for s in servers if urlparse(s).netloc not in {None, '', our_server_name} ] our_server_global_room_alias_full = f'#{name}:{servers[0]}' # try joining a global room on any of the available servers, starting with ours for server in servers: global_room_alias_full = f'#{name}:{server}' try: global_room = client.join_room(global_room_alias_full) except MatrixRequestError as ex: if ex.code not in (403, 404, 500): raise log.debug( 'Could not join global room', room_alias_full=global_room_alias_full, _exception=ex, ) else: if our_server_global_room_alias_full not in global_room.aliases: # we managed to join a global room, but it's not aliased in our server global_room.add_room_alias(our_server_global_room_alias_full) global_room.aliases.append(our_server_global_room_alias_full) break else: log.debug('Could not join any global room, trying to create one') for _ in range(JOIN_RETRIES): try: global_room = client.create_room(name, is_public=True) except MatrixRequestError as ex: if ex.code not in (400, 409): raise try: global_room = client.join_room( our_server_global_room_alias_full, ) except MatrixRequestError as ex: if ex.code not in (404, 403): raise else: break else: break else: raise TransportError('Could neither join nor create a global room') return global_room
def create_sync_filter( self, rooms: Optional[Iterable[Room]] = None, not_rooms: Optional[Iterable[Room]] = None, limit: Optional[int] = None, ) -> Optional[int]: """ Create a matrix sync filter A whitelist and blacklist of rooms can be supplied optionally. If no whitelist ist given, all rooms are whitelisted. The blacklist is applied on top of the whitelist. Ref. https://matrix.org/docs/spec/client_server/r0.6.0#api-endpoints Args: rooms: whitelist of rooms, if not given all rooms are whitelisted not_rooms: blacklist of rooms, applied after the whitelist limit: maximum number of messages to return """ if not_rooms is None and rooms is None and limit is None: return None broadcast_room_filter: Dict[str, Dict] = { # Get all presence updates "presence": { "types": ["m.presence"] }, # filter account data "account_data": { "not_types": ["*"] }, # Ignore "message receipts" from all rooms "room": { "ephemeral": { "not_types": ["m.receipt"] } }, } if not_rooms: negative_rooms = [room.room_id for room in not_rooms] broadcast_room_filter["room"].update({ # Filter out all unwanted rooms "not_rooms": negative_rooms }) if rooms: positive_rooms = [room.room_id for room in rooms] broadcast_room_filter["room"].update({ # Set all wanted rooms "rooms": positive_rooms }) limit_filter: Dict[str, Any] = {} if limit is not None: limit_filter = {"room": {"timeline": {"limit": limit}}} final_filter = broadcast_room_filter merge_dict(final_filter, limit_filter) try: # 0 is a valid filter ID filter_response = self.api.create_filter(self.user_id, final_filter) filter_id = filter_response.get("filter_id") log.debug("Sync filter created", filter_id=filter_id, filter=final_filter) except MatrixRequestError as ex: raise TransportError( f"Failed to create filter: {final_filter} for user {self.user_id}" ) from ex return filter_id