diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py index 15da3f053d8..f1e59f048ba 100644 --- a/homeassistant/components/notify/matrix.py +++ b/homeassistant/components/notify/matrix.py @@ -7,12 +7,13 @@ https://home-assistant.io/components/notify.matrix/ import logging import json import os +from urllib.parse import urlparse import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, + BaseNotificationService) from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL REQUIREMENTS = ['matrix-client==0.0.6'] @@ -20,7 +21,6 @@ REQUIREMENTS = ['matrix-client==0.0.6'] _LOGGER = logging.getLogger(__name__) SESSION_FILE = 'matrix.conf' -AUTH_TOKENS = dict() CONF_HOMESERVER = 'homeserver' CONF_DEFAULT_ROOM = 'default_room' @@ -36,120 +36,160 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" - if not AUTH_TOKENS: - load_token(hass.config.path(SESSION_FILE)) + from matrix_client.client import MatrixRequestError - return MatrixNotificationService( - config.get(CONF_HOMESERVER), - config.get(CONF_DEFAULT_ROOM), - config.get(CONF_VERIFY_SSL), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD) - ) + try: + return MatrixNotificationService( + os.path.join(hass.config.path(), SESSION_FILE), + config.get(CONF_HOMESERVER), + config.get(CONF_DEFAULT_ROOM), + config.get(CONF_VERIFY_SSL), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) + + except MatrixRequestError: + return None class MatrixNotificationService(BaseNotificationService): - """Wrapper for the Matrix Notification Client.""" + """Send Notifications to a Matrix Room.""" - def __init__(self, homeserver, default_room, verify_ssl, + def __init__(self, config_file, homeserver, default_room, verify_ssl, username, password): - """Buffer configuration data for send_message.""" + """Setup the client.""" + self.session_filepath = config_file + self.auth_tokens = self.get_auth_tokens() + self.homeserver = homeserver self.default_room = default_room self.verify_tls = verify_ssl self.username = username self.password = password - def send_message(self, message, **kwargs): - """Wrapper function pass default parameters to actual send_message.""" - send_message( - message, - self.homeserver, - kwargs.get(ATTR_TARGET) or [self.default_room], - self.verify_tls, - self.username, - self.password - ) + self.mx_id = "{user}@{homeserver}".format( + user=username, homeserver=urlparse(homeserver).netloc) + # Login, this will raise a MatrixRequestError if login is unsuccessful + self.client = self.login() -def load_token(session_file): - """Load authentication tokens from persistent storage, if exists.""" - if not os.path.exists(session_file): - return + def get_auth_tokens(self): + """ + Read sorted authentication tokens from disk. - with open(session_file) as handle: - data = json.load(handle) + Returns the auth_tokens dictionary. + """ + if not os.path.exists(self.session_filepath): + return {} - for mx_id, token in data.items(): - AUTH_TOKENS[mx_id] = token - - -def store_token(mx_id, token): - """Store authentication token to session and persistent storage.""" - AUTH_TOKENS[mx_id] = token - - with open(SESSION_FILE, 'w') as handle: - handle.write(json.dumps(AUTH_TOKENS)) - - -def send_message(message, homeserver, target_rooms, verify_tls, - username, password): - """Do everything thats necessary to send a message to a Matrix room.""" - from matrix_client.client import MatrixClient, MatrixRequestError - - def login_by_token(): - """Login using authentication token.""" try: - return MatrixClient( - base_url=homeserver, - token=AUTH_TOKENS[mx_id], - user_id=username, - valid_cert_check=verify_tls - ) - except MatrixRequestError as ex: - _LOGGER.info("login_by_token: (%d) %s", ex.code, ex.content) + with open(self.session_filepath) as handle: + data = json.load(handle) + + auth_tokens = {} + for mx_id, token in data.items(): + auth_tokens[mx_id] = token + + return auth_tokens + + except (OSError, IOError, PermissionError) as ex: + _LOGGER.warning( + "Loading authentication tokens from file '%s' failed: %s", + self.session_filepath, str(ex)) + return {} + + def store_auth_token(self, token): + """Store authentication token to session and persistent storage.""" + self.auth_tokens[self.mx_id] = token - def login_by_password(): - """Login using password authentication.""" try: - _client = MatrixClient( - base_url=homeserver, valid_cert_check=verify_tls) - _client.login_with_password(username, password) - store_token(mx_id, _client.token) - return _client - except MatrixRequestError as ex: - _LOGGER.error("login_by_password: (%d) %s", ex.code, ex.content) + with open(self.session_filepath, 'w') as handle: + handle.write(json.dumps(self.auth_tokens)) - # This is as close as we can get to the mx_id, since there is no - # homeserver discovery protocol we have to fall back to the homeserver url - # instead of the actual domain it serves. - mx_id = "{user}@{homeserver}".format(user=username, homeserver=homeserver) + # Not saving the tokens to disk should not stop the client, we can just + # login using the password every time. + except (OSError, IOError, PermissionError) as ex: + _LOGGER.warning( + "Storing authentication tokens to file '%s' failed: %s", + self.session_filepath, str(ex)) - if mx_id in AUTH_TOKENS: - client = login_by_token() + def login(self): + """Login to the matrix homeserver and return the client instance.""" + from matrix_client.client import MatrixRequestError + + # Attempt to generate a valid client using either of the two possible + # login methods: + client = None + + # If we have an authentication token + if self.mx_id in self.auth_tokens: + try: + client = self.login_by_token() + _LOGGER.debug("Logged in using stored token.") + + except MatrixRequestError as ex: + _LOGGER.warning( + "Login by token failed, falling back to password. " + "login_by_token raised: (%d) %s", + ex.code, ex.content) + + # If we still don't have a client try password. if not client: - client = login_by_password() - if not client: + try: + client = self.login_by_password() + _LOGGER.debug("Logged in using password.") + + except MatrixRequestError as ex: _LOGGER.error( - "Login failed, both token and username/password invalid") - return - else: - client = login_by_password() - if not client: - _LOGGER.error("Login failed, username/password invalid") - return + "Login failed, both token and username/password invalid " + "login_by_password raised: (%d) %s", + ex.code, ex.content) - rooms = client.get_rooms() - for target_room in target_rooms: - try: - if target_room in rooms: - room = rooms[target_room] - else: - room = client.join_room(target_room) + # re-raise the error so the constructor can catch it. + raise - _LOGGER.debug(room.send_text(message)) - except MatrixRequestError as ex: - _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", - target_room, ex.code, ex.content - ) + return client + + def login_by_token(self): + """Login using authentication token and return the client.""" + from matrix_client.client import MatrixClient + + return MatrixClient( + base_url=self.homeserver, + token=self.auth_tokens[self.mx_id], + user_id=self.username, + valid_cert_check=self.verify_tls) + + def login_by_password(self): + """Login using password authentication and return the client.""" + from matrix_client.client import MatrixClient + + _client = MatrixClient( + base_url=self.homeserver, + valid_cert_check=self.verify_tls) + + _client.login_with_password(self.username, self.password) + + self.store_auth_token(_client.token) + + return _client + + def send_message(self, message, **kwargs): + """Send the message to the matrix server.""" + from matrix_client.client import MatrixRequestError + + target_rooms = kwargs.get(ATTR_TARGET) or [self.default_room] + + rooms = self.client.get_rooms() + for target_room in target_rooms: + try: + if target_room in rooms: + room = rooms[target_room] + else: + room = self.client.join_room(target_room) + + _LOGGER.debug(room.send_text(message)) + + except MatrixRequestError as ex: + _LOGGER.error( + "Unable to deliver message to room '%s': (%d): %s", + target_room, ex.code, ex.content)