@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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[] => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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}` },
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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())}
|
||||
|
||||
@@ -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('+') ? '' : '+'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user