mirror of https://github.com/kortix-ai/suna.git
chore(dev): functional telegram webhook
This commit is contained in:
parent
daf2afa2a3
commit
f0440892ba
|
@ -5,8 +5,8 @@ import uuid
|
|||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from .models import SlackEventRequest, WebhookExecutionResult
|
||||
from .providers import SlackWebhookProvider, GenericWebhookProvider
|
||||
from .models import SlackEventRequest, TelegramUpdateRequest, WebhookExecutionResult
|
||||
from .providers import SlackWebhookProvider, TelegramWebhookProvider, GenericWebhookProvider
|
||||
from workflows.models import WorkflowDefinition
|
||||
|
||||
from services.supabase import DBConnection
|
||||
|
@ -47,7 +47,8 @@ async def trigger_workflow_webhook(
|
|||
workflow_id: str,
|
||||
request: Request,
|
||||
x_slack_signature: Optional[str] = Header(None),
|
||||
x_slack_request_timestamp: Optional[str] = Header(None)
|
||||
x_slack_request_timestamp: Optional[str] = Header(None),
|
||||
x_telegram_bot_api_secret_token: Optional[str] = Header(None)
|
||||
):
|
||||
"""Handle webhook triggers for workflows."""
|
||||
try:
|
||||
|
@ -69,11 +70,18 @@ async def trigger_workflow_webhook(
|
|||
logger.error(f"[Webhook] Failed to parse JSON: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"Invalid JSON payload: {str(e)}")
|
||||
|
||||
provider_type = "slack" if x_slack_signature else "generic"
|
||||
# Detect provider type based on headers and data structure
|
||||
if x_slack_signature:
|
||||
provider_type = "slack"
|
||||
elif x_telegram_bot_api_secret_token or (data and "update_id" in data):
|
||||
provider_type = "telegram"
|
||||
else:
|
||||
provider_type = "generic"
|
||||
|
||||
logger.info(f"[Webhook] Detected provider type: {provider_type}")
|
||||
logger.info(f"[Webhook] Slack signature present: {bool(x_slack_signature)}")
|
||||
logger.info(f"[Webhook] Slack timestamp present: {bool(x_slack_request_timestamp)}")
|
||||
logger.info(f"[Webhook] Telegram secret token present: {bool(x_telegram_bot_api_secret_token)}")
|
||||
|
||||
# Handle Slack URL verification challenge first
|
||||
if provider_type == "slack" and data.get("type") == "url_verification":
|
||||
|
@ -148,6 +156,8 @@ async def trigger_workflow_webhook(
|
|||
}
|
||||
else:
|
||||
result = await _handle_slack_webhook(workflow, data, body, x_slack_signature, x_slack_request_timestamp)
|
||||
elif provider_type == "telegram":
|
||||
result = await _handle_telegram_webhook(workflow, data, x_telegram_bot_api_secret_token)
|
||||
else:
|
||||
result = await _handle_generic_webhook(workflow, data)
|
||||
|
||||
|
@ -364,6 +374,66 @@ async def _handle_slack_webhook(
|
|||
logger.error(f"Error handling Slack webhook: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"Error processing Slack webhook: {str(e)}")
|
||||
|
||||
async def _handle_telegram_webhook(
|
||||
workflow: WorkflowDefinition,
|
||||
data: Dict[str, Any],
|
||||
secret_token: Optional[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle Telegram webhook specifically."""
|
||||
try:
|
||||
# Validate as TelegramUpdateRequest
|
||||
telegram_update = TelegramUpdateRequest(**data)
|
||||
|
||||
# Find Telegram webhook config
|
||||
webhook_config = None
|
||||
for trigger in workflow.triggers:
|
||||
if trigger.type == 'WEBHOOK' and trigger.config.get('type') == 'telegram':
|
||||
webhook_config = trigger.config
|
||||
break
|
||||
|
||||
if not webhook_config:
|
||||
raise HTTPException(status_code=400, detail="Telegram webhook not configured for this workflow")
|
||||
|
||||
# Verify secret token if configured
|
||||
if webhook_config.get('telegram', {}).get('secret_token'):
|
||||
expected_secret = webhook_config['telegram']['secret_token']
|
||||
if not secret_token or not TelegramWebhookProvider.verify_webhook_secret(b'', secret_token, expected_secret):
|
||||
raise HTTPException(status_code=401, detail="Invalid Telegram secret token")
|
||||
|
||||
payload = TelegramWebhookProvider.process_update(telegram_update)
|
||||
|
||||
if payload:
|
||||
execution_variables = {
|
||||
"telegram_text": payload.text,
|
||||
"telegram_user_id": payload.user_id,
|
||||
"telegram_chat_id": payload.chat_id,
|
||||
"telegram_message_id": payload.message_id,
|
||||
"telegram_timestamp": payload.timestamp,
|
||||
"telegram_update_type": payload.update_type,
|
||||
"telegram_user_first_name": payload.user_first_name,
|
||||
"telegram_user_last_name": payload.user_last_name,
|
||||
"telegram_user_username": payload.user_username,
|
||||
"telegram_chat_type": payload.chat_type,
|
||||
"telegram_chat_title": payload.chat_title,
|
||||
"trigger_type": "webhook",
|
||||
"webhook_provider": "telegram"
|
||||
}
|
||||
|
||||
return {
|
||||
"should_execute": True,
|
||||
"execution_variables": execution_variables,
|
||||
"trigger_data": payload.model_dump()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"should_execute": False,
|
||||
"response": {"message": "Update processed but no action needed"}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling Telegram webhook: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"Error processing Telegram webhook: {str(e)}")
|
||||
|
||||
async def _handle_generic_webhook(workflow: WorkflowDefinition, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle generic webhook."""
|
||||
try:
|
||||
|
|
|
@ -5,7 +5,7 @@ from datetime import datetime
|
|||
class WebhookTriggerRequest(BaseModel):
|
||||
"""Base webhook trigger request."""
|
||||
workflow_id: str
|
||||
provider: Literal['slack', 'generic'] = 'slack'
|
||||
provider: Literal['slack', 'telegram', 'generic'] = 'slack'
|
||||
data: Dict[str, Any]
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
timestamp: Optional[datetime] = None
|
||||
|
@ -22,6 +22,17 @@ class SlackEventRequest(BaseModel):
|
|||
authed_users: Optional[list] = None
|
||||
challenge: Optional[str] = None
|
||||
|
||||
class TelegramUpdateRequest(BaseModel):
|
||||
"""Telegram update request model."""
|
||||
update_id: int
|
||||
message: Optional[Dict[str, Any]] = None
|
||||
edited_message: Optional[Dict[str, Any]] = None
|
||||
channel_post: Optional[Dict[str, Any]] = None
|
||||
edited_channel_post: Optional[Dict[str, Any]] = None
|
||||
inline_query: Optional[Dict[str, Any]] = None
|
||||
chosen_inline_result: Optional[Dict[str, Any]] = None
|
||||
callback_query: Optional[Dict[str, Any]] = None
|
||||
|
||||
class SlackWebhookPayload(BaseModel):
|
||||
"""Slack webhook payload after processing."""
|
||||
text: str
|
||||
|
@ -32,6 +43,20 @@ class SlackWebhookPayload(BaseModel):
|
|||
event_type: str
|
||||
trigger_word: Optional[str] = None
|
||||
|
||||
class TelegramWebhookPayload(BaseModel):
|
||||
"""Telegram webhook payload after processing."""
|
||||
text: str
|
||||
user_id: str
|
||||
chat_id: str
|
||||
message_id: int
|
||||
timestamp: int
|
||||
update_type: str
|
||||
user_first_name: Optional[str] = None
|
||||
user_last_name: Optional[str] = None
|
||||
user_username: Optional[str] = None
|
||||
chat_type: Optional[str] = None
|
||||
chat_title: Optional[str] = None
|
||||
|
||||
class WebhookExecutionResult(BaseModel):
|
||||
"""Result of webhook execution."""
|
||||
success: bool
|
||||
|
|
|
@ -2,9 +2,10 @@ import hmac
|
|||
import hashlib
|
||||
import time
|
||||
import json
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import HTTPException
|
||||
from .models import SlackEventRequest, SlackWebhookPayload
|
||||
from .models import SlackEventRequest, SlackWebhookPayload, TelegramUpdateRequest, TelegramWebhookPayload
|
||||
from utils.logger import logger
|
||||
|
||||
class SlackWebhookProvider:
|
||||
|
@ -89,6 +90,225 @@ class SlackWebhookProvider:
|
|||
logger.error(f"Error processing Slack event: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"Error processing Slack event: {str(e)}")
|
||||
|
||||
class TelegramWebhookProvider:
|
||||
"""Handles Telegram webhook events and verification."""
|
||||
|
||||
@staticmethod
|
||||
def verify_webhook_secret(body: bytes, secret_token: str, telegram_secret_token: str) -> bool:
|
||||
"""Verify Telegram webhook secret token."""
|
||||
try:
|
||||
return secret_token == telegram_secret_token
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying Telegram secret token: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def setup_webhook(bot_token: str, webhook_url: str, secret_token: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Automatically set up the Telegram webhook by calling the Telegram Bot API.
|
||||
|
||||
Args:
|
||||
bot_token: The Telegram bot token
|
||||
webhook_url: The webhook URL to set
|
||||
secret_token: Optional secret token for additional security
|
||||
|
||||
Returns:
|
||||
Dict containing the API response
|
||||
"""
|
||||
try:
|
||||
telegram_api_url = f"https://api.telegram.org/bot{bot_token}/setWebhook"
|
||||
|
||||
payload = {
|
||||
"url": webhook_url,
|
||||
"drop_pending_updates": True # Clear any pending updates
|
||||
}
|
||||
|
||||
if secret_token:
|
||||
payload["secret_token"] = secret_token
|
||||
|
||||
logger.info(f"Setting up Telegram webhook: {webhook_url}")
|
||||
|
||||
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"):
|
||||
logger.info(f"Successfully set up Telegram webhook: {response_data.get('description', 'Webhook set')}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": response_data.get("description", "Webhook set successfully"),
|
||||
"response": response_data
|
||||
}
|
||||
else:
|
||||
error_msg = response_data.get("description", f"HTTP {response.status_code}")
|
||||
logger.error(f"Failed to set up Telegram webhook: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"response": response_data
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_msg = "Timeout while connecting to Telegram API"
|
||||
logger.error(f"Telegram webhook setup failed: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = f"Error setting up Telegram webhook: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def remove_webhook(bot_token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Remove the Telegram webhook by calling the Telegram Bot API.
|
||||
|
||||
Args:
|
||||
bot_token: The Telegram bot token
|
||||
|
||||
Returns:
|
||||
Dict containing the API response
|
||||
"""
|
||||
try:
|
||||
telegram_api_url = f"https://api.telegram.org/bot{bot_token}/deleteWebhook"
|
||||
|
||||
payload = {
|
||||
"drop_pending_updates": True # Clear any pending updates
|
||||
}
|
||||
|
||||
logger.info("Removing Telegram webhook")
|
||||
|
||||
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"):
|
||||
logger.info(f"Successfully removed Telegram webhook: {response_data.get('description', 'Webhook removed')}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": response_data.get("description", "Webhook removed successfully"),
|
||||
"response": response_data
|
||||
}
|
||||
else:
|
||||
error_msg = response_data.get("description", f"HTTP {response.status_code}")
|
||||
logger.error(f"Failed to remove Telegram webhook: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"response": response_data
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
error_msg = "Timeout while connecting to Telegram API"
|
||||
logger.error(f"Telegram webhook removal failed: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = f"Error removing Telegram webhook: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"error": error_msg
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def process_update(update_data: TelegramUpdateRequest) -> Optional[TelegramWebhookPayload]:
|
||||
"""Process Telegram update and extract relevant data."""
|
||||
try:
|
||||
# Handle regular messages
|
||||
if update_data.message:
|
||||
message = update_data.message
|
||||
text = message.get("text", "")
|
||||
|
||||
# Skip if no text content
|
||||
if not text:
|
||||
logger.info("Telegram message has no text content")
|
||||
return None
|
||||
|
||||
user = message.get("from", {})
|
||||
chat = message.get("chat", {})
|
||||
|
||||
return TelegramWebhookPayload(
|
||||
text=text,
|
||||
user_id=str(user.get("id", "")),
|
||||
chat_id=str(chat.get("id", "")),
|
||||
message_id=message.get("message_id", 0),
|
||||
timestamp=message.get("date", 0),
|
||||
update_type="message",
|
||||
user_first_name=user.get("first_name"),
|
||||
user_last_name=user.get("last_name"),
|
||||
user_username=user.get("username"),
|
||||
chat_type=chat.get("type"),
|
||||
chat_title=chat.get("title")
|
||||
)
|
||||
|
||||
# Handle edited messages
|
||||
elif update_data.edited_message:
|
||||
message = update_data.edited_message
|
||||
text = message.get("text", "")
|
||||
|
||||
if not text:
|
||||
logger.info("Telegram edited message has no text content")
|
||||
return None
|
||||
|
||||
user = message.get("from", {})
|
||||
chat = message.get("chat", {})
|
||||
|
||||
return TelegramWebhookPayload(
|
||||
text=text,
|
||||
user_id=str(user.get("id", "")),
|
||||
chat_id=str(chat.get("id", "")),
|
||||
message_id=message.get("message_id", 0),
|
||||
timestamp=message.get("edit_date", message.get("date", 0)),
|
||||
update_type="edited_message",
|
||||
user_first_name=user.get("first_name"),
|
||||
user_last_name=user.get("last_name"),
|
||||
user_username=user.get("username"),
|
||||
chat_type=chat.get("type"),
|
||||
chat_title=chat.get("title")
|
||||
)
|
||||
|
||||
# Handle callback queries (inline keyboard button presses)
|
||||
elif update_data.callback_query:
|
||||
callback = update_data.callback_query
|
||||
data = callback.get("data", "")
|
||||
|
||||
if not data:
|
||||
logger.info("Telegram callback query has no data")
|
||||
return None
|
||||
|
||||
user = callback.get("from", {})
|
||||
message = callback.get("message", {})
|
||||
chat = message.get("chat", {}) if message else {}
|
||||
|
||||
return TelegramWebhookPayload(
|
||||
text=f"Callback: {data}",
|
||||
user_id=str(user.get("id", "")),
|
||||
chat_id=str(chat.get("id", "")),
|
||||
message_id=message.get("message_id", 0) if message else 0,
|
||||
timestamp=int(time.time()),
|
||||
update_type="callback_query",
|
||||
user_first_name=user.get("first_name"),
|
||||
user_last_name=user.get("last_name"),
|
||||
user_username=user.get("username"),
|
||||
chat_type=chat.get("type"),
|
||||
chat_title=chat.get("title")
|
||||
)
|
||||
|
||||
logger.warning(f"Unhandled Telegram update type: {update_data.dict()}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Telegram update: {e}")
|
||||
raise HTTPException(status_code=400, detail=f"Error processing Telegram update: {str(e)}")
|
||||
|
||||
class GenericWebhookProvider:
|
||||
"""Handles generic webhook events."""
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ from scheduling.models import (
|
|||
ScheduleCreateRequest, ScheduleConfig as QStashScheduleConfig,
|
||||
SimpleScheduleConfig, CronScheduleConfig
|
||||
)
|
||||
from webhooks.providers import TelegramWebhookProvider
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
@ -740,6 +741,19 @@ async def update_workflow_flow(
|
|||
# Also try to unschedule from old APScheduler as fallback
|
||||
await workflow_scheduler.unschedule_workflow(workflow_id)
|
||||
|
||||
telegram_triggers = [trigger for trigger in updated_workflow.triggers if trigger.type == 'WEBHOOK' and trigger.config.get('type') == 'telegram']
|
||||
if telegram_triggers:
|
||||
try:
|
||||
import os
|
||||
base_url = (
|
||||
os.getenv('WEBHOOK_BASE_URL', 'http://localhost:3000')
|
||||
)
|
||||
|
||||
await _setup_telegram_webhooks_for_workflow(updated_workflow, base_url)
|
||||
logger.info(f"Processed Telegram webhook setup for workflow {workflow_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to set up Telegram webhooks for workflow {workflow_id}: {e}")
|
||||
|
||||
return updated_workflow
|
||||
|
||||
except HTTPException:
|
||||
|
@ -1107,3 +1121,38 @@ async def _remove_qstash_schedules_for_workflow(workflow_id: str):
|
|||
except Exception as e:
|
||||
logger.error(f"Failed to remove QStash schedules for workflow {workflow_id}: {e}")
|
||||
raise
|
||||
|
||||
async def _setup_telegram_webhooks_for_workflow(workflow: WorkflowDefinition, base_url: str):
|
||||
"""Set up Telegram webhooks for a workflow if configured."""
|
||||
try:
|
||||
telegram_triggers = [
|
||||
trigger for trigger in workflow.triggers
|
||||
if trigger.type == 'WEBHOOK' and trigger.config.get('type') == 'telegram'
|
||||
]
|
||||
|
||||
for trigger in telegram_triggers:
|
||||
telegram_config = trigger.config.get('telegram')
|
||||
if not telegram_config:
|
||||
continue
|
||||
|
||||
bot_token = telegram_config.get('bot_token')
|
||||
secret_token = telegram_config.get('secret_token')
|
||||
|
||||
if not bot_token:
|
||||
logger.warning(f"No bot token found for Telegram webhook in workflow {workflow.id}")
|
||||
continue
|
||||
|
||||
webhook_url = f"{base_url}/api/webhooks/trigger/{workflow.id}"
|
||||
result = await TelegramWebhookProvider.setup_webhook(
|
||||
bot_token=bot_token,
|
||||
webhook_url=webhook_url,
|
||||
secret_token=secret_token
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
logger.info(f"Successfully set up Telegram webhook for workflow {workflow.id}: {result.get('message')}")
|
||||
else:
|
||||
logger.error(f"Failed to set up Telegram webhook for workflow {workflow.id}: {result.get('error')}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting up Telegram webhooks for workflow {workflow.id}: {e}")
|
|
@ -1,5 +1,5 @@
|
|||
from typing import List, Dict, Any, Optional
|
||||
from .models import WorkflowNode, WorkflowEdge, WorkflowDefinition, WorkflowStep, WorkflowTrigger, InputNodeConfig, ScheduleConfig
|
||||
from .models import WorkflowNode, WorkflowEdge, WorkflowDefinition, WorkflowStep, WorkflowTrigger, InputNodeConfig, ScheduleConfig, WebhookConfig
|
||||
from .tool_examples import get_tools_xml_examples
|
||||
import uuid
|
||||
from utils.logger import logger
|
||||
|
@ -143,10 +143,19 @@ class WorkflowConverter:
|
|||
enabled=schedule_data.get('enabled', True)
|
||||
)
|
||||
|
||||
webhook_config = None
|
||||
if data.get('trigger_type') == 'WEBHOOK' and data.get('webhook_config'):
|
||||
webhook_data = data.get('webhook_config', {})
|
||||
try:
|
||||
webhook_config = WebhookConfig(**webhook_data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse webhook config: {e}, using raw data")
|
||||
webhook_config = webhook_data
|
||||
|
||||
return InputNodeConfig(
|
||||
prompt=data.get('prompt', ''),
|
||||
trigger_type=data.get('trigger_type', 'MANUAL'),
|
||||
webhook_config=data.get('webhook_config'),
|
||||
webhook_config=webhook_config,
|
||||
schedule_config=schedule_config,
|
||||
variables=data.get('variables')
|
||||
)
|
||||
|
@ -181,6 +190,14 @@ class WorkflowConverter:
|
|||
trigger_config['slack'] = slack_config.dict()
|
||||
else:
|
||||
trigger_config['slack'] = slack_config
|
||||
elif webhook_config.type == 'telegram' and webhook_config.telegram:
|
||||
telegram_config = webhook_config.telegram
|
||||
if hasattr(telegram_config, 'model_dump'):
|
||||
trigger_config['telegram'] = telegram_config.model_dump()
|
||||
elif hasattr(telegram_config, 'dict'):
|
||||
trigger_config['telegram'] = telegram_config.dict()
|
||||
else:
|
||||
trigger_config['telegram'] = telegram_config
|
||||
elif webhook_config.generic:
|
||||
generic_config = webhook_config.generic
|
||||
if hasattr(generic_config, 'model_dump'):
|
||||
|
@ -198,6 +215,8 @@ class WorkflowConverter:
|
|||
|
||||
if webhook_config.get('type') == 'slack' and webhook_config.get('slack'):
|
||||
trigger_config['slack'] = webhook_config['slack']
|
||||
elif webhook_config.get('type') == 'telegram' and webhook_config.get('telegram'):
|
||||
trigger_config['telegram'] = webhook_config['telegram']
|
||||
elif webhook_config.get('generic'):
|
||||
trigger_config['generic'] = webhook_config['generic']
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any, Optional, Literal
|
||||
from pydantic import BaseModel, field_validator
|
||||
from typing import List, Dict, Any, Optional, Literal, Union
|
||||
from datetime import datetime
|
||||
|
||||
class ScheduleConfig(BaseModel):
|
||||
|
@ -19,6 +19,12 @@ class SlackWebhookConfig(BaseModel):
|
|||
channel: Optional[str] = None
|
||||
username: Optional[str] = None
|
||||
|
||||
class TelegramWebhookConfig(BaseModel):
|
||||
"""Configuration for Telegram webhook integration."""
|
||||
webhook_url: str
|
||||
bot_token: str
|
||||
secret_token: Optional[str] = None
|
||||
|
||||
class GenericWebhookConfig(BaseModel):
|
||||
"""Configuration for generic webhook integration."""
|
||||
url: str
|
||||
|
@ -27,17 +33,18 @@ class GenericWebhookConfig(BaseModel):
|
|||
|
||||
class WebhookConfig(BaseModel):
|
||||
"""Configuration for webhook triggers."""
|
||||
type: Literal['slack', 'generic'] = 'slack'
|
||||
type: Literal['slack', 'telegram', 'generic'] = 'slack'
|
||||
method: Optional[Literal['POST', 'GET', 'PUT']] = 'POST'
|
||||
authentication: Optional[Literal['none', 'api_key', 'bearer']] = 'none'
|
||||
slack: Optional[SlackWebhookConfig] = None
|
||||
telegram: Optional[TelegramWebhookConfig] = None
|
||||
generic: Optional[GenericWebhookConfig] = None
|
||||
|
||||
class InputNodeConfig(BaseModel):
|
||||
"""Configuration for workflow input nodes."""
|
||||
prompt: str = ""
|
||||
trigger_type: Literal['MANUAL', 'WEBHOOK', 'SCHEDULE'] = 'MANUAL'
|
||||
webhook_config: Optional[WebhookConfig] = None
|
||||
webhook_config: Optional[Union[WebhookConfig, Dict[str, Any]]] = None
|
||||
schedule_config: Optional[ScheduleConfig] = None
|
||||
variables: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
|
|
@ -96,6 +96,8 @@ const InputNode = memo(({ data, selected, id }: NodeProps) => {
|
|||
case 'WEBHOOK':
|
||||
if (nodeData.webhook_config?.type === 'slack') {
|
||||
return 'Slack webhook';
|
||||
} else if (nodeData.webhook_config?.type === 'telegram') {
|
||||
return 'Telegram webhook';
|
||||
}
|
||||
return `${nodeData.webhook_config?.method || 'POST'} webhook`;
|
||||
case 'MANUAL':
|
||||
|
@ -200,7 +202,7 @@ const InputNode = memo(({ data, selected, id }: NodeProps) => {
|
|||
<Label className="text-xs">Webhook Provider</Label>
|
||||
<Select
|
||||
value={nodeData.webhook_config?.type || 'slack'}
|
||||
onValueChange={(value: 'slack' | 'generic') =>
|
||||
onValueChange={(value: 'slack' | 'telegram' | 'generic') =>
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
...nodeData.webhook_config,
|
||||
|
@ -214,6 +216,7 @@ const InputNode = memo(({ data, selected, id }: NodeProps) => {
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="slack">Slack</SelectItem>
|
||||
<SelectItem value="telegram">Telegram</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
@ -248,6 +251,37 @@ const InputNode = memo(({ data, selected, id }: NodeProps) => {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeData.webhook_config?.type === 'telegram' && (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (!nodeData.webhook_config) {
|
||||
updateNodeData(id, {
|
||||
webhook_config: {
|
||||
type: 'telegram',
|
||||
method: 'POST',
|
||||
authentication: 'none'
|
||||
}
|
||||
});
|
||||
}
|
||||
setIsWebhookDialogOpen(true);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
Configure Telegram Webhook
|
||||
</Button>
|
||||
|
||||
{nodeData.webhook_config?.telegram?.webhook_url && nodeData.webhook_config?.telegram?.bot_token && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
✓ Telegram webhook configured
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { SlackWebhookConfig } from "./providers/SlackWebhookConfig";
|
||||
import { TelegramWebhookConfig } from "./providers/TelegramWebhookConfig";
|
||||
import { WebhookConfig } from "./types";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
@ -33,14 +34,28 @@ export function WebhookConfigDialog({
|
|||
);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const webhookUrl = `${typeof window !== 'undefined' ? window.location.origin : process.env.NEXT_PUBLIC_FRONTEND_URL || 'http://localhost:3000'}/api/webhooks/trigger/${workflowId}`;
|
||||
|
||||
const handleSave = () => {
|
||||
if (webhookConfig.type === 'slack') {
|
||||
if (!webhookConfig.slack?.webhook_url || !webhookConfig.slack?.signing_secret) {
|
||||
toast.error("Please fill in all required Slack configuration fields");
|
||||
return;
|
||||
}
|
||||
} else if (webhookConfig.type === 'telegram') {
|
||||
if (!webhookConfig.telegram?.webhook_url || !webhookConfig.telegram?.bot_token) {
|
||||
toast.error("Please fill in all required Telegram configuration fields");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSave(webhookConfig);
|
||||
|
||||
if (webhookConfig.type === 'telegram') {
|
||||
toast.success("Telegram webhook configuration saved! The webhook will be automatically set up with Telegram.");
|
||||
} else {
|
||||
toast.success("Webhook configuration saved successfully!");
|
||||
}
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
|
@ -66,9 +81,14 @@ export function WebhookConfigDialog({
|
|||
<Tabs
|
||||
value={webhookConfig.type}
|
||||
onValueChange={(value) =>
|
||||
setWebhookConfig(prev => ({ ...prev, type: value as 'slack' | 'generic' }))
|
||||
setWebhookConfig(prev => ({ ...prev, type: value as 'slack' | 'telegram' | 'generic' }))
|
||||
}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="slack">Slack</TabsTrigger>
|
||||
<TabsTrigger value="telegram">Telegram</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="slack" className="space-y-4">
|
||||
<SlackWebhookConfig
|
||||
config={webhookConfig.slack}
|
||||
|
@ -78,7 +98,18 @@ export function WebhookConfigDialog({
|
|||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="telegram" className="space-y-4">
|
||||
<TelegramWebhookConfig
|
||||
config={webhookConfig.telegram}
|
||||
webhookUrl={webhookUrl}
|
||||
onChange={(telegramConfig) =>
|
||||
setWebhookConfig(prev => ({ ...prev, telegram: telegramConfig }))
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Eye, EyeOff, ExternalLink, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
import { TelegramWebhookConfig as TelegramConfig } from "../types";
|
||||
|
||||
interface TelegramWebhookConfigProps {
|
||||
config?: TelegramConfig;
|
||||
onChange: (config: TelegramConfig) => void;
|
||||
webhookUrl?: string; // URL from the parent dialog
|
||||
}
|
||||
|
||||
export function TelegramWebhookConfig({ config, onChange, webhookUrl }: TelegramWebhookConfigProps) {
|
||||
const [showBotToken, setShowBotToken] = useState(false);
|
||||
const [showSecretToken, setShowSecretToken] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Initialize webhook URL if not set
|
||||
useEffect(() => {
|
||||
if (webhookUrl && (!config?.webhook_url || config.webhook_url === '')) {
|
||||
const newConfig = { ...config, webhook_url: webhookUrl };
|
||||
onChange(newConfig);
|
||||
}
|
||||
}, [webhookUrl, config, onChange]);
|
||||
|
||||
const updateConfig = (field: keyof TelegramConfig, value: string) => {
|
||||
const newConfig = { ...config, [field]: value };
|
||||
onChange(newConfig);
|
||||
|
||||
// Clear error for this field
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const validateField = (field: keyof TelegramConfig, value: string) => {
|
||||
const newErrors = { ...errors };
|
||||
|
||||
switch (field) {
|
||||
case 'webhook_url':
|
||||
if (!value) {
|
||||
newErrors[field] = 'Webhook URL is required';
|
||||
} else if (!value.startsWith('https://') || !value.includes('/api/webhooks/trigger/')) {
|
||||
newErrors[field] = 'Please use the webhook URL provided above';
|
||||
} else {
|
||||
delete newErrors[field];
|
||||
}
|
||||
break;
|
||||
case 'bot_token':
|
||||
if (!value) {
|
||||
newErrors[field] = 'Bot token is required';
|
||||
} else if (!value.includes(':') || value.length < 40) {
|
||||
newErrors[field] = 'Bot token format seems incorrect (should be like 123456:ABC-DEF...)';
|
||||
} else {
|
||||
delete newErrors[field];
|
||||
}
|
||||
break;
|
||||
case 'secret_token':
|
||||
if (value && (value.length < 1 || value.length > 256)) {
|
||||
newErrors[field] = 'Secret token must be between 1-256 characters';
|
||||
} else {
|
||||
delete newErrors[field];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
delete newErrors[field];
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
};
|
||||
|
||||
const getFieldStatus = (field: keyof TelegramConfig) => {
|
||||
const value = config?.[field] || '';
|
||||
if (errors[field]) return 'error';
|
||||
if (value && !errors[field]) return 'success';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<div className="w-6 h-6 rounded flex items-center justify-center bg-blue-500">
|
||||
<span className="text-white font-bold text-xs">T</span>
|
||||
</div>
|
||||
Setup Instructions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<div className="space-y-2">
|
||||
<p className="flex items-start gap-2">
|
||||
<span className="bg-primary/10 text-primary rounded-full w-5 h-5 flex items-center justify-center text-xs font-medium shrink-0 mt-0.5">1</span>
|
||||
<span>Create a Telegram bot by messaging <Button variant="link" className="h-auto p-0 text-sm" asChild>
|
||||
<a href="https://t.me/botfather" target="_blank" rel="noopener noreferrer">
|
||||
@BotFather <ExternalLink className="h-3 w-3 ml-1" />
|
||||
</a>
|
||||
</Button></span>
|
||||
</p>
|
||||
<p className="flex items-start gap-2">
|
||||
<span className="bg-primary/10 text-primary rounded-full w-5 h-5 flex items-center justify-center text-xs font-medium shrink-0 mt-0.5">2</span>
|
||||
<span>Send the command: <Badge variant="outline" className="text-xs">/newbot</Badge></span>
|
||||
</p>
|
||||
<p className="flex items-start gap-2">
|
||||
<span className="bg-primary/10 text-primary rounded-full w-5 h-5 flex items-center justify-center text-xs font-medium shrink-0 mt-0.5">3</span>
|
||||
<span>Follow the prompts to choose a name and username for your bot</span>
|
||||
</p>
|
||||
<p className="flex items-start gap-2">
|
||||
<span className="bg-primary/10 text-primary rounded-full w-5 h-5 flex items-center justify-center text-xs font-medium shrink-0 mt-0.5">4</span>
|
||||
<span>Copy the bot token from BotFather and paste it below</span>
|
||||
</p>
|
||||
<p className="flex items-start gap-2">
|
||||
<span className="bg-primary/10 text-primary rounded-full w-5 h-5 flex items-center justify-center text-xs font-medium shrink-0 mt-0.5">5</span>
|
||||
<span>Configure the bot token and secret token below</span>
|
||||
</p>
|
||||
<p className="flex items-start gap-2">
|
||||
<span className="bg-primary/10 text-primary rounded-full w-5 h-5 flex items-center justify-center text-xs font-medium shrink-0 mt-0.5">6</span>
|
||||
<span>Save the configuration - the webhook will be automatically set up!</span>
|
||||
</p>
|
||||
<p className="flex items-start gap-2">
|
||||
<span className="bg-primary/10 text-primary rounded-full w-5 h-5 flex items-center justify-center text-xs font-medium shrink-0 mt-0.5">7</span>
|
||||
<span>Start chatting with your bot to trigger workflows!</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="webhook-url" className="text-sm font-medium flex items-center gap-2">
|
||||
Webhook URL (copy from above)
|
||||
<span className="text-red-500">*</span>
|
||||
{getFieldStatus('webhook_url') === 'success' && (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
{getFieldStatus('webhook_url') === 'error' && (
|
||||
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id="webhook-url"
|
||||
placeholder="Paste the webhook URL from above"
|
||||
value={config?.webhook_url || ''}
|
||||
onChange={(e) => updateConfig('webhook_url', e.target.value)}
|
||||
onBlur={(e) => validateField('webhook_url', e.target.value)}
|
||||
className={getFieldStatus('webhook_url') === 'error' ? 'border-red-500' : ''}
|
||||
/>
|
||||
{errors.webhook_url && (
|
||||
<p className="text-xs text-red-500">{errors.webhook_url}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bot-token" className="text-sm font-medium flex items-center gap-2">
|
||||
Bot Token
|
||||
<span className="text-red-500">*</span>
|
||||
{getFieldStatus('bot_token') === 'success' && (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
{getFieldStatus('bot_token') === 'error' && (
|
||||
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="bot-token"
|
||||
type={showBotToken ? "text" : "password"}
|
||||
placeholder="Enter your Telegram bot token (e.g., 123456:ABC-DEF...)"
|
||||
value={config?.bot_token || ''}
|
||||
onChange={(e) => updateConfig('bot_token', e.target.value)}
|
||||
onBlur={(e) => validateField('bot_token', e.target.value)}
|
||||
className={getFieldStatus('bot_token') === 'error' ? 'border-red-500 pr-10' : 'pr-10'}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowBotToken(!showBotToken)}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0"
|
||||
>
|
||||
{showBotToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
{errors.bot_token && (
|
||||
<p className="text-xs text-red-500">{errors.bot_token}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="secret-token" className="text-sm font-medium flex items-center gap-2">
|
||||
Secret Token (Optional)
|
||||
{getFieldStatus('secret_token') === 'success' && (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
{getFieldStatus('secret_token') === 'error' && (
|
||||
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="secret-token"
|
||||
type={showSecretToken ? "text" : "password"}
|
||||
placeholder="Enter secret token for additional security (optional)"
|
||||
value={config?.secret_token || ''}
|
||||
onChange={(e) => updateConfig('secret_token', e.target.value)}
|
||||
onBlur={(e) => validateField('secret_token', e.target.value)}
|
||||
className={getFieldStatus('secret_token') === 'error' ? 'border-red-500 pr-10' : 'pr-10'}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowSecretToken(!showSecretToken)}
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0"
|
||||
>
|
||||
{showSecretToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
{errors.secret_token && (
|
||||
<p className="text-xs text-red-500">{errors.secret_token}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Secret token provides additional security by verifying requests come from Telegram
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,6 +5,12 @@ export interface SlackWebhookConfig {
|
|||
username?: string;
|
||||
}
|
||||
|
||||
export interface TelegramWebhookConfig {
|
||||
webhook_url: string;
|
||||
bot_token: string;
|
||||
secret_token?: string;
|
||||
}
|
||||
|
||||
export interface GenericWebhookConfig {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
|
@ -12,10 +18,11 @@ export interface GenericWebhookConfig {
|
|||
}
|
||||
|
||||
export interface WebhookConfig {
|
||||
type: 'slack' | 'generic';
|
||||
type: 'slack' | 'telegram' | 'generic';
|
||||
method?: 'POST' | 'GET' | 'PUT';
|
||||
authentication?: 'none' | 'api_key' | 'bearer';
|
||||
slack?: SlackWebhookConfig;
|
||||
telegram?: TelegramWebhookConfig;
|
||||
generic?: GenericWebhookConfig;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue