2022-11-29 10:02:40 +01:00
|
|
|
import { validateButtonInput } from '@/features/blocks/inputs/buttons/api'
|
|
|
|
|
import { validateEmail } from '@/features/blocks/inputs/email/api'
|
2022-12-22 17:02:34 +01:00
|
|
|
import {
|
|
|
|
|
formatPhoneNumber,
|
|
|
|
|
validatePhoneNumber,
|
|
|
|
|
} from '@/features/blocks/inputs/phone/api'
|
2022-11-29 10:02:40 +01:00
|
|
|
import { validateUrl } from '@/features/blocks/inputs/url/api'
|
2022-12-22 17:02:34 +01:00
|
|
|
import { parseVariables } from '@/features/variables'
|
2022-11-29 10:02:40 +01:00
|
|
|
import prisma from '@/lib/prisma'
|
|
|
|
|
import { TRPCError } from '@trpc/server'
|
2023-01-04 15:48:57 +01:00
|
|
|
import got from 'got'
|
2022-11-29 10:02:40 +01:00
|
|
|
import {
|
|
|
|
|
Block,
|
2022-12-22 17:02:34 +01:00
|
|
|
BlockType,
|
2022-11-29 10:02:40 +01:00
|
|
|
BubbleBlockType,
|
|
|
|
|
ChatReply,
|
|
|
|
|
InputBlock,
|
|
|
|
|
InputBlockType,
|
|
|
|
|
SessionState,
|
|
|
|
|
Variable,
|
|
|
|
|
} from 'models'
|
2023-01-04 15:48:57 +01:00
|
|
|
import { isInputBlock, isNotDefined } from 'utils'
|
2022-11-29 10:02:40 +01:00
|
|
|
import { executeGroup } from './executeGroup'
|
|
|
|
|
import { getNextGroup } from './getNextGroup'
|
|
|
|
|
|
|
|
|
|
export const continueBotFlow =
|
|
|
|
|
(state: SessionState) =>
|
|
|
|
|
async (
|
2022-12-22 17:02:34 +01:00
|
|
|
reply?: string
|
2022-11-29 10:02:40 +01:00
|
|
|
): Promise<ChatReply & { newSessionState?: SessionState }> => {
|
|
|
|
|
const group = state.typebot.groups.find(
|
|
|
|
|
(group) => group.id === state.currentBlock?.groupId
|
|
|
|
|
)
|
|
|
|
|
const blockIndex =
|
|
|
|
|
group?.blocks.findIndex(
|
|
|
|
|
(block) => block.id === state.currentBlock?.blockId
|
|
|
|
|
) ?? -1
|
|
|
|
|
|
2022-12-22 17:02:34 +01:00
|
|
|
const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null
|
2022-11-29 10:02:40 +01:00
|
|
|
|
|
|
|
|
if (!block || !group)
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'INTERNAL_SERVER_ERROR',
|
|
|
|
|
message: 'Current block not found',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!isInputBlock(block))
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'INTERNAL_SERVER_ERROR',
|
|
|
|
|
message: 'Current block is not an input block',
|
|
|
|
|
})
|
|
|
|
|
|
2022-12-22 17:02:34 +01:00
|
|
|
const formattedReply = formatReply(reply, block.type)
|
|
|
|
|
|
2023-01-27 18:32:20 +01:00
|
|
|
if (
|
|
|
|
|
!formattedReply ||
|
|
|
|
|
!isReplyValid(formattedReply, block) ||
|
|
|
|
|
(!formatReply && !canSkip(block.type))
|
|
|
|
|
)
|
2022-12-22 17:02:34 +01:00
|
|
|
return parseRetryMessage(block)
|
2022-11-29 10:02:40 +01:00
|
|
|
|
2022-12-22 17:02:34 +01:00
|
|
|
const newVariables = await processAndSaveAnswer(
|
|
|
|
|
state,
|
|
|
|
|
block
|
|
|
|
|
)(formattedReply)
|
2022-11-29 10:02:40 +01:00
|
|
|
|
|
|
|
|
const newSessionState = {
|
|
|
|
|
...state,
|
|
|
|
|
typebot: {
|
|
|
|
|
...state.typebot,
|
|
|
|
|
variables: newVariables,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
|
|
|
|
|
|
2022-12-22 17:02:34 +01:00
|
|
|
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
|
|
|
|
|
|
|
|
|
|
if (groupHasMoreBlocks && !nextEdgeId) {
|
2022-11-29 10:02:40 +01:00
|
|
|
return executeGroup(newSessionState)({
|
|
|
|
|
...group,
|
|
|
|
|
blocks: group.blocks.slice(blockIndex + 1),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!nextEdgeId && state.linkedTypebots.queue.length === 0)
|
|
|
|
|
return { messages: [] }
|
|
|
|
|
|
|
|
|
|
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
|
|
|
|
|
|
|
|
|
|
if (!nextGroup) return { messages: [] }
|
|
|
|
|
|
|
|
|
|
return executeGroup(newSessionState)(nextGroup.group)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const processAndSaveAnswer =
|
2023-01-04 15:48:57 +01:00
|
|
|
(
|
|
|
|
|
state: Pick<SessionState, 'result' | 'typebot' | 'isPreview'>,
|
|
|
|
|
block: InputBlock
|
|
|
|
|
) =>
|
2022-11-29 10:02:40 +01:00
|
|
|
async (reply: string): Promise<Variable[]> => {
|
2023-01-04 15:48:57 +01:00
|
|
|
state.result &&
|
|
|
|
|
!state.isPreview &&
|
|
|
|
|
(await saveAnswer(state.result.id, block)(reply))
|
2022-11-29 10:02:40 +01:00
|
|
|
const newVariables = saveVariableValueIfAny(state, block)(reply)
|
|
|
|
|
return newVariables
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const saveVariableValueIfAny =
|
|
|
|
|
(state: Pick<SessionState, 'result' | 'typebot'>, block: InputBlock) =>
|
|
|
|
|
(reply: string): Variable[] => {
|
|
|
|
|
if (!block.options.variableId) return state.typebot.variables
|
|
|
|
|
const variable = state.typebot.variables.find(
|
|
|
|
|
(variable) => variable.id === block.options.variableId
|
|
|
|
|
)
|
|
|
|
|
if (!variable) return state.typebot.variables
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
...state.typebot.variables.filter(
|
|
|
|
|
(variable) => variable.id !== block.options.variableId
|
|
|
|
|
),
|
|
|
|
|
{
|
|
|
|
|
...variable,
|
|
|
|
|
value: reply,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-22 17:02:34 +01:00
|
|
|
const parseRetryMessage = (
|
|
|
|
|
block: InputBlock
|
|
|
|
|
): Pick<ChatReply, 'messages' | 'input'> => {
|
|
|
|
|
const retryMessage =
|
|
|
|
|
'retryMessageContent' in block.options && block.options.retryMessageContent
|
|
|
|
|
? block.options.retryMessageContent
|
|
|
|
|
: 'Invalid message. Please, try again.'
|
|
|
|
|
return {
|
|
|
|
|
messages: [
|
|
|
|
|
{
|
2023-01-27 10:54:59 +01:00
|
|
|
id: block.id,
|
2022-12-22 17:02:34 +01:00
|
|
|
type: BubbleBlockType.TEXT,
|
|
|
|
|
content: {
|
|
|
|
|
plainText: retryMessage,
|
|
|
|
|
html: `<div>${retryMessage}</div>`,
|
|
|
|
|
},
|
2022-11-29 10:02:40 +01:00
|
|
|
},
|
2022-12-22 17:02:34 +01:00
|
|
|
],
|
|
|
|
|
input: block,
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-29 10:02:40 +01:00
|
|
|
|
|
|
|
|
const saveAnswer =
|
|
|
|
|
(resultId: string, block: InputBlock) => async (reply: string) => {
|
2023-01-04 15:48:57 +01:00
|
|
|
const answer = {
|
|
|
|
|
resultId: resultId,
|
|
|
|
|
blockId: block.id,
|
|
|
|
|
groupId: block.groupId,
|
|
|
|
|
content: reply,
|
|
|
|
|
variableId: block.options.variableId,
|
|
|
|
|
storageUsed: 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (reply.includes('http') && block.type === InputBlockType.FILE) {
|
|
|
|
|
answer.storageUsed = await computeStorageUsed(reply)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await prisma.answer.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
resultId_blockId_groupId: {
|
|
|
|
|
resultId,
|
|
|
|
|
groupId: block.groupId,
|
|
|
|
|
blockId: block.id,
|
|
|
|
|
},
|
2022-11-29 10:02:40 +01:00
|
|
|
},
|
2023-01-04 15:48:57 +01:00
|
|
|
create: answer,
|
|
|
|
|
update: answer,
|
2022-11-29 10:02:40 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-04 15:48:57 +01:00
|
|
|
const computeStorageUsed = async (reply: string) => {
|
|
|
|
|
let storageUsed = 0
|
|
|
|
|
const fileUrls = reply.split(', ')
|
|
|
|
|
const hasReachedStorageLimit = fileUrls[0] === null
|
|
|
|
|
if (!hasReachedStorageLimit) {
|
|
|
|
|
for (const url of fileUrls) {
|
|
|
|
|
const { headers } = await got(url)
|
|
|
|
|
const size = headers['content-length']
|
|
|
|
|
if (isNotDefined(size)) continue
|
|
|
|
|
storageUsed += parseInt(size, 10)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return storageUsed
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-22 17:02:34 +01:00
|
|
|
const getOutgoingEdgeId =
|
|
|
|
|
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) =>
|
|
|
|
|
(block: InputBlock, reply?: string) => {
|
|
|
|
|
if (
|
|
|
|
|
block.type === InputBlockType.CHOICE &&
|
|
|
|
|
!block.options.isMultipleChoice &&
|
|
|
|
|
reply
|
|
|
|
|
) {
|
|
|
|
|
const matchedItem = block.items.find(
|
|
|
|
|
(item) => parseVariables(variables)(item.content) === reply
|
|
|
|
|
)
|
|
|
|
|
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId
|
|
|
|
|
}
|
|
|
|
|
return block.outgoingEdgeId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const formatReply = (
|
|
|
|
|
inputValue: string | undefined,
|
|
|
|
|
blockType: BlockType
|
|
|
|
|
): string | null => {
|
|
|
|
|
if (!inputValue) return null
|
|
|
|
|
switch (blockType) {
|
|
|
|
|
case InputBlockType.PHONE:
|
|
|
|
|
return formatPhoneNumber(inputValue)
|
|
|
|
|
}
|
|
|
|
|
return inputValue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const isReplyValid = (inputValue: string, block: Block): boolean => {
|
2022-11-29 10:02:40 +01:00
|
|
|
switch (block.type) {
|
|
|
|
|
case InputBlockType.EMAIL:
|
|
|
|
|
return validateEmail(inputValue)
|
|
|
|
|
case InputBlockType.PHONE:
|
|
|
|
|
return validatePhoneNumber(inputValue)
|
|
|
|
|
case InputBlockType.URL:
|
|
|
|
|
return validateUrl(inputValue)
|
|
|
|
|
case InputBlockType.CHOICE:
|
2023-01-25 16:43:25 +01:00
|
|
|
if (block.options.isMultipleChoice) return true
|
2022-11-29 10:02:40 +01:00
|
|
|
return validateButtonInput(block, inputValue)
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
2023-01-27 18:32:20 +01:00
|
|
|
|
|
|
|
|
export const canSkip = (inputType: InputBlockType) =>
|
|
|
|
|
inputType === InputBlockType.FILE
|