Update blocks

This commit is contained in:
Krzysztof Czerwinski
2026-02-17 15:14:50 +09:00
parent eb285eadd9
commit b6d7e9ad8c
6 changed files with 976 additions and 55 deletions

View File

@@ -5,6 +5,7 @@ Provides utilities for making authenticated requests to the Telegram Bot API.
"""
import logging
from io import BytesIO
from typing import Any, Optional
from backend.data.model import APIKeyCredentials
@@ -66,6 +67,49 @@ async def call_telegram_api(
return result.get("result", {})
async def call_telegram_api_with_file(
credentials: APIKeyCredentials,
method: str,
file_field: str,
file_data: bytes,
filename: str,
content_type: str,
data: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
"""
Make a multipart/form-data request to the Telegram Bot API with a file upload.
Args:
credentials: Bot token credentials
method: API method name (e.g., "sendPhoto", "sendVoice")
file_field: Form field name for the file (e.g., "photo", "voice")
file_data: Raw file bytes
filename: Filename for the upload
content_type: MIME type of the file
data: Additional form parameters
Returns:
API response result
Raises:
TelegramAPIException: If the API returns an error
"""
token = credentials.api_key.get_secret_value()
url = get_bot_api_url(token, method)
files = [(file_field, (filename, BytesIO(file_data), content_type))]
response = await Requests().post(url, files=files, data=data or {})
result = response.json()
if not result.get("ok"):
error_code = result.get("error_code", 0)
description = result.get("description", "Unknown error")
raise TelegramAPIException(description, error_code)
return result.get("result", {})
async def get_file_info(
credentials: APIKeyCredentials, file_id: str
) -> dict[str, Any]:

View File

@@ -43,5 +43,5 @@ TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}

View File

@@ -1,5 +1,5 @@
"""
Telegram action blocks for sending messages, photos, and voice messages.
Telegram action blocks for sending messages, media, and managing chat content.
"""
import base64
@@ -16,10 +16,10 @@ from backend.data.block import (
)
from backend.data.execution import ExecutionContext
from backend.data.model import APIKeyCredentials, SchemaField
from backend.util.file import store_media_file
from backend.util.file import get_exec_file_path, store_media_file
from backend.util.type import MediaFileType
from ._api import call_telegram_api, download_telegram_file
from ._api import call_telegram_api, call_telegram_api_with_file, download_telegram_file
from ._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
class ParseMode(str, Enum):
"""Telegram message parse modes."""
NONE = ""
NONE = "none"
MARKDOWN = "Markdown"
MARKDOWNV2 = "MarkdownV2"
HTML = "HTML"
@@ -73,7 +73,7 @@ class SendTelegramMessageBlock(Block):
def __init__(self):
super().__init__(
id="b2c3d4e5-f6a7-8901-bcde-f23456789012",
id="787678ad-1f47-4efc-89df-643f9908621a",
description="Send a text message to a Telegram chat.",
categories={BlockCategory.SOCIAL},
input_schema=SendTelegramMessageBlock.Input,
@@ -108,7 +108,7 @@ class SendTelegramMessageBlock(Block):
"chat_id": chat_id,
"text": text,
}
if parse_mode:
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
if reply_to_message_id:
data["reply_to_message_id"] = reply_to_message_id
@@ -169,7 +169,7 @@ class SendTelegramPhotoBlock(Block):
def __init__(self):
super().__init__(
id="c3d4e5f6-a7b8-9012-cdef-345678901234",
id="ceb9fd95-fd95-49ff-b57b-278957255716",
description="Send a photo to a Telegram chat.",
categories={BlockCategory.SOCIAL},
input_schema=SendTelegramPhotoBlock.Input,
@@ -185,32 +185,63 @@ class SendTelegramPhotoBlock(Block):
("status", "Photo sent"),
],
test_mock={
"_send_photo": lambda *args, **kwargs: {"message_id": 123}
"_send_photo_url": lambda *args, **kwargs: {"message_id": 123}
},
)
async def _send_photo(
async def _send_photo_url(
self,
credentials: APIKeyCredentials,
chat_id: int,
photo_data: str,
photo_url: str,
caption: str,
parse_mode: str,
reply_to_message_id: Optional[int],
) -> dict:
data: dict = {
"chat_id": chat_id,
"photo": photo_data,
"photo": photo_url,
}
if caption:
data["caption"] = caption
if parse_mode:
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
if reply_to_message_id:
data["reply_to_message_id"] = reply_to_message_id
return await call_telegram_api(credentials, "sendPhoto", data)
async def _send_photo_file(
self,
credentials: APIKeyCredentials,
chat_id: int,
file_path: str,
caption: str,
parse_mode: str,
reply_to_message_id: Optional[int],
) -> dict:
from pathlib import Path
path = Path(file_path)
file_bytes = path.read_bytes()
data: dict = {"chat_id": str(chat_id)}
if caption:
data["caption"] = caption
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
if reply_to_message_id:
data["reply_to_message_id"] = str(reply_to_message_id)
return await call_telegram_api_with_file(
credentials,
"sendPhoto",
file_field="photo",
file_data=file_bytes,
filename=path.name,
content_type="image/jpeg",
data=data,
)
async def run(
self,
input_data: Input,
@@ -224,23 +255,36 @@ class SendTelegramPhotoBlock(Block):
# If it's a URL, pass it directly to Telegram (it will fetch it)
if photo_input.startswith(("http://", "https://")):
photo_data = photo_input
result = await self._send_photo_url(
credentials=credentials,
chat_id=input_data.chat_id,
photo_url=photo_input,
caption=input_data.caption,
parse_mode=input_data.parse_mode.value,
reply_to_message_id=input_data.reply_to_message_id,
)
else:
# For data URIs or workspace:// references, use store_media_file
photo_data = await store_media_file(
file=photo_input,
# For data URIs or workspace:// references, resolve to local
# file and upload via multipart/form-data
relative_path = await store_media_file(
file=input_data.photo,
execution_context=execution_context,
return_format="for_external_api",
return_format="for_local_processing",
)
if not execution_context.graph_exec_id:
raise ValueError("graph_exec_id is required")
abs_path = get_exec_file_path(
execution_context.graph_exec_id, relative_path
)
result = await self._send_photo_file(
credentials=credentials,
chat_id=input_data.chat_id,
file_path=abs_path,
caption=input_data.caption,
parse_mode=input_data.parse_mode.value,
reply_to_message_id=input_data.reply_to_message_id,
)
result = await self._send_photo(
credentials=credentials,
chat_id=input_data.chat_id,
photo_data=photo_data,
caption=input_data.caption,
parse_mode=input_data.parse_mode.value,
reply_to_message_id=input_data.reply_to_message_id,
)
yield "message_id", result.get("message_id", 0)
yield "status", "Photo sent"
except Exception as e:
@@ -281,7 +325,7 @@ class SendTelegramVoiceBlock(Block):
def __init__(self):
super().__init__(
id="d4e5f6a7-b8c9-0123-def0-456789012345",
id="7a0ef660-1a5b-4951-8642-c13a0c8d6d93",
description="Send a voice message to a Telegram chat. "
"Voice must be OGG format with OPUS codec.",
categories={BlockCategory.SOCIAL},
@@ -298,22 +342,22 @@ class SendTelegramVoiceBlock(Block):
("status", "Voice sent"),
],
test_mock={
"_send_voice": lambda *args, **kwargs: {"message_id": 123}
"_send_voice_url": lambda *args, **kwargs: {"message_id": 123}
},
)
async def _send_voice(
async def _send_voice_url(
self,
credentials: APIKeyCredentials,
chat_id: int,
voice_data: str,
voice_url: str,
caption: str,
duration: Optional[int],
reply_to_message_id: Optional[int],
) -> dict:
data: dict = {
"chat_id": chat_id,
"voice": voice_data,
"voice": voice_url,
}
if caption:
data["caption"] = caption
@@ -324,6 +368,37 @@ class SendTelegramVoiceBlock(Block):
return await call_telegram_api(credentials, "sendVoice", data)
async def _send_voice_file(
self,
credentials: APIKeyCredentials,
chat_id: int,
file_path: str,
caption: str,
duration: Optional[int],
reply_to_message_id: Optional[int],
) -> dict:
from pathlib import Path
path = Path(file_path)
file_bytes = path.read_bytes()
data: dict = {"chat_id": str(chat_id)}
if caption:
data["caption"] = caption
if duration:
data["duration"] = str(duration)
if reply_to_message_id:
data["reply_to_message_id"] = str(reply_to_message_id)
return await call_telegram_api_with_file(
credentials,
"sendVoice",
file_field="voice",
file_data=file_bytes,
filename=path.name,
content_type="audio/ogg",
data=data,
)
async def run(
self,
input_data: Input,
@@ -337,22 +412,36 @@ class SendTelegramVoiceBlock(Block):
# If it's a URL, pass it directly to Telegram
if voice_input.startswith(("http://", "https://")):
voice_data = voice_input
result = await self._send_voice_url(
credentials=credentials,
chat_id=input_data.chat_id,
voice_url=voice_input,
caption=input_data.caption,
duration=input_data.duration,
reply_to_message_id=input_data.reply_to_message_id,
)
else:
voice_data = await store_media_file(
file=voice_input,
# For data URIs or workspace:// references, resolve to local
# file and upload via multipart/form-data
relative_path = await store_media_file(
file=input_data.voice,
execution_context=execution_context,
return_format="for_external_api",
return_format="for_local_processing",
)
if not execution_context.graph_exec_id:
raise ValueError("graph_exec_id is required")
abs_path = get_exec_file_path(
execution_context.graph_exec_id, relative_path
)
result = await self._send_voice_file(
credentials=credentials,
chat_id=input_data.chat_id,
file_path=abs_path,
caption=input_data.caption,
duration=input_data.duration,
reply_to_message_id=input_data.reply_to_message_id,
)
result = await self._send_voice(
credentials=credentials,
chat_id=input_data.chat_id,
voice_data=voice_data,
caption=input_data.caption,
duration=input_data.duration,
reply_to_message_id=input_data.reply_to_message_id,
)
yield "message_id", result.get("message_id", 0)
yield "status", "Voice sent"
except Exception as e:
@@ -383,7 +472,7 @@ class ReplyToTelegramMessageBlock(Block):
def __init__(self):
super().__init__(
id="e5f6a7b8-c9d0-1234-ef01-567890123456",
id="c2b1c976-844f-4b6c-ab21-4973d2ceab15",
description="Reply to a specific message in a Telegram chat.",
categories={BlockCategory.SOCIAL},
input_schema=ReplyToTelegramMessageBlock.Input,
@@ -417,7 +506,7 @@ class ReplyToTelegramMessageBlock(Block):
"text": text,
"reply_to_message_id": reply_to_message_id,
}
if parse_mode:
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
return await call_telegram_api(credentials, "sendMessage", data)
@@ -457,7 +546,7 @@ class GetTelegramFileBlock(Block):
def __init__(self):
super().__init__(
id="f6a7b8c9-d0e1-2345-f012-678901234567",
id="b600aaf2-6272-40c6-b973-c3984747c5bd",
description="Download a file from Telegram using its file_id. "
"Use this to process photos, voice messages, or documents received.",
categories={BlockCategory.SOCIAL},
@@ -499,9 +588,9 @@ class GetTelegramFileBlock(Block):
file_id=input_data.file_id,
)
# Convert to data URI
# Convert to data URI and wrap as MediaFileType
mime_type = "application/octet-stream"
data_uri = (
data_uri = MediaFileType(
f"data:{mime_type};base64,"
f"{base64.b64encode(file_content).decode()}"
)
@@ -517,3 +606,658 @@ class GetTelegramFileBlock(Block):
yield "status", "File downloaded"
except Exception as e:
raise ValueError(f"Failed to download file: {e}")
class DeleteTelegramMessageBlock(Block):
"""Delete a message from a Telegram chat."""
class Input(BlockSchemaInput):
credentials: TelegramCredentialsInput = TelegramCredentialsField()
chat_id: int = SchemaField(
description="The chat ID containing the message"
)
message_id: int = SchemaField(
description="The ID of the message to delete"
)
class Output(BlockSchemaOutput):
status: str = SchemaField(description="Status of the operation")
def __init__(self):
super().__init__(
id="bb4fd91a-883e-4d29-9879-b06c4bb79d30",
description="Delete a message from a Telegram chat. "
"Bots can delete their own messages and incoming messages "
"in private chats at any time.",
categories={BlockCategory.SOCIAL},
input_schema=DeleteTelegramMessageBlock.Input,
output_schema=DeleteTelegramMessageBlock.Output,
test_input={
"chat_id": 12345678,
"message_id": 42,
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("status", "Message deleted"),
],
test_mock={
"_delete_message": lambda *args, **kwargs: True
},
)
async def _delete_message(
self,
credentials: APIKeyCredentials,
chat_id: int,
message_id: int,
) -> bool:
await call_telegram_api(
credentials,
"deleteMessage",
{"chat_id": chat_id, "message_id": message_id},
)
return True
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
await self._delete_message(
credentials=credentials,
chat_id=input_data.chat_id,
message_id=input_data.message_id,
)
yield "status", "Message deleted"
except Exception as e:
raise ValueError(f"Failed to delete message: {e}")
class EditTelegramMessageBlock(Block):
"""Edit the text of an existing message."""
class Input(BlockSchemaInput):
credentials: TelegramCredentialsInput = TelegramCredentialsField()
chat_id: int = SchemaField(
description="The chat ID containing the message"
)
message_id: int = SchemaField(
description="The ID of the message to edit"
)
text: str = SchemaField(
description="New text for the message (max 4096 characters)"
)
parse_mode: ParseMode = SchemaField(
description="Message formatting mode",
default=ParseMode.NONE,
advanced=True,
)
class Output(BlockSchemaOutput):
message_id: int = SchemaField(description="The ID of the edited message")
status: str = SchemaField(description="Status of the operation")
def __init__(self):
super().__init__(
id="c55816a2-37af-4901-ba19-36edcf2acfc1",
description="Edit the text of an existing message sent by the bot.",
categories={BlockCategory.SOCIAL},
input_schema=EditTelegramMessageBlock.Input,
output_schema=EditTelegramMessageBlock.Output,
test_input={
"chat_id": 12345678,
"message_id": 42,
"text": "Updated text!",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("message_id", 42),
("status", "Message edited"),
],
test_mock={
"_edit_message": lambda *args, **kwargs: {"message_id": 42}
},
)
async def _edit_message(
self,
credentials: APIKeyCredentials,
chat_id: int,
message_id: int,
text: str,
parse_mode: str,
) -> dict:
data: dict = {
"chat_id": chat_id,
"message_id": message_id,
"text": text,
}
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
return await call_telegram_api(credentials, "editMessageText", data)
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
try:
result = await self._edit_message(
credentials=credentials,
chat_id=input_data.chat_id,
message_id=input_data.message_id,
text=input_data.text,
parse_mode=input_data.parse_mode.value,
)
yield "message_id", result.get("message_id", 0)
yield "status", "Message edited"
except Exception as e:
raise ValueError(f"Failed to edit message: {e}")
class SendTelegramAudioBlock(Block):
"""Send an audio file to a Telegram chat, displayed in the music player."""
class Input(BlockSchemaInput):
credentials: TelegramCredentialsInput = TelegramCredentialsField()
chat_id: int = SchemaField(
description="The chat ID to send the audio to"
)
audio: MediaFileType = SchemaField(
description="Audio file to send (MP3 or M4A format). "
"Can be URL, data URI, or workspace:// reference."
)
caption: str = SchemaField(
description="Caption for the audio file",
default="",
advanced=True,
)
title: str = SchemaField(
description="Track title",
default="",
advanced=True,
)
performer: str = SchemaField(
description="Track performer/artist",
default="",
advanced=True,
)
duration: Optional[int] = SchemaField(
description="Duration in seconds",
default=None,
advanced=True,
)
reply_to_message_id: Optional[int] = SchemaField(
description="Message ID to reply to",
default=None,
advanced=True,
)
class Output(BlockSchemaOutput):
message_id: int = SchemaField(description="The ID of the sent message")
status: str = SchemaField(description="Status of the operation")
def __init__(self):
super().__init__(
id="9044829a-7915-4ab4-a50f-89922ba3679f",
description="Send an audio file to a Telegram chat. "
"The file is displayed in the music player. "
"For voice messages, use the Send Voice block instead.",
categories={BlockCategory.SOCIAL},
input_schema=SendTelegramAudioBlock.Input,
output_schema=SendTelegramAudioBlock.Output,
test_input={
"chat_id": 12345678,
"audio": "https://example.com/track.mp3",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("message_id", 123),
("status", "Audio sent"),
],
test_mock={
"_send_audio_url": lambda *args, **kwargs: {"message_id": 123}
},
)
async def _send_audio_url(
self,
credentials: APIKeyCredentials,
chat_id: int,
audio_url: str,
caption: str,
title: str,
performer: str,
duration: Optional[int],
reply_to_message_id: Optional[int],
) -> dict:
data: dict = {
"chat_id": chat_id,
"audio": audio_url,
}
if caption:
data["caption"] = caption
if title:
data["title"] = title
if performer:
data["performer"] = performer
if duration:
data["duration"] = duration
if reply_to_message_id:
data["reply_to_message_id"] = reply_to_message_id
return await call_telegram_api(credentials, "sendAudio", data)
async def _send_audio_file(
self,
credentials: APIKeyCredentials,
chat_id: int,
file_path: str,
caption: str,
title: str,
performer: str,
duration: Optional[int],
reply_to_message_id: Optional[int],
) -> dict:
from pathlib import Path
path = Path(file_path)
file_bytes = path.read_bytes()
data: dict = {"chat_id": str(chat_id)}
if caption:
data["caption"] = caption
if title:
data["title"] = title
if performer:
data["performer"] = performer
if duration:
data["duration"] = str(duration)
if reply_to_message_id:
data["reply_to_message_id"] = str(reply_to_message_id)
return await call_telegram_api_with_file(
credentials,
"sendAudio",
file_field="audio",
file_data=file_bytes,
filename=path.name,
content_type="audio/mpeg",
data=data,
)
async def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
execution_context: ExecutionContext,
**kwargs,
) -> BlockOutput:
try:
audio_input = input_data.audio
if audio_input.startswith(("http://", "https://")):
result = await self._send_audio_url(
credentials=credentials,
chat_id=input_data.chat_id,
audio_url=audio_input,
caption=input_data.caption,
title=input_data.title,
performer=input_data.performer,
duration=input_data.duration,
reply_to_message_id=input_data.reply_to_message_id,
)
else:
relative_path = await store_media_file(
file=input_data.audio,
execution_context=execution_context,
return_format="for_local_processing",
)
if not execution_context.graph_exec_id:
raise ValueError("graph_exec_id is required")
abs_path = get_exec_file_path(
execution_context.graph_exec_id, relative_path
)
result = await self._send_audio_file(
credentials=credentials,
chat_id=input_data.chat_id,
file_path=abs_path,
caption=input_data.caption,
title=input_data.title,
performer=input_data.performer,
duration=input_data.duration,
reply_to_message_id=input_data.reply_to_message_id,
)
yield "message_id", result.get("message_id", 0)
yield "status", "Audio sent"
except Exception as e:
raise ValueError(f"Failed to send audio: {e}")
class SendTelegramDocumentBlock(Block):
"""Send a document to a Telegram chat."""
class Input(BlockSchemaInput):
credentials: TelegramCredentialsInput = TelegramCredentialsField()
chat_id: int = SchemaField(
description="The chat ID to send the document to"
)
document: MediaFileType = SchemaField(
description="Document to send (any file type). "
"Can be URL, data URI, or workspace:// reference."
)
filename: str = SchemaField(
description="Filename shown to the recipient. "
"If empty, the original filename is used (may be a random ID "
"for uploaded files).",
default="",
)
caption: str = SchemaField(
description="Caption for the document",
default="",
advanced=True,
)
parse_mode: ParseMode = SchemaField(
description="Caption formatting mode",
default=ParseMode.NONE,
advanced=True,
)
reply_to_message_id: Optional[int] = SchemaField(
description="Message ID to reply to",
default=None,
advanced=True,
)
class Output(BlockSchemaOutput):
message_id: int = SchemaField(description="The ID of the sent message")
status: str = SchemaField(description="Status of the operation")
def __init__(self):
super().__init__(
id="554f4bad-4c99-48ba-9d1c-75840b71c5ad",
description="Send a document (any file type) to a Telegram chat.",
categories={BlockCategory.SOCIAL},
input_schema=SendTelegramDocumentBlock.Input,
output_schema=SendTelegramDocumentBlock.Output,
test_input={
"chat_id": 12345678,
"document": "https://example.com/file.pdf",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("message_id", 123),
("status", "Document sent"),
],
test_mock={
"_send_document_url": lambda *args, **kwargs: {"message_id": 123}
},
)
async def _send_document_url(
self,
credentials: APIKeyCredentials,
chat_id: int,
document_url: str,
caption: str,
parse_mode: str,
reply_to_message_id: Optional[int],
) -> dict:
data: dict = {
"chat_id": chat_id,
"document": document_url,
}
if caption:
data["caption"] = caption
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
if reply_to_message_id:
data["reply_to_message_id"] = reply_to_message_id
return await call_telegram_api(credentials, "sendDocument", data)
async def _send_document_file(
self,
credentials: APIKeyCredentials,
chat_id: int,
file_path: str,
display_filename: str,
caption: str,
parse_mode: str,
reply_to_message_id: Optional[int],
) -> dict:
from pathlib import Path
path = Path(file_path)
file_bytes = path.read_bytes()
data: dict = {"chat_id": str(chat_id)}
if caption:
data["caption"] = caption
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
if reply_to_message_id:
data["reply_to_message_id"] = str(reply_to_message_id)
return await call_telegram_api_with_file(
credentials,
"sendDocument",
file_field="document",
file_data=file_bytes,
filename=display_filename or path.name,
content_type="application/octet-stream",
data=data,
)
async def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
execution_context: ExecutionContext,
**kwargs,
) -> BlockOutput:
try:
doc_input = input_data.document
if doc_input.startswith(("http://", "https://")):
result = await self._send_document_url(
credentials=credentials,
chat_id=input_data.chat_id,
document_url=doc_input,
caption=input_data.caption,
parse_mode=input_data.parse_mode.value,
reply_to_message_id=input_data.reply_to_message_id,
)
else:
relative_path = await store_media_file(
file=input_data.document,
execution_context=execution_context,
return_format="for_local_processing",
)
if not execution_context.graph_exec_id:
raise ValueError("graph_exec_id is required")
abs_path = get_exec_file_path(
execution_context.graph_exec_id, relative_path
)
result = await self._send_document_file(
credentials=credentials,
chat_id=input_data.chat_id,
file_path=abs_path,
display_filename=input_data.filename,
caption=input_data.caption,
parse_mode=input_data.parse_mode.value,
reply_to_message_id=input_data.reply_to_message_id,
)
yield "message_id", result.get("message_id", 0)
yield "status", "Document sent"
except Exception as e:
raise ValueError(f"Failed to send document: {e}")
class SendTelegramVideoBlock(Block):
"""Send a video to a Telegram chat."""
class Input(BlockSchemaInput):
credentials: TelegramCredentialsInput = TelegramCredentialsField()
chat_id: int = SchemaField(
description="The chat ID to send the video to"
)
video: MediaFileType = SchemaField(
description="Video to send (MP4 format). "
"Can be URL, data URI, or workspace:// reference."
)
caption: str = SchemaField(
description="Caption for the video",
default="",
advanced=True,
)
parse_mode: ParseMode = SchemaField(
description="Caption formatting mode",
default=ParseMode.NONE,
advanced=True,
)
duration: Optional[int] = SchemaField(
description="Duration in seconds",
default=None,
advanced=True,
)
reply_to_message_id: Optional[int] = SchemaField(
description="Message ID to reply to",
default=None,
advanced=True,
)
class Output(BlockSchemaOutput):
message_id: int = SchemaField(description="The ID of the sent message")
status: str = SchemaField(description="Status of the operation")
def __init__(self):
super().__init__(
id="cda075af-3f31-47b0-baa9-0050b3bd78bd",
description="Send a video to a Telegram chat.",
categories={BlockCategory.SOCIAL},
input_schema=SendTelegramVideoBlock.Input,
output_schema=SendTelegramVideoBlock.Output,
test_input={
"chat_id": 12345678,
"video": "https://example.com/video.mp4",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("message_id", 123),
("status", "Video sent"),
],
test_mock={
"_send_video_url": lambda *args, **kwargs: {"message_id": 123}
},
)
async def _send_video_url(
self,
credentials: APIKeyCredentials,
chat_id: int,
video_url: str,
caption: str,
parse_mode: str,
duration: Optional[int],
reply_to_message_id: Optional[int],
) -> dict:
data: dict = {
"chat_id": chat_id,
"video": video_url,
}
if caption:
data["caption"] = caption
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
if duration:
data["duration"] = duration
if reply_to_message_id:
data["reply_to_message_id"] = reply_to_message_id
return await call_telegram_api(credentials, "sendVideo", data)
async def _send_video_file(
self,
credentials: APIKeyCredentials,
chat_id: int,
file_path: str,
caption: str,
parse_mode: str,
duration: Optional[int],
reply_to_message_id: Optional[int],
) -> dict:
from pathlib import Path
path = Path(file_path)
file_bytes = path.read_bytes()
data: dict = {"chat_id": str(chat_id)}
if caption:
data["caption"] = caption
if parse_mode and parse_mode != "none":
data["parse_mode"] = parse_mode
if duration:
data["duration"] = str(duration)
if reply_to_message_id:
data["reply_to_message_id"] = str(reply_to_message_id)
return await call_telegram_api_with_file(
credentials,
"sendVideo",
file_field="video",
file_data=file_bytes,
filename=path.name,
content_type="video/mp4",
data=data,
)
async def run(
self,
input_data: Input,
*,
credentials: APIKeyCredentials,
execution_context: ExecutionContext,
**kwargs,
) -> BlockOutput:
try:
video_input = input_data.video
if video_input.startswith(("http://", "https://")):
result = await self._send_video_url(
credentials=credentials,
chat_id=input_data.chat_id,
video_url=video_input,
caption=input_data.caption,
parse_mode=input_data.parse_mode.value,
duration=input_data.duration,
reply_to_message_id=input_data.reply_to_message_id,
)
else:
relative_path = await store_media_file(
file=input_data.video,
execution_context=execution_context,
return_format="for_local_processing",
)
if not execution_context.graph_exec_id:
raise ValueError("graph_exec_id is required")
abs_path = get_exec_file_path(
execution_context.graph_exec_id, relative_path
)
result = await self._send_video_file(
credentials=credentials,
chat_id=input_data.chat_id,
file_path=abs_path,
caption=input_data.caption,
parse_mode=input_data.parse_mode.value,
duration=input_data.duration,
reply_to_message_id=input_data.reply_to_message_id,
)
yield "message_id", result.get("message_id", 0)
yield "status", "Video sent"
except Exception as e:
raise ValueError(f"Failed to send video: {e}")

View File

@@ -117,11 +117,18 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
description="File ID of the audio file (for audio messages). "
"Use GetTelegramFileBlock to download."
)
file_id: str = SchemaField(
description="File ID for document/video messages. "
"Use GetTelegramFileBlock to download."
)
file_name: str = SchemaField(
description="Original filename (for document/audio messages)"
)
caption: str = SchemaField(description="Caption for media messages")
def __init__(self):
super().__init__(
id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
id="4435e4e0-df6e-4301-8f35-ad70b12fc9ec",
description="Triggers when a message is received by your Telegram bot. "
"Supports text, photos, voice messages, and audio files.",
categories={BlockCategory.SOCIAL},
@@ -152,6 +159,8 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
("photo_file_id", ""),
("voice_file_id", ""),
("audio_file_id", ""),
("file_id", ""),
("file_name", ""),
("caption", ""),
],
)
@@ -178,16 +187,20 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
yield "photo_file_id", ""
yield "voice_file_id", ""
yield "audio_file_id", ""
yield "file_id", ""
yield "file_name", ""
yield "caption", ""
elif "photo" in message:
# Get the largest photo (last in array)
photos = message.get("photo", [])
file_id = photos[-1]["file_id"] if photos else ""
photo_fid = photos[-1]["file_id"] if photos else ""
yield "event", "photo"
yield "text", ""
yield "photo_file_id", file_id
yield "photo_file_id", photo_fid
yield "voice_file_id", ""
yield "audio_file_id", ""
yield "file_id", ""
yield "file_name", ""
yield "caption", message.get("caption", "")
elif "voice" in message:
voice = message.get("voice", {})
@@ -196,6 +209,8 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
yield "photo_file_id", ""
yield "voice_file_id", voice.get("file_id", "")
yield "audio_file_id", ""
yield "file_id", ""
yield "file_name", ""
yield "caption", message.get("caption", "")
elif "audio" in message:
audio = message.get("audio", {})
@@ -204,6 +219,8 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
yield "photo_file_id", ""
yield "voice_file_id", ""
yield "audio_file_id", audio.get("file_id", "")
yield "file_id", ""
yield "file_name", audio.get("file_name", "")
yield "caption", message.get("caption", "")
elif "document" in message:
document = message.get("document", {})
@@ -211,7 +228,9 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
yield "text", ""
yield "photo_file_id", ""
yield "voice_file_id", ""
yield "audio_file_id", document.get("file_id", "")
yield "audio_file_id", ""
yield "file_id", document.get("file_id", "")
yield "file_name", document.get("file_name", "")
yield "caption", message.get("caption", "")
elif "video" in message:
video = message.get("video", {})
@@ -219,7 +238,9 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
yield "text", ""
yield "photo_file_id", ""
yield "voice_file_id", ""
yield "audio_file_id", video.get("file_id", "")
yield "audio_file_id", ""
yield "file_id", video.get("file_id", "")
yield "file_name", video.get("file_name", "")
yield "caption", message.get("caption", "")
else:
yield "event", "other"
@@ -227,4 +248,113 @@ class TelegramMessageTriggerBlock(TelegramTriggerBase, Block):
yield "photo_file_id", ""
yield "voice_file_id", ""
yield "audio_file_id", ""
yield "file_id", ""
yield "file_name", ""
yield "caption", ""
# Example payload for reaction trigger testing
EXAMPLE_REACTION_PAYLOAD = {
"update_id": 123456790,
"message_reaction": {
"chat": {
"id": 12345678,
"first_name": "John",
"last_name": "Doe",
"username": "johndoe",
"type": "private",
},
"message_id": 42,
"user": {
"id": 12345678,
"is_bot": False,
"first_name": "John",
"username": "johndoe",
},
"date": 1234567890,
"new_reaction": [{"type": "emoji", "emoji": "👍"}],
"old_reaction": [],
},
}
class TelegramMessageReactionTriggerBlock(TelegramTriggerBase, Block):
"""
Triggers when a reaction to a message is changed.
Works automatically in private chats. In group chats, the bot must be
an administrator to receive reaction updates.
"""
class Input(TelegramTriggerBase.Input):
pass
class Output(BlockSchemaOutput):
payload: dict = SchemaField(
description="The complete webhook payload from Telegram"
)
chat_id: int = SchemaField(
description="The chat ID where the reaction occurred"
)
message_id: int = SchemaField(
description="The message ID that was reacted to"
)
user_id: int = SchemaField(
description="The user ID who changed the reaction"
)
username: str = SchemaField(
description="Username of the user (may be empty)"
)
new_reactions: list = SchemaField(
description="List of new reactions on the message"
)
old_reactions: list = SchemaField(
description="List of previous reactions on the message"
)
def __init__(self):
super().__init__(
id="82525328-9368-4966-8f0c-cd78e80181fd",
description="Triggers when a reaction to a message is changed. "
"Works in private chats automatically. "
"In groups, the bot must be an administrator.",
categories={BlockCategory.SOCIAL},
input_schema=TelegramMessageReactionTriggerBlock.Input,
output_schema=TelegramMessageReactionTriggerBlock.Output,
webhook_config=BlockWebhookConfig(
provider=ProviderName.TELEGRAM,
webhook_type=TelegramWebhookType.BOT,
resource_format="bot",
event_filter_input="",
event_format="message_reaction",
),
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"payload": EXAMPLE_REACTION_PAYLOAD,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("payload", EXAMPLE_REACTION_PAYLOAD),
("chat_id", 12345678),
("message_id", 42),
("user_id", 12345678),
("username", "johndoe"),
("new_reactions", [{"type": "emoji", "emoji": "👍"}]),
("old_reactions", []),
],
)
async def run(self, input_data: Input, **kwargs) -> BlockOutput:
payload = input_data.payload
reaction = payload.get("message_reaction", {})
chat = reaction.get("chat", {})
user = reaction.get("user", {})
yield "payload", payload
yield "chat_id", chat.get("id", 0)
yield "message_id", reaction.get("message_id", 0)
yield "user_id", user.get("id", 0)
yield "username", user.get("username", "")
yield "new_reactions", reaction.get("new_reaction", [])
yield "old_reactions", reaction.get("old_reaction", [])

View File

@@ -83,6 +83,8 @@ class TelegramWebhooksManager(BaseWebhooksManager[TelegramWebhookType]):
event_type = "message.other"
elif "edited_message" in payload:
event_type = "edited_message"
elif "message_reaction" in payload:
event_type = "message_reaction"
elif "callback_query" in payload:
event_type = "callback_query"
else:
@@ -123,7 +125,7 @@ class TelegramWebhooksManager(BaseWebhooksManager[TelegramWebhookType]):
webhook_data = {
"url": ingress_url,
"secret_token": secret,
"allowed_updates": ["message", "edited_message"],
"allowed_updates": ["message", "edited_message", "message_reaction"],
}
response = await Requests().post(url, json=webhook_data)

View File

@@ -54,7 +54,8 @@ export default function BuilderPage() {
);
}
return isNewFlowEditorEnabled ? (
// return isNewFlowEditorEnabled ? (
return true ? (
<ReactFlowProvider>
<Flow />
</ReactFlowProvider>