Files
oai-web/server/tools/telegram_tool.py
2026-04-08 12:43:24 +02:00

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)