From e10a506c9608dafd9462ea6d20d698551eb751c1 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 27 Sep 2023 16:45:14 +0200 Subject: [PATCH] =?UTF-8?q?:bug:=20(whatsapp)=20Fix=20preview=20failing=20?= =?UTF-8?q?to=20start=20and=20wait=20timeo=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/whatsapp/startWhatsAppPreview.ts | 1 + apps/docs/docs/embed/whatsapp/overview.md | 3 +- .../blocks/logic/wait/executeWait.ts | 19 ++- .../bot-engine/whatsapp/resumeWhatsAppFlow.ts | 22 ++-- .../whatsapp/sendChatReplyToWhatsApp.ts | 114 +++++++++++++----- 5 files changed, 110 insertions(+), 49 deletions(-) diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index ebf78fc87..ada1c2238 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -109,6 +109,7 @@ export const startWhatsAppPreview = authenticatedProcedure phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID, systemUserAccessToken: env.META_SYSTEM_USER_TOKEN, }, + state: newSessionState, }) await saveStateToDatabase({ clientSideActions: [], diff --git a/apps/docs/docs/embed/whatsapp/overview.md b/apps/docs/docs/embed/whatsapp/overview.md index 8942932e3..8163de142 100644 --- a/apps/docs/docs/embed/whatsapp/overview.md +++ b/apps/docs/docs/embed/whatsapp/overview.md @@ -21,14 +21,13 @@ Head over to the Share tab of your bot and click on the WhatsApp button to get t WhatsApp environment have some limitations that you need to keep in mind when building the bot: - GIF and SVG image files are not supported. They won't be displayed. -- Buttons content can't be longer than 20 characters +- Buttons content can't be longer than 20 characters. If the content is longer, it will be truncated. - Incompatible blocks, if present, they will be skipped: - Payment input block - Chatwoot block - Script block - Google Analytics block - Meta Pixel blocks - - Execute on client options ## Contact information diff --git a/packages/bot-engine/blocks/logic/wait/executeWait.ts b/packages/bot-engine/blocks/logic/wait/executeWait.ts index 55cf11a72..0df209e03 100644 --- a/packages/bot-engine/blocks/logic/wait/executeWait.ts +++ b/packages/bot-engine/blocks/logic/wait/executeWait.ts @@ -7,22 +7,21 @@ export const executeWait = ( block: WaitBlock ): ExecuteLogicResponse => { const { variables } = state.typebotsQueue[0].typebot - if (!block.options.secondsToWaitFor) - return { outgoingEdgeId: block.outgoingEdgeId } const parsedSecondsToWaitFor = safeParseInt( parseVariables(variables)(block.options.secondsToWaitFor) ) return { outgoingEdgeId: block.outgoingEdgeId, - clientSideActions: parsedSecondsToWaitFor - ? [ - { - wait: { secondsToWaitFor: parsedSecondsToWaitFor }, - expectsDedicatedReply: block.options.shouldPause, - }, - ] - : undefined, + clientSideActions: + parsedSecondsToWaitFor || block.options.shouldPause + ? [ + { + wait: { secondsToWaitFor: parsedSecondsToWaitFor ?? 0 }, + expectsDedicatedReply: block.options.shouldPause, + }, + ] + : undefined, } } diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 2cb7bddb7..9b19eccd5 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -12,6 +12,15 @@ import { decrypt } from '@typebot.io/lib/api' import { saveStateToDatabase } from '../saveStateToDatabase' import prisma from '@typebot.io/lib/prisma' import { isDefined } from '@typebot.io/lib/utils' +import { startBotFlow } from '../startBotFlow' + +type Props = { + receivedMessage: WhatsAppIncomingMessage + sessionId: string + credentialsId?: string + workspaceId?: string + contact: NonNullable['contact'] +} export const resumeWhatsAppFlow = async ({ receivedMessage, @@ -19,13 +28,7 @@ export const resumeWhatsAppFlow = async ({ workspaceId, credentialsId, contact, -}: { - receivedMessage: WhatsAppIncomingMessage - sessionId: string - credentialsId?: string - workspaceId?: string - contact: NonNullable['contact'] -}) => { +}: Props): Promise<{ message: string }> => { const messageSendDate = new Date(Number(receivedMessage.timestamp) * 1000) const messageSentBefore3MinutesAgo = messageSendDate.getTime() < Date.now() - 180000 @@ -62,7 +65,9 @@ export const resumeWhatsAppFlow = async ({ const resumeResponse = session && !isSessionExpired - ? await continueBotFlow(session.state)(messageContent) + ? session.state.currentBlock + ? await continueBotFlow(session.state)(messageContent) + : await startBotFlow(session.state) : workspaceId ? await startWhatsAppSession({ incomingMessage: messageContent, @@ -90,6 +95,7 @@ export const resumeWhatsAppFlow = async ({ typingEmulation: newSessionState.typingEmulation, clientSideActions, credentials, + state: newSessionState, }) await saveStateToDatabase({ diff --git a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts index dbcf743c9..4ad555a99 100644 --- a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts +++ b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts @@ -15,6 +15,7 @@ import { HTTPError } from 'got' import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage' import { isNotDefined } from '@typebot.io/lib/utils' import { computeTypingDuration } from '../computeTypingDuration' +import { continueBotFlow } from '../continueBotFlow' // Media can take some time to be delivered. This make sure we don't send a message before the media is delivered. const messageAfterMediaTimeout = 5000 @@ -23,6 +24,7 @@ type Props = { to: string typingEmulation: SessionState['typingEmulation'] credentials: WhatsAppCredentials['data'] + state: SessionState } & Pick export const sendChatReplyToWhatsApp = async ({ @@ -32,13 +34,36 @@ export const sendChatReplyToWhatsApp = async ({ input, clientSideActions, credentials, -}: Props) => { + state, +}: Props): Promise => { const messagesBeforeInput = isLastMessageIncludedInInput(input) ? messages.slice(0, -1) : messages const sentMessages: WhatsAppSendingMessage[] = [] + const clientSideActionsBeforeMessages = + clientSideActions?.filter((action) => + isNotDefined(action.lastBubbleBlockId) + ) ?? [] + + for (const action of clientSideActionsBeforeMessages) { + const result = await executeClientSideAction({ to, credentials })(action) + if (!result) continue + const { input, newSessionState, messages, clientSideActions } = + await continueBotFlow(state)(result.replyToSend) + + return sendChatReplyToWhatsApp({ + to, + messages, + input, + typingEmulation: newSessionState.typingEmulation, + clientSideActions, + credentials, + state: newSessionState, + }) + } + for (const message of messagesBeforeInput) { const whatsAppMessage = convertMessageToWhatsAppMessage(message) if (isNotDefined(whatsAppMessage)) continue @@ -60,6 +85,28 @@ export const sendChatReplyToWhatsApp = async ({ credentials, }) sentMessages.push(whatsAppMessage) + const clientSideActionsAfterMessage = + clientSideActions?.filter( + (action) => action.lastBubbleBlockId === message.id + ) ?? [] + for (const action of clientSideActionsAfterMessage) { + const result = await executeClientSideAction({ to, credentials })( + action + ) + if (!result) continue + const { input, newSessionState, messages, clientSideActions } = + await continueBotFlow(state)(result.replyToSend) + + return sendChatReplyToWhatsApp({ + to, + messages, + input, + typingEmulation: newSessionState.typingEmulation, + clientSideActions, + credentials, + state: newSessionState, + }) + } } catch (err) { captureException(err, { extra: { message } }) console.log('Failed to send message:', JSON.stringify(message, null, 2)) @@ -68,34 +115,6 @@ export const sendChatReplyToWhatsApp = async ({ } } - if (clientSideActions) - for (const clientSideAction of clientSideActions) { - if ('redirect' in clientSideAction && clientSideAction.redirect.url) { - const message = { - type: 'text', - text: { - body: clientSideAction.redirect.url, - preview_url: true, - }, - } satisfies WhatsAppSendingMessage - try { - await sendWhatsAppMessage({ - to, - message, - credentials, - }) - } catch (err) { - captureException(err, { extra: { message } }) - console.log( - 'Failed to send message:', - JSON.stringify(message, null, 2) - ) - if (err instanceof HTTPError) - console.log('HTTPError', err.response.statusCode, err.response.body) - } - } - } - if (input) { const inputWhatsAppMessages = convertInputToWhatsAppMessages( input, @@ -160,3 +179,40 @@ const isLastMessageIncludedInInput = (input: ChatReply['input']): boolean => { if (isNotDefined(input)) return false return input.type === InputBlockType.CHOICE } + +const executeClientSideAction = + (context: { to: string; credentials: WhatsAppCredentials['data'] }) => + async ( + clientSideAction: NonNullable[number] + ): Promise<{ replyToSend: string | undefined } | void> => { + if ('wait' in clientSideAction) { + await new Promise((resolve) => + setTimeout(resolve, clientSideAction.wait.secondsToWaitFor * 1000) + ) + if (!clientSideAction.expectsDedicatedReply) return + return { + replyToSend: undefined, + } + } + if ('redirect' in clientSideAction && clientSideAction.redirect.url) { + const message = { + type: 'text', + text: { + body: clientSideAction.redirect.url, + preview_url: true, + }, + } satisfies WhatsAppSendingMessage + try { + await sendWhatsAppMessage({ + to: context.to, + message, + credentials: context.credentials, + }) + } catch (err) { + captureException(err, { extra: { message } }) + console.log('Failed to send message:', JSON.stringify(message, null, 2)) + if (err instanceof HTTPError) + console.log('HTTPError', err.response.statusCode, err.response.body) + } + } + }