|
|
|
|
@@ -11,10 +11,11 @@ import {
|
|
|
|
|
ChatLog,
|
|
|
|
|
ExecutableHttpRequest,
|
|
|
|
|
AnswerInSessionState,
|
|
|
|
|
TypebotInSession,
|
|
|
|
|
} from '@typebot.io/schemas'
|
|
|
|
|
import { stringify } from 'qs'
|
|
|
|
|
import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
|
|
|
|
import got, { Method, HTTPError, OptionsInit } from 'got'
|
|
|
|
|
import ky, { HTTPError, Options } from 'ky'
|
|
|
|
|
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
|
|
|
|
import { ExecuteIntegrationResponse } from '../../../types'
|
|
|
|
|
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
|
|
|
|
@@ -60,9 +61,11 @@ export const executeWebhookBlock = async (
|
|
|
|
|
})) as HttpRequest | null)
|
|
|
|
|
: null)
|
|
|
|
|
if (!webhook) return { outgoingEdgeId: block.outgoingEdgeId }
|
|
|
|
|
const parsedWebhook = await parseWebhookAttributes(state)({
|
|
|
|
|
const parsedWebhook = await parseWebhookAttributes({
|
|
|
|
|
webhook,
|
|
|
|
|
isCustomBody: block.options?.isCustomBody,
|
|
|
|
|
typebot: state.typebotsQueue[0].typebot,
|
|
|
|
|
answers: state.typebotsQueue[0].answers,
|
|
|
|
|
})
|
|
|
|
|
if (!parsedWebhook) {
|
|
|
|
|
logs.push({
|
|
|
|
|
@@ -104,69 +107,69 @@ export const executeWebhookBlock = async (
|
|
|
|
|
|
|
|
|
|
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
|
|
|
|
|
|
|
|
|
const parseWebhookAttributes =
|
|
|
|
|
(state: SessionState) =>
|
|
|
|
|
async ({
|
|
|
|
|
webhook,
|
|
|
|
|
isCustomBody,
|
|
|
|
|
}: {
|
|
|
|
|
webhook: HttpRequest
|
|
|
|
|
isCustomBody?: boolean
|
|
|
|
|
}): Promise<ParsedWebhook | undefined> => {
|
|
|
|
|
if (!webhook.url) return
|
|
|
|
|
const { typebot } = state.typebotsQueue[0]
|
|
|
|
|
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 &&
|
|
|
|
|
isDefined(basicAuthHeaderIdx) &&
|
|
|
|
|
webhook.headers?.at(basicAuthHeaderIdx)?.value?.includes(':')
|
|
|
|
|
if (isUsernamePasswordBasicAuth) {
|
|
|
|
|
const [username, password] =
|
|
|
|
|
webhook.headers?.at(basicAuthHeaderIdx)?.value?.slice(6).split(':') ??
|
|
|
|
|
[]
|
|
|
|
|
basicAuth.username = username
|
|
|
|
|
basicAuth.password = password
|
|
|
|
|
webhook.headers?.splice(basicAuthHeaderIdx, 1)
|
|
|
|
|
}
|
|
|
|
|
const headers = convertKeyValueTableToObject(
|
|
|
|
|
webhook.headers,
|
|
|
|
|
typebot.variables
|
|
|
|
|
) as ExecutableHttpRequest['headers'] | undefined
|
|
|
|
|
const queryParams = stringify(
|
|
|
|
|
convertKeyValueTableToObject(webhook.queryParams, typebot.variables)
|
|
|
|
|
)
|
|
|
|
|
const bodyContent = await getBodyContent({
|
|
|
|
|
body: webhook.body,
|
|
|
|
|
answers: state.typebotsQueue[0].answers,
|
|
|
|
|
variables: typebot.variables,
|
|
|
|
|
isCustomBody,
|
|
|
|
|
})
|
|
|
|
|
const method = webhook.method ?? defaultWebhookAttributes.method
|
|
|
|
|
const { data: body, isJson } =
|
|
|
|
|
bodyContent && method !== HttpMethod.GET
|
|
|
|
|
? safeJsonParse(
|
|
|
|
|
parseVariables(typebot.variables, {
|
|
|
|
|
isInsideJson: !checkIfBodyIsAVariable(bodyContent),
|
|
|
|
|
})(bodyContent)
|
|
|
|
|
)
|
|
|
|
|
: { data: undefined, isJson: false }
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
url: parseVariables(typebot.variables)(
|
|
|
|
|
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
|
|
|
|
),
|
|
|
|
|
basicAuth,
|
|
|
|
|
method,
|
|
|
|
|
headers,
|
|
|
|
|
body,
|
|
|
|
|
isJson,
|
|
|
|
|
}
|
|
|
|
|
export const parseWebhookAttributes = async ({
|
|
|
|
|
webhook,
|
|
|
|
|
isCustomBody,
|
|
|
|
|
typebot,
|
|
|
|
|
answers,
|
|
|
|
|
}: {
|
|
|
|
|
webhook: HttpRequest
|
|
|
|
|
isCustomBody?: boolean
|
|
|
|
|
typebot: TypebotInSession
|
|
|
|
|
answers: AnswerInSessionState[]
|
|
|
|
|
}): Promise<ParsedWebhook | undefined> => {
|
|
|
|
|
if (!webhook.url) return
|
|
|
|
|
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 &&
|
|
|
|
|
isDefined(basicAuthHeaderIdx) &&
|
|
|
|
|
webhook.headers?.at(basicAuthHeaderIdx)?.value?.includes(':')
|
|
|
|
|
if (isUsernamePasswordBasicAuth) {
|
|
|
|
|
const [username, password] =
|
|
|
|
|
webhook.headers?.at(basicAuthHeaderIdx)?.value?.slice(6).split(':') ?? []
|
|
|
|
|
basicAuth.username = username
|
|
|
|
|
basicAuth.password = password
|
|
|
|
|
webhook.headers?.splice(basicAuthHeaderIdx, 1)
|
|
|
|
|
}
|
|
|
|
|
const headers = convertKeyValueTableToObject(
|
|
|
|
|
webhook.headers,
|
|
|
|
|
typebot.variables
|
|
|
|
|
) as ExecutableHttpRequest['headers'] | undefined
|
|
|
|
|
const queryParams = stringify(
|
|
|
|
|
convertKeyValueTableToObject(webhook.queryParams, typebot.variables)
|
|
|
|
|
)
|
|
|
|
|
const bodyContent = await getBodyContent({
|
|
|
|
|
body: webhook.body,
|
|
|
|
|
answers,
|
|
|
|
|
variables: typebot.variables,
|
|
|
|
|
isCustomBody,
|
|
|
|
|
})
|
|
|
|
|
const method = webhook.method ?? defaultWebhookAttributes.method
|
|
|
|
|
const { data: body, isJson } =
|
|
|
|
|
bodyContent && method !== HttpMethod.GET
|
|
|
|
|
? safeJsonParse(
|
|
|
|
|
parseVariables(typebot.variables, {
|
|
|
|
|
isInsideJson: !checkIfBodyIsAVariable(bodyContent),
|
|
|
|
|
})(bodyContent)
|
|
|
|
|
)
|
|
|
|
|
: { data: undefined, isJson: false }
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
url: parseVariables(typebot.variables)(
|
|
|
|
|
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
|
|
|
|
),
|
|
|
|
|
basicAuth,
|
|
|
|
|
method,
|
|
|
|
|
headers,
|
|
|
|
|
body,
|
|
|
|
|
isJson,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const executeWebhook = async (
|
|
|
|
|
webhook: ParsedWebhook,
|
|
|
|
|
@@ -177,7 +180,8 @@ export const executeWebhook = async (
|
|
|
|
|
startTimeShouldBeUpdated?: boolean
|
|
|
|
|
}> => {
|
|
|
|
|
const logs: ChatLog[] = []
|
|
|
|
|
const { headers, url, method, basicAuth, body, isJson } = webhook
|
|
|
|
|
|
|
|
|
|
const { headers, url, method, basicAuth, isJson } = webhook
|
|
|
|
|
const contentType = headers ? headers['Content-Type'] : undefined
|
|
|
|
|
|
|
|
|
|
const isLongRequest = params.disableRequestTimeout
|
|
|
|
|
@@ -186,59 +190,60 @@ export const executeWebhook = async (
|
|
|
|
|
url?.includes(whiteListedUrl)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const isFormData = contentType?.includes('x-www-form-urlencoded')
|
|
|
|
|
|
|
|
|
|
let body = webhook.body
|
|
|
|
|
|
|
|
|
|
if (isFormData && isJson) body = parseFormDataBody(body as object)
|
|
|
|
|
|
|
|
|
|
const request = {
|
|
|
|
|
url,
|
|
|
|
|
method: method as Method,
|
|
|
|
|
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 as string) : undefined,
|
|
|
|
|
timeout: {
|
|
|
|
|
response: isNotDefined(env.CHAT_API_TIMEOUT)
|
|
|
|
|
? undefined
|
|
|
|
|
: params.timeout && params.timeout !== defaultTimeout
|
|
|
|
|
? Math.min(params.timeout, maxTimeout) * 1000
|
|
|
|
|
: isLongRequest
|
|
|
|
|
? maxTimeout * 1000
|
|
|
|
|
: defaultTimeout * 1000,
|
|
|
|
|
},
|
|
|
|
|
} satisfies OptionsInit
|
|
|
|
|
json: !isFormData && body && isJson ? body : undefined,
|
|
|
|
|
body: (isFormData && body ? body : undefined) as any,
|
|
|
|
|
timeout: isNotDefined(env.CHAT_API_TIMEOUT)
|
|
|
|
|
? undefined
|
|
|
|
|
: params.timeout && params.timeout !== defaultTimeout
|
|
|
|
|
? Math.min(params.timeout, maxTimeout) * 1000
|
|
|
|
|
: isLongRequest
|
|
|
|
|
? maxTimeout * 1000
|
|
|
|
|
: defaultTimeout * 1000,
|
|
|
|
|
} satisfies Options & { url: string; body: any }
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await got(request.url, omit(request, 'url'))
|
|
|
|
|
const response = await ky(request.url, omit(request, 'url'))
|
|
|
|
|
const body = await response.text()
|
|
|
|
|
logs.push({
|
|
|
|
|
status: 'success',
|
|
|
|
|
description: webhookSuccessDescription,
|
|
|
|
|
details: {
|
|
|
|
|
statusCode: response.statusCode,
|
|
|
|
|
response: safeJsonParse(response.body).data,
|
|
|
|
|
statusCode: response.status,
|
|
|
|
|
response: safeJsonParse(body).data,
|
|
|
|
|
request,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
response: {
|
|
|
|
|
statusCode: response.statusCode,
|
|
|
|
|
data: safeJsonParse(response.body).data,
|
|
|
|
|
statusCode: response.status,
|
|
|
|
|
data: safeJsonParse(body).data,
|
|
|
|
|
},
|
|
|
|
|
logs,
|
|
|
|
|
startTimeShouldBeUpdated: true,
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof HTTPError) {
|
|
|
|
|
const responseBody = await error.response.text()
|
|
|
|
|
const response = {
|
|
|
|
|
statusCode: error.response.statusCode,
|
|
|
|
|
data: safeJsonParse(error.response.body as string).data,
|
|
|
|
|
statusCode: error.response.status,
|
|
|
|
|
data: safeJsonParse(responseBody).data,
|
|
|
|
|
}
|
|
|
|
|
logs.push({
|
|
|
|
|
status: 'error',
|
|
|
|
|
description: webhookErrorDescription,
|
|
|
|
|
details: {
|
|
|
|
|
statusCode: error.response.statusCode,
|
|
|
|
|
statusCode: error.response.status,
|
|
|
|
|
request,
|
|
|
|
|
response,
|
|
|
|
|
},
|
|
|
|
|
@@ -257,7 +262,7 @@ export const executeWebhook = async (
|
|
|
|
|
}
|
|
|
|
|
logs.push({
|
|
|
|
|
status: 'error',
|
|
|
|
|
description: `Webhook request timed out. (${request.timeout.response}ms)`,
|
|
|
|
|
description: `Webhook request timed out. (${request.timeout}ms)`,
|
|
|
|
|
details: {
|
|
|
|
|
response,
|
|
|
|
|
request,
|
|
|
|
|
@@ -320,10 +325,18 @@ export const convertKeyValueTableToObject = (
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
const safeJsonParse = (json: string): { data: any; isJson: boolean } => {
|
|
|
|
|
const safeJsonParse = (json: unknown): { data: any; isJson: boolean } => {
|
|
|
|
|
try {
|
|
|
|
|
return { data: JSON.parse(json), isJson: true }
|
|
|
|
|
return { data: JSON.parse(json as string), isJson: true }
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return { data: json, isJson: false }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parseFormDataBody = (body: object) => {
|
|
|
|
|
const searchParams = new URLSearchParams()
|
|
|
|
|
Object.entries(body as object).forEach(([key, value]) => {
|
|
|
|
|
searchParams.set(key, value)
|
|
|
|
|
})
|
|
|
|
|
return searchParams
|
|
|
|
|
}
|
|
|
|
|
|