2
0

(webhook) Add client execution option

This commit is contained in:
Baptiste Arnaud
2023-05-26 09:20:22 +02:00
parent 084a17ffc8
commit 75f9da0a4f
23 changed files with 426 additions and 306 deletions

View File

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