From 6c112544a697acb83d1c7cc183c55be8696cc67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C6=B0u=20Quang=20V=C5=A9?= Date: Tue, 8 Sep 2026 21:29:31 +0700 Subject: [PATCH] Optimize telegram and zalo bots logic --- scripts/telegram_bot_handle_tool.py | 169 +++++++++++++------ scripts/zalo_bot_handle_tool.py | 116 ++++++++----- send_to_telegram_full_llm.yaml | 1 + telegram_bot_webhook.yaml | 245 ++++++++++++++++------------ zalo_bot_webhook.yaml | 17 +- 5 files changed, 347 insertions(+), 201 deletions(-) diff --git a/scripts/telegram_bot_handle_tool.py b/scripts/telegram_bot_handle_tool.py index 0004b8e..75da7c5 100644 --- a/scripts/telegram_bot_handle_tool.py +++ b/scripts/telegram_bot_handle_tool.py @@ -19,6 +19,12 @@ if TOKEN: TOKEN = TOKEN.strip() +if ( + API_URL := pyscript.config.get("telegram_bot_api_url") # noqa: F821 # ty:ignore[unresolved-reference] + or "https://api.telegram.org" +): + API_URL = API_URL.strip().rstrip("/") + _session: httpx.AsyncClient | None = None _session_lock = asyncio.Lock() @@ -26,6 +32,13 @@ if not TOKEN: raise ValueError("Telegram bot token is missing") + +def _clean_error(error: Exception | str) -> str: + """Sanitize error message to prevent leaking bot token.""" + err_str = str(error) + return err_str.replace(TOKEN, "[REDACTED]") if TOKEN else err_str + + ACTIONS_CHAT: tuple[str, ...] = ( "typing", "upload_photo", @@ -106,38 +119,63 @@ async def _ensure_dir(path: str) -> None: await asyncio.to_thread(os.makedirs, path, exist_ok=True) -async def _get_file(client: httpx.AsyncClient, file_id: str) -> str | None: +async def _get_file(client: httpx.AsyncClient, file_id: str) -> tuple[str | None, str | None]: """Resolve a Telegram file identifier to its server path.""" - url = f"https://api.telegram.org/bot{TOKEN}/getFile" - payload = {"file_id": file_id} + url = f"{API_URL}/bot{TOKEN}/getFile" + payload = {"file_id": file_id.strip()} data = orjson.dumps(payload).decode("utf-8") resp = await client.post(url, content=data, headers={"Content-Type": "application/json"}) - resp.raise_for_status() - result = orjson.loads(resp.content) - return result.get("result", {}).get("file_path") + try: + result = orjson.loads(resp.content) + except Exception: + result = {} + if not resp.is_success or not result.get("ok"): + description = result.get("description") or resp.reason_phrase or f"HTTP {resp.status_code}" + if "file is too big" in description.lower(): + return ( + None, + f"File size exceeds Telegram Bot API 20MB download limit for public servers ({description}). " + "To download files up to 2GB, configure a local Telegram Bot API server via 'telegram_bot_api_url'.", + ) + return None, f"Telegram API error ({resp.status_code}): {description}" + if file_path := result.get("result", {}).get("file_path"): + return file_path, None + return None, "Unable to retrieve the file_path from Telegram." -async def _download_file(client: httpx.AsyncClient, file_id: str) -> tuple[str, None] | tuple[None, str]: +async def _download_file( + client: httpx.AsyncClient, + file_id: str, + file_name: str | None = None, +) -> tuple[str, None] | tuple[None, str]: """Download a file from Telegram and save it locally.""" + file_path = None try: - online_file_path = await _get_file(client, file_id) + online_file_path, err = await _get_file(client, file_id) if not online_file_path: - return None, "Unable to retrieve the file_path from Telegram." + return None, err or "Unable to retrieve the file_path from Telegram." - url = f"https://api.telegram.org/file/bot{TOKEN}/{online_file_path}" + url = f"{API_URL}/file/bot{TOKEN}/{online_file_path}" - file_name = os.path.basename(online_file_path) - base, ext = os.path.splitext(file_name) + target_file_name = os.path.basename(online_file_path) + base, ext = os.path.splitext(target_file_name) + if not ext and file_name: + _, fallback_ext = os.path.splitext(file_name) + ext = fallback_ext timestamp = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime()) - file_name = f"{base}_{timestamp}_{secrets.token_hex(4)}{ext}" + target_file_name = f"{base}_{timestamp}_{secrets.token_hex(4)}{ext}" - file_path = os.path.join(DIRECTORY, file_name) + file_path = os.path.join(DIRECTORY, target_file_name) await asyncio.to_thread(_download_file_chunks, url, file_path) return file_path, None except Exception as error: - return None, f"Download failed: {error}" + if file_path and os.path.exists(file_path): + with contextlib.suppress(OSError): + os.remove(file_path) + clean_error = _clean_error(error) + return None, f"Download failed: {clean_error}" async def _send_message( @@ -149,7 +187,7 @@ async def _send_message( parse_mode: str | None = None, ) -> dict[str, Any]: """Send a text message via the Telegram API.""" - url = f"https://api.telegram.org/bot{TOKEN}/sendMessage" + url = f"{API_URL}/bot{TOKEN}/sendMessage" text = message if len(text) > 4096: text = f"{text[:4093]}..." @@ -213,7 +251,7 @@ async def _send_location( if reply_to_message_id is not None: payload["reply_parameters"] = {"message_id": reply_to_message_id} - url = f"https://api.telegram.org/bot{TOKEN}/sendLocation" + url = f"{API_URL}/bot{TOKEN}/sendLocation" data = orjson.dumps(payload).decode("utf-8") resp = await client.post(url, content=data, headers={"Content-Type": "application/json"}) resp.raise_for_status() @@ -257,7 +295,7 @@ async def _send_photo( f = await asyncio.to_thread(_open_file, file_path, "rb") try: files = {"photo": (filename, f, content_type)} - url = f"https://api.telegram.org/bot{TOKEN}/sendPhoto" + url = f"{API_URL}/bot{TOKEN}/sendPhoto" resp = await client.post(url, data=form_data, files=files) resp.raise_for_status() return orjson.loads(resp.content) @@ -315,7 +353,7 @@ async def _send_media_file( opened_files[field] = attachment_file files[field] = (attachment_name, attachment_file, attachment_type or "application/octet-stream") - url = f"https://api.telegram.org/bot{TOKEN}/{endpoint}" + url = f"{API_URL}/bot{TOKEN}/{endpoint}" resp = await client.post(url, data=form_data, files=files) resp.raise_for_status() return orjson.loads(resp.content) @@ -576,7 +614,7 @@ async def _send_voice( async def _get_webhook_info(client: httpx.AsyncClient) -> dict[str, Any]: """Retrieve current Telegram webhook status.""" - url = f"https://api.telegram.org/bot{TOKEN}/getWebhookInfo" + url = f"{API_URL}/bot{TOKEN}/getWebhookInfo" resp = await client.get(url) resp.raise_for_status() return orjson.loads(resp.content) @@ -589,7 +627,7 @@ async def _set_webhook( secret_token: str, ) -> dict[str, Any]: """Configure the Telegram webhook URL.""" - url = f"https://api.telegram.org/bot{TOKEN}/setWebhook" + url = f"{API_URL}/bot{TOKEN}/setWebhook" params = { "url": f"{base_url}/api/webhook/{webhook_id}", "drop_pending_updates": True, @@ -603,7 +641,7 @@ async def _set_webhook( async def _delete_webhook(client: httpx.AsyncClient) -> dict[str, Any]: """Remove the Telegram webhook configuration.""" - url = f"https://api.telegram.org/bot{TOKEN}/deleteWebhook" + url = f"{API_URL}/bot{TOKEN}/deleteWebhook" params = {"drop_pending_updates": True} data = orjson.dumps(params).decode("utf-8") resp = await client.post(url, content=data, headers={"Content-Type": "application/json"}) @@ -618,7 +656,7 @@ async def _get_updates( limit: int | None = None, ) -> dict[str, Any]: """Fetch updates from Telegram using long polling.""" - url = f"https://api.telegram.org/bot{TOKEN}/getUpdates" + url = f"{API_URL}/bot{TOKEN}/getUpdates" params: dict[str, Any] = {"timeout": timeout} if offset is not None: params["offset"] = offset @@ -632,7 +670,7 @@ async def _get_updates( async def _get_me(client: httpx.AsyncClient) -> dict[str, Any]: """Retrieve basic bot account information.""" - url = f"https://api.telegram.org/bot{TOKEN}/getMe" + url = f"{API_URL}/bot{TOKEN}/getMe" resp = await client.get(url) resp.raise_for_status() return orjson.loads(resp.content) @@ -647,7 +685,7 @@ async def _send_chat_action( """Broadcast a chat action status to a conversation.""" if action not in ACTIONS_CHAT: raise ValueError(f"Unsupported chat action: {action}. Allowed: {', '.join(ACTIONS_CHAT)}") - url = f"https://api.telegram.org/bot{TOKEN}/sendChatAction" + url = f"{API_URL}/bot{TOKEN}/sendChatAction" params = { "chat_id": chat_id, "action": action, @@ -802,12 +840,17 @@ async def send_telegram_message( ) return response or {"error": "Failed to send message"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] -async def get_telegram_file(file_id: str) -> dict[str, Any]: +async def get_telegram_file( + file_id: str, + mime_type: str | None = None, + file_name: str | None = None, +) -> dict[str, Any]: """ yaml name: Get Telegram File @@ -819,19 +862,31 @@ async def get_telegram_file(file_id: str) -> dict[str, Any]: required: true selector: text: + mime_type: + name: MIME Type + description: Optional MIME type of the file if known from Telegram metadata. + selector: + text: + file_name: + name: File Name + description: Optional original filename from Telegram metadata. + selector: + text: """ if not file_id: return {"error": "Missing a required argument: file_id"} + cleaned_file_id = file_id.strip() try: client = await _ensure_session() await _ensure_dir(DIRECTORY) - file_path, error = await _download_file(client, file_id) + file_path, error = await _download_file(client, cleaned_file_id, file_name=file_name) if not file_path: return {"error": f"Unable to download the file from Telegram. {error}"} mimetypes.add_type("text/plain", ".yaml") - mime_type, _ = mimetypes.guess_file_type(file_path) + detected_mime, _ = mimetypes.guess_file_type(file_path) + resolved_mime = detected_mime or (mime_type.strip().lower() if mime_type else None) file_path = _to_relative_path(file_path) support_file_types = ( "image/", @@ -849,13 +904,14 @@ async def get_telegram_file(file_id: str) -> dict[str, Any]: ) response: dict[str, Any] = { "file_path": file_path, - "mime_type": mime_type, - "supported": bool(mime_type and mime_type.lower().startswith(support_file_types)), + "mime_type": resolved_mime, + "supported": bool(resolved_mime and resolved_mime.lower().startswith(support_file_types)), } return response except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -869,8 +925,9 @@ async def get_telegram_webhook() -> dict[str, Any]: client = await _ensure_session() return await _get_webhook_info(client) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -899,8 +956,9 @@ async def set_telegram_webhook(webhook_id: str | None = None) -> dict[str, Any]: response["webhook_id"] = webhook_id return response except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -914,8 +972,9 @@ async def delete_telegram_webhook() -> dict[str, Any]: client = await _ensure_session() return await _delete_webhook(client) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -964,8 +1023,9 @@ async def get_telegram_updates( } return response except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -979,8 +1039,9 @@ async def get_telegram_bot_info() -> dict[str, Any]: client = await _ensure_session() return await _get_me(client) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -1041,8 +1102,9 @@ async def send_telegram_chat_action( ) return response or {"error": "Failed to send message"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -1228,8 +1290,9 @@ async def send_telegram_location( ) return response or {"error": "Failed to send location"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -1305,8 +1368,9 @@ async def send_telegram_photo( ) return response or {"error": "Failed to send photo"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} async def _send_telegram_media_action( @@ -1326,8 +1390,9 @@ async def _send_telegram_media_action( response = await sender(client, chat_id, file_path, **kwargs) return response or {"error": f"Failed to send {media_name}"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] diff --git a/scripts/zalo_bot_handle_tool.py b/scripts/zalo_bot_handle_tool.py index f8ec5a0..7c303d2 100644 --- a/scripts/zalo_bot_handle_tool.py +++ b/scripts/zalo_bot_handle_tool.py @@ -32,6 +32,12 @@ ZALO_API_BASE_URL = f"https://bot-api.zaloplatforms.com/bot{TOKEN}" +def _clean_error(error: Exception | str) -> str: + """Sanitize error message to prevent leaking bot token.""" + err_str = str(error) + return err_str.replace(TOKEN, "[REDACTED]") if TOKEN else err_str + + @pyscript_compile # noqa: F821 # ty:ignore[unresolved-reference] def _create_session() -> httpx.AsyncClient: """Create the HTTPX client in native Python for executor-safe SSL setup.""" @@ -119,7 +125,7 @@ def _open_file(path: str, mode: str): @pyscript_compile # noqa: F821 # ty:ignore[unresolved-reference] -def _download_file_chunks_with_headers(url: str, original_name: str, directory: str) -> str: +def _download_file_chunks_with_headers(url: str, original_name: str, directory: str) -> tuple[str, str]: """Download a file in chunks using httpx.Client, guess the extension, and write to disk.""" with httpx.Client(timeout=300) as client, client.stream("GET", url) as resp: resp.raise_for_status() @@ -135,14 +141,20 @@ def _download_file_chunks_with_headers(url: str, original_name: str, directory: file_name = f"{base}_{timestamp}_{secrets.token_hex(4)}{extension}" file_path = os.path.join(directory, file_name) - with open(file_path, "wb") as f: - for chunk in resp.iter_bytes(65536): - f.write(chunk) - f.flush() + try: + with open(file_path, "wb") as f: + for chunk in resp.iter_bytes(65536): + f.write(chunk) + f.flush() + with contextlib.suppress(OSError): + os.fsync(f.fileno()) + except Exception: with contextlib.suppress(OSError): - os.fsync(f.fileno()) + if os.path.exists(file_path): + os.remove(file_path) + raise - return file_path + return file_path, content_type @pyscript_compile # noqa: F821 # ty:ignore[unresolved-reference] @@ -165,23 +177,24 @@ async def _cleanup_old_files(directory: str, days: int = 30) -> None: await asyncio.to_thread(_cleanup_disk_sync, directory, cutoff) -async def _download_file(client: httpx.AsyncClient, url: str) -> tuple[str, None] | tuple[None, str]: +async def _download_file(client: httpx.AsyncClient, url: str) -> tuple[str, str, None] | tuple[None, None, str]: """Download a file from a URL and save it locally.""" try: safe_url = _validate_download_url(url) parsed_url = urlparse(safe_url) original_name = Path(parsed_url.path).name or "zalo_file" - file_path = await asyncio.to_thread( + file_path, content_type = await asyncio.to_thread( _download_file_chunks_with_headers, safe_url, original_name, DIRECTORY, ) - return file_path, None + return file_path, content_type, None except Exception as error: - return None, f"Download failed: {error}" + clean_error = _clean_error(error) + return None, None, f"Download failed: {clean_error}" async def _send_message(client: httpx.AsyncClient, chat_id: str, message: str) -> dict[str, Any]: @@ -418,8 +431,9 @@ async def send_zalo_message(chat_id: str, message: str) -> dict[str, Any]: response = await _send_message(client, chat_id, message) return response or {"error": "Failed to send message"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -443,12 +457,18 @@ async def get_zalo_file(url: str) -> dict[str, Any]: client = await _ensure_session() await _ensure_dir(DIRECTORY) - file_path, error = await _download_file(client, url) + file_path, header_content_type, error = await _download_file(client, url) if not file_path: return {"error": f"Unable to download the file from Zalo. {error}"} mimetypes.add_type("text/plain", ".yaml") - mime_type, _ = mimetypes.guess_file_type(file_path) + guessed_mime, _ = ( + mimetypes.guess_file_type(file_path) + if hasattr(mimetypes, "guess_file_type") + else mimetypes.guess_type(file_path) + ) + header_mime = header_content_type.split(";")[0].strip() if header_content_type else None + resolved_mime = guessed_mime or header_mime file_path = _to_relative_path(file_path) support_file_types = ( "image/", @@ -456,16 +476,24 @@ async def get_zalo_file(url: str) -> dict[str, Any]: "audio/", "text/", "application/pdf", + "application/rtf", + "application/msword", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", ) response: dict[str, Any] = { "file_path": file_path, - "mime_type": mime_type, - "supported": bool(mime_type and mime_type.startswith(support_file_types)), + "mime_type": resolved_mime, + "supported": bool(resolved_mime and resolved_mime.lower().startswith(support_file_types)), } return response except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -479,8 +507,9 @@ async def get_zalo_webhook() -> dict[str, Any]: client = await _ensure_session() return await _get_webhook_info(client) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -496,8 +525,9 @@ async def test_zalo_webhook() -> dict[str, Any]: client = await _ensure_session() return await _test_webhook(client) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -526,8 +556,9 @@ async def set_zalo_webhook(webhook_id: str | None = None) -> dict[str, Any]: response["webhook_id"] = webhook_id return response except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -541,8 +572,9 @@ async def delete_zalo_webhook() -> dict[str, Any]: client = await _ensure_session() return await _delete_webhook(client) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -574,8 +606,9 @@ async def get_zalo_updates(timeout: int = 30) -> dict[str, Any]: } return response except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -589,8 +622,9 @@ async def get_zalo_bot_info() -> dict[str, Any]: client = await _ensure_session() return await _get_me(client) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -614,8 +648,9 @@ async def send_zalo_chat_action(chat_id: str) -> dict[str, Any]: response = await _send_chat_action(client, chat_id) return response or {"error": "Failed to send message"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -657,8 +692,9 @@ async def send_zalo_photo( public_url, published_path = await _copy_to_www(file_path) return await _send_photo(client, chat_id, public_url, caption=caption) except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} finally: if published_path: task.create(_delayed_remove, published_path, 30) # noqa: F821 # ty:ignore[unresolved-reference] @@ -691,8 +727,9 @@ async def send_zalo_sticker(chat_id: str, sticker: str) -> dict[str, Any]: response = await _send_sticker(client, chat_id, sticker) return response or {"error": "Failed to send sticker"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} @service(supports_response="only") # noqa: F821 # ty:ignore[unresolved-reference] @@ -728,5 +765,6 @@ async def send_zalo_voice(chat_id: str, voice_url: str) -> dict[str, Any]: response = await _send_voice(client, chat_id, safe_voice_url) return response or {"error": "Failed to send voice message"} except Exception as error: - log.error(f"{__name__}: {error}") # noqa: F821 # ty:ignore[unresolved-reference] - return {"error": f"An unexpected error occurred during processing: {error}"} + clean_error = _clean_error(error) + log.error(f"{__name__}: {clean_error}") # noqa: F821 # ty:ignore[unresolved-reference] + return {"error": f"An unexpected error occurred during processing: {clean_error}"} diff --git a/send_to_telegram_full_llm.yaml b/send_to_telegram_full_llm.yaml index 27ab045..644d51f 100644 --- a/send_to_telegram_full_llm.yaml +++ b/send_to_telegram_full_llm.yaml @@ -23,6 +23,7 @@ blueprint: allow_all_imports: true hass_is_global: true telegram_bot_token: !secret telegram_bot_token + telegram_bot_api_url: http://localhost:8081 # Optional: local Telegram Bot API server for downloading/uploading files up to 2GB ``` ``` diff --git a/telegram_bot_webhook.yaml b/telegram_bot_webhook.yaml index 51d4e62..7b252d3 100644 --- a/telegram_bot_webhook.yaml +++ b/telegram_bot_webhook.yaml @@ -25,6 +25,7 @@ blueprint: allow_all_imports: true hass_is_global: true telegram_bot_token: !secret telegram_bot_token + telegram_bot_api_url: http://localhost:8081 # Optional: local Telegram Bot API server for downloading/uploading files up to 2GB ``` ``` @@ -144,198 +145,230 @@ actions: - variables: conversation_id: >- {{ get_chat.value if (get_chat.status == 'ok' and get_chat.value | default('', true) | length > 0) }} + msg: "{{ trigger.json.message }}" + media_type: >- + {% if trigger.json.message.photo is defined %}photo + {% elif trigger.json.message.video is defined %}video + {% elif trigger.json.message.video_note is defined %}video_note + {% elif trigger.json.message.voice is defined %}voice + {% elif trigger.json.message.audio is defined %}audio + {% elif trigger.json.message.document is defined %}document + {% elif trigger.json.message.reply_to_message is defined and trigger.json.message.reply_to_message.photo is defined %}photo + {% elif trigger.json.message.reply_to_message is defined and trigger.json.message.reply_to_message.video is defined %}video + {% elif trigger.json.message.reply_to_message is defined and trigger.json.message.reply_to_message.video_note is defined %}video_note + {% elif trigger.json.message.reply_to_message is defined and trigger.json.message.reply_to_message.voice is defined %}voice + {% elif trigger.json.message.reply_to_message is defined and trigger.json.message.reply_to_message.audio is defined %}audio + {% elif trigger.json.message.reply_to_message is defined and trigger.json.message.reply_to_message.document is defined %}document + {% elif trigger.json.message.text is defined %}text + {% else %}unknown + {% endif %} - choose: - conditions: - condition: template - value_template: "{{ trigger.json.message.text is defined }}" + value_template: "{{ media_type == 'photo' }}" sequence: - variables: - agent_input: - system: |- - Respond in language: {{ language | trim }} - prompt: |- - {% if trigger.json.message.reply_to_message is defined %} {% - if trigger.json.message.reply_to_message.text is defined %} {{ - trigger.json.message.reply_to_message.text }} {% endif %} {% - if trigger.json.message.reply_to_message.caption is defined %} - {{ trigger.json.message.reply_to_message.caption }} {% endif - %} {% endif %} - {{ trigger.json.message.text }} - telegram_chat_id: "{{ trigger.json.message.chat.id }}" - telegram_message_thread_id: "{{trigger.json.message.message_thread_id if trigger.json.message.message_thread_id is defined }}" - alias: Process text - - conditions: - - condition: template - value_template: "{{ trigger.json.message.photo is defined }}" - sequence: + target_photo: "{{ msg.photo if msg.photo is defined else msg.reply_to_message.photo }}" - action: pyscript.get_telegram_file data: - file_id: "{{ trigger.json.message.photo[-1].file_id }}" + file_id: "{{ target_photo[-1].file_id }}" response_variable: file - variables: agent_input: system: |- Use `File Content Analyzer`. If prompt empty, follow file instructions. Else return summary. No confirmation. Language: {{ language | trim }} - prompt: |- - {% if trigger.json.message.reply_to_message is defined %} {% - if trigger.json.message.reply_to_message.text is defined %} {{ - trigger.json.message.reply_to_message.text }} {% endif %} {% - if trigger.json.message.reply_to_message.caption is defined %} - {{ trigger.json.message.reply_to_message.caption }} {% endif - %} {% endif %} - {% if trigger.json.message.caption is defined %} {{ - trigger.json.message.caption }} {% endif %} + prompt: >- + {{ [ + (msg.reply_to_message.text if msg.reply_to_message is defined and msg.reply_to_message.text is defined else ''), + (msg.reply_to_message.caption if msg.reply_to_message is defined and msg.reply_to_message.caption is defined else ''), + (msg.caption if msg.caption is defined else ''), + (msg.text if msg.text is defined else '') + ] | select('ne', '') | join(' ') | trim }} file_path: "{{ file.file_path if file.file_path is defined }}" mime_type: "{{ file.mime_type if file.mime_type is defined }}" - telegram_chat_id: "{{ trigger.json.message.chat.id }}" - telegram_message_thread_id: "{{trigger.json.message.message_thread_id if trigger.json.message.message_thread_id is defined }}" + telegram_chat_id: "{{ msg.chat.id }}" + telegram_message_thread_id: "{{ msg.message_thread_id if msg.message_thread_id is defined }}" supported: "{{ file.supported if file.supported is defined }}" alias: Process photo - conditions: - condition: template - value_template: "{{ trigger.json.message.video is defined }}" + value_template: "{{ media_type == 'video' }}" sequence: + - variables: + target_video: "{{ msg.video if msg.video is defined else msg.reply_to_message.video }}" - action: pyscript.get_telegram_file data: - file_id: "{{ trigger.json.message.video.file_id }}" + file_id: "{{ target_video.file_id }}" + mime_type: "{{ target_video.mime_type if target_video.mime_type is defined }}" + file_name: "{{ target_video.file_name if target_video.file_name is defined }}" response_variable: file - variables: agent_input: system: |- Use `File Content Analyzer`. If prompt empty, follow file instructions. Else return summary. No confirmation. Language: {{ language | trim }} - prompt: |- - {% if trigger.json.message.reply_to_message is defined %} {% - if trigger.json.message.reply_to_message.text is defined %} {{ - trigger.json.message.reply_to_message.text }} {% endif %} {% - if trigger.json.message.reply_to_message.caption is defined %} - {{ trigger.json.message.reply_to_message.caption }} {% endif - %} {% endif %} - {% if trigger.json.message.caption is defined %} {{ - trigger.json.message.caption }} {% endif %} + prompt: >- + {{ [ + (msg.reply_to_message.text if msg.reply_to_message is defined and msg.reply_to_message.text is defined else ''), + (msg.reply_to_message.caption if msg.reply_to_message is defined and msg.reply_to_message.caption is defined else ''), + (msg.caption if msg.caption is defined else ''), + (msg.text if msg.text is defined else '') + ] | select('ne', '') | join(' ') | trim }} file_path: "{{ file.file_path if file.file_path is defined }}" mime_type: "{{ file.mime_type if file.mime_type is defined }}" - telegram_chat_id: "{{ trigger.json.message.chat.id }}" - telegram_message_thread_id: "{{trigger.json.message.message_thread_id if trigger.json.message.message_thread_id is defined }}" + telegram_chat_id: "{{ msg.chat.id }}" + telegram_message_thread_id: "{{ msg.message_thread_id if msg.message_thread_id is defined }}" supported: "{{ file.supported if file.supported is defined }}" alias: Process video - conditions: - condition: template - value_template: "{{ trigger.json.message.video_note is defined }}" + value_template: "{{ media_type == 'video_note' }}" sequence: + - variables: + target_video_note: "{{ msg.video_note if msg.video_note is defined else msg.reply_to_message.video_note }}" - action: pyscript.get_telegram_file data: - file_id: "{{ trigger.json.message.video_note.file_id }}" + file_id: "{{ target_video_note.file_id }}" response_variable: file - variables: agent_input: system: |- Use `File Content Analyzer`. If prompt empty, follow file instructions. Else return summary. No confirmation. Language: {{ language | trim }} - prompt: |- - {% if trigger.json.message.reply_to_message is defined %} {% - if trigger.json.message.reply_to_message.text is defined %} {{ - trigger.json.message.reply_to_message.text }} {% endif %} {% - if trigger.json.message.reply_to_message.caption is defined %} - {{ trigger.json.message.reply_to_message.caption }} {% endif - %} {% endif %} - {% if trigger.json.message.caption is defined %} {{ - trigger.json.message.caption }} {% endif %} + prompt: >- + {{ [ + (msg.reply_to_message.text if msg.reply_to_message is defined and msg.reply_to_message.text is defined else ''), + (msg.reply_to_message.caption if msg.reply_to_message is defined and msg.reply_to_message.caption is defined else ''), + (msg.caption if msg.caption is defined else ''), + (msg.text if msg.text is defined else '') + ] | select('ne', '') | join(' ') | trim }} file_path: "{{ file.file_path if file.file_path is defined }}" mime_type: "{{ file.mime_type if file.mime_type is defined }}" - telegram_chat_id: "{{ trigger.json.message.chat.id }}" - telegram_message_thread_id: "{{trigger.json.message.message_thread_id if trigger.json.message.message_thread_id is defined }}" + telegram_chat_id: "{{ msg.chat.id }}" + telegram_message_thread_id: "{{ msg.message_thread_id if msg.message_thread_id is defined }}" supported: "{{ file.supported if file.supported is defined }}" alias: Process video note - conditions: - condition: template - value_template: "{{ trigger.json.message.voice is defined }}" + value_template: "{{ media_type == 'voice' }}" sequence: + - variables: + target_voice: "{{ msg.voice if msg.voice is defined else msg.reply_to_message.voice }}" - action: pyscript.get_telegram_file data: - file_id: "{{ trigger.json.message.voice.file_id }}" + file_id: "{{ target_voice.file_id }}" + mime_type: "{{ target_voice.mime_type if target_voice.mime_type is defined }}" response_variable: file - variables: agent_input: system: |- Use `File Content Analyzer`. If prompt empty, follow file instructions. Else return summary. No confirmation. Language: {{ language | trim }} - prompt: |- - {% if trigger.json.message.reply_to_message is defined %} {% - if trigger.json.message.reply_to_message.text is defined %} {{ - trigger.json.message.reply_to_message.text }} {% endif %} {% - if trigger.json.message.reply_to_message.caption is defined %} - {{ trigger.json.message.reply_to_message.caption }} {% endif - %} {% endif %} - {% if trigger.json.message.caption is defined %} {{ - trigger.json.message.caption }} {% endif %} + prompt: >- + {{ [ + (msg.reply_to_message.text if msg.reply_to_message is defined and msg.reply_to_message.text is defined else ''), + (msg.reply_to_message.caption if msg.reply_to_message is defined and msg.reply_to_message.caption is defined else ''), + (msg.caption if msg.caption is defined else ''), + (msg.text if msg.text is defined else '') + ] | select('ne', '') | join(' ') | trim }} file_path: "{{ file.file_path if file.file_path is defined }}" mime_type: "{{ file.mime_type if file.mime_type is defined }}" - telegram_chat_id: "{{ trigger.json.message.chat.id }}" - telegram_message_thread_id: "{{trigger.json.message.message_thread_id if trigger.json.message.message_thread_id is defined }}" + telegram_chat_id: "{{ msg.chat.id }}" + telegram_message_thread_id: "{{ msg.message_thread_id if msg.message_thread_id is defined }}" supported: "{{ file.supported if file.supported is defined }}" alias: Process voice - conditions: - condition: template - value_template: "{{ trigger.json.message.audio is defined }}" + value_template: "{{ media_type == 'audio' }}" sequence: + - variables: + target_audio: "{{ msg.audio if msg.audio is defined else msg.reply_to_message.audio }}" - action: pyscript.get_telegram_file data: - file_id: "{{ trigger.json.message.audio.file_id }}" + file_id: "{{ target_audio.file_id }}" + mime_type: "{{ target_audio.mime_type if target_audio.mime_type is defined }}" + file_name: "{{ target_audio.file_name if target_audio.file_name is defined }}" response_variable: file - variables: agent_input: system: |- Use `File Content Analyzer`. If prompt empty, follow file instructions. Else return summary. No confirmation. Language: {{ language | trim }} - prompt: |- - {% if trigger.json.message.reply_to_message is defined %} {% - if trigger.json.message.reply_to_message.text is defined %} {{ - trigger.json.message.reply_to_message.text }} {% endif %} {% - if trigger.json.message.reply_to_message.caption is defined %} - {{ trigger.json.message.reply_to_message.caption }} {% endif - %} {% endif %} - {% if trigger.json.message.caption is defined %} {{ - trigger.json.message.caption }} {% endif %} + prompt: >- + {{ [ + (msg.reply_to_message.text if msg.reply_to_message is defined and msg.reply_to_message.text is defined else ''), + (msg.reply_to_message.caption if msg.reply_to_message is defined and msg.reply_to_message.caption is defined else ''), + (msg.caption if msg.caption is defined else ''), + (msg.text if msg.text is defined else '') + ] | select('ne', '') | join(' ') | trim }} file_path: "{{ file.file_path if file.file_path is defined }}" mime_type: "{{ file.mime_type if file.mime_type is defined }}" - telegram_chat_id: "{{ trigger.json.message.chat.id }}" - telegram_message_thread_id: "{{trigger.json.message.message_thread_id if trigger.json.message.message_thread_id is defined }}" + telegram_chat_id: "{{ msg.chat.id }}" + telegram_message_thread_id: "{{ msg.message_thread_id if msg.message_thread_id is defined }}" supported: "{{ file.supported if file.supported is defined }}" alias: Process audio - conditions: - condition: template - value_template: "{{ trigger.json.message.document is defined }}" + value_template: "{{ media_type == 'document' }}" sequence: + - variables: + target_document: "{{ msg.document if msg.document is defined else msg.reply_to_message.document }}" - action: pyscript.get_telegram_file data: - file_id: "{{ trigger.json.message.document.file_id }}" + file_id: "{{ target_document.file_id }}" + mime_type: "{{ target_document.mime_type if target_document.mime_type is defined }}" + file_name: "{{ target_document.file_name if target_document.file_name is defined }}" response_variable: file - variables: agent_input: system: |- Use `File Content Analyzer`. If prompt empty, follow file instructions. Else return summary. No confirmation. Language: {{ language | trim }} - prompt: |- - {% if trigger.json.message.reply_to_message is defined %} {% - if trigger.json.message.reply_to_message.text is defined %} {{ - trigger.json.message.reply_to_message.text }} {% endif %} {% - if trigger.json.message.reply_to_message.caption is defined %} - {{ trigger.json.message.reply_to_message.caption }} {% endif - %} {% endif %} - {% if trigger.json.message.caption is defined %} {{ - trigger.json.message.caption }} {% endif %} + prompt: >- + {{ [ + (msg.reply_to_message.text if msg.reply_to_message is defined and msg.reply_to_message.text is defined else ''), + (msg.reply_to_message.caption if msg.reply_to_message is defined and msg.reply_to_message.caption is defined else ''), + (msg.caption if msg.caption is defined else ''), + (msg.text if msg.text is defined else '') + ] | select('ne', '') | join(' ') | trim }} file_path: "{{ file.file_path if file.file_path is defined }}" mime_type: "{{ file.mime_type if file.mime_type is defined }}" - telegram_chat_id: "{{ trigger.json.message.chat.id }}" - telegram_message_thread_id: "{{trigger.json.message.message_thread_id if trigger.json.message.message_thread_id is defined }}" + telegram_chat_id: "{{ msg.chat.id }}" + telegram_message_thread_id: "{{ msg.message_thread_id if msg.message_thread_id is defined }}" supported: "{{ file.supported if file.supported is defined }}" alias: Process document + - conditions: + - condition: template + value_template: "{{ media_type == 'text' }}" + sequence: + - variables: + agent_input: + system: |- + Respond in language: {{ language | trim }} + prompt: >- + {{ [ + (msg.reply_to_message.text if msg.reply_to_message is defined and msg.reply_to_message.text is defined else ''), + (msg.reply_to_message.caption if msg.reply_to_message is defined and msg.reply_to_message.caption is defined else ''), + msg.text + ] | select('ne', '') | join(' ') | trim }} + telegram_chat_id: "{{ msg.chat.id }}" + telegram_message_thread_id: "{{ msg.message_thread_id if msg.message_thread_id is defined }}" + alias: Process text - choose: + - conditions: + - condition: template + value_template: "{{ file is defined and file.error is defined }}" + sequence: + - action: pyscript.send_telegram_message + data: + chat_id: "{{ msg.chat.id }}" + message: "{{ file.error }}" + reply_to_message_id: "{{ msg.message_id }}" + response_variable: telegram_response - conditions: - condition: template value_template: "{{ supported is defined and not supported | bool(true) }}" sequence: - action: pyscript.send_telegram_message data: - chat_id: "{{ trigger.json.message.chat.id }}" + chat_id: "{{ msg.chat.id }}" message: This file type is not supported for analysis at the moment. The file has been stored without further processing. - reply_to_message_id: "{{ trigger.json.message.message_id }}" + reply_to_message_id: "{{ msg.message_id }}" response_variable: telegram_response - conditions: - condition: template @@ -343,17 +376,17 @@ actions: sequence: - if: - condition: template - value_template: "{{ trigger.json.message.message_thread_id is defined }}" + value_template: "{{ msg.message_thread_id is defined }}" then: - action: pyscript.send_telegram_chat_action data: - chat_id: "{{ trigger.json.message.chat.id }}" - message_thread_id: "{{ trigger.json.message.message_thread_id }}" + chat_id: "{{ msg.chat.id }}" + message_thread_id: "{{ msg.message_thread_id }}" response_variable: telegram_chat_action else: - action: pyscript.send_telegram_chat_action data: - chat_id: "{{ trigger.json.message.chat.id }}" + chat_id: "{{ msg.chat.id }}" response_variable: telegram_chat_action - if: - condition: template @@ -393,14 +426,14 @@ actions: response_variable: set_chat - action: pyscript.send_telegram_message data: - chat_id: "{{ trigger.json.message.chat.id }}" + chat_id: "{{ msg.chat.id }}" message: "{{ response | trim }}" - reply_to_message_id: "{{ trigger.json.message.message_id }}" + reply_to_message_id: "{{ msg.message_id }}" response_variable: telegram_response default: - action: pyscript.send_telegram_message data: - chat_id: "{{ trigger.json.message.chat.id }}" + chat_id: "{{ msg.chat.id }}" message: This type of message is not supported by the current handler. - reply_to_message_id: "{{ trigger.json.message.message_id }}" + reply_to_message_id: "{{ msg.message_id }}" response_variable: telegram_response diff --git a/zalo_bot_webhook.yaml b/zalo_bot_webhook.yaml index 0fe83ad..037af5e 100644 --- a/zalo_bot_webhook.yaml +++ b/zalo_bot_webhook.yaml @@ -135,7 +135,7 @@ conditions: actions: - variables: chat_id: >- - {{ ('id_' ~ trigger.json.message.chat.id ~ '_' ~ trigger.json.message.from.id) | slugify }} + {{ ('id_' ~ trigger.json.message.chat.id ~ ('_' ~ trigger.json.message.from.id if trigger.json.message.from is defined and trigger.json.message.from.id is defined else '')) | slugify }} - action: pyscript.memory_cache_get data: key: "{{ chat_id }}" @@ -153,7 +153,7 @@ actions: system: |- Respond in language: {{ language | trim }} prompt: |- - {{ trigger.json.message.text }} + {{ trigger.json.message.text | trim }} zalo_chat_id: "{{ trigger.json.message.chat.id }}" alias: Process text - conditions: @@ -162,20 +162,29 @@ actions: sequence: - action: pyscript.get_zalo_file data: - url: "{{ trigger.json.message.photo_url }}" + url: "{{ trigger.json.message.photo_url if trigger.json.message.photo_url is defined else trigger.json.message.photo }}" response_variable: file - variables: agent_input: system: |- Use `File Content Analyzer`. If prompt empty, follow file instructions. Else return summary. No confirmation. Language: {{ language | trim }} prompt: |- - {{ trigger.json.message.caption }} + {{ (trigger.json.message.caption if trigger.json.message.caption is defined else trigger.json.message.text) | default('') | trim }} file_path: "{{ file.file_path if file.file_path is defined }}" mime_type: "{{ file.mime_type if file.mime_type is defined }}" zalo_chat_id: "{{ trigger.json.message.chat.id }}" supported: "{{ file.supported if file.supported is defined }}" alias: Process photo - choose: + - conditions: + - condition: template + value_template: "{{ file is defined and file.error is defined }}" + sequence: + - action: pyscript.send_zalo_message + data: + chat_id: "{{ trigger.json.message.chat.id }}" + message: "{{ file.error }}" + response_variable: zalo_response - conditions: - condition: template value_template: "{{ supported is defined and not supported | bool(true) }}"