Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion homeassistant/components/telegram_bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import logging
from ssl import SSLContext
from types import MappingProxyType
from typing import Any
from typing import Any, cast

import httpx
from telegram import (
Expand All @@ -23,6 +23,7 @@
InputMediaVideo,
InputPollOption,
Message,
PhotoSize,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
Update,
Expand Down Expand Up @@ -56,6 +57,10 @@
ATTR_DISABLE_NOTIF,
ATTR_DISABLE_WEB_PREV,
ATTR_FILE,
ATTR_FILE_ID,
ATTR_FILE_MIME_TYPE,
ATTR_FILE_NAME,
ATTR_FILE_SIZE,
ATTR_FROM_FIRST,
ATTR_FROM_LAST,
ATTR_INLINE_MESSAGE_ID,
Expand Down Expand Up @@ -85,6 +90,7 @@
CONF_CHAT_ID,
CONF_PROXY_URL,
DOMAIN,
EVENT_TELEGRAM_ATTACHMENT,
EVENT_TELEGRAM_CALLBACK,
EVENT_TELEGRAM_COMMAND,
EVENT_TELEGRAM_SENT,
Expand Down Expand Up @@ -182,6 +188,10 @@ def _get_message_event_data(self, message: Message) -> tuple[str, dict[str, Any]
# This is a command message - set event type to command and split data into command and args
event_type = EVENT_TELEGRAM_COMMAND
event_data.update(self._get_command_event_data(message.text))
elif filters.ATTACHMENT.filter(message):
event_type = EVENT_TELEGRAM_ATTACHMENT
event_data[ATTR_TEXT] = message.caption
event_data.update(self._get_file_id_event_data(message))
else:
event_type = EVENT_TELEGRAM_TEXT
event_data[ATTR_TEXT] = message.text
Expand All @@ -191,6 +201,26 @@ def _get_message_event_data(self, message: Message) -> tuple[str, dict[str, Any]

return event_type, event_data

def _get_file_id_event_data(self, message: Message) -> dict[str, Any]:
"""Extract file_id from a message attachment, if any."""
if filters.PHOTO.filter(message):
photos = cast(Sequence[PhotoSize], message.effective_attachment)
return {
ATTR_FILE_ID: photos[-1].file_id,
Comment thread
aviadlevy marked this conversation as resolved.
ATTR_FILE_MIME_TYPE: "image/jpeg", # telegram always uses jpeg for photos
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states 'telegram always uses jpeg for photos' but this is incorrect. Telegram can deliver photos in WebP format or JPEG depending on the client and API version. Consider using a more accurate comment like 'Telegram typically delivers photos as JPEG' or remove the hardcoded mime type and use getattr with a fallback.

Suggested change
ATTR_FILE_MIME_TYPE: "image/jpeg", # telegram always uses jpeg for photos
ATTR_FILE_MIME_TYPE: getattr(photos[-1], "mime_type", "image/jpeg"), # Telegram typically delivers photos as JPEG, but other formats are possible

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@aviadlevy aviadlevy Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PhotoSize does not hold mime_type fields. other formats other than JPEG is only available as Document.
https://stackoverflow.com/a/64927595

ATTR_FILE_SIZE: photos[-1].file_size,
}
return {
k: getattr(message.effective_attachment, v)
for k, v in (
(ATTR_FILE_ID, "file_id"),
(ATTR_FILE_NAME, "file_name"),
(ATTR_FILE_MIME_TYPE, "mime_type"),
Comment thread
aviadlevy marked this conversation as resolved.
(ATTR_FILE_SIZE, "file_size"),
)
if hasattr(message.effective_attachment, v)
}
Comment thread
aviadlevy marked this conversation as resolved.
Comment on lines +213 to +222
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The dictionary comprehension silently skips attributes that don't exist using hasattr. This could mask issues if expected attributes are missing. Consider logging when attributes are missing or documenting which attachment types have which attributes to make the behavior more transparent.

Suggested change
return {
k: getattr(message.effective_attachment, v)
for k, v in (
(ATTR_FILE_ID, "file_id"),
(ATTR_FILE_NAME, "file_name"),
(ATTR_FILE_MIME_TYPE, "mime_type"),
(ATTR_FILE_SIZE, "file_size"),
)
if hasattr(message.effective_attachment, v)
}
result = {}
for k, v in (
(ATTR_FILE_ID, "file_id"),
(ATTR_FILE_NAME, "file_name"),
(ATTR_FILE_MIME_TYPE, "mime_type"),
(ATTR_FILE_SIZE, "file_size"),
):
if hasattr(message.effective_attachment, v):
result[k] = getattr(message.effective_attachment, v)
else:
_LOGGER.debug(
"Attachment of type %s is missing expected attribute '%s'",
type(message.effective_attachment).__name__,
v,
)
return result

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't think it's necessary to have that log, but willing to change that if anyone think otherwise


def _get_user_event_data(self, user: User) -> dict[str, Any]:
return {
ATTR_USER_ID: user.id,
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/telegram_bot/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
EVENT_TELEGRAM_CALLBACK = "telegram_callback"
EVENT_TELEGRAM_COMMAND = "telegram_command"
EVENT_TELEGRAM_TEXT = "telegram_text"
EVENT_TELEGRAM_ATTACHMENT = "telegram_attachment"
EVENT_TELEGRAM_SENT = "telegram_sent"

PARSER_HTML = "html"
Expand Down Expand Up @@ -90,6 +91,10 @@
ATTR_DISABLE_WEB_PREV = "disable_web_page_preview"
ATTR_EDITED_MSG = "edited_message"
ATTR_FILE = "file"
ATTR_FILE_ID = "file_id"
ATTR_FILE_MIME_TYPE = "file_mime_type"
ATTR_FILE_NAME = "file_name"
ATTR_FILE_SIZE = "file_size"
ATTR_FROM_FIRST = "from_first"
ATTR_FROM_LAST = "from_last"
ATTR_KEYBOARD = "keyboard"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"update_id": 2,
"message": {
"message_id": 2,
"date": 1441645532,
"from": {
"id": 12345678,
"is_bot": false,
"last_name": "Test Lastname",
"first_name": "Test Firstname",
"username": "Testusername"
},
"chat": {
"last_name": "Test Lastname",
"id": 1111111,
"type": "private",
"first_name": "Test Firstname",
"username": "Testusername"
},
"document": {
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
"file_unique_id": "AQADcbzEbT-5xuBa",
"mime_type": "application/pdf",
"file_size": 123456
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"update_id": 1,
"message": {
"message_id": 1,
"date": 1441645532,
"from": {
"id": 12345678,
"is_bot": false,
"last_name": "Test Lastname",
"first_name": "Test Firstname",
"username": "Testusername"
},
"chat": {
"last_name": "Test Lastname",
"id": 1111111,
"type": "private",
"first_name": "Test Firstname",
"username": "Testusername"
},
"photo": [
{
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
"file_unique_id": "AQADcbzEbT-5xuBa",
"file_size": 1234,
"width": 90,
"height": 90
},
{
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
"file_unique_id": "AQADcbzEbT-5xuBa",
"file_size": 12345,
"width": 320,
"height": 320
},
{
"file_id": "AgACAgUAAxkBAAIBUWJ5aXl6Y3h5bXl6Y3h5bXl6Y3gAAJxvMRtP7nG4Fq7m0m0vBAAMCAAN5AAMjBA",
"file_unique_id": "AQADcbzEbT-5xuBa",
"file_size": 123456,
"width": 800,
"height": 800
}
]
}
}
49 changes: 48 additions & 1 deletion tests/components/telegram_bot/test_telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@
ServiceValidationError,
)
from homeassistant.setup import async_setup_component
from homeassistant.util import json as json_util
from homeassistant.util.file import write_utf8_file

from tests.common import MockConfigEntry, async_capture_events
from tests.common import MockConfigEntry, async_capture_events, async_load_fixture
from tests.typing import ClientSessionGenerator


Expand Down Expand Up @@ -478,6 +479,52 @@ async def test_webhook_endpoint_generates_telegram_callback_event(
assert isinstance(events[0].context, Context)


@pytest.mark.parametrize(
("attachment_type"),
[
("photo"),
("document"),
],
)
async def test_webhook_endpoint_generates_telegram_attachment_event(
hass: HomeAssistant,
webhook_platform: None,
hass_client: ClientSessionGenerator,
mock_generate_secret_token: str,
attachment_type: str,
) -> None:
"""POST to the configured webhook endpoint and assert fired `telegram_attachment` event for photo and document."""
client = await hass_client()
events = async_capture_events(hass, "telegram_attachment")
update_message_attachment = await async_load_fixture(
hass, f"update_message_attachment_{attachment_type}.json", DOMAIN
)

response = await client.post(
f"{TELEGRAM_WEBHOOK_URL}_123456",
data=update_message_attachment,
headers={
"X-Telegram-Bot-Api-Secret-Token": mock_generate_secret_token,
"Content-Type": "application/json",
},
)
assert response.status == 200
assert (await response.read()).decode("utf-8") == ""

# Make sure event has fired
Comment thread
aviadlevy marked this conversation as resolved.
await hass.async_block_till_done()

assert len(events) == 1
loaded = json_util.json_loads(update_message_attachment)
if attachment_type == "photo":
expected_file_id = loaded["message"]["photo"][-1]["file_id"]
else:
expected_file_id = loaded["message"][attachment_type]["file_id"]

assert events[0].data["file_id"] == expected_file_id
assert isinstance(events[0].context, Context)


async def test_polling_platform_message_text_update(
hass: HomeAssistant,
config_polling,
Expand Down
Loading