mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 06:47:09 +00:00
Telegram bot component (incl. webhook and polling platform) (#6913)
* first commit. * removed pointless string statement * manually removed # homeassistant.components.telegram_webhooks from requirements_all.txt * deleted obsolete file. * coveragerc abc
This commit is contained in:
parent
edf500e66b
commit
7cb8f49d62
@ -431,7 +431,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/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
|
131
homeassistant/components/telegram_bot/__init__.py
Normal file
131
homeassistant/components/telegram_bot/__init__.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""
|
||||
Component to receive telegram messages.
|
||||
|
||||
Either by polling or webhook.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_API_KEY
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import discovery, config_per_platform
|
||||
from homeassistant.setup import async_prepare_setup_platform
|
||||
|
||||
DOMAIN = 'telegram_bot'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_TELEGRAM_COMMAND = 'telegram_command'
|
||||
EVENT_TELEGRAM_TEXT = 'telegram_text'
|
||||
|
||||
ATTR_COMMAND = 'command'
|
||||
ATTR_USER_ID = 'user_id'
|
||||
ATTR_ARGS = 'args'
|
||||
ATTR_FROM_FIRST = 'from_first'
|
||||
ATTR_FROM_LAST = 'from_last'
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
CONF_ALLOWED_CHAT_IDS = 'allowed_chat_ids'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_ALLOWED_CHAT_IDS): vol.All(cv.ensure_list,
|
||||
[cv.positive_int])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the telegram bot component."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(p_type, p_config=None, discovery_info=None):
|
||||
"""Setup a telegram bot platform."""
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, p_type)
|
||||
|
||||
if platform is None:
|
||||
_LOGGER.error("Unknown notification service specified")
|
||||
return
|
||||
|
||||
_LOGGER.info("Setting up1 %s.%s", DOMAIN, p_type)
|
||||
|
||||
try:
|
||||
if hasattr(platform, 'async_setup_platform'):
|
||||
notify_service = yield from \
|
||||
platform.async_setup_platform(hass, p_config,
|
||||
discovery_info)
|
||||
elif hasattr(platform, 'setup_platform'):
|
||||
notify_service = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_platform, hass, p_config,
|
||||
discovery_info)
|
||||
else:
|
||||
raise HomeAssistantError("Invalid telegram bot platform.")
|
||||
|
||||
if notify_service is None:
|
||||
_LOGGER.error(
|
||||
"Failed to initialize telegram bot %s", p_type)
|
||||
return
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
return True
|
||||
|
||||
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
|
||||
in config_per_platform(config, DOMAIN)]
|
||||
|
||||
if setup_tasks:
|
||||
yield from asyncio.wait(setup_tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_platform_discovered(platform, info):
|
||||
"""Callback to load a platform."""
|
||||
yield from async_setup_platform(platform, discovery_info=info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BaseTelegramBotEntity:
|
||||
"""The base class for the telegram bot."""
|
||||
|
||||
def __init__(self, hass, allowed_chat_ids):
|
||||
"""Initialize the bot base class."""
|
||||
self.allowed_chat_ids = allowed_chat_ids
|
||||
self.hass = hass
|
||||
|
||||
def process_message(self, data):
|
||||
"""Check for basic message rules and fire an event if message is ok."""
|
||||
data = data.get('message')
|
||||
|
||||
if (not data
|
||||
or 'from' not in data
|
||||
or 'text' not in data
|
||||
or data['from'].get('id') not in self.allowed_chat_ids):
|
||||
# Message is not correct.
|
||||
_LOGGER.error("Incoming message does not have required data.")
|
||||
return False
|
||||
|
||||
event = EVENT_TELEGRAM_COMMAND
|
||||
event_data = {
|
||||
ATTR_USER_ID: data['from']['id'],
|
||||
ATTR_FROM_FIRST: data['from']['first_name'],
|
||||
ATTR_FROM_LAST: data['from']['last_name']}
|
||||
|
||||
if data['text'][0] == '/':
|
||||
pieces = data['text'].split(' ')
|
||||
event_data[ATTR_COMMAND] = pieces[0]
|
||||
event_data[ATTR_ARGS] = pieces[1:]
|
||||
|
||||
else:
|
||||
event_data[ATTR_TEXT] = data['text']
|
||||
event = EVENT_TELEGRAM_TEXT
|
||||
|
||||
self.hass.bus.async_fire(event, event_data)
|
||||
|
||||
return True
|
121
homeassistant/components/telegram_bot/polling.py
Normal file
121
homeassistant/components/telegram_bot/polling.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""Telegram bot polling implementation."""
|
||||
|
||||
import asyncio
|
||||
from asyncio.futures import CancelledError
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
from homeassistant.components.telegram_bot import CONF_ALLOWED_CHAT_IDS, \
|
||||
BaseTelegramBotEntity, PLATFORM_SCHEMA
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, \
|
||||
EVENT_HOMEASSISTANT_STOP, CONF_API_KEY
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-telegram-bot==5.3.0']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the polling platform."""
|
||||
import telegram
|
||||
bot = telegram.Bot(config[CONF_API_KEY])
|
||||
pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS])
|
||||
|
||||
@callback
|
||||
def _start_bot(_event):
|
||||
"""Start the bot."""
|
||||
pol.start_polling()
|
||||
|
||||
@callback
|
||||
def _stop_bot(_event):
|
||||
"""Stop the bot."""
|
||||
pol.stop_polling()
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
_start_bot
|
||||
)
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
_stop_bot
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class TelegramPoll(BaseTelegramBotEntity):
|
||||
"""asyncio telegram incoming message handler."""
|
||||
|
||||
def __init__(self, bot, hass, allowed_chat_ids):
|
||||
"""Initialize the polling instance."""
|
||||
BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids)
|
||||
self.update_id = 0
|
||||
self.websession = async_get_clientsession(hass)
|
||||
self.update_url = '{0}/getUpdates'.format(bot.base_url)
|
||||
self.polling_task = None # The actuall polling task.
|
||||
self.timeout = 15 # async post timeout
|
||||
# polling timeout should always be less than async post timeout.
|
||||
self.post_data = {'timeout': self.timeout - 5}
|
||||
|
||||
def start_polling(self):
|
||||
"""Start the polling task."""
|
||||
self.polling_task = self.hass.async_add_job(self.check_incoming())
|
||||
|
||||
def stop_polling(self):
|
||||
"""Stop the polling task."""
|
||||
self.polling_task.cancel()
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_updates(self, offset):
|
||||
"""Bypass the default long polling method to enable asyncio."""
|
||||
resp = None
|
||||
_json = [] # The actual value to be returned.
|
||||
|
||||
if offset:
|
||||
self.post_data['offset'] = offset
|
||||
try:
|
||||
with async_timeout.timeout(self.timeout, loop=self.hass.loop):
|
||||
resp = yield from self.websession.post(
|
||||
self.update_url, data=self.post_data,
|
||||
headers={'connection': 'keep-alive'}
|
||||
)
|
||||
if resp.status != 200:
|
||||
_LOGGER.error("Error %s on %s", resp.status, self.update_url)
|
||||
_json = yield from resp.json()
|
||||
except ValueError:
|
||||
_LOGGER.error("Error parsing Json message")
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
_LOGGER.error("Client connection error")
|
||||
finally:
|
||||
if resp is not None:
|
||||
yield from resp.release()
|
||||
|
||||
return _json
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle(self):
|
||||
"""" Receiving and processing incoming messages."""
|
||||
_updates = yield from self.get_updates(self.update_id)
|
||||
for update in _updates['result']:
|
||||
self.update_id = update['update_id'] + 1
|
||||
self.process_message(update)
|
||||
|
||||
@asyncio.coroutine
|
||||
def check_incoming(self):
|
||||
""""Loop which continuously checks for incoming telegram messages."""
|
||||
try:
|
||||
while True:
|
||||
# Each handle call sends a long polling post request
|
||||
# to the telegram server. If no incoming message it will return
|
||||
# an empty list. Calling self.handle() without any delay or
|
||||
# timeout will for this reason not really stress the processor.
|
||||
yield from self.handle()
|
||||
except CancelledError:
|
||||
_LOGGER.debug("Stopping telegram polling bot")
|
97
homeassistant/components/telegram_bot/webhooks.py
Normal file
97
homeassistant/components/telegram_bot/webhooks.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""
|
||||
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.components.telegram_bot import CONF_ALLOWED_CHAT_IDS, \
|
||||
BaseTelegramBotEntity, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.components.http.util import get_real_ip
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
REQUIREMENTS = ['python-telegram-bot==5.3.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TELEGRAM_HANDLER_URL = '/api/telegram_webhooks'
|
||||
|
||||
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')
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS):
|
||||
vol.All(cv.ensure_list, [ip_network])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the polling platform."""
|
||||
import telegram
|
||||
bot = telegram.Bot(config[CONF_API_KEY])
|
||||
|
||||
current_status = bot.getWebhookInfo()
|
||||
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)
|
||||
|
||||
hass.http.register_view(
|
||||
BotPushReceiver(
|
||||
hass,
|
||||
config[CONF_ALLOWED_CHAT_IDS],
|
||||
config[CONF_TRUSTED_NETWORKS]))
|
||||
|
||||
else:
|
||||
_LOGGER.error("set telegram webhook failed %s", handler_url)
|
||||
|
||||
|
||||
class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity):
|
||||
"""Handle pushes from telegram."""
|
||||
|
||||
requires_auth = False
|
||||
url = TELEGRAM_HANDLER_URL
|
||||
name = "telegram_webhooks"
|
||||
|
||||
def __init__(self, hass, allowed_chat_ids, trusted_networks):
|
||||
"""Initialize the class."""
|
||||
BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids)
|
||||
self.trusted_networks = trusted_networks
|
||||
|
||||
@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()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
|
||||
if not self.process_message(data):
|
||||
return self.json_message('Invalid message', HTTP_BAD_REQUEST)
|
||||
else:
|
||||
return self.json({})
|
@ -1,143 +0,0 @@
|
||||
"""
|
||||
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'
|
||||
EVENT_TELEGRAM_TEXT = 'telegram_text'
|
||||
|
||||
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_TEXT = 'text'
|
||||
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()
|
||||
except ValueError:
|
||||
_LOGGER.error("Received telegram data: %s", data)
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
|
||||
# check for basic message rules
|
||||
data = data.get('message')
|
||||
if not data or 'from' not in data or 'text' not in data:
|
||||
return self.json({})
|
||||
|
||||
if data['from'].get('id') not in self.users:
|
||||
_LOGGER.warning("User not allowed")
|
||||
return self.json_message('Invalid user', HTTP_BAD_REQUEST)
|
||||
|
||||
_LOGGER.debug("Received telegram data: %s", data)
|
||||
if not data['text']:
|
||||
_LOGGER.warning('no text')
|
||||
return self.json({})
|
||||
|
||||
if data['text'][:1] == '/':
|
||||
# telegram command "/blabla arg1 arg2 ..."
|
||||
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'],
|
||||
})
|
||||
|
||||
# telegram text "bla bla"
|
||||
request.app['hass'].bus.async_fire(EVENT_TELEGRAM_TEXT, {
|
||||
ATTR_TEXT: data['text'],
|
||||
ATTR_USER_ID: data['from']['id'],
|
||||
})
|
||||
|
||||
return self.json({})
|
@ -632,8 +632,9 @@ python-pushover==0.2
|
||||
# homeassistant.components.sensor.synologydsm
|
||||
python-synology==0.1.0
|
||||
|
||||
# homeassistant.components.telegram_webhooks
|
||||
# homeassistant.components.notify.telegram
|
||||
# homeassistant.components.telegram_bot.polling
|
||||
# homeassistant.components.telegram_bot.webhooks
|
||||
python-telegram-bot==5.3.0
|
||||
|
||||
# homeassistant.components.sensor.twitch
|
||||
|
Loading…
x
Reference in New Issue
Block a user