mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(webhooks): fixed all webhook structures (#935)
* fix for variable format + trig * fixed slack variable * microsoft teams working * fixed outlook, plus added other minor documentation changes and fixed subblock * removed discord webhook logic * added airtable logic * bun run lint * test * test again * test again 2 * test again 3 * test again 4 * test again 4 * test again 4 * bun run lint * test 5 * test 6 * test 7 * test 7 * test 7 * test 7 * test 7 * test 7 * test 8 * test 9 * test 9 * test 9 * test 10 * test 10 * bun run lint, plus github fixed * removed some debug statements #935 * testing resolver removing * testing trig --------- Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local> Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
This commit is contained in:
@@ -352,12 +352,15 @@ async function createAirtableWebhookSubscription(
|
||||
return // Cannot proceed without base/table IDs
|
||||
}
|
||||
|
||||
const accessToken = await getOAuthToken(userId, 'airtable') // Use 'airtable' as the providerId key
|
||||
const accessToken = await getOAuthToken(userId, 'airtable')
|
||||
if (!accessToken) {
|
||||
logger.warn(
|
||||
`[${requestId}] Could not retrieve Airtable access token for user ${userId}. Cannot create webhook in Airtable.`
|
||||
)
|
||||
return
|
||||
// Instead of silently returning, throw an error with clear user guidance
|
||||
throw new Error(
|
||||
'Airtable account connection required. Please connect your Airtable account in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
const requestOrigin = new URL(request.url).origin
|
||||
|
||||
@@ -100,20 +100,41 @@ export async function POST(
|
||||
return new NextResponse('Failed to read request body', { status: 400 })
|
||||
}
|
||||
|
||||
// Parse the body as JSON
|
||||
// Parse the body - handle both JSON and form-encoded payloads
|
||||
let body: any
|
||||
try {
|
||||
body = JSON.parse(rawBody)
|
||||
// Check content type to handle both JSON and form-encoded payloads
|
||||
const contentType = request.headers.get('content-type') || ''
|
||||
|
||||
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
// GitHub sends form-encoded data with JSON in the 'payload' field
|
||||
const formData = new URLSearchParams(rawBody)
|
||||
const payloadString = formData.get('payload')
|
||||
|
||||
if (!payloadString) {
|
||||
logger.warn(`[${requestId}] No payload field found in form-encoded data`)
|
||||
return new NextResponse('Missing payload field', { status: 400 })
|
||||
}
|
||||
|
||||
body = JSON.parse(payloadString)
|
||||
logger.debug(`[${requestId}] Parsed form-encoded GitHub webhook payload`)
|
||||
} else {
|
||||
// Default to JSON parsing
|
||||
body = JSON.parse(rawBody)
|
||||
logger.debug(`[${requestId}] Parsed JSON webhook payload`)
|
||||
}
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
logger.warn(`[${requestId}] Rejecting empty JSON object`)
|
||||
return new NextResponse('Empty JSON payload', { status: 400 })
|
||||
}
|
||||
} catch (parseError) {
|
||||
logger.error(`[${requestId}] Failed to parse JSON body`, {
|
||||
logger.error(`[${requestId}] Failed to parse webhook body`, {
|
||||
error: parseError instanceof Error ? parseError.message : String(parseError),
|
||||
contentType: request.headers.get('content-type'),
|
||||
bodyPreview: `${rawBody?.slice(0, 100)}...`,
|
||||
})
|
||||
return new NextResponse('Invalid JSON payload', { status: 400 })
|
||||
return new NextResponse('Invalid payload format', { status: 400 })
|
||||
}
|
||||
|
||||
// Handle Slack challenge
|
||||
|
||||
@@ -94,6 +94,8 @@ export function TriggerModal({
|
||||
setSelectedCredentialId(credentialValue)
|
||||
if (triggerDef.provider === 'gmail') {
|
||||
loadGmailLabels(credentialValue)
|
||||
} else if (triggerDef.provider === 'outlook') {
|
||||
loadOutlookFolders(credentialValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +141,30 @@ export function TriggerModal({
|
||||
}
|
||||
}
|
||||
|
||||
// Load Outlook folders for the selected credential
|
||||
const loadOutlookFolders = async (credentialId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/tools/outlook/folders?credentialId=${credentialId}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.folders && Array.isArray(data.folders)) {
|
||||
const folderOptions = data.folders.map((folder: any) => ({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
}))
|
||||
setDynamicOptions((prev) => ({
|
||||
...prev,
|
||||
folderIds: folderOptions,
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
logger.error('Failed to load Outlook folders:', response.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error loading Outlook folders:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate webhook path and URL
|
||||
useEffect(() => {
|
||||
// For triggers that don't use webhooks (like Gmail polling), skip URL generation
|
||||
@@ -152,15 +178,14 @@ export function TriggerModal({
|
||||
|
||||
// If no path exists, generate one automatically
|
||||
if (!finalPath) {
|
||||
const timestamp = Date.now()
|
||||
const randomId = Math.random().toString(36).substring(2, 8)
|
||||
finalPath = `/${triggerDef.provider}/${timestamp}-${randomId}`
|
||||
// Use UUID format consistent with other webhooks
|
||||
finalPath = crypto.randomUUID()
|
||||
setGeneratedPath(finalPath)
|
||||
}
|
||||
|
||||
if (finalPath) {
|
||||
const baseUrl = window.location.origin
|
||||
setWebhookUrl(`${baseUrl}/api/webhooks/trigger${finalPath}`)
|
||||
setWebhookUrl(`${baseUrl}/api/webhooks/trigger/${finalPath}`)
|
||||
}
|
||||
}, [triggerPath, triggerDef.provider, triggerDef.requiresCredentials, triggerDef.webhook])
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export {
|
||||
AirtableConfig,
|
||||
DiscordConfig,
|
||||
GenericConfig,
|
||||
GithubConfig,
|
||||
GmailConfig,
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Terminal } from 'lucide-react'
|
||||
import { Alert, AlertDescription, AlertTitle, CodeBlock, Input } from '@/components/ui'
|
||||
import {
|
||||
ConfigField,
|
||||
ConfigSection,
|
||||
InstructionsSection,
|
||||
TestResultDisplay,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components'
|
||||
|
||||
interface DiscordConfigProps {
|
||||
webhookName: string
|
||||
setWebhookName: (name: string) => void
|
||||
avatarUrl: string
|
||||
setAvatarUrl: (url: string) => void
|
||||
isLoadingToken: boolean
|
||||
testResult: {
|
||||
success: boolean
|
||||
message?: string
|
||||
test?: any
|
||||
} | null
|
||||
copied: string | null
|
||||
copyToClipboard: (text: string, type: string) => void
|
||||
testWebhook: () => Promise<void>
|
||||
}
|
||||
|
||||
const examplePayload = JSON.stringify(
|
||||
{
|
||||
content: 'Hello from Sim!',
|
||||
username: 'Optional Custom Name',
|
||||
avatar_url: 'https://example.com/avatar.png',
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
export function DiscordConfig({
|
||||
webhookName,
|
||||
setWebhookName,
|
||||
avatarUrl,
|
||||
setAvatarUrl,
|
||||
isLoadingToken,
|
||||
testResult,
|
||||
copied,
|
||||
copyToClipboard,
|
||||
testWebhook, // Passed to TestResultDisplay
|
||||
}: DiscordConfigProps) {
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
<ConfigSection title='Discord Appearance (Optional)'>
|
||||
<ConfigField
|
||||
id='discord-webhook-name'
|
||||
label='Webhook Name'
|
||||
description='This name will be displayed as the sender of messages in Discord.'
|
||||
>
|
||||
<Input
|
||||
id='discord-webhook-name'
|
||||
value={webhookName}
|
||||
onChange={(e) => setWebhookName(e.target.value)}
|
||||
placeholder='Sim Bot'
|
||||
disabled={isLoadingToken}
|
||||
/>
|
||||
</ConfigField>
|
||||
|
||||
<ConfigField
|
||||
id='discord-avatar-url'
|
||||
label='Avatar URL'
|
||||
description="URL to an image that will be used as the webhook's avatar."
|
||||
>
|
||||
<Input
|
||||
id='discord-avatar-url'
|
||||
value={avatarUrl}
|
||||
onChange={(e) => setAvatarUrl(e.target.value)}
|
||||
placeholder='https://example.com/avatar.png'
|
||||
disabled={isLoadingToken}
|
||||
type='url'
|
||||
/>
|
||||
</ConfigField>
|
||||
</ConfigSection>
|
||||
|
||||
<TestResultDisplay
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
showCurlCommand={true} // Discord can be tested via curl
|
||||
/>
|
||||
|
||||
<InstructionsSection
|
||||
title='Receiving Events from Discord (Incoming Webhook)'
|
||||
tip='Create a webhook in Discord and paste its URL into the Webhook URL field above.'
|
||||
>
|
||||
<ol className='list-inside list-decimal space-y-1'>
|
||||
<li>Go to Discord Server Settings {'>'} Integrations.</li>
|
||||
<li>Click "Webhooks" then "New Webhook".</li>
|
||||
<li>Customize the name and channel.</li>
|
||||
<li>Click "Copy Webhook URL".</li>
|
||||
<li>
|
||||
Paste the copied Discord URL into the main <strong>Webhook URL</strong> field above.
|
||||
</li>
|
||||
<li>Your workflow triggers when Discord sends an event to that URL.</li>
|
||||
</ol>
|
||||
</InstructionsSection>
|
||||
|
||||
<InstructionsSection title='Sending Messages to Discord (Outgoing via this URL)'>
|
||||
<p>
|
||||
To send messages <i>to</i> Discord using the Sim Webhook URL (above), make a POST request
|
||||
with a JSON body like this:
|
||||
</p>
|
||||
<CodeBlock language='json' code={examplePayload} className='mt-2 text-sm' />
|
||||
<ul className='mt-3 list-outside list-disc space-y-1 pl-4'>
|
||||
<li>Customize message appearance with embeds (see Discord docs).</li>
|
||||
<li>Override the default username/avatar per request if needed.</li>
|
||||
</ul>
|
||||
</InstructionsSection>
|
||||
|
||||
<Alert>
|
||||
<Terminal className='h-4 w-4' />
|
||||
<AlertTitle>Security Note</AlertTitle>
|
||||
<AlertDescription>
|
||||
The Sim Webhook URL allows sending messages <i>to</i> Discord. Treat it like a password.
|
||||
Don't share it publicly.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export { AirtableConfig } from './airtable'
|
||||
export { DiscordConfig } from './discord'
|
||||
export { GenericConfig } from './generic'
|
||||
export { GithubConfig } from './github'
|
||||
export { GmailConfig } from './gmail'
|
||||
|
||||
@@ -137,6 +137,10 @@ export function SlackConfig({
|
||||
<li>Paste the Webhook URL (from above) into the "Request URL" field</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>
|
||||
Go to <strong>Install App</strong> in the left sidebar and install the app into your
|
||||
desired Slack workspace and channel.
|
||||
</li>
|
||||
<li>Save changes in both Slack and here.</li>
|
||||
</ol>
|
||||
</InstructionsSection>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
AirtableConfig,
|
||||
DeleteConfirmDialog,
|
||||
DiscordConfig,
|
||||
GenericConfig,
|
||||
GithubConfig,
|
||||
GmailConfig,
|
||||
@@ -83,8 +82,7 @@ export function WebhookModal({
|
||||
// Provider-specific state
|
||||
const [whatsappVerificationToken, setWhatsappVerificationToken] = useState('')
|
||||
const [githubContentType, setGithubContentType] = useState('application/json')
|
||||
const [discordWebhookName, setDiscordWebhookName] = useState('')
|
||||
const [discordAvatarUrl, setDiscordAvatarUrl] = useState('')
|
||||
|
||||
const [slackSigningSecret, setSlackSigningSecret] = useState('')
|
||||
const [telegramBotToken, setTelegramBotToken] = useState('')
|
||||
// Microsoft Teams-specific state
|
||||
@@ -106,8 +104,7 @@ export function WebhookModal({
|
||||
secretHeaderName: '',
|
||||
requireAuth: false,
|
||||
allowedIps: '',
|
||||
discordWebhookName: '',
|
||||
discordAvatarUrl: '',
|
||||
|
||||
airtableWebhookSecret: '',
|
||||
airtableBaseId: '',
|
||||
airtableTableId: '',
|
||||
@@ -184,18 +181,6 @@ export function WebhookModal({
|
||||
const contentType = config.contentType || 'application/json'
|
||||
setGithubContentType(contentType)
|
||||
setOriginalValues((prev) => ({ ...prev, githubContentType: contentType }))
|
||||
} else if (webhookProvider === 'discord') {
|
||||
const webhookName = config.webhookName || ''
|
||||
const avatarUrl = config.avatarUrl || ''
|
||||
|
||||
setDiscordWebhookName(webhookName)
|
||||
setDiscordAvatarUrl(avatarUrl)
|
||||
|
||||
setOriginalValues((prev) => ({
|
||||
...prev,
|
||||
discordWebhookName: webhookName,
|
||||
discordAvatarUrl: avatarUrl,
|
||||
}))
|
||||
} else if (webhookProvider === 'generic') {
|
||||
// Set general webhook configuration
|
||||
const token = config.token || ''
|
||||
@@ -328,9 +313,6 @@ export function WebhookModal({
|
||||
(webhookProvider === 'whatsapp' &&
|
||||
whatsappVerificationToken !== originalValues.whatsappVerificationToken) ||
|
||||
(webhookProvider === 'github' && githubContentType !== originalValues.githubContentType) ||
|
||||
(webhookProvider === 'discord' &&
|
||||
(discordWebhookName !== originalValues.discordWebhookName ||
|
||||
discordAvatarUrl !== originalValues.discordAvatarUrl)) ||
|
||||
(webhookProvider === 'generic' &&
|
||||
(generalToken !== originalValues.generalToken ||
|
||||
secretHeaderName !== originalValues.secretHeaderName ||
|
||||
@@ -357,8 +339,6 @@ export function WebhookModal({
|
||||
webhookProvider,
|
||||
whatsappVerificationToken,
|
||||
githubContentType,
|
||||
discordWebhookName,
|
||||
discordAvatarUrl,
|
||||
generalToken,
|
||||
secretHeaderName,
|
||||
requireAuth,
|
||||
@@ -393,9 +373,7 @@ export function WebhookModal({
|
||||
case 'github':
|
||||
isValid = generalToken.trim() !== ''
|
||||
break
|
||||
case 'discord':
|
||||
isValid = discordWebhookName.trim() !== ''
|
||||
break
|
||||
|
||||
case 'telegram':
|
||||
isValid = telegramBotToken.trim() !== ''
|
||||
break
|
||||
@@ -442,11 +420,6 @@ export function WebhookModal({
|
||||
return { verificationToken: whatsappVerificationToken }
|
||||
case 'github':
|
||||
return { contentType: githubContentType }
|
||||
case 'discord':
|
||||
return {
|
||||
webhookName: discordWebhookName || undefined,
|
||||
avatarUrl: discordAvatarUrl || undefined,
|
||||
}
|
||||
case 'stripe':
|
||||
return {}
|
||||
case 'gmail':
|
||||
@@ -539,8 +512,6 @@ export function WebhookModal({
|
||||
secretHeaderName,
|
||||
requireAuth,
|
||||
allowedIps,
|
||||
discordWebhookName,
|
||||
discordAvatarUrl,
|
||||
slackSigningSecret,
|
||||
airtableWebhookSecret,
|
||||
airtableBaseId,
|
||||
@@ -738,20 +709,7 @@ export function WebhookModal({
|
||||
setIncludeRawEmail={setIncludeRawEmail}
|
||||
/>
|
||||
)
|
||||
case 'discord':
|
||||
return (
|
||||
<DiscordConfig
|
||||
webhookName={discordWebhookName}
|
||||
setWebhookName={setDiscordWebhookName}
|
||||
avatarUrl={discordAvatarUrl}
|
||||
setAvatarUrl={setDiscordAvatarUrl}
|
||||
isLoadingToken={isLoadingToken}
|
||||
testResult={testResult}
|
||||
copied={copied}
|
||||
copyToClipboard={copyToClipboard}
|
||||
testWebhook={testWebhook}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'stripe':
|
||||
return (
|
||||
<StripeConfig
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ExternalLink } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
AirtableIcon,
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
GmailIcon,
|
||||
MicrosoftTeamsIcon,
|
||||
@@ -46,11 +45,6 @@ export interface GitHubConfig {
|
||||
contentType: string
|
||||
}
|
||||
|
||||
export interface DiscordConfig {
|
||||
webhookName?: string
|
||||
avatarUrl?: string
|
||||
}
|
||||
|
||||
export type StripeConfig = Record<string, never>
|
||||
|
||||
export interface GeneralWebhookConfig {
|
||||
@@ -103,7 +97,6 @@ export interface MicrosoftTeamsConfig {
|
||||
export type ProviderConfig =
|
||||
| WhatsAppConfig
|
||||
| GitHubConfig
|
||||
| DiscordConfig
|
||||
| StripeConfig
|
||||
| GeneralWebhookConfig
|
||||
| SlackConfig
|
||||
@@ -219,25 +212,7 @@ export const WEBHOOK_PROVIDERS: { [key: string]: WebhookProvider } = {
|
||||
},
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
id: 'discord',
|
||||
name: 'Discord',
|
||||
icon: (props) => <DiscordIcon {...props} />,
|
||||
configFields: {
|
||||
webhookName: {
|
||||
type: 'string',
|
||||
label: 'Webhook Name',
|
||||
placeholder: 'Enter a name for the webhook',
|
||||
description: 'Custom name that will appear as the message sender in Discord.',
|
||||
},
|
||||
avatarUrl: {
|
||||
type: 'string',
|
||||
label: 'Avatar URL',
|
||||
placeholder: 'https://example.com/avatar.png',
|
||||
description: 'URL to an image that will be used as the webhook avatar.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
stripe: {
|
||||
id: 'stripe',
|
||||
name: 'Stripe',
|
||||
|
||||
@@ -10,7 +10,7 @@ import { fetchAndProcessAirtablePayloads, formatWebhookInput } from '@/lib/webho
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
|
||||
import { db } from '@/db'
|
||||
import { environment as environmentTable, userStats } from '@/db/schema'
|
||||
import { environment as environmentTable, userStats, webhook } from '@/db/schema'
|
||||
import { Executor } from '@/executor'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { mergeSubblockState } from '@/stores/workflows/server-utils'
|
||||
@@ -141,10 +141,21 @@ export const webhookExecution = task({
|
||||
`[${requestId}] Processing Airtable webhook via fetchAndProcessAirtablePayloads`
|
||||
)
|
||||
|
||||
// Load the actual webhook record from database to get providerConfig
|
||||
const [webhookRecord] = await db
|
||||
.select()
|
||||
.from(webhook)
|
||||
.where(eq(webhook.id, payload.webhookId))
|
||||
.limit(1)
|
||||
|
||||
if (!webhookRecord) {
|
||||
throw new Error(`Webhook record not found: ${payload.webhookId}`)
|
||||
}
|
||||
|
||||
const webhookData = {
|
||||
id: payload.webhookId,
|
||||
provider: payload.provider,
|
||||
providerConfig: {}, // Will be loaded within fetchAndProcessAirtablePayloads
|
||||
providerConfig: webhookRecord.providerConfig,
|
||||
}
|
||||
|
||||
// Create a mock workflow object for Airtable processing
|
||||
@@ -153,12 +164,85 @@ export const webhookExecution = task({
|
||||
userId: payload.userId,
|
||||
}
|
||||
|
||||
await fetchAndProcessAirtablePayloads(webhookData, mockWorkflow, requestId)
|
||||
// Get the processed Airtable input
|
||||
const airtableInput = await fetchAndProcessAirtablePayloads(
|
||||
webhookData,
|
||||
mockWorkflow,
|
||||
requestId
|
||||
)
|
||||
|
||||
// If we got input (changes), execute the workflow like other providers
|
||||
if (airtableInput) {
|
||||
logger.info(`[${requestId}] Executing workflow with Airtable changes`)
|
||||
|
||||
// Create executor and execute (same as standard webhook flow)
|
||||
const executor = new Executor({
|
||||
workflow: serializedWorkflow,
|
||||
currentBlockStates: processedBlockStates,
|
||||
envVarValues: decryptedEnvVars,
|
||||
workflowInput: airtableInput,
|
||||
workflowVariables,
|
||||
contextExtensions: {
|
||||
executionId,
|
||||
workspaceId: '',
|
||||
},
|
||||
})
|
||||
|
||||
// Set up logging on the executor
|
||||
loggingSession.setupExecutor(executor)
|
||||
|
||||
// Execute the workflow
|
||||
const result = await executor.execute(payload.workflowId, payload.blockId)
|
||||
|
||||
// Check if we got a StreamingExecution result
|
||||
const executionResult =
|
||||
'stream' in result && 'execution' in result ? result.execution : result
|
||||
|
||||
logger.info(`[${requestId}] Airtable webhook execution completed`, {
|
||||
success: executionResult.success,
|
||||
workflowId: payload.workflowId,
|
||||
})
|
||||
|
||||
// Update workflow run counts on success
|
||||
if (executionResult.success) {
|
||||
await updateWorkflowRunCounts(payload.workflowId)
|
||||
|
||||
// Track execution in user stats
|
||||
await db
|
||||
.update(userStats)
|
||||
.set({
|
||||
totalWebhookTriggers: sql`total_webhook_triggers + 1`,
|
||||
lastActive: sql`now()`,
|
||||
})
|
||||
.where(eq(userStats.userId, payload.userId))
|
||||
}
|
||||
|
||||
// Build trace spans and complete logging session
|
||||
const { traceSpans, totalDuration } = buildTraceSpans(executionResult)
|
||||
|
||||
await loggingSession.safeComplete({
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: totalDuration || 0,
|
||||
finalOutput: executionResult.output || {},
|
||||
traceSpans: traceSpans as any,
|
||||
})
|
||||
|
||||
return {
|
||||
success: executionResult.success,
|
||||
workflowId: payload.workflowId,
|
||||
executionId,
|
||||
output: executionResult.output,
|
||||
executedAt: new Date().toISOString(),
|
||||
provider: payload.provider,
|
||||
}
|
||||
}
|
||||
// No changes to process
|
||||
logger.info(`[${requestId}] No Airtable changes to process`)
|
||||
|
||||
await loggingSession.safeComplete({
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: 0,
|
||||
finalOutput: { message: 'Airtable webhook processed' },
|
||||
finalOutput: { message: 'No Airtable changes to process' },
|
||||
traceSpans: [],
|
||||
})
|
||||
|
||||
@@ -166,7 +250,7 @@ export const webhookExecution = task({
|
||||
success: true,
|
||||
workflowId: payload.workflowId,
|
||||
executionId,
|
||||
output: { message: 'Airtable webhook processed' },
|
||||
output: { message: 'No Airtable changes to process' },
|
||||
executedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,19 +213,5 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Message content' },
|
||||
data: { type: 'json', description: 'Response data' },
|
||||
// Trigger outputs
|
||||
content: { type: 'string', description: 'Message content from Discord webhook' },
|
||||
username: { type: 'string', description: 'Username of the sender (if provided)' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL of the sender (if provided)' },
|
||||
timestamp: { type: 'string', description: 'Timestamp when the webhook was triggered' },
|
||||
webhook_id: { type: 'string', description: 'Discord webhook identifier' },
|
||||
webhook_token: { type: 'string', description: 'Discord webhook token' },
|
||||
guild_id: { type: 'string', description: 'Discord server/guild ID' },
|
||||
channel_id: { type: 'string', description: 'Discord channel ID where the event occurred' },
|
||||
embeds: { type: 'string', description: 'Embedded content data (if any)' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['discord_webhook'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -125,6 +125,14 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
condition: { field: 'operation', value: ['write_chat', 'write_channel'] },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'triggerConfig',
|
||||
title: 'Trigger Configuration',
|
||||
type: 'trigger-config',
|
||||
layout: 'full',
|
||||
triggerProvider: 'microsoftteams',
|
||||
availableTriggers: ['microsoftteams_webhook'],
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
|
||||
@@ -46,6 +46,14 @@ export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = {
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'triggerConfig',
|
||||
title: 'Trigger Configuration',
|
||||
type: 'trigger-config',
|
||||
layout: 'full',
|
||||
triggerProvider: 'whatsapp',
|
||||
availableTriggers: ['whatsapp_webhook'],
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['whatsapp_send_message'],
|
||||
|
||||
@@ -23,14 +23,172 @@ export class TriggerBlockHandler implements BlockHandler {
|
||||
async execute(
|
||||
block: SerializedBlock,
|
||||
inputs: Record<string, any>,
|
||||
_context: ExecutionContext
|
||||
context: ExecutionContext
|
||||
): Promise<any> {
|
||||
logger.info(`Executing trigger block: ${block.id} (Type: ${block.metadata?.id})`)
|
||||
|
||||
// Trigger blocks don't execute anything - they just pass through their input data
|
||||
// The input data comes from the webhook execution context or initial workflow inputs
|
||||
// For trigger blocks, return the starter block's output which contains the workflow input
|
||||
// This ensures webhook data like message, sender, chat, etc. are accessible
|
||||
const starterBlock = context.workflow?.blocks?.find((b) => b.metadata?.id === 'starter')
|
||||
if (starterBlock) {
|
||||
const starterState = context.blockStates.get(starterBlock.id)
|
||||
if (starterState?.output && Object.keys(starterState.output).length > 0) {
|
||||
const starterOutput = starterState.output
|
||||
|
||||
// For trigger blocks, return the inputs directly - these contain the webhook/trigger data
|
||||
// Generic handling for webhook triggers - extract provider-specific data
|
||||
// Check if this is a webhook execution with nested structure
|
||||
if (starterOutput.webhook?.data) {
|
||||
const webhookData = starterOutput.webhook.data
|
||||
const provider = webhookData.provider
|
||||
|
||||
logger.debug(`Processing webhook trigger for block ${block.id}`, {
|
||||
provider,
|
||||
blockType: block.metadata?.id,
|
||||
})
|
||||
|
||||
// Extract the flattened properties that should be at root level
|
||||
const result: any = {
|
||||
// Always keep the input at root level
|
||||
input: starterOutput.input,
|
||||
}
|
||||
|
||||
// FIRST: Copy all existing top-level properties (like 'event', 'message', etc.)
|
||||
// This ensures that properties already flattened in webhook utils are preserved
|
||||
for (const [key, value] of Object.entries(starterOutput)) {
|
||||
if (key !== 'webhook' && key !== provider) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// SECOND: Generic extraction logic based on common webhook patterns
|
||||
// Pattern 1: Provider-specific nested object (telegram, microsoftteams, etc.)
|
||||
if (provider && starterOutput[provider]) {
|
||||
// Copy all properties from provider object to root level for direct access
|
||||
const providerData = starterOutput[provider]
|
||||
|
||||
for (const [key, value] of Object.entries(providerData)) {
|
||||
// Special handling for GitHub provider - copy all properties
|
||||
if (provider === 'github') {
|
||||
// For GitHub, copy all properties (objects and primitives) to root level
|
||||
if (!result[key]) {
|
||||
// Special handling for complex objects that might have enumeration issues
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
try {
|
||||
// Deep clone complex objects to avoid reference issues
|
||||
result[key] = JSON.parse(JSON.stringify(value))
|
||||
} catch (error) {
|
||||
// If JSON serialization fails, try direct assignment
|
||||
result[key] = value
|
||||
}
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other providers, keep existing logic (only copy objects)
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// Don't overwrite existing top-level properties
|
||||
if (!result[key]) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep nested structure for backwards compatibility
|
||||
result[provider] = providerData
|
||||
|
||||
// Special handling for GitHub complex objects that might not be copied by the main loop
|
||||
if (provider === 'github') {
|
||||
// Comprehensive GitHub object extraction from multiple possible sources
|
||||
const githubObjects = ['repository', 'sender', 'pusher', 'commits', 'head_commit']
|
||||
|
||||
for (const objName of githubObjects) {
|
||||
// ALWAYS try to get the object, even if something exists (fix for conflicts)
|
||||
let objectValue = null
|
||||
|
||||
// Source 1: Direct from provider data
|
||||
if (providerData[objName]) {
|
||||
objectValue = providerData[objName]
|
||||
}
|
||||
// Source 2: From webhook payload (raw GitHub webhook)
|
||||
else if (starterOutput.webhook?.data?.payload?.[objName]) {
|
||||
objectValue = starterOutput.webhook.data.payload[objName]
|
||||
}
|
||||
// Source 3: For commits, try parsing JSON string version if no object found
|
||||
else if (objName === 'commits' && typeof result.commits === 'string') {
|
||||
try {
|
||||
objectValue = JSON.parse(result.commits)
|
||||
} catch (e) {
|
||||
// Keep as string if parsing fails
|
||||
objectValue = result.commits
|
||||
}
|
||||
}
|
||||
|
||||
// FORCE the object to root level (removed the !result[objName] condition)
|
||||
if (objectValue !== null && objectValue !== undefined) {
|
||||
result[objName] = objectValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Provider data directly in webhook.data (based on actual structure)
|
||||
else if (provider && webhookData[provider]) {
|
||||
const providerData = webhookData[provider]
|
||||
|
||||
// Extract all provider properties to root level
|
||||
for (const [key, value] of Object.entries(providerData)) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// Don't overwrite existing top-level properties
|
||||
if (!result[key]) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep nested structure for backwards compatibility
|
||||
result[provider] = providerData
|
||||
}
|
||||
|
||||
// Pattern 3: Email providers with data in webhook.data.payload.email (Gmail, Outlook)
|
||||
else if (
|
||||
provider &&
|
||||
(provider === 'gmail' || provider === 'outlook') &&
|
||||
webhookData.payload?.email
|
||||
) {
|
||||
const emailData = webhookData.payload.email
|
||||
|
||||
// Flatten email fields to root level for direct access
|
||||
for (const [key, value] of Object.entries(emailData)) {
|
||||
if (!result[key]) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the email object for backwards compatibility
|
||||
result.email = emailData
|
||||
|
||||
// Also keep timestamp if present in payload
|
||||
if (webhookData.payload.timestamp) {
|
||||
result.timestamp = webhookData.payload.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
// Always keep webhook metadata
|
||||
if (starterOutput.webhook) result.webhook = starterOutput.webhook
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
logger.debug(`Returning starter block output for trigger block ${block.id}`, {
|
||||
starterOutputKeys: Object.keys(starterOutput),
|
||||
})
|
||||
return starterOutput
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to resolved inputs if no starter block output
|
||||
if (inputs && Object.keys(inputs).length > 0) {
|
||||
logger.debug(`Returning trigger inputs for block ${block.id}`, {
|
||||
inputKeys: Object.keys(inputs),
|
||||
|
||||
@@ -223,55 +223,81 @@ export function formatWebhookInput(
|
||||
input = 'Message received'
|
||||
}
|
||||
|
||||
// Create the message object for easier access
|
||||
const messageObj = {
|
||||
id: message.message_id,
|
||||
text: message.text,
|
||||
caption: message.caption,
|
||||
date: message.date,
|
||||
messageType: message.photo
|
||||
? 'photo'
|
||||
: message.document
|
||||
? 'document'
|
||||
: message.audio
|
||||
? 'audio'
|
||||
: message.video
|
||||
? 'video'
|
||||
: message.voice
|
||||
? 'voice'
|
||||
: message.sticker
|
||||
? 'sticker'
|
||||
: message.location
|
||||
? 'location'
|
||||
: message.contact
|
||||
? 'contact'
|
||||
: message.poll
|
||||
? 'poll'
|
||||
: 'text',
|
||||
raw: message,
|
||||
}
|
||||
|
||||
// Create sender object
|
||||
const senderObj = message.from
|
||||
? {
|
||||
id: message.from.id,
|
||||
firstName: message.from.first_name,
|
||||
lastName: message.from.last_name,
|
||||
username: message.from.username,
|
||||
languageCode: message.from.language_code,
|
||||
isBot: message.from.is_bot,
|
||||
}
|
||||
: null
|
||||
|
||||
// Create chat object
|
||||
const chatObj = message.chat
|
||||
? {
|
||||
id: message.chat.id,
|
||||
type: message.chat.type,
|
||||
title: message.chat.title,
|
||||
username: message.chat.username,
|
||||
firstName: message.chat.first_name,
|
||||
lastName: message.chat.last_name,
|
||||
}
|
||||
: null
|
||||
|
||||
return {
|
||||
input, // Primary workflow input - the message content
|
||||
|
||||
// NEW: Top-level properties for backward compatibility with <blockName.message> syntax
|
||||
message: messageObj,
|
||||
sender: senderObj,
|
||||
chat: chatObj,
|
||||
updateId: body.update_id,
|
||||
updateType: body.message
|
||||
? 'message'
|
||||
: body.edited_message
|
||||
? 'edited_message'
|
||||
: body.channel_post
|
||||
? 'channel_post'
|
||||
: body.edited_channel_post
|
||||
? 'edited_channel_post'
|
||||
: 'unknown',
|
||||
|
||||
// Keep the nested structure for the new telegram.message.text syntax
|
||||
telegram: {
|
||||
message: {
|
||||
id: message.message_id,
|
||||
text: message.text,
|
||||
caption: message.caption,
|
||||
date: message.date,
|
||||
messageType: message.photo
|
||||
? 'photo'
|
||||
: message.document
|
||||
? 'document'
|
||||
: message.audio
|
||||
? 'audio'
|
||||
: message.video
|
||||
? 'video'
|
||||
: message.voice
|
||||
? 'voice'
|
||||
: message.sticker
|
||||
? 'sticker'
|
||||
: message.location
|
||||
? 'location'
|
||||
: message.contact
|
||||
? 'contact'
|
||||
: message.poll
|
||||
? 'poll'
|
||||
: 'text',
|
||||
raw: message,
|
||||
},
|
||||
sender: message.from
|
||||
? {
|
||||
id: message.from.id,
|
||||
firstName: message.from.first_name,
|
||||
lastName: message.from.last_name,
|
||||
username: message.from.username,
|
||||
languageCode: message.from.language_code,
|
||||
isBot: message.from.is_bot,
|
||||
}
|
||||
: null,
|
||||
chat: message.chat
|
||||
? {
|
||||
id: message.chat.id,
|
||||
type: message.chat.type,
|
||||
title: message.chat.title,
|
||||
username: message.chat.username,
|
||||
firstName: message.chat.first_name,
|
||||
lastName: message.chat.last_name,
|
||||
}
|
||||
: null,
|
||||
message: messageObj,
|
||||
sender: senderObj,
|
||||
chat: chatObj,
|
||||
updateId: body.update_id,
|
||||
updateType: body.message
|
||||
? 'message'
|
||||
@@ -331,6 +357,13 @@ export function formatWebhookInput(
|
||||
return body
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'outlook') {
|
||||
if (body && typeof body === 'object' && 'email' in body) {
|
||||
return body // { email: {...}, timestamp: ... }
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'microsoftteams') {
|
||||
// Microsoft Teams outgoing webhook - Teams sending data to us
|
||||
const messageText = body?.text || ''
|
||||
@@ -341,6 +374,19 @@ export function formatWebhookInput(
|
||||
|
||||
return {
|
||||
input: messageText, // Primary workflow input - the message text
|
||||
|
||||
// Top-level properties for backward compatibility with <blockName.text> syntax
|
||||
type: body?.type || 'message',
|
||||
id: messageId,
|
||||
timestamp,
|
||||
localTimestamp: body?.localTimestamp || '',
|
||||
serviceUrl: body?.serviceUrl || '',
|
||||
channelId: body?.channelId || '',
|
||||
from_id: from.id || '',
|
||||
from_name: from.name || '',
|
||||
conversation_id: conversation.id || '',
|
||||
text: messageText,
|
||||
|
||||
microsoftteams: {
|
||||
message: {
|
||||
id: messageId,
|
||||
@@ -385,7 +431,210 @@ export function formatWebhookInput(
|
||||
}
|
||||
}
|
||||
|
||||
// Generic format for Slack and other providers
|
||||
if (foundWebhook.provider === 'slack') {
|
||||
// Slack input formatting logic - check for valid event
|
||||
const event = body?.event
|
||||
|
||||
if (event && body?.type === 'event_callback') {
|
||||
// Extract event text with fallbacks for different event types
|
||||
let input = ''
|
||||
|
||||
if (event.text) {
|
||||
input = event.text
|
||||
} else if (event.type === 'app_mention') {
|
||||
input = 'App mention received'
|
||||
} else {
|
||||
input = 'Slack event received'
|
||||
}
|
||||
|
||||
// Create the event object for easier access
|
||||
const eventObj = {
|
||||
event_type: event.type || '',
|
||||
channel: event.channel || '',
|
||||
channel_name: '', // Could be resolved via additional API calls if needed
|
||||
user: event.user || '',
|
||||
user_name: '', // Could be resolved via additional API calls if needed
|
||||
text: event.text || '',
|
||||
timestamp: event.ts || event.event_ts || '',
|
||||
team_id: body.team_id || event.team || '',
|
||||
event_id: body.event_id || '',
|
||||
}
|
||||
|
||||
return {
|
||||
input, // Primary workflow input - the event content
|
||||
|
||||
// // // Top-level properties for backward compatibility with <blockName.event> syntax
|
||||
event: eventObj,
|
||||
|
||||
// Keep the nested structure for the new slack.event.text syntax
|
||||
slack: {
|
||||
event: eventObj,
|
||||
},
|
||||
webhook: {
|
||||
data: {
|
||||
provider: 'slack',
|
||||
path: foundWebhook.path,
|
||||
providerConfig: foundWebhook.providerConfig,
|
||||
payload: body,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
method: request.method,
|
||||
},
|
||||
},
|
||||
workflowId: foundWorkflow.id,
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for unknown Slack event types
|
||||
logger.warn('Unknown Slack event type', {
|
||||
type: body?.type,
|
||||
hasEvent: !!body?.event,
|
||||
bodyKeys: Object.keys(body || {}),
|
||||
})
|
||||
|
||||
return {
|
||||
input: 'Slack webhook received',
|
||||
slack: {
|
||||
event: {
|
||||
event_type: body?.event?.type || body?.type || 'unknown',
|
||||
channel: body?.event?.channel || '',
|
||||
user: body?.event?.user || '',
|
||||
text: body?.event?.text || '',
|
||||
timestamp: body?.event?.ts || '',
|
||||
team_id: body?.team_id || '',
|
||||
event_id: body?.event_id || '',
|
||||
},
|
||||
},
|
||||
webhook: {
|
||||
data: {
|
||||
provider: 'slack',
|
||||
path: foundWebhook.path,
|
||||
providerConfig: foundWebhook.providerConfig,
|
||||
payload: body,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
method: request.method,
|
||||
},
|
||||
},
|
||||
workflowId: foundWorkflow.id,
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'github') {
|
||||
// GitHub webhook input formatting logic
|
||||
const eventType = request.headers.get('x-github-event') || 'unknown'
|
||||
const delivery = request.headers.get('x-github-delivery') || ''
|
||||
|
||||
// Extract common GitHub properties
|
||||
const repository = body?.repository || {}
|
||||
const sender = body?.sender || {}
|
||||
const action = body?.action || ''
|
||||
|
||||
// Build GitHub-specific variables based on the trigger config outputs
|
||||
const githubData = {
|
||||
// Event metadata
|
||||
event_type: eventType,
|
||||
action: action,
|
||||
delivery_id: delivery,
|
||||
|
||||
// Repository information (avoid 'repository' to prevent conflict with the object)
|
||||
repository_full_name: repository.full_name || '',
|
||||
repository_name: repository.name || '',
|
||||
repository_owner: repository.owner?.login || '',
|
||||
repository_id: repository.id || '',
|
||||
repository_url: repository.html_url || '',
|
||||
|
||||
// Sender information (avoid 'sender' to prevent conflict with the object)
|
||||
sender_login: sender.login || '',
|
||||
sender_id: sender.id || '',
|
||||
sender_type: sender.type || '',
|
||||
sender_url: sender.html_url || '',
|
||||
|
||||
// Event-specific data
|
||||
...(body?.ref && {
|
||||
ref: body.ref,
|
||||
branch: body.ref?.replace('refs/heads/', '') || '',
|
||||
}),
|
||||
...(body?.before && { before: body.before }),
|
||||
...(body?.after && { after: body.after }),
|
||||
...(body?.commits && {
|
||||
commits: JSON.stringify(body.commits),
|
||||
commit_count: body.commits.length || 0,
|
||||
}),
|
||||
...(body?.head_commit && {
|
||||
commit_message: body.head_commit.message || '',
|
||||
commit_author: body.head_commit.author?.name || '',
|
||||
commit_sha: body.head_commit.id || '',
|
||||
commit_url: body.head_commit.url || '',
|
||||
}),
|
||||
...(body?.pull_request && {
|
||||
pull_request: JSON.stringify(body.pull_request),
|
||||
pr_number: body.pull_request.number || '',
|
||||
pr_title: body.pull_request.title || '',
|
||||
pr_state: body.pull_request.state || '',
|
||||
pr_url: body.pull_request.html_url || '',
|
||||
}),
|
||||
...(body?.issue && {
|
||||
issue: JSON.stringify(body.issue),
|
||||
issue_number: body.issue.number || '',
|
||||
issue_title: body.issue.title || '',
|
||||
issue_state: body.issue.state || '',
|
||||
issue_url: body.issue.html_url || '',
|
||||
}),
|
||||
...(body?.comment && {
|
||||
comment: JSON.stringify(body.comment),
|
||||
comment_body: body.comment.body || '',
|
||||
comment_url: body.comment.html_url || '',
|
||||
}),
|
||||
}
|
||||
|
||||
// Set input based on event type for workflow processing
|
||||
let input = ''
|
||||
switch (eventType) {
|
||||
case 'push':
|
||||
input = `Push to ${githubData.branch || githubData.ref}: ${githubData.commit_message || 'No commit message'}`
|
||||
break
|
||||
case 'pull_request':
|
||||
input = `${action} pull request: ${githubData.pr_title || 'No title'}`
|
||||
break
|
||||
case 'issues':
|
||||
input = `${action} issue: ${githubData.issue_title || 'No title'}`
|
||||
break
|
||||
case 'issue_comment':
|
||||
case 'pull_request_review_comment':
|
||||
input = `Comment ${action}: ${githubData.comment_body?.slice(0, 100) || 'No comment body'}${(githubData.comment_body?.length || 0) > 100 ? '...' : ''}`
|
||||
break
|
||||
default:
|
||||
input = `GitHub ${eventType} event${action ? ` (${action})` : ''}`
|
||||
}
|
||||
|
||||
return {
|
||||
input, // Primary workflow input
|
||||
|
||||
// Top-level properties for backward compatibility
|
||||
...githubData,
|
||||
|
||||
// GitHub data structured for trigger handler to extract
|
||||
github: {
|
||||
// Processed convenience variables
|
||||
...githubData,
|
||||
// Raw GitHub webhook payload for direct field access
|
||||
...body,
|
||||
},
|
||||
|
||||
webhook: {
|
||||
data: {
|
||||
provider: 'github',
|
||||
path: foundWebhook.path,
|
||||
providerConfig: foundWebhook.providerConfig,
|
||||
payload: body,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
method: request.method,
|
||||
},
|
||||
},
|
||||
workflowId: foundWorkflow.id,
|
||||
}
|
||||
}
|
||||
|
||||
// Generic format for other providers
|
||||
return {
|
||||
webhook: {
|
||||
data: {
|
||||
|
||||
@@ -225,6 +225,15 @@ export class Serializer {
|
||||
// This catches missing API keys, credentials, and other user-provided values early
|
||||
// Fields that are user-or-llm will be validated later after parameter merging
|
||||
|
||||
// Skip validation if the block is in trigger mode
|
||||
if (block.triggerMode || blockConfig.category === 'triggers') {
|
||||
logger.info('Skipping validation for block in trigger mode', {
|
||||
blockId: block.id,
|
||||
blockType: block.type,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get the tool configuration to check parameter visibility
|
||||
const toolAccess = blockConfig.tools?.access
|
||||
if (!toolAccess || toolAccess.length === 0) {
|
||||
|
||||
@@ -6,10 +6,14 @@ export const airtableWebhookTrigger: TriggerConfig = {
|
||||
name: 'Airtable Webhook',
|
||||
provider: 'airtable',
|
||||
description:
|
||||
'Trigger workflow from Airtable record changes like create, update, and delete events',
|
||||
'Trigger workflow from Airtable record changes like create, update, and delete events (requires Airtable credentials)',
|
||||
version: '1.0.0',
|
||||
icon: AirtableIcon,
|
||||
|
||||
// Airtable requires OAuth credentials to create webhooks
|
||||
requiresCredentials: true,
|
||||
credentialProvider: 'airtable',
|
||||
|
||||
configFields: {
|
||||
baseId: {
|
||||
type: 'string',
|
||||
@@ -69,12 +73,12 @@ export const airtableWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
|
||||
instructions: [
|
||||
'Connect your Airtable account using the "Select Airtable credential" button above.',
|
||||
'Ensure you have provided the correct Base ID and Table ID above.',
|
||||
'Sim will automatically configure the webhook in your Airtable account when you save.',
|
||||
'Any changes made to records in the specified table will trigger this workflow.',
|
||||
"If 'Include Full Record Data' is enabled, the entire record will be sent; otherwise, only the changed fields are sent.",
|
||||
'You can find your Base ID in the Airtable URL or API documentation for your base.',
|
||||
'Table IDs can be found in the Airtable API documentation or by inspecting the table URL.',
|
||||
'You can find your Base ID in the Airtable URL: https://airtable.com/[baseId]/...',
|
||||
'You can find your Table ID by clicking on the table name and looking in the URL.',
|
||||
'The webhook will trigger whenever records are created, updated, or deleted in the specified table.',
|
||||
'Make sure your Airtable account has appropriate permissions for the specified base.',
|
||||
],
|
||||
|
||||
samplePayload: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { discordWebhookTrigger } from './webhook'
|
||||
@@ -1,95 +0,0 @@
|
||||
import { DiscordIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '../types'
|
||||
|
||||
export const discordWebhookTrigger: TriggerConfig = {
|
||||
id: 'discord_webhook',
|
||||
name: 'Discord Webhook',
|
||||
provider: 'discord',
|
||||
description: 'Trigger workflow from Discord webhook events and send messages to Discord channels',
|
||||
version: '1.0.0',
|
||||
icon: DiscordIcon,
|
||||
|
||||
configFields: {
|
||||
webhookName: {
|
||||
type: 'string',
|
||||
label: 'Webhook Name',
|
||||
placeholder: 'Sim Bot',
|
||||
description: 'This name will be displayed as the sender of messages in Discord.',
|
||||
required: false,
|
||||
},
|
||||
avatarUrl: {
|
||||
type: 'string',
|
||||
label: 'Avatar URL',
|
||||
placeholder: 'https://example.com/avatar.png',
|
||||
description: "URL to an image that will be used as the webhook's avatar.",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Message content from Discord webhook',
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
description: 'Username of the sender (if provided)',
|
||||
},
|
||||
avatar_url: {
|
||||
type: 'string',
|
||||
description: 'Avatar URL of the sender (if provided)',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Timestamp when the webhook was triggered',
|
||||
},
|
||||
webhook_id: {
|
||||
type: 'string',
|
||||
description: 'Discord webhook identifier',
|
||||
},
|
||||
webhook_token: {
|
||||
type: 'string',
|
||||
description: 'Discord webhook token',
|
||||
},
|
||||
guild_id: {
|
||||
type: 'string',
|
||||
description: 'Discord server/guild ID',
|
||||
},
|
||||
channel_id: {
|
||||
type: 'string',
|
||||
description: 'Discord channel ID where the event occurred',
|
||||
},
|
||||
embeds: {
|
||||
type: 'string',
|
||||
description: 'Embedded content data (if any)',
|
||||
},
|
||||
},
|
||||
|
||||
instructions: [
|
||||
'Go to Discord Server Settings > Integrations.',
|
||||
'Click "Webhooks" then "New Webhook".',
|
||||
'Customize the name and channel.',
|
||||
'Click "Copy Webhook URL".',
|
||||
'Paste the copied Discord URL into the main <strong>Webhook URL</strong> field above.',
|
||||
'Your workflow triggers when Discord sends an event to that URL.',
|
||||
],
|
||||
|
||||
samplePayload: {
|
||||
content: 'Hello from Sim!',
|
||||
username: 'Optional Custom Name',
|
||||
avatar_url: 'https://example.com/avatar.png',
|
||||
timestamp: new Date().toISOString(),
|
||||
webhook_id: '1234567890123456789',
|
||||
webhook_token: 'example-webhook-token',
|
||||
guild_id: '0987654321098765432',
|
||||
channel_id: '1122334455667788990',
|
||||
embeds: [],
|
||||
},
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -37,37 +37,10 @@ export const githubWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'The action that was performed (e.g., opened, closed, synchronize)',
|
||||
},
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'Type of GitHub event (e.g., push, pull_request, issues)',
|
||||
},
|
||||
repository: {
|
||||
type: 'string',
|
||||
description: 'Repository full name (owner/repo)',
|
||||
},
|
||||
repository_name: {
|
||||
type: 'string',
|
||||
description: 'Repository name only',
|
||||
},
|
||||
repository_owner: {
|
||||
type: 'string',
|
||||
description: 'Repository owner username or organization',
|
||||
},
|
||||
sender: {
|
||||
type: 'string',
|
||||
description: 'Username of the user who triggered the event',
|
||||
},
|
||||
sender_id: {
|
||||
type: 'string',
|
||||
description: 'User ID of the sender',
|
||||
},
|
||||
// GitHub webhook payload structure - maps 1:1 to actual GitHub webhook body
|
||||
ref: {
|
||||
type: 'string',
|
||||
description: 'Git reference (for push events)',
|
||||
description: 'Git reference (e.g., refs/heads/fix/telegram-wh)',
|
||||
},
|
||||
before: {
|
||||
type: 'string',
|
||||
@@ -77,34 +50,414 @@ export const githubWebhookTrigger: TriggerConfig = {
|
||||
type: 'string',
|
||||
description: 'SHA of the commit after the push',
|
||||
},
|
||||
created: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the push created the reference',
|
||||
},
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the push deleted the reference',
|
||||
},
|
||||
forced: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the push was forced',
|
||||
},
|
||||
base_ref: {
|
||||
type: 'string',
|
||||
description: 'Base reference for the push',
|
||||
},
|
||||
compare: {
|
||||
type: 'string',
|
||||
description: 'URL to compare the changes',
|
||||
},
|
||||
repository: {
|
||||
id: {
|
||||
type: 'number',
|
||||
description: 'Repository ID',
|
||||
},
|
||||
node_id: {
|
||||
type: 'string',
|
||||
description: 'Repository node ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Repository name',
|
||||
},
|
||||
full_name: {
|
||||
type: 'string',
|
||||
description: 'Repository full name (owner/repo)',
|
||||
},
|
||||
private: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the repository is private',
|
||||
},
|
||||
html_url: {
|
||||
type: 'string',
|
||||
description: 'Repository HTML URL',
|
||||
},
|
||||
fork: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the repository is a fork',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Repository API URL',
|
||||
},
|
||||
created_at: {
|
||||
type: 'number',
|
||||
description: 'Repository creation timestamp',
|
||||
},
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
description: 'Repository last updated time',
|
||||
},
|
||||
pushed_at: {
|
||||
type: 'number',
|
||||
description: 'Repository last push timestamp',
|
||||
},
|
||||
git_url: {
|
||||
type: 'string',
|
||||
description: 'Repository git URL',
|
||||
},
|
||||
ssh_url: {
|
||||
type: 'string',
|
||||
description: 'Repository SSH URL',
|
||||
},
|
||||
clone_url: {
|
||||
type: 'string',
|
||||
description: 'Repository clone URL',
|
||||
},
|
||||
homepage: {
|
||||
type: 'string',
|
||||
description: 'Repository homepage URL',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
description: 'Repository size',
|
||||
},
|
||||
stargazers_count: {
|
||||
type: 'number',
|
||||
description: 'Number of stars',
|
||||
},
|
||||
watchers_count: {
|
||||
type: 'number',
|
||||
description: 'Number of watchers',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
description: 'Primary programming language',
|
||||
},
|
||||
forks_count: {
|
||||
type: 'number',
|
||||
description: 'Number of forks',
|
||||
},
|
||||
archived: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the repository is archived',
|
||||
},
|
||||
disabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the repository is disabled',
|
||||
},
|
||||
open_issues_count: {
|
||||
type: 'number',
|
||||
description: 'Number of open issues',
|
||||
},
|
||||
topics: {
|
||||
type: 'array',
|
||||
description: 'Repository topics',
|
||||
},
|
||||
visibility: {
|
||||
type: 'string',
|
||||
description: 'Repository visibility (public, private)',
|
||||
},
|
||||
forks: {
|
||||
type: 'number',
|
||||
description: 'Number of forks',
|
||||
},
|
||||
open_issues: {
|
||||
type: 'number',
|
||||
description: 'Number of open issues',
|
||||
},
|
||||
watchers: {
|
||||
type: 'number',
|
||||
description: 'Number of watchers',
|
||||
},
|
||||
default_branch: {
|
||||
type: 'string',
|
||||
description: 'Default branch name',
|
||||
},
|
||||
stargazers: {
|
||||
type: 'number',
|
||||
description: 'Number of stargazers',
|
||||
},
|
||||
master_branch: {
|
||||
type: 'string',
|
||||
description: 'Master branch name',
|
||||
},
|
||||
owner: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Owner name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Owner email',
|
||||
},
|
||||
login: {
|
||||
type: 'string',
|
||||
description: 'Owner username',
|
||||
},
|
||||
id: {
|
||||
type: 'number',
|
||||
description: 'Owner ID',
|
||||
},
|
||||
node_id: {
|
||||
type: 'string',
|
||||
description: 'Owner node ID',
|
||||
},
|
||||
avatar_url: {
|
||||
type: 'string',
|
||||
description: 'Owner avatar URL',
|
||||
},
|
||||
gravatar_id: {
|
||||
type: 'string',
|
||||
description: 'Owner gravatar ID',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Owner API URL',
|
||||
},
|
||||
html_url: {
|
||||
type: 'string',
|
||||
description: 'Owner profile URL',
|
||||
},
|
||||
user_view_type: {
|
||||
type: 'string',
|
||||
description: 'User view type',
|
||||
},
|
||||
site_admin: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the owner is a site admin',
|
||||
},
|
||||
},
|
||||
license: {
|
||||
type: 'object',
|
||||
description: 'Repository license information',
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'License key (e.g., apache-2.0)',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'License name',
|
||||
},
|
||||
spdx_id: {
|
||||
type: 'string',
|
||||
description: 'SPDX license identifier',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'License URL',
|
||||
},
|
||||
node_id: {
|
||||
type: 'string',
|
||||
description: 'License node ID',
|
||||
},
|
||||
},
|
||||
},
|
||||
pusher: {
|
||||
type: 'object',
|
||||
description: 'Information about who pushed the changes',
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Pusher name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Pusher email',
|
||||
},
|
||||
},
|
||||
sender: {
|
||||
login: {
|
||||
type: 'string',
|
||||
description: 'Sender username',
|
||||
},
|
||||
id: {
|
||||
type: 'number',
|
||||
description: 'Sender ID',
|
||||
},
|
||||
node_id: {
|
||||
type: 'string',
|
||||
description: 'Sender node ID',
|
||||
},
|
||||
avatar_url: {
|
||||
type: 'string',
|
||||
description: 'Sender avatar URL',
|
||||
},
|
||||
gravatar_id: {
|
||||
type: 'string',
|
||||
description: 'Sender gravatar ID',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Sender API URL',
|
||||
},
|
||||
html_url: {
|
||||
type: 'string',
|
||||
description: 'Sender profile URL',
|
||||
},
|
||||
user_view_type: {
|
||||
type: 'string',
|
||||
description: 'User view type',
|
||||
},
|
||||
site_admin: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the sender is a site admin',
|
||||
},
|
||||
},
|
||||
commits: {
|
||||
type: 'string',
|
||||
description: 'Array of commit objects (for push events)',
|
||||
type: 'array',
|
||||
description: 'Array of commit objects',
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Commit SHA',
|
||||
},
|
||||
tree_id: {
|
||||
type: 'string',
|
||||
description: 'Tree SHA',
|
||||
},
|
||||
distinct: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the commit is distinct',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Commit message',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Commit timestamp',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Commit URL',
|
||||
},
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'Commit author',
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Author name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Author email',
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'Commit committer',
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Committer name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Committer email',
|
||||
},
|
||||
},
|
||||
added: {
|
||||
type: 'array',
|
||||
description: 'Array of added files',
|
||||
},
|
||||
removed: {
|
||||
type: 'array',
|
||||
description: 'Array of removed files',
|
||||
},
|
||||
modified: {
|
||||
type: 'array',
|
||||
description: 'Array of modified files',
|
||||
},
|
||||
},
|
||||
pull_request: {
|
||||
type: 'string',
|
||||
description: 'Pull request object (for pull_request events)',
|
||||
head_commit: {
|
||||
type: 'object',
|
||||
description: 'Head commit object',
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Commit SHA',
|
||||
},
|
||||
tree_id: {
|
||||
type: 'string',
|
||||
description: 'Tree SHA',
|
||||
},
|
||||
distinct: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the commit is distinct',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Commit message',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Commit timestamp',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Commit URL',
|
||||
},
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'Commit author',
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Author name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Author email',
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'Commit committer',
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Committer name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Committer email',
|
||||
},
|
||||
},
|
||||
added: {
|
||||
type: 'array',
|
||||
description: 'Array of added files',
|
||||
},
|
||||
removed: {
|
||||
type: 'array',
|
||||
description: 'Array of removed files',
|
||||
},
|
||||
modified: {
|
||||
type: 'array',
|
||||
description: 'Array of modified files',
|
||||
},
|
||||
},
|
||||
issue: {
|
||||
|
||||
// Convenient flat fields for easy access
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'Issue object (for issues events)',
|
||||
description: 'Type of GitHub event (e.g., push, pull_request, issues)',
|
||||
},
|
||||
comment: {
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Comment object (for comment events)',
|
||||
description: 'The action that was performed (e.g., opened, closed, synchronize)',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
description: 'Branch name extracted from ref',
|
||||
},
|
||||
commit_message: {
|
||||
type: 'string',
|
||||
description: 'Latest commit message',
|
||||
},
|
||||
commit_author: {
|
||||
type: 'string',
|
||||
description: 'Author of the latest commit',
|
||||
},
|
||||
},
|
||||
|
||||
instructions: [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Import trigger definitions
|
||||
|
||||
import { airtableWebhookTrigger } from './airtable'
|
||||
import { discordWebhookTrigger } from './discord'
|
||||
import { genericWebhookTrigger } from './generic'
|
||||
import { githubWebhookTrigger } from './github'
|
||||
import { gmailPollingTrigger } from './gmail'
|
||||
@@ -17,7 +16,6 @@ import { whatsappWebhookTrigger } from './whatsapp'
|
||||
export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
slack_webhook: slackWebhookTrigger,
|
||||
airtable_webhook: airtableWebhookTrigger,
|
||||
discord_webhook: discordWebhookTrigger,
|
||||
generic_webhook: genericWebhookTrigger,
|
||||
github_webhook: githubWebhookTrigger,
|
||||
gmail_poller: gmailPollingTrigger,
|
||||
|
||||
@@ -21,41 +21,43 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'Type of Slack event (e.g., app_mention, message)',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Slack channel ID where the event occurred',
|
||||
},
|
||||
channel_name: {
|
||||
type: 'string',
|
||||
description: 'Human-readable channel name',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
description: 'User ID who triggered the event',
|
||||
},
|
||||
user_name: {
|
||||
type: 'string',
|
||||
description: 'Username who triggered the event',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text content',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Event timestamp',
|
||||
},
|
||||
team_id: {
|
||||
type: 'string',
|
||||
description: 'Slack workspace/team ID',
|
||||
},
|
||||
event_id: {
|
||||
type: 'string',
|
||||
description: 'Unique event identifier',
|
||||
event: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'Type of Slack event (e.g., app_mention, message)',
|
||||
},
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Slack channel ID where the event occurred',
|
||||
},
|
||||
channel_name: {
|
||||
type: 'string',
|
||||
description: 'Human-readable channel name',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
description: 'User ID who triggered the event',
|
||||
},
|
||||
user_name: {
|
||||
type: 'string',
|
||||
description: 'Username who triggered the event',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text content',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'Event timestamp',
|
||||
},
|
||||
team_id: {
|
||||
type: 'string',
|
||||
description: 'Slack workspace/team ID',
|
||||
},
|
||||
event_id: {
|
||||
type: 'string',
|
||||
description: 'Unique event identifier',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -21,53 +21,55 @@ export const telegramWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
update_id: {
|
||||
type: 'number',
|
||||
description: 'Unique identifier for the update',
|
||||
},
|
||||
message_id: {
|
||||
type: 'number',
|
||||
description: 'Unique message identifier',
|
||||
},
|
||||
from_id: {
|
||||
type: 'number',
|
||||
description: 'User ID who sent the message',
|
||||
},
|
||||
from_username: {
|
||||
type: 'string',
|
||||
description: 'Username of the sender',
|
||||
},
|
||||
from_first_name: {
|
||||
type: 'string',
|
||||
description: 'First name of the sender',
|
||||
},
|
||||
from_last_name: {
|
||||
type: 'string',
|
||||
description: 'Last name of the sender',
|
||||
},
|
||||
chat_id: {
|
||||
type: 'number',
|
||||
description: 'Unique identifier for the chat',
|
||||
},
|
||||
chat_type: {
|
||||
type: 'string',
|
||||
description: 'Type of chat (private, group, supergroup, channel)',
|
||||
},
|
||||
chat_title: {
|
||||
type: 'string',
|
||||
description: 'Title of the chat (for groups and channels)',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text content',
|
||||
},
|
||||
date: {
|
||||
type: 'number',
|
||||
description: 'Date the message was sent (Unix timestamp)',
|
||||
},
|
||||
entities: {
|
||||
type: 'string',
|
||||
description: 'Special entities in the message (mentions, hashtags, etc.) as JSON string',
|
||||
message: {
|
||||
update_id: {
|
||||
type: 'number',
|
||||
description: 'Unique identifier for the update',
|
||||
},
|
||||
message_id: {
|
||||
type: 'number',
|
||||
description: 'Unique message identifier',
|
||||
},
|
||||
from_id: {
|
||||
type: 'number',
|
||||
description: 'User ID who sent the message',
|
||||
},
|
||||
from_username: {
|
||||
type: 'string',
|
||||
description: 'Username of the sender',
|
||||
},
|
||||
from_first_name: {
|
||||
type: 'string',
|
||||
description: 'First name of the sender',
|
||||
},
|
||||
from_last_name: {
|
||||
type: 'string',
|
||||
description: 'Last name of the sender',
|
||||
},
|
||||
chat_id: {
|
||||
type: 'number',
|
||||
description: 'Unique identifier for the chat',
|
||||
},
|
||||
chat_type: {
|
||||
type: 'string',
|
||||
description: 'Type of chat (private, group, supergroup, channel)',
|
||||
},
|
||||
chat_title: {
|
||||
type: 'string',
|
||||
description: 'Title of the chat (for groups and channels)',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text content',
|
||||
},
|
||||
date: {
|
||||
type: 'number',
|
||||
description: 'Date the message was sent (Unix timestamp)',
|
||||
},
|
||||
entities: {
|
||||
type: 'string',
|
||||
description: 'Special entities in the message (mentions, hashtags, etc.) as JSON string',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export const whatsappWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
|
||||
instructions: [
|
||||
'Go to your <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer" class="text-primary underline transition-colors hover:text-primary/80">Meta for Developers Apps</a> page.',
|
||||
'Go to your <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer" class="text-primary underline transition-colors hover:text-primary/80">Meta for Developers Apps</a> page and navigate to the "Build with us" --> "App Events" section.',
|
||||
'If you don\'t have an app:<br><ul class="mt-1 ml-5 list-disc"><li>Create an app from scratch</li><li>Give it a name and select your workspace</li></ul>',
|
||||
'Select your App, then navigate to WhatsApp > Configuration.',
|
||||
'Find the Webhooks section and click "Edit".',
|
||||
|
||||
Reference in New Issue
Block a user