diff --git a/README.md b/README.md index 3531df3..8a06d73 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ A Home Assistant custom integration that monitors [Immich](https://immich.app/) - **Services** - Custom service calls: - `immich_album_watcher.refresh` - Force immediate data refresh - `immich_album_watcher.get_recent_assets` - Get recent assets from an album - - `immich_album_watcher.send_telegram_media_group` - Send media to Telegram chats + - `immich_album_watcher.send_telegram_notification` - Send text, photo, video, or media group to Telegram - **Share Link Management** - Button entities to create and delete share links: - Create/delete public (unprotected) share links - Create/delete password-protected share links @@ -115,12 +115,49 @@ data: count: 10 ``` -### Send Telegram Media Group +### Send Telegram Notification -Send media URLs to a Telegram chat as a media group (up to 10 items). Useful for forwarding album photos/videos to Telegram. The service downloads media from Immich and uploads it to Telegram, bypassing any CORS restrictions. +Send notifications to Telegram. Supports multiple formats: + +- **Text message** - When `urls` is empty or not provided +- **Single photo** - When `urls` contains one photo +- **Single video** - When `urls` contains one video +- **Media group** - When `urls` contains multiple items + +The service downloads media from Immich and uploads it to Telegram, bypassing any CORS restrictions. Large lists of media are automatically split into multiple media groups based on the `max_group_size` parameter (default: 10 items per group). + +**Examples:** + +Text message: ```yaml -service: immich_album_watcher.send_telegram_media_group +service: immich_album_watcher.send_telegram_notification +target: + entity_id: sensor.album_name_asset_count +data: + chat_id: "-1001234567890" + caption: "Check out the new album!" + disable_web_page_preview: true +``` + +Single photo: + +```yaml +service: immich_album_watcher.send_telegram_notification +target: + entity_id: sensor.album_name_asset_count +data: + chat_id: "-1001234567890" + urls: + - url: "https://immich.example.com/api/assets/xxx/thumbnail?key=yyy" + type: photo + caption: "Beautiful sunset!" +``` + +Media group: + +```yaml +service: immich_album_watcher.send_telegram_notification target: entity_id: sensor.album_name_asset_count data: @@ -131,18 +168,21 @@ data: - url: "https://immich.example.com/api/assets/zzz/video/playback?key=yyy" type: video caption: "New photos from the album!" - reply_to_message_id: 123 # optional + reply_to_message_id: 123 ``` | Field | Description | Required | |-------|-------------|----------| | `chat_id` | Telegram chat ID to send to | Yes | -| `urls` | List of media items with `url` and `type` (photo/video) | Yes | +| `urls` | List of media items with `url` and `type` (photo/video). Empty for text message. | No | | `bot_token` | Telegram bot token (uses configured token if not provided) | No | -| `caption` | Caption for the media group (applied to first item) | No | +| `caption` | For media: caption applied to first item. For text: the message text. | No | | `reply_to_message_id` | Message ID to reply to | No | +| `disable_web_page_preview` | Disable link previews in text messages | No | +| `max_group_size` | Maximum media items per group (2-10). Large lists split into multiple groups. Default: 10 | No | +| `chunk_delay` | Delay in milliseconds between sending multiple groups (0-60000). Useful for rate limiting. Default: 0 | No | -The service returns a response with `success` status and `message_ids` of sent messages. +The service returns a response with `success` status and `message_id` (single message), `message_ids` (media group), or `groups_sent` (number of groups when split). ## Events diff --git a/custom_components/immich_album_watcher/const.py b/custom_components/immich_album_watcher/const.py index ca964c9..c796824 100644 --- a/custom_components/immich_album_watcher/const.py +++ b/custom_components/immich_album_watcher/const.py @@ -70,4 +70,4 @@ PLATFORMS: Final = ["sensor", "binary_sensor", "camera", "text", "button"] # Services SERVICE_REFRESH: Final = "refresh" SERVICE_GET_RECENT_ASSETS: Final = "get_recent_assets" -SERVICE_SEND_TELEGRAM_MEDIA_GROUP: Final = "send_telegram_media_group" +SERVICE_SEND_TELEGRAM_NOTIFICATION: Final = "send_telegram_notification" diff --git a/custom_components/immich_album_watcher/sensor.py b/custom_components/immich_album_watcher/sensor.py index 17ec7cc..244efdb 100644 --- a/custom_components/immich_album_watcher/sensor.py +++ b/custom_components/immich_album_watcher/sensor.py @@ -42,7 +42,7 @@ from .const import ( DOMAIN, SERVICE_GET_RECENT_ASSETS, SERVICE_REFRESH, - SERVICE_SEND_TELEGRAM_MEDIA_GROUP, + SERVICE_SEND_TELEGRAM_NOTIFICATION, ) from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator @@ -99,15 +99,22 @@ async def async_setup_entry( ) platform.async_register_entity_service( - SERVICE_SEND_TELEGRAM_MEDIA_GROUP, + SERVICE_SEND_TELEGRAM_NOTIFICATION, { vol.Optional("bot_token"): str, vol.Required("chat_id"): vol.Coerce(str), - vol.Required("urls"): vol.All(list, vol.Length(min=1, max=10)), + vol.Optional("urls"): list, vol.Optional("caption"): str, vol.Optional("reply_to_message_id"): vol.Coerce(int), + vol.Optional("disable_web_page_preview"): bool, + vol.Optional("max_group_size", default=10): vol.All( + vol.Coerce(int), vol.Range(min=2, max=10) + ), + vol.Optional("chunk_delay", default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=60000) + ), }, - "async_send_telegram_media_group", + "async_send_telegram_notification", supports_response=SupportsResponse.ONLY, ) @@ -167,15 +174,24 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se assets = await self.coordinator.async_get_recent_assets(count) return {"assets": assets} - async def async_send_telegram_media_group( + async def async_send_telegram_notification( self, chat_id: str, - urls: list[dict[str, str]], + urls: list[dict[str, str]] | None = None, bot_token: str | None = None, caption: str | None = None, reply_to_message_id: int | None = None, + disable_web_page_preview: bool | None = None, + max_group_size: int = 10, + chunk_delay: int = 0, ) -> ServiceResponse: - """Send media URLs to Telegram as a media group. + """Send notification to Telegram. + + Supports: + - Empty URLs: sends a simple text message + - Single photo: uses sendPhoto API + - Single video: uses sendVideo API + - Multiple items: uses sendMediaGroup API (splits into multiple groups if needed) Each item in urls should be a dict with 'url' and 'type' (photo/video). Downloads media and uploads to Telegram to bypass CORS restrictions. @@ -195,81 +211,63 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se session = async_get_clientsession(self.hass) - # Download all media files - media_files: list[tuple[str, bytes, str]] = [] - for i, item in enumerate(urls): - url = item.get("url") - media_type = item.get("type", "photo") + # Handle empty URLs - send simple text message + if not urls: + return await self._send_telegram_message( + session, token, chat_id, caption or "", reply_to_message_id, disable_web_page_preview + ) - if not url: - return { - "success": False, - "error": f"Missing 'url' in item {i}", - } + # Handle single photo + if len(urls) == 1 and urls[0].get("type", "photo") == "photo": + return await self._send_telegram_photo( + session, token, chat_id, urls[0].get("url"), caption, reply_to_message_id + ) - if media_type not in ("photo", "video"): - return { - "success": False, - "error": f"Invalid type '{media_type}' in item {i}. Must be 'photo' or 'video'.", - } + # Handle single video + if len(urls) == 1 and urls[0].get("type") == "video": + return await self._send_telegram_video( + session, token, chat_id, urls[0].get("url"), caption, reply_to_message_id + ) - try: - _LOGGER.debug("Downloading media %d from %s", i, url[:80]) - async with session.get(url) as resp: - if resp.status != 200: - return { - "success": False, - "error": f"Failed to download media {i}: HTTP {resp.status}", - } - data = await resp.read() - ext = "jpg" if media_type == "photo" else "mp4" - filename = f"media_{i}.{ext}" - media_files.append((media_type, data, filename)) - _LOGGER.debug("Downloaded media %d: %d bytes", i, len(data)) - except aiohttp.ClientError as err: - return { - "success": False, - "error": f"Failed to download media {i}: {err}", - } + # Handle multiple items - send as media group(s) + return await self._send_telegram_media_group( + session, token, chat_id, urls, caption, reply_to_message_id, max_group_size, chunk_delay + ) - # Build multipart form - form = FormData() - form.add_field("chat_id", chat_id) + async def _send_telegram_message( + self, + session: Any, + token: str, + chat_id: str, + text: str, + reply_to_message_id: int | None = None, + disable_web_page_preview: bool | None = None, + ) -> ServiceResponse: + """Send a simple text message to Telegram.""" + import aiohttp + + telegram_url = f"https://api.telegram.org/bot{token}/sendMessage" + + payload: dict[str, Any] = { + "chat_id": chat_id, + "text": text or "Notification from Home Assistant", + } if reply_to_message_id: - form.add_field("reply_to_message_id", str(reply_to_message_id)) + payload["reply_to_message_id"] = reply_to_message_id - # Build media JSON with attach:// references - media_json = [] - for i, (media_type, data, filename) in enumerate(media_files): - attach_name = f"file{i}" - media_item: dict[str, Any] = { - "type": media_type, - "media": f"attach://{attach_name}", - } - if i == 0 and caption: - media_item["caption"] = caption - media_json.append(media_item) - - content_type = "image/jpeg" if media_type == "photo" else "video/mp4" - form.add_field(attach_name, data, filename=filename, content_type=content_type) - - form.add_field("media", json.dumps(media_json)) - - # Send to Telegram - telegram_url = f"https://api.telegram.org/bot{token}/sendMediaGroup" + if disable_web_page_preview is not None: + payload["disable_web_page_preview"] = disable_web_page_preview try: - _LOGGER.debug("Uploading %d files to Telegram", len(media_files)) - async with session.post(telegram_url, data=form) as response: + _LOGGER.debug("Sending text message to Telegram") + async with session.post(telegram_url, json=payload) as response: result = await response.json() _LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok")) if response.status == 200 and result.get("ok"): return { "success": True, - "message_ids": [ - msg.get("message_id") for msg in result.get("result", []) - ], + "message_id": result.get("result", {}).get("message_id"), } else: _LOGGER.error("Telegram API error: %s", result) @@ -279,9 +277,299 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se "error_code": result.get("error_code"), } except aiohttp.ClientError as err: - _LOGGER.error("Telegram upload failed: %s", err) + _LOGGER.error("Telegram message send failed: %s", err) return {"success": False, "error": str(err)} + async def _send_telegram_photo( + self, + session: Any, + token: str, + chat_id: str, + url: str | None, + caption: str | None = None, + reply_to_message_id: int | None = None, + ) -> ServiceResponse: + """Send a single photo to Telegram.""" + import aiohttp + from aiohttp import FormData + + if not url: + return {"success": False, "error": "Missing 'url' for photo"} + + try: + # Download the photo + _LOGGER.debug("Downloading photo from %s", url[:80]) + async with session.get(url) as resp: + if resp.status != 200: + return { + "success": False, + "error": f"Failed to download photo: HTTP {resp.status}", + } + data = await resp.read() + _LOGGER.debug("Downloaded photo: %d bytes", len(data)) + + # Build multipart form + form = FormData() + form.add_field("chat_id", chat_id) + form.add_field("photo", data, filename="photo.jpg", content_type="image/jpeg") + + if caption: + form.add_field("caption", caption) + + if reply_to_message_id: + form.add_field("reply_to_message_id", str(reply_to_message_id)) + + # Send to Telegram + telegram_url = f"https://api.telegram.org/bot{token}/sendPhoto" + + _LOGGER.debug("Uploading photo to Telegram") + async with session.post(telegram_url, data=form) as response: + result = await response.json() + _LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok")) + if response.status == 200 and result.get("ok"): + return { + "success": True, + "message_id": result.get("result", {}).get("message_id"), + } + else: + _LOGGER.error("Telegram API error: %s", result) + return { + "success": False, + "error": result.get("description", "Unknown Telegram error"), + "error_code": result.get("error_code"), + } + except aiohttp.ClientError as err: + _LOGGER.error("Telegram photo upload failed: %s", err) + return {"success": False, "error": str(err)} + + async def _send_telegram_video( + self, + session: Any, + token: str, + chat_id: str, + url: str | None, + caption: str | None = None, + reply_to_message_id: int | None = None, + ) -> ServiceResponse: + """Send a single video to Telegram.""" + import aiohttp + from aiohttp import FormData + + if not url: + return {"success": False, "error": "Missing 'url' for video"} + + try: + # Download the video + _LOGGER.debug("Downloading video from %s", url[:80]) + async with session.get(url) as resp: + if resp.status != 200: + return { + "success": False, + "error": f"Failed to download video: HTTP {resp.status}", + } + data = await resp.read() + _LOGGER.debug("Downloaded video: %d bytes", len(data)) + + # Build multipart form + form = FormData() + form.add_field("chat_id", chat_id) + form.add_field("video", data, filename="video.mp4", content_type="video/mp4") + + if caption: + form.add_field("caption", caption) + + if reply_to_message_id: + form.add_field("reply_to_message_id", str(reply_to_message_id)) + + # Send to Telegram + telegram_url = f"https://api.telegram.org/bot{token}/sendVideo" + + _LOGGER.debug("Uploading video to Telegram") + async with session.post(telegram_url, data=form) as response: + result = await response.json() + _LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok")) + if response.status == 200 and result.get("ok"): + return { + "success": True, + "message_id": result.get("result", {}).get("message_id"), + } + else: + _LOGGER.error("Telegram API error: %s", result) + return { + "success": False, + "error": result.get("description", "Unknown Telegram error"), + "error_code": result.get("error_code"), + } + except aiohttp.ClientError as err: + _LOGGER.error("Telegram video upload failed: %s", err) + return {"success": False, "error": str(err)} + + async def _send_telegram_media_group( + self, + session: Any, + token: str, + chat_id: str, + urls: list[dict[str, str]], + caption: str | None = None, + reply_to_message_id: int | None = None, + max_group_size: int = 10, + chunk_delay: int = 0, + ) -> ServiceResponse: + """Send media URLs to Telegram as media group(s). + + If urls list exceeds max_group_size, splits into multiple media groups. + For chunks with single items, uses sendPhoto/sendVideo APIs. + Applies chunk_delay (in milliseconds) between groups if specified. + """ + import json + import asyncio + import aiohttp + from aiohttp import FormData + + # Split URLs into chunks based on max_group_size + chunks = [urls[i:i + max_group_size] for i in range(0, len(urls), max_group_size)] + all_message_ids = [] + + _LOGGER.debug("Sending %d media items in %d chunk(s) of max %d items (delay: %dms)", + len(urls), len(chunks), max_group_size, chunk_delay) + + for chunk_idx, chunk in enumerate(chunks): + # Add delay before sending subsequent chunks + if chunk_idx > 0 and chunk_delay > 0: + delay_seconds = chunk_delay / 1000 + _LOGGER.debug("Waiting %dms (%ss) before sending chunk %d/%d", + chunk_delay, delay_seconds, chunk_idx + 1, len(chunks)) + await asyncio.sleep(delay_seconds) + + # Optimize: Use single-item APIs for chunks with 1 item + if len(chunk) == 1: + item = chunk[0] + media_type = item.get("type", "photo") + url = item.get("url") + + # Only apply caption and reply_to to the first chunk + chunk_caption = caption if chunk_idx == 0 else None + chunk_reply_to = reply_to_message_id if chunk_idx == 0 else None + + if media_type == "photo": + _LOGGER.debug("Sending chunk %d/%d as single photo", chunk_idx + 1, len(chunks)) + result = await self._send_telegram_photo( + session, token, chat_id, url, chunk_caption, chunk_reply_to + ) + else: # video + _LOGGER.debug("Sending chunk %d/%d as single video", chunk_idx + 1, len(chunks)) + result = await self._send_telegram_video( + session, token, chat_id, url, chunk_caption, chunk_reply_to + ) + + if not result.get("success"): + result["failed_at_chunk"] = chunk_idx + 1 + return result + + all_message_ids.append(result.get("message_id")) + continue + # Multi-item chunk: use sendMediaGroup + _LOGGER.debug("Sending chunk %d/%d as media group (%d items)", chunk_idx + 1, len(chunks), len(chunk)) + + # Download all media files for this chunk + media_files: list[tuple[str, bytes, str]] = [] + for i, item in enumerate(chunk): + url = item.get("url") + media_type = item.get("type", "photo") + + if not url: + return { + "success": False, + "error": f"Missing 'url' in item {chunk_idx * max_group_size + i}", + } + + if media_type not in ("photo", "video"): + return { + "success": False, + "error": f"Invalid type '{media_type}' in item {chunk_idx * max_group_size + i}. Must be 'photo' or 'video'.", + } + + try: + _LOGGER.debug("Downloading media %d from %s", chunk_idx * max_group_size + i, url[:80]) + async with session.get(url) as resp: + if resp.status != 200: + return { + "success": False, + "error": f"Failed to download media {chunk_idx * max_group_size + i}: HTTP {resp.status}", + } + data = await resp.read() + ext = "jpg" if media_type == "photo" else "mp4" + filename = f"media_{chunk_idx * max_group_size + i}.{ext}" + media_files.append((media_type, data, filename)) + _LOGGER.debug("Downloaded media %d: %d bytes", chunk_idx * max_group_size + i, len(data)) + except aiohttp.ClientError as err: + return { + "success": False, + "error": f"Failed to download media {chunk_idx * max_group_size + i}: {err}", + } + + # Build multipart form + form = FormData() + form.add_field("chat_id", chat_id) + + # Only use reply_to_message_id for the first chunk + if chunk_idx == 0 and reply_to_message_id: + form.add_field("reply_to_message_id", str(reply_to_message_id)) + + # Build media JSON with attach:// references + media_json = [] + for i, (media_type, data, filename) in enumerate(media_files): + attach_name = f"file{i}" + media_item: dict[str, Any] = { + "type": media_type, + "media": f"attach://{attach_name}", + } + # Only add caption to the first item of the first chunk + if chunk_idx == 0 and i == 0 and caption: + media_item["caption"] = caption + media_json.append(media_item) + + content_type = "image/jpeg" if media_type == "photo" else "video/mp4" + form.add_field(attach_name, data, filename=filename, content_type=content_type) + + form.add_field("media", json.dumps(media_json)) + + # Send to Telegram + telegram_url = f"https://api.telegram.org/bot{token}/sendMediaGroup" + + try: + _LOGGER.debug("Uploading media group chunk %d/%d (%d files) to Telegram", + chunk_idx + 1, len(chunks), len(media_files)) + async with session.post(telegram_url, data=form) as response: + result = await response.json() + _LOGGER.debug("Telegram API response: status=%d, ok=%s", response.status, result.get("ok")) + if response.status == 200 and result.get("ok"): + chunk_message_ids = [ + msg.get("message_id") for msg in result.get("result", []) + ] + all_message_ids.extend(chunk_message_ids) + else: + _LOGGER.error("Telegram API error for chunk %d: %s", chunk_idx + 1, result) + return { + "success": False, + "error": result.get("description", "Unknown Telegram error"), + "error_code": result.get("error_code"), + "failed_at_chunk": chunk_idx + 1, + } + except aiohttp.ClientError as err: + _LOGGER.error("Telegram upload failed for chunk %d: %s", chunk_idx + 1, err) + return { + "success": False, + "error": str(err), + "failed_at_chunk": chunk_idx + 1, + } + + return { + "success": True, + "message_ids": all_message_ids, + "chunks_sent": len(chunks), + } + class ImmichAlbumIdSensor(ImmichAlbumBaseSensor): """Sensor exposing the Immich album ID.""" diff --git a/custom_components/immich_album_watcher/services.yaml b/custom_components/immich_album_watcher/services.yaml index 20eaddc..b1e7ef1 100644 --- a/custom_components/immich_album_watcher/services.yaml +++ b/custom_components/immich_album_watcher/services.yaml @@ -25,9 +25,9 @@ get_recent_assets: max: 100 mode: slider -send_telegram_media_group: - name: Send Telegram Media Group - description: Send specified media URLs to a Telegram chat as a media group. +send_telegram_notification: + name: Send Telegram Notification + description: Send a notification to Telegram (text, photo, video, or media group). target: entity: integration: immich_album_watcher @@ -47,13 +47,13 @@ send_telegram_media_group: text: urls: name: URLs - description: List of media URLs to send (max 10). Each item should have 'url' and 'type' (photo/video). - required: true + description: List of media URLs to send. Each item should have 'url' and 'type' (photo/video). If empty, sends a text message. Large lists are automatically split into multiple media groups. + required: false selector: object: caption: name: Caption - description: Optional caption for the media group (applied to first item). + description: Caption text. For media, applied to first item. For empty URLs, this is the message text. required: false selector: text: @@ -65,3 +65,31 @@ send_telegram_media_group: selector: number: mode: box + disable_web_page_preview: + name: Disable Web Page Preview + description: Disable link previews in text messages. + required: false + selector: + boolean: + max_group_size: + name: Max Group Size + description: Maximum number of media items per media group (2-10). Large lists will be split into multiple groups. + required: false + default: 10 + selector: + number: + min: 2 + max: 10 + mode: slider + chunk_delay: + name: Chunk Delay + description: Delay in milliseconds between sending multiple media groups (0-60000). Useful for rate limiting. + required: false + default: 0 + selector: + number: + min: 0 + max: 60000 + step: 100 + unit_of_measurement: "ms" + mode: slider diff --git a/custom_components/immich_album_watcher/translations/en.json b/custom_components/immich_album_watcher/translations/en.json index 9b51895..f8f842e 100644 --- a/custom_components/immich_album_watcher/translations/en.json +++ b/custom_components/immich_album_watcher/translations/en.json @@ -123,7 +123,7 @@ }, "data_description": { "scan_interval": "How often to check for album changes (10-3600 seconds)", - "telegram_bot_token": "Bot token for sending media groups to Telegram" + "telegram_bot_token": "Bot token for sending notifications to Telegram" } } } @@ -143,9 +143,9 @@ } } }, - "send_telegram_media_group": { - "name": "Send Telegram Media Group", - "description": "Send media URLs to Telegram as a media group.", + "send_telegram_notification": { + "name": "Send Telegram Notification", + "description": "Send a notification to Telegram (text, photo, video, or media group).", "fields": { "bot_token": { "name": "Bot Token", @@ -157,15 +157,27 @@ }, "urls": { "name": "URLs", - "description": "List of media URLs with type (photo/video)." + "description": "List of media URLs with type (photo/video). If empty, sends a text message. Large lists are automatically split into multiple media groups." }, "caption": { "name": "Caption", - "description": "Optional caption for the first media item." + "description": "Caption text. For media, applied to first item. For empty URLs, this is the message text." }, "reply_to_message_id": { "name": "Reply To", "description": "Optional message ID to reply to." + }, + "disable_web_page_preview": { + "name": "Disable Web Page Preview", + "description": "Disable link previews in text messages." + }, + "max_group_size": { + "name": "Max Group Size", + "description": "Maximum number of media items per media group (2-10). Large lists will be split into multiple groups." + }, + "chunk_delay": { + "name": "Chunk Delay", + "description": "Delay in milliseconds between sending multiple media groups (0-60000). Useful for rate limiting." } } } diff --git a/custom_components/immich_album_watcher/translations/ru.json b/custom_components/immich_album_watcher/translations/ru.json index e8d3ed8..86b1c79 100644 --- a/custom_components/immich_album_watcher/translations/ru.json +++ b/custom_components/immich_album_watcher/translations/ru.json @@ -123,7 +123,7 @@ }, "data_description": { "scan_interval": "Как часто проверять изменения в альбомах (10-3600 секунд)", - "telegram_bot_token": "Токен бота для отправки медиа-групп в Telegram" + "telegram_bot_token": "Токен бота для отправки уведомлений в Telegram" } } } @@ -143,9 +143,9 @@ } } }, - "send_telegram_media_group": { - "name": "Отправить медиа-группу в Telegram", - "description": "Отправить медиа-файлы в Telegram как группу.", + "send_telegram_notification": { + "name": "Отправить уведомление в Telegram", + "description": "Отправить уведомление в Telegram (текст, фото, видео или медиа-группу).", "fields": { "bot_token": { "name": "Токен бота", @@ -157,15 +157,27 @@ }, "urls": { "name": "URL-адреса", - "description": "Список URL медиа-файлов с типом (photo/video)." + "description": "Список URL медиа-файлов с типом (photo/video). Если пусто, отправляет текстовое сообщение. Большие списки автоматически разделяются на несколько медиа-групп." }, "caption": { "name": "Подпись", - "description": "Необязательная подпись для первого медиа-файла." + "description": "Текст подписи. Для медиа применяется к первому элементу. Для пустых URLs это текст сообщения." }, "reply_to_message_id": { "name": "Ответ на", "description": "ID сообщения для ответа (необязательно)." + }, + "disable_web_page_preview": { + "name": "Отключить предпросмотр ссылок", + "description": "Отключить предпросмотр ссылок в текстовых сообщениях." + }, + "max_group_size": { + "name": "Макс. размер группы", + "description": "Максимальное количество медиа-файлов в одной группе (2-10). Большие списки будут разделены на несколько групп." + }, + "chunk_delay": { + "name": "Задержка между группами", + "description": "Задержка в миллисекундах между отправкой нескольких медиа-групп (0-60000). Полезно для ограничения скорости." } } }