Add send_message_draft action to telegram_bot (#165682)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Denis Shulyaka
2026-04-14 18:57:10 +03:00
committed by GitHub
parent d8c8f82c7e
commit 98b41d25f3
8 changed files with 178 additions and 0 deletions

View File

@@ -62,6 +62,7 @@ from .const import (
ATTR_DIRECTORY_PATH,
ATTR_DISABLE_NOTIF,
ATTR_DISABLE_WEB_PREV,
ATTR_DRAFT_ID,
ATTR_FILE,
ATTR_FILE_ID,
ATTR_FILE_NAME,
@@ -129,6 +130,7 @@ from .const import (
SERVICE_SEND_LOCATION,
SERVICE_SEND_MEDIA_GROUP,
SERVICE_SEND_MESSAGE,
SERVICE_SEND_MESSAGE_DRAFT,
SERVICE_SEND_PHOTO,
SERVICE_SEND_POLL,
SERVICE_SEND_STICKER,
@@ -176,6 +178,19 @@ SERVICE_SCHEMA_SEND_MESSAGE = vol.All(
),
)
SERVICE_SCHEMA_SEND_MESSAGE_DRAFT = vol.Schema(
{
vol.Optional(ATTR_ENTITY_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_CHAT_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(ATTR_MESSAGE_THREAD_ID): vol.Coerce(int),
vol.Required(ATTR_DRAFT_ID): vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_PARSER): ATTR_PARSER_SCHEMA,
}
)
SERVICE_SCHEMA_SEND_CHAT_ACTION = vol.All(
cv.deprecated(ATTR_TIMEOUT),
vol.Schema(
@@ -424,6 +439,7 @@ SERVICE_SCHEMA_DOWNLOAD_FILE = vol.Schema(
SERVICE_MAP: dict[str, VolSchemaType] = {
SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE,
SERVICE_SEND_MESSAGE_DRAFT: SERVICE_SCHEMA_SEND_MESSAGE_DRAFT,
SERVICE_SEND_CHAT_ACTION: SERVICE_SCHEMA_SEND_CHAT_ACTION,
SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE,
SERVICE_SEND_MEDIA_GROUP: SERVICE_SCHEMA_SEND_MEDIA_GROUP,
@@ -615,6 +631,8 @@ async def _call_service(
await notify_service.set_message_reaction(context=service.context, **kwargs)
elif service_name == SERVICE_EDIT_MESSAGE_MEDIA:
await notify_service.edit_message_media(context=service.context, **kwargs)
elif service_name == SERVICE_SEND_MESSAGE_DRAFT:
await notify_service.send_message_draft(context=service.context, **kwargs)
elif service_name == SERVICE_DOWNLOAD_FILE:
return await notify_service.download_file(context=service.context, **kwargs)
else:

View File

@@ -1013,6 +1013,36 @@ class TelegramNotificationService:
context=context,
)
async def send_message_draft(
self,
message: str,
chat_id: int,
draft_id: int,
context: Context | None = None,
**kwargs: dict[str, Any],
) -> None:
"""Stream a partial message to a user while the message is being generated."""
params = self._get_msg_kwargs(kwargs)
_LOGGER.debug(
"Sending message draft %s in chat ID %s with params: %s",
draft_id,
chat_id,
params,
)
await self._send_msg(
self.bot.send_message_draft,
None,
chat_id=chat_id,
draft_id=draft_id,
text=message,
message_thread_id=params[ATTR_MESSAGE_THREAD_ID],
parse_mode=params[ATTR_PARSER],
read_timeout=params[ATTR_TIMEOUT],
context=context,
)
async def download_file(
self,
file_id: str,

View File

@@ -31,6 +31,7 @@ DEFAULT_TRUSTED_NETWORKS = [ip_network("149.154.160.0/20"), ip_network("91.108.4
SERVICE_SEND_CHAT_ACTION = "send_chat_action"
SERVICE_SEND_MESSAGE = "send_message"
SERVICE_SEND_MESSAGE_DRAFT = "send_message_draft"
SERVICE_SEND_PHOTO = "send_photo"
SERVICE_SEND_MEDIA_GROUP = "send_media_group"
SERVICE_SEND_STICKER = "send_sticker"
@@ -90,6 +91,7 @@ ATTR_DATE = "date"
ATTR_DISABLE_NOTIF = "disable_notification"
ATTR_DISABLE_WEB_PREV = "disable_web_page_preview"
ATTR_DIRECTORY_PATH = "directory_path"
ATTR_DRAFT_ID = "draft_id"
ATTR_EDITED_MSG = "edited_message"
ATTR_FILE = "file"
ATTR_FILE_ID = "file_id"

View File

@@ -49,6 +49,9 @@
"send_message": {
"service": "mdi:send"
},
"send_message_draft": {
"service": "mdi:chat-processing"
},
"send_photo": {
"service": "mdi:camera"
},

View File

@@ -1198,3 +1198,50 @@ download_file:
example: "my_downloaded_file"
selector:
text:
send_message_draft:
fields:
entity_id:
selector:
entity:
filter:
domain: notify
integration: telegram_bot
multiple: true
reorder: true
message_thread_id:
selector:
number:
mode: box
draft_id:
required: true
selector:
number:
mode: box
min: 1
message:
example: The garage door has been o
required: true
selector:
text:
parse_mode:
selector:
select:
options:
- "html"
- "markdown"
- "markdownv2"
- "plain_text"
translation_key: "parse_mode"
advanced:
collapsed: true
fields:
config_entry_id:
selector:
config_entry:
integration: telegram_bot
chat_id:
example: "[12345, 67890] or 12345"
selector:
text:
multiple: true

View File

@@ -951,6 +951,45 @@
}
}
},
"send_message_draft": {
"description": "Stream a partial message to a user while the message is being generated.",
"fields": {
"chat_id": {
"description": "One or more pre-authorized chat IDs to send the message draft to.",
"name": "[%key:component::telegram_bot::services::edit_message::fields::chat_id::name%]"
},
"config_entry_id": {
"description": "The config entry representing the Telegram bot to send the message draft.",
"name": "[%key:component::telegram_bot::services::send_message::fields::config_entry_id::name%]"
},
"draft_id": {
"description": "Unique identifier of the message draft. Changes of drafts with the same identifier are animated.",
"name": "Draft ID"
},
"entity_id": {
"description": "[%key:component::telegram_bot::services::send_message::fields::entity_id::description%]",
"name": "[%key:component::telegram_bot::services::send_message::fields::entity_id::name%]"
},
"message": {
"description": "Available part of the message for temporary notification.\nCan't parse entities? Format your message according to the [formatting options]({formatting_options_url}).",
"name": "[%key:component::telegram_bot::services::send_message::fields::message::name%]"
},
"message_thread_id": {
"description": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::description%]",
"name": "[%key:component::telegram_bot::services::send_message::fields::message_thread_id::name%]"
},
"parse_mode": {
"description": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::description%]",
"name": "[%key:component::telegram_bot::services::send_message::fields::parse_mode::name%]"
}
},
"name": "Send message draft",
"sections": {
"advanced": {
"name": "[%key:component::telegram_bot::services::send_message::sections::advanced::name%]"
}
}
},
"send_photo": {
"description": "Sends a photo.",
"fields": {

View File

@@ -142,6 +142,7 @@ def mock_external_calls() -> Generator[None]:
patch.object(BotMock, "get_me", return_value=test_user),
patch.object(BotMock, "bot", test_user),
patch.object(BotMock, "send_message", return_value=message),
patch.object(BotMock, "send_message_draft", return_value=True),
patch.object(BotMock, "send_photo", return_value=message),
patch.object(BotMock, "send_media_group", side_effect=mock_send_media_group),
patch.object(BotMock, "send_sticker", return_value=message),

View File

@@ -1690,6 +1690,44 @@ async def test_set_message_reaction(
)
async def test_send_message_draft(
hass: HomeAssistant,
mock_broadcast_config_entry: MockConfigEntry,
mock_external_calls: None,
) -> None:
"""Test send message draft."""
mock_broadcast_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_broadcast_config_entry.entry_id)
await hass.async_block_till_done()
with patch(
"homeassistant.components.telegram_bot.bot.Bot.send_message_draft",
AsyncMock(return_value=True),
) as mock:
await hass.services.async_call(
DOMAIN,
"send_message_draft",
{
ATTR_CHAT_ID: 123456,
ATTR_MESSAGE: "_Thinking..._",
ATTR_MESSAGE_THREAD_ID: "123",
ATTR_PARSER: PARSER_MD2,
"draft_id": "3456",
},
blocking=True,
)
await hass.async_block_till_done()
mock.assert_called_once_with(
chat_id=123456,
draft_id=3456,
text="_Thinking..._",
message_thread_id=123,
parse_mode=PARSER_MD2,
read_timeout=None,
)
@pytest.mark.parametrize(
("service", "input"),
[