core/homeassistant/components/notify/matrix.py

196 lines
6.3 KiB
Python

"""
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)