From fb184b4b6fc66deea724eeeb9853d4c3f9ee5e93 Mon Sep 17 00:00:00 2001 From: Simao Date: Fri, 7 Jul 2017 08:14:24 +0200 Subject: [PATCH] Added support for upload of remote or local files to slack (#8278) * Added support for upload of remote or local files to slack * Checking local file with hass.config.is_allowed_path prior to posting it --- homeassistant/components/notify/slack.py | 104 +++++++++++++++++++++-- 1 file changed, 95 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index fa7332326da..a6257970566 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -5,11 +5,15 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.slack/ """ import logging +import requests +from requests.auth import HTTPDigestAuth +from requests.auth import HTTPBasicAuth import voluptuous as vol from homeassistant.components.notify import ( - ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) + ATTR_TARGET, ATTR_TITLE, ATTR_DATA, + PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import ( CONF_API_KEY, CONF_USERNAME, CONF_ICON) import homeassistant.helpers.config_validation as cv @@ -19,6 +23,19 @@ REQUIREMENTS = ['slacker==0.9.50'] _LOGGER = logging.getLogger(__name__) CONF_CHANNEL = 'default_channel' +CONF_TIMEOUT = 15 + +# Top level attributes in 'data' +ATTR_ATTACHMENTS = 'attachments' +ATTR_FILE = 'file' +# Attributes contained in file +ATTR_FILE_URL = 'url' +ATTR_FILE_PATH = 'path' +ATTR_FILE_USERNAME = 'username' +ATTR_FILE_PASSWORD = 'password' +ATTR_FILE_AUTH = 'auth' +# Any other value or absense of 'auth' lead to basic authentication being used +ATTR_FILE_AUTH_DIGEST = 'digest' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -38,7 +55,8 @@ def get_service(hass, config, discovery_info=None): config[CONF_CHANNEL], config[CONF_API_KEY], config.get(CONF_USERNAME, None), - config.get(CONF_ICON, None)) + config.get(CONF_ICON, None), + hass.config.is_allowed_path) except slacker.Error: _LOGGER.exception("Authentication failed") @@ -48,7 +66,9 @@ def get_service(hass, config, discovery_info=None): class SlackNotificationService(BaseNotificationService): """Implement the notification service for Slack.""" - def __init__(self, default_channel, api_token, username, icon): + def __init__(self, default_channel, + api_token, username, + icon, is_allowed_path): """Initialize the service.""" from slacker import Slacker self._default_channel = default_channel @@ -60,6 +80,7 @@ class SlackNotificationService(BaseNotificationService): else: self._as_user = True + self.is_allowed_path = is_allowed_path self.slack = Slacker(self._api_token) self.slack.auth.test() @@ -72,14 +93,79 @@ class SlackNotificationService(BaseNotificationService): else: targets = kwargs.get(ATTR_TARGET) - data = kwargs.get('data') - attachments = data.get('attachments') if data else None + data = kwargs.get(ATTR_DATA) + attachments = data.get(ATTR_ATTACHMENTS) if data else None + file = data.get(ATTR_FILE) if data else None + title = kwargs.get(ATTR_TITLE) for target in targets: try: - self.slack.chat.post_message( - target, message, as_user=self._as_user, - username=self._username, icon_emoji=self._icon, - attachments=attachments, link_names=True) + if file is not None: + # Load from file or url + file_as_bytes = self.load_file( + url=file.get(ATTR_FILE_URL), + local_path=file.get(ATTR_FILE_PATH), + username=file.get(ATTR_FILE_USERNAME), + password=file.get(ATTR_FILE_PASSWORD), + auth=file.get(ATTR_FILE_AUTH)) + # Choose filename + if file.get(ATTR_FILE_URL): + filename = file.get(ATTR_FILE_URL) + else: + filename = file.get(ATTR_FILE_PATH) + # Prepare structure for slack API + data = { + 'content': None, + 'filetype': None, + 'filename': filename, + # if optional title is none use the filename + 'title': title if title else filename, + 'initial_comment': message, + 'channels': target + } + # Post to slack + self.slack.files.post('files.upload', + data=data, + files={'file': file_as_bytes}) + else: + self.slack.chat.post_message( + target, message, as_user=self._as_user, + username=self._username, icon_emoji=self._icon, + attachments=attachments, link_names=True) except slacker.Error as err: _LOGGER.error("Could not send notification. Error: %s", err) + + def load_file(self, url=None, local_path=None, + username=None, password=None, auth=None): + """Load image/document/etc from a local path or url.""" + try: + if url is not None: + # check whether authentication parameters are provided + if username is not None and password is not None: + # Use digest or basic authentication + if ATTR_FILE_AUTH_DIGEST == auth: + auth_ = HTTPDigestAuth(username, password) + else: + auth_ = HTTPBasicAuth(username, password) + # load file from url with authentication + req = requests.get(url, auth=auth_, timeout=CONF_TIMEOUT) + else: + # load file from url without authentication + req = requests.get(url, timeout=CONF_TIMEOUT) + return req.content + + elif local_path is not None: + # Check whether path is whitelisted in configuration.yaml + if self.is_allowed_path(local_path): + # load file from local path on server + return open(local_path, "rb") + _LOGGER.warning("'%s' is not secure to load data from!", + local_path) + else: + # neither url nor path provided + _LOGGER.warning("Neither url nor local path found in params!") + + except OSError as error: + _LOGGER.error("Can't load from url or local path: %s", error) + + return None