|
|
|
|
@@ -1,45 +1,35 @@
|
|
|
|
|
import {
|
|
|
|
|
PublicTypebot,
|
|
|
|
|
ResultValues,
|
|
|
|
|
Typebot,
|
|
|
|
|
Variable,
|
|
|
|
|
HttpRequest,
|
|
|
|
|
HttpResponse,
|
|
|
|
|
Block,
|
|
|
|
|
PublicTypebot,
|
|
|
|
|
AnswerInSessionState,
|
|
|
|
|
} from '@typebot.io/schemas'
|
|
|
|
|
import { NextApiRequest, NextApiResponse } from 'next'
|
|
|
|
|
import got, { Method, Headers, HTTPError } from 'got'
|
|
|
|
|
import { byId, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
|
|
|
|
import { byId } from '@typebot.io/lib'
|
|
|
|
|
import { isWebhookBlock } from '@typebot.io/schemas/helpers'
|
|
|
|
|
import { parseAnswers } from '@typebot.io/results/parseAnswers'
|
|
|
|
|
import { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api'
|
|
|
|
|
import { stringify } from 'qs'
|
|
|
|
|
import Cors from 'cors'
|
|
|
|
|
import prisma from '@typebot.io/lib/prisma'
|
|
|
|
|
import { fetchLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots'
|
|
|
|
|
import { getPreviouslyLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
|
|
|
|
|
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
|
|
|
|
import { saveErrorLog } from '@typebot.io/bot-engine/logs/saveErrorLog'
|
|
|
|
|
import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog'
|
|
|
|
|
import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult'
|
|
|
|
|
import {
|
|
|
|
|
HttpMethod,
|
|
|
|
|
defaultTimeout,
|
|
|
|
|
defaultWebhookAttributes,
|
|
|
|
|
maxTimeout,
|
|
|
|
|
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
|
|
|
|
|
import { getBlockById } from '@typebot.io/schemas/helpers'
|
|
|
|
|
import {
|
|
|
|
|
convertKeyValueTableToObject,
|
|
|
|
|
longReqTimeoutWhitelist,
|
|
|
|
|
executeWebhook,
|
|
|
|
|
parseWebhookAttributes,
|
|
|
|
|
} from '@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock'
|
|
|
|
|
import { env } from '@typebot.io/env'
|
|
|
|
|
import { fetchLinkedParentTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedParentTypebots'
|
|
|
|
|
import { fetchLinkedChildTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots'
|
|
|
|
|
import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult'
|
|
|
|
|
import { saveLog } from '@typebot.io/bot-engine/logs/saveLog'
|
|
|
|
|
import { authenticateUser } from '@/helpers/authenticateUser'
|
|
|
|
|
|
|
|
|
|
const cors = initMiddleware(Cors())
|
|
|
|
|
|
|
|
|
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
|
|
|
await cors(req, res)
|
|
|
|
|
if (req.method === 'POST') {
|
|
|
|
|
const user = await authenticateUser(req)
|
|
|
|
|
const typebotId = req.query.typebotId as string
|
|
|
|
|
const blockId = req.query.blockId as string
|
|
|
|
|
const resultId = req.query.resultId as string | undefined
|
|
|
|
|
@@ -72,232 +62,81 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
|
|
|
.status(404)
|
|
|
|
|
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
|
|
|
|
|
const { group } = getBlockById(blockId, typebot.groups)
|
|
|
|
|
const result = await executeWebhook(typebot)({
|
|
|
|
|
webhook,
|
|
|
|
|
variables,
|
|
|
|
|
groupId: group.id,
|
|
|
|
|
resultValues,
|
|
|
|
|
resultId,
|
|
|
|
|
const linkedTypebotsParents = (await fetchLinkedParentTypebots({
|
|
|
|
|
isPreview: !('typebotId' in typebot),
|
|
|
|
|
parentTypebotIds,
|
|
|
|
|
userId: user?.id,
|
|
|
|
|
})) as (Typebot | PublicTypebot)[]
|
|
|
|
|
const linkedTypebotsChildren = await fetchLinkedChildTypebots({
|
|
|
|
|
isPreview: !('typebotId' in typebot),
|
|
|
|
|
typebots: [typebot],
|
|
|
|
|
userId: user?.id,
|
|
|
|
|
})([])
|
|
|
|
|
|
|
|
|
|
const linkedTypebots = [...linkedTypebotsParents, ...linkedTypebotsChildren]
|
|
|
|
|
|
|
|
|
|
const answers = resultValues
|
|
|
|
|
? resultValues.answers.map((answer) => ({
|
|
|
|
|
key:
|
|
|
|
|
(answer.variableId
|
|
|
|
|
? typebot.variables.find(
|
|
|
|
|
(variable) => variable.id === answer.variableId
|
|
|
|
|
)?.name
|
|
|
|
|
: typebot.groups.find((group) =>
|
|
|
|
|
group.blocks.find((block) => block.id === answer.blockId)
|
|
|
|
|
)?.title) ?? '',
|
|
|
|
|
value: answer.content,
|
|
|
|
|
}))
|
|
|
|
|
: arrayify(
|
|
|
|
|
await parseSampleResult(typebot, linkedTypebots)(group.id, variables)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const parsedWebhook = await parseWebhookAttributes({
|
|
|
|
|
webhook,
|
|
|
|
|
isCustomBody: block.options?.isCustomBody,
|
|
|
|
|
typebot: {
|
|
|
|
|
...typebot,
|
|
|
|
|
variables: typebot.variables.map((v) => {
|
|
|
|
|
const matchingVariable = variables.find(byId(v.id))
|
|
|
|
|
if (!matchingVariable) return v
|
|
|
|
|
return { ...v, value: matchingVariable.value }
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
answers,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!parsedWebhook)
|
|
|
|
|
return res.status(500).send({
|
|
|
|
|
statusCode: 500,
|
|
|
|
|
data: { message: `Couldn't parse webhook attributes` },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { response, logs } = await executeWebhook(parsedWebhook, {
|
|
|
|
|
timeout: block.options?.timeout,
|
|
|
|
|
})
|
|
|
|
|
return res.status(200).send(result)
|
|
|
|
|
|
|
|
|
|
if (resultId)
|
|
|
|
|
await Promise.all(
|
|
|
|
|
logs?.map((log) =>
|
|
|
|
|
saveLog({
|
|
|
|
|
message: log.description,
|
|
|
|
|
details: log.details,
|
|
|
|
|
status: log.status as 'error' | 'success' | 'info',
|
|
|
|
|
resultId,
|
|
|
|
|
})
|
|
|
|
|
) ?? []
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return res.status(200).send(response)
|
|
|
|
|
}
|
|
|
|
|
return methodNotAllowed(res)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
|
|
|
|
|
|
|
|
|
export const executeWebhook =
|
|
|
|
|
(typebot: Typebot) =>
|
|
|
|
|
async ({
|
|
|
|
|
webhook,
|
|
|
|
|
variables,
|
|
|
|
|
groupId,
|
|
|
|
|
resultValues,
|
|
|
|
|
resultId,
|
|
|
|
|
parentTypebotIds = [],
|
|
|
|
|
isCustomBody,
|
|
|
|
|
timeout,
|
|
|
|
|
}: {
|
|
|
|
|
webhook: HttpRequest
|
|
|
|
|
variables: Variable[]
|
|
|
|
|
groupId: string
|
|
|
|
|
resultValues?: ResultValues
|
|
|
|
|
resultId?: string
|
|
|
|
|
parentTypebotIds: string[]
|
|
|
|
|
isCustomBody?: boolean
|
|
|
|
|
timeout?: number
|
|
|
|
|
}): Promise<HttpResponse> => {
|
|
|
|
|
if (!webhook.url)
|
|
|
|
|
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')
|
|
|
|
|
) ?? -1
|
|
|
|
|
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 linkedTypebotsParents = (await fetchLinkedTypebots({
|
|
|
|
|
isPreview: !('typebotId' in typebot),
|
|
|
|
|
typebotIds: parentTypebotIds,
|
|
|
|
|
})) as (Typebot | PublicTypebot)[]
|
|
|
|
|
const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({
|
|
|
|
|
isPreview: !('typebotId' in typebot),
|
|
|
|
|
typebots: [typebot],
|
|
|
|
|
})([])
|
|
|
|
|
const bodyContent = await getBodyContent(typebot, [
|
|
|
|
|
...linkedTypebotsParents,
|
|
|
|
|
...linkedTypebotsChildren,
|
|
|
|
|
])({
|
|
|
|
|
body: webhook.body,
|
|
|
|
|
isCustomBody,
|
|
|
|
|
resultValues,
|
|
|
|
|
groupId,
|
|
|
|
|
variables,
|
|
|
|
|
})
|
|
|
|
|
const { data: body, isJson } =
|
|
|
|
|
bodyContent && webhook.method !== HttpMethod.GET
|
|
|
|
|
? safeJsonParse(
|
|
|
|
|
parseVariables(variables, {
|
|
|
|
|
isInsideJson: !checkIfBodyIsAVariable(bodyContent),
|
|
|
|
|
})(bodyContent)
|
|
|
|
|
)
|
|
|
|
|
: { data: undefined, isJson: false }
|
|
|
|
|
|
|
|
|
|
const url = parseVariables(variables)(
|
|
|
|
|
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const isLongRequest = longReqTimeoutWhitelist.some((whiteListedUrl) =>
|
|
|
|
|
url?.includes(whiteListedUrl)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const request = {
|
|
|
|
|
url,
|
|
|
|
|
method: (webhook.method ?? defaultWebhookAttributes.method) as Method,
|
|
|
|
|
headers: 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,
|
|
|
|
|
timeout: {
|
|
|
|
|
response: isNotDefined(env.CHAT_API_TIMEOUT)
|
|
|
|
|
? undefined
|
|
|
|
|
: timeout && timeout !== defaultTimeout
|
|
|
|
|
? Math.min(timeout, maxTimeout) * 1000
|
|
|
|
|
: isLongRequest
|
|
|
|
|
? maxTimeout * 1000
|
|
|
|
|
: defaultTimeout * 1000,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const response = await got(request.url, omit(request, 'url'))
|
|
|
|
|
await saveSuccessLog({
|
|
|
|
|
resultId,
|
|
|
|
|
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({
|
|
|
|
|
resultId,
|
|
|
|
|
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({
|
|
|
|
|
resultId,
|
|
|
|
|
message: 'Webhook failed to execute',
|
|
|
|
|
details: {
|
|
|
|
|
request,
|
|
|
|
|
response,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getBodyContent =
|
|
|
|
|
(
|
|
|
|
|
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
|
|
|
|
|
linkedTypebots: (Typebot | PublicTypebot)[]
|
|
|
|
|
) =>
|
|
|
|
|
async ({
|
|
|
|
|
body,
|
|
|
|
|
resultValues,
|
|
|
|
|
groupId,
|
|
|
|
|
variables,
|
|
|
|
|
isCustomBody,
|
|
|
|
|
}: {
|
|
|
|
|
body?: string | null
|
|
|
|
|
resultValues?: ResultValues
|
|
|
|
|
groupId: string
|
|
|
|
|
variables: Variable[]
|
|
|
|
|
isCustomBody?: boolean
|
|
|
|
|
}): Promise<string | undefined> => {
|
|
|
|
|
return body === '{{state}}' || isEmpty(body) || isCustomBody !== true
|
|
|
|
|
? JSON.stringify(
|
|
|
|
|
resultValues
|
|
|
|
|
? parseAnswers({
|
|
|
|
|
answers: resultValues.answers.map((answer) => ({
|
|
|
|
|
key:
|
|
|
|
|
(answer.variableId
|
|
|
|
|
? typebot.variables.find(
|
|
|
|
|
(variable) => variable.id === answer.variableId
|
|
|
|
|
)?.name
|
|
|
|
|
: typebot.groups.find((group) =>
|
|
|
|
|
group.blocks.find(
|
|
|
|
|
(block) => block.id === answer.blockId
|
|
|
|
|
)
|
|
|
|
|
)?.title) ?? '',
|
|
|
|
|
value: answer.content,
|
|
|
|
|
})),
|
|
|
|
|
variables: resultValues.variables,
|
|
|
|
|
})
|
|
|
|
|
: await parseSampleResult(typebot, linkedTypebots)(
|
|
|
|
|
groupId,
|
|
|
|
|
variables
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
: body ?? undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
const safeJsonParse = (json: string): { data: any; isJson: boolean } => {
|
|
|
|
|
try {
|
|
|
|
|
return { data: JSON.parse(json), isJson: true }
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return { data: json, isJson: false }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const arrayify = (
|
|
|
|
|
obj: Record<string, string | boolean | undefined>
|
|
|
|
|
): AnswerInSessionState[] =>
|
|
|
|
|
Object.entries(obj)
|
|
|
|
|
.map(([key, value]) => ({ key, value: value?.toString() }))
|
|
|
|
|
.filter((a) => a.value) as AnswerInSessionState[]
|
|
|
|
|
|
|
|
|
|
export default handler
|
|
|
|
|
|