diff --git a/.coveragerc b/.coveragerc index 9de3b6f8cf6..14f9a8b29f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -386,6 +386,7 @@ omit = homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py + homeassistant/components/telegram_webhooks.py homeassistant/components/thingspeak.py homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/picotts.py diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index fd901016fc5..0020c67eefb 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['python-telegram-bot==5.3.0'] ATTR_PHOTO = 'photo' +ATTR_KEYBOARD = 'keyboard' ATTR_DOCUMENT = 'document' ATTR_CAPTION = 'caption' ATTR_URL = 'url' @@ -107,6 +108,10 @@ class TelegramNotificationService(BaseNotificationService): return self.send_location(data.get(ATTR_LOCATION)) elif data is not None and ATTR_DOCUMENT in data: return self.send_document(data.get(ATTR_DOCUMENT)) + elif data is not None and ATTR_KEYBOARD in data: + keys = data.get(ATTR_KEYBOARD) + keys = keys if isinstance(keys, list) else [keys] + return self.send_keyboard(message, keys) if title: text = '{} {}'.format(title, message) @@ -122,7 +127,18 @@ class TelegramNotificationService(BaseNotificationService): parse_mode=parse_mode) except telegram.error.TelegramError: _LOGGER.exception("Error sending message") - return + + def send_keyboard(self, message, keys): + """Display keyboard.""" + import telegram + + keyboard = telegram.ReplyKeyboardMarkup([ + [key.strip() for key in row.split(",")] for row in keys]) + try: + self.bot.sendMessage(chat_id=self._chat_id, text=message, + reply_markup=keyboard) + except telegram.error.TelegramError: + _LOGGER.exception("Error sending message") def send_photo(self, data): """Send a photo.""" @@ -141,7 +157,6 @@ class TelegramNotificationService(BaseNotificationService): photo=photo, caption=caption) except telegram.error.TelegramError: _LOGGER.exception("Error sending photo") - return def send_document(self, data): """Send a document.""" @@ -160,7 +175,6 @@ class TelegramNotificationService(BaseNotificationService): document=document, caption=caption) except telegram.error.TelegramError: _LOGGER.exception("Error sending document") - return def send_location(self, gps): """Send a location.""" @@ -174,4 +188,3 @@ class TelegramNotificationService(BaseNotificationService): latitude=latitude, longitude=longitude) except telegram.error.TelegramError: _LOGGER.exception("Error sending location") - return diff --git a/homeassistant/components/telegram_webhooks.py b/homeassistant/components/telegram_webhooks.py new file mode 100644 index 00000000000..541b81a9567 --- /dev/null +++ b/homeassistant/components/telegram_webhooks.py @@ -0,0 +1,130 @@ +""" +Allows utilizing telegram webhooks. + +See https://core.telegram.org/bots/webhooks for details + about webhooks. + +""" +import asyncio +import logging +from ipaddress import ip_network + +import voluptuous as vol + +from homeassistant.const import ( + HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) +import homeassistant.helpers.config_validation as cv +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_API_KEY +from homeassistant.components.http.util import get_real_ip + +DOMAIN = 'telegram_webhooks' +DEPENDENCIES = ['http'] +REQUIREMENTS = ['python-telegram-bot==5.3.0'] + +_LOGGER = logging.getLogger(__name__) + +EVENT_TELEGRAM_COMMAND = 'telegram.command' + +TELEGRAM_HANDLER_URL = '/api/telegram_webhooks' + +CONF_USER_ID = 'user_id' +CONF_TRUSTED_NETWORKS = 'trusted_networks' +DEFAULT_TRUSTED_NETWORKS = [ + ip_network('149.154.167.197/32'), + ip_network('149.154.167.198/31'), + ip_network('149.154.167.200/29'), + ip_network('149.154.167.208/28'), + ip_network('149.154.167.224/29'), + ip_network('149.154.167.232/31') +] + +ATTR_COMMAND = 'command' +ATTR_USER_ID = 'user_id' +ATTR_ARGS = 'args' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS): + vol.All(cv.ensure_list, [ip_network]), + vol.Required(CONF_USER_ID): {cv.string: cv.positive_int}, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the telegram_webhooks component. + + register webhook if API_KEY is specified + register /api/telegram_webhooks as web service for telegram bot + """ + import telegram + + conf = config[DOMAIN] + + if CONF_API_KEY in conf: + bot = telegram.Bot(conf[CONF_API_KEY]) + current_status = bot.getWebhookInfo() + _LOGGER.debug("telegram webhook status: %s", current_status) + handler_url = "{0}{1}".format(hass.config.api.base_url, + TELEGRAM_HANDLER_URL) + if current_status and current_status['url'] != handler_url: + if bot.setWebhook(handler_url): + _LOGGER.info("set new telegram webhook %s", handler_url) + else: + _LOGGER.error("set telegram webhook failed %s", handler_url) + + hass.http.register_view(BotPushReceiver(conf[CONF_USER_ID], + conf[CONF_TRUSTED_NETWORKS])) + return True + + +class BotPushReceiver(HomeAssistantView): + """Handle pushes from telegram.""" + + requires_auth = False + url = TELEGRAM_HANDLER_URL + name = "telegram_webhooks" + + def __init__(self, user_id_array, trusted_networks): + """Initialize users allowed to send messages to bot.""" + self.trusted_networks = trusted_networks + self.users = {user_id: dev_id for dev_id, user_id in + user_id_array.items()} + _LOGGER.debug("users allowed: %s", self.users) + + @asyncio.coroutine + def post(self, request): + """Accept the POST from telegram.""" + real_ip = get_real_ip(request) + if not any(real_ip in net for net in self.trusted_networks): + _LOGGER.warning("Access denied from %s", real_ip) + return self.json_message('Access denied', HTTP_UNAUTHORIZED) + + try: + data = yield from request.json() + data = data['message'] + + if data['text'][0] != '/': + _LOGGER.warning('no command') + return self.json({}) + except (ValueError, IndexError): + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + try: + if data['from']['id'] not in self.users: + raise ValueError() + except (ValueError, IndexError): + _LOGGER.warning("User not allowed") + return self.json_message('Invalid user', HTTP_BAD_REQUEST) + + _LOGGER.debug("Received telegram data: %s", data) + + pieces = data['text'].split(' ') + request.app['hass'].bus.async_fire(EVENT_TELEGRAM_COMMAND, { + ATTR_COMMAND: pieces[0], + ATTR_ARGS: " ".join(pieces[1:]), + ATTR_USER_ID: data['from']['id'], + }) + return self.json({}) diff --git a/requirements_all.txt b/requirements_all.txt index e67826fa1f6..7a8c30b0635 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,6 +546,7 @@ python-pushover==0.2 # homeassistant.components.sensor.synologydsm python-synology==0.1.0 +# homeassistant.components.telegram_webhooks # homeassistant.components.notify.telegram python-telegram-bot==5.3.0