Telegram Bot enhancements with callback queries and more notification options#7294
Telegram Bot enhancements with callback queries and more notification options#7294azogue wants to merge 3 commits into
Conversation
| if ATTR_DISABLE_NOTIF in data: | ||
| params['disable_notification'] = data[ATTR_DISABLE_NOTIF] | ||
| if ATTR_DISABLE_WEB_PREV in data: | ||
| params['disable_web_page_preview'] = data[ATTR_DISABLE_WEB_PREV] |
There was a problem hiding this comment.
line too long (80 > 79 characters)
| - Callback replies for edit messages, reply_markup keyboards and captions, and for answering callback queries with: `data: {'callback_query'|'edit_message'| 'edit_caption'|'edit_replymarkup': ...}` | ||
| - Line break between title and message fields: `'{}\n{}'.format(title, message)` | ||
| - Ablility to target multiple pre-authorized chat_ids (`target=[12345, 67890]`) when sending a message. | ||
| - BREAKING CHANGE: use array of `user_id` to allow one notifier to comunicate with multiple users (first user is the default, but you can pass a `ATTR_TARGET=chat_id_X` to send a message to other recipient). (Reading `chat_id` as User1 to work with old configuration) |
There was a problem hiding this comment.
line too long (267 > 79 characters)
| - `disable_notification`, `disable_web_page_preview` and `reply_to_message_id` optional keyword args. | ||
| - Callback replies for edit messages, reply_markup keyboards and captions, and for answering callback queries with: `data: {'callback_query'|'edit_message'| 'edit_caption'|'edit_replymarkup': ...}` | ||
| - Line break between title and message fields: `'{}\n{}'.format(title, message)` | ||
| - Ablility to target multiple pre-authorized chat_ids (`target=[12345, 67890]`) when sending a message. |
There was a problem hiding this comment.
line too long (103 > 79 characters)
| - Custom reply_markup (keyboard or inline_keyboard) for every type of message (message, photo, location & document). | ||
| - `disable_notification`, `disable_web_page_preview` and `reply_to_message_id` optional keyword args. | ||
| - Callback replies for edit messages, reply_markup keyboards and captions, and for answering callback queries with: `data: {'callback_query'|'edit_message'| 'edit_caption'|'edit_replymarkup': ...}` | ||
| - Line break between title and message fields: `'{}\n{}'.format(title, message)` |
There was a problem hiding this comment.
line too long (80 > 79 characters)
| - Inline keyboards with `data: {'inline_keyboard': [(text_btn1, data_callback_btn1), ...]}` | ||
| - Custom reply_markup (keyboard or inline_keyboard) for every type of message (message, photo, location & document). | ||
| - `disable_notification`, `disable_web_page_preview` and `reply_to_message_id` optional keyword args. | ||
| - Callback replies for edit messages, reply_markup keyboards and captions, and for answering callback queries with: `data: {'callback_query'|'edit_message'| 'edit_caption'|'edit_replymarkup': ...}` |
There was a problem hiding this comment.
line too long (197 > 79 characters)
| - Customized for using any of both parsers (`markdown` and `html`) in any message with: `data: {'parse_mode': 'html'}`, with markdown as default, but can be globally customized with 'parse_mode' in yaml config. | ||
| - Inline keyboards with `data: {'inline_keyboard': [(text_btn1, data_callback_btn1), ...]}` | ||
| - Custom reply_markup (keyboard or inline_keyboard) for every type of message (message, photo, location & document). | ||
| - `disable_notification`, `disable_web_page_preview` and `reply_to_message_id` optional keyword args. |
There was a problem hiding this comment.
line too long (101 > 79 characters)
| Changes: | ||
| - Customized for using any of both parsers (`markdown` and `html`) in any message with: `data: {'parse_mode': 'html'}`, with markdown as default, but can be globally customized with 'parse_mode' in yaml config. | ||
| - Inline keyboards with `data: {'inline_keyboard': [(text_btn1, data_callback_btn1), ...]}` | ||
| - Custom reply_markup (keyboard or inline_keyboard) for every type of message (message, photo, location & document). |
There was a problem hiding this comment.
line too long (116 > 79 characters)
|
|
||
| Changes: | ||
| - Customized for using any of both parsers (`markdown` and `html`) in any message with: `data: {'parse_mode': 'html'}`, with markdown as default, but can be globally customized with 'parse_mode' in yaml config. | ||
| - Inline keyboards with `data: {'inline_keyboard': [(text_btn1, data_callback_btn1), ...]}` |
There was a problem hiding this comment.
line too long (91 > 79 characters)
| https://home-assistant.io/components/notify.telegram/ | ||
|
|
||
| Changes: | ||
| - Customized for using any of both parsers (`markdown` and `html`) in any message with: `data: {'parse_mode': 'html'}`, with markdown as default, but can be globally customized with 'parse_mode' in yaml config. |
There was a problem hiding this comment.
line too long (210 > 79 characters)
|
##Added new doc with yaml examples for using telegram callback queries: Sample automations with callback queries and inline keyboardsQuick example to show some of the callback capabilities of inline keyboards with a dumb automation consisting in a simple repeater of normal text that presents an inline keyboard with 3 buttons: 'EDIT', 'NO' and 'REMOVE BUTTON':
Text repeater: - alias: 'telegram bot that repeats text'
hide_entity: true
trigger:
platform: event
event_type: telegram_text
action:
- service: notify.telegram
data_template:
title: '*Dumb automation*'
target: '{{ trigger.event.data.user_id }}'
message: 'You said: ``` {{ trigger.event.data.text }} ```'
data:
disable_notification: true
inline_keyboard:
- '/edit,/NO'
- '/remove button'Message editor: - alias: 'telegram bot last sended msg edit'
hide_entity: true
trigger:
platform: event
event_type: telegram_callback
event_data:
data: '/edit'
action:
- service: notify.telegram
data_template:
target: '{{ trigger.event.data.user_id }}'
message: 'Editing the message!'
data:
callback_query:
callback_query_id: '{{ trigger.event.data.id }}'
show_alert: true
- service: notify.telegram
data_template:
title: '*Message edit*'
target: '{{ trigger.event.data.user_id }}'
message: >
Callback received from {{ trigger.event.data.from_first }}.
Message id: {{ trigger.event.data.message.message_id }}.
Data: ``` {{ trigger.event.data.data }} ```
data:
edit_message:
message_id: '{{ trigger.event.data.message.message_id }}'
disable_notification: true
inline_keyboard:
- '/edit,/NO'
- '/remove button'Keyboard editor: - alias: 'telegram bot keyboard edit'
hide_entity: true
trigger:
platform: event
event_type: telegram_callback
event_data:
data: '/remove button'
action:
- service: notify.telegram
data_template:
target: '{{ trigger.event.data.user_id }}'
message: 'Callback received for editing the inline keyboard!'
data:
callback_query:
callback_query_id: '{{ trigger.event.data.id }}'
show_alert: false
- service: notify.telegram
data_template:
target: '{{ trigger.event.data.user_id }}'
message: '' # this is needed for the general hass notify service
data:
edit_replymarkup:
message_id: 'last'
disable_notification: true
inline_keyboard:
- '/edit,/NO'Only acknowledges the 'NO' answer: - alias: 'telegram bot simply acknowledges'
hide_entity: true
trigger:
platform: event
event_type: telegram_callback
event_data:
data: '/NO'
action:
- service: notify.telegram
data_template:
target: '{{ trigger.event.data.user_id }}'
message: 'OK, you said no!'
data:
callback_query:
callback_query_id: '{{ trigger.event.data.id }}'
show_alert: falseSnapshots of these automations running:I don't know if it is desirable to add the images to the PR |
|
I don't think images are necessary in this case since hopefully the end users of this function is familiar with Telegram and it's not support. |
|
But a picture says more than thousand words. 😄 |
There was a problem hiding this comment.
This comment is a pretty good indicator that we should move the communication with telegram into the component. The telegram notify service can then just be forwarding things to the telegram component.
When moving it to the telegram component, break it up in different services instead of overloading the "send_message" service like is now.
|
@balloob, I'm not sure what you mean by this, do you mean we should remove the notification component and create new services into the Telegram Bot component? So that the Telegram Bot component serves to send and receive messages, and not just to receive them? Announcing new services like these:
|
|
Yes, that's what I mean. But don't remove the notify telegram service, instead have that service just call |
There was a problem hiding this comment.
I'm testing the requested changes, I hope tomorrow I'll have the time to finish the PR and the associated doc.
I have limited the configuration of a single platform ('webhooks' or 'polling') within the telegram_bot component, linking the notification services to the name 'telegram_bot / service_name', according to the previous comment.
The notify.telegram component now depends on telegram_bot and does not require the python-telegram module. Now it is only an optional "fast link" to the new notification services of the telegram_bot component, but allows backward compatibility for all service calls to notify.telegram with the old syntax.
Basically, it allows you to generate a customised shortcut to send notifications to a particular chat_id.
It would look like this:
"""
Telegram platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.telegram/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, ATTR_TARGET,
PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import ATTR_LOCATION
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'telegram_bot'
DEPENDENCIES = [DOMAIN]
ATTR_KEYBOARD = 'keyboard'
ATTR_INLINE_KEYBOARD = 'inline_keyboard'
ATTR_PHOTO = 'photo'
ATTR_DOCUMENT = 'document'
CONF_CHAT_ID = 'chat_id'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CHAT_ID): cv.positive_int,
})
def get_service(hass, config, discovery_info=None):
"""Get the Telegram notification service."""
chat_id = config.get(CONF_CHAT_ID)
return TelegramNotificationService(hass, chat_id)
class TelegramNotificationService(BaseNotificationService):
"""Implement the notification service for Telegram."""
def __init__(self, hass, chat_id):
"""Initialize the service."""
self._chat_id = chat_id
self.hass = hass
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
service_data = dict(target=kwargs.get(ATTR_TARGET, self._chat_id))
if ATTR_TITLE in kwargs:
service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)})
if message:
service_data.update({ATTR_MESSAGE: message})
data = kwargs.get(ATTR_DATA)
# Get keyboard info
if data is not None and ATTR_KEYBOARD in data:
keys = data.get(ATTR_KEYBOARD)
keys = keys if isinstance(keys, list) else [keys]
service_data.update(keyboard=keys)
elif data is not None and ATTR_INLINE_KEYBOARD in data:
keys = data.get(ATTR_INLINE_KEYBOARD)
keys = keys if isinstance(keys, list) else [keys]
service_data.update(inline_keyboard=keys)
# Send a photo, a document or a location
if data is not None and ATTR_PHOTO in data:
photos = data.get(ATTR_PHOTO, None)
photos = photos if isinstance(photos, list) else [photos]
for photo_data in photos:
service_data.update(photo_data)
self.hass.services.call(
DOMAIN, 'send_photo', service_data=service_data)
return
elif data is not None and ATTR_LOCATION in data:
service_data.update(data.get(ATTR_LOCATION))
return self.hass.services.call(
DOMAIN, 'send_location', service_data=service_data)
elif data is not None and ATTR_DOCUMENT in data:
service_data.update(data.get(ATTR_DOCUMENT))
return self.hass.services.call(
DOMAIN, 'send_document', service_data=service_data)
# Send message
_LOGGER.debug('TELEGRAM NOTIFIER calling %s.send_message with %s',
DOMAIN, service_data)
return self.hass.services.call(
DOMAIN, 'send_message', service_data=service_data)|
I'm testing the requested changes, I hope tomorrow I'll have the time to finish the PR and the associated doc. I have limited the configuration of a single platform ('webhooks' or 'polling') within the The notify.telegram component now depends on """
Telegram platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.telegram/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, ATTR_TARGET,
PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import ATTR_LOCATION
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'telegram_bot'
DEPENDENCIES = [DOMAIN]
ATTR_KEYBOARD = 'keyboard'
ATTR_INLINE_KEYBOARD = 'inline_keyboard'
ATTR_PHOTO = 'photo'
ATTR_DOCUMENT = 'document'
CONF_CHAT_ID = 'chat_id'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CHAT_ID): cv.positive_int,
})
def get_service(hass, config, discovery_info=None):
"""Get the Telegram notification service."""
chat_id = config.get(CONF_CHAT_ID)
return TelegramNotificationService(hass, chat_id)
class TelegramNotificationService(BaseNotificationService):
"""Implement the notification service for Telegram."""
def __init__(self, hass, chat_id):
"""Initialize the service."""
self._chat_id = chat_id
self.hass = hass
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
service_data = dict(target=kwargs.get(ATTR_TARGET, self._chat_id))
if ATTR_TITLE in kwargs:
service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)})
if message:
service_data.update({ATTR_MESSAGE: message})
data = kwargs.get(ATTR_DATA)
# Get keyboard info
if data is not None and ATTR_KEYBOARD in data:
keys = data.get(ATTR_KEYBOARD)
keys = keys if isinstance(keys, list) else [keys]
service_data.update(keyboard=keys)
elif data is not None and ATTR_INLINE_KEYBOARD in data:
keys = data.get(ATTR_INLINE_KEYBOARD)
keys = keys if isinstance(keys, list) else [keys]
service_data.update(inline_keyboard=keys)
# Send a photo, a document or a location
if data is not None and ATTR_PHOTO in data:
photos = data.get(ATTR_PHOTO, None)
photos = photos if isinstance(photos, list) else [photos]
for photo_data in photos:
service_data.update(photo_data)
self.hass.services.call(
DOMAIN, 'send_photo', service_data=service_data)
return
elif data is not None and ATTR_LOCATION in data:
service_data.update(data.get(ATTR_LOCATION))
return self.hass.services.call(
DOMAIN, 'send_location', service_data=service_data)
elif data is not None and ATTR_DOCUMENT in data:
service_data.update(data.get(ATTR_DOCUMENT))
return self.hass.services.call(
DOMAIN, 'send_document', service_data=service_data)
# Send message
_LOGGER.debug('TELEGRAM NOTIFIER calling %s.send_message with %s',
DOMAIN, service_data)
return self.hass.services.call(
DOMAIN, 'send_message', service_data=service_data) |
|
Yeah that looks great 👍 |
There was a problem hiding this comment.
visually indented line with same indent as next logical line
There was a problem hiding this comment.
If only 1 is allowed, we should only setup 1. So line 293 should be updated.
However, if we only allow 1, we should not have a PLATFORM_SCHEMA to begin with but instead have a CONFIG_SCHEMA.
There was a problem hiding this comment.
Please import built-ins at the top
There was a problem hiding this comment.
Does this do any I/O ? If so, you can't do it inside an async context
There was a problem hiding this comment.
If both platforms have the same dependency, let's remove it from platforms and move it to the component.
There was a problem hiding this comment.
This logic is no longer needed, both have async_setup_platform now.
There was a problem hiding this comment.
Wait, not sure how this code sneaked in here but this can be removed.
There was a problem hiding this comment.
There is no need to create these wrappers since no one is extending this class. Just have the service handler call everything in the executor.
yield from hass.async_add_job(partial(notify_service.send_file, True, **kwargs))|
@balloob, the changes are done. I have manually tested both platforms: I do not know if the change from PLATFORM_SCHEMA to CONFIG_SCHEMA has repercussions on the documentation pages (actually, the user configuration remains the same), since the parameters are being described on each platform, rather than on the component page. |
There was a problem hiding this comment.
Please don't extend the BaseNotificationService
There was a problem hiding this comment.
Please just internalize it at the once place where you use this function.
There was a problem hiding this comment.
This is a leftover from the notify service, you can now pass hass in if you need it.
There was a problem hiding this comment.
I hardly see anyone use dicts like this. It's actually worse perf to create a dict like this compared to specifying it with dict syntax:
params = {
'parse_mode': self._parse_mode,
# etc…
}There was a problem hiding this comment.
I did not know that the performance was worse!, I use a lot of that way of creating dictionaries (aesthetically I usually like it more ;-), I will try to change my style from now on...
|
Almost there, just saw a few small changes. Also noticed you got some conflicts, not sure what they are. Are there any breaking changes for the config? |
I understand not, since I do not think there is anyone who has defined both platforms (polling and webhooks) or multiple bots, although the change from PLATFORM_SCHEMA to CONFIG_SCHEMA I think stops allowing this configuration: telegram_bot:
- platform: webhooks
api_key: telegram_api_key
allowed_chat_ids:
- 12345
- 67890So for some specific user may be a problem, but is easily solved by eliminating the platform hyphen. The other change, in
I think they are simply representing my very limited experience with git, I'm learning slowly. I'll see how I can fix them. |
- Receive callback queries and produce `telegram_callback` events:
```
{'chat_instance': 'XXXXXXXXXXXXXXXXXXX',
'data': '[/data sended]',
'from': {'username': '[USERNAME]',
'last_name': '[LAST_NAME]', 'first_name': '[FIRST_NAME]',
'id': 123456789},
'id': '1234567890123456789',
'message': { original_msg }
```
- Customized for using any of both parsers (`markdown` and `html`) in any message with: `data: {'parse_mode': 'html'}`, with markdown as default, but can be globally customized with 'parse_mode' in yaml config.
- Inline keyboards with `data: {'inline_keyboard': [(text_btn1, data_callback_btn1), ...]}`
- Custom reply_markup (keyboard or inline_keyboard) for every type of message (message, photo, location & document).
- `disable_notification`, `disable_web_page_preview` and `reply_to_message_id` optional keyword args.
- Callback replies for edit messages, reply_markup keyboards and captions, and for answering callback queries with: `data: {'callback_query'|'edit_message'| 'edit_caption'|'edit_replymarkup': ...}`
- Line break between title and message fields: `'{}\n{}'.format(title, message)`
- Ablility to target multiple pre-authorized chat_ids (`target=[12345, 67890]`) when sending a message.
- Requested changes: move Telegram notification services to `telegram_bot` component and forward service calls from the telegram notify service to the telegram component. Added descriptions of the new services with a services.yaml file.
- Requested changes: CONFIG_SCHEMA instead of PLATFORM_SCHEMA; no need for async wrappers; no need to validate api_key getting the notification service; removed common requirements from platforms; built-ins at the top; I/O in async; removed async_platform_discovered; removed unused logic for setup_platform.
- Requested changes: TelegramNotificationService class does not inherit from the BaseNotificationService class; no need for a coroutine (async_get_service) to get the TelegramNotificationService; dict creation.
|
I think this is the old one. |

Description:
When making inline keyboards with the
telegramnotify platform, if pressed, they send data like this:which isn't processed correctly with the current
telegram_botplatform. With these changes, when received, HA sends atelegram_callbackevent with the callback data, the chat instance, the original message and a unique id of the callback.Also, some changes in the
telegramnotifier for using more features of the Telegram API 2.0, like:disable_notification,disable_web_page_previewandreply_to_message_idoptional keyword args, or the inline keyboards withdata: {'inline_keyboard': [(text_btn1, data_callback_btn1), ...]}.target=[12345, 67890]) when sending a message.markdownandhtml) in any message with:data: {'parse_mode': 'html'}, with markdown as default, but can be globally customized with 'parse_mode' in the yaml config.data: {'callback_query'|'edit_message'| 'edit_caption'|'edit_replymarkup': ...}'{}\n{}'.format(title, message)With these changes it is now very easy to make a custom bot witch presents messages and buttons with posible responses, and change dynamically these messages and inline keyboards guiding the user in some sort of a wizard menu.
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.io#2508
Example entry for
configuration.yaml(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed: