Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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 @@ -17,6 +17,7 @@
InlineKeyboardMarkup,
InputPollOption,
Message,
PhotoSize,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
Update,
Expand Down Expand Up @@ -50,6 +51,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_KEYBOARD,
Expand Down Expand Up @@ -78,6 +83,7 @@
CONF_CHAT_ID,
CONF_PROXY_URL,
DOMAIN,
EVENT_TELEGRAM_ATTACHMENT,
EVENT_TELEGRAM_CALLBACK,
EVENT_TELEGRAM_COMMAND,
EVENT_TELEGRAM_SENT,
Expand Down Expand Up @@ -175,6 +181,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 @@ -184,6 +194,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

Copilot AI Oct 16, 2025

Copy link

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.

@aviadlevy aviadlevy Oct 16, 2025

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.

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

Copilot AI Oct 16, 2025

Copy link

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 @@ -53,6 +53,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 @@ -89,6 +90,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
49 changes: 49 additions & 0 deletions tests/components/telegram_bot/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,55 @@ def update_callback_query():
}


@pytest.fixture
def update_message_attachment():
"""Fixture for mocking an incoming update of type message/attachment."""
return {
"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,
},
],
},
}
Comment thread
aviadlevy marked this conversation as resolved.
Outdated


@pytest.fixture
def mock_broadcast_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
Expand Down
30 changes: 30 additions & 0 deletions tests/components/telegram_bot/test_telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,36 @@ async def test_webhook_endpoint_generates_telegram_callback_event(
assert isinstance(events[0].context, Context)


async def test_webhook_endpoint_generates_telegram_attachment_event(
hass: HomeAssistant,
webhook_platform,
hass_client: ClientSessionGenerator,
update_message_attachment,
mock_generate_secret_token,
Comment thread
aviadlevy marked this conversation as resolved.
Outdated
) -> None:
"""POST to the configured webhook endpoint and assert fired `telegram_attachment` event."""
client = await hass_client()
events = async_capture_events(hass, "telegram_attachment")

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

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

assert len(events) == 1
assert (
events[0].data["file_id"]
== update_message_attachment["message"]["photo"][-1]["file_id"]
)
assert isinstance(events[0].context, Context)


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