⚡ (whatsapp) Improve whatsApp management and media collection
Closes #796
This commit is contained in:
@ -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
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
Reference in New Issue
Block a user