mirror of https://github.com/kortix-ai/suna.git
389 lines
16 KiB
Python
389 lines
16 KiB
Python
"""
|
|
Telegram trigger provider for agent triggers.
|
|
|
|
This provider handles Telegram bot integration for triggering agents based on messages.
|
|
"""
|
|
|
|
import httpx
|
|
import json
|
|
from typing import Dict, Any, Optional
|
|
from utils.logger import logger
|
|
from ..core import TriggerProvider, TriggerType, TriggerEvent, TriggerResult, TriggerConfig, ProviderDefinition
|
|
|
|
class TelegramTriggerProvider(TriggerProvider):
|
|
"""Telegram trigger provider for agents."""
|
|
|
|
def __init__(self, provider_definition: Optional[ProviderDefinition] = None):
|
|
super().__init__(TriggerType.TELEGRAM, provider_definition)
|
|
|
|
async def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Validate Telegram trigger configuration.
|
|
|
|
Required config:
|
|
- bot_token: Telegram bot token
|
|
- secret_token: Optional webhook secret token
|
|
- allowed_users: Optional list of allowed user IDs
|
|
- allowed_chats: Optional list of allowed chat IDs
|
|
- trigger_commands: Optional list of commands that trigger the agent
|
|
- trigger_keywords: Optional list of keywords that trigger the agent
|
|
"""
|
|
if not config.get('bot_token'):
|
|
raise ValueError("bot_token is required for Telegram triggers")
|
|
|
|
bot_token = config['bot_token']
|
|
if not bot_token or ':' not in bot_token:
|
|
raise ValueError("Invalid bot_token format. Should be like '123456:ABC-DEF...'")
|
|
|
|
validated_config = {
|
|
'bot_token': bot_token,
|
|
'secret_token': config.get('secret_token', ''),
|
|
'allowed_users': config.get('allowed_users', []),
|
|
'allowed_chats': config.get('allowed_chats', []),
|
|
'trigger_commands': config.get('trigger_commands', []),
|
|
'trigger_keywords': config.get('trigger_keywords', []),
|
|
'respond_to_all_messages': config.get('respond_to_all_messages', False),
|
|
'response_mode': config.get('response_mode', 'reply'), # 'reply' or 'new_message'
|
|
}
|
|
|
|
# Validate lists are actually lists
|
|
for list_field in ['allowed_users', 'allowed_chats', 'trigger_commands', 'trigger_keywords']:
|
|
if not isinstance(validated_config[list_field], list):
|
|
raise ValueError(f"{list_field} must be a list")
|
|
|
|
return validated_config
|
|
|
|
async def setup_trigger(self, trigger_config: TriggerConfig) -> bool:
|
|
"""Set up Telegram webhook for the trigger."""
|
|
try:
|
|
bot_token = trigger_config.config['bot_token']
|
|
secret_token = trigger_config.config.get('secret_token', '')
|
|
|
|
# Get webhook URL - this should be provided by the trigger manager
|
|
webhook_url = self.get_webhook_url(trigger_config.trigger_id, self._get_base_url())
|
|
|
|
# Set up Telegram webhook
|
|
result = await self._setup_telegram_webhook(bot_token, webhook_url, secret_token)
|
|
|
|
if result.get('success'):
|
|
logger.info(f"Successfully set up Telegram webhook for trigger {trigger_config.trigger_id}")
|
|
return True
|
|
else:
|
|
logger.error(f"Failed to set up Telegram webhook for trigger {trigger_config.trigger_id}: {result.get('error')}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error setting up Telegram trigger {trigger_config.trigger_id}: {e}")
|
|
return False
|
|
|
|
async def teardown_trigger(self, trigger_config: TriggerConfig) -> bool:
|
|
"""Remove Telegram webhook for the trigger."""
|
|
try:
|
|
bot_token = trigger_config.config['bot_token']
|
|
result = await self._remove_telegram_webhook(bot_token)
|
|
|
|
if result.get('success'):
|
|
logger.info(f"Successfully removed Telegram webhook for trigger {trigger_config.trigger_id}")
|
|
return True
|
|
else:
|
|
logger.warning(f"Failed to remove Telegram webhook for trigger {trigger_config.trigger_id}: {result.get('error')}")
|
|
return False # Don't fail teardown if webhook removal fails
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error tearing down Telegram trigger {trigger_config.trigger_id}: {e}")
|
|
return False
|
|
|
|
async def process_event(self, event: TriggerEvent) -> TriggerResult:
|
|
"""Process Telegram webhook event."""
|
|
try:
|
|
# Parse Telegram update
|
|
update_data = event.raw_data
|
|
|
|
if not update_data.get('update_id'):
|
|
return TriggerResult(
|
|
success=False,
|
|
error_message="Invalid Telegram update format"
|
|
)
|
|
|
|
# Extract message data
|
|
message_data = self._extract_message_data(update_data)
|
|
if not message_data:
|
|
return TriggerResult(
|
|
success=True,
|
|
should_execute_agent=False,
|
|
response_data={"message": "No processable message in update"}
|
|
)
|
|
|
|
config = {
|
|
'respond_to_all_messages': True,
|
|
'trigger_commands': [],
|
|
'trigger_keywords': [],
|
|
'allowed_users': [],
|
|
'allowed_chats': []
|
|
}
|
|
|
|
should_trigger = await self._should_trigger_agent(message_data, config)
|
|
|
|
if not should_trigger:
|
|
return TriggerResult(
|
|
success=True,
|
|
should_execute_agent=False,
|
|
response_data={"message": "Message did not meet trigger criteria"}
|
|
)
|
|
|
|
agent_prompt = self._create_agent_prompt(message_data, config)
|
|
|
|
execution_variables = {
|
|
'telegram_message_text': message_data['text'],
|
|
'telegram_user_id': message_data['user_id'],
|
|
'telegram_user_name': message_data.get('user_name', 'Unknown'),
|
|
'telegram_chat_id': message_data['chat_id'],
|
|
'telegram_chat_type': message_data.get('chat_type', 'private'),
|
|
'telegram_message_id': message_data['message_id'],
|
|
'telegram_update_id': update_data['update_id'],
|
|
'trigger_type': 'telegram',
|
|
'trigger_id': event.trigger_id,
|
|
'agent_id': event.agent_id
|
|
}
|
|
|
|
return TriggerResult(
|
|
success=True,
|
|
should_execute_agent=True,
|
|
agent_prompt=agent_prompt,
|
|
execution_variables=execution_variables,
|
|
metadata={
|
|
'telegram_data': message_data,
|
|
'trigger_config': config
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing Telegram event for trigger {event.trigger_id}: {e}")
|
|
return TriggerResult(
|
|
success=False,
|
|
error_message=f"Error processing Telegram event: {str(e)}"
|
|
)
|
|
|
|
async def health_check(self, trigger_config: TriggerConfig) -> bool:
|
|
"""Check if Telegram bot is healthy."""
|
|
try:
|
|
bot_token = trigger_config.config['bot_token']
|
|
|
|
# Test bot token by getting bot info
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"https://api.telegram.org/bot{bot_token}/getMe")
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return data.get('ok', False)
|
|
else:
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Health check failed for Telegram trigger {trigger_config.trigger_id}: {e}")
|
|
return False
|
|
|
|
def get_config_schema(self) -> Dict[str, Any]:
|
|
"""Get configuration schema for Telegram triggers."""
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"bot_token": {
|
|
"type": "string",
|
|
"description": "Telegram bot token from @BotFather",
|
|
"pattern": r"^\d+:[A-Za-z0-9_-]+$"
|
|
},
|
|
"secret_token": {
|
|
"type": "string",
|
|
"description": "Optional secret token for webhook security",
|
|
"maxLength": 256
|
|
},
|
|
"allowed_users": {
|
|
"type": "array",
|
|
"items": {"type": "integer"},
|
|
"description": "List of allowed user IDs (empty = allow all)"
|
|
},
|
|
"allowed_chats": {
|
|
"type": "array",
|
|
"items": {"type": "integer"},
|
|
"description": "List of allowed chat IDs (empty = allow all)"
|
|
},
|
|
"trigger_commands": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Commands that trigger the agent (e.g., ['/help', '/start'])"
|
|
},
|
|
"trigger_keywords": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Keywords that trigger the agent"
|
|
},
|
|
"respond_to_all_messages": {
|
|
"type": "boolean",
|
|
"description": "Whether to respond to all messages (ignores commands/keywords)",
|
|
"default": False
|
|
},
|
|
"response_mode": {
|
|
"type": "string",
|
|
"enum": ["reply", "new_message"],
|
|
"description": "How to respond - reply to message or send new message",
|
|
"default": "reply"
|
|
}
|
|
},
|
|
"required": ["bot_token"],
|
|
"additionalProperties": False
|
|
}
|
|
|
|
def get_webhook_url(self, trigger_id: str, base_url: str) -> str:
|
|
"""Get webhook URL for this Telegram trigger."""
|
|
return f"{base_url}/api/triggers/{trigger_id}/webhook"
|
|
|
|
def _extract_message_data(self, update_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
"""Extract message data from Telegram update."""
|
|
message = None
|
|
|
|
# Check for regular message
|
|
if update_data.get('message'):
|
|
message = update_data['message']
|
|
# Check for edited message
|
|
elif update_data.get('edited_message'):
|
|
message = update_data['edited_message']
|
|
|
|
if not message:
|
|
return None
|
|
|
|
# Extract user info
|
|
user = message.get('from', {})
|
|
chat = message.get('chat', {})
|
|
|
|
return {
|
|
'text': message.get('text', ''),
|
|
'message_id': message.get('message_id', 0),
|
|
'user_id': user.get('id', 0),
|
|
'user_name': user.get('first_name', '') + (' ' + user.get('last_name', '')).strip(),
|
|
'username': user.get('username', ''),
|
|
'chat_id': chat.get('id', 0),
|
|
'chat_type': chat.get('type', 'private'),
|
|
'chat_title': chat.get('title', ''),
|
|
'timestamp': message.get('date', 0)
|
|
}
|
|
|
|
async def _should_trigger_agent(self, message_data: Dict[str, Any], config: Dict[str, Any]) -> bool:
|
|
"""Determine if message should trigger the agent."""
|
|
# Check if message has text
|
|
if not message_data.get('text'):
|
|
return False
|
|
|
|
# Check user allowlist
|
|
allowed_users = config.get('allowed_users', [])
|
|
if allowed_users and message_data['user_id'] not in allowed_users:
|
|
return False
|
|
|
|
# Check chat allowlist
|
|
allowed_chats = config.get('allowed_chats', [])
|
|
if allowed_chats and message_data['chat_id'] not in allowed_chats:
|
|
return False
|
|
|
|
# If respond to all messages is enabled, trigger for any allowed message
|
|
if config.get('respond_to_all_messages', False):
|
|
return True
|
|
|
|
message_text = message_data['text'].lower()
|
|
|
|
# Check trigger commands
|
|
trigger_commands = config.get('trigger_commands', [])
|
|
if trigger_commands:
|
|
for command in trigger_commands:
|
|
if message_text.startswith(command.lower()):
|
|
return True
|
|
|
|
# Check trigger keywords
|
|
trigger_keywords = config.get('trigger_keywords', [])
|
|
if trigger_keywords:
|
|
for keyword in trigger_keywords:
|
|
if keyword.lower() in message_text:
|
|
return True
|
|
|
|
# If no specific triggers are configured, don't trigger
|
|
if not trigger_commands and not trigger_keywords:
|
|
return False
|
|
|
|
return False
|
|
|
|
def _create_agent_prompt(self, message_data: Dict[str, Any], config: Dict[str, Any]) -> str:
|
|
"""Create prompt for the agent based on the message."""
|
|
message_text = message_data.get('text', '')
|
|
return message_text
|
|
|
|
async def _setup_telegram_webhook(self, bot_token: str, webhook_url: str, secret_token: str = "") -> Dict[str, Any]:
|
|
"""Set up Telegram webhook."""
|
|
try:
|
|
telegram_api_url = f"https://api.telegram.org/bot{bot_token}/setWebhook"
|
|
|
|
payload = {
|
|
"url": webhook_url,
|
|
"drop_pending_updates": True
|
|
}
|
|
|
|
if secret_token:
|
|
payload["secret_token"] = secret_token
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.post(telegram_api_url, json=payload)
|
|
response_data = response.json()
|
|
|
|
if response.status_code == 200 and response_data.get("ok"):
|
|
return {
|
|
"success": True,
|
|
"message": response_data.get("description", "Webhook set successfully")
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"error": response_data.get("description", f"HTTP {response.status_code}")
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"Error setting up webhook: {str(e)}"
|
|
}
|
|
|
|
async def _remove_telegram_webhook(self, bot_token: str) -> Dict[str, Any]:
|
|
"""Remove Telegram webhook."""
|
|
try:
|
|
telegram_api_url = f"https://api.telegram.org/bot{bot_token}/deleteWebhook"
|
|
|
|
payload = {
|
|
"drop_pending_updates": True
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.post(telegram_api_url, json=payload)
|
|
response_data = response.json()
|
|
|
|
if response.status_code == 200 and response_data.get("ok"):
|
|
return {
|
|
"success": True,
|
|
"message": response_data.get("description", "Webhook removed successfully")
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"error": response_data.get("description", f"HTTP {response.status_code}")
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": f"Error removing webhook: {str(e)}"
|
|
}
|
|
|
|
def _get_base_url(self) -> str:
|
|
"""Get base URL for webhooks."""
|
|
import os
|
|
return os.getenv("WEBHOOK_BASE_URL", "http://localhost:8000")
|
|
|
|
async def _get_trigger_config(self, trigger_id: str) -> Optional[TriggerConfig]:
|
|
"""Get trigger configuration by ID."""
|
|
return None |