diff --git a/chat_functions.py b/chat_functions.py index 7e32f14..fd6ffc5 100644 --- a/chat_functions.py +++ b/chat_functions.py @@ -46,6 +46,7 @@ async def send_text_to_room( room_id, "m.room.message", content, + ignore_unverified_devices=True, ) except SendRetryError: logger.exception(f"Unable to send message response to {room_id}") diff --git a/config.py b/config.py index f20e1f8..8aa283f 100644 --- a/config.py +++ b/config.py @@ -41,24 +41,20 @@ class Config(object): handler.setFormatter(formatter) logger.addHandler(handler) - # Database setup - self.database_filepath = self._get_cfg(["database", "filepath"], required=True) + # Storage setup + self.database_filepath = self._get_cfg(["storage", "database_filepath"], required=True) + self.store_filepath = self._get_cfg(["storage", "store_filepath"], required=True) # Matrix bot account setup self.user_id = self._get_cfg(["matrix", "user_id"], required=True) if not re.match("@.*:.*", self.user_id): raise ConfigError("matrix.user_id must be in the form @name:domain") - self.access_token = self._get_cfg(["matrix", "access_token"], required=True) - - self.device_id = self._get_cfg(["matrix", "device_id"]) - if not self.device_id: - logger.warning( - "Config option matrix.device_id is not provided, which means " - "that end-to-end encryption won't work correctly" - ) - + self.user_password = self._get_cfg(["matrix", "user_password"], required=True) + self.device_id = self._get_cfg(["matrix", "device_id"], required=True) + self.device_name = self._get_cfg(["matrix", "device_name"], default="nio-template") self.homeserver_url = self._get_cfg(["matrix", "homeserver_url"], required=True) + self.enable_encryption = self._get_cfg(["matrix", "enable_encryption"], default=False) self.command_prefix = self._get_cfg(["command_prefix"], default="!c") + " " @@ -75,8 +71,6 @@ class Config(object): ConfigError: If required is specified and the object is not found (and there is no default value provided), this error will be raised """ - path_str = '.'.join(path) - # Sift through the the config until we reach our option config = self.config for name in path: @@ -86,7 +80,7 @@ class Config(object): if config is None: # Raise an error if it was required if required or not default: - raise ConfigError(f"Config option {path_str} is required") + raise ConfigError(f"Config option {'.'.join(path)} is required") # or return the default value return default diff --git a/main.py b/main.py index d4e31ee..55f0936 100644 --- a/main.py +++ b/main.py @@ -2,17 +2,22 @@ import logging import asyncio +from time import sleep from nio import ( AsyncClient, AsyncClientConfig, RoomMessageText, InviteEvent, - SyncError, + LoginError, + LocalProtocolError, +) +from aiohttp import ( + ServerDisconnectedError, + ClientConnectionError, ) from callbacks import Callbacks from config import Config from storage import Storage -from sync_token import SyncToken logger = logging.getLogger(__name__) @@ -28,41 +33,73 @@ async def main(): client_config = AsyncClientConfig( max_limit_exceeded=0, max_timeouts=0, + store_sync_tokens=True, ) # Initialize the matrix client + if config.enable_encryption: + store_path = config.store_filepath + else: + store_path = None + client = AsyncClient( config.homeserver_url, config.user_id, device_id=config.device_id, + store_path=store_path, config=client_config, ) - # Assign an access token to the bot instead of logging in and creating a new device - client.access_token = config.access_token - # Set up event callbacks callbacks = Callbacks(client, store, config) client.add_event_callback(callbacks.message, (RoomMessageText,)) client.add_event_callback(callbacks.invite, (InviteEvent,)) - # Create a new sync token, attempting to load one from the database if it has one already - sync_token = SyncToken(store) - - # Sync loop + # Keep trying to reconnect on failure (with some time in-between) while True: - # Sync with the server - sync_response = await client.sync(timeout=30000, full_state=True, - since=sync_token.token) + try: + # Try to login with the configured username/password + try: + login_response = await client.login( + password=config.user_password, + device_name=config.device_name, + ) - # Check if the sync had an error - if type(sync_response) == SyncError: - logger.warning("Error in client sync: %s", sync_response.message) - continue + # Check if login failed + if type(login_response) == LoginError: + logger.error(f"Failed to login: %s", login_response.message) + return False + except LocalProtocolError as e: + # There's an edge case here where the user enables encryption but hasn't installed + # the correct C dependencies. In that case, a LocalProtocolError is raised on login. + # Warn the user if these conditions are met. + if config.enable_encryption: + logger.fatal( + "Failed to login and encryption is enabled. Have you installed the correct dependencies? " + "https://github.com/poljar/matrix-nio#installation" + ) + return False + else: + # We don't know why this was raised. Throw it at the user + logger.fatal("Error logging in: %s", e) - # Save the latest sync token to the database - token = sync_response.next_batch - if token: - sync_token.update(token) + # Login succeeded! + + # Sync encryption keys with the server + # Required for participating in encrypted rooms + if client.should_upload_keys: + await client.keys_upload() + + logger.info(f"Logged in as {config.user_id}") + await client.sync_forever(timeout=30000, full_state=True) + + except (ClientConnectionError, ServerDisconnectedError): + logger.warning("Unable to connect to homeserver, retrying in 15s...") + + # Sleep so we don't bombard the server with login requests + sleep(15) + finally: + # Make sure to close the client connection on disconnect + await client.close() asyncio.get_event_loop().run_until_complete(main()) diff --git a/sample.config.yaml b/sample.config.yaml index 088a5a1..bfb16f4 100644 --- a/sample.config.yaml +++ b/sample.config.yaml @@ -9,16 +9,27 @@ command_prefix: "!c" matrix: # The Matrix User ID of the bot account user_id: "@bot:example.com" - # The access token of the bot account - access_token: "" - # The device ID given on login - device_id: ABCDEFGHIJ + # Matrix account password + user_password: "" # The URL of the homeserver to connect to homeserver_url: https://example.com + # The device ID that is **non pre-existing** device + # If this device ID already exists, messages will be dropped silently in encrypted rooms + device_id: ABCDEFGHIJ + # What to name the logged in device + device_name: nio-template + # End-to-end encryption support + # + # Enabling this requires installing the matrix-nio encryption dependencies + # as described here: https://github.com/poljar/matrix-nio#installation + enable_encryption: true -database: +storage: # The path to the database - filepath: "bot.db" + database_filepath: "bot.db" + # The path to a directory for internal bot storage + # containing encryption keys, sync tokens, etc. + store_filepath: "./store" # Logging setup logging: