♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@@ -7,6 +7,8 @@ import { TypebotPageProps, TypebotPageV2 } from '@/components/TypebotPageV2'
import { TypebotPageV3, TypebotV3PageProps } from '@/components/TypebotPageV3'
import { env } from '@typebot.io/env'
import prisma from '@typebot.io/lib/prisma'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
// Browsers that doesn't support ES modules and/or web components
const incompatibleBrowsers = [
@@ -61,6 +63,7 @@ export const getServerSideProps: GetServerSideProps = async (
const publishedTypebot = isMatchingViewerUrl
? await getTypebotFromPublicId(context.query.publicId?.toString())
: await getTypebotFromCustomDomain(customDomain)
return {
props: {
publishedTypebot,
@@ -106,11 +109,14 @@ const getTypebotFromPublicId = async (publicId?: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background: publishedTypebot.theme.general.background,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
isHideQueryParamsEnabled:
publishedTypebot.settings.general.isHideQueryParamsEnabled ?? null,
metadata: publishedTypebot.settings.metadata,
} as Pick<
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
metadata: publishedTypebot.settings.metadata ?? {},
} satisfies Pick<
TypebotV3PageProps,
| 'name'
| 'publicId'
@@ -148,11 +154,14 @@ const getTypebotFromCustomDomain = async (customDomain: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background: publishedTypebot.theme.general.background,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
isHideQueryParamsEnabled:
publishedTypebot.settings.general.isHideQueryParamsEnabled ?? null,
metadata: publishedTypebot.settings.metadata,
} as Pick<
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
metadata: publishedTypebot.settings.metadata ?? {},
} satisfies Pick<
TypebotV3PageProps,
| 'name'
| 'publicId'
@@ -214,9 +223,14 @@ const App = ({
url={props.url}
name={publishedTypebot.name}
publicId={publishedTypebot.publicId}
isHideQueryParamsEnabled={publishedTypebot.isHideQueryParamsEnabled}
background={publishedTypebot.background}
metadata={publishedTypebot.metadata}
isHideQueryParamsEnabled={
publishedTypebot.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled
}
background={
publishedTypebot.background ?? defaultTheme.general.background
}
metadata={publishedTypebot.metadata ?? {}}
/>
)
}

View File

@@ -8,17 +8,19 @@ import {
import { hasValue, isDefined } from '@typebot.io/lib'
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from 'google-spreadsheet'
import {
ComparisonOperators,
GoogleSheetsAction,
GoogleSheetsGetOptions,
GoogleSheetsInsertRowOptions,
GoogleSheetsUpdateRowOptions,
LogicalOperator,
} from '@typebot.io/schemas'
import Cors from 'cors'
import { getAuthenticatedGoogleClient } from '@/lib/google-sheets'
import { saveErrorLog } from '@typebot.io/bot-engine/logs/saveErrorLog'
import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog'
import { GoogleSheetsAction } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/constants'
import {
ComparisonOperators,
LogicalOperator,
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
const cors = initMiddleware(Cors())
@@ -43,11 +45,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const getRows = async (req: NextApiRequest, res: NextApiResponse) => {
const sheetId = req.query.sheetId as string
const spreadsheetId = req.query.spreadsheetId as string
const { resultId, credentialsId, referenceCell, filter, columns } =
req.body as GoogleSheetsGetOptions & {
resultId?: string
columns: string[] | string
}
const body = req.body as GoogleSheetsGetOptions & {
resultId?: string
columns: string[] | string
}
const referenceCell = 'referenceCell' in body ? body.referenceCell : undefined
const { resultId, credentialsId, filter, columns } = body
if (!hasValue(credentialsId)) {
badRequest(res)
@@ -141,11 +144,13 @@ const insertRow = async (req: NextApiRequest, res: NextApiResponse) => {
const updateRow = async (req: NextApiRequest, res: NextApiResponse) => {
const sheetId = req.query.sheetId as string
const spreadsheetId = req.query.spreadsheetId as string
const { resultId, credentialsId, values, referenceCell } =
req.body as GoogleSheetsUpdateRowOptions & {
resultId?: string
values: { [key: string]: string }
}
const body = req.body as GoogleSheetsUpdateRowOptions & {
resultId?: string
values: { [key: string]: string }
}
const referenceCell = 'referenceCell' in body ? body.referenceCell : undefined
const { resultId, credentialsId, values } = body
if (!hasValue(credentialsId) || !referenceCell) return badRequest(res)
const auth = await getAuthenticatedGoogleClient(credentialsId)
if (!auth)
@@ -181,7 +186,7 @@ const matchFilter = (
filter: NonNullable<GoogleSheetsGetOptions['filter']>
) => {
return filter.logicalOperator === LogicalOperator.AND
? filter.comparisons.every(
? filter.comparisons?.every(
(comparison) =>
comparison.column &&
matchComparison(
@@ -190,7 +195,7 @@ const matchFilter = (
comparison.value
)
)
: filter.comparisons.some(
: filter.comparisons?.some(
(comparison) =>
comparison.column &&
matchComparison(

View File

@@ -10,12 +10,13 @@ import Stripe from 'stripe'
import Cors from 'cors'
import {
PaymentInputOptions,
PaymentInputBlock,
StripeCredentials,
Variable,
} from '@typebot.io/schemas'
import prisma from '@typebot.io/lib/prisma'
import { parseVariables } from '@typebot.io/bot-engine/variables/parseVariables'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
const cors = initMiddleware(Cors())
@@ -43,11 +44,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { inputOptions, isPreview, variables } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as {
inputOptions: PaymentInputOptions
inputOptions: PaymentInputBlock['options']
isPreview: boolean
variables: Variable[]
}
if (!inputOptions.credentialsId) return forbidden(res)
if (!inputOptions?.credentialsId) return forbidden(res)
const stripeKeys = await getStripeInfo(inputOptions.credentialsId)
if (!stripeKeys) return forbidden(res)
const stripe = new Stripe(
@@ -56,9 +57,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
: stripeKeys.live.secretKey,
{ apiVersion: '2022-11-15' }
)
const currency =
inputOptions.currency ?? defaultPaymentInputOptions.currency
const amount = Math.round(
Number(parseVariables(variables)(inputOptions.amount)) *
(isZeroDecimalCurrency(inputOptions.currency) ? 1 : 100)
(isZeroDecimalCurrency(currency) ? 1 : 100)
)
if (isNaN(amount)) return badRequest(res)
// Create a PaymentIntent with the order amount and currency
@@ -68,7 +73,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: inputOptions.currency,
currency,
receipt_email: receiptEmail === '' ? undefined : receiptEmail,
automatic_payment_methods: {
enabled: true,
@@ -81,10 +86,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
isPreview && stripeKeys.test?.publicKey
? stripeKeys.test.publicKey
: stripeKeys.live.publicKey,
amountLabel: `${
amount / (isZeroDecimalCurrency(inputOptions.currency) ? 1 : 100)
}${
currencySymbols[inputOptions.currency] ?? ` ${inputOptions.currency}`
amountLabel: `${amount / (isZeroDecimalCurrency(currency) ? 1 : 100)}${
currencySymbols[currency] ?? ` ${currency}`
}`,
})
} catch (err) {

View File

@@ -1,30 +1,33 @@
import {
defaultWebhookAttributes,
KeyValue,
PublicTypebot,
ResultValues,
Typebot,
Variable,
Webhook,
WebhookOptions,
WebhookResponse,
WebhookBlock,
Block,
} from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import got, { Method, Headers, HTTPError } from 'got'
import { byId, omit } from '@typebot.io/lib'
import { byId, isEmpty, omit } from '@typebot.io/lib'
import { parseAnswers } from '@typebot.io/lib/results'
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 { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
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/bot-engine/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,
defaultWebhookAttributes,
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
import { getBlockById } from '@typebot.io/lib/getBlockById'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
const cors = initMiddleware(Cors())
@@ -47,40 +50,36 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
})) as unknown as (Typebot & { webhooks: Webhook[] }) | null
if (!typebot) return notFound(res)
const block = typebot.groups
.flatMap((g) => g.blocks)
.find(byId(blockId)) as WebhookBlock
.flatMap<Block>((g) => g.blocks)
.find(byId(blockId))
if (block?.type !== IntegrationBlockType.WEBHOOK)
return notFound(res, 'Webhook block not found')
const webhookId = 'webhookId' in block ? block.webhookId : undefined
const webhook =
block.options.webhook ?? typebot.webhooks.find(byId(block.webhookId))
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 preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const { group } = getBlockById(blockId, typebot.groups)
const result = await executeWebhook(typebot)({
webhook: preparedWebhook,
webhook,
variables,
groupId: block.groupId,
groupId: group.id,
resultValues,
resultId,
parentTypebotIds,
isCustomBody: block.options?.isCustomBody,
})
return res.status(200).send(result)
}
return methodNotAllowed(res)
}
const prepareWebhookAttributes = (
webhook: Webhook,
options: WebhookOptions
): Webhook => {
if (options.isAdvancedConfig === false) {
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
} else if (options.isCustomBody === false) {
return { ...webhook, body: '{{state}}' }
}
return webhook
}
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
export const executeWebhook =
@@ -92,6 +91,7 @@ export const executeWebhook =
resultValues,
resultId,
parentTypebotIds = [],
isCustomBody,
}: {
webhook: Webhook
variables: Variable[]
@@ -99,27 +99,29 @@ export const executeWebhook =
resultValues?: ResultValues
resultId?: string
parentTypebotIds: string[]
isCustomBody?: boolean
}): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method)
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')
)
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(':')
webhook.headers?.[basicAuthHeaderIdx].value?.includes(':')
if (isUsernamePasswordBasicAuth) {
const [username, password] =
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
webhook.headers?.[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
basicAuth.username = username
basicAuth.password = password
webhook.headers.splice(basicAuthHeaderIdx, 1)
webhook.headers?.splice(basicAuthHeaderIdx, 1)
}
const headers = convertKeyValueTableToObject(webhook.headers, variables) as
| Headers
@@ -141,6 +143,7 @@ export const executeWebhook =
...linkedTypebotsChildren,
])({
body: webhook.body,
isCustomBody,
resultValues,
groupId,
variables,
@@ -158,8 +161,8 @@ export const executeWebhook =
url: parseVariables(variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
),
method: webhook.method as Method,
headers,
method: (webhook.method ?? defaultWebhookAttributes.method) as Method,
headers: headers ?? {},
...basicAuth,
json:
!contentType?.includes('x-www-form-urlencoded') && body && isJson
@@ -229,14 +232,15 @@ const getBodyContent =
resultValues,
groupId,
variables,
isCustomBody,
}: {
body?: string | null
resultValues?: ResultValues
groupId: string
variables: Variable[]
isCustomBody?: boolean
}): Promise<string | undefined> => {
if (!body) return
return body === '{{state}}'
return isEmpty(body) && isCustomBody !== true
? JSON.stringify(
resultValues
? parseAnswers({
@@ -260,7 +264,7 @@ const getBodyContent =
variables
)
)
: body
: body ?? undefined
}
const convertKeyValueTableToObject = (

View File

@@ -1,7 +1,7 @@
import {
PublicTypebot,
ResultValues,
SendEmailOptions,
SendEmailBlock,
SmtpCredentials,
} from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
@@ -56,12 +56,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
fileUrls,
} = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as SendEmailOptions & {
) as SendEmailBlock['options'] & {
resultValues: ResultValues
fileUrls?: string
}
const { name: replyToName } = parseEmailRecipient(replyTo)
if (!credentialsId)
return res.status(404).send({ message: "Couldn't find credentials" })
const { host, port, isTlsEnabled, username, password, from } =
(await getEmailInfo(credentialsId)) ?? {}
if (!from)
@@ -186,9 +189,10 @@ const getEmailBody = async ({
}: {
typebotId: string
resultValues: ResultValues
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined
> => {
} & Pick<
NonNullable<SendEmailBlock['options']>,
'isCustomBody' | 'isBodyCode' | 'body'
>): Promise<{ html?: string; text?: string } | undefined> => {
if (isCustomBody || (isNotDefined(isCustomBody) && !isEmpty(body)))
return {
html: isBodyCode ? body : undefined,

View File

@@ -2,7 +2,7 @@ import { authenticateUser } from '@/helpers/authenticateUser'
import prisma from '@typebot.io/lib/prisma'
import { Group, WebhookBlock } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { byId, isWebhookBlock } from '@typebot.io/lib'
import { isWebhookBlock } from '@typebot.io/lib'
import { methodNotAllowed } from '@typebot.io/lib/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@@ -28,7 +28,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
...blocks.map((b) => ({
blockId: b.id,
name: `${group.title} > ${b.id}`,
url: typebot?.webhooks.find(byId(b.webhookId))?.url ?? undefined,
url:
typebot?.webhooks.find((w) => {
if ('id' in w && 'webhookId' in b) return w.id === b.webhookId
return false
})?.url ?? undefined,
})),
]
}, [])

View File

@@ -1,8 +1,8 @@
import { authenticateUser } from '@/helpers/authenticateUser'
import prisma from '@typebot.io/lib/prisma'
import { Group, WebhookBlock } from '@typebot.io/schemas'
import { Group } from '@typebot.io/schemas'
import { NextApiRequest, NextApiResponse } from 'next'
import { byId, isNotDefined, isWebhookBlock } from '@typebot.io/lib'
import { isNotDefined, isWebhookBlock } from '@typebot.io/lib'
import { methodNotAllowed } from '@typebot.io/lib/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
@@ -24,15 +24,19 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
(block) =>
isWebhookBlock(block) &&
isNotDefined(
typebot?.webhooks.find(byId((block as WebhookBlock).webhookId))?.url
typebot?.webhooks.find((w) => {
if ('id' in w && 'webhookId' in block)
return w.id === block.webhookId
return false
})?.url
)
)
return [
...emptyWebhookBlocks,
...blocks.map((s) => ({
id: s.id,
groupId: s.groupId,
name: `${group.title} > ${s.id}`,
...blocks.map((b) => ({
id: b.id,
groupId: group.id,
name: `${group.title} > ${b.id}`,
})),
]
}, [])

View File

@@ -16,7 +16,7 @@ export const getServerSideProps: GetServerSideProps = async (
const publishedTypebot = await getTypebotFromPublicId(
context.query.publicId?.toString()
)
const headCode = publishedTypebot?.settings.metadata.customHeadCode
const headCode = publishedTypebot?.settings.metadata?.customHeadCode
return {
props: {
publishedTypebot,