2
0

Introducing The Forge (#1072)

The Forge allows anyone to easily create their own Typebot Block.

Closes #380
This commit is contained in:
Baptiste Arnaud
2023-12-13 10:22:02 +01:00
committed by GitHub
parent c373108b55
commit 5e019bbb22
184 changed files with 42659 additions and 37411 deletions

View File

@@ -5,9 +5,9 @@ import {
} from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { filterChoiceItems } from './filterChoiceItems'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { transformStringVariablesToList } from '../../../variables/transformVariablesToList'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { transformVariablesToList } from '@typebot.io/variables/transformVariablesToList'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
export const injectVariableValuesInButtonsInputBlock =
(state: SessionState) =>
@@ -38,7 +38,7 @@ const getVariableValue =
(variable: VariableWithValue): (string | null)[] => {
if (!Array.isArray(variable.value)) {
const { variables } = state.typebotsQueue[0].typebot
const [transformedVariable] = transformStringVariablesToList(variables)([
const [transformedVariable] = transformVariablesToList(variables)([
variable.id,
])
updateVariablesInSession(state)([transformedVariable])

View File

@@ -1,7 +1,7 @@
import { getPrefilledInputValue } from '../../../getPrefilledValue'
import { DateInputBlock, SessionState, Variable } from '@typebot.io/schemas'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { parseVariables } from '../../../variables/parseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const parseDateInput =
(state: SessionState) => (block: DateInputBlock) => {

View File

@@ -1,6 +1,6 @@
import { isNotDefined } from '@typebot.io/lib'
import { NumberInputBlock, Variable } from '@typebot.io/schemas'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const validateNumber = (
inputValue: string,

View File

@@ -7,7 +7,7 @@ import {
} from '@typebot.io/schemas'
import Stripe from 'stripe'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import prisma from '@typebot.io/lib/prisma'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'

View File

@@ -5,7 +5,7 @@ import {
} from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { filterPictureChoiceItems } from './filterPictureChoiceItems'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const injectVariableValuesInPictureChoiceBlock =
(variables: Variable[]) =>

View File

@@ -2,9 +2,9 @@ import { ExecuteIntegrationResponse } from '../../../types'
import { env } from '@typebot.io/env'
import { isDefined } from '@typebot.io/lib'
import { ChatwootBlock, SessionState } from '@typebot.io/schemas'
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
import { parseVariables } from '../../../variables/parseVariables'
import { extractVariablesFromText } from '@typebot.io/variables/extractVariablesFromText'
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultChatwootOptions } from '@typebot.io/schemas/features/blocks/integrations/chatwoot/constants'
const parseSetUserCode = (

View File

@@ -8,8 +8,8 @@ import { isNotEmpty, byId, isDefined } from '@typebot.io/lib'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '../../../types'
import { matchFilter } from './helpers/matchFilter'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
export const getRow = async (
state: SessionState,

View File

@@ -1,5 +1,5 @@
import { Variable, Cell } from '@typebot.io/schemas'
import { parseVariables } from '../../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const parseCellValues =
(variables: Variable[]) =>

View File

@@ -7,7 +7,7 @@ import { parseCellValues } from './helpers/parseCellValues'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '../../../types'
import { matchFilter } from './helpers/matchFilter'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const updateRow = async (
state: SessionState,

View File

@@ -1,6 +1,6 @@
import { ExecuteIntegrationResponse } from '../../../types'
import { ExecuteIntegrationResponse } from '../../../../types'
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const executeGoogleAnalyticsBlock = (
state: SessionState,

View File

@@ -7,12 +7,12 @@ import { isNotEmpty } from '@typebot.io/lib'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import prisma from '@typebot.io/lib/prisma'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
import { ExecuteIntegrationResponse } from '../../../../types'
import { ExecuteIntegrationResponse } from '../../../../../types'
import OpenAI, { ClientOptions } from 'openai'
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
import { updateVariablesInSession } from '../../../../variables/updateVariablesInSession'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { createId } from '@paralleldrive/cuid2'
import { parseVariables } from '../../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const createSpeechOpenAI = async (
state: SessionState,

View File

@@ -15,9 +15,9 @@ import { parseChatCompletionMessages } from './parseChatCompletionMessages'
import { executeChatCompletionOpenAIRequest } from './executeChatCompletionOpenAIRequest'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
import prisma from '@typebot.io/lib/prisma'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { ExecuteIntegrationResponse } from '../../../../types'
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import {
chatCompletionMessageRoles,
defaultOpenAIOptions,

View File

@@ -1,7 +1,7 @@
import { SessionState } from '@typebot.io/schemas'
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { createChatCompletionOpenAI } from './createChatCompletionOpenAI'
import { ExecuteIntegrationResponse } from '../../../types'
import { ExecuteIntegrationResponse } from '../../../../types'
import { createSpeechOpenAI } from './audio/createSpeechOpenAI'
export const executeOpenAIBlock = async (

View File

@@ -7,7 +7,7 @@ import {
} from '@typebot.io/schemas/features/blocks/integrations/openai'
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
import { OpenAIStream } from 'ai'
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
import { ClientOptions, OpenAI } from 'openai'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'

View File

@@ -2,8 +2,8 @@ import { byId, isNotEmpty } from '@typebot.io/lib'
import { Variable, VariableWithValue } from '@typebot.io/schemas'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import type { OpenAI } from 'openai'
import { parseVariables } from '../../../variables/parseVariables'
import { transformStringVariablesToList } from '../../../variables/transformVariablesToList'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { transformVariablesToList } from '@typebot.io/variables/transformVariablesToList'
export const parseChatCompletionMessages =
(variables: Variable[]) =>
@@ -24,7 +24,7 @@ export const parseChatCompletionMessages =
)
return
variablesTransformedToList.push(
...transformStringVariablesToList(variables)([
...transformVariablesToList(variables)([
message.content.assistantMessagesVariableId,
message.content.userMessagesVariableId,
])

View File

@@ -2,7 +2,7 @@ import { byId, isDefined } from '@typebot.io/lib'
import { ContinueChatResponse, SessionState } from '@typebot.io/schemas'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { VariableWithUnknowValue } from '@typebot.io/schemas/features/typebot/variable'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
export const resumeChatCompletion =
(

View File

@@ -1,6 +1,6 @@
import { PixelBlock, SessionState } from '@typebot.io/schemas'
import { ExecuteIntegrationResponse } from '../../../types'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const executePixelBlock = (
state: SessionState,

View File

@@ -14,11 +14,11 @@ import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { defaultFrom, defaultTransportOptions } from './constants'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
import { env } from '@typebot.io/env'
import { ExecuteIntegrationResponse } from '../../../types'
import prisma from '@typebot.io/lib/prisma'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
export const executeSendEmailBlock = async (

View File

@@ -18,7 +18,7 @@ import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import got, { Method, HTTPError, OptionsInit } from 'got'
import { resumeWebhookExecution } from './resumeWebhookExecution'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import prisma from '@typebot.io/lib/prisma'
import {
HttpMethod,

View File

@@ -9,8 +9,8 @@ import {
} from '@typebot.io/schemas'
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariables } from '../../../variables/parseVariables'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
type Props = {
state: SessionState

View File

@@ -10,7 +10,7 @@ import { byId, isDefined, isEmpty } from '@typebot.io/lib'
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import prisma from '@typebot.io/lib/prisma'
import { ExecuteIntegrationResponse } from '../../../types'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
const URL = 'https://api.zemantic.ai/v1/search-documents'

View File

@@ -1,7 +1,7 @@
import { isNotDefined, isDefined } from '@typebot.io/lib'
import { Comparison, Condition, Variable } from '@typebot.io/schemas'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
import { parseVariables } from '../../../variables/parseVariables'
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import {
LogicalOperator,
ComparisonOperators,

View File

@@ -1,7 +1,7 @@
import { RedirectBlock, SessionState } from '@typebot.io/schemas'
import { sanitizeUrl } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '../../../types'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const executeRedirect = (
state: SessionState,

View File

@@ -1,8 +1,8 @@
import { ExecuteLogicResponse } from '../../../types'
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
import { parseVariables } from '../../../variables/parseVariables'
import { extractVariablesFromText } from '@typebot.io/variables/extractVariablesFromText'
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const executeScript = (
state: SessionState,

View File

@@ -2,9 +2,9 @@ import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '../../../types'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
import { parseVariables } from '../../../variables/parseVariables'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { createId } from '@paralleldrive/cuid2'
export const executeSetVariable = (

View File

@@ -1,6 +1,6 @@
import { ExecuteLogicResponse } from '../../../types'
import { SessionState, WaitBlock } from '@typebot.io/schemas'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { isNotDefined } from '@typebot.io/lib'
export const executeWait = (

View File

@@ -12,7 +12,6 @@ import { getNextGroup } from './getNextGroup'
import { validateEmail } from './blocks/inputs/email/validateEmail'
import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber'
import { validateUrl } from './blocks/inputs/url/validateUrl'
import { resumeChatCompletion } from './blocks/integrations/openai/resumeChatCompletion'
import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution'
import { upsertAnswer } from './queries/upsertAnswer'
import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply'
@@ -21,8 +20,8 @@ import { validateNumber } from './blocks/inputs/number/validateNumber'
import { parseDateReply } from './blocks/inputs/date/parseDateReply'
import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply'
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
import { parseVariables } from './variables/parseVariables'
import { updateVariablesInSession } from './variables/updateVariablesInSession'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { startBotFlow } from './startBotFlow'
import { TRPCError } from '@trpc/server'
import { parseNumber } from './blocks/inputs/number/parseNumber'
@@ -37,6 +36,9 @@ import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
import { VisitedEdge } from '@typebot.io/prisma'
import { getBlockById } from '@typebot.io/lib/getBlockById'
import { ForgedBlock, forgedBlocks } from '@typebot.io/forge-schemas'
import { enabledBlocks } from '@typebot.io/forge-repository'
import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resumeChatCompletion'
type Params = {
version: 1 | 2
@@ -80,14 +82,9 @@ export const continueBotFlow = async (
}
newSessionState = updateVariablesInSession(state)([newVariable])
}
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
const result = resumeWebhookExecution({
state,
block,
response: JSON.parse(reply),
})
if (result.newSessionState) newSessionState = result.newSessionState
} else if (
}
// Legacy
else if (
block.type === IntegrationBlockType.OPEN_AI &&
block.options?.task === 'Create chat completion'
) {
@@ -99,6 +96,58 @@ export const continueBotFlow = async (
})(reply)
newSessionState = result.newSessionState
}
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
const result = resumeWebhookExecution({
state,
block,
response: JSON.parse(reply),
})
if (result.newSessionState) newSessionState = result.newSessionState
} else if (
enabledBlocks.includes(block.type as (typeof enabledBlocks)[number])
) {
if (reply) {
const options = (block as ForgedBlock).options
const action = forgedBlocks
.find((b) => b.id === block.type)
?.actions.find((a) => a.name === options?.action)
if (action) {
if (action.run?.stream?.getStreamVariableId) {
firstBubbleWasStreamed = true
const variableToUpdate =
state.typebotsQueue[0].typebot.variables.find(
(v) => v.id === action?.run?.stream?.getStreamVariableId(options)
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
}
if (
action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId
) {
const variableToUpdate =
state.typebotsQueue[0].typebot.variables.find(
(v) =>
v.id ===
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
options
)
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
}
}
}
}
let formattedReply: string | undefined

View File

@@ -6,6 +6,7 @@ import {
SessionState,
} from '@typebot.io/schemas'
import {
createId,
isBubbleBlock,
isInputBlock,
isIntegrationBlock,
@@ -20,7 +21,7 @@ import { injectVariableValuesInButtonsInputBlock } from './blocks/inputs/buttons
import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock'
import { getPrefilledInputValue } from './getPrefilledValue'
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
import { deepParseVariables } from './variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import {
BubbleBlockWithDefinedContent,
parseBubbleBlock,
@@ -139,10 +140,21 @@ export const executeGroup = async (
lastBubbleBlockId,
})),
]
if (
'customEmbedBubble' in executionResponse &&
executionResponse.customEmbedBubble
) {
messages.push({
id: createId(),
...executionResponse.customEmbedBubble,
})
}
if (
executionResponse.clientSideActions?.find(
(action) => action.expectsDedicatedReply
)
) ||
('customEmbedBubble' in executionResponse &&
executionResponse.customEmbedBubble)
) {
return {
messages,

View File

@@ -1,14 +1,15 @@
import { executeOpenAIBlock } from './blocks/integrations/openai/executeOpenAIBlock'
import { executeSendEmailBlock } from './blocks/integrations/sendEmail/executeSendEmailBlock'
import { executeWebhookBlock } from './blocks/integrations/webhook/executeWebhookBlock'
import { executeChatwootBlock } from './blocks/integrations/chatwoot/executeChatwootBlock'
import { executeGoogleAnalyticsBlock } from './blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock'
import { executeGoogleAnalyticsBlock } from './blocks/integrations/legacy/googleAnalytics/executeGoogleAnalyticsBlock'
import { executeGoogleSheetBlock } from './blocks/integrations/googleSheets/executeGoogleSheetBlock'
import { executePixelBlock } from './blocks/integrations/pixel/executePixelBlock'
import { executeZemanticAiBlock } from './blocks/integrations/zemanticAi/executeZemanticAiBlock'
import { IntegrationBlock, SessionState } from '@typebot.io/schemas'
import { ExecuteIntegrationResponse } from './types'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { executeOpenAIBlock } from './blocks/integrations/legacy/openai/executeOpenAIBlock'
import { executeForgedBlock } from './forge/executeForgedBlock'
export const executeIntegration =
(state: SessionState) =>
@@ -33,5 +34,7 @@ export const executeIntegration =
return executePixelBlock(state, block)
case IntegrationBlockType.ZEMANTIC_AI:
return executeZemanticAiBlock(state, block)
default:
return executeForgedBlock(state, block)
}
}

View File

@@ -0,0 +1,207 @@
import { VariableStore, LogsStore } from '@typebot.io/forge'
import { ForgedBlock, forgedBlocks } from '@typebot.io/forge-schemas'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
import prisma from '@typebot.io/lib/prisma'
import {
SessionState,
ContinueChatResponse,
Block,
TypebotInSession,
} from '@typebot.io/schemas'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import {
ParseVariablesOptions,
parseVariables,
} from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { ExecuteIntegrationResponse } from '../types'
import { byId } from '@typebot.io/lib'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
export const executeForgedBlock = async (
state: SessionState,
block: ForgedBlock
): Promise<ExecuteIntegrationResponse> => {
const blockDef = forgedBlocks.find((b) => b.id === block.type)
if (!blockDef) return { outgoingEdgeId: block.outgoingEdgeId }
const action = blockDef.actions.find((a) => a.name === block.options.action)
const noCredentialsError = {
status: 'error',
description: 'Credentials not provided for integration',
}
let credentials: { data: string; iv: string } | null = null
if (blockDef.auth) {
if (!block.options.credentialsId) {
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [noCredentialsError],
}
}
credentials = await prisma.credentials.findUnique({
where: {
id: block.options.credentialsId,
},
})
if (!credentials) {
console.error('Could not find credentials in database')
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [noCredentialsError],
}
}
}
const typebot = state.typebotsQueue[0].typebot
if (
action?.run?.stream &&
isPlaneteScale() &&
credentials &&
isCredentialsV2(credentials) &&
state.isStreamEnabled &&
!state.whatsApp &&
isNextBubbleTextWithStreamingVar(typebot)(
block.id,
action.run.stream.getStreamVariableId(block.options)
)
) {
return {
outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [
{
expectsDedicatedReply: true,
stream: true,
},
],
}
}
let newSessionState = state
const variables: VariableStore = {
get: (id: string) => {
const variable = newSessionState.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === id
)
return variable?.value
},
set: (id: string, value: unknown) => {
const variable = newSessionState.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === id
)
if (!variable) return
newSessionState = updateVariablesInSession(newSessionState)([
{ ...variable, value },
])
},
parse: (text: string, params?: ParseVariablesOptions) =>
parseVariables(
newSessionState.typebotsQueue[0].typebot.variables,
params
)(text),
}
let logs: NonNullable<ContinueChatResponse['logs']> = []
const logsStore: LogsStore = {
add: (log) => {
if (typeof log === 'string') {
logs.push({
status: 'error',
description: log,
})
return
}
logs.push(log)
},
}
const credentialsData = credentials
? await decrypt(credentials.data, credentials.iv)
: undefined
const parsedOptions = deepParseVariables(
state.typebotsQueue[0].typebot.variables
)(block.options)
await action?.run?.server?.({
credentials: credentialsData ?? {},
options: parsedOptions,
variables,
logs: logsStore,
})
const clientSideActions: ExecuteIntegrationResponse['clientSideActions'] = []
if (
action?.run?.web?.parseFunction &&
(state.typebotsQueue[0].resultId || !blockDef.isDisabledInPreview)
) {
clientSideActions.push({
codeToExecute: action?.run?.web?.parseFunction({
options: parsedOptions,
}),
})
}
return {
newSessionState,
outgoingEdgeId: block.outgoingEdgeId,
logs,
clientSideActions,
customEmbedBubble: action?.run?.web?.displayEmbedBubble
? {
type: 'custom-embed',
content: {
initFunction: action.run.web.displayEmbedBubble.parseInitFunction({
options: parsedOptions,
}),
waitForEventFunction:
action.run.web.displayEmbedBubble.waitForEvent?.parseFunction?.({
options: parsedOptions,
}),
},
}
: undefined,
}
}
const isNextBubbleTextWithStreamingVar =
(typebot: TypebotInSession) =>
(blockId: string, streamVariableId?: string): boolean => {
const streamVariable = typebot.variables.find(
(variable) => variable.id === streamVariableId
)
if (!streamVariable) return false
const nextBlock = getNextBlock(typebot)(blockId)
if (!nextBlock) return false
return (
nextBlock.type === BubbleBlockType.TEXT &&
(nextBlock.content?.richText?.length ?? 0) > 0 &&
nextBlock.content?.richText?.at(0)?.children.at(0).text ===
`{{${streamVariable.name}}}`
)
}
const getNextBlock =
(typebot: TypebotInSession) =>
(blockId: string): Block | undefined => {
const group = typebot.groups.find((group) =>
group.blocks.find(byId(blockId))
)
if (!group) return
const blockIndex = group.blocks.findIndex(byId(blockId))
const nextBlockInGroup = group.blocks.at(blockIndex + 1)
if (nextBlockInGroup) return nextBlockInGroup
const outgoingEdgeId = group.blocks.at(blockIndex)?.outgoingEdgeId
if (!outgoingEdgeId) return
const outgoingEdge = typebot.edges.find(byId(outgoingEdgeId))
if (!outgoingEdge) return
const connectedGroup = typebot.groups.find(byId(outgoingEdge?.to.groupId))
if (!connectedGroup) return
return outgoingEdge.to.blockId
? connectedGroup.blocks.find(
(block) => block.id === outgoingEdge.to.blockId
)
: connectedGroup?.blocks.at(0)
}
const isCredentialsV2 = (credentials: { iv: string }) =>
credentials.iv.length === 24

View File

@@ -16,6 +16,7 @@
"@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*",
"@udecode/plate-common": "21.1.5",
"@udecode/plate-serializer-md": "24.4.0",
"ai": "2.2.24",
@@ -34,6 +35,9 @@
},
"devDependencies": {
"@types/nodemailer": "6.4.8",
"@types/qs": "6.9.7"
"@types/qs": "6.9.7",
"@typebot.io/forge-schemas": "workspace:*",
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*"
}
}

View File

@@ -5,12 +5,12 @@ import {
ContinueChatResponse,
Typebot,
} from '@typebot.io/schemas'
import { deepParseVariables } from './variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import {
getVariablesToParseInfoInText,
parseVariables,
} from './variables/parseVariables'
} from '@typebot.io/variables/parseVariables'
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
import {
createDeserializeMdPlugin,

View File

@@ -1,5 +1,5 @@
import { SessionState, ContinueChatResponse } from '@typebot.io/schemas'
import { parseVariables } from './variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const parseDynamicTheme = (
state: SessionState | undefined

View File

@@ -24,18 +24,20 @@ import { findTypebot } from './queries/findTypebot'
import { findPublicTypebot } from './queries/findPublicTypebot'
import { findResult } from './queries/findResult'
import { startBotFlow } from './startBotFlow'
import { prefillVariables } from './variables/prefillVariables'
import { deepParseVariables } from './variables/deepParseVariables'
import { injectVariablesFromExistingResult } from './variables/injectVariablesFromExistingResult'
import { prefillVariables } from '@typebot.io/variables/prefillVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { injectVariablesFromExistingResult } from '@typebot.io/variables/injectVariablesFromExistingResult'
import { getNextGroup } from './getNextGroup'
import { upsertResult } from './queries/upsertResult'
import { continueBotFlow } from './continueBotFlow'
import { parseVariables } from './variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { VisitedEdge } from '@typebot.io/prisma'
import { env } from '@typebot.io/env'
import { forgedBlocks } from '@typebot.io/forge-schemas'
import { FunctionToExecute } from '@typebot.io/forge'
type StartParams =
| ({
@@ -425,9 +427,7 @@ const parseStartClientSideAction = (
)
return
return {
startPropsToInject,
}
return { startPropsToInject }
}
const sanitizeAndParseTheme = (

View File

@@ -1,4 +1,8 @@
import { ContinueChatResponse, SessionState } from '@typebot.io/schemas'
import {
ContinueChatResponse,
CustomEmbedBubble,
SessionState,
} from '@typebot.io/schemas'
export type EdgeId = string
@@ -11,6 +15,7 @@ export type ExecuteIntegrationResponse = {
outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState
startTimeShouldBeUpdated?: boolean
customEmbedBubble?: CustomEmbedBubble
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
export type ParsedReply =

View File

@@ -1,65 +0,0 @@
import { Variable } from '@typebot.io/schemas'
import {
defaultParseVariablesOptions,
parseVariables,
ParseVariablesOptions,
} from './parseVariables'
import { parseGuessedTypeFromString } from './parseGuessedTypeFromString'
type DeepParseOptions = {
guessCorrectTypes?: boolean
removeEmptyStrings?: boolean
}
export const deepParseVariables =
(
variables: Variable[],
deepParseOptions: DeepParseOptions = {
guessCorrectTypes: false,
removeEmptyStrings: false,
},
parseVariablesOptions: ParseVariablesOptions = defaultParseVariablesOptions
) =>
<T extends Record<string, unknown>>(object: T): T =>
Object.keys(object).reduce<T>((newObj, key) => {
const currentValue = object[key]
if (typeof currentValue === 'string') {
const parsedVariable = parseVariables(
variables,
parseVariablesOptions
)(currentValue)
if (deepParseOptions.removeEmptyStrings && parsedVariable === '')
return newObj
return {
...newObj,
[key]: deepParseOptions.guessCorrectTypes
? parseGuessedTypeFromString(parsedVariable)
: parsedVariable,
}
}
if (currentValue instanceof Object && currentValue.constructor === Object)
return {
...newObj,
[key]: deepParseVariables(
variables,
deepParseOptions,
parseVariablesOptions
)(currentValue as Record<string, unknown>),
}
if (currentValue instanceof Array)
return {
...newObj,
[key]: currentValue.map(
deepParseVariables(
variables,
deepParseOptions,
parseVariablesOptions
)
),
}
return { ...newObj, [key]: currentValue }
}, {} as T)

View File

@@ -1,19 +0,0 @@
import { Variable } from '@typebot.io/schemas'
export const extractVariablesFromText =
(variables: Variable[]) =>
(text: string): Variable[] => {
const matches = [...text.matchAll(/\{\{(.*?)\}\}/g)]
return matches.reduce<Variable[]>((acc, match) => {
const variableName = match[1]
const variable = variables.find(
(variable) => variable.name === variableName
)
if (
!variable ||
acc.find((accVariable) => accVariable.id === variable.id)
)
return acc
return [...acc, variable]
}, [])
}

View File

@@ -1,12 +0,0 @@
import { Variable } from '@typebot.io/schemas'
export const findUniqueVariableValue =
(variables: Variable[]) =>
(value: string | undefined): Variable['value'] => {
if (!value || !value.startsWith('{{') || !value.endsWith('}}')) return null
const variableName = value.slice(2, -2)
const variable = variables.find(
(variable) => variable.name === variableName
)
return variable?.value ?? null
}

View File

@@ -1 +0,0 @@
export const hasVariable = (str: string): boolean => /\{\{(.*?)\}\}/g.test(str)

View File

@@ -1,17 +0,0 @@
import { Result, Variable } from '@typebot.io/schemas'
export const injectVariablesFromExistingResult = (
variables: Variable[],
resultVariables: Result['variables']
): Variable[] =>
variables.map((variable) => {
const resultVariable = resultVariables.find(
(resultVariable) =>
resultVariable.name === variable.name && !variable.value
)
if (!resultVariable) return variable
return {
...variable,
value: resultVariable.value,
}
})

View File

@@ -1,12 +0,0 @@
export const parseGuessedTypeFromString = (value: string): unknown => {
if (value === 'undefined') return undefined
return safeJsonParse(value)
}
const safeJsonParse = (value: string): unknown => {
try {
return JSON.parse(value)
} catch {
return value
}
}

View File

@@ -1,22 +0,0 @@
import { Variable } from '@typebot.io/schemas'
export const parseGuessedValueType = (
value: Variable['value']
): string | (string | null)[] | boolean | number | null | undefined => {
if (value === null) return null
if (value === undefined) return undefined
if (typeof value !== 'string') return value
const isStartingWithZero =
value.startsWith('0') && !value.startsWith('0.') && value.length > 1
if (typeof value === 'string' && isStartingWithZero) return value
const isStartingWithPlus = value.startsWith('+')
if (typeof value === 'string' && isStartingWithPlus) return value
if (typeof value === 'number') return value
if (value === 'true') return true
if (value === 'false') return false
if (value === 'null') return null
if (value === 'undefined') return undefined
// isNaN works with strings
if (isNaN(value as unknown as number)) return value
return Number(value)
}

View File

@@ -1,12 +0,0 @@
import { Variable } from '@typebot.io/schemas'
import { parseGuessedValueType } from './parseGuessedValueType'
import { parseVariables } from './parseVariables'
export const parseVariableNumber =
(variables: Variable[]) =>
(input: number | `{{${string}}}` | undefined): number | undefined => {
if (typeof input === 'number' || input === undefined) return input
const parsedInput = parseGuessedValueType(parseVariables(variables)(input))
if (typeof parsedInput !== 'number') return undefined
return parsedInput
}

View File

@@ -1,158 +0,0 @@
import { safeStringify } from '@typebot.io/lib/safeStringify'
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
import { Variable, VariableWithValue } from '@typebot.io/schemas'
import { parseGuessedValueType } from './parseGuessedValueType'
export type ParseVariablesOptions = {
fieldToParse?: 'value' | 'id'
isInsideJson?: boolean
takeLatestIfList?: boolean
isInsideHtml?: boolean
}
export const defaultParseVariablesOptions: ParseVariablesOptions = {
fieldToParse: 'value',
isInsideJson: false,
takeLatestIfList: false,
isInsideHtml: false,
}
// {{= inline code =}}
const inlineCodeRegex = /\{\{=(.+?)=\}\}/g
// {{variable}} and ${{{variable}}}
const variableRegex = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g
export const parseVariables =
(
variables: Variable[],
options: ParseVariablesOptions = defaultParseVariablesOptions
) =>
(text: string | undefined): string => {
if (!text || text === '') return ''
const textWithInlineCodeParsed = text.replace(
inlineCodeRegex,
(_full, inlineCodeToEvaluate) =>
evaluateInlineCode(inlineCodeToEvaluate, { variables })
)
return textWithInlineCodeParsed.replace(
variableRegex,
(_full, nameInCurlyBraces, _dollarSign, nameInTemplateLitteral) => {
const dollarSign = (_dollarSign ?? '') as string
const matchedVarName = nameInCurlyBraces ?? nameInTemplateLitteral
const variable = variables.find((variable) => {
return (
matchedVarName === variable.name &&
(options.fieldToParse === 'id' || isDefined(variable.value))
)
}) as VariableWithValue | undefined
if (!variable) return dollarSign + ''
if (options.fieldToParse === 'id') return dollarSign + variable.id
const { value } = variable
if (options.isInsideJson)
return dollarSign + parseVariableValueInJson(value)
const parsedValue =
dollarSign +
safeStringify(
options.takeLatestIfList && Array.isArray(value)
? value[value.length - 1]
: value
)
if (!parsedValue) return dollarSign + ''
if (options.isInsideHtml) return parseVariableValueInHtml(parsedValue)
return parsedValue
}
)
}
const evaluateInlineCode = (
code: string,
{ variables }: { variables: Variable[] }
) => {
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
code.includes('return ') ? code : `return ${code}`
)
try {
const func = Function(...variables.map((v) => v.id), evaluating)
return func(...variables.map((v) => parseGuessedValueType(v.value)))
} catch (err) {
console.log(err)
return parseVariables(variables)(code)
}
}
type VariableToParseInformation = {
startIndex: number
endIndex: number
textToReplace: string
value: string
}
export const getVariablesToParseInfoInText = (
text: string,
{
variables,
takeLatestIfList,
}: { variables: Variable[]; takeLatestIfList?: boolean }
): VariableToParseInformation[] => {
const variablesToParseInfo: VariableToParseInformation[] = []
const inlineCodeMatches = [...text.matchAll(inlineCodeRegex)]
inlineCodeMatches.forEach((match) => {
if (isNotDefined(match.index) || !match[0].length) return
const inlineCodeToEvaluate = match[1]
const evaluatedValue = evaluateInlineCode(inlineCodeToEvaluate, {
variables,
})
variablesToParseInfo.push({
startIndex: match.index,
endIndex: match.index + match[0].length,
textToReplace: match[0],
value:
safeStringify(
takeLatestIfList && Array.isArray(evaluatedValue)
? evaluatedValue[evaluatedValue.length - 1]
: evaluatedValue
) ?? '',
})
})
const textWithInlineCodeParsed = text.replace(
inlineCodeRegex,
(_full, inlineCodeToEvaluate) =>
evaluateInlineCode(inlineCodeToEvaluate, { variables })
)
const variableMatches = [...textWithInlineCodeParsed.matchAll(variableRegex)]
variableMatches.forEach((match) => {
if (isNotDefined(match.index) || !match[0].length) return
const matchedVarName = match[1] ?? match[3]
const variable = variables.find((variable) => {
return matchedVarName === variable.name && isDefined(variable.value)
}) as VariableWithValue | undefined
variablesToParseInfo.push({
startIndex: match.index,
endIndex: match.index + match[0].length,
textToReplace: match[0],
value:
safeStringify(
takeLatestIfList && Array.isArray(variable?.value)
? variable?.value[variable?.value.length - 1]
: variable?.value
) ?? '',
})
})
return variablesToParseInfo.sort((a, b) => a.startIndex - b.startIndex)
}
const parseVariableValueInJson = (value: VariableWithValue['value']) => {
const stringifiedValue = JSON.stringify(value)
if (typeof value === 'string') return stringifiedValue.slice(1, -1)
return stringifiedValue
}
const parseVariableValueInHtml = (
value: VariableWithValue['value']
): string => {
if (typeof value === 'string')
return value.replace(/</g, '&lt;').replace(/>/g, '&gt;')
return JSON.stringify(value).replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

View File

@@ -1,15 +0,0 @@
import { safeStringify } from '@typebot.io/lib/safeStringify'
import { StartChatInput, Variable } from '@typebot.io/schemas'
export const prefillVariables = (
variables: Variable[],
prefilledVariables: NonNullable<StartChatInput['prefilledVariables']>
): Variable[] =>
variables.map((variable) => {
const prefilledVariable = prefilledVariables[variable.name]
if (!prefilledVariable) return variable
return {
...variable,
value: safeStringify(prefilledVariable),
}
})

View File

@@ -1,26 +0,0 @@
import { isNotDefined } from '@typebot.io/lib/utils'
import { Variable, VariableWithValue } from '@typebot.io/schemas'
export const transformStringVariablesToList =
(variables: Variable[]) =>
(variableIds: string[]): VariableWithValue[] => {
const newVariables = variables.reduce<VariableWithValue[]>(
(variables, variable) => {
if (
!variableIds.includes(variable.id) ||
isNotDefined(variable.value) ||
typeof variable.value !== 'string'
)
return variables
return [
...variables,
{
...variable,
value: [variable.value],
},
]
},
[]
)
return newVariables
}

View File

@@ -1,45 +0,0 @@
import { safeStringify } from '@typebot.io/lib/safeStringify'
import {
SessionState,
VariableWithUnknowValue,
Variable,
} from '@typebot.io/schemas'
export const updateVariablesInSession =
(state: SessionState) =>
(newVariables: VariableWithUnknowValue[]): SessionState => ({
...state,
typebotsQueue: state.typebotsQueue.map((typebotInQueue, index) =>
index === 0
? {
...typebotInQueue,
typebot: {
...typebotInQueue.typebot,
variables: updateTypebotVariables(typebotInQueue.typebot)(
newVariables
),
},
}
: typebotInQueue
),
})
const updateTypebotVariables =
(typebot: { variables: Variable[] }) =>
(newVariables: VariableWithUnknowValue[]): Variable[] => {
const serializedNewVariables = newVariables.map((variable) => ({
...variable,
value: Array.isArray(variable.value)
? variable.value.map(safeStringify)
: safeStringify(variable.value),
}))
return [
...typebot.variables.filter((existingVariable) =>
serializedNewVariables.every(
(newVariable) => existingVariable.id !== newVariable.id
)
),
...serializedNewVariables,
]
}