Refactor matrix notify service (#7122)
* Refactor matrix notify service. This refactor aims to close #6118 by making the save / restore of the authentication tokens much more resilient to failure. It also refactors the module so that all the functionality is part of the class and that a login failure causes the service to fail on setup rather than at message send time. * Make the linter overlords happy * Improve logger levels and messages * small style change * Fix indentation issue
This commit is contained in:
parent
4f5ec3e360
commit
7960206e2e
1 changed files with 135 additions and 95 deletions
|
@ -7,12 +7,13 @@ https://home-assistant.io/components/notify.matrix/
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA,
|
||||||
ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
|
BaseNotificationService)
|
||||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL
|
||||||
|
|
||||||
REQUIREMENTS = ['matrix-client==0.0.6']
|
REQUIREMENTS = ['matrix-client==0.0.6']
|
||||||
|
@ -20,7 +21,6 @@ REQUIREMENTS = ['matrix-client==0.0.6']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SESSION_FILE = 'matrix.conf'
|
SESSION_FILE = 'matrix.conf'
|
||||||
AUTH_TOKENS = dict()
|
|
||||||
|
|
||||||
CONF_HOMESERVER = 'homeserver'
|
CONF_HOMESERVER = 'homeserver'
|
||||||
CONF_DEFAULT_ROOM = 'default_room'
|
CONF_DEFAULT_ROOM = 'default_room'
|
||||||
|
@ -36,120 +36,160 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
|
||||||
def get_service(hass, config, discovery_info=None):
|
def get_service(hass, config, discovery_info=None):
|
||||||
"""Get the Matrix notification service."""
|
"""Get the Matrix notification service."""
|
||||||
if not AUTH_TOKENS:
|
from matrix_client.client import MatrixRequestError
|
||||||
load_token(hass.config.path(SESSION_FILE))
|
|
||||||
|
|
||||||
|
try:
|
||||||
return MatrixNotificationService(
|
return MatrixNotificationService(
|
||||||
|
os.path.join(hass.config.path(), SESSION_FILE),
|
||||||
config.get(CONF_HOMESERVER),
|
config.get(CONF_HOMESERVER),
|
||||||
config.get(CONF_DEFAULT_ROOM),
|
config.get(CONF_DEFAULT_ROOM),
|
||||||
config.get(CONF_VERIFY_SSL),
|
config.get(CONF_VERIFY_SSL),
|
||||||
config.get(CONF_USERNAME),
|
config.get(CONF_USERNAME),
|
||||||
config.get(CONF_PASSWORD)
|
config.get(CONF_PASSWORD))
|
||||||
)
|
|
||||||
|
except MatrixRequestError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class MatrixNotificationService(BaseNotificationService):
|
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):
|
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.homeserver = homeserver
|
||||||
self.default_room = default_room
|
self.default_room = default_room
|
||||||
self.verify_tls = verify_ssl
|
self.verify_tls = verify_ssl
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
def send_message(self, message, **kwargs):
|
self.mx_id = "{user}@{homeserver}".format(
|
||||||
"""Wrapper function pass default parameters to actual send_message."""
|
user=username, homeserver=urlparse(homeserver).netloc)
|
||||||
send_message(
|
|
||||||
message,
|
|
||||||
self.homeserver,
|
|
||||||
kwargs.get(ATTR_TARGET) or [self.default_room],
|
|
||||||
self.verify_tls,
|
|
||||||
self.username,
|
|
||||||
self.password
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Login, this will raise a MatrixRequestError if login is unsuccessful
|
||||||
|
self.client = self.login()
|
||||||
|
|
||||||
def load_token(session_file):
|
def get_auth_tokens(self):
|
||||||
"""Load authentication tokens from persistent storage, if exists."""
|
"""
|
||||||
if not os.path.exists(session_file):
|
Read sorted authentication tokens from disk.
|
||||||
return
|
|
||||||
|
|
||||||
with open(session_file) as handle:
|
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)
|
data = json.load(handle)
|
||||||
|
|
||||||
|
auth_tokens = {}
|
||||||
for mx_id, token in data.items():
|
for mx_id, token in data.items():
|
||||||
AUTH_TOKENS[mx_id] = token
|
auth_tokens[mx_id] = token
|
||||||
|
|
||||||
|
return auth_tokens
|
||||||
|
|
||||||
def store_token(mx_id, token):
|
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."""
|
"""Store authentication token to session and persistent storage."""
|
||||||
AUTH_TOKENS[mx_id] = token
|
self.auth_tokens[self.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:
|
try:
|
||||||
return MatrixClient(
|
with open(self.session_filepath, 'w') as handle:
|
||||||
base_url=homeserver,
|
handle.write(json.dumps(self.auth_tokens))
|
||||||
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)
|
|
||||||
|
|
||||||
def login_by_password():
|
# Not saving the tokens to disk should not stop the client, we can just
|
||||||
"""Login using password authentication."""
|
# 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:
|
try:
|
||||||
_client = MatrixClient(
|
client = self.login_by_token()
|
||||||
base_url=homeserver, valid_cert_check=verify_tls)
|
_LOGGER.debug("Logged in using stored token.")
|
||||||
_client.login_with_password(username, password)
|
|
||||||
store_token(mx_id, _client.token)
|
|
||||||
return _client
|
|
||||||
except MatrixRequestError as ex:
|
except MatrixRequestError as ex:
|
||||||
_LOGGER.error("login_by_password: (%d) %s", ex.code, ex.content)
|
_LOGGER.warning(
|
||||||
|
"Login by token failed, falling back to password. "
|
||||||
|
"login_by_token raised: (%d) %s",
|
||||||
|
ex.code, ex.content)
|
||||||
|
|
||||||
# This is as close as we can get to the mx_id, since there is no
|
# If we still don't have a client try password.
|
||||||
# homeserver discovery protocol we have to fall back to the homeserver url
|
if not client:
|
||||||
# instead of the actual domain it serves.
|
try:
|
||||||
mx_id = "{user}@{homeserver}".format(user=username, homeserver=homeserver)
|
client = self.login_by_password()
|
||||||
|
_LOGGER.debug("Logged in using password.")
|
||||||
|
|
||||||
if mx_id in AUTH_TOKENS:
|
except MatrixRequestError as ex:
|
||||||
client = login_by_token()
|
|
||||||
if not client:
|
|
||||||
client = login_by_password()
|
|
||||||
if not client:
|
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Login failed, both token and username/password invalid")
|
"Login failed, both token and username/password invalid "
|
||||||
return
|
"login_by_password raised: (%d) %s",
|
||||||
else:
|
ex.code, ex.content)
|
||||||
client = login_by_password()
|
|
||||||
if not client:
|
|
||||||
_LOGGER.error("Login failed, username/password invalid")
|
|
||||||
return
|
|
||||||
|
|
||||||
rooms = client.get_rooms()
|
# 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:
|
for target_room in target_rooms:
|
||||||
try:
|
try:
|
||||||
if target_room in rooms:
|
if target_room in rooms:
|
||||||
room = rooms[target_room]
|
room = rooms[target_room]
|
||||||
else:
|
else:
|
||||||
room = client.join_room(target_room)
|
room = self.client.join_room(target_room)
|
||||||
|
|
||||||
_LOGGER.debug(room.send_text(message))
|
_LOGGER.debug(room.send_text(message))
|
||||||
|
|
||||||
except MatrixRequestError as ex:
|
except MatrixRequestError as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Unable to deliver message to room '%s': (%d): %s",
|
"Unable to deliver message to room '%s': (%d): %s",
|
||||||
target_room, ex.code, ex.content
|
target_room, ex.code, ex.content)
|
||||||
)
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue