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

@@ -21,7 +21,7 @@ const injectViewerUrlIfVercelPreview = (val) => {
process.env.VERCEL_BUILDER_PROJECT_NAME,
process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME
)
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
},
headers: async () => {

View File

@@ -67,7 +67,7 @@
"framer-motion": "10.3.0",
"google-auth-library": "8.9.0",
"google-spreadsheet": "4.1.1",
"got": "12.6.0",
"ky": "1.2.3",
"immer": "10.0.2",
"jsonwebtoken": "9.0.1",
"libphonenumber-js": "1.10.37",

View File

@@ -32,7 +32,6 @@ export const getResultExample = authenticatedProcedure
})
)
.query(async ({ input: { typebotId, blockId }, ctx: { user } }) => {
console.log('user', user)
const typebot = (await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: {

View File

@@ -17,7 +17,7 @@ export const convertVariablesForTestToVariables = (
) as Variable
return { ...variable, value: parseVariableValue(variableForTest.value) }
}, {}),
]
].filter((v) => v.value)
}
const parseVariableValue = (value: string | undefined): string | string[] => {

View File

@@ -1,6 +1,5 @@
import { Variable, HttpResponse } from '@typebot.io/schemas'
import { sendRequest } from '@typebot.io/lib'
import { env } from '@typebot.io/env'
export const executeWebhook = (
typebotId: string,
@@ -8,7 +7,7 @@ export const executeWebhook = (
{ blockId }: { blockId: string }
) =>
sendRequest<HttpResponse>({
url: `${env.NEXT_PUBLIC_VIEWER_URL[0]}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook`,
url: `/api/typebots/${typebotId}/blocks/${blockId}/testWebhook`,
method: 'POST',
body: {
variables,

View File

@@ -5,7 +5,7 @@ import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { ZemanticAiCredentials } from '@typebot.io/schemas/features/blocks/integrations/zemanticAi'
import got from 'got'
import ky from 'ky'
export const listProjects = authenticatedProcedure
.input(
@@ -58,7 +58,7 @@ export const listProjects = authenticatedProcedure
const url = 'https://api.zemantic.ai/v1/projects'
try {
const response = await got
const response = await ky
.get(url, {
headers: {
Authorization: `Bearer ${data.apiKey}`,

View File

@@ -3,7 +3,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { customDomainSchema } from '@typebot.io/schemas/features/customDomains'
import got, { HTTPError } from 'got'
import ky, { HTTPError } from 'ky'
import { env } from '@typebot.io/env'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
@@ -61,12 +61,12 @@ export const createCustomDomain = authenticatedProcedure
try {
await createDomainOnVercel(name)
} catch (err) {
console.log(err)
if (err instanceof HTTPError && err.response.statusCode !== 409)
if (err instanceof HTTPError && err.response.status !== 409) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to create custom domain on Vercel',
})
}
}
const customDomain = await prisma.customDomain.create({
@@ -91,8 +91,12 @@ export const createCustomDomain = authenticatedProcedure
})
const createDomainOnVercel = (name: string) =>
got.post({
url: `https://api.vercel.com/v10/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains?teamId=${env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` },
json: { name },
})
ky.post(
`https://api.vercel.com/v10/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains?teamId=${env.VERCEL_TEAM_ID}`,
{
headers: {
authorization: `Bearer ${env.VERCEL_TOKEN}`,
},
json: { name },
}
)

View File

@@ -2,7 +2,7 @@ import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import got from 'got'
import ky from 'ky'
import { env } from '@typebot.io/env'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
@@ -63,7 +63,9 @@ export const deleteCustomDomain = authenticatedProcedure
})
const deleteDomainOnVercel = (name: string) =>
got.delete({
url: `https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` },
})
ky.delete(
`https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`,
{
headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` },
}
)

View File

@@ -377,6 +377,7 @@ const SystemUserToken = ({
<ListItem>Copy and paste the generated token:</ListItem>
<TextInput
isRequired
type="password"
label="System User Token"
defaultValue={initialToken}
onChange={(val) => setToken(val.trim())}

View File

@@ -1,6 +1,6 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import got from 'got'
import ky from 'ky'
import prisma from '@typebot.io/lib/prisma'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { TRPCError } from '@trpc/server'
@@ -22,16 +22,13 @@ export const getPhoneNumber = authenticatedProcedure
code: 'NOT_FOUND',
message: 'Credentials not found',
})
const { display_phone_number } = (await got(
`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}`,
{
const { display_phone_number } = await ky
.get(`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}`, {
headers: {
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
},
}
).json()) as {
display_phone_number: string
}
})
.json<{ display_phone_number: string }>()
const formattedPhoneNumber = `${
display_phone_number.startsWith('+') ? '' : '+'

View File

@@ -1,6 +1,6 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import got from 'got'
import ky from 'ky'
import { TRPCError } from '@trpc/server'
import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp'
import prisma from '@typebot.io/lib/prisma'
@@ -28,21 +28,23 @@ export const getSystemTokenInfo = authenticatedProcedure
})
const {
data: { expires_at, scopes, app_id, application },
} = (await got(
`${env.WHATSAPP_CLOUD_API_URL}/v17.0/debug_token?input_token=${credentials.systemUserAccessToken}`,
{
headers: {
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
},
}
).json()) as {
data: {
app_id: string
application: string
expires_at: number
scopes: string[]
}
}
} = await ky
.get(
`${env.WHATSAPP_CLOUD_API_URL}/v17.0/debug_token?input_token=${credentials.systemUserAccessToken}`,
{
headers: {
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
},
}
)
.json<{
data: {
app_id: string
application: string
expires_at: number
scopes: string[]
}
}>()
return {
appId: app_id,

View File

@@ -3,7 +3,7 @@ import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { startSession } from '@typebot.io/bot-engine/startSession'
import { env } from '@typebot.io/env'
import { HTTPError } from 'got'
import { HTTPError } from 'ky'
import prisma from '@typebot.io/lib/prisma'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
@@ -169,7 +169,7 @@ export const startWhatsAppPreview = authenticatedProcedure
},
})
} catch (err) {
if (err instanceof HTTPError) console.log(err.response.body)
if (err instanceof HTTPError) console.log(await err.response.text())
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Request to Meta to send preview message failed',

View File

@@ -16,7 +16,7 @@ import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitat
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis/nodejs'
import got from 'got'
import ky from 'ky'
import { env } from '@typebot.io/env'
import * as Sentry from '@sentry/nextjs'
import { getIp } from '@typebot.io/lib/getIp'
@@ -164,10 +164,12 @@ export const getAuthOptions = ({
if (!account) return false
const isNewUser = !('createdAt' in user && isDefined(user.createdAt))
if (isNewUser && user.email) {
const { body } = await got.get(
'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf'
)
const disposableEmailDomains = body.split('\n')
const data = await ky
.get(
'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf'
)
.text()
const disposableEmailDomains = data.split('\n')
if (disposableEmailDomains.includes(user.email.split('@')[1]))
return false
}

View File

@@ -5,7 +5,7 @@ import {
methodNotAllowed,
notAuthenticated,
} from '@typebot.io/lib/api'
import { got } from 'got'
import ky from 'ky'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import { env } from '@typebot.io/env'
@@ -31,9 +31,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}
const deleteDomainOnVercel = (name: string) =>
got.delete({
url: `https://api.vercel.com/v8/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`,
headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` },
})
ky.delete(
`https://api.vercel.com/v8/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`,
{
headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` },
}
)
export default handler

View File

@@ -0,0 +1,114 @@
import {
Typebot,
Variable,
HttpRequest,
Block,
AnswerInSessionState,
} from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { byId } from '@typebot.io/lib'
import { isWebhookBlock } from '@typebot.io/schemas/helpers'
import { methodNotAllowed, notFound } from '@typebot.io/lib/api'
import prisma from '@typebot.io/lib/prisma'
import { getBlockById } from '@typebot.io/schemas/helpers'
import {
executeWebhook,
parseWebhookAttributes,
} from '@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock'
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 { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const user = await getAuthenticatedUser(req, res)
const typebotId = req.query.typebotId as string
const blockId = req.query.blockId as string
const resultId = req.query.resultId as string | undefined
const { variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
variables: Variable[]
}
const typebot = (await prisma.typebot.findUnique({
where: { id: typebotId },
include: { webhooks: true },
})) as unknown as (Typebot & { webhooks: HttpRequest[] }) | null
if (!typebot) return notFound(res)
const block = typebot.groups
.flatMap<Block>((g) => g.blocks)
.find(byId(blockId))
if (!block || !isWebhookBlock(block))
return notFound(res, 'Webhook block not found')
const webhookId = 'webhookId' in block ? block.webhookId : undefined
const webhook =
block.options?.webhook ??
typebot.webhooks.find((w) => {
if ('id' in w) return w.id === webhookId
return false
})
if (!webhook)
return res
.status(404)
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
const { group } = getBlockById(blockId, typebot.groups)
const linkedTypebots = await fetchLinkedChildTypebots({
isPreview: !('typebotId' in typebot),
typebots: [typebot],
userId: user?.id,
})([])
const answers = 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,
})
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 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

View File

@@ -9,7 +9,7 @@ Here is the `sendMessage` action of the Telegram block:
```ts
import { createAction, option } from '@typebot.io/forge'
import { auth } from '../auth'
import { got } from 'got'
import ky from 'ky'
export const sendMessage = createAction({
auth,
@@ -27,14 +27,14 @@ export const sendMessage = createAction({
run: {
server: async ({ credentials: { token }, options: { chatId, text } }) => {
try {
await got.post(`https://api.telegram.org/bot${token}/sendMessage`, {
await ky.post(`https://api.telegram.org/bot${token}/sendMessage`, {
json: {
chat_id: chatId,
text,
},
})
} catch (error) {
console.log('ERROR', error.response.body)
console.log('ERROR', await error.response.text())
}
},
},

View File

@@ -13,8 +13,6 @@ An action can do one of the following things:
The most common action is to execute a function on the server. This is done by simply declaring that function in the action block.
If you need to use an external package that is not compatible with web browser environment, you will have to dynamically import it in the server function. You can find an example of this in the [Anthropic's Create Chat Message action](https://github.com/baptisteArno/typebot.io/blob/main/packages/forge/blocks/anthropic/actions/createChatMessage.tsx)
Example:
```ts

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