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(),
})