mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 10:47:10 +00:00
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
This commit is contained in:
parent
63ff173305
commit
fb184b4b6f
@ -5,11 +5,15 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/notify.slack/
|
https://home-assistant.io/components/notify.slack/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPDigestAuth
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
|
ATTR_TARGET, ATTR_TITLE, ATTR_DATA,
|
||||||
|
PLATFORM_SCHEMA, BaseNotificationService)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -19,6 +23,19 @@ REQUIREMENTS = ['slacker==0.9.50']
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_CHANNEL = 'default_channel'
|
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({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
@ -38,7 +55,8 @@ def get_service(hass, config, discovery_info=None):
|
|||||||
config[CONF_CHANNEL],
|
config[CONF_CHANNEL],
|
||||||
config[CONF_API_KEY],
|
config[CONF_API_KEY],
|
||||||
config.get(CONF_USERNAME, None),
|
config.get(CONF_USERNAME, None),
|
||||||
config.get(CONF_ICON, None))
|
config.get(CONF_ICON, None),
|
||||||
|
hass.config.is_allowed_path)
|
||||||
|
|
||||||
except slacker.Error:
|
except slacker.Error:
|
||||||
_LOGGER.exception("Authentication failed")
|
_LOGGER.exception("Authentication failed")
|
||||||
@ -48,7 +66,9 @@ def get_service(hass, config, discovery_info=None):
|
|||||||
class SlackNotificationService(BaseNotificationService):
|
class SlackNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for Slack."""
|
"""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."""
|
"""Initialize the service."""
|
||||||
from slacker import Slacker
|
from slacker import Slacker
|
||||||
self._default_channel = default_channel
|
self._default_channel = default_channel
|
||||||
@ -60,6 +80,7 @@ class SlackNotificationService(BaseNotificationService):
|
|||||||
else:
|
else:
|
||||||
self._as_user = True
|
self._as_user = True
|
||||||
|
|
||||||
|
self.is_allowed_path = is_allowed_path
|
||||||
self.slack = Slacker(self._api_token)
|
self.slack = Slacker(self._api_token)
|
||||||
self.slack.auth.test()
|
self.slack.auth.test()
|
||||||
|
|
||||||
@ -72,14 +93,79 @@ class SlackNotificationService(BaseNotificationService):
|
|||||||
else:
|
else:
|
||||||
targets = kwargs.get(ATTR_TARGET)
|
targets = kwargs.get(ATTR_TARGET)
|
||||||
|
|
||||||
data = kwargs.get('data')
|
data = kwargs.get(ATTR_DATA)
|
||||||
attachments = data.get('attachments') if data else None
|
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:
|
for target in targets:
|
||||||
try:
|
try:
|
||||||
self.slack.chat.post_message(
|
if file is not None:
|
||||||
target, message, as_user=self._as_user,
|
# Load from file or url
|
||||||
username=self._username, icon_emoji=self._icon,
|
file_as_bytes = self.load_file(
|
||||||
attachments=attachments, link_names=True)
|
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:
|
except slacker.Error as err:
|
||||||
_LOGGER.error("Could not send notification. Error: %s", 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user