Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
001fe1c
Add download file service to Telegram bot integration
aviadlevy Oct 16, 2025
369027c
rename to directory_path
aviadlevy Oct 16, 2025
7e23945
paramaterize tests to cover user providing / not providing file_name
aviadlevy Oct 16, 2025
8102083
Add test for failed to get file
aviadlevy Oct 16, 2025
d9a30ea
add test user providing directory path
aviadlevy Oct 16, 2025
95ec8fa
Update default directory path for file downloads and add validation f…
aviadlevy Oct 17, 2025
43263e4
fix got an unexpected keyword argument 'config_entry_id'
aviadlevy Oct 17, 2025
542eb0d
Add handling for external directory paths in file download test
aviadlevy Oct 17, 2025
d327ee5
Merge remote-tracking branch 'origin' into download_file
aviadlevy Oct 17, 2025
413b260
Use DOMAIN constant
aviadlevy Oct 17, 2025
cde1749
fix description
aviadlevy Oct 17, 2025
e8455a4
Use native hass methods for directory handling
aviadlevy Oct 17, 2025
61326f2
make sure directory creation is async
aviadlevy Oct 17, 2025
3a25d16
do not create new temp folder on test
aviadlevy Oct 17, 2025
2ccd5d4
Add 1 more test case
aviadlevy Oct 17, 2025
4cac8bc
improve test slightly more
aviadlevy Oct 17, 2025
a8c185e
prevent potential race condition
aviadlevy Oct 18, 2025
ba5b8a1
Merge branch 'dev' into download_file
aviadlevy Oct 18, 2025
8968a78
Merge branch 'dev' into download_file
frenck Oct 18, 2025
159b048
Merge branch 'dev' into download_file
aviadlevy Oct 21, 2025
6b1220a
use better paramaterize so no need for conditions
aviadlevy Oct 21, 2025
b84b0f2
try-except the download action
aviadlevy Oct 21, 2025
223c7e0
Merge branch 'dev' into download_file
aviadlevy Oct 24, 2025
d67b9dd
Merge branch 'dev' into download_file
aviadlevy Oct 25, 2025
019dbcd
Merge branch 'dev' into download_file
aviadlevy Oct 25, 2025
e8a62cc
Merge remote-tracking branch 'origin/dev' into download_file
aviadlevy Oct 29, 2025
fc72351
Merge branch 'dev' into download_file
aviadlevy Oct 30, 2025
d1bf419
Merge branch 'dev' into download_file
aviadlevy Oct 31, 2025
8978426
Merge branch 'dev' into download_file
aviadlevy Nov 2, 2025
3b09c16
Improve code to make more natively async
aviadlevy Nov 4, 2025
a3b31a7
Merge branch 'dev' into download_file
aviadlevy Nov 4, 2025
e149e2f
Merge branch 'dev' into download_file
aviadlevy Nov 9, 2025
bd0de06
Merge branch 'dev' into download_file
aviadlevy Nov 12, 2025
771e9f6
Merge branch 'dev' into download_file
aviadlevy Nov 17, 2025
5c5ef41
Merge remote-tracking branch 'origin/dev' into download_file
aviadlevy Nov 27, 2025
0784704
Merge branch 'dev' into download_file
aviadlevy Nov 29, 2025
76ada0e
Merge branch 'dev' into download_file
aviadlevy Nov 30, 2025
f29f7d8
Merge remote-tracking branch 'origin/dev' into download_file
aviadlevy Dec 15, 2025
c2a2a22
Merge branch 'download_file' of github.com:aviadlevy/core into downlo…
aviadlevy Dec 15, 2025
4119222
solve PR comments
aviadlevy Dec 15, 2025
b01f529
Merge branch 'dev' into download_file
aviadlevy Dec 15, 2025
c7b0cdf
better I/O handling
aviadlevy Dec 15, 2025
4473fa8
debug why fail only on CI
aviadlevy Dec 15, 2025
24010ef
Updated test_telegram_bot.py to add the resolved Path to the allowlist
aviadlevy Dec 15, 2025
522b325
keep directory for tests
aviadlevy Dec 15, 2025
5ea4fc0
create missing test telegram config dir if not exists
aviadlevy Dec 15, 2025
adeda1e
Merge branch 'dev' into download_file
aviadlevy Dec 15, 2025
d4d3ff6
better handle dir creation and add test for not exists dir
aviadlevy Dec 16, 2025
0542ddf
use tmp_path fixture
aviadlevy Dec 16, 2025
7cb9a0c
Merge branch 'dev' into download_file
aviadlevy Dec 16, 2025
d6864ec
Capitallize and remove comment
aviadlevy Dec 17, 2025
d2e176f
split custom / no-custom dir tests
aviadlevy Dec 17, 2025
5256307
Set response support to ONLY for SERVICE_DOWNLOAD_FILE
aviadlevy Dec 17, 2025
47004a4
set return response true for download_file tests
aviadlevy Dec 17, 2025
0aea16f
download file service to SupportsResponse.OPTIONAL
aviadlevy Dec 17, 2025
7bc69f0
Add ServiceResponse to download_file service tests and validate respo…
aviadlevy Dec 17, 2025
9784416
extract common assertions
aviadlevy Dec 17, 2025
9cd46fe
Catch also TelegramError
aviadlevy Dec 18, 2025
4d0aa4f
Add more test coverage for errors during file download
aviadlevy Dec 18, 2025
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
17 changes: 17 additions & 0 deletions homeassistant/components/telegram_bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@
ATTR_CAPTION,
ATTR_CHAT_ACTION,
ATTR_CHAT_ID,
ATTR_DIRECTORY_PATH,
ATTR_DISABLE_NOTIF,
ATTR_DISABLE_WEB_PREV,
ATTR_FILE,
ATTR_FILE_ID,
ATTR_FILE_NAME,
ATTR_IS_ANONYMOUS,
ATTR_IS_BIG,
ATTR_KEYBOARD,
Expand Down Expand Up @@ -90,6 +93,7 @@
PLATFORM_WEBHOOKS,
SERVICE_ANSWER_CALLBACK_QUERY,
SERVICE_DELETE_MESSAGE,
SERVICE_DOWNLOAD_FILE,
SERVICE_EDIT_CAPTION,
SERVICE_EDIT_MESSAGE,
SERVICE_EDIT_MESSAGE_MEDIA,
Expand Down Expand Up @@ -312,6 +316,15 @@
}
)

SERVICE_SCHEMA_DOWNLOAD_FILE = vol.Schema(
{
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
vol.Required(ATTR_FILE_ID): cv.string,
vol.Optional(ATTR_DIRECTORY_PATH): cv.string,
vol.Optional(ATTR_FILE_NAME): cv.string,
}
)

SERVICE_MAP: dict[str, VolSchemaType] = {
SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE,
SERVICE_SEND_CHAT_ACTION: SERVICE_SCHEMA_SEND_CHAT_ACTION,
Expand All @@ -331,6 +344,7 @@
SERVICE_DELETE_MESSAGE: SERVICE_SCHEMA_DELETE_MESSAGE,
SERVICE_LEAVE_CHAT: SERVICE_SCHEMA_LEAVE_CHAT,
SERVICE_SET_MESSAGE_REACTION: SERVICE_SCHEMA_SET_MESSAGE_REACTION,
SERVICE_DOWNLOAD_FILE: SERVICE_SCHEMA_DOWNLOAD_FILE,
}


Expand Down Expand Up @@ -426,6 +440,8 @@ async def async_send_telegram_message(service: ServiceCall) -> ServiceResponse:
await notify_service.set_message_reaction(context=service.context, **kwargs)
elif msgtype == SERVICE_EDIT_MESSAGE_MEDIA:
await notify_service.edit_message_media(context=service.context, **kwargs)
elif msgtype == SERVICE_DOWNLOAD_FILE:
return await notify_service.download_file(context=service.context, **kwargs)
Comment thread
MartinHjelmare marked this conversation as resolved.
else:
await notify_service.edit_message(
msgtype, context=service.context, **kwargs
Expand Down Expand Up @@ -471,6 +487,7 @@ async def async_send_telegram_message(service: ServiceCall) -> ServiceResponse:
SERVICE_SEND_STICKER,
SERVICE_SEND_LOCATION,
SERVICE_SEND_POLL,
SERVICE_DOWNLOAD_FILE,
]:
supports_response = SupportsResponse.OPTIONAL

Expand Down
67 changes: 67 additions & 0 deletions homeassistant/components/telegram_bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from collections.abc import Callable, Sequence
import io
import logging
import os
from pathlib import Path
from ssl import SSLContext
from types import MappingProxyType
from typing import Any, cast
Expand All @@ -13,6 +15,7 @@
from telegram import (
Bot,
CallbackQuery,
File,
InlineKeyboardButton,
InlineKeyboardMarkup,
InputMedia,
Expand Down Expand Up @@ -45,6 +48,7 @@
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.json import JsonValueType
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context

from .const import (
Expand All @@ -61,6 +65,7 @@
ATTR_FILE_ID,
ATTR_FILE_MIME_TYPE,
ATTR_FILE_NAME,
ATTR_FILE_PATH,
ATTR_FILE_SIZE,
ATTR_FROM_FIRST,
ATTR_FROM_LAST,
Expand Down Expand Up @@ -1036,6 +1041,68 @@ async def set_message_reaction(
context=context,
)

async def download_file(
self,
file_id: str,
directory_path: str | None = None,
file_name: str | None = None,
context: Context | None = None,
**kwargs: dict[str, Any],
) -> dict[str, JsonValueType]:
"""Download a file from Telegram."""
if not directory_path:
directory_path = self.hass.config.path(DOMAIN)
if not await self.hass.async_add_executor_job(
self.hass.config.is_allowed_path, directory_path
Comment thread
MartinHjelmare marked this conversation as resolved.
):
raise ServiceValidationError(
Comment thread
MartinHjelmare marked this conversation as resolved.
"File path has not been configured in allowlist_external_dirs.",
translation_domain=DOMAIN,
translation_key="allowlist_external_dirs_error",
)
file: File = await self._send_msg(
Comment thread
aviadlevy marked this conversation as resolved.
self.bot.get_file,
"Error getting file",
None,
file_id=file_id,
context=context,
)
Comment thread
aviadlevy marked this conversation as resolved.
if not file.file_path:
raise HomeAssistantError(
Comment thread
MartinHjelmare marked this conversation as resolved.
translation_domain=DOMAIN,
translation_key="action_failed",
translation_placeholders={
"error": "No file path returned from Telegram"
},
)
if not file_name:
file_name = os.path.basename(file.file_path)

custom_path = os.path.join(directory_path, file_name)
await self.hass.async_add_executor_job(
self._prepare_download_directory, directory_path
)
_LOGGER.debug("Download file %s to %s", file_id, custom_path)
try:
file_content = await file.download_as_bytearray()
await self.hass.async_add_executor_job(
Path(custom_path).write_bytes, file_content
)
except (RuntimeError, OSError, TelegramError) as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="action_failed",
translation_placeholders={"error": str(exc)},
) from exc
return {ATTR_FILE_PATH: custom_path}

@staticmethod
def _prepare_download_directory(directory_path: str) -> None:
"""Create download directory if it does not exist."""
if not os.path.exists(directory_path):
_LOGGER.debug("directory %s does not exist, creating it", directory_path)
os.makedirs(directory_path, exist_ok=True)


def initialize_bot(hass: HomeAssistant, p_config: MappingProxyType[str, Any]) -> Bot:
"""Initialize telegram bot with proxy support."""
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/telegram_bot/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
SERVICE_ANSWER_CALLBACK_QUERY = "answer_callback_query"
SERVICE_DELETE_MESSAGE = "delete_message"
SERVICE_LEAVE_CHAT = "leave_chat"
SERVICE_DOWNLOAD_FILE = "download_file"

SIGNAL_UPDATE_EVENT = "telegram_bot_update_event"
EVENT_TELEGRAM_CALLBACK = "telegram_callback"
Expand Down Expand Up @@ -83,9 +84,11 @@
ATTR_DATE = "date"
ATTR_DISABLE_NOTIF = "disable_notification"
ATTR_DISABLE_WEB_PREV = "disable_web_page_preview"
ATTR_DIRECTORY_PATH = "directory_path"
ATTR_EDITED_MSG = "edited_message"
ATTR_FILE = "file"
ATTR_FILE_ID = "file_id"
ATTR_FILE_PATH = "file_path"
ATTR_FILE_MIME_TYPE = "file_mime_type"
ATTR_FILE_NAME = "file_name"
ATTR_FILE_SIZE = "file_size"
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/telegram_bot/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"delete_message": {
"service": "mdi:delete"
},
"download_file": {
"service": "mdi:paperclip"
},
"edit_caption": {
"service": "mdi:pencil"
},
Expand Down
22 changes: 22 additions & 0 deletions homeassistant/components/telegram_bot/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -911,3 +911,25 @@ set_message_reaction:
required: false
selector:
boolean:

download_file:
fields:
config_entry_id:
selector:
config_entry:
integration: telegram_bot
file_id:
required: true
example: "ABCD1234Efgh5678Ijkl90mnopQRStuvwx"
selector:
text:
directory_path:
required: false
default: "/config/telegram_bot"
selector:
text:
file_name:
required: false
example: "my_downloaded_file"
selector:
text:
23 changes: 23 additions & 0 deletions homeassistant/components/telegram_bot/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,29 @@
},
"name": "Delete message"
},
"download_file": {
"description": "Download the file to a local path.",
"fields": {
"config_entry_id": {
"description": "The config entry representing the Telegram bot to get the file.",
"name": "[%key:component::telegram_bot::services::send_message::fields::config_entry_id::name%]"
},
"directory_path": {
"description": "Local directory path to save the file to. Defaults to the 'telegram_bot' directory within your Home Assistant configuration directory.",
"example": "/config/telegram_bot",
"name": "Directory path"
},
"file_id": {
"description": "ID of the file to get.",
"name": "File ID"
},
"file_name": {
"description": "Name to save the file as. If not provided, the original file name will be used.",
"name": "File name"
}
},
"name": "Download file"
},
"edit_caption": {
"description": "Edits the caption of a previously sent message.",
"fields": {
Expand Down
Loading
Loading