From 47d3cbe04ac0476703880465c1b26b9ea7f4bcd1 Mon Sep 17 00:00:00 2001 From: Adam Gough <77861281+aadamgough@users.noreply.github.com> Date: Thu, 1 May 2025 22:26:33 -0700 Subject: [PATCH] feat(telegram): added telegram webhook, block, tools, & docs (#314) * jira and confluence token refresh * telegram message tool * webhook configured * telegram requires permanent url * webhook working * telegram done * test * telegram webhook working * package-lock.json * style curl command * added setWebhook logic * added some documentation for chat ID * styling changes * added delete webhook logic * adding docs * updated docs for jira and telegram * added links * moved function to other route.ts * logger * mrge.io changes --------- Co-authored-by: Adam Gough --- docs/content/docs/tools/jira.mdx | 13 ++ docs/content/docs/tools/telegram.mdx | 100 +++++++++++++++ sim/app/api/webhooks/[id]/route.ts | 63 +++++++++- sim/app/api/webhooks/route.ts | 89 ++++++++++++++ .../components/providers/telegram-config.tsx | 114 ++++++++++++++++++ .../webhook/components/webhook-modal.tsx | 83 +++++++++++-- .../components/webhook/webhook-config.tsx | 34 ++++++ .../components/sub-block/sub-block.tsx | 22 +++- sim/blocks/blocks/starter.ts | 1 + sim/blocks/blocks/telegram.ts | 64 ++++++++++ sim/blocks/index.ts | 3 + sim/components/icons.tsx | 25 ++++ sim/lib/oauth.ts | 8 ++ sim/package-lock.json | 105 ++++++++++++++++ sim/tools/registry.ts | 2 + sim/tools/telegram/index.ts | 3 + sim/tools/telegram/message.ts | 57 +++++++++ sim/tools/telegram/types.ts | 23 ++++ 18 files changed, 795 insertions(+), 14 deletions(-) create mode 100644 docs/content/docs/tools/telegram.mdx create mode 100644 sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram-config.tsx create mode 100644 sim/blocks/blocks/telegram.ts create mode 100644 sim/tools/telegram/index.ts create mode 100644 sim/tools/telegram/message.ts create mode 100644 sim/tools/telegram/types.ts diff --git a/docs/content/docs/tools/jira.mdx b/docs/content/docs/tools/jira.mdx index 626ad1e54..1e2022074 100644 --- a/docs/content/docs/tools/jira.mdx +++ b/docs/content/docs/tools/jira.mdx @@ -25,6 +25,19 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" `} /> +[Jira](https://www.atlassian.com/jira) is a leading project management and issue tracking platform that helps teams plan, track, and manage agile software development projects effectively. As part of the Atlassian suite, Jira has become the industry standard for software development teams and project management professionals worldwide. + +Jira provides a comprehensive set of tools for managing complex projects through its flexible and customizable workflow system. With its robust API and integration capabilities, Jira enables teams to streamline their development processes and maintain clear visibility of project progress. + +Key features of Jira include: + +- Agile Project Management: Support for Scrum and Kanban methodologies with customizable boards and workflows +- Issue Tracking: Sophisticated tracking system for bugs, stories, epics, and tasks with detailed reporting +- Workflow Automation: Powerful automation rules to streamline repetitive tasks and processes +- Advanced Search: JQL (Jira Query Language) for complex issue filtering and reporting + +In Sim Studio, the Jira integration allows your agents to seamlessly interact with your project management workflow. This creates opportunities for automated issue creation, updates, and tracking as part of your AI workflows. The integration enables agents to create, retrieve, and update Jira issues programmatically, facilitating automated project management tasks and ensuring that important information is properly tracked and documented. By connecting Sim Studio with Jira, you can build intelligent agents that maintain project visibility while automating routine project management tasks, enhancing team productivity and ensuring consistent project tracking. + ## Usage Instructions Connect to Jira workspaces to read, write, and update issues. Access content, metadata, and integrate Jira documentation into your workflows. diff --git a/docs/content/docs/tools/telegram.mdx b/docs/content/docs/tools/telegram.mdx new file mode 100644 index 000000000..7a5d0fa9b --- /dev/null +++ b/docs/content/docs/tools/telegram.mdx @@ -0,0 +1,100 @@ +--- +title: Telegram +description: Send a message through Telegram +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + + `} +/> + +[Telegram](https://telegram.org) is a secure, cloud-based messaging platform that enables fast and reliable communication across devices and platforms. With over 700 million monthly active users, Telegram has established itself as one of the world's leading messaging services, known for its security, speed, and powerful API capabilities. + + + +Telegram's Bot API provides a robust framework for creating automated messaging solutions and integrating communication features into applications. With support for rich media, inline keyboards, and custom commands, Telegram bots can facilitate sophisticated interaction patterns and automated workflows. + +Key features of Telegram include: + +- Secure Communication: End-to-end encryption and secure cloud storage for messages and media +- Bot Platform: Powerful bot API for creating automated messaging solutions and interactive experiences +- Rich Media Support: Send and receive messages with text formatting, images, files, and interactive elements +- Global Reach: Connect with users worldwide with support for multiple languages and platforms + +In Sim Studio, the Telegram integration enables your agents to leverage these powerful messaging capabilities as part of their workflows. This creates opportunities for automated notifications, alerts, and interactive conversations through Telegram's secure messaging platform. The integration allows agents to send messages programmatically to individuals or channels, enabling timely communication and updates. By connecting Sim Studio with Telegram, you can build intelligent agents that engage users through a secure and widely-adopted messaging platform, perfect for delivering notifications, updates, and interactive communications. + +## Usage Instructions + +Send messages to any Telegram channel using your Bot API key. Integrate automated notifications and alerts into your workflow to keep your team informed. + + + +## Tools + +### `telegram_message` + +Send messages to Telegram channels or users through the Telegram Bot API. Enables direct communication and notifications with message tracking and chat confirmation. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | Your Telegram Bot API Token | +| `chatId` | string | Yes | Target Telegram chat ID | +| `text` | string | Yes | Message text to send | + +#### Output + +| Parameter | Type | +| --------- | ---- | +| `ok` | string | +| `date` | string | + + + +## Block Configuration + +### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | Bot Token - Enter your Telegram Bot Token | + + + +### Outputs + +| Output | Type | Description | +| ------ | ---- | ----------- | +| `response` | object | Output from response | +| ↳ `ok` | boolean | ok of the response | +| ↳ `result` | json | result of the response | + + +## Notes + +- Category: `tools` +- Type: `telegram` diff --git a/sim/app/api/webhooks/[id]/route.ts b/sim/app/api/webhooks/[id]/route.ts index 63ebd7cd1..958adea98 100644 --- a/sim/app/api/webhooks/[id]/route.ts +++ b/sim/app/api/webhooks/[id]/route.ts @@ -160,13 +160,70 @@ export async function DELETE( return NextResponse.json({ error: 'Unauthorized' }, { status: 403 }) } - // Delete the webhook + const foundWebhook = webhooks[0].webhook + + // If it's a Telegram webhook, delete it from Telegram first + if (foundWebhook.provider === 'telegram') { + try { + const { botToken } = foundWebhook.providerConfig as { botToken: string } + + if (!botToken) { + logger.warn(`[${requestId}] Missing botToken for Telegram webhook deletion.`, { + webhookId: id, + }) + return NextResponse.json( + { error: 'Missing botToken for Telegram webhook deletion' }, + { status: 400 } + ) + } + + const telegramApiUrl = `https://api.telegram.org/bot${botToken}/deleteWebhook` + const telegramResponse = await fetch(telegramApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + + const responseBody = await telegramResponse.json() + if (!telegramResponse.ok || !responseBody.ok) { + const errorMessage = responseBody.description || `Failed to delete Telegram webhook. Status: ${telegramResponse.status}` + logger.error(`[${requestId}] ${errorMessage}`, { + response: responseBody + }) + return NextResponse.json( + { error: 'Failed to delete webhook from Telegram', details: errorMessage }, + { status: 500 } + ) + } + + logger.info(`[${requestId}] Successfully deleted Telegram webhook for webhook ${id}`) + } catch (error: any) { + logger.error(`[${requestId}] Error deleting Telegram webhook`, { + webhookId: id, + error: error.message, + stack: error.stack, + }) + return NextResponse.json( + { + error: 'Failed to delete webhook from Telegram', + details: error.message + }, + { status: 500 } + ) + } + } + + // Delete the webhook from the database await db.delete(webhook).where(eq(webhook.id, id)) logger.info(`[${requestId}] Successfully deleted webhook: ${id}`) return NextResponse.json({ success: true }, { status: 200 }) - } catch (error) { - logger.error(`[${requestId}] Error deleting webhook`, error) + } catch (error: any) { + logger.error(`[${requestId}] Error deleting webhook`, { + error: error.message, + stack: error.stack + }) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } diff --git a/sim/app/api/webhooks/route.ts b/sim/app/api/webhooks/route.ts index bb2fff058..d0a47ab91 100644 --- a/sim/app/api/webhooks/route.ts +++ b/sim/app/api/webhooks/route.ts @@ -162,6 +162,26 @@ export async function POST(request: NextRequest) { } // --- End Airtable specific logic --- + // --- Attempt to create webhook in Telegram if provider is 'telegram' --- + if (savedWebhook && provider === 'telegram') { + logger.info( + `[${requestId}] Telegram provider detected. Attempting to create webhook in Telegram.` + ) + try { + await createTelegramWebhookSubscription(request, userId, savedWebhook, requestId) + } catch (err) { + logger.error(`[${requestId}] Error creating Telegram webhook`, err) + return NextResponse.json( + { + error: 'Failed to create webhook in Telegram', + details: err instanceof Error ? err.message : 'Unknown error', + }, + { status: 500 } + ) + } + } + // --- End Telegram specific logic --- + const status = existingWebhooks.length > 0 ? 200 : 201 return NextResponse.json({ webhook: savedWebhook }, { status }) } catch (error: any) { @@ -289,3 +309,72 @@ async function createAirtableWebhookSubscription( ) } } + +// Helper function to create the webhook subscription in Telegram +async function createTelegramWebhookSubscription( + request: NextRequest, + userId: string, + webhookData: any, + requestId: string +) { + try { + const { path, providerConfig } = webhookData + const { botToken, triggerPhrase } = providerConfig || {} + + if (!botToken || !triggerPhrase) { + logger.warn(`[${requestId}] Missing botToken or triggerPhrase for Telegram webhook creation.`, { + webhookId: webhookData.id, + }) + return // Cannot proceed without botToken and triggerPhrase + } + + const requestOrigin = new URL(request.url).origin + // Ensure origin does not point to localhost for external API calls + const effectiveOrigin = requestOrigin.includes('localhost') + ? process.env.NEXT_PUBLIC_APP_URL || requestOrigin // Use env var if available, fallback to original + : requestOrigin + + const notificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${path}` + if (effectiveOrigin !== requestOrigin) { + logger.debug( + `[${requestId}] Remapped localhost origin to ${effectiveOrigin} for notificationUrl` + ) + } + + const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook` + + const requestBody: any = { + url: notificationUrl, + allowed_updates: ['message'], + } + + const telegramResponse = await fetch(telegramApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await telegramResponse.json() + if (!telegramResponse.ok || !responseBody.ok) { + const errorMessage = responseBody.description || `Failed to create Telegram webhook. Status: ${telegramResponse.status}` + logger.error(`[${requestId}] ${errorMessage}`, { + response: responseBody + }) + throw new Error(errorMessage) + } + + logger.info( + `[${requestId}] Successfully created Telegram webhook for webhook ${webhookData.id}.` + ) + } catch (error: any) { + logger.error( + `[${requestId}] Exception during Telegram webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + } +} diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram-config.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram-config.tsx new file mode 100644 index 000000000..1b3c1a57d --- /dev/null +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/telegram-config.tsx @@ -0,0 +1,114 @@ +import { Input } from '@/components/ui/input' +import { Skeleton } from '@/components/ui/skeleton' +import { ConfigField } from '../ui/config-field' +import { ConfigSection } from '../ui/config-section' +import { InstructionsSection } from '../ui/instructions-section' +import { TestResultDisplay as WebhookTestResult } from '../ui/test-result' + +interface TelegramConfigProps { + botToken: string + setBotToken: (value: string) => void + triggerPhrase: string + setTriggerPhrase: (value: string) => void + isLoadingToken: boolean + testResult: any + copied: string | null + copyToClipboard: (text: string, type: string) => void + testWebhook?: () => void // Optional test function + webhookId?: string // Webhook ID to enable testing + webhookUrl: string // Added webhook URL +} + +export function TelegramConfig({ + botToken, + setBotToken, + triggerPhrase, + setTriggerPhrase, + isLoadingToken, + testResult, + copied, + copyToClipboard, + testWebhook, + webhookId, + webhookUrl, +}: TelegramConfigProps) { + + return ( +
+ + + {isLoadingToken ? ( + + ) : ( + { + setBotToken(e.target.value); + }} + placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" + type="password" + required + /> + )} + + + + {isLoadingToken ? ( + + ) : ( + { + setTriggerPhrase(e.target.value); + }} + placeholder="/start_workflow" + required + /> + )} + + + + {testResult && ( + + )} + + +
    +
  1. + Message "/newbot" to {' '} + { + e.stopPropagation(); + window.open('https://t.me/BotFather', '_blank', 'noopener,noreferrer'); + e.preventDefault(); + }} + > + @BotFather + {' '} in Telegram to create a bot and copy its token. +
  2. +
  3. Enter your Bot Token and a trigger phrase above.
  4. +
  5. Ensure your webhook URL uses HTTPS with a valid SSL certificate.
  6. +
  7. Save settings and send the trigger phrase to your bot to start the workflow.
  8. +
+
+
+ ) +} diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx index fb95a7025..a51edaa55 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/webhook-modal.tsx @@ -22,6 +22,7 @@ import { DeleteConfirmDialog } from './ui/confirmation' import { UnsavedChangesDialog } from './ui/confirmation' import { WebhookDialogFooter } from './ui/webhook-footer' import { WebhookUrlField } from './ui/webhook-url' +import { TelegramConfig } from './providers/telegram-config' const logger = createLogger('WebhookModal') @@ -78,7 +79,8 @@ export function WebhookModal({ const [discordWebhookName, setDiscordWebhookName] = useState('') const [discordAvatarUrl, setDiscordAvatarUrl] = useState('') const [slackSigningSecret, setSlackSigningSecret] = useState('') - + const [telegramBotToken, setTelegramBotToken] = useState('') + const [telegramTriggerPhrase, setTelegramTriggerPhrase] = useState('') // Airtable-specific state const [airtableWebhookSecret, setAirtableWebhookSecret] = useState('') const [airtableBaseId, setAirtableBaseId] = useState('') @@ -100,6 +102,8 @@ export function WebhookModal({ airtableBaseId: '', airtableTableId: '', airtableIncludeCellValues: false, + telegramBotToken: '', + telegramTriggerPhrase: '', }) // Get the current provider configuration @@ -213,6 +217,18 @@ export function WebhookModal({ airtableTableId: tableIdVal, airtableIncludeCellValues: includeCells, })) + } else if (webhookProvider === 'telegram') { + const botToken = config.botToken || '' + const triggerPhrase = config.triggerPhrase || '' + + setTelegramBotToken(botToken) + setTelegramTriggerPhrase(triggerPhrase) + + setOriginalValues((prev) => ({ + ...prev, + telegramBotToken: botToken, + telegramTriggerPhrase: triggerPhrase, + })) } } } @@ -250,7 +266,10 @@ export function WebhookModal({ (airtableWebhookSecret !== originalValues.airtableWebhookSecret || airtableBaseId !== originalValues.airtableBaseId || airtableTableId !== originalValues.airtableTableId || - airtableIncludeCellValues !== originalValues.airtableIncludeCellValues)) + airtableIncludeCellValues !== originalValues.airtableIncludeCellValues)) || + (webhookProvider === 'telegram' && + (telegramBotToken !== originalValues.telegramBotToken || + telegramTriggerPhrase !== originalValues.telegramTriggerPhrase)) setHasUnsavedChanges(hasChanges) }, [ @@ -269,6 +288,8 @@ export function WebhookModal({ airtableBaseId, airtableTableId, airtableIncludeCellValues, + telegramBotToken, + telegramTriggerPhrase, ]) // Validate required fields for current provider @@ -282,7 +303,6 @@ export function WebhookModal({ isValid = slackSigningSecret.trim() !== '' break case 'whatsapp': - // Although we auto-generate a token on creation, user could clear it when editing isValid = whatsappVerificationToken.trim() !== '' break case 'github': @@ -291,6 +311,9 @@ export function WebhookModal({ case 'discord': isValid = discordWebhookName.trim() !== '' break + case 'telegram': + isValid = telegramBotToken.trim() !== '' && telegramTriggerPhrase.trim() !== '' + break } setIsCurrentConfigValid(isValid) }, [ @@ -299,6 +322,8 @@ export function WebhookModal({ airtableTableId, slackSigningSecret, whatsappVerificationToken, + telegramBotToken, + telegramTriggerPhrase, ]) // Use the provided path or generate a UUID-based path @@ -355,12 +380,18 @@ export function WebhookModal({ tableId: airtableTableId, includeCellValuesInFieldIds: airtableIncludeCellValues ? 'all' : undefined, } + case 'telegram': + return { + botToken: telegramBotToken || undefined, + triggerPhrase: telegramTriggerPhrase || undefined, + } default: return {} } } const handleSave = async () => { + logger.debug('Saving webhook...') if (!isCurrentConfigValid) { logger.warn('Attempted to save with invalid configuration') // Add user feedback for invalid configuration @@ -400,6 +431,8 @@ export function WebhookModal({ airtableBaseId, airtableTableId, airtableIncludeCellValues, + telegramBotToken, + telegramTriggerPhrase, }) setHasUnsavedChanges(false) setTestResult({ @@ -413,8 +446,8 @@ export function WebhookModal({ }) } } - } catch (error) { - logger.error('Error saving webhook:', { error }) + } catch (error: any) { + logger.error('Error saving webhook:', error) setTestResult({ success: false, message: @@ -497,10 +530,26 @@ export function WebhookModal({ test: data.test, }) } else { - setTestResult({ - success: false, - message: data.message || data.error || 'Webhook test failed with success=false', - }) + // For Telegram, provide more specific error messages + if (webhookProvider === 'telegram') { + const errorMessage = data.message || data.error || 'Webhook test failed' + if (errorMessage.includes('SSL')) { + setTestResult({ + success: false, + message: 'Telegram webhooks require HTTPS. Please ensure your domain has a valid SSL certificate.', + }) + } else { + setTestResult({ + success: false, + message: `Telegram webhook test failed: ${errorMessage}`, + }) + } + } else { + setTestResult({ + success: false, + message: data.message || data.error || 'Webhook test failed with success=false', + }) + } } } catch (error: any) { logger.error('Error testing webhook:', { error }) @@ -597,6 +646,22 @@ export function WebhookModal({ webhookUrl={webhookUrl} /> ) + case 'telegram': + return ( + + ) case 'generic': default: return ( diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx index efd419298..05cd2914b 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/webhook-config.tsx @@ -8,6 +8,7 @@ import { SlackIcon, StripeIcon, WhatsAppIcon, + TelegramIcon, } from '@/components/icons' import { Button } from '@/components/ui/button' import { createLogger } from '@/lib/logs/console-logger' @@ -70,6 +71,11 @@ export interface AirtableWebhookConfig { includeCellValuesInFieldIds?: 'all' | undefined } +export interface TelegramConfig { + botToken: string + triggerPhrase: string +} + // Union type for all provider configurations export type ProviderConfig = | WhatsAppConfig @@ -79,6 +85,7 @@ export type ProviderConfig = | GeneralWebhookConfig | SlackConfig | AirtableWebhookConfig + | TelegramConfig | Record // Define available webhook providers @@ -212,6 +219,25 @@ export const WEBHOOK_PROVIDERS: { [key: string]: WebhookProvider } = { }, }, }, + telegram: { + id: 'telegram', + name: 'Telegram', + icon: (props) => , + configFields: { + botToken: { + type: 'string', + label: 'Bot Token', + placeholder: 'Enter your Telegram Bot Token', + description: 'The token for your Telegram bot.', + }, + triggerPhrase: { + type: 'string', + label: 'Trigger Phrase', + placeholder: '/start_workflow', + description: 'The phrase that will trigger the workflow when sent to the bot.', + }, + }, + }, } interface WebhookConfigProps { @@ -242,6 +268,14 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf // Store provider-specific configuration const [providerConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig') + // Reset provider config when provider changes + useEffect(() => { + if (webhookProvider) { + // Reset the provider config when the provider changes + setProviderConfig({}) + } + }, [webhookProvider, setProviderConfig]) + // Store the actual provider from the database const [actualProvider, setActualProvider] = useState(null) diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index c4aa19c32..ba4823385 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -25,6 +25,7 @@ import { Table } from './components/table' import { TimeInput } from './components/time-input' import { ToolInput } from './components/tool-input/tool-input' import { WebhookConfig } from './components/webhook/webhook-config' +import { Info } from 'lucide-react' interface SubBlockProps { blockId: string @@ -192,18 +193,35 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) { return (
{config.type !== 'switch' && ( -