Optional encryption support

This commit is contained in:
Andrew Morgan 2020-02-24 22:13:28 +00:00
parent f40373837c
commit 1eacb2926e
4 changed files with 83 additions and 40 deletions

View file

@ -46,6 +46,7 @@ async def send_text_to_room(
room_id, room_id,
"m.room.message", "m.room.message",
content, content,
ignore_unverified_devices=True,
) )
except SendRetryError: except SendRetryError:
logger.exception(f"Unable to send message response to {room_id}") logger.exception(f"Unable to send message response to {room_id}")

View file

@ -41,24 +41,20 @@ class Config(object):
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
# Database setup # Storage setup
self.database_filepath = self._get_cfg(["database", "filepath"], required=True) 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 # Matrix bot account setup
self.user_id = self._get_cfg(["matrix", "user_id"], required=True) self.user_id = self._get_cfg(["matrix", "user_id"], required=True)
if not re.match("@.*:.*", self.user_id): if not re.match("@.*:.*", self.user_id):
raise ConfigError("matrix.user_id must be in the form @name:domain") raise ConfigError("matrix.user_id must be in the form @name:domain")
self.access_token = self._get_cfg(["matrix", "access_token"], required=True) self.user_password = self._get_cfg(["matrix", "user_password"], required=True)
self.device_id = self._get_cfg(["matrix", "device_id"], required=True)
self.device_id = self._get_cfg(["matrix", "device_id"]) self.device_name = self._get_cfg(["matrix", "device_name"], default="nio-template")
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.homeserver_url = self._get_cfg(["matrix", "homeserver_url"], required=True) 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") + " " 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 ConfigError: If required is specified and the object is not found
(and there is no default value provided), this error will be raised (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 # Sift through the the config until we reach our option
config = self.config config = self.config
for name in path: for name in path:
@ -86,7 +80,7 @@ class Config(object):
if config is None: if config is None:
# Raise an error if it was required # Raise an error if it was required
if required or not default: 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 # or return the default value
return default return default

77
main.py
View file

@ -2,17 +2,22 @@
import logging import logging
import asyncio import asyncio
from time import sleep
from nio import ( from nio import (
AsyncClient, AsyncClient,
AsyncClientConfig, AsyncClientConfig,
RoomMessageText, RoomMessageText,
InviteEvent, InviteEvent,
SyncError, LoginError,
LocalProtocolError,
)
from aiohttp import (
ServerDisconnectedError,
ClientConnectionError,
) )
from callbacks import Callbacks from callbacks import Callbacks
from config import Config from config import Config
from storage import Storage from storage import Storage
from sync_token import SyncToken
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,41 +33,73 @@ async def main():
client_config = AsyncClientConfig( client_config = AsyncClientConfig(
max_limit_exceeded=0, max_limit_exceeded=0,
max_timeouts=0, max_timeouts=0,
store_sync_tokens=True,
) )
# Initialize the matrix client # Initialize the matrix client
if config.enable_encryption:
store_path = config.store_filepath
else:
store_path = None
client = AsyncClient( client = AsyncClient(
config.homeserver_url, config.homeserver_url,
config.user_id, config.user_id,
device_id=config.device_id, device_id=config.device_id,
store_path=store_path,
config=client_config, 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 # Set up event callbacks
callbacks = Callbacks(client, store, config) callbacks = Callbacks(client, store, config)
client.add_event_callback(callbacks.message, (RoomMessageText,)) client.add_event_callback(callbacks.message, (RoomMessageText,))
client.add_event_callback(callbacks.invite, (InviteEvent,)) client.add_event_callback(callbacks.invite, (InviteEvent,))
# Create a new sync token, attempting to load one from the database if it has one already # Keep trying to reconnect on failure (with some time in-between)
sync_token = SyncToken(store)
# Sync loop
while True: while True:
# Sync with the server try:
sync_response = await client.sync(timeout=30000, full_state=True, # Try to login with the configured username/password
since=sync_token.token) try:
login_response = await client.login(
password=config.user_password,
device_name=config.device_name,
)
# Check if the sync had an error # Check if login failed
if type(sync_response) == SyncError: if type(login_response) == LoginError:
logger.warning("Error in client sync: %s", sync_response.message) logger.error(f"Failed to login: %s", login_response.message)
continue 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 # Login succeeded!
token = sync_response.next_batch
if token: # Sync encryption keys with the server
sync_token.update(token) # 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()) asyncio.get_event_loop().run_until_complete(main())

View file

@ -9,16 +9,27 @@ command_prefix: "!c"
matrix: matrix:
# The Matrix User ID of the bot account # The Matrix User ID of the bot account
user_id: "@bot:example.com" user_id: "@bot:example.com"
# The access token of the bot account # Matrix account password
access_token: "" user_password: ""
# The device ID given on login
device_id: ABCDEFGHIJ
# The URL of the homeserver to connect to # The URL of the homeserver to connect to
homeserver_url: https://example.com 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 # 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 setup
logging: logging: