chore(dev): functional telegram webhook

This commit is contained in:
Soumyadas15 2025-06-20 13:54:16 +05:30
parent daf2afa2a3
commit f0440892ba
10 changed files with 712 additions and 16 deletions

View File

@ -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:

View File

@ -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

View File

@ -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."""

View File

@ -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}")

View File

@ -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']

View File

@ -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

View File

@ -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>
)}

View File

@ -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

View File

@ -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>
);
}

View File

@ -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;
}