Skip to content

Commit 6038f15

Browse files
authored
Add support for Telegram message attachments (home-assistant#153216)
1 parent a875825 commit 6038f15

File tree

5 files changed

+155
-2
lines changed

5 files changed

+155
-2
lines changed

homeassistant/components/telegram_bot/bot.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
from ssl import SSLContext
99
from types import MappingProxyType
10-
from typing import Any
10+
from typing import Any, cast
1111

1212
import httpx
1313
from telegram import (
@@ -23,6 +23,7 @@
2323
InputMediaVideo,
2424
InputPollOption,
2525
Message,
26+
PhotoSize,
2627
ReplyKeyboardMarkup,
2728
ReplyKeyboardRemove,
2829
Update,
@@ -56,6 +57,10 @@
5657
ATTR_DISABLE_NOTIF,
5758
ATTR_DISABLE_WEB_PREV,
5859
ATTR_FILE,
60+
ATTR_FILE_ID,
61+
ATTR_FILE_MIME_TYPE,
62+
ATTR_FILE_NAME,
63+
ATTR_FILE_SIZE,
5964
ATTR_FROM_FIRST,
6065
ATTR_FROM_LAST,
6166
ATTR_INLINE_MESSAGE_ID,
@@ -86,6 +91,7 @@
8691
CONF_CHAT_ID,
8792
CONF_PROXY_URL,
8893
DOMAIN,
94+
EVENT_TELEGRAM_ATTACHMENT,
8995
EVENT_TELEGRAM_CALLBACK,
9096
EVENT_TELEGRAM_COMMAND,
9197
EVENT_TELEGRAM_SENT,
@@ -183,6 +189,10 @@ def _get_message_event_data(self, message: Message) -> tuple[str, dict[str, Any]
183189
# This is a command message - set event type to command and split data into command and args
184190
event_type = EVENT_TELEGRAM_COMMAND
185191
event_data.update(self._get_command_event_data(message.text))
192+
elif filters.ATTACHMENT.filter(message):
193+
event_type = EVENT_TELEGRAM_ATTACHMENT
194+
event_data[ATTR_TEXT] = message.caption
195+
event_data.update(self._get_file_id_event_data(message))
186196
else:
187197
event_type = EVENT_TELEGRAM_TEXT
188198
event_data[ATTR_TEXT] = message.text
@@ -192,6 +202,26 @@ def _get_message_event_data(self, message: Message) -> tuple[str, dict[str, Any]
192202

193203
return event_type, event_data
194204

205+
def _get_file_id_event_data(self, message: Message) -> dict[str, Any]:
206+
"""Extract file_id from a message attachment, if any."""
207+
if filters.PHOTO.filter(message):
208+
photos = cast(Sequence[PhotoSize], message.effective_attachment)
209+
return {
210+
ATTR_FILE_ID: photos[-1].file_id,
211+
ATTR_FILE_MIME_TYPE: "image/jpeg", # telegram always uses jpeg for photos
212+
ATTR_FILE_SIZE: photos[-1].file_size,
213+
}
214+
return {
215+
k: getattr(message.effective_attachment, v)
216+
for k, v in (
217+
(ATTR_FILE_ID, "file_id"),
218+
(ATTR_FILE_NAME, "file_name"),
219+
(ATTR_FILE_MIME_TYPE, "mime_type"),
220+
(ATTR_FILE_SIZE, "file_size"),
221+
)
222+
if hasattr(message.effective_attachment, v)
223+
}
224+
195225
def _get_user_event_data(self, user: User) -> dict[str, Any]:
196226
return {
197227
ATTR_USER_ID: user.id,

homeassistant/components/telegram_bot/const.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
EVENT_TELEGRAM_CALLBACK = "telegram_callback"
5555
EVENT_TELEGRAM_COMMAND = "telegram_command"
5656
EVENT_TELEGRAM_TEXT = "telegram_text"
57+
EVENT_TELEGRAM_ATTACHMENT = "telegram_attachment"
5758
EVENT_TELEGRAM_SENT = "telegram_sent"
5859

5960
PARSER_HTML = "html"
@@ -90,6 +91,10 @@
9091
ATTR_DISABLE_WEB_PREV = "disable_web_page_preview"
9192
ATTR_EDITED_MSG = "edited_message"
9293
ATTR_FILE = "file"
94+
ATTR_FILE_ID = "file_id"
95+
ATTR_FILE_MIME_TYPE = "file_mime_type"
96+
ATTR_FILE_NAME = "file_name"
97+
ATTR_FILE_SIZE = "file_size"
9398
ATTR_FROM_FIRST = "from_first"
9499
ATTR_FROM_LAST = "from_last"
95100
ATTR_KEYBOARD = "keyboard"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"update_id": 2,
3+
"message": {
4+
"message_id": 2,
5+
"date": 1441645532,
6+
"from": {
7+
"id": 12345678,
8+
"is_bot": false,
9+
"last_name": "Test Lastname",
10+
"first_name": "Test Firstname",
11+
"username": "Testusername"
12+
},
13+
"chat": {
14+
"last_name": "Test Lastname",
15+
"id": 1111111,
16+
"type": "private",
17+
"first_name": "Test Firstname",
18+
"username": "Testusername"
19+
},
20+
"document": {
21+
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
22+
"file_unique_id": "AQADcbzEbT-5xuBa",
23+
"mime_type": "application/pdf",
24+
"file_size": 123456
25+
}
26+
}
27+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"update_id": 1,
3+
"message": {
4+
"message_id": 1,
5+
"date": 1441645532,
6+
"from": {
7+
"id": 12345678,
8+
"is_bot": false,
9+
"last_name": "Test Lastname",
10+
"first_name": "Test Firstname",
11+
"username": "Testusername"
12+
},
13+
"chat": {
14+
"last_name": "Test Lastname",
15+
"id": 1111111,
16+
"type": "private",
17+
"first_name": "Test Firstname",
18+
"username": "Testusername"
19+
},
20+
"photo": [
21+
{
22+
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
23+
"file_unique_id": "AQADcbzEbT-5xuBa",
24+
"file_size": 1234,
25+
"width": 90,
26+
"height": 90
27+
},
28+
{
29+
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
30+
"file_unique_id": "AQADcbzEbT-5xuBa",
31+
"file_size": 12345,
32+
"width": 320,
33+
"height": 320
34+
},
35+
{
36+
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
37+
"file_unique_id": "AQADcbzEbT-5xuBa",
38+
"file_size": 123456,
39+
"width": 800,
40+
"height": 800
41+
}
42+
]
43+
}
44+
}

tests/components/telegram_bot/test_telegram_bot.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,10 @@
9191
ServiceValidationError,
9292
)
9393
from homeassistant.setup import async_setup_component
94+
from homeassistant.util import json as json_util
9495
from homeassistant.util.file import write_utf8_file
9596

96-
from tests.common import MockConfigEntry, async_capture_events
97+
from tests.common import MockConfigEntry, async_capture_events, async_load_fixture
9798
from tests.typing import ClientSessionGenerator
9899

99100

@@ -565,6 +566,52 @@ async def test_webhook_endpoint_generates_telegram_callback_event(
565566
assert isinstance(events[0].context, Context)
566567

567568

569+
@pytest.mark.parametrize(
570+
("attachment_type"),
571+
[
572+
("photo"),
573+
("document"),
574+
],
575+
)
576+
async def test_webhook_endpoint_generates_telegram_attachment_event(
577+
hass: HomeAssistant,
578+
webhook_platform: None,
579+
hass_client: ClientSessionGenerator,
580+
mock_generate_secret_token: str,
581+
attachment_type: str,
582+
) -> None:
583+
"""POST to the configured webhook endpoint and assert fired `telegram_attachment` event for photo and document."""
584+
client = await hass_client()
585+
events = async_capture_events(hass, "telegram_attachment")
586+
update_message_attachment = await async_load_fixture(
587+
hass, f"update_message_attachment_{attachment_type}.json", DOMAIN
588+
)
589+
590+
response = await client.post(
591+
f"{TELEGRAM_WEBHOOK_URL}_123456",
592+
data=update_message_attachment,
593+
headers={
594+
"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token,
595+
"Content-Type": "application/json",
596+
},
597+
)
598+
assert response.status == 200
599+
assert (await response.read()).decode("utf-8") == ""
600+
601+
# Make sure event has fired
602+
await hass.async_block_till_done()
603+
604+
assert len(events) == 1
605+
loaded = json_util.json_loads(update_message_attachment)
606+
if attachment_type == "photo":
607+
expected_file_id = loaded["message"]["photo"][-1]["file_id"]
608+
else:
609+
expected_file_id = loaded["message"][attachment_type]["file_id"]
610+
611+
assert events[0].data["file_id"] == expected_file_id
612+
assert isinstance(events[0].context, Context)
613+
614+
568615
async def test_polling_platform_message_text_update(
569616
hass: HomeAssistant,
570617
config_polling,

0 commit comments

Comments
 (0)