2022-11-29 10:02:40 +01:00
|
|
|
import prisma from '@/lib/prisma'
|
|
|
|
|
import {
|
|
|
|
|
WebhookBlock,
|
|
|
|
|
ZapierBlock,
|
|
|
|
|
MakeComBlock,
|
|
|
|
|
PabblyConnectBlock,
|
|
|
|
|
VariableWithUnknowValue,
|
|
|
|
|
SessionState,
|
|
|
|
|
Webhook,
|
|
|
|
|
Typebot,
|
|
|
|
|
Variable,
|
|
|
|
|
WebhookResponse,
|
|
|
|
|
WebhookOptions,
|
|
|
|
|
defaultWebhookAttributes,
|
|
|
|
|
HttpMethod,
|
|
|
|
|
PublicTypebot,
|
|
|
|
|
KeyValue,
|
2023-01-25 11:27:47 +01:00
|
|
|
ReplyLog,
|
2023-03-07 14:41:57 +01:00
|
|
|
ResultInSession,
|
2023-03-15 08:35:16 +01:00
|
|
|
} from '@typebot.io/schemas'
|
2022-11-29 10:02:40 +01:00
|
|
|
import { stringify } from 'qs'
|
2023-03-15 08:35:16 +01:00
|
|
|
import { byId, omit } from '@typebot.io/lib'
|
|
|
|
|
import { parseAnswers } from '@typebot.io/lib/results'
|
2022-11-29 10:02:40 +01:00
|
|
|
import got, { Method, Headers, HTTPError } from 'got'
|
|
|
|
|
import { parseSampleResult } from './parseSampleResult'
|
2023-03-15 12:21:52 +01:00
|
|
|
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 'bot-engine'
|
2022-11-29 10:02:40 +01:00
|
|
|
|
|
|
|
|
export const executeWebhookBlock = async (
|
|
|
|
|
state: SessionState,
|
|
|
|
|
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
|
|
|
|
|
): Promise<ExecuteIntegrationResponse> => {
|
|
|
|
|
const { typebot, result } = state
|
2023-01-25 11:27:47 +01:00
|
|
|
let log: ReplyLog | undefined
|
2022-11-29 10:02:40 +01:00
|
|
|
const webhook = (await prisma.webhook.findUnique({
|
|
|
|
|
where: { id: block.webhookId },
|
|
|
|
|
})) as Webhook | null
|
|
|
|
|
if (!webhook) {
|
2023-01-25 16:15:03 +01:00
|
|
|
log = {
|
|
|
|
|
status: 'error',
|
|
|
|
|
description: `Couldn't find webhook with id ${block.webhookId}`,
|
|
|
|
|
}
|
2022-12-22 17:02:34 +01:00
|
|
|
result &&
|
|
|
|
|
(await saveErrorLog({
|
|
|
|
|
resultId: result.id,
|
2023-01-25 16:15:03 +01:00
|
|
|
message: log.description,
|
2022-12-22 17:02:34 +01:00
|
|
|
}))
|
2023-01-25 16:15:03 +01:00
|
|
|
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
|
2022-11-29 10:02:40 +01:00
|
|
|
}
|
|
|
|
|
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
2022-12-22 11:49:46 +01:00
|
|
|
const webhookResponse = await executeWebhook({ typebot })(
|
2022-11-29 10:02:40 +01:00
|
|
|
preparedWebhook,
|
|
|
|
|
typebot.variables,
|
|
|
|
|
block.groupId,
|
2023-03-07 14:41:57 +01:00
|
|
|
result
|
2022-11-29 10:02:40 +01:00
|
|
|
)
|
|
|
|
|
const status = webhookResponse.statusCode.toString()
|
|
|
|
|
const isError = status.startsWith('4') || status.startsWith('5')
|
|
|
|
|
|
|
|
|
|
if (isError) {
|
2023-01-25 11:27:47 +01:00
|
|
|
log = {
|
|
|
|
|
status: 'error',
|
|
|
|
|
description: `Webhook returned error: ${webhookResponse.data}`,
|
|
|
|
|
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
|
|
|
|
|
}
|
2022-12-22 17:02:34 +01:00
|
|
|
result &&
|
|
|
|
|
(await saveErrorLog({
|
|
|
|
|
resultId: result.id,
|
2023-01-25 11:27:47 +01:00
|
|
|
message: log.description,
|
|
|
|
|
details: log.details,
|
2022-12-22 17:02:34 +01:00
|
|
|
}))
|
2022-11-29 10:02:40 +01:00
|
|
|
} else {
|
2023-01-25 11:27:47 +01:00
|
|
|
log = {
|
|
|
|
|
status: 'success',
|
|
|
|
|
description: `Webhook executed successfully!`,
|
|
|
|
|
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
|
|
|
|
|
}
|
2022-12-22 17:02:34 +01:00
|
|
|
result &&
|
|
|
|
|
(await saveSuccessLog({
|
|
|
|
|
resultId: result.id,
|
2023-01-25 11:27:47 +01:00
|
|
|
message: log.description,
|
2022-12-22 17:02:34 +01:00
|
|
|
details: JSON.stringify(webhookResponse.data, null, 2).substring(
|
|
|
|
|
0,
|
|
|
|
|
1000
|
|
|
|
|
),
|
|
|
|
|
}))
|
2022-11-29 10:02:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
return {
|
|
|
|
|
outgoingEdgeId: block.outgoingEdgeId,
|
|
|
|
|
newSessionState,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-25 11:27:47 +01:00
|
|
|
return { outgoingEdgeId: block.outgoingEdgeId, logs: log ? [log] : undefined }
|
2022-11-29 10:02:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prepareWebhookAttributes = (
|
|
|
|
|
webhook: Webhook,
|
|
|
|
|
options: WebhookOptions
|
|
|
|
|
): Webhook => {
|
|
|
|
|
if (options.isAdvancedConfig === false) {
|
|
|
|
|
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
|
|
|
|
|
} else if (options.isCustomBody === false) {
|
|
|
|
|
return { ...webhook, body: '{{state}}' }
|
|
|
|
|
}
|
|
|
|
|
return webhook
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
|
|
|
|
|
|
|
|
|
export const executeWebhook =
|
2022-12-22 11:49:46 +01:00
|
|
|
({ typebot }: Pick<SessionState, 'typebot'>) =>
|
2022-11-29 10:02:40 +01:00
|
|
|
async (
|
|
|
|
|
webhook: Webhook,
|
|
|
|
|
variables: Variable[],
|
|
|
|
|
groupId: string,
|
2023-03-07 14:41:57 +01:00
|
|
|
result: ResultInSession
|
2022-11-29 10:02:40 +01:00
|
|
|
): Promise<WebhookResponse> => {
|
|
|
|
|
if (!webhook.url || !webhook.method)
|
|
|
|
|
return {
|
|
|
|
|
statusCode: 400,
|
|
|
|
|
data: { message: `Webhook doesn't have url or method` },
|
|
|
|
|
}
|
|
|
|
|
const basicAuth: { username?: string; password?: string } = {}
|
|
|
|
|
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
|
|
|
|
(h) =>
|
|
|
|
|
h.key?.toLowerCase() === 'authorization' &&
|
|
|
|
|
h.value?.toLowerCase()?.includes('basic')
|
|
|
|
|
)
|
|
|
|
|
const isUsernamePasswordBasicAuth =
|
|
|
|
|
basicAuthHeaderIdx !== -1 &&
|
|
|
|
|
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
|
|
|
|
|
if (isUsernamePasswordBasicAuth) {
|
|
|
|
|
const [username, password] =
|
|
|
|
|
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
|
|
|
|
|
basicAuth.username = username
|
|
|
|
|
basicAuth.password = password
|
|
|
|
|
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
|
|
|
|
}
|
|
|
|
|
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
|
|
|
|
|
| Headers
|
|
|
|
|
| undefined
|
|
|
|
|
const queryParams = stringify(
|
|
|
|
|
convertKeyValueTableToObject(webhook.queryParams, variables)
|
|
|
|
|
)
|
|
|
|
|
const contentType = headers ? headers['Content-Type'] : undefined
|
|
|
|
|
|
|
|
|
|
const bodyContent = await getBodyContent(
|
|
|
|
|
typebot,
|
2022-12-22 11:49:46 +01:00
|
|
|
[]
|
2022-11-29 10:02:40 +01:00
|
|
|
)({
|
|
|
|
|
body: webhook.body,
|
2023-03-07 14:41:57 +01:00
|
|
|
result,
|
2022-11-29 10:02:40 +01:00
|
|
|
groupId,
|
2023-02-13 14:49:00 +01:00
|
|
|
variables,
|
2022-11-29 10:02:40 +01:00
|
|
|
})
|
|
|
|
|
const { data: body, isJson } =
|
|
|
|
|
bodyContent && webhook.method !== HttpMethod.GET
|
|
|
|
|
? safeJsonParse(
|
|
|
|
|
parseVariables(variables, {
|
|
|
|
|
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
|
|
|
|
|
})(bodyContent)
|
|
|
|
|
)
|
|
|
|
|
: { data: undefined, isJson: false }
|
|
|
|
|
|
|
|
|
|
const request = {
|
|
|
|
|
url: parseVariables(variables)(
|
|
|
|
|
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
|
|
|
|
),
|
|
|
|
|
method: webhook.method as Method,
|
|
|
|
|
headers,
|
|
|
|
|
...basicAuth,
|
|
|
|
|
json:
|
2023-03-13 16:46:53 +01:00
|
|
|
!contentType?.includes('x-www-form-urlencoded') && body && isJson
|
|
|
|
|
? body
|
|
|
|
|
: undefined,
|
|
|
|
|
form:
|
|
|
|
|
contentType?.includes('x-www-form-urlencoded') && body
|
2022-11-29 10:02:40 +01:00
|
|
|
? body
|
|
|
|
|
: undefined,
|
|
|
|
|
body: body && !isJson ? body : undefined,
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const response = await got(request.url, omit(request, 'url'))
|
|
|
|
|
await saveSuccessLog({
|
2023-03-07 14:41:57 +01:00
|
|
|
resultId: result.id,
|
2022-11-29 10:02:40 +01:00
|
|
|
message: 'Webhook successfuly executed.',
|
|
|
|
|
details: {
|
|
|
|
|
statusCode: response.statusCode,
|
|
|
|
|
request,
|
|
|
|
|
response: safeJsonParse(response.body).data,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
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({
|
2023-03-07 14:41:57 +01:00
|
|
|
resultId: result.id,
|
2022-11-29 10:02:40 +01:00
|
|
|
message: 'Webhook returned an error',
|
|
|
|
|
details: {
|
|
|
|
|
request,
|
|
|
|
|
response,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return response
|
|
|
|
|
}
|
|
|
|
|
const response = {
|
|
|
|
|
statusCode: 500,
|
|
|
|
|
data: { message: `Error from Typebot server: ${error}` },
|
|
|
|
|
}
|
|
|
|
|
console.error(error)
|
|
|
|
|
await saveErrorLog({
|
2023-03-07 14:41:57 +01:00
|
|
|
resultId: result.id,
|
2022-11-29 10:02:40 +01:00
|
|
|
message: 'Webhook failed to execute',
|
|
|
|
|
details: {
|
|
|
|
|
request,
|
|
|
|
|
response,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getBodyContent =
|
|
|
|
|
(
|
|
|
|
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
|
|
|
|
linkedTypebots: (Typebot | PublicTypebot)[]
|
|
|
|
|
) =>
|
|
|
|
|
async ({
|
|
|
|
|
body,
|
2023-03-07 14:41:57 +01:00
|
|
|
result,
|
2022-11-29 10:02:40 +01:00
|
|
|
groupId,
|
2023-02-13 14:49:00 +01:00
|
|
|
variables,
|
2022-11-29 10:02:40 +01:00
|
|
|
}: {
|
|
|
|
|
body?: string | null
|
2023-03-07 14:41:57 +01:00
|
|
|
result?: ResultInSession
|
2022-11-29 10:02:40 +01:00
|
|
|
groupId: string
|
2023-02-13 14:49:00 +01:00
|
|
|
variables: Variable[]
|
2022-11-29 10:02:40 +01:00
|
|
|
}): Promise<string | undefined> => {
|
|
|
|
|
if (!body) return
|
|
|
|
|
return body === '{{state}}'
|
|
|
|
|
? JSON.stringify(
|
2023-03-07 14:41:57 +01:00
|
|
|
result
|
|
|
|
|
? parseAnswers(typebot, linkedTypebots)(result)
|
2023-02-13 14:49:00 +01:00
|
|
|
: await parseSampleResult(typebot, linkedTypebots)(
|
|
|
|
|
groupId,
|
|
|
|
|
variables
|
|
|
|
|
)
|
2022-11-29 10:02:40 +01:00
|
|
|
)
|
|
|
|
|
: body
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const convertKeyValueTableToObject = (
|
|
|
|
|
keyValues: KeyValue[] | undefined,
|
|
|
|
|
variables: Variable[]
|
|
|
|
|
) => {
|
|
|
|
|
if (!keyValues) return
|
|
|
|
|
return keyValues.reduce((object, item) => {
|
|
|
|
|
if (!item.key) return {}
|
|
|
|
|
return {
|
|
|
|
|
...object,
|
|
|
|
|
[item.key]: parseVariables(variables)(item.value ?? ''),
|
|
|
|
|
}
|
|
|
|
|
}, {})
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-22 17:02:34 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2022-11-29 10:02:40 +01:00
|
|
|
const safeJsonParse = (json: string): { data: any; isJson: boolean } => {
|
|
|
|
|
try {
|
|
|
|
|
return { data: JSON.parse(json), isJson: true }
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return { data: json, isJson: false }
|
|
|
|
|
}
|
|
|
|
|
}
|