2
0

♻️ Migrate from got to ky (#1416)

Closes #1415
This commit is contained in:
Baptiste Arnaud
2024-04-05 09:01:16 +02:00
committed by GitHub
parent ccc7101dd3
commit d96f384e02
59 changed files with 990 additions and 628 deletions

View File

@@ -17,7 +17,7 @@ const injectViewerUrlIfVercelPreview = (val) => {
)
return
process.env.NEXT_PUBLIC_VIEWER_URL = `https://${process.env.VERCEL_BRANCH_URL}`
if (process.env.NEXT_PUBLIC_CHAT_API_URL.includes('{{pr_id}}'))
if (process.env.NEXT_PUBLIC_CHAT_API_URL?.includes('{{pr_id}}'))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
'{{pr_id}}',
@@ -56,12 +56,14 @@ const nextConfig = {
if (nextRuntime === 'edge') {
config.resolve.alias['minio'] = false
config.resolve.alias['got'] = false
config.resolve.alias['qrcode'] = false
return config
}
// These packages are imports from the integrations definition files that can be ignored for the client.
config.resolve.alias['minio'] = false
config.resolve.alias['got'] = false
config.resolve.alias['openai'] = false
config.resolve.alias['qrcode'] = false
return config
},
async redirects() {

View File

@@ -26,6 +26,7 @@
"cors": "2.8.5",
"google-spreadsheet": "4.1.1",
"got": "12.6.0",
"ky": "1.2.3",
"next": "14.1.0",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.8",

View File

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