2
0
Files
bot/packages/bot-engine/whatsapp/startWhatsAppSession.ts
2023-09-29 11:23:04 +02:00

190 lines
5.2 KiB
TypeScript

import prisma from '@typebot.io/lib/prisma'
import {
ChatReply,
ComparisonOperators,
LogicalOperator,
PublicTypebot,
SessionState,
Settings,
Typebot,
} from '@typebot.io/schemas'
import {
WhatsAppCredentials,
defaultSessionExpiryTimeout,
} from '@typebot.io/schemas/features/whatsapp'
import { isInputBlock, isNotDefined } from '@typebot.io/lib/utils'
import { startSession } from '../startSession'
import { getNextGroup } from '../getNextGroup'
import { continueBotFlow } from '../continueBotFlow'
import { upsertResult } from '../queries/upsertResult'
type Props = {
incomingMessage?: string
workspaceId?: string
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
contact: NonNullable<SessionState['whatsApp']>['contact']
}
export const startWhatsAppSession = async ({
incomingMessage,
workspaceId,
credentials,
contact,
}: Props): Promise<
| (ChatReply & {
newSessionState: SessionState
})
| undefined
> => {
const publicTypebotsWithWhatsAppEnabled =
(await prisma.publicTypebot.findMany({
where: {
typebot: { workspaceId, whatsAppCredentialsId: credentials.id },
},
select: {
settings: true,
typebot: {
select: {
publicId: true,
},
},
},
})) as (Pick<PublicTypebot, 'settings'> & {
typebot: Pick<Typebot, 'publicId'>
})[]
const botsWithWhatsAppEnabled = publicTypebotsWithWhatsAppEnabled.filter(
(publicTypebot) =>
publicTypebot.typebot.publicId &&
publicTypebot.settings.whatsApp?.isEnabled
)
const publicTypebot =
botsWithWhatsAppEnabled.find(
(publicTypebot) =>
publicTypebot.settings.whatsApp?.startCondition &&
messageMatchStartCondition(
incomingMessage ?? '',
publicTypebot.settings.whatsApp?.startCondition
)
) ?? botsWithWhatsAppEnabled[0]
if (isNotDefined(publicTypebot)) return
const sessionExpiryTimeoutHours =
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
defaultSessionExpiryTimeout
let chatReply = await startSession({
startParams: {
typebot: publicTypebot.typebot.publicId as string,
},
userId: undefined,
initialSessionState: {
whatsApp: {
contact,
},
expiryTimeout: sessionExpiryTimeoutHours * 60 * 60 * 1000,
},
})
let sessionState: SessionState = chatReply.newSessionState
// If first block is an input block, we can directly continue the bot flow
const firstEdgeId =
sessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
const nextGroup = await getNextGroup(sessionState)(firstEdgeId)
sessionState = nextGroup.newSessionState
const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) {
const resultId = sessionState.typebotsQueue[0].resultId
if (resultId)
await upsertResult({
hasStarted: true,
isCompleted: false,
resultId,
typebot: sessionState.typebotsQueue[0].typebot,
})
chatReply = await continueBotFlow({
...sessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
})(incomingMessage)
}
return chatReply
}
export const messageMatchStartCondition = (
message: string,
startCondition: NonNullable<Settings['whatsApp']>['startCondition']
) => {
if (!startCondition) return true
return startCondition.logicalOperator === LogicalOperator.AND
? startCondition.comparisons.every((comparison) =>
matchComparison(
message,
comparison.comparisonOperator,
comparison.value
)
)
: startCondition.comparisons.some((comparison) =>
matchComparison(
message,
comparison.comparisonOperator,
comparison.value
)
)
}
const matchComparison = (
inputValue: string,
comparisonOperator?: ComparisonOperators,
value?: string
): boolean | undefined => {
if (!comparisonOperator) return false
switch (comparisonOperator) {
case ComparisonOperators.CONTAINS: {
if (!value) return false
return inputValue
.trim()
.toLowerCase()
.includes(value.trim().toLowerCase())
}
case ComparisonOperators.EQUAL: {
return inputValue === value
}
case ComparisonOperators.NOT_EQUAL: {
return inputValue !== value
}
case ComparisonOperators.GREATER: {
if (!value) return false
return parseFloat(inputValue) > parseFloat(value)
}
case ComparisonOperators.LESS: {
if (!value) return false
return parseFloat(inputValue) < parseFloat(value)
}
case ComparisonOperators.IS_SET: {
return inputValue.length > 0
}
case ComparisonOperators.IS_EMPTY: {
return inputValue.length === 0
}
case ComparisonOperators.STARTS_WITH: {
if (!value) return false
return inputValue.toLowerCase().startsWith(value.toLowerCase())
}
case ComparisonOperators.ENDS_WITH: {
if (!value) return false
return inputValue.toLowerCase().endsWith(value.toLowerCase())
}
case ComparisonOperators.NOT_CONTAINS: {
if (!value) return false
return !inputValue
.trim()
.toLowerCase()
.includes(value.trim().toLowerCase())
}
}
}