mirror of
https://github.com/skalavala/mysmarthome.git
synced 2025-08-21 04:33:22 +00:00
cleaned up, and updated to latest version.
This commit is contained in:
988
custom_components/telegram_bot/__init__.py
Normal file
988
custom_components/telegram_bot/__init__.py
Normal file
@@ -0,0 +1,988 @@
|
||||
"""Support to send and receive Telegram messages."""
|
||||
from functools import partial
|
||||
import importlib
|
||||
import io
|
||||
from ipaddress import ip_network
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
from telegram import (
|
||||
Bot,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
ReplyKeyboardRemove,
|
||||
)
|
||||
from telegram.error import TelegramError
|
||||
from telegram.parsemode import ParseMode
|
||||
from telegram.utils.request import Request
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_COMMAND,
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_API_KEY,
|
||||
CONF_PLATFORM,
|
||||
CONF_URL,
|
||||
HTTP_BEARER_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DATA = "data"
|
||||
ATTR_MESSAGE = "message"
|
||||
ATTR_TITLE = "title"
|
||||
|
||||
ATTR_ARGS = "args"
|
||||
ATTR_AUTHENTICATION = "authentication"
|
||||
ATTR_CALLBACK_QUERY = "callback_query"
|
||||
ATTR_CALLBACK_QUERY_ID = "callback_query_id"
|
||||
ATTR_CAPTION = "caption"
|
||||
ATTR_CHAT_ID = "chat_id"
|
||||
ATTR_CHAT_INSTANCE = "chat_instance"
|
||||
ATTR_DISABLE_NOTIF = "disable_notification"
|
||||
ATTR_DISABLE_WEB_PREV = "disable_web_page_preview"
|
||||
ATTR_EDITED_MSG = "edited_message"
|
||||
ATTR_FILE = "file"
|
||||
ATTR_FROM_FIRST = "from_first"
|
||||
ATTR_FROM_LAST = "from_last"
|
||||
ATTR_KEYBOARD = "keyboard"
|
||||
ATTR_KEYBOARD_INLINE = "inline_keyboard"
|
||||
ATTR_MESSAGEID = "message_id"
|
||||
ATTR_MSG = "message"
|
||||
ATTR_MSGID = "id"
|
||||
ATTR_PARSER = "parse_mode"
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_REPLY_TO_MSGID = "reply_to_message_id"
|
||||
ATTR_REPLYMARKUP = "reply_markup"
|
||||
ATTR_SHOW_ALERT = "show_alert"
|
||||
ATTR_STICKER_ID = "sticker_id"
|
||||
ATTR_TARGET = "target"
|
||||
ATTR_TEXT = "text"
|
||||
ATTR_URL = "url"
|
||||
ATTR_USER_ID = "user_id"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_VERIFY_SSL = "verify_ssl"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
ATTR_MESSAGE_TAG = "message_tag"
|
||||
ATTR_CHANNEL_POST = "channel_post"
|
||||
|
||||
CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids"
|
||||
CONF_PROXY_URL = "proxy_url"
|
||||
CONF_PROXY_PARAMS = "proxy_params"
|
||||
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
||||
|
||||
DOMAIN = "telegram_bot"
|
||||
|
||||
SERVICE_SEND_MESSAGE = "send_message"
|
||||
SERVICE_SEND_PHOTO = "send_photo"
|
||||
SERVICE_SEND_STICKER = "send_sticker"
|
||||
SERVICE_SEND_ANIMATION = "send_animation"
|
||||
SERVICE_SEND_VIDEO = "send_video"
|
||||
SERVICE_SEND_VOICE = "send_voice"
|
||||
SERVICE_SEND_DOCUMENT = "send_document"
|
||||
SERVICE_SEND_LOCATION = "send_location"
|
||||
SERVICE_EDIT_MESSAGE = "edit_message"
|
||||
SERVICE_EDIT_CAPTION = "edit_caption"
|
||||
SERVICE_EDIT_REPLYMARKUP = "edit_replymarkup"
|
||||
SERVICE_ANSWER_CALLBACK_QUERY = "answer_callback_query"
|
||||
SERVICE_DELETE_MESSAGE = "delete_message"
|
||||
SERVICE_LEAVE_CHAT = "leave_chat"
|
||||
|
||||
EVENT_TELEGRAM_CALLBACK = "telegram_callback"
|
||||
EVENT_TELEGRAM_COMMAND = "telegram_command"
|
||||
EVENT_TELEGRAM_TEXT = "telegram_text"
|
||||
EVENT_TELEGRAM_SENT = "telegram_sent"
|
||||
|
||||
PARSER_HTML = "html"
|
||||
PARSER_MD = "markdown"
|
||||
|
||||
DEFAULT_TRUSTED_NETWORKS = [ip_network("149.154.160.0/20"), ip_network("91.108.4.0/22")]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): vol.In(
|
||||
("broadcast", "polling", "webhooks")
|
||||
),
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_ALLOWED_CHAT_IDS): vol.All(
|
||||
cv.ensure_list, [vol.Coerce(int)]
|
||||
),
|
||||
vol.Optional(ATTR_PARSER, default=PARSER_MD): cv.string,
|
||||
vol.Optional(CONF_PROXY_URL): cv.string,
|
||||
vol.Optional(CONF_PROXY_PARAMS): dict,
|
||||
# webhooks
|
||||
vol.Optional(CONF_URL): cv.url,
|
||||
vol.Optional(
|
||||
CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS
|
||||
): vol.All(cv.ensure_list, [ip_network]),
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
BASE_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(ATTR_PARSER): cv.string,
|
||||
vol.Optional(ATTR_DISABLE_NOTIF): cv.boolean,
|
||||
vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean,
|
||||
vol.Optional(ATTR_KEYBOARD): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list,
|
||||
vol.Optional(ATTR_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(ATTR_MESSAGE_TAG): cv.string,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_MESSAGE = BASE_SERVICE_SCHEMA.extend(
|
||||
{vol.Required(ATTR_MESSAGE): cv.template, vol.Optional(ATTR_TITLE): cv.template}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_FILE = BASE_SERVICE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(ATTR_URL): cv.template,
|
||||
vol.Optional(ATTR_FILE): cv.template,
|
||||
vol.Optional(ATTR_CAPTION): cv.template,
|
||||
vol.Optional(ATTR_USERNAME): cv.string,
|
||||
vol.Optional(ATTR_PASSWORD): cv.string,
|
||||
vol.Optional(ATTR_AUTHENTICATION): cv.string,
|
||||
vol.Optional(ATTR_VERIFY_SSL): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_STICKER = SERVICE_SCHEMA_SEND_FILE.extend(
|
||||
{vol.Optional(ATTR_STICKER_ID): cv.string}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SEND_LOCATION = BASE_SERVICE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_LONGITUDE): cv.template,
|
||||
vol.Required(ATTR_LATITUDE): cv.template,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_EDIT_MESSAGE = SERVICE_SCHEMA_SEND_MESSAGE.extend(
|
||||
{
|
||||
vol.Required(ATTR_MESSAGEID): vol.Any(
|
||||
cv.positive_int, vol.All(cv.string, "last")
|
||||
),
|
||||
vol.Required(ATTR_CHAT_ID): vol.Coerce(int),
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_EDIT_CAPTION = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MESSAGEID): vol.Any(
|
||||
cv.positive_int, vol.All(cv.string, "last")
|
||||
),
|
||||
vol.Required(ATTR_CHAT_ID): vol.Coerce(int),
|
||||
vol.Required(ATTR_CAPTION): cv.template,
|
||||
vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_EDIT_REPLYMARKUP = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MESSAGEID): vol.Any(
|
||||
cv.positive_int, vol.All(cv.string, "last")
|
||||
),
|
||||
vol.Required(ATTR_CHAT_ID): vol.Coerce(int),
|
||||
vol.Required(ATTR_KEYBOARD_INLINE): cv.ensure_list,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_ANSWER_CALLBACK_QUERY = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MESSAGE): cv.template,
|
||||
vol.Required(ATTR_CALLBACK_QUERY_ID): vol.Coerce(int),
|
||||
vol.Optional(ATTR_SHOW_ALERT): cv.boolean,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_DELETE_MESSAGE = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CHAT_ID): vol.Coerce(int),
|
||||
vol.Required(ATTR_MESSAGEID): vol.Any(
|
||||
cv.positive_int, vol.All(cv.string, "last")
|
||||
),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_LEAVE_CHAT = vol.Schema({vol.Required(ATTR_CHAT_ID): vol.Coerce(int)})
|
||||
|
||||
SERVICE_MAP = {
|
||||
SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE,
|
||||
SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE,
|
||||
SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_STICKER,
|
||||
SERVICE_SEND_ANIMATION: SERVICE_SCHEMA_SEND_FILE,
|
||||
SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE,
|
||||
SERVICE_SEND_VOICE: SERVICE_SCHEMA_SEND_FILE,
|
||||
SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE,
|
||||
SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION,
|
||||
SERVICE_EDIT_MESSAGE: SERVICE_SCHEMA_EDIT_MESSAGE,
|
||||
SERVICE_EDIT_CAPTION: SERVICE_SCHEMA_EDIT_CAPTION,
|
||||
SERVICE_EDIT_REPLYMARKUP: SERVICE_SCHEMA_EDIT_REPLYMARKUP,
|
||||
SERVICE_ANSWER_CALLBACK_QUERY: SERVICE_SCHEMA_ANSWER_CALLBACK_QUERY,
|
||||
SERVICE_DELETE_MESSAGE: SERVICE_SCHEMA_DELETE_MESSAGE,
|
||||
SERVICE_LEAVE_CHAT: SERVICE_SCHEMA_LEAVE_CHAT,
|
||||
}
|
||||
|
||||
|
||||
def load_data(
|
||||
hass,
|
||||
url=None,
|
||||
filepath=None,
|
||||
username=None,
|
||||
password=None,
|
||||
authentication=None,
|
||||
num_retries=5,
|
||||
verify_ssl=None,
|
||||
):
|
||||
"""Load data into ByteIO/File container from a source."""
|
||||
try:
|
||||
if url is not None:
|
||||
# Load data from URL
|
||||
params = {"timeout": 15}
|
||||
if authentication == HTTP_BEARER_AUTHENTICATION and password is not None:
|
||||
params["headers"] = {"Authorization": f"Bearer {password}"}
|
||||
elif username is not None and password is not None:
|
||||
if authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
params["auth"] = HTTPDigestAuth(username, password)
|
||||
else:
|
||||
params["auth"] = HTTPBasicAuth(username, password)
|
||||
if verify_ssl is not None:
|
||||
params["verify"] = verify_ssl
|
||||
retry_num = 0
|
||||
while retry_num < num_retries:
|
||||
req = requests.get(url, **params)
|
||||
if not req.ok:
|
||||
_LOGGER.warning(
|
||||
"Status code %s (retry #%s) loading %s",
|
||||
req.status_code,
|
||||
retry_num + 1,
|
||||
url,
|
||||
)
|
||||
else:
|
||||
data = io.BytesIO(req.content)
|
||||
if data.read():
|
||||
data.seek(0)
|
||||
data.name = url
|
||||
return data
|
||||
_LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url)
|
||||
retry_num += 1
|
||||
_LOGGER.warning("Can't load data in %s after %s retries", url, retry_num)
|
||||
elif filepath is not None:
|
||||
if hass.config.is_allowed_path(filepath):
|
||||
return open(filepath, "rb")
|
||||
|
||||
_LOGGER.warning("'%s' are not secure to load data from!", filepath)
|
||||
else:
|
||||
_LOGGER.warning("Can't load data. No data found in params!")
|
||||
|
||||
except (OSError, TypeError) as error:
|
||||
_LOGGER.error("Can't load data into ByteIO: %s", error)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Telegram bot component."""
|
||||
if not config[DOMAIN]:
|
||||
return False
|
||||
|
||||
for p_config in config[DOMAIN]:
|
||||
|
||||
p_type = p_config.get(CONF_PLATFORM)
|
||||
|
||||
platform = importlib.import_module(f".{p_config[CONF_PLATFORM]}", __name__)
|
||||
|
||||
_LOGGER.info("Setting up %s.%s", DOMAIN, p_type)
|
||||
try:
|
||||
receiver_service = await platform.async_setup_platform(hass, p_config)
|
||||
if receiver_service is False:
|
||||
_LOGGER.error("Failed to initialize Telegram bot %s", p_type)
|
||||
return False
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error setting up platform %s", p_type)
|
||||
return False
|
||||
|
||||
bot = initialize_bot(p_config)
|
||||
notify_service = TelegramNotificationService(
|
||||
hass, bot, p_config.get(CONF_ALLOWED_CHAT_IDS), p_config.get(ATTR_PARSER)
|
||||
)
|
||||
|
||||
async def async_send_telegram_message(service: ServiceCall) -> None:
|
||||
"""Handle sending Telegram Bot message service calls."""
|
||||
|
||||
def _render_template_attr(data, attribute):
|
||||
if attribute_templ := data.get(attribute):
|
||||
if any(
|
||||
isinstance(attribute_templ, vtype) for vtype in (float, int, str)
|
||||
):
|
||||
data[attribute] = attribute_templ
|
||||
else:
|
||||
attribute_templ.hass = hass
|
||||
try:
|
||||
data[attribute] = attribute_templ.async_render(
|
||||
parse_result=False
|
||||
)
|
||||
except TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"TemplateError in %s: %s -> %s",
|
||||
attribute,
|
||||
attribute_templ.template,
|
||||
exc,
|
||||
)
|
||||
data[attribute] = attribute_templ.template
|
||||
|
||||
msgtype = service.service
|
||||
kwargs = dict(service.data)
|
||||
for attribute in (
|
||||
ATTR_MESSAGE,
|
||||
ATTR_TITLE,
|
||||
ATTR_URL,
|
||||
ATTR_FILE,
|
||||
ATTR_CAPTION,
|
||||
ATTR_LONGITUDE,
|
||||
ATTR_LATITUDE,
|
||||
):
|
||||
_render_template_attr(kwargs, attribute)
|
||||
_LOGGER.debug("New telegram message %s: %s", msgtype, kwargs)
|
||||
|
||||
if msgtype == SERVICE_SEND_MESSAGE:
|
||||
await hass.async_add_executor_job(
|
||||
partial(notify_service.send_message, **kwargs)
|
||||
)
|
||||
elif msgtype in [
|
||||
SERVICE_SEND_PHOTO,
|
||||
SERVICE_SEND_ANIMATION,
|
||||
SERVICE_SEND_VIDEO,
|
||||
SERVICE_SEND_VOICE,
|
||||
SERVICE_SEND_DOCUMENT,
|
||||
]:
|
||||
await hass.async_add_executor_job(
|
||||
partial(notify_service.send_file, msgtype, **kwargs)
|
||||
)
|
||||
elif msgtype == SERVICE_SEND_STICKER:
|
||||
await hass.async_add_executor_job(
|
||||
partial(notify_service.send_sticker, **kwargs)
|
||||
)
|
||||
elif msgtype == SERVICE_SEND_LOCATION:
|
||||
await hass.async_add_executor_job(
|
||||
partial(notify_service.send_location, **kwargs)
|
||||
)
|
||||
elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY:
|
||||
await hass.async_add_executor_job(
|
||||
partial(notify_service.answer_callback_query, **kwargs)
|
||||
)
|
||||
elif msgtype == SERVICE_DELETE_MESSAGE:
|
||||
await hass.async_add_executor_job(
|
||||
partial(notify_service.delete_message, **kwargs)
|
||||
)
|
||||
else:
|
||||
await hass.async_add_executor_job(
|
||||
partial(notify_service.edit_message, msgtype, **kwargs)
|
||||
)
|
||||
|
||||
# Register notification services
|
||||
for service_notif, schema in SERVICE_MAP.items():
|
||||
hass.services.async_register(
|
||||
DOMAIN, service_notif, async_send_telegram_message, schema=schema
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def initialize_bot(p_config):
|
||||
"""Initialize telegram bot with proxy support."""
|
||||
|
||||
api_key = p_config.get(CONF_API_KEY)
|
||||
proxy_url = p_config.get(CONF_PROXY_URL)
|
||||
proxy_params = p_config.get(CONF_PROXY_PARAMS)
|
||||
|
||||
if proxy_url is not None:
|
||||
request = Request(
|
||||
con_pool_size=8, proxy_url=proxy_url, urllib3_proxy_kwargs=proxy_params
|
||||
)
|
||||
else:
|
||||
request = Request(con_pool_size=8)
|
||||
return Bot(token=api_key, request=request)
|
||||
|
||||
|
||||
class TelegramNotificationService:
|
||||
"""Implement the notification services for the Telegram Bot domain."""
|
||||
|
||||
def __init__(self, hass, bot, allowed_chat_ids, parser):
|
||||
"""Initialize the service."""
|
||||
|
||||
self.allowed_chat_ids = allowed_chat_ids
|
||||
self._default_user = self.allowed_chat_ids[0]
|
||||
self._last_message_id = {user: None for user in self.allowed_chat_ids}
|
||||
self._parsers = {PARSER_HTML: ParseMode.HTML, PARSER_MD: ParseMode.MARKDOWN}
|
||||
self._parse_mode = self._parsers.get(parser)
|
||||
self.bot = bot
|
||||
self.hass = hass
|
||||
|
||||
def _get_msg_ids(self, msg_data, chat_id):
|
||||
"""Get the message id to edit.
|
||||
|
||||
This can be one of (message_id, inline_message_id) from a msg dict,
|
||||
returning a tuple.
|
||||
**You can use 'last' as message_id** to edit
|
||||
the message last sent in the chat_id.
|
||||
"""
|
||||
message_id = inline_message_id = None
|
||||
if ATTR_MESSAGEID in msg_data:
|
||||
message_id = msg_data[ATTR_MESSAGEID]
|
||||
if (
|
||||
isinstance(message_id, str)
|
||||
and (message_id == "last")
|
||||
and (self._last_message_id[chat_id] is not None)
|
||||
):
|
||||
message_id = self._last_message_id[chat_id]
|
||||
else:
|
||||
inline_message_id = msg_data["inline_message_id"]
|
||||
return message_id, inline_message_id
|
||||
|
||||
def _get_target_chat_ids(self, target):
|
||||
"""Validate chat_id targets or return default target (first).
|
||||
|
||||
:param target: optional list of integers ([12234, -12345])
|
||||
:return list of chat_id targets (integers)
|
||||
"""
|
||||
if target is not None:
|
||||
if isinstance(target, int):
|
||||
target = [target]
|
||||
chat_ids = [t for t in target if t in self.allowed_chat_ids]
|
||||
if chat_ids:
|
||||
return chat_ids
|
||||
_LOGGER.warning(
|
||||
"Disallowed targets: %s, using default: %s", target, self._default_user
|
||||
)
|
||||
return [self._default_user]
|
||||
|
||||
def _get_msg_kwargs(self, data):
|
||||
"""Get parameters in message data kwargs."""
|
||||
|
||||
def _make_row_inline_keyboard(row_keyboard):
|
||||
"""Make a list of InlineKeyboardButtons.
|
||||
|
||||
It can accept:
|
||||
- a list of tuples like:
|
||||
`[(text_b1, data_callback_b1),
|
||||
(text_b2, data_callback_b2), ...]
|
||||
- a string like: `/cmd1, /cmd2, /cmd3`
|
||||
- or a string like: `text_b1:/cmd1, text_b2:/cmd2`
|
||||
"""
|
||||
|
||||
buttons = []
|
||||
if isinstance(row_keyboard, str):
|
||||
for key in row_keyboard.split(","):
|
||||
if ":/" in key:
|
||||
# commands like: 'Label:/cmd' become ('Label', '/cmd')
|
||||
label = key.split(":/")[0]
|
||||
command = key[len(label) + 1 :]
|
||||
buttons.append(
|
||||
InlineKeyboardButton(label, callback_data=command)
|
||||
)
|
||||
else:
|
||||
# commands like: '/cmd' become ('CMD', '/cmd')
|
||||
label = key.strip()[1:].upper()
|
||||
buttons.append(InlineKeyboardButton(label, callback_data=key))
|
||||
elif isinstance(row_keyboard, list):
|
||||
for entry in row_keyboard:
|
||||
text_btn, data_btn = entry
|
||||
buttons.append(
|
||||
InlineKeyboardButton(text_btn, callback_data=data_btn)
|
||||
)
|
||||
else:
|
||||
raise ValueError(str(row_keyboard))
|
||||
return buttons
|
||||
|
||||
# Defaults
|
||||
params = {
|
||||
ATTR_PARSER: self._parse_mode,
|
||||
ATTR_DISABLE_NOTIF: False,
|
||||
ATTR_DISABLE_WEB_PREV: None,
|
||||
ATTR_REPLY_TO_MSGID: None,
|
||||
ATTR_REPLYMARKUP: None,
|
||||
ATTR_TIMEOUT: None,
|
||||
ATTR_MESSAGE_TAG: None,
|
||||
}
|
||||
if data is not None:
|
||||
if ATTR_PARSER in data:
|
||||
params[ATTR_PARSER] = self._parsers.get(
|
||||
data[ATTR_PARSER], self._parse_mode
|
||||
)
|
||||
if ATTR_TIMEOUT in data:
|
||||
params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT]
|
||||
if ATTR_DISABLE_NOTIF in data:
|
||||
params[ATTR_DISABLE_NOTIF] = data[ATTR_DISABLE_NOTIF]
|
||||
if ATTR_DISABLE_WEB_PREV in data:
|
||||
params[ATTR_DISABLE_WEB_PREV] = data[ATTR_DISABLE_WEB_PREV]
|
||||
if ATTR_REPLY_TO_MSGID in data:
|
||||
params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID]
|
||||
if ATTR_MESSAGE_TAG in data:
|
||||
params[ATTR_MESSAGE_TAG] = data[ATTR_MESSAGE_TAG]
|
||||
# Keyboards:
|
||||
if ATTR_KEYBOARD in data:
|
||||
keys = data.get(ATTR_KEYBOARD)
|
||||
keys = keys if isinstance(keys, list) else [keys]
|
||||
if keys:
|
||||
params[ATTR_REPLYMARKUP] = ReplyKeyboardMarkup(
|
||||
[[key.strip() for key in row.split(",")] for row in keys]
|
||||
)
|
||||
else:
|
||||
params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True)
|
||||
|
||||
elif ATTR_KEYBOARD_INLINE in data:
|
||||
keys = data.get(ATTR_KEYBOARD_INLINE)
|
||||
keys = keys if isinstance(keys, list) else [keys]
|
||||
params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup(
|
||||
[_make_row_inline_keyboard(row) for row in keys]
|
||||
)
|
||||
return params
|
||||
|
||||
def _send_msg(self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg):
|
||||
"""Send one message."""
|
||||
|
||||
try:
|
||||
out = func_send(*args_msg, **kwargs_msg)
|
||||
if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID):
|
||||
chat_id = out.chat_id
|
||||
message_id = out[ATTR_MESSAGEID]
|
||||
self._last_message_id[chat_id] = message_id
|
||||
_LOGGER.debug(
|
||||
"Last message ID: %s (from chat_id %s)",
|
||||
self._last_message_id,
|
||||
chat_id,
|
||||
)
|
||||
|
||||
event_data = {
|
||||
ATTR_CHAT_ID: chat_id,
|
||||
ATTR_MESSAGEID: message_id,
|
||||
}
|
||||
if message_tag is not None:
|
||||
event_data[ATTR_MESSAGE_TAG] = message_tag
|
||||
self.hass.bus.fire(EVENT_TELEGRAM_SENT, event_data)
|
||||
elif not isinstance(out, bool):
|
||||
_LOGGER.warning(
|
||||
"Update last message: out_type:%s, out=%s", type(out), out
|
||||
)
|
||||
return out
|
||||
except TelegramError as exc:
|
||||
_LOGGER.error(
|
||||
"%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg
|
||||
)
|
||||
|
||||
def send_message(self, message="", target=None, **kwargs):
|
||||
"""Send a message to one or multiple pre-allowed chat IDs."""
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
text = f"{title}\n{message}" if title else message
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params)
|
||||
self._send_msg(
|
||||
self.bot.send_message,
|
||||
"Error sending message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id,
|
||||
text,
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_to_message_id=params[ATTR_REPLY_TO_MSGID],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
|
||||
def delete_message(self, chat_id=None, **kwargs):
|
||||
"""Delete a previously sent message."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
message_id, _ = self._get_msg_ids(kwargs, chat_id)
|
||||
_LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id)
|
||||
deleted = self._send_msg(
|
||||
self.bot.delete_message, "Error deleting message", None, chat_id, message_id
|
||||
)
|
||||
# reduce message_id anyway:
|
||||
if self._last_message_id[chat_id] is not None:
|
||||
# change last msg_id for deque(n_msgs)?
|
||||
self._last_message_id[chat_id] -= 1
|
||||
return deleted
|
||||
|
||||
def edit_message(self, type_edit, chat_id=None, **kwargs):
|
||||
"""Edit a previously sent message."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id)
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
_LOGGER.debug(
|
||||
"Edit message %s in chat ID %s with params: %s",
|
||||
message_id or inline_message_id,
|
||||
chat_id,
|
||||
params,
|
||||
)
|
||||
if type_edit == SERVICE_EDIT_MESSAGE:
|
||||
message = kwargs.get(ATTR_MESSAGE)
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
text = f"{title}\n{message}" if title else message
|
||||
_LOGGER.debug("Editing message with ID %s", message_id or inline_message_id)
|
||||
return self._send_msg(
|
||||
self.bot.edit_message_text,
|
||||
"Error editing text message",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
text,
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
if type_edit == SERVICE_EDIT_CAPTION:
|
||||
return self._send_msg(
|
||||
self.bot.edit_message_caption,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
)
|
||||
|
||||
return self._send_msg(
|
||||
self.bot.edit_message_reply_markup,
|
||||
"Error editing message attributes",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
inline_message_id=inline_message_id,
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
|
||||
def answer_callback_query(
|
||||
self, message, callback_query_id, show_alert=False, **kwargs
|
||||
):
|
||||
"""Answer a callback originated with a press in an inline keyboard."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
_LOGGER.debug(
|
||||
"Answer callback query with callback ID %s: %s, alert: %s",
|
||||
callback_query_id,
|
||||
message,
|
||||
show_alert,
|
||||
)
|
||||
self._send_msg(
|
||||
self.bot.answer_callback_query,
|
||||
"Error sending answer callback query",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
callback_query_id,
|
||||
text=message,
|
||||
show_alert=show_alert,
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
|
||||
def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs):
|
||||
"""Send a photo, sticker, video, or document."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
file_content = load_data(
|
||||
self.hass,
|
||||
url=kwargs.get(ATTR_URL),
|
||||
filepath=kwargs.get(ATTR_FILE),
|
||||
username=kwargs.get(ATTR_USERNAME),
|
||||
password=kwargs.get(ATTR_PASSWORD),
|
||||
authentication=kwargs.get(ATTR_AUTHENTICATION),
|
||||
verify_ssl=kwargs.get(ATTR_VERIFY_SSL),
|
||||
)
|
||||
|
||||
if file_content:
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug("Sending file to chat ID %s", chat_id)
|
||||
|
||||
if file_type == SERVICE_SEND_PHOTO:
|
||||
self._send_msg(
|
||||
self.bot.send_photo,
|
||||
"Error sending photo",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
photo=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
)
|
||||
|
||||
elif file_type == SERVICE_SEND_STICKER:
|
||||
self._send_msg(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
sticker=file_content,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
|
||||
elif file_type == SERVICE_SEND_VIDEO:
|
||||
self._send_msg(
|
||||
self.bot.send_video,
|
||||
"Error sending video",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
video=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
)
|
||||
elif file_type == SERVICE_SEND_DOCUMENT:
|
||||
self._send_msg(
|
||||
self.bot.send_document,
|
||||
"Error sending document",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
document=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
)
|
||||
elif file_type == SERVICE_SEND_VOICE:
|
||||
self._send_msg(
|
||||
self.bot.send_voice,
|
||||
"Error sending voice",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
voice=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
elif file_type == SERVICE_SEND_ANIMATION:
|
||||
self._send_msg(
|
||||
self.bot.send_animation,
|
||||
"Error sending animation",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
animation=file_content,
|
||||
caption=kwargs.get(ATTR_CAPTION),
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
parse_mode=params[ATTR_PARSER],
|
||||
)
|
||||
|
||||
file_content.seek(0)
|
||||
else:
|
||||
_LOGGER.error("Can't send file with kwargs: %s", kwargs)
|
||||
|
||||
def send_sticker(self, target=None, **kwargs):
|
||||
"""Send a sticker from a telegram sticker pack."""
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
stickerid = kwargs.get(ATTR_STICKER_ID)
|
||||
if stickerid:
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
self._send_msg(
|
||||
self.bot.send_sticker,
|
||||
"Error sending sticker",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
sticker=stickerid,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
reply_markup=params[ATTR_REPLYMARKUP],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
else:
|
||||
self.send_file(SERVICE_SEND_STICKER, target, **kwargs)
|
||||
|
||||
def send_location(self, latitude, longitude, target=None, **kwargs):
|
||||
"""Send a location."""
|
||||
latitude = float(latitude)
|
||||
longitude = float(longitude)
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
for chat_id in self._get_target_chat_ids(target):
|
||||
_LOGGER.debug(
|
||||
"Send location %s/%s to chat ID %s", latitude, longitude, chat_id
|
||||
)
|
||||
self._send_msg(
|
||||
self.bot.send_location,
|
||||
"Error sending location",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id=chat_id,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
disable_notification=params[ATTR_DISABLE_NOTIF],
|
||||
timeout=params[ATTR_TIMEOUT],
|
||||
)
|
||||
|
||||
def leave_chat(self, chat_id=None):
|
||||
"""Remove bot from chat."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
_LOGGER.debug("Leave from chat ID %s", chat_id)
|
||||
leaved = self._send_msg(
|
||||
self.bot.leave_chat, "Error leaving chat", None, chat_id
|
||||
)
|
||||
return leaved
|
||||
|
||||
|
||||
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 _get_message_data(self, msg_data):
|
||||
"""Return boolean msg_data_is_ok and dict msg_data."""
|
||||
if not msg_data:
|
||||
return False, None
|
||||
bad_fields = (
|
||||
"text" not in msg_data and "data" not in msg_data and "chat" not in msg_data
|
||||
)
|
||||
if bad_fields or "from" not in msg_data:
|
||||
# Message is not correct.
|
||||
_LOGGER.error("Incoming message does not have required data (%s)", msg_data)
|
||||
return False, None
|
||||
|
||||
if (
|
||||
msg_data["from"].get("id") not in self.allowed_chat_ids
|
||||
and msg_data["chat"].get("id") not in self.allowed_chat_ids
|
||||
# and msg_data["message"]["chat"].get("id") not in self.allowed_chat_ids
|
||||
):
|
||||
# Neither from id nor chat id was in allowed_chat_ids,
|
||||
# origin is not allowed.
|
||||
_LOGGER.error("Incoming message is not allowed (%s)", msg_data)
|
||||
return True, None
|
||||
|
||||
data = {
|
||||
ATTR_USER_ID: msg_data["from"]["id"],
|
||||
ATTR_FROM_FIRST: msg_data["from"]["first_name"],
|
||||
}
|
||||
if "message_id" in msg_data:
|
||||
data[ATTR_MSGID] = msg_data["message_id"]
|
||||
if "last_name" in msg_data["from"]:
|
||||
data[ATTR_FROM_LAST] = msg_data["from"]["last_name"]
|
||||
if "chat" in msg_data:
|
||||
data[ATTR_CHAT_ID] = msg_data["chat"]["id"]
|
||||
elif ATTR_MESSAGE in msg_data and "chat" in msg_data[ATTR_MESSAGE]:
|
||||
data[ATTR_CHAT_ID] = msg_data[ATTR_MESSAGE]["chat"]["id"]
|
||||
|
||||
return True, data
|
||||
|
||||
def _get_channel_post_data(self, msg_data):
|
||||
"""Return boolean msg_data_is_ok and dict msg_data."""
|
||||
if not msg_data:
|
||||
return False, None
|
||||
|
||||
if "sender_chat" in msg_data and "chat" in msg_data and "text" in msg_data:
|
||||
if (
|
||||
msg_data["sender_chat"].get("id") not in self.allowed_chat_ids
|
||||
and msg_data["chat"].get("id") not in self.allowed_chat_ids
|
||||
):
|
||||
# Neither sender_chat id nor chat id was in allowed_chat_ids,
|
||||
# origin is not allowed.
|
||||
_LOGGER.error("Incoming message is not allowed (%s)", msg_data)
|
||||
return True, None
|
||||
|
||||
data = {
|
||||
ATTR_MSGID: msg_data["message_id"],
|
||||
ATTR_CHAT_ID: msg_data["chat"]["id"],
|
||||
ATTR_TEXT: msg_data["text"],
|
||||
}
|
||||
return True, data
|
||||
|
||||
_LOGGER.error("Incoming message does not have required data (%s)", msg_data)
|
||||
return False, None
|
||||
|
||||
def process_message(self, data):
|
||||
"""Check for basic message rules and fire an event if message is ok."""
|
||||
if ATTR_MSG in data or ATTR_EDITED_MSG in data:
|
||||
event = EVENT_TELEGRAM_COMMAND
|
||||
if ATTR_MSG in data:
|
||||
data = data.get(ATTR_MSG)
|
||||
else:
|
||||
data = data.get(ATTR_EDITED_MSG)
|
||||
message_ok, event_data = self._get_message_data(data)
|
||||
if event_data is None:
|
||||
return message_ok
|
||||
|
||||
if ATTR_MSGID in data:
|
||||
event_data[ATTR_MSGID] = data[ATTR_MSGID]
|
||||
|
||||
if "text" in data:
|
||||
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
|
||||
else:
|
||||
_LOGGER.warning("Message without text data received: %s", data)
|
||||
event_data[ATTR_TEXT] = str(data)
|
||||
event = EVENT_TELEGRAM_TEXT
|
||||
|
||||
self.hass.bus.async_fire(event, event_data)
|
||||
return True
|
||||
if ATTR_CALLBACK_QUERY in data:
|
||||
event = EVENT_TELEGRAM_CALLBACK
|
||||
data = data.get(ATTR_CALLBACK_QUERY)
|
||||
message_ok, event_data = self._get_message_data(data)
|
||||
if event_data is None:
|
||||
return message_ok
|
||||
|
||||
query_data = event_data[ATTR_DATA] = data[ATTR_DATA]
|
||||
|
||||
if query_data[0] == "/":
|
||||
pieces = query_data.split(" ")
|
||||
event_data[ATTR_COMMAND] = pieces[0]
|
||||
event_data[ATTR_ARGS] = pieces[1:]
|
||||
|
||||
event_data[ATTR_MSG] = data[ATTR_MSG]
|
||||
event_data[ATTR_CHAT_INSTANCE] = data[ATTR_CHAT_INSTANCE]
|
||||
event_data[ATTR_MSGID] = data[ATTR_MSGID]
|
||||
|
||||
self.hass.bus.async_fire(event, event_data)
|
||||
return True
|
||||
if ATTR_CHANNEL_POST in data:
|
||||
event = EVENT_TELEGRAM_TEXT
|
||||
data = data.get(ATTR_CHANNEL_POST)
|
||||
message_ok, event_data = self._get_channel_post_data(data)
|
||||
if event_data is None:
|
||||
return message_ok
|
||||
|
||||
self.hass.bus.async_fire(event, event_data)
|
||||
return True
|
||||
|
||||
_LOGGER.warning("Message with unknown data received: %s", data)
|
||||
return True
|
17
custom_components/telegram_bot/broadcast.py
Normal file
17
custom_components/telegram_bot/broadcast.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Support for Telegram bot to send messages only."""
|
||||
import logging
|
||||
|
||||
from . import initialize_bot
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config):
|
||||
"""Set up the Telegram broadcast platform."""
|
||||
bot = initialize_bot(config)
|
||||
|
||||
bot_config = await hass.async_add_executor_job(bot.getMe)
|
||||
_LOGGER.debug(
|
||||
"Telegram broadcast platform setup with bot %s", bot_config["username"]
|
||||
)
|
||||
return True
|
11
custom_components/telegram_bot/manifest.json
Normal file
11
custom_components/telegram_bot/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "telegram_bot",
|
||||
"name": "Telegram bot",
|
||||
"version": "1.0",
|
||||
"documentation": "https://www.home-assistant.io/integrations/telegram_bot",
|
||||
"requirements": ["python-telegram-bot==13.1", "PySocks==1.7.1"],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": [],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["telegram"]
|
||||
}
|
99
custom_components/telegram_bot/polling.py
Normal file
99
custom_components/telegram_bot/polling.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Support for Telegram bot using polling."""
|
||||
import logging
|
||||
|
||||
from telegram import Update
|
||||
from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut
|
||||
from telegram.ext import CallbackContext, Dispatcher, Handler, Updater
|
||||
from telegram.utils.types import HandlerArg
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
from . import CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity, initialize_bot
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config):
|
||||
"""Set up the Telegram polling platform."""
|
||||
bot = initialize_bot(config)
|
||||
pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS])
|
||||
|
||||
def _start_bot(_event):
|
||||
"""Start the bot."""
|
||||
pol.start_polling()
|
||||
|
||||
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
|
||||
|
||||
|
||||
def process_error(update: Update, context: CallbackContext):
|
||||
"""Telegram bot error handler."""
|
||||
try:
|
||||
raise context.error
|
||||
except (TimedOut, NetworkError, RetryAfter):
|
||||
# Long polling timeout or connection problem. Nothing serious.
|
||||
pass
|
||||
except TelegramError:
|
||||
_LOGGER.error('Update "%s" caused error: "%s"', update, context.error)
|
||||
|
||||
|
||||
def message_handler(handler):
|
||||
"""Create messages handler."""
|
||||
|
||||
class MessageHandler(Handler):
|
||||
"""Telegram bot message handler."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the messages handler instance."""
|
||||
super().__init__(handler)
|
||||
|
||||
def check_update(self, update):
|
||||
"""Check is update valid."""
|
||||
return isinstance(update, Update)
|
||||
|
||||
def handle_update(
|
||||
self,
|
||||
update: HandlerArg,
|
||||
dispatcher: Dispatcher,
|
||||
check_result: object,
|
||||
context: CallbackContext = None,
|
||||
):
|
||||
"""Handle update."""
|
||||
optional_args = self.collect_optional_args(dispatcher, update)
|
||||
context.args = optional_args
|
||||
return self.callback(update, context)
|
||||
|
||||
return MessageHandler()
|
||||
|
||||
|
||||
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.updater = Updater(bot=bot, workers=4)
|
||||
self.dispatcher = self.updater.dispatcher
|
||||
|
||||
self.dispatcher.add_handler(message_handler(self.process_update))
|
||||
self.dispatcher.add_error_handler(process_error)
|
||||
|
||||
def start_polling(self):
|
||||
"""Start the polling task."""
|
||||
self.updater.start_polling()
|
||||
|
||||
def stop_polling(self):
|
||||
"""Stop the polling task."""
|
||||
self.updater.stop()
|
||||
|
||||
def process_update(self, update: HandlerArg, context: CallbackContext):
|
||||
"""Process incoming message."""
|
||||
self.process_message(update.to_dict())
|
840
custom_components/telegram_bot/services.yaml
Normal file
840
custom_components/telegram_bot/services.yaml
Normal file
@@ -0,0 +1,840 @@
|
||||
# Describes the format for available Telegram bot services
|
||||
|
||||
send_message:
|
||||
name: Send message
|
||||
description: Send a notification.
|
||||
fields:
|
||||
message:
|
||||
name: Message
|
||||
description: Message body of the notification.
|
||||
required: true
|
||||
example: The garage door has been open for 10 minutes.
|
||||
selector:
|
||||
text:
|
||||
title:
|
||||
name: Title
|
||||
description: Optional title for your notification. Will be composed as '%title\n%message'
|
||||
example: "Your Garage Door Friend"
|
||||
selector:
|
||||
text:
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the notification to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
parse_mode:
|
||||
name: Parse mode
|
||||
description: "Parser for the message text."
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "html"
|
||||
- "markdown"
|
||||
- "markdown2"
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
disable_web_page_preview:
|
||||
name: Disable web page preview
|
||||
description: Disables link previews for links in the message.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send message. Will help with timeout errors (poor internet connection, etc)s
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard. Empty list clears a previously set keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text button2:/button2", "Text button3:/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
message_tag:
|
||||
name: Message tag
|
||||
description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
|
||||
example: "msg_to_edit"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_photo:
|
||||
name: Send photo
|
||||
description: Send a photo.
|
||||
fields:
|
||||
url:
|
||||
name: URL
|
||||
description: Remote path to an image.
|
||||
example: "http://example.org/path/to/the/image.png"
|
||||
selector:
|
||||
text:
|
||||
file:
|
||||
name: File
|
||||
description: Local path to an image.
|
||||
example: "/path/to/the/image.png"
|
||||
selector:
|
||||
text:
|
||||
caption:
|
||||
name: Caption
|
||||
description: The title of the image.
|
||||
example: "My image"
|
||||
selector:
|
||||
text:
|
||||
username:
|
||||
name: Username
|
||||
description: Username for a URL which require HTTP authentication.
|
||||
example: myuser
|
||||
selector:
|
||||
text:
|
||||
password:
|
||||
name: Password
|
||||
description: Password (or bearer token) for a URL which require HTTP authentication.
|
||||
example: myuser_pwd
|
||||
selector:
|
||||
text:
|
||||
authentication:
|
||||
name: Authentication method
|
||||
description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
|
||||
default: digest
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "digest"
|
||||
- "bearer_token"
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
parse_mode:
|
||||
name: Parse mode
|
||||
description: "Parser for the message text."
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "html"
|
||||
- "markdown"
|
||||
- "markdown2"
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
verify_ssl:
|
||||
name: Verify SSL
|
||||
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
message_tag:
|
||||
name: Message tag
|
||||
description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
|
||||
example: "msg_to_edit"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_sticker:
|
||||
name: Send sticker
|
||||
description: Send a sticker.
|
||||
fields:
|
||||
url:
|
||||
name: URL
|
||||
description: Remote path to a static .webp or animated .tgs sticker.
|
||||
example: "http://example.org/path/to/the/sticker.webp"
|
||||
selector:
|
||||
text:
|
||||
file:
|
||||
name: File
|
||||
description: Local path to a static .webp or animated .tgs sticker.
|
||||
example: "/path/to/the/sticker.webp"
|
||||
selector:
|
||||
text:
|
||||
sticker_id:
|
||||
name: Sticker ID
|
||||
description: ID of a sticker that exists on telegram servers
|
||||
example: CAACAgIAAxkBAAEDDldhZD-hqWclr6krLq-FWSfCrGNmOQAC9gAD9HsZAAFeYY-ltPYnrCEE
|
||||
selector:
|
||||
text:
|
||||
username:
|
||||
name: Username
|
||||
description: Username for a URL which require HTTP authentication.
|
||||
example: myuser
|
||||
selector:
|
||||
text:
|
||||
password:
|
||||
name: Password
|
||||
description: Password (or bearer token) for a URL which require HTTP authentication.
|
||||
example: myuser_pwd
|
||||
selector:
|
||||
text:
|
||||
authentication:
|
||||
name: Authentication method
|
||||
description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
|
||||
default: digest
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "digest"
|
||||
- "bearer_token"
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
verify_ssl:
|
||||
name: Verify SSL
|
||||
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
message_tag:
|
||||
name: Message tag
|
||||
description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
|
||||
example: "msg_to_edit"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_animation:
|
||||
name: Send animation
|
||||
description: Send an anmiation.
|
||||
fields:
|
||||
url:
|
||||
name: URL
|
||||
description: Remote path to a GIF or H.264/MPEG-4 AVC video without sound.
|
||||
example: "http://example.org/path/to/the/animation.gif"
|
||||
selector:
|
||||
text:
|
||||
file:
|
||||
name: File
|
||||
description: Local path to a GIF or H.264/MPEG-4 AVC video without sound.
|
||||
example: "/path/to/the/animation.gif"
|
||||
selector:
|
||||
text:
|
||||
caption:
|
||||
name: Caption
|
||||
description: The title of the animation.
|
||||
example: "My animation"
|
||||
selector:
|
||||
text:
|
||||
username:
|
||||
name: Username
|
||||
description: Username for a URL which require HTTP authentication.
|
||||
example: myuser
|
||||
selector:
|
||||
text:
|
||||
password:
|
||||
name: Password
|
||||
description: Password (or bearer token) for a URL which require HTTP authentication.
|
||||
example: myuser_pwd
|
||||
selector:
|
||||
text:
|
||||
authentication:
|
||||
name: Authentication method
|
||||
description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
|
||||
default: digest
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "digest"
|
||||
- "bearer_token"
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
parse_mode:
|
||||
name: Parse Mode
|
||||
description: "Parser for the message text."
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "html"
|
||||
- "markdown"
|
||||
- "markdown2"
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
verify_ssl:
|
||||
name: Verify SSL
|
||||
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
|
||||
send_video:
|
||||
name: Send video
|
||||
description: Send a video.
|
||||
fields:
|
||||
url:
|
||||
name: URL
|
||||
description: Remote path to a video.
|
||||
example: "http://example.org/path/to/the/video.mp4"
|
||||
selector:
|
||||
text:
|
||||
file:
|
||||
name: File
|
||||
description: Local path to a video.
|
||||
example: "/path/to/the/video.mp4"
|
||||
selector:
|
||||
text:
|
||||
caption:
|
||||
name: Caption
|
||||
description: The title of the video.
|
||||
example: "My video"
|
||||
selector:
|
||||
text:
|
||||
username:
|
||||
name: Username
|
||||
description: Username for a URL which require HTTP authentication.
|
||||
example: myuser
|
||||
selector:
|
||||
text:
|
||||
password:
|
||||
name: Password
|
||||
description: Password (or bearer token) for a URL which require HTTP authentication.
|
||||
example: myuser_pwd
|
||||
selector:
|
||||
text:
|
||||
authentication:
|
||||
name: Authentication method
|
||||
description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
|
||||
default: digest
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "digest"
|
||||
- "bearer_token"
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
parse_mode:
|
||||
name: Parse mode
|
||||
description: "Parser for the message text."
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "html"
|
||||
- "markdown"
|
||||
- "markdown2"
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
verify_ssl:
|
||||
name: Verify SSL
|
||||
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send video. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
message_tag:
|
||||
name: Message tag
|
||||
description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
|
||||
example: "msg_to_edit"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_voice:
|
||||
name: Send voice
|
||||
description: Send a voice message.
|
||||
fields:
|
||||
url:
|
||||
name: URL
|
||||
description: Remote path to a voice message.
|
||||
example: "http://example.org/path/to/the/voice.opus"
|
||||
selector:
|
||||
text:
|
||||
file:
|
||||
name: File
|
||||
description: Local path to a voice message.
|
||||
example: "/path/to/the/voice.opus"
|
||||
selector:
|
||||
text:
|
||||
caption:
|
||||
name: Caption
|
||||
description: The title of the voice message.
|
||||
example: "My microphone recording"
|
||||
selector:
|
||||
text:
|
||||
username:
|
||||
name: Username
|
||||
description: Username for a URL which require HTTP authentication.
|
||||
example: myuser
|
||||
selector:
|
||||
text:
|
||||
password:
|
||||
name: Password
|
||||
description: Password (or bearer token) for a URL which require HTTP authentication.
|
||||
example: myuser_pwd
|
||||
selector:
|
||||
text:
|
||||
authentication:
|
||||
name: Authentication method
|
||||
description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
|
||||
default: digest
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "digest"
|
||||
- "bearer_token"
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
verify_ssl:
|
||||
name: Verify SSL
|
||||
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send voice. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
message_tag:
|
||||
name: Message tag
|
||||
description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
|
||||
example: "msg_to_edit"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_document:
|
||||
name: Send document
|
||||
description: Send a document.
|
||||
fields:
|
||||
url:
|
||||
name: URL
|
||||
description: Remote path to a document.
|
||||
example: "http://example.org/path/to/the/document.odf"
|
||||
selector:
|
||||
text:
|
||||
file:
|
||||
name: File
|
||||
description: Local path to a document.
|
||||
example: "/tmp/whatever.odf"
|
||||
selector:
|
||||
text:
|
||||
caption:
|
||||
name: Caption
|
||||
description: The title of the document.
|
||||
example: Document Title xy
|
||||
selector:
|
||||
text:
|
||||
username:
|
||||
name: Username
|
||||
description: Username for a URL which require HTTP authentication.
|
||||
example: myuser
|
||||
selector:
|
||||
text:
|
||||
password:
|
||||
name: Password
|
||||
description: Password (or bearer token) for a URL which require HTTP authentication.
|
||||
example: myuser_pwd
|
||||
selector:
|
||||
text:
|
||||
authentication:
|
||||
name: Authentication method
|
||||
description: Define which authentication method to use. Set to `digest` to use HTTP digest authentication, or `bearer_token` for OAuth 2.0 bearer token authentication. Defaults to `basic`.
|
||||
default: digest
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "digest"
|
||||
- "bearer_token"
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
parse_mode:
|
||||
name: Parse mode
|
||||
description: "Parser for the message text."
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "html"
|
||||
- "markdown"
|
||||
- "markdown2"
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
verify_ssl:
|
||||
name: Verify SSL
|
||||
description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send document. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
message_tag:
|
||||
name: Message tag
|
||||
description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
|
||||
example: "msg_to_edit"
|
||||
selector:
|
||||
text:
|
||||
|
||||
send_location:
|
||||
name: Send location
|
||||
description: Send a location.
|
||||
fields:
|
||||
latitude:
|
||||
name: Latitude
|
||||
description: The latitude to send.
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: -90
|
||||
max: 90
|
||||
step: 0.001
|
||||
unit_of_measurement: "°"
|
||||
longitude:
|
||||
name: Longitude
|
||||
description: The longitude to send.
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: -180
|
||||
max: 180
|
||||
step: 0.001
|
||||
unit_of_measurement: "°"
|
||||
target:
|
||||
name: Target
|
||||
description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default.
|
||||
example: "[12345, 67890] or 12345"
|
||||
selector:
|
||||
object:
|
||||
disable_notification:
|
||||
name: Disable notification
|
||||
description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
keyboard:
|
||||
name: Keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom keyboard.
|
||||
example: '["/command1, /command2", "/command3"]'
|
||||
selector:
|
||||
object:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
message_tag:
|
||||
name: Message tag
|
||||
description: "Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}"
|
||||
example: "msg_to_edit"
|
||||
selector:
|
||||
text:
|
||||
|
||||
edit_message:
|
||||
name: Edit message
|
||||
description: Edit a previously sent message.
|
||||
fields:
|
||||
message_id:
|
||||
name: Message ID
|
||||
description: id of the message to edit.
|
||||
required: true
|
||||
example: "{{ trigger.event.data.message.message_id }}"
|
||||
selector:
|
||||
text:
|
||||
chat_id:
|
||||
name: Chat ID
|
||||
description: The chat_id where to edit the message.
|
||||
required: true
|
||||
example: 12345
|
||||
selector:
|
||||
text:
|
||||
message:
|
||||
name: Message
|
||||
description: Message body of the notification.
|
||||
example: The garage door has been open for 10 minutes.
|
||||
selector:
|
||||
text:
|
||||
title:
|
||||
name: Title
|
||||
description: Optional title for your notification. Will be composed as '%title\n%message'
|
||||
example: "Your Garage Door Friend"
|
||||
selector:
|
||||
text:
|
||||
parse_mode:
|
||||
name: Parse mode
|
||||
description: "Parser for the message text."
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "html"
|
||||
- "markdown"
|
||||
- "markdown2"
|
||||
disable_web_page_preview:
|
||||
name: Disable web page preview
|
||||
description: Disables link previews for links in the message.
|
||||
selector:
|
||||
boolean:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
|
||||
edit_caption:
|
||||
name: Edit caption
|
||||
description: Edit the caption of a previously sent message.
|
||||
fields:
|
||||
message_id:
|
||||
name: Message ID
|
||||
description: id of the message to edit.
|
||||
required: true
|
||||
example: "{{ trigger.event.data.message.message_id }}"
|
||||
selector:
|
||||
text:
|
||||
chat_id:
|
||||
name: Chat ID
|
||||
description: The chat_id where to edit the caption.
|
||||
required: true
|
||||
example: 12345
|
||||
selector:
|
||||
text:
|
||||
caption:
|
||||
name: Caption
|
||||
description: Message body of the notification.
|
||||
required: true
|
||||
example: The garage door has been open for 10 minutes.
|
||||
selector:
|
||||
text:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
|
||||
edit_replymarkup:
|
||||
name: Edit reply markup
|
||||
description: Edit the inline keyboard of a previously sent message.
|
||||
fields:
|
||||
message_id:
|
||||
name: Message ID
|
||||
description: id of the message to edit.
|
||||
required: true
|
||||
example: "{{ trigger.event.data.message.message_id }}"
|
||||
selector:
|
||||
text:
|
||||
chat_id:
|
||||
name: Chat ID
|
||||
description: The chat_id where to edit the reply_markup.
|
||||
required: true
|
||||
example: 12345
|
||||
selector:
|
||||
text:
|
||||
inline_keyboard:
|
||||
name: Inline keyboard
|
||||
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
|
||||
required: true
|
||||
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
|
||||
selector:
|
||||
object:
|
||||
|
||||
answer_callback_query:
|
||||
name: Answer callback query
|
||||
description: Respond to a callback query originated by clicking on an online keyboard button. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert.
|
||||
fields:
|
||||
message:
|
||||
name: Message
|
||||
description: Unformatted text message body of the notification.
|
||||
required: true
|
||||
example: "OK, I'm listening"
|
||||
selector:
|
||||
text:
|
||||
callback_query_id:
|
||||
name: Callback query ID
|
||||
description: Unique id of the callback response.
|
||||
required: true
|
||||
example: "{{ trigger.event.data.id }}"
|
||||
selector:
|
||||
text:
|
||||
show_alert:
|
||||
name: Show alert
|
||||
description: Show a permanent notification.
|
||||
required: true
|
||||
selector:
|
||||
boolean:
|
||||
timeout:
|
||||
name: Timeout
|
||||
description: Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc)
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
|
||||
delete_message:
|
||||
name: Delete message
|
||||
description: Delete a previously sent message.
|
||||
fields:
|
||||
message_id:
|
||||
name: Message ID
|
||||
description: id of the message to delete.
|
||||
required: true
|
||||
example: "{{ trigger.event.data.message.message_id }}"
|
||||
selector:
|
||||
text:
|
||||
chat_id:
|
||||
name: Chat ID
|
||||
description: The chat_id where to delete the message.
|
||||
required: true
|
||||
example: 12345
|
||||
selector:
|
||||
text:
|
107
custom_components/telegram_bot/webhooks.py
Normal file
107
custom_components/telegram_bot/webhooks.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Support for Telegram bots using webhooks."""
|
||||
import datetime as dt
|
||||
from http import HTTPStatus
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
|
||||
from telegram.error import TimedOut
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.network import get_url
|
||||
|
||||
from . import (
|
||||
CONF_ALLOWED_CHAT_IDS,
|
||||
CONF_TRUSTED_NETWORKS,
|
||||
CONF_URL,
|
||||
BaseTelegramBotEntity,
|
||||
initialize_bot,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TELEGRAM_HANDLER_URL = "/api/telegram_webhooks"
|
||||
REMOVE_HANDLER_URL = ""
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config):
|
||||
"""Set up the Telegram webhooks platform."""
|
||||
|
||||
bot = initialize_bot(config)
|
||||
|
||||
current_status = await hass.async_add_executor_job(bot.getWebhookInfo)
|
||||
if not (base_url := config.get(CONF_URL)):
|
||||
base_url = get_url(hass, require_ssl=True, allow_internal=False)
|
||||
|
||||
# Some logging of Bot current status:
|
||||
last_error_date = getattr(current_status, "last_error_date", None)
|
||||
if (last_error_date is not None) and (isinstance(last_error_date, int)):
|
||||
last_error_date = dt.datetime.fromtimestamp(last_error_date)
|
||||
_LOGGER.info(
|
||||
"Telegram webhook last_error_date: %s. Status: %s",
|
||||
last_error_date,
|
||||
current_status,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("telegram webhook Status: %s", current_status)
|
||||
|
||||
handler_url = f"{base_url}{TELEGRAM_HANDLER_URL}"
|
||||
if not handler_url.startswith("https"):
|
||||
_LOGGER.error("Invalid telegram webhook %s must be https", handler_url)
|
||||
return False
|
||||
|
||||
def _try_to_set_webhook():
|
||||
retry_num = 0
|
||||
while retry_num < 3:
|
||||
try:
|
||||
return bot.setWebhook(handler_url, timeout=5)
|
||||
except TimedOut:
|
||||
retry_num += 1
|
||||
_LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num)
|
||||
|
||||
if current_status and current_status["url"] != handler_url:
|
||||
result = await hass.async_add_executor_job(_try_to_set_webhook)
|
||||
if result:
|
||||
_LOGGER.info("Set new telegram webhook %s", handler_url)
|
||||
else:
|
||||
_LOGGER.error("Set telegram webhook failed %s", handler_url)
|
||||
return False
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, lambda event: bot.setWebhook(REMOVE_HANDLER_URL)
|
||||
)
|
||||
hass.http.register_view(
|
||||
BotPushReceiver(
|
||||
hass, config[CONF_ALLOWED_CHAT_IDS], config[CONF_TRUSTED_NETWORKS]
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
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
|
||||
|
||||
async def post(self, request):
|
||||
"""Accept the POST from telegram."""
|
||||
real_ip = ip_address(request.remote)
|
||||
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", HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST)
|
||||
|
||||
if not self.process_message(data):
|
||||
return self.json_message("Invalid message", HTTPStatus.BAD_REQUEST)
|
||||
return None
|
Reference in New Issue
Block a user