""" Matrix notification service. For more details about this platform, please refer to the documentation at 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.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL REQUIREMENTS = ['matrix-client==0.0.6'] _LOGGER = logging.getLogger(__name__) SESSION_FILE = 'matrix.conf' CONF_HOMESERVER = 'homeserver' CONF_DEFAULT_ROOM = 'default_room' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOMESERVER): cv.url, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_DEFAULT_ROOM): cv.string, }) def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" from matrix_client.client import MatrixRequestError 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): """Send Notifications to a Matrix Room.""" def __init__(self, config_file, homeserver, default_room, verify_ssl, username, password): """Set up 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 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 get_auth_tokens(self): """ Read sorted authentication tokens from disk. Returns the auth_tokens dictionary. """ if not os.path.exists(self.session_filepath): return {} try: 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 try: with open(self.session_filepath, 'w') as handle: handle.write(json.dumps(self.auth_tokens)) # 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)) 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: 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 " "login_by_password raised: (%d) %s", ex.code, ex.content) # re-raise the error so the constructor can catch it. raise 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)