124 lines
5.0 KiB
Python
124 lines
5.0 KiB
Python
"""
|
|
tools/telegram_tool.py — Outbound Telegram messages for agents.
|
|
|
|
Sends to whitelisted chat IDs only. No confirmation required.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
from ..database import credential_store
|
|
from ..telegram.triggers import is_allowed
|
|
from .base import BaseTool, ToolResult
|
|
|
|
_API = "https://api.telegram.org/bot{token}/sendMessage"
|
|
|
|
|
|
class TelegramTool(BaseTool):
|
|
name = "telegram"
|
|
description = (
|
|
"Send a Telegram message to a whitelisted chat ID. "
|
|
"Use for notifications, alerts, and replies to Telegram users. "
|
|
"chat_id must be in the Telegram whitelist."
|
|
)
|
|
input_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"chat_id": {
|
|
"type": "string",
|
|
"description": "Telegram chat ID to send the message to",
|
|
},
|
|
"message": {
|
|
"type": "string",
|
|
"description": "Message text (plain text, max 4096 characters)",
|
|
},
|
|
},
|
|
"required": ["chat_id", "message"],
|
|
}
|
|
requires_confirmation = False
|
|
allowed_in_scheduled_tasks = True
|
|
|
|
async def execute(self, chat_id: str, message: str, **kwargs) -> ToolResult:
|
|
# Resolve current user for per-user token and whitelist check
|
|
from ..context_vars import current_user as _current_user
|
|
user = _current_user.get(None)
|
|
|
|
# Try global token first, then per-user token
|
|
token = await credential_store.get("telegram:bot_token")
|
|
if not token and user:
|
|
from ..database import user_settings_store
|
|
token = await user_settings_store.get(user.id, "telegram_bot_token")
|
|
if not token:
|
|
return ToolResult(
|
|
success=False,
|
|
error="Telegram is not configured. Add telegram:bot_token in Settings → Credentials.",
|
|
)
|
|
|
|
# Security: chat_id must be whitelisted (check user scope, then global)
|
|
allowed = await is_allowed(chat_id, user_id=user.id if user else "GLOBAL")
|
|
if not allowed and user:
|
|
allowed = await is_allowed(chat_id, user_id="GLOBAL")
|
|
if not allowed:
|
|
# Return whitelisted IDs so the model can retry with a real one
|
|
from ..telegram.triggers import list_whitelist
|
|
global_ids = [r["chat_id"] for r in await list_whitelist("GLOBAL")]
|
|
user_ids = [r["chat_id"] for r in await list_whitelist(user.id)] if user else []
|
|
all_ids = list(dict.fromkeys(global_ids + user_ids)) # deduplicate, preserve order
|
|
hint = f" Whitelisted chat_ids: {', '.join(all_ids)}." if all_ids else " No chat IDs are whitelisted yet — add one in Settings → Telegram."
|
|
return ToolResult(
|
|
success=False,
|
|
error=f"chat_id '{chat_id}' is not in the Telegram whitelist.{hint}",
|
|
)
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as http:
|
|
resp = await http.post(
|
|
_API.format(token=token),
|
|
json={"chat_id": chat_id, "text": message[:4096]},
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
if not data.get("ok"):
|
|
return ToolResult(success=False, error=f"Telegram API error: {data}")
|
|
|
|
return ToolResult(success=True, data={"sent": True, "chat_id": chat_id})
|
|
|
|
except httpx.HTTPStatusError as e:
|
|
return ToolResult(success=False, error=f"Telegram HTTP error: {e.response.status_code}")
|
|
except Exception as e:
|
|
return ToolResult(success=False, error=f"Telegram error: {e}")
|
|
|
|
|
|
class BoundTelegramTool(TelegramTool):
|
|
"""TelegramTool with a pre-configured chat_id — used by email handling accounts.
|
|
|
|
The model only supplies the message; chat_id is fixed at account configuration time
|
|
so the model cannot send to arbitrary chats.
|
|
"""
|
|
|
|
def __init__(self, chat_id: str, reply_keyword: str | None = None) -> None:
|
|
self._bound_chat_id = chat_id
|
|
self._reply_keyword = reply_keyword
|
|
hint = f"\n\n💬 Reply: /{reply_keyword} <your message>" if reply_keyword else ""
|
|
self._reply_hint = hint
|
|
self.description = (
|
|
f"Send a Telegram message to the configured chat (chat_id {chat_id}). "
|
|
"Only supply the message text — the destination is fixed. "
|
|
f"A reply hint will be appended automatically{' (/' + reply_keyword + ')' if reply_keyword else ''}."
|
|
)
|
|
self.input_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"message": {
|
|
"type": "string",
|
|
"description": "Message text (plain text, max 4096 characters)",
|
|
},
|
|
},
|
|
"required": ["message"],
|
|
}
|
|
|
|
async def execute(self, message: str, **kwargs) -> ToolResult: # type: ignore[override]
|
|
full_message = message + self._reply_hint if self._reply_hint else message
|
|
return await super().execute(chat_id=self._bound_chat_id, message=full_message, **kwargs)
|