2
0

(whatsapp) Improve whatsApp management and media collection

Closes #796
This commit is contained in:
Baptiste Arnaud
2023-09-22 11:08:41 +02:00
parent 8f4e5b5d63
commit 9e0109f561
22 changed files with 505 additions and 452 deletions

View File

@ -1,46 +0,0 @@
import got from 'got'
import { TRPCError } from '@trpc/server'
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
type Props = {
mediaId: string
systemUserToken: string
downloadPath: string
}
export const downloadMedia = async ({
mediaId,
systemUserToken,
downloadPath,
}: Props) => {
const { body } = await got.get({
url: `https://graph.facebook.com/v17.0/${mediaId}`,
headers: {
Authorization: `Bearer ${systemUserToken}`,
},
})
const parsedBody = JSON.parse(body) as { url: string; mime_type: string }
if (!parsedBody.url)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Request to Facebook failed. Could not find media url.',
cause: body,
})
const streamBuffer = await got(parsedBody.url, {
headers: {
Authorization: `Bearer ${systemUserToken}`,
},
}).buffer()
const typebotUrl = await uploadFileToBucket({
fileName: `public/${downloadPath}/${mediaId}`,
file: streamBuffer,
mimeType: parsedBody.mime_type,
})
await got.delete({
url: `https://graph.facebook.com/v17.0/${mediaId}`,
headers: {
Authorization: `Bearer ${systemUserToken}`,
},
})
return typebotUrl
}

View File

@ -6,7 +6,6 @@ import {
import { env } from '@typebot.io/env'
import { sendChatReplyToWhatsApp } from './sendChatReplyToWhatsApp'
import { startWhatsAppSession } from './startWhatsAppSession'
import { downloadMedia } from './downloadMedia'
import { getSession } from '../queries/getSession'
import { continueBotFlow } from '../continueBotFlow'
import { decrypt } from '@typebot.io/lib/api'
@ -17,12 +16,12 @@ export const resumeWhatsAppFlow = async ({
receivedMessage,
sessionId,
workspaceId,
phoneNumberId,
credentialsId,
contact,
}: {
receivedMessage: WhatsAppIncomingMessage
sessionId: string
phoneNumberId: string
credentialsId?: string
workspaceId?: string
contact: NonNullable<SessionState['whatsApp']>['contact']
}) => {
@ -38,22 +37,14 @@ export const resumeWhatsAppFlow = async ({
const session = await getSession(sessionId)
const initialCredentials = session
? await getCredentials(phoneNumberId)(session.state)
: undefined
const isPreview = workspaceId === undefined || credentialsId === undefined
const { typebot, resultId } = session?.state.typebotsQueue[0] ?? {}
const { typebot } = session?.state.typebotsQueue[0] ?? {}
const messageContent = await getIncomingMessageContent({
message: receivedMessage,
systemUserToken: initialCredentials?.systemUserAccessToken,
downloadPath:
typebot && resultId
? `typebots/${typebot.id}/results/${resultId}`
: undefined,
typebotId: typebot?.id,
})
const isPreview = workspaceId === undefined
const sessionState =
isPreview && session?.state
? ({
@ -64,6 +55,15 @@ export const resumeWhatsAppFlow = async ({
} satisfies SessionState)
: session?.state
const credentials = await getCredentials({ credentialsId, isPreview })
if (!credentials) {
console.error('Could not find credentials')
return {
message: 'Message received',
}
}
const resumeResponse = sessionState
? await continueBotFlow(sessionState)(messageContent)
: workspaceId
@ -71,7 +71,7 @@ export const resumeWhatsAppFlow = async ({
message: receivedMessage,
sessionId,
workspaceId,
phoneNumberId,
credentials: { ...credentials, id: credentialsId as string },
contact,
})
: undefined
@ -83,17 +83,6 @@ export const resumeWhatsAppFlow = async ({
}
}
const credentials =
initialCredentials ??
(await getCredentials(phoneNumberId)(resumeResponse.newSessionState))
if (!credentials) {
console.error('Could not find credentials')
return {
message: 'Message received',
}
}
const { input, logs, newSessionState, messages, clientSideActions } =
resumeResponse
@ -127,12 +116,10 @@ export const resumeWhatsAppFlow = async ({
const getIncomingMessageContent = async ({
message,
systemUserToken,
downloadPath,
typebotId,
}: {
message: WhatsAppIncomingMessage
systemUserToken: string | undefined
downloadPath?: string
typebotId?: string
}): Promise<string | undefined> => {
switch (message.type) {
case 'text':
@ -147,46 +134,52 @@ const getIncomingMessageContent = async ({
return
case 'video':
case 'image':
if (!systemUserToken || !downloadPath) return ''
return downloadMedia({
mediaId: 'video' in message ? message.video.id : message.image.id,
systemUserToken,
downloadPath,
})
if (!typebotId) return
const mediaId = 'video' in message ? message.video.id : message.image.id
return (
env.NEXTAUTH_URL +
`/api/typebots/${typebotId}/whatsapp/media/${mediaId}`
)
}
}
const getCredentials =
(phoneNumberId: string) =>
async (
state: SessionState
): Promise<WhatsAppCredentials['data'] | undefined> => {
const isPreview = !state.typebotsQueue[0].resultId
if (isPreview) {
if (!env.META_SYSTEM_USER_TOKEN) return
return {
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
phoneNumberId,
}
}
if (!state.whatsApp) return
const credentials = await prisma.credentials.findUnique({
where: {
id: state.whatsApp.credentialsId,
},
select: {
data: true,
iv: true,
},
})
if (!credentials) return
const data = (await decrypt(
credentials.data,
credentials.iv
)) as WhatsAppCredentials['data']
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
return {
systemUserAccessToken: data.systemUserAccessToken,
phoneNumberId,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
}
}
if (!credentialsId) return
const credentials = await prisma.credentials.findUnique({
where: {
id: credentialsId,
},
select: {
data: true,
iv: true,
},
})
if (!credentials) return
const data = (await decrypt(
credentials.data,
credentials.iv
)) as WhatsAppCredentials['data']
return {
systemUserAccessToken: data.systemUserAccessToken,
phoneNumberId: data.phoneNumberId,
}
}

View File

@ -13,21 +13,20 @@ import {
WhatsAppIncomingMessage,
} from '@typebot.io/schemas/features/whatsapp'
import { isNotDefined } from '@typebot.io/lib/utils'
import { decrypt } from '@typebot.io/lib/api/encryption'
import { startSession } from '../startSession'
type Props = {
message: WhatsAppIncomingMessage
sessionId: string
workspaceId?: string
phoneNumberId: string
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
contact: NonNullable<SessionState['whatsApp']>['contact']
}
export const startWhatsAppSession = async ({
message,
workspaceId,
phoneNumberId,
credentials,
contact,
}: Props): Promise<
| (ChatReply & {
@ -38,7 +37,7 @@ export const startWhatsAppSession = async ({
const publicTypebotsWithWhatsAppEnabled =
(await prisma.publicTypebot.findMany({
where: {
typebot: { workspaceId, whatsAppPhoneNumberId: phoneNumberId },
typebot: { workspaceId, whatsAppCredentialsId: credentials.id },
},
select: {
settings: true,
@ -55,7 +54,7 @@ export const startWhatsAppSession = async ({
const botsWithWhatsAppEnabled = publicTypebotsWithWhatsAppEnabled.filter(
(publicTypebot) =>
publicTypebot.typebot.publicId &&
publicTypebot.settings.whatsApp?.credentialsId
publicTypebot.settings.whatsApp?.isEnabled
)
const publicTypebot =
@ -70,19 +69,6 @@ export const startWhatsAppSession = async ({
if (isNotDefined(publicTypebot)) return
const encryptedCredentials = await prisma.credentials.findUnique({
where: {
id: publicTypebot.settings.whatsApp?.credentialsId,
},
})
if (!encryptedCredentials) return
const credentials = (await decrypt(
encryptedCredentials?.data,
encryptedCredentials?.iv
)) as WhatsAppCredentials['data']
if (credentials.phoneNumberId !== phoneNumberId) return
const session = await startSession({
startParams: {
typebot: publicTypebot.typebot.publicId as string,
@ -96,8 +82,7 @@ export const startWhatsAppSession = async ({
...session.newSessionState,
whatsApp: {
contact,
credentialsId: publicTypebot?.settings.whatsApp
?.credentialsId as string,
credentialsId: credentials.id,
},
},
}