1
mirror of https://github.com/home-assistant/core synced 2024-09-12 15:16:21 +02:00
ha-core/homeassistant/components/notify/xmpp.py

341 lines
13 KiB
Python
Raw Normal View History

2015-05-07 22:31:49 +02:00
"""
Jabber (XMPP) notification service.
2015-10-13 22:06:29 +02:00
For more details about this platform, please refer to the documentation at
2015-11-09 18:33:11 +01:00
https://home-assistant.io/components/notify.xmpp/
2015-05-07 22:31:49 +02:00
"""
from concurrent.futures import TimeoutError as FutTimeoutError
2015-05-07 22:31:49 +02:00
import logging
import mimetypes
import pathlib
import random
import string
2015-05-07 22:31:49 +02:00
import requests
2016-09-02 06:27:28 +02:00
import voluptuous as vol
2015-05-07 22:31:49 +02:00
from homeassistant.components.notify import (
2016-09-02 06:27:28 +02:00
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (
CONF_PASSWORD, CONF_RECIPIENT, CONF_RESOURCE, CONF_ROOM, CONF_SENDER)
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.template as template_helper
2015-05-07 22:31:49 +02:00
REQUIREMENTS = ['slixmpp==1.4.1']
2015-07-07 09:01:46 +02:00
2016-10-11 08:24:10 +02:00
_LOGGER = logging.getLogger(__name__)
2016-09-02 06:27:28 +02:00
ATTR_DATA = 'data'
ATTR_PATH = 'path'
ATTR_PATH_TEMPLATE = 'path_template'
ATTR_TIMEOUT = 'timeout'
ATTR_URL = 'url'
ATTR_URL_TEMPLATE = 'url_template'
ATTR_VERIFY = 'verify'
2016-09-02 06:27:28 +02:00
CONF_TLS = 'tls'
CONF_VERIFY = 'verify'
2016-09-02 06:27:28 +02:00
DEFAULT_CONTENT_TYPE = 'application/octet-stream'
DEFAULT_RESOURCE = 'home-assistant'
XEP_0363_TIMEOUT = 10
2016-09-02 06:27:28 +02:00
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENDER): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_RECIPIENT): cv.string,
vol.Optional(CONF_RESOURCE, default=DEFAULT_RESOURCE): cv.string,
vol.Optional(CONF_ROOM, default=''): cv.string,
2016-09-02 06:27:28 +02:00
vol.Optional(CONF_TLS, default=True): cv.boolean,
vol.Optional(CONF_VERIFY, default=True): cv.boolean,
2016-09-02 06:27:28 +02:00
})
async def async_get_service(hass, config, discovery_info=None):
2016-03-08 11:46:32 +01:00
"""Get the Jabber (XMPP) notification service."""
return XmppNotificationService(
config.get(CONF_SENDER), config.get(CONF_RESOURCE),
config.get(CONF_PASSWORD), config.get(CONF_RECIPIENT),
config.get(CONF_TLS), config.get(CONF_VERIFY),
config.get(CONF_ROOM), hass)
2015-05-07 22:31:49 +02:00
class XmppNotificationService(BaseNotificationService):
2016-03-08 11:46:32 +01:00
"""Implement the notification service for Jabber (XMPP)."""
2015-05-07 22:31:49 +02:00
def __init__(self, sender, resource, password,
recipient, tls, verify, room, hass):
2016-03-08 11:46:32 +01:00
"""Initialize the service."""
self._hass = hass
2015-05-07 22:31:49 +02:00
self._sender = sender
self._resource = resource
2015-05-07 22:31:49 +02:00
self._password = password
self._recipient = recipient
self._tls = tls
self._verify = verify
self._room = room
2015-05-07 22:31:49 +02:00
async def async_send_message(self, message="", **kwargs):
2016-03-08 11:46:32 +01:00
"""Send a message to a user."""
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
text = '{}: {}'.format(title, message) if title else message
data = kwargs.get(ATTR_DATA)
timeout = data.get(ATTR_TIMEOUT, XEP_0363_TIMEOUT) if data else None
2015-05-07 22:31:49 +02:00
await async_send_message(
'{}/{}'.format(self._sender, self._resource),
self._password, self._recipient, self._tls,
self._verify, self._room, self._hass, text,
timeout, data)
2015-05-07 22:31:49 +02:00
async def async_send_message(
sender, password, recipient, use_tls, verify_certificate, room, hass,
message, timeout=None, data=None):
2016-03-08 11:46:32 +01:00
"""Send a message over XMPP."""
import slixmpp
from slixmpp.exceptions import IqError, IqTimeout, XMPPError
from slixmpp.xmlstream.xmlstream import NotConnectedError
from slixmpp.plugins.xep_0363.http_upload import FileTooBig, \
FileUploadError, UploadServiceNotFound
2015-05-07 22:31:49 +02:00
class SendNotificationBot(slixmpp.ClientXMPP):
2016-03-08 11:46:32 +01:00
"""Service for sending Jabber (XMPP) messages."""
2015-05-07 22:31:49 +02:00
2015-11-09 07:15:34 +01:00
def __init__(self):
2016-03-08 11:46:32 +01:00
"""Initialize the Jabber Bot."""
super().__init__(sender, password)
2015-05-07 22:31:49 +02:00
self.loop = hass.loop
self.force_starttls = use_tls
2015-11-09 07:15:34 +01:00
self.use_ipv6 = False
self.add_event_handler(
'failed_auth', self.disconnect_on_login_fail)
2015-11-09 07:15:34 +01:00
self.add_event_handler('session_start', self.start)
if room:
self.register_plugin('xep_0045') # MUC
if not verify_certificate:
self.add_event_handler('ssl_invalid_cert',
self.discard_ssl_invalid_cert)
if data:
# Init XEPs for image sending
self.register_plugin('xep_0030') # OOB dep
self.register_plugin('xep_0066') # Out of Band Data
self.register_plugin('xep_0071') # XHTML IM
self.register_plugin('xep_0128') # Service Discovery
self.register_plugin('xep_0363') # HTTP upload
self.connect(force_starttls=self.force_starttls, use_ssl=False)
2015-05-07 22:31:49 +02:00
async def start(self, event):
2016-03-08 11:46:32 +01:00
"""Start the communication and sends the message."""
# Sending image and message independently from each other
if data:
await self.send_file(timeout=timeout)
if message:
self.send_text_message()
self.disconnect(wait=True)
async def send_file(self, timeout=None):
"""Send file via XMPP.
Send XMPP file message using OOB (XEP_0066) and
HTTP Upload (XEP_0363)
"""
if room:
self.plugin['xep_0045'].join_muc(room, sender, wait=True)
try:
# Uploading with XEP_0363
_LOGGER.debug("Timeout set to %ss", timeout)
url = await self.upload_file(timeout=timeout)
_LOGGER.info("Upload success")
if room:
_LOGGER.info("Sending file to %s", room)
message = self.Message(sto=room, stype='groupchat')
else:
_LOGGER.info("Sending file to %s", recipient)
message = self.Message(sto=recipient, stype='chat')
message['body'] = url
# pylint: disable=invalid-sequence-index
message['oob']['url'] = url
try:
message.send()
except (IqError, IqTimeout, XMPPError) as ex:
_LOGGER.error("Could not send image message %s", ex)
except (IqError, IqTimeout, XMPPError) as ex:
_LOGGER.error("Upload error, could not send message %s", ex)
except NotConnectedError as ex:
_LOGGER.error("Connection error %s", ex)
except FileTooBig as ex:
_LOGGER.error(
"File too big for server, could not upload file %s", ex)
except UploadServiceNotFound as ex:
_LOGGER.error("UploadServiceNotFound: "
" could not upload file %s", ex)
except FileUploadError as ex:
_LOGGER.error("FileUploadError, could not upload file %s", ex)
except requests.exceptions.SSLError as ex:
_LOGGER.error("Cannot establish SSL connection %s", ex)
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Cannot connect to server %s", ex)
except (FileNotFoundError,
PermissionError,
IsADirectoryError,
TimeoutError) as ex:
_LOGGER.error("Error reading file %s", ex)
except FutTimeoutError as ex:
_LOGGER.error("The server did not respond in time, %s", ex)
async def upload_file(self, timeout=None):
"""Upload file to Jabber server and return new URL.
upload a file with Jabber XEP_0363 from a remote URL or a local
file path and return a URL of that file.
"""
if data.get(ATTR_URL_TEMPLATE):
_LOGGER.debug(
"Got url template: %s", data[ATTR_URL_TEMPLATE])
templ = template_helper.Template(
data[ATTR_URL_TEMPLATE], hass)
get_url = template_helper.render_complex(templ, None)
url = await self.upload_file_from_url(
get_url, timeout=timeout)
elif data.get(ATTR_URL):
url = await self.upload_file_from_url(
data[ATTR_URL], timeout=timeout)
elif data.get(ATTR_PATH_TEMPLATE):
_LOGGER.debug(
"Got path template: %s", data[ATTR_PATH_TEMPLATE])
templ = template_helper.Template(
data[ATTR_PATH_TEMPLATE], hass)
get_path = template_helper.render_complex(templ, None)
url = await self.upload_file_from_path(
get_path, timeout=timeout)
elif data.get(ATTR_PATH):
url = await self.upload_file_from_path(
data[ATTR_PATH], timeout=timeout)
else:
url = None
if url is None:
_LOGGER.error("No path or URL found for file")
raise FileUploadError("Could not upload file")
return url
async def upload_file_from_url(self, url, timeout=None):
"""Upload a file from a URL. Returns a URL.
uploaded via XEP_0363 and HTTP and returns the resulting URL
"""
_LOGGER.info("Getting file from %s", url)
def get_url(url):
"""Return result for GET request to url."""
return requests.get(
url, verify=data.get(ATTR_VERIFY, True), timeout=timeout)
result = await hass.async_add_executor_job(get_url, url)
if result.status_code >= 400:
_LOGGER.error("Could not load file from %s", url)
return None
filesize = len(result.content)
# we need a file extension, the upload server needs a
# filename, if none is provided, through the path we guess
# the extension
# also setting random filename for privacy
if data.get(ATTR_PATH):
# using given path as base for new filename. Don't guess type
filename = self.get_random_filename(data.get(ATTR_PATH))
else:
extension = mimetypes.guess_extension(
result.headers['Content-Type']) or ".unknown"
_LOGGER.debug("Got %s extension", extension)
filename = self.get_random_filename(None, extension=extension)
_LOGGER.info("Uploading file from URL, %s", filename)
url = await self['xep_0363'].upload_file(
filename, size=filesize, input_file=result.content,
content_type=result.headers['Content-Type'], timeout=timeout)
return url
async def upload_file_from_path(self, path, timeout=None):
"""Upload a file from a local file path via XEP_0363."""
_LOGGER.info('Uploading file from path, %s ...', path)
if not hass.config.is_allowed_path(path):
raise PermissionError(
"Could not access file. Not in whitelist.")
with open(path, 'rb') as upfile:
_LOGGER.debug("Reading file %s", path)
input_file = upfile.read()
filesize = len(input_file)
_LOGGER.debug("Filesize is %s bytes", filesize)
content_type = mimetypes.guess_type(path)[0]
if content_type is None:
content_type = DEFAULT_CONTENT_TYPE
_LOGGER.debug("Content type is %s", content_type)
# set random filename for privacy
filename = self.get_random_filename(data.get(ATTR_PATH))
_LOGGER.debug("Uploading file with random filename %s", filename)
url = await self['xep_0363'].upload_file(
filename, size=filesize, input_file=input_file,
content_type=content_type, timeout=timeout)
return url
def send_text_message(self):
"""Send a text only message to a room or a recipient."""
try:
if room:
_LOGGER.debug("Joining room %s", room)
self.plugin['xep_0045'].join_muc(room, sender, wait=True)
self.send_message(
mto=room, mbody=message, mtype='groupchat')
else:
_LOGGER.debug("Sending message to %s", recipient)
self.send_message(
mto=recipient, mbody=message, mtype='chat')
except (IqError, IqTimeout, XMPPError) as ex:
_LOGGER.error("Could not send text message %s", ex)
except NotConnectedError as ex:
_LOGGER.error("Connection error %s", ex)
# pylint: disable=no-self-use
def get_random_filename(self, filename, extension=None):
"""Return a random filename, leaving the extension intact."""
if extension is None:
path = pathlib.Path(filename)
if path.suffix:
extension = ''.join(path.suffixes)
else:
extension = ".txt"
return ''.join(random.choice(string.ascii_letters)
for i in range(10)) + extension
2015-05-07 22:31:49 +02:00
def disconnect_on_login_fail(self, event):
"""Disconnect from the server if credentials are invalid."""
_LOGGER.warning("Login failed")
2015-11-09 07:15:34 +01:00
self.disconnect()
2015-05-07 22:31:49 +02:00
@staticmethod
def discard_ssl_invalid_cert(event):
"""Do nothing if ssl certificate is invalid."""
_LOGGER.info("Ignoring invalid SSL certificate as requested")
2015-11-09 07:21:02 +01:00
SendNotificationBot()