diff --git a/apps/builder/src/features/credentials/api/createCredentials.ts b/apps/builder/src/features/credentials/api/createCredentials.ts index 62047d980..c0da7f0f1 100644 --- a/apps/builder/src/features/credentials/api/createCredentials.ts +++ b/apps/builder/src/features/credentials/api/createCredentials.ts @@ -31,14 +31,16 @@ export const createCredentials = authenticatedProcedure }) .input( z.object({ - credentials: z.discriminatedUnion('type', [ - stripeCredentialsSchema.pick(inputShape), - smtpCredentialsSchema.pick(inputShape), - googleSheetsCredentialsSchema.pick(inputShape), - openAICredentialsSchema.pick(inputShape), - whatsAppCredentialsSchema.pick(inputShape), - zemanticAiCredentialsSchema.pick(inputShape), - ]), + credentials: z + .discriminatedUnion('type', [ + stripeCredentialsSchema.pick(inputShape), + smtpCredentialsSchema.pick(inputShape), + googleSheetsCredentialsSchema.pick(inputShape), + openAICredentialsSchema.pick(inputShape), + whatsAppCredentialsSchema.pick(inputShape), + zemanticAiCredentialsSchema.pick(inputShape), + ]) + .and(z.object({ id: z.string().cuid2().optional() })), }) ) .output( diff --git a/apps/builder/src/features/editor/providers/TypebotProvider.tsx b/apps/builder/src/features/editor/providers/TypebotProvider.tsx index dcbf2cee4..387f80961 100644 --- a/apps/builder/src/features/editor/providers/TypebotProvider.tsx +++ b/apps/builder/src/features/editor/providers/TypebotProvider.tsx @@ -39,7 +39,7 @@ type UpdateTypebotPayload = Partial< | 'customDomain' | 'resultsTablePreferences' | 'isClosed' - | 'whatsAppPhoneNumberId' + | 'whatsAppCredentialsId' > > diff --git a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx index cae7170de..6c1596f17 100644 --- a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx +++ b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx @@ -43,6 +43,7 @@ import { env } from '@typebot.io/env' import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils' import { getViewerUrl } from '@typebot.io/lib/getViewerUrl' import React, { useState } from 'react' +import { createId } from '@paralleldrive/cuid2' const steps = [ { title: 'Requirements' }, @@ -57,6 +58,8 @@ type Props = { onNewCredentials: (id: string) => void } +const credentialsId = createId() + export const WhatsAppCredentialsModal = ({ isOpen, onClose, @@ -115,6 +118,7 @@ export const WhatsAppCredentialsModal = ({ if (!workspace) return mutate({ credentials: { + id: credentialsId, type: 'whatsApp', workspaceId: workspace.id, name: phoneNumberName, @@ -269,7 +273,7 @@ export const WhatsAppCredentialsModal = ({ )} @@ -442,18 +446,16 @@ const PhoneNumber = ({ const Webhook = ({ appId, verificationToken, - phoneNumberId, + credentialsId, }: { appId?: string verificationToken: string - phoneNumberId: string + credentialsId: string }) => { const { workspace } = useWorkspace() const webhookUrl = `${ env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? getViewerUrl() - }/api/v1/workspaces/${ - workspace?.id - }/whatsapp/phoneNumbers/${phoneNumberId}/webhook` + }/api/v1/workspaces/${workspace?.id}/whatsapp/${credentialsId}/webhook` return ( diff --git a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx index 37fbcc09a..065e49db3 100644 --- a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx +++ b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppModal.tsx @@ -30,7 +30,6 @@ import { PublishButton } from '../../../PublishButton' import { useParentModal } from '@/features/graph/providers/ParentModalProvider' import { trpc } from '@/lib/trpc' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' -import { isDefined } from '@typebot.io/lib/utils' import { TableList } from '@/components/TableList' import { Comparison, LogicalOperator } from '@typebot.io/schemas' import { DropdownList } from '@/components/DropdownList' @@ -51,18 +50,25 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => { const { data: phoneNumberData } = trpc.whatsApp.getPhoneNumber.useQuery( { - credentialsId: whatsAppSettings?.credentialsId as string, + credentialsId: typebot?.whatsAppCredentialsId as string, }, { - enabled: !!whatsAppSettings?.credentialsId, + enabled: !!typebot?.whatsAppCredentialsId, } ) const toggleEnableWhatsApp = (isChecked: boolean) => { - if (!phoneNumberData?.id) return + if (!phoneNumberData?.id || !typebot) return updateTypebot({ - updates: { whatsAppPhoneNumberId: isChecked ? phoneNumberData.id : null }, - save: true, + updates: { + settings: { + ...typebot.settings, + whatsApp: { + ...typebot.settings.whatsApp, + isEnabled: isChecked, + }, + }, + }, }) } @@ -70,13 +76,7 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => { if (!typebot) return updateTypebot({ updates: { - settings: { - ...typebot.settings, - whatsApp: { - ...typebot.settings.whatsApp, - credentialsId, - }, - }, + whatsAppCredentialsId: credentialsId, }, }) } @@ -148,7 +148,9 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => { { )} - {typebot?.settings.whatsApp?.credentialsId && ( + {typebot?.whatsAppCredentialsId && ( <> @@ -196,22 +198,22 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => { - - - Publish your bot: - - - + + + Publish your bot: + + + {phoneNumberData?.id && ( { + if (req.method === 'GET') { + const user = await getAuthenticatedUser(req, res) + if (!user) return notAuthenticated(res) + + const typebotId = req.query.typebotId as string + + const typebot = await prisma.typebot.findFirst({ + where: { + id: typebotId, + }, + select: { + whatsAppCredentialsId: true, + workspace: { + select: { + credentials: { + where: { + type: 'whatsApp', + }, + }, + members: { + select: { + userId: true, + }, + }, + }, + }, + }, + }) + + if (!typebot?.workspace || isReadWorkspaceFobidden(typebot.workspace, user)) + return notFound(res, 'Workspace not found') + + if (!typebot) return notFound(res, 'Typebot not found') + + const mediaId = req.query.mediaId as string + const credentialsId = typebot.whatsAppCredentialsId + + const credentials = typebot.workspace.credentials.find( + (credential) => credential.id === credentialsId + ) + + if (!credentials) return notFound(res, 'Credentials not found') + + const credentialsData = (await decrypt( + credentials.data, + credentials.iv + )) as WhatsAppCredentials['data'] + + const { body } = await got.get({ + url: `https://graph.facebook.com/v17.0/${mediaId}`, + headers: { + Authorization: `Bearer ${credentialsData.systemUserAccessToken}`, + }, + }) + + const parsedBody = JSON.parse(body) as { url: string; mime_type: string } + + const buffer = await got(parsedBody.url, { + headers: { + Authorization: `Bearer ${credentialsData.systemUserAccessToken}`, + }, + }).buffer() + + res.setHeader('Content-Type', parsedBody.mime_type) + res.setHeader('Cache-Control', 'public, max-age=86400') + + return res.send(buffer) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index e4cff484d..2f38237be 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -8633,8 +8633,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -12745,8 +12745,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -12877,6 +12877,10 @@ "whatsAppPhoneNumberId": { "type": "string", "nullable": true + }, + "whatsAppCredentialsId": { + "type": "string", + "nullable": true } }, "required": [ @@ -12899,7 +12903,8 @@ "resultsTablePreferences", "isArchived", "isClosed", - "whatsAppPhoneNumberId" + "whatsAppPhoneNumberId", + "whatsAppCredentialsId" ], "additionalProperties": false } @@ -16819,8 +16824,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -17018,6 +17023,10 @@ "whatsAppPhoneNumberId": { "type": "string", "nullable": true + }, + "whatsAppCredentialsId": { + "type": "string", + "nullable": true } }, "additionalProperties": false @@ -20951,8 +20960,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -21083,6 +21092,10 @@ "whatsAppPhoneNumberId": { "type": "string", "nullable": true + }, + "whatsAppCredentialsId": { + "type": "string", + "nullable": true } }, "required": [ @@ -21105,7 +21118,8 @@ "resultsTablePreferences", "isArchived", "isClosed", - "whatsAppPhoneNumberId" + "whatsAppPhoneNumberId", + "whatsAppCredentialsId" ], "additionalProperties": false } @@ -25049,8 +25063,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -25181,6 +25195,10 @@ "whatsAppPhoneNumberId": { "type": "string", "nullable": true + }, + "whatsAppCredentialsId": { + "type": "string", + "nullable": true } }, "required": [ @@ -25203,7 +25221,8 @@ "resultsTablePreferences", "isArchived", "isClosed", - "whatsAppPhoneNumberId" + "whatsAppPhoneNumberId", + "whatsAppCredentialsId" ], "additionalProperties": false }, @@ -29206,8 +29225,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -30849,294 +30868,307 @@ "type": "object", "properties": { "credentials": { - "anyOf": [ + "allOf": [ { - "type": "object", - "properties": { - "data": { + "anyOf": [ + { "type": "object", "properties": { - "live": { + "data": { "type": "object", "properties": { - "secretKey": { + "live": { + "type": "object", + "properties": { + "secretKey": { + "type": "string" + }, + "publicKey": { + "type": "string" + } + }, + "required": [ + "secretKey", + "publicKey" + ], + "additionalProperties": false + }, + "test": { + "type": "object", + "properties": { + "secretKey": { + "type": "string" + }, + "publicKey": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "live", + "test" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "stripe" + ] + }, + "workspaceId": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "data", + "type", + "workspaceId", + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "host": { "type": "string" }, - "publicKey": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "isTlsEnabled": { + "type": "boolean" + }, + "port": { + "type": "number" + }, + "from": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "port", + "from" + ], + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "smtp" + ] + }, + "workspaceId": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "data", + "type", + "workspaceId", + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string", + "nullable": true + }, + "expiry_date": { + "type": "number", + "nullable": true + }, + "access_token": { + "type": "string", + "nullable": true + }, + "token_type": { + "type": "string", + "nullable": true + }, + "id_token": { + "type": "string", + "nullable": true + }, + "scope": { + "type": "string" + } + }, + "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "google sheets" + ] + }, + "workspaceId": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "data", + "type", + "workspaceId", + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "apiKey": { "type": "string" } }, "required": [ - "secretKey", - "publicKey" + "apiKey" ], "additionalProperties": false }, - "test": { + "type": { + "type": "string", + "enum": [ + "openai" + ] + }, + "workspaceId": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "data", + "type", + "workspaceId", + "name" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { - "secretKey": { + "systemUserAccessToken": { "type": "string" }, - "publicKey": { + "phoneNumberId": { "type": "string" } }, + "required": [ + "systemUserAccessToken", + "phoneNumberId" + ], "additionalProperties": false + }, + "type": { + "type": "string", + "enum": [ + "whatsApp" + ] + }, + "workspaceId": { + "type": "string" + }, + "name": { + "type": "string" } }, "required": [ - "live", - "test" + "data", + "type", + "workspaceId", + "name" ], "additionalProperties": false }, - "type": { - "type": "string", - "enum": [ - "stripe" - ] - }, - "workspaceId": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "data", - "type", - "workspaceId", - "name" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "data": { + { "type": "object", "properties": { - "host": { - "type": "string" - }, - "username": { - "type": "string" - }, - "password": { - "type": "string" - }, - "isTlsEnabled": { - "type": "boolean" - }, - "port": { - "type": "number" - }, - "from": { + "data": { "type": "object", "properties": { - "email": { - "type": "string" - }, - "name": { + "apiKey": { "type": "string" } }, + "required": [ + "apiKey" + ], "additionalProperties": false - } - }, - "required": [ - "port", - "from" - ], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "smtp" - ] - }, - "workspaceId": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "data", - "type", - "workspaceId", - "name" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "refresh_token": { + }, + "type": { "type": "string", - "nullable": true + "enum": [ + "zemanticAi" + ] }, - "expiry_date": { - "type": "number", - "nullable": true - }, - "access_token": { - "type": "string", - "nullable": true - }, - "token_type": { - "type": "string", - "nullable": true - }, - "id_token": { - "type": "string", - "nullable": true - }, - "scope": { + "workspaceId": { "type": "string" - } - }, - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "google sheets" - ] - }, - "workspaceId": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "data", - "type", - "workspaceId", - "name" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "apiKey": { + }, + "name": { "type": "string" } }, "required": [ - "apiKey" + "data", + "type", + "workspaceId", + "name" ], "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "openai" - ] - }, - "workspaceId": { - "type": "string" - }, - "name": { - "type": "string" } - }, - "required": [ - "data", - "type", - "workspaceId", - "name" - ], - "additionalProperties": false + ] }, { "type": "object", "properties": { - "data": { - "type": "object", - "properties": { - "systemUserAccessToken": { - "type": "string" - }, - "phoneNumberId": { - "type": "string" - } - }, - "required": [ - "systemUserAccessToken", - "phoneNumberId" - ], - "additionalProperties": false - }, - "type": { + "id": { "type": "string", - "enum": [ - "whatsApp" - ] - }, - "workspaceId": { - "type": "string" - }, - "name": { - "type": "string" + "pattern": "^[a-z][a-z0-9]*$" } - }, - "required": [ - "data", - "type", - "workspaceId", - "name" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "apiKey": { - "type": "string" - } - }, - "required": [ - "apiKey" - ], - "additionalProperties": false - }, - "type": { - "type": "string", - "enum": [ - "zemanticAi" - ] - }, - "workspaceId": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "data", - "type", - "workspaceId", - "name" - ], - "additionalProperties": false + } } ] } diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index cc90077c2..b7938b708 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -3761,8 +3761,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -6172,8 +6172,8 @@ "whatsApp": { "type": "object", "properties": { - "credentialsId": { - "type": "string" + "isEnabled": { + "type": "boolean" }, "startCondition": { "type": "object", @@ -6350,17 +6350,12 @@ "presignedUrl": { "type": "string" }, - "formData": { - "type": "object", - "additionalProperties": {} - }, "hasReachedStorageLimit": { "type": "boolean" } }, "required": [ "presignedUrl", - "formData", "hasReachedStorageLimit" ], "additionalProperties": false @@ -6508,7 +6503,7 @@ } } }, - "/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook": { + "/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook": { "get": { "operationId": "whatsAppRouter-subscribeWebhook", "summary": "Subscribe webhook", @@ -6530,7 +6525,7 @@ } }, { - "name": "phoneNumberId", + "name": "credentialsId", "in": "path", "required": true, "schema": { @@ -6932,7 +6927,7 @@ } }, { - "name": "phoneNumberId", + "name": "credentialsId", "in": "path", "required": true, "schema": { diff --git a/apps/viewer/src/features/whatsapp/api/receiveMessage.ts b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts index 7f6f99c19..7353ffeda 100644 --- a/apps/viewer/src/features/whatsapp/api/receiveMessage.ts +++ b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts @@ -8,14 +8,14 @@ export const receiveMessage = publicProcedure .meta({ openapi: { method: 'POST', - path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook', + path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook', summary: 'Message webhook', tags: ['WhatsApp'], }, }) .input( z - .object({ workspaceId: z.string(), phoneNumberId: z.string() }) + .object({ workspaceId: z.string(), credentialsId: z.string() }) .merge(whatsAppWebhookRequestBodySchema) ) .output( @@ -23,7 +23,7 @@ export const receiveMessage = publicProcedure message: z.string(), }) ) - .mutation(async ({ input: { entry, workspaceId, phoneNumberId } }) => { + .mutation(async ({ input: { entry, workspaceId, credentialsId } }) => { const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0) if (isNotDefined(receivedMessage)) return { message: 'No message found' } const contactName = @@ -32,8 +32,8 @@ export const receiveMessage = publicProcedure entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? '' return resumeWhatsAppFlow({ receivedMessage, - sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`, - phoneNumberId, + sessionId: `wa-${credentialsId}-${receivedMessage.from}`, + credentialsId, workspaceId, contact: { name: contactName, diff --git a/apps/viewer/src/features/whatsapp/api/subscribeWebhook.ts b/apps/viewer/src/features/whatsapp/api/subscribeWebhook.ts index cf0f2a77d..a5d932f82 100644 --- a/apps/viewer/src/features/whatsapp/api/subscribeWebhook.ts +++ b/apps/viewer/src/features/whatsapp/api/subscribeWebhook.ts @@ -7,7 +7,7 @@ export const subscribeWebhook = publicProcedure .meta({ openapi: { method: 'GET', - path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook', + path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook', summary: 'Subscribe webhook', tags: ['WhatsApp'], protect: true, @@ -16,7 +16,7 @@ export const subscribeWebhook = publicProcedure .input( z.object({ workspaceId: z.string(), - phoneNumberId: z.string(), + credentialsId: z.string(), 'hub.challenge': z.string(), 'hub.verify_token': z.string(), }) diff --git a/packages/bot-engine/whatsapp/downloadMedia.ts b/packages/bot-engine/whatsapp/downloadMedia.ts deleted file mode 100644 index b002318c2..000000000 --- a/packages/bot-engine/whatsapp/downloadMedia.ts +++ /dev/null @@ -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 -} diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 8c0603b8f..3403413e6 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -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['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 => { 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 => { - 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 => { + 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, + } +} diff --git a/packages/bot-engine/whatsapp/startWhatsAppSession.ts b/packages/bot-engine/whatsapp/startWhatsAppSession.ts index 64ddbbc1e..07903e19a 100644 --- a/packages/bot-engine/whatsapp/startWhatsAppSession.ts +++ b/packages/bot-engine/whatsapp/startWhatsAppSession.ts @@ -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 contact: NonNullable['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, }, }, } diff --git a/packages/lib/playwright/databaseHelpers.ts b/packages/lib/playwright/databaseHelpers.ts index 88189d11e..240278e08 100644 --- a/packages/lib/playwright/databaseHelpers.ts +++ b/packages/lib/playwright/databaseHelpers.ts @@ -32,6 +32,7 @@ export const parseTestTypebot = ( isClosed: false, resultsTablePreferences: null, whatsAppPhoneNumberId: null, + whatsAppCredentialsId: null, variables: [{ id: 'var1', name: 'var1' }], ...partialTypebot, edges: [ diff --git a/packages/prisma/mysql/schema.prisma b/packages/prisma/mysql/schema.prisma index 24af601d4..9d0513ae2 100644 --- a/packages/prisma/mysql/schema.prisma +++ b/packages/prisma/mysql/schema.prisma @@ -199,6 +199,7 @@ model Typebot { isArchived Boolean @default(false) isClosed Boolean @default(false) whatsAppPhoneNumberId String? + whatsAppCredentialsId String? @@index([workspaceId]) @@index([folderId]) diff --git a/packages/prisma/postgresql/migrations/20230922081540_add_whatsapp_credentials_id/migration.sql b/packages/prisma/postgresql/migrations/20230922081540_add_whatsapp_credentials_id/migration.sql new file mode 100644 index 000000000..6699e4458 --- /dev/null +++ b/packages/prisma/postgresql/migrations/20230922081540_add_whatsapp_credentials_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Typebot" ADD COLUMN "whatsAppCredentialsId" TEXT; diff --git a/packages/prisma/postgresql/schema.prisma b/packages/prisma/postgresql/schema.prisma index efe31d634..e6404645c 100644 --- a/packages/prisma/postgresql/schema.prisma +++ b/packages/prisma/postgresql/schema.prisma @@ -183,6 +183,7 @@ model Typebot { isArchived Boolean @default(false) isClosed Boolean @default(false) whatsAppPhoneNumberId String? + whatsAppCredentialsId String? @@index([workspaceId]) @@index([isArchived, createdAt(sort: Desc)]) diff --git a/packages/schemas/features/typebot/typebot.ts b/packages/schemas/features/typebot/typebot.ts index f6d424157..70f8c41e6 100644 --- a/packages/schemas/features/typebot/typebot.ts +++ b/packages/schemas/features/typebot/typebot.ts @@ -57,6 +57,7 @@ export const typebotSchema = z.preprocess( isArchived: z.boolean(), isClosed: z.boolean(), whatsAppPhoneNumberId: z.string().nullable(), + whatsAppCredentialsId: z.string().nullable(), }) satisfies z.ZodType ) diff --git a/packages/schemas/features/whatsapp.ts b/packages/schemas/features/whatsapp.ts index 48860637a..74dff104b 100644 --- a/packages/schemas/features/whatsapp.ts +++ b/packages/schemas/features/whatsapp.ts @@ -188,7 +188,7 @@ const startConditionSchema = z.object({ }) export const whatsAppSettingsSchema = z.object({ - credentialsId: z.string().optional(), + isEnabled: z.boolean().optional(), startCondition: startConditionSchema.optional(), })