2023-08-29 10:01:28 +02:00
|
|
|
import { SessionState } from '@typebot.io/schemas'
|
|
|
|
import {
|
|
|
|
WhatsAppCredentials,
|
|
|
|
WhatsAppIncomingMessage,
|
|
|
|
} from '@typebot.io/schemas/features/whatsapp'
|
2023-09-20 15:26:52 +02:00
|
|
|
import { env } from '@typebot.io/env'
|
|
|
|
import { sendChatReplyToWhatsApp } from './sendChatReplyToWhatsApp'
|
2023-08-29 10:01:28 +02:00
|
|
|
import { startWhatsAppSession } from './startWhatsAppSession'
|
2023-09-20 15:26:52 +02:00
|
|
|
import { getSession } from '../queries/getSession'
|
|
|
|
import { continueBotFlow } from '../continueBotFlow'
|
2023-10-06 16:34:10 +02:00
|
|
|
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
2023-09-20 15:26:52 +02:00
|
|
|
import { saveStateToDatabase } from '../saveStateToDatabase'
|
2023-09-22 17:12:15 +02:00
|
|
|
import { isDefined } from '@typebot.io/lib/utils'
|
2024-01-30 08:02:10 +01:00
|
|
|
import { Reply } from '../types'
|
2024-04-17 12:10:19 +02:00
|
|
|
import { PrismaClient } from '@typebot.io/prisma'
|
2023-09-27 16:45:14 +02:00
|
|
|
|
|
|
|
type Props = {
|
|
|
|
receivedMessage: WhatsAppIncomingMessage
|
|
|
|
sessionId: string
|
|
|
|
credentialsId?: string
|
2024-02-26 13:54:41 +01:00
|
|
|
phoneNumberId?: string
|
2023-09-27 16:45:14 +02:00
|
|
|
workspaceId?: string
|
|
|
|
contact: NonNullable<SessionState['whatsApp']>['contact']
|
|
|
|
}
|
2023-08-29 10:01:28 +02:00
|
|
|
|
|
|
|
export const resumeWhatsAppFlow = async ({
|
|
|
|
receivedMessage,
|
|
|
|
sessionId,
|
|
|
|
workspaceId,
|
2023-09-22 11:08:41 +02:00
|
|
|
credentialsId,
|
2024-02-26 13:54:41 +01:00
|
|
|
phoneNumberId,
|
2023-08-29 10:01:28 +02:00
|
|
|
contact,
|
2023-09-27 16:45:14 +02:00
|
|
|
}: Props): Promise<{ message: string }> => {
|
2024-04-17 11:01:34 +02:00
|
|
|
console.log('fire')
|
2023-08-29 10:01:28 +02:00
|
|
|
const messageSendDate = new Date(Number(receivedMessage.timestamp) * 1000)
|
2024-04-17 11:09:12 +02:00
|
|
|
console.log('messageSendDate:', messageSendDate)
|
2023-08-29 10:01:28 +02:00
|
|
|
const messageSentBefore3MinutesAgo =
|
|
|
|
messageSendDate.getTime() < Date.now() - 180000
|
2024-04-17 11:09:12 +02:00
|
|
|
console.log('messageSentBefore3MinutesAgo:', messageSentBefore3MinutesAgo)
|
2023-08-29 10:01:28 +02:00
|
|
|
if (messageSentBefore3MinutesAgo) {
|
|
|
|
console.log('Message is too old', messageSendDate.getTime())
|
|
|
|
return {
|
|
|
|
message: 'Message received',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-22 11:08:41 +02:00
|
|
|
const isPreview = workspaceId === undefined || credentialsId === undefined
|
2023-08-29 10:01:28 +02:00
|
|
|
|
2024-04-17 11:20:58 +02:00
|
|
|
console.log('get credentials', credentialsId)
|
2023-09-22 11:08:41 +02:00
|
|
|
const credentials = await getCredentials({ credentialsId, isPreview })
|
|
|
|
|
2024-04-17 11:09:12 +02:00
|
|
|
console.log('credentials', isDefined(credentials))
|
2023-09-22 11:08:41 +02:00
|
|
|
if (!credentials) {
|
|
|
|
console.error('Could not find credentials')
|
|
|
|
return {
|
|
|
|
message: 'Message received',
|
|
|
|
}
|
|
|
|
}
|
2024-04-17 11:09:12 +02:00
|
|
|
console.log('yes')
|
2024-02-28 15:49:18 +01:00
|
|
|
if (credentials.phoneNumberId !== phoneNumberId && !isPreview) {
|
2024-02-26 13:54:41 +01:00
|
|
|
console.error('Credentials point to another phone ID, skipping...')
|
|
|
|
return {
|
|
|
|
message: 'Message received',
|
|
|
|
}
|
|
|
|
}
|
2024-04-17 11:09:12 +02:00
|
|
|
console.log('yes2')
|
2024-01-30 08:02:10 +01:00
|
|
|
const reply = await getIncomingMessageContent({
|
|
|
|
message: receivedMessage,
|
|
|
|
workspaceId,
|
|
|
|
accessToken: credentials?.systemUserAccessToken,
|
|
|
|
})
|
|
|
|
|
2024-04-17 11:01:34 +02:00
|
|
|
console.log('incoming message content:', reply)
|
|
|
|
|
2024-03-07 14:26:02 +01:00
|
|
|
const session = await getSession(sessionId)
|
|
|
|
|
2023-09-22 17:12:15 +02:00
|
|
|
const isSessionExpired =
|
|
|
|
session &&
|
|
|
|
isDefined(session.state.expiryTimeout) &&
|
|
|
|
session?.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
|
|
|
|
|
2024-04-17 11:01:34 +02:00
|
|
|
console.log('Process')
|
2023-09-22 17:12:15 +02:00
|
|
|
const resumeResponse =
|
2023-09-26 09:50:20 +02:00
|
|
|
session && !isSessionExpired
|
2024-01-30 08:02:10 +01:00
|
|
|
? await continueBotFlow(reply, {
|
2023-10-06 10:14:26 +02:00
|
|
|
version: 2,
|
|
|
|
state: { ...session.state, whatsApp: { contact } },
|
|
|
|
})
|
2023-09-22 17:12:15 +02:00
|
|
|
: workspaceId
|
|
|
|
? await startWhatsAppSession({
|
2024-01-30 08:02:10 +01:00
|
|
|
incomingMessage: reply,
|
2023-09-22 17:12:15 +02:00
|
|
|
workspaceId,
|
|
|
|
credentials: { ...credentials, id: credentialsId as string },
|
|
|
|
contact,
|
|
|
|
})
|
2023-10-25 14:26:10 +02:00
|
|
|
: { error: 'workspaceId not found' }
|
2023-08-29 10:01:28 +02:00
|
|
|
|
2024-04-17 11:01:34 +02:00
|
|
|
console.log('resumeResponse:', resumeResponse)
|
|
|
|
|
2023-10-25 14:26:10 +02:00
|
|
|
if ('error' in resumeResponse) {
|
|
|
|
console.log('Chat not starting:', resumeResponse.error)
|
2023-08-29 10:01:28 +02:00
|
|
|
return {
|
|
|
|
message: 'Message received',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-08 15:34:16 +01:00
|
|
|
const {
|
|
|
|
input,
|
|
|
|
logs,
|
|
|
|
newSessionState,
|
|
|
|
messages,
|
|
|
|
clientSideActions,
|
|
|
|
visitedEdges,
|
|
|
|
} = resumeResponse
|
2023-08-29 10:01:28 +02:00
|
|
|
|
2024-01-24 12:03:41 +01:00
|
|
|
const isFirstChatChunk = (!session || isSessionExpired) ?? false
|
2023-08-29 10:01:28 +02:00
|
|
|
await sendChatReplyToWhatsApp({
|
|
|
|
to: receivedMessage.from,
|
|
|
|
messages,
|
|
|
|
input,
|
2024-01-24 12:03:41 +01:00
|
|
|
isFirstChatChunk,
|
2023-08-29 10:01:28 +02:00
|
|
|
typingEmulation: newSessionState.typingEmulation,
|
|
|
|
clientSideActions,
|
|
|
|
credentials,
|
2023-09-27 16:45:14 +02:00
|
|
|
state: newSessionState,
|
2023-08-29 10:01:28 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
await saveStateToDatabase({
|
2023-10-03 16:06:37 +02:00
|
|
|
forceCreateSession: !session && isDefined(input),
|
2023-08-29 10:01:28 +02:00
|
|
|
clientSideActions: [],
|
|
|
|
input,
|
|
|
|
logs,
|
|
|
|
session: {
|
|
|
|
id: sessionId,
|
|
|
|
state: {
|
|
|
|
...newSessionState,
|
2023-11-08 15:34:16 +01:00
|
|
|
currentBlockId: !input ? undefined : newSessionState.currentBlockId,
|
2023-08-29 10:01:28 +02:00
|
|
|
},
|
|
|
|
},
|
2023-11-08 15:34:16 +01:00
|
|
|
visitedEdges,
|
2023-08-29 10:01:28 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
message: 'Message received',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const getIncomingMessageContent = async ({
|
|
|
|
message,
|
2024-01-30 08:02:10 +01:00
|
|
|
workspaceId,
|
|
|
|
accessToken,
|
2023-08-29 10:01:28 +02:00
|
|
|
}: {
|
|
|
|
message: WhatsAppIncomingMessage
|
2024-01-30 08:02:10 +01:00
|
|
|
workspaceId?: string
|
|
|
|
accessToken: string
|
|
|
|
}): Promise<Reply> => {
|
2023-08-29 10:01:28 +02:00
|
|
|
switch (message.type) {
|
|
|
|
case 'text':
|
|
|
|
return message.text.body
|
|
|
|
case 'button':
|
|
|
|
return message.button.text
|
|
|
|
case 'interactive': {
|
|
|
|
return message.interactive.button_reply.id
|
|
|
|
}
|
|
|
|
case 'document':
|
|
|
|
case 'audio':
|
|
|
|
case 'video':
|
|
|
|
case 'image':
|
2023-12-20 10:35:34 +01:00
|
|
|
let mediaId: string | undefined
|
|
|
|
if (message.type === 'video') mediaId = message.video.id
|
|
|
|
if (message.type === 'image') mediaId = message.image.id
|
|
|
|
if (message.type === 'audio') mediaId = message.audio.id
|
|
|
|
if (message.type === 'document') mediaId = message.document.id
|
|
|
|
if (!mediaId) return
|
2024-01-30 08:02:10 +01:00
|
|
|
return { type: 'whatsapp media', mediaId, workspaceId, accessToken }
|
2024-01-18 14:01:22 +01:00
|
|
|
case 'location':
|
|
|
|
return `${message.location.latitude}, ${message.location.longitude}`
|
2023-08-29 10:01:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-22 11:08:41 +02:00
|
|
|
const getCredentials = async ({
|
|
|
|
credentialsId,
|
|
|
|
isPreview,
|
|
|
|
}: {
|
|
|
|
credentialsId?: string
|
|
|
|
isPreview: boolean
|
|
|
|
}): Promise<WhatsAppCredentials['data'] | undefined> => {
|
|
|
|
if (isPreview) {
|
|
|
|
if (
|
|
|
|
!env.META_SYSTEM_USER_TOKEN ||
|
|
|
|
!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID
|
|
|
|
)
|
|
|
|
return
|
2023-08-29 10:01:28 +02:00
|
|
|
return {
|
2023-09-22 11:08:41 +02:00
|
|
|
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
|
|
|
|
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
|
2023-08-29 10:01:28 +02:00
|
|
|
}
|
|
|
|
}
|
2023-09-22 11:08:41 +02:00
|
|
|
|
|
|
|
if (!credentialsId) return
|
|
|
|
|
2024-04-17 12:10:19 +02:00
|
|
|
console.log('prisma client')
|
|
|
|
const prisma = new PrismaClient()
|
|
|
|
console.log('prisma go', new Date().toISOString())
|
2023-09-22 11:08:41 +02:00
|
|
|
const credentials = await prisma.credentials.findUnique({
|
|
|
|
where: {
|
|
|
|
id: credentialsId,
|
|
|
|
},
|
|
|
|
select: {
|
|
|
|
data: true,
|
|
|
|
iv: true,
|
|
|
|
},
|
|
|
|
})
|
2024-04-17 11:20:58 +02:00
|
|
|
console.log('prisma done')
|
2023-09-22 11:08:41 +02:00
|
|
|
if (!credentials) return
|
2024-04-17 11:20:58 +02:00
|
|
|
console.log('decrypt')
|
2023-09-22 11:08:41 +02:00
|
|
|
const data = (await decrypt(
|
|
|
|
credentials.data,
|
|
|
|
credentials.iv
|
|
|
|
)) as WhatsAppCredentials['data']
|
2024-04-17 11:20:58 +02:00
|
|
|
console.log('decrypted')
|
2023-09-22 11:08:41 +02:00
|
|
|
return {
|
|
|
|
systemUserAccessToken: data.systemUserAccessToken,
|
|
|
|
phoneNumberId: data.phoneNumberId,
|
|
|
|
}
|
|
|
|
}
|