⚡ (webhook) Add client execution option
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
||||
ZapierBlock,
|
||||
MakeComBlock,
|
||||
PabblyConnectBlock,
|
||||
VariableWithUnknowValue,
|
||||
SessionState,
|
||||
Webhook,
|
||||
Typebot,
|
||||
@@ -17,17 +16,23 @@ import {
|
||||
KeyValue,
|
||||
ReplyLog,
|
||||
ResultInSession,
|
||||
ExecutableWebhook,
|
||||
} from '@typebot.io/schemas'
|
||||
import { stringify } from 'qs'
|
||||
import { byId, omit } from '@typebot.io/lib'
|
||||
import { omit } from '@typebot.io/lib'
|
||||
import { parseAnswers } from '@typebot.io/lib/results'
|
||||
import got, { Method, Headers, HTTPError } from 'got'
|
||||
import got, { Method, HTTPError, OptionsInit } from 'got'
|
||||
import { parseSampleResult } from './parseSampleResult'
|
||||
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
||||
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
||||
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
||||
import { updateVariables } from '@/features/variables/updateVariables'
|
||||
import { parseVariables } from '@/features/variables/parseVariables'
|
||||
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
||||
|
||||
type ParsedWebhook = ExecutableWebhook & {
|
||||
basicAuth: { username?: string; password?: string }
|
||||
isJson: boolean
|
||||
}
|
||||
|
||||
export const executeWebhookBlock = async (
|
||||
state: SessionState,
|
||||
@@ -51,70 +56,34 @@ export const executeWebhookBlock = async (
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
|
||||
}
|
||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||
const webhookResponse = await executeWebhook({ typebot })(
|
||||
preparedWebhook,
|
||||
typebot.variables,
|
||||
const parsedWebhook = await parseWebhookAttributes(
|
||||
typebot,
|
||||
block.groupId,
|
||||
result
|
||||
)
|
||||
const status = webhookResponse.statusCode.toString()
|
||||
const isError = status.startsWith('4') || status.startsWith('5')
|
||||
|
||||
if (isError) {
|
||||
)(preparedWebhook)
|
||||
if (!parsedWebhook) {
|
||||
log = {
|
||||
status: 'error',
|
||||
description: `Webhook returned error: ${webhookResponse.data}`,
|
||||
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
|
||||
description: `Couldn't parse webhook attributes`,
|
||||
}
|
||||
result &&
|
||||
(await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
details: log.details,
|
||||
}))
|
||||
} else {
|
||||
log = {
|
||||
status: 'success',
|
||||
description: `Webhook executed successfully!`,
|
||||
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
|
||||
}
|
||||
result &&
|
||||
(await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: log.description,
|
||||
details: JSON.stringify(webhookResponse.data, null, 2).substring(
|
||||
0,
|
||||
1000
|
||||
),
|
||||
}))
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
|
||||
}
|
||||
|
||||
const newVariables = block.options.responseVariableMapping.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, varMapping) => {
|
||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||
const existingVariable = typebot.variables.find(byId(varMapping.variableId))
|
||||
if (!existingVariable) return newVariables
|
||||
const func = Function(
|
||||
'data',
|
||||
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`
|
||||
)
|
||||
try {
|
||||
const value: unknown = func(webhookResponse)
|
||||
return [...newVariables, { ...existingVariable, value }]
|
||||
} catch (err) {
|
||||
return newVariables
|
||||
}
|
||||
}, [])
|
||||
if (newVariables.length > 0) {
|
||||
const newSessionState = await updateVariables(state)(newVariables)
|
||||
if (block.options.isExecutedOnClient)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
newSessionState,
|
||||
clientSideActions: [
|
||||
{
|
||||
webhookToExecute: parsedWebhook,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs: log ? [log] : undefined }
|
||||
const webhookResponse = await executeWebhook(parsedWebhook, result)
|
||||
return resumeWebhookExecution(state, block)(webhookResponse)
|
||||
}
|
||||
|
||||
const prepareWebhookAttributes = (
|
||||
@@ -131,19 +100,15 @@ const prepareWebhookAttributes = (
|
||||
|
||||
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
||||
|
||||
export const executeWebhook =
|
||||
({ typebot }: Pick<SessionState, 'typebot'>) =>
|
||||
async (
|
||||
webhook: Webhook,
|
||||
variables: Variable[],
|
||||
const parseWebhookAttributes =
|
||||
(
|
||||
typebot: SessionState['typebot'],
|
||||
groupId: string,
|
||||
result: ResultInSession
|
||||
): Promise<WebhookResponse> => {
|
||||
if (!webhook.url || !webhook.method)
|
||||
return {
|
||||
statusCode: 400,
|
||||
data: { message: `Webhook doesn't have url or method` },
|
||||
}
|
||||
) =>
|
||||
async (webhook: Webhook): Promise<ParsedWebhook | undefined> => {
|
||||
if (!webhook.url || !webhook.method) return
|
||||
const { variables } = typebot
|
||||
const basicAuth: { username?: string; password?: string } = {}
|
||||
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
||||
(h) =>
|
||||
@@ -161,13 +126,11 @@ export const executeWebhook =
|
||||
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
||||
}
|
||||
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
|
||||
| Headers
|
||||
| ExecutableWebhook['headers']
|
||||
| undefined
|
||||
const queryParams = stringify(
|
||||
convertKeyValueTableToObject(webhook.queryParams, variables)
|
||||
)
|
||||
const contentType = headers ? headers['Content-Type'] : undefined
|
||||
|
||||
const bodyContent = await getBodyContent(
|
||||
typebot,
|
||||
[]
|
||||
@@ -186,62 +149,62 @@ export const executeWebhook =
|
||||
)
|
||||
: { data: undefined, isJson: false }
|
||||
|
||||
const request = {
|
||||
return {
|
||||
url: parseVariables(variables)(
|
||||
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
||||
),
|
||||
method: webhook.method as Method,
|
||||
basicAuth,
|
||||
method: webhook.method,
|
||||
headers,
|
||||
...basicAuth,
|
||||
json:
|
||||
!contentType?.includes('x-www-form-urlencoded') && body && isJson
|
||||
? body
|
||||
: undefined,
|
||||
form:
|
||||
contentType?.includes('x-www-form-urlencoded') && body
|
||||
? body
|
||||
: undefined,
|
||||
body: body && !isJson ? body : undefined,
|
||||
body,
|
||||
isJson,
|
||||
}
|
||||
try {
|
||||
const response = await got(request.url, omit(request, 'url'))
|
||||
await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook successfuly executed.',
|
||||
details: {
|
||||
statusCode: response.statusCode,
|
||||
request,
|
||||
response: safeJsonParse(response.body).data,
|
||||
},
|
||||
})
|
||||
return {
|
||||
}
|
||||
|
||||
export const executeWebhook = async (
|
||||
webhook: ParsedWebhook,
|
||||
result: ResultInSession
|
||||
): Promise<WebhookResponse> => {
|
||||
const { headers, url, method, basicAuth, body, isJson } = webhook
|
||||
const contentType = headers ? headers['Content-Type'] : undefined
|
||||
|
||||
const request = {
|
||||
url,
|
||||
method: method as Method,
|
||||
headers,
|
||||
...(basicAuth ?? {}),
|
||||
json:
|
||||
!contentType?.includes('x-www-form-urlencoded') && body && isJson
|
||||
? body
|
||||
: undefined,
|
||||
form:
|
||||
contentType?.includes('x-www-form-urlencoded') && body ? body : undefined,
|
||||
body: body && !isJson ? (body as string) : undefined,
|
||||
} satisfies OptionsInit
|
||||
try {
|
||||
const response = await got(request.url, omit(request, 'url'))
|
||||
await saveSuccessLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook successfuly executed.',
|
||||
details: {
|
||||
statusCode: response.statusCode,
|
||||
data: safeJsonParse(response.body).data,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
const response = {
|
||||
statusCode: error.response.statusCode,
|
||||
data: safeJsonParse(error.response.body as string).data,
|
||||
}
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook returned an error',
|
||||
details: {
|
||||
request,
|
||||
response,
|
||||
},
|
||||
})
|
||||
return response
|
||||
}
|
||||
request,
|
||||
response: safeJsonParse(response.body).data,
|
||||
},
|
||||
})
|
||||
return {
|
||||
statusCode: response.statusCode,
|
||||
data: safeJsonParse(response.body).data,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
const response = {
|
||||
statusCode: 500,
|
||||
data: { message: `Error from Typebot server: ${error}` },
|
||||
statusCode: error.response.statusCode,
|
||||
data: safeJsonParse(error.response.body as string).data,
|
||||
}
|
||||
console.error(error)
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook failed to execute',
|
||||
message: 'Webhook returned an error',
|
||||
details: {
|
||||
request,
|
||||
response,
|
||||
@@ -249,7 +212,22 @@ export const executeWebhook =
|
||||
})
|
||||
return response
|
||||
}
|
||||
const response = {
|
||||
statusCode: 500,
|
||||
data: { message: `Error from Typebot server: ${error}` },
|
||||
}
|
||||
console.error(error)
|
||||
await saveErrorLog({
|
||||
resultId: result.id,
|
||||
message: 'Webhook failed to execute',
|
||||
details: {
|
||||
request,
|
||||
response,
|
||||
},
|
||||
})
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
const getBodyContent =
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user