2
0

♻️ 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

@ -16,6 +16,7 @@
"@trpc/server": "10.40.0",
"@typebot.io/bot-engine": "workspace:*",
"@typebot.io/nextjs": "workspace:*",
"@typebot.io/js": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"ai": "2.2.14",
"bot-engine": "workspace:*",
@ -54,7 +55,7 @@
"next-runtime-env": "1.6.2",
"papaparse": "5.4.1",
"superjson": "1.12.4",
"typescript": "5.1.6",
"zod": "3.21.4"
"typescript": "5.2.2",
"zod": "3.22.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -1,10 +1,12 @@
import { connect } from '@planetscale/database'
import { env } from '@typebot.io/env'
import { IntegrationBlockType, SessionState } from '@typebot.io/schemas'
import { SessionState } from '@typebot.io/schemas'
import { StreamingTextResponse } from 'ai'
import { getChatCompletionStream } from '@typebot.io/bot-engine/blocks/integrations/openai/getChatCompletionStream'
import OpenAI from 'openai'
import { NextResponse } from 'next/dist/server/web/spec-extension/response'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { getBlockById } from '@typebot.io/lib/getBlockById'
export const runtime = 'edge'
export const preferredRegion = 'lhr1'
@ -55,22 +57,16 @@ export async function POST(req: Request) {
const state = (chatSession.rows.at(0) as { state: SessionState } | undefined)
?.state
if (!state)
if (!state || !state.currentBlockId)
return NextResponse.json(
{ message: 'No state found' },
{ status: 400, headers: responseHeaders }
)
const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId
const { group, block } = getBlockById(
state.currentBlockId,
state.typebotsQueue[0].typebot.groups
)
const blockIndex =
group?.blocks.findIndex(
(block) => block.id === state.currentBlock?.blockId
) ?? -1
const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null
if (!block || !group)
return NextResponse.json(
{ message: 'Current block not found' },
@ -79,7 +75,7 @@ export async function POST(req: Request) {
if (
block.type !== IntegrationBlockType.OPEN_AI ||
block.options.task !== 'Create chat completion'
block.options?.task !== 'Create chat completion'
)
return NextResponse.json(
{ message: 'Current block is not an OpenAI block' },

View File

@ -1,20 +1,28 @@
import { gtmHeadSnippet } from '@/lib/google-tag-manager'
import { Metadata } from '@typebot.io/schemas'
import Head from 'next/head'
import Script from 'next/script'
import React from 'react'
import { isNotEmpty } from '@typebot.io/lib'
import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
import { Settings } from '@typebot.io/schemas'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
type SEOProps = {
url: string
typebotName: string
metadata: Metadata
metadata?: Settings['metadata']
}
export const SEO = ({
url,
typebotName,
metadata: { title, description, favIconUrl, imageUrl, googleTagManagerId },
metadata: {
title,
description,
favIconUrl,
imageUrl,
googleTagManagerId,
} = {},
}: SEOProps) => (
<>
<Head key="seo">
@ -23,7 +31,7 @@ export const SEO = ({
<link
rel="icon"
type="image/png"
href={favIconUrl ?? 'https://viewer.typebot.io/favicon.png'}
href={favIconUrl ?? defaultSettings.metadata.favIconUrl(getViewerUrl())}
/>
<meta name="title" content={title ?? typebotName} />
<meta
@ -48,7 +56,7 @@ export const SEO = ({
<meta
property="og:image"
itemProp="image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
content={imageUrl ?? defaultSettings.metadata.imageUrl(getViewerUrl())}
/>
<meta property="twitter:card" content="summary_large_image" />
@ -63,7 +71,7 @@ export const SEO = ({
/>
<meta
property="twitter:image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
content={imageUrl ?? defaultSettings.metadata.imageUrl(getViewerUrl())}
/>
</Head>
{isNotEmpty(googleTagManagerId) && (

View File

@ -1,3 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
import { TypebotViewer } from 'bot-engine'
import {
AnswerInput,
@ -23,6 +25,7 @@ import {
import { upsertAnswerQuery } from '@/features/answers/queries/upsertAnswerQuery'
import { createResultQuery } from '@/features/results/queries/createResultQuery'
import { updateResultQuery } from '@/features/results/queries/updateResultQuery'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
export type TypebotPageProps = {
publishedTypebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> & {
@ -64,7 +67,7 @@ export const TypebotPageV2 = ({
setPredefinedVariables(predefinedVariables)
initializeResult().then()
if (isDefined(customHeadCode)) injectCustomHeadCode(customHeadCode)
const gtmId = publishedTypebot.settings.metadata.googleTagManagerId
const gtmId = publishedTypebot.settings.metadata?.googleTagManagerId
if (isNotEmpty(gtmId)) document.body.prepend(gtmBodyElement(gtmId))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -73,7 +76,8 @@ export const TypebotPageV2 = ({
const hasQueryParams = asPath.includes('?')
if (
hasQueryParams &&
publishedTypebot.settings.general.isHideQueryParamsEnabled !== false
(publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled) !== false
)
push(asPath.split('?')[0], undefined, { shallow: true })
}
@ -91,7 +95,8 @@ export const TypebotPageV2 = ({
if (data?.result) {
setResultId(data.result.id)
if (
publishedTypebot.settings.general.isNewResultOnRefreshEnabled !== true
publishedTypebot.settings.general?.isNewResultOnRefreshEnabled !==
true
)
setResultInSession(data.result.id)
}

View File

@ -2,14 +2,15 @@ import { Standard } from '@typebot.io/nextjs'
import { useRouter } from 'next/router'
import { SEO } from './Seo'
import { Typebot } from '@typebot.io/schemas/features/typebot/typebot'
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/enums'
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
export type TypebotV3PageProps = {
url: string
name: string
publicId: string | null
isHideQueryParamsEnabled: boolean | null
background: Typebot['theme']['general']['background']
background: NonNullable<Typebot['theme']['general']>['background']
metadata: Typebot['settings']['metadata']
}
@ -25,7 +26,14 @@ export const TypebotPageV3 = ({
const clearQueryParamsIfNecessary = () => {
const hasQueryParams = asPath.includes('?')
if (!hasQueryParams || !(isHideQueryParamsEnabled ?? true)) return
if (
!hasQueryParams ||
!(
isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled
)
)
return
push(asPath.split('?')[0], undefined, { shallow: true })
}

View File

@ -57,6 +57,7 @@ export const sendMessageV1 = publicProcedure
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 1,
startParams,
@ -77,6 +78,7 @@ export const sendMessageV1 = publicProcedure
input,
logs: allLogs,
clientSideActions,
visitedEdges,
})
return {
@ -103,6 +105,7 @@ export const sendMessageV1 = publicProcedure
newSessionState,
logs,
lastMessageNewFormat,
visitedEdges,
} = await continueBotFlow(message, { version: 1, state: session.state })
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
@ -116,6 +119,7 @@ export const sendMessageV1 = publicProcedure
input,
logs: allLogs,
clientSideActions,
visitedEdges,
})
return {

View File

@ -57,6 +57,7 @@ export const sendMessageV2 = publicProcedure
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams,
@ -77,6 +78,7 @@ export const sendMessageV2 = publicProcedure
input,
logs: allLogs,
clientSideActions,
visitedEdges,
})
return {
@ -103,6 +105,7 @@ export const sendMessageV2 = publicProcedure
newSessionState,
logs,
lastMessageNewFormat,
visitedEdges,
} = await continueBotFlow(message, { version: 2, state: session.state })
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
@ -116,6 +119,7 @@ export const sendMessageV2 = publicProcedure
input,
logs: allLogs,
clientSideActions,
visitedEdges,
})
return {

View File

@ -94,7 +94,7 @@ const updateSessionState = (
},
}
: typebotInQueue
),
) as SessionState['typebotsQueue'],
})
const updateVariablesInSession = (

View File

@ -1,17 +1,19 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import {
Block,
FileInputBlock,
InputBlockType,
LogicBlockType,
PublicTypebot,
TypebotLinkBlock,
parseGroups,
} from '@typebot.io/schemas'
import { byId, isDefined } from '@typebot.io/lib'
import { z } from 'zod'
import { generatePresignedUrl } from '@typebot.io/lib/s3/deprecated/generatePresignedUrl'
import { env } from '@typebot.io/env'
import prisma from '@typebot.io/lib/prisma'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { PublicTypebot } from '@typebot.io/prisma'
export const getUploadUrl = publicProcedure
.meta({
@ -45,13 +47,20 @@ export const getUploadUrl = publicProcedure
'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY',
})
const publicTypebot = (await prisma.publicTypebot.findFirst({
const publicTypebot = await prisma.publicTypebot.findFirst({
where: { typebotId },
select: {
version: true,
groups: true,
typebotId: true,
},
})) as Pick<PublicTypebot, 'groups' | 'typebotId'>
})
if (!publicTypebot)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Typebot not found',
})
const fileUploadBlock = await getFileUploadBlock(publicTypebot, blockId)
@ -73,27 +82,32 @@ export const getUploadUrl = publicProcedure
})
const getFileUploadBlock = async (
publicTypebot: Pick<PublicTypebot, 'groups' | 'typebotId'>,
publicTypebot: Pick<PublicTypebot, 'groups' | 'typebotId' | 'version'>,
blockId: string
): Promise<FileInputBlock | null> => {
const fileUploadBlock = publicTypebot.groups
.flatMap((group) => group.blocks)
const groups = parseGroups(publicTypebot.groups, {
typebotVersion: publicTypebot.version,
})
const fileUploadBlock = groups
.flatMap<Block>((group) => group.blocks)
.find(byId(blockId))
if (fileUploadBlock?.type === InputBlockType.FILE) return fileUploadBlock
const linkedTypebotIds = publicTypebot.groups
.flatMap((group) => group.blocks)
const linkedTypebotIds = groups
.flatMap<Block>((group) => group.blocks)
.filter((block) => block.type === LogicBlockType.TYPEBOT_LINK)
.flatMap((block) => (block as TypebotLinkBlock).options.typebotId)
.flatMap((block) => (block as TypebotLinkBlock).options?.typebotId)
.filter(isDefined)
const linkedTypebots = (await prisma.publicTypebot.findMany({
const linkedTypebots = await prisma.publicTypebot.findMany({
where: { typebotId: { in: linkedTypebotIds } },
select: {
groups: true,
},
})) as Pick<PublicTypebot, 'groups'>[]
const fileUploadBlockFromLinkedTypebots = linkedTypebots
.flatMap((typebot) => typebot.groups)
.flatMap((group) => group.blocks)
})
const fileUploadBlockFromLinkedTypebots = parseGroups(
linkedTypebots.flatMap((typebot) => typebot.groups),
{ typebotVersion: publicTypebot.version }
)
.flatMap<Block>((group) => group.blocks)
.find(byId(blockId))
if (fileUploadBlockFromLinkedTypebots?.type === InputBlockType.FILE)
return fileUploadBlockFromLinkedTypebots

View File

@ -3,9 +3,11 @@ import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy'
import { env } from '@typebot.io/env'
import { InputBlockType, publicTypebotSchema } from '@typebot.io/schemas'
import prisma from '@typebot.io/lib/prisma'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { parseGroups } from '@typebot.io/schemas'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { getBlockById } from '@typebot.io/lib/getBlockById'
export const generateUploadUrl = publicProcedure
.meta({
@ -56,6 +58,7 @@ export const generateUploadUrl = publicProcedure
typebotId: filePathProps.typebotId,
},
select: {
version: true,
groups: true,
typebot: {
select: {
@ -75,8 +78,9 @@ export const generateUploadUrl = publicProcedure
const filePath = `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${filePathProps.fileName}`
const fileUploadBlock = publicTypebotSchema._def.schema.shape.groups
.parse(publicTypebot.groups)
const fileUploadBlock = parseGroups(publicTypebot.groups, {
typebotVersion: publicTypebot.version,
})
.flatMap((group) => group.blocks)
.find((block) => block.id === filePathProps.blockId)
@ -90,7 +94,7 @@ export const generateUploadUrl = publicProcedure
fileType,
filePath,
maxFileSize:
fileUploadBlock.options.sizeLimit ??
fileUploadBlock.options?.sizeLimit ??
env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
})
@ -118,6 +122,7 @@ export const generateUploadUrl = publicProcedure
typebotId,
},
select: {
version: true,
groups: true,
typebot: {
select: {
@ -139,10 +144,18 @@ export const generateUploadUrl = publicProcedure
const filePath = `public/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${filePathProps.fileName}`
const fileUploadBlock = publicTypebotSchema._def.schema.shape.groups
.parse(publicTypebot.groups)
.flatMap((group) => group.blocks)
.find((block) => block.id === session.state.currentBlock?.blockId)
if (session.state.currentBlockId === undefined)
throw new TRPCError({
code: 'BAD_REQUEST',
message: "Can't find currentBlockId in session state",
})
const { block: fileUploadBlock } = getBlockById(
session.state.currentBlockId,
parseGroups(publicTypebot.groups, {
typebotVersion: publicTypebot.version,
})
)
if (fileUploadBlock?.type !== InputBlockType.FILE)
throw new TRPCError({
@ -154,8 +167,9 @@ export const generateUploadUrl = publicProcedure
fileType,
filePath,
maxFileSize:
fileUploadBlock.options.sizeLimit ??
env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
fileUploadBlock.options && 'sizeLimit' in fileUploadBlock.options
? fileUploadBlock.options.sizeLimit
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
})
return {

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,

View File

@ -4,6 +4,7 @@
"updatedAt": "2022-03-08T16:07:18.899Z",
"name": "My typebot",
"folderId": null,
"version": "4",
"groups": [
{
"id": "1qQrnsLzRim1LqCrhbj1MW",

View File

@ -3,6 +3,7 @@
"createdAt": "2022-03-08T15:58:49.720Z",
"updatedAt": "2022-03-08T16:07:18.899Z",
"name": "My typebot",
"version": "4",
"folderId": null,
"groups": [
{

View File

@ -5,6 +5,7 @@
"icon": null,
"name": "Another typebot copy",
"folderId": null,
"version": "4",
"groups": [
{
"id": "clbovazhy000q3b6o716dlfq8",

View File

@ -4,6 +4,7 @@
"updatedAt": "2022-03-23T08:41:30.106Z",
"name": "My typebot",
"folderId": null,
"version": "4",
"groups": [
{
"id": "cl13bgvlk0000t71a4wabccvw",

View File

@ -5,6 +5,7 @@
"icon": null,
"name": "My typebot copy",
"folderId": null,
"version": "4",
"groups": [
{
"id": "clbnrow4e000c3b6oycsv9cu3",

View File

@ -5,6 +5,7 @@
"icon": null,
"name": "My typebot",
"folderId": null,
"version": "4",
"groups": [
{
"id": "cl9ip9u0j0000d71a5d98gwni",

View File

@ -9,7 +9,7 @@ import {
deleteWebhooks,
importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
test.afterEach(async () => {
await deleteWebhooks(['chat-webhook-id'])

View File

@ -2,10 +2,7 @@ import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import {
defaultChatwootOptions,
IntegrationBlockType,
} from '@typebot.io/schemas'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
const typebotId = createId()
@ -19,7 +16,6 @@ test('should work as expected', async ({ page }) => {
{
type: IntegrationBlockType.CHATWOOT,
options: {
...defaultChatwootOptions,
websiteToken: chatwootTestWebsiteToken,
},
},

View File

@ -1,18 +1,13 @@
import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import {
defaultSettings,
defaultTextInputOptions,
InputBlockType,
Metadata,
} from '@typebot.io/schemas'
import {
createTypebots,
updateTypebot,
} from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
const settings = defaultSettings({ isBrandingEnabled: true })
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { Settings } from '@typebot.io/schemas'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
test('Result should be overwritten on page refresh', async ({ page }) => {
const typebotId = createId()
@ -20,9 +15,7 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
{
id: typebotId,
settings: {
...settings,
general: {
...settings.general,
rememberUser: {
isEnabled: true,
storage: 'session',
@ -31,7 +24,6 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
},
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
@ -60,7 +52,6 @@ test.describe('Create result on page refresh enabled', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
@ -88,7 +79,6 @@ test('Hide query params', async ({ page }) => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
@ -98,8 +88,7 @@ test('Hide query params', async ({ page }) => {
await updateTypebot({
id: typebotId,
settings: {
...settings,
general: { ...settings.general, isHideQueryParamsEnabled: false },
general: { isHideQueryParamsEnabled: false },
},
})
await page.goto(`/${typebotId}-public?Name=John`)
@ -116,7 +105,6 @@ test('Show close message', async ({ page }) => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
isClosed: true,
},
@ -127,7 +115,7 @@ test('Show close message', async ({ page }) => {
test('Should correctly parse metadata', async ({ page }) => {
const typebotId = createId()
const customMetadata: Metadata = {
const customMetadata: Settings['metadata'] = {
description: 'My custom description',
title: 'Custom title',
favIconUrl: 'https://www.baptistearno.com/favicon.png',
@ -138,12 +126,10 @@ test('Should correctly parse metadata', async ({ page }) => {
{
id: typebotId,
settings: {
...settings,
metadata: customMetadata,
},
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])

View File

@ -1,11 +1,11 @@
import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
import {
createWebhook,
importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions'
import { getTestAsset } from '@/test/utils/playwright'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
const typebotId = createId()