@ -4,11 +4,12 @@ import {
|
||||
ContinueChatResponse,
|
||||
Group,
|
||||
InputBlock,
|
||||
Message,
|
||||
SessionState,
|
||||
SetVariableHistoryItem,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { byId, isDefined } from '@typebot.io/lib'
|
||||
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
||||
import { executeGroup, parseInput } from './executeGroup'
|
||||
import { getNextGroup } from './getNextGroup'
|
||||
@ -42,10 +43,9 @@ import { ForgedBlock } from '@typebot.io/forge-repository/types'
|
||||
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
|
||||
import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resumeChatCompletion'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { downloadMedia } from './whatsapp/downloadMedia'
|
||||
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
|
||||
import { isURL } from '@typebot.io/lib/validators/isURL'
|
||||
import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers'
|
||||
import { resetSessionState } from './resetSessionState'
|
||||
|
||||
type Params = {
|
||||
version: 1 | 2
|
||||
@ -69,7 +69,11 @@ export const continueBotFlow = async (
|
||||
const setVariableHistory: SetVariableHistoryItem[] = []
|
||||
|
||||
if (!newSessionState.currentBlockId)
|
||||
return startBotFlow({ state, version, textBubbleContentFormat })
|
||||
return startBotFlow({
|
||||
state: resetSessionState(newSessionState),
|
||||
version,
|
||||
textBubbleContentFormat,
|
||||
})
|
||||
|
||||
const { block, group, blockIndex } = getBlockById(
|
||||
newSessionState.currentBlockId,
|
||||
@ -161,7 +165,7 @@ export const continueBotFlow = async (
|
||||
newVariables: [
|
||||
{
|
||||
...variableToUpdate,
|
||||
value: safeJsonParse(reply as string),
|
||||
value: reply?.text ? safeJsonParse(reply?.text) : undefined,
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -187,7 +191,14 @@ export const continueBotFlow = async (
|
||||
|
||||
formattedReply =
|
||||
'reply' in parsedReplyResult ? parsedReplyResult.reply : undefined
|
||||
newSessionState = await processAndSaveAnswer(state, block)(formattedReply)
|
||||
newSessionState = await processAndSaveAnswer(
|
||||
state,
|
||||
block
|
||||
)(
|
||||
isDefined(formattedReply)
|
||||
? { ...reply, type: 'text', text: formattedReply }
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
|
||||
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
|
||||
@ -267,37 +278,92 @@ export const continueBotFlow = async (
|
||||
|
||||
const processAndSaveAnswer =
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
async (reply: string | undefined): Promise<SessionState> => {
|
||||
async (reply: Message | undefined): Promise<SessionState> => {
|
||||
if (!reply) return state
|
||||
let newState = await saveAnswerInDb(state, block)(reply)
|
||||
return newState
|
||||
return saveAnswerInDb(state, block)(reply)
|
||||
}
|
||||
|
||||
const saveVariableValueIfAny =
|
||||
const saveVariablesValueIfAny =
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
(reply: string): SessionState => {
|
||||
(reply: Message): SessionState => {
|
||||
if (!block.options?.variableId) return state
|
||||
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === block.options?.variableId
|
||||
)
|
||||
if (!foundVariable) return state
|
||||
|
||||
const { updatedState } = updateVariablesInSession({
|
||||
newVariables: [
|
||||
{
|
||||
...foundVariable,
|
||||
value: Array.isArray(foundVariable.value)
|
||||
? foundVariable.value.concat(reply)
|
||||
: reply,
|
||||
},
|
||||
],
|
||||
currentBlockId: undefined,
|
||||
state,
|
||||
})
|
||||
|
||||
return updatedState
|
||||
const newSessionState = saveAttachmentsVarIfAny({ block, reply, state })
|
||||
return saveInputVarIfAny({ block, reply, state: newSessionState })
|
||||
}
|
||||
|
||||
const saveAttachmentsVarIfAny = ({
|
||||
block,
|
||||
reply,
|
||||
state,
|
||||
}: {
|
||||
block: InputBlock
|
||||
reply: Message
|
||||
state: SessionState
|
||||
}): SessionState => {
|
||||
if (
|
||||
block.type !== InputBlockType.TEXT ||
|
||||
!block.options?.attachments?.isEnabled ||
|
||||
!block.options?.attachments?.saveVariableId ||
|
||||
!reply.attachedFileUrls ||
|
||||
reply.attachedFileUrls.length === 0
|
||||
)
|
||||
return state
|
||||
|
||||
const variable = state.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === block.options?.attachments?.saveVariableId
|
||||
)
|
||||
|
||||
if (!variable) return state
|
||||
|
||||
const { updatedState } = updateVariablesInSession({
|
||||
newVariables: [
|
||||
{
|
||||
id: variable.id,
|
||||
name: variable.name,
|
||||
value: Array.isArray(variable.value)
|
||||
? variable.value.concat(reply.attachedFileUrls)
|
||||
: reply.attachedFileUrls.length === 1
|
||||
? reply.attachedFileUrls[0]
|
||||
: reply.attachedFileUrls,
|
||||
},
|
||||
],
|
||||
currentBlockId: undefined,
|
||||
state,
|
||||
})
|
||||
return updatedState
|
||||
}
|
||||
|
||||
const saveInputVarIfAny = ({
|
||||
block,
|
||||
reply,
|
||||
state,
|
||||
}: {
|
||||
block: InputBlock
|
||||
reply: Message
|
||||
state: SessionState
|
||||
}): SessionState => {
|
||||
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === block.options?.variableId
|
||||
)
|
||||
if (!foundVariable) return state
|
||||
|
||||
const { updatedState } = updateVariablesInSession({
|
||||
newVariables: [
|
||||
{
|
||||
...foundVariable,
|
||||
value:
|
||||
Array.isArray(foundVariable.value) && reply.text
|
||||
? foundVariable.value.concat(reply.text)
|
||||
: reply.text,
|
||||
},
|
||||
],
|
||||
currentBlockId: undefined,
|
||||
state,
|
||||
})
|
||||
|
||||
return updatedState
|
||||
}
|
||||
|
||||
const parseRetryMessage =
|
||||
(state: SessionState) =>
|
||||
async (
|
||||
@ -346,26 +412,27 @@ const parseDefaultRetryMessage = (block: InputBlock): string => {
|
||||
|
||||
const saveAnswerInDb =
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
async (reply: string): Promise<SessionState> => {
|
||||
async (reply: Message): Promise<SessionState> => {
|
||||
let newSessionState = state
|
||||
await saveAnswer({
|
||||
answer: {
|
||||
blockId: block.id,
|
||||
content: reply,
|
||||
content: reply.text,
|
||||
attachedFileUrls: reply.attachedFileUrls,
|
||||
},
|
||||
reply,
|
||||
state,
|
||||
})
|
||||
|
||||
newSessionState = {
|
||||
...saveVariableValueIfAny(newSessionState, block)(reply),
|
||||
...saveVariablesValueIfAny(newSessionState, block)(reply),
|
||||
previewMetadata: state.typebotsQueue[0].resultId
|
||||
? newSessionState.previewMetadata
|
||||
: {
|
||||
...newSessionState.previewMetadata,
|
||||
answers: (newSessionState.previewMetadata?.answers ?? []).concat({
|
||||
blockId: block.id,
|
||||
content: reply,
|
||||
content: reply.text,
|
||||
attachedFileUrls: reply.attachedFileUrls,
|
||||
}),
|
||||
},
|
||||
}
|
||||
@ -378,7 +445,10 @@ const saveAnswerInDb =
|
||||
|
||||
return setNewAnswerInState(newSessionState)({
|
||||
key: key ?? block.id,
|
||||
value: reply,
|
||||
value:
|
||||
(reply.attachedFileUrls ?? []).length > 0
|
||||
? `${reply.attachedFileUrls!.join(', ')}\n\n${reply.text}`
|
||||
: reply.text,
|
||||
})
|
||||
}
|
||||
|
||||
@ -465,41 +535,17 @@ const getOutgoingEdgeId =
|
||||
const parseReply =
|
||||
(state: SessionState) =>
|
||||
async (reply: Reply, block: InputBlock): Promise<ParsedReply> => {
|
||||
if (reply && typeof reply !== 'string') {
|
||||
if (block.type !== InputBlockType.FILE) return { status: 'fail' }
|
||||
if (block.options?.visibility !== 'Public') {
|
||||
return {
|
||||
status: 'success',
|
||||
reply:
|
||||
env.NEXTAUTH_URL +
|
||||
`/api/typebots/${state.typebotsQueue[0].typebot.id}/whatsapp/media/${reply.mediaId}`,
|
||||
}
|
||||
}
|
||||
const { file, mimeType } = await downloadMedia({
|
||||
mediaId: reply.mediaId,
|
||||
systemUserAccessToken: reply.accessToken,
|
||||
})
|
||||
const url = await uploadFileToBucket({
|
||||
file,
|
||||
key: `public/workspaces/${reply.workspaceId}/typebots/${state.typebotsQueue[0].typebot.id}/results/${state.typebotsQueue[0].resultId}/${reply.mediaId}`,
|
||||
mimeType,
|
||||
})
|
||||
return {
|
||||
status: 'success',
|
||||
reply: url,
|
||||
}
|
||||
}
|
||||
switch (block.type) {
|
||||
case InputBlockType.EMAIL: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
const formattedEmail = formatEmail(reply)
|
||||
const formattedEmail = formatEmail(reply.text)
|
||||
if (!formattedEmail) return { status: 'fail' }
|
||||
return { status: 'success', reply: formattedEmail }
|
||||
}
|
||||
case InputBlockType.PHONE: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
const formattedPhone = formatPhoneNumber(
|
||||
reply,
|
||||
reply.text,
|
||||
block.options?.defaultCountryCode
|
||||
)
|
||||
if (!formattedPhone) return { status: 'fail' }
|
||||
@ -507,58 +553,60 @@ const parseReply =
|
||||
}
|
||||
case InputBlockType.URL: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
const isValid = isURL(reply, { require_protocol: false })
|
||||
const isValid = isURL(reply.text, { require_protocol: false })
|
||||
if (!isValid) return { status: 'fail' }
|
||||
return { status: 'success', reply: reply }
|
||||
return { status: 'success', reply: reply.text }
|
||||
}
|
||||
case InputBlockType.CHOICE: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
return parseButtonsReply(state)(reply, block)
|
||||
return parseButtonsReply(state)(reply.text, block)
|
||||
}
|
||||
case InputBlockType.NUMBER: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
const isValid = validateNumber(reply, {
|
||||
const isValid = validateNumber(reply.text, {
|
||||
options: block.options,
|
||||
variables: state.typebotsQueue[0].typebot.variables,
|
||||
})
|
||||
if (!isValid) return { status: 'fail' }
|
||||
return { status: 'success', reply: parseNumber(reply) }
|
||||
return { status: 'success', reply: parseNumber(reply.text) }
|
||||
}
|
||||
case InputBlockType.DATE: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
return parseDateReply(reply, block)
|
||||
return parseDateReply(reply.text, block)
|
||||
}
|
||||
case InputBlockType.FILE: {
|
||||
if (!reply)
|
||||
return block.options?.isRequired ?? defaultFileInputOptions.isRequired
|
||||
? { status: 'fail' }
|
||||
: { status: 'skip' }
|
||||
const urls = reply.split(', ')
|
||||
const urls = reply.text.split(', ')
|
||||
const status = urls.some((url) =>
|
||||
isURL(url, { require_tld: env.S3_ENDPOINT !== 'localhost' })
|
||||
)
|
||||
? 'success'
|
||||
: 'fail'
|
||||
return { status, reply: reply }
|
||||
if (!block.options?.isMultipleAllowed && urls.length > 1)
|
||||
return { status, reply: reply.text.split(',')[0] }
|
||||
return { status, reply: reply.text }
|
||||
}
|
||||
case InputBlockType.PAYMENT: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
if (reply === 'fail') return { status: 'fail' }
|
||||
return { status: 'success', reply: reply }
|
||||
if (reply.text === 'fail') return { status: 'fail' }
|
||||
return { status: 'success', reply: reply.text }
|
||||
}
|
||||
case InputBlockType.RATING: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
const isValid = validateRatingReply(reply, block)
|
||||
const isValid = validateRatingReply(reply.text, block)
|
||||
if (!isValid) return { status: 'fail' }
|
||||
return { status: 'success', reply: reply }
|
||||
return { status: 'success', reply: reply.text }
|
||||
}
|
||||
case InputBlockType.PICTURE_CHOICE: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
return parsePictureChoicesReply(state)(reply, block)
|
||||
return parsePictureChoicesReply(state)(reply.text, block)
|
||||
}
|
||||
case InputBlockType.TEXT: {
|
||||
if (!reply) return { status: 'fail' }
|
||||
return { status: 'success', reply: reply }
|
||||
return { status: 'success', reply: reply.text }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user