Initial commit
This commit is contained in:
123
server/tools/telegram_tool.py
Normal file
123
server/tools/telegram_tool.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user