diff --git a/apps/builder/package.json b/apps/builder/package.json index 41d495f7b..e3c0fdcf7 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -93,7 +93,8 @@ "tinycolor2": "1.6.0", "trpc-openapi": "1.2.0", "unsplash-js": "^7.0.18", - "use-debounce": "9.0.4" + "use-debounce": "9.0.4", + "@typebot.io/viewer": "workspace:*" }, "devDependencies": { "@chakra-ui/styled-system": "2.9.1", diff --git a/apps/builder/src/features/preview/api/sendWhatsAppInitialMessage.ts b/apps/builder/src/features/preview/api/sendWhatsAppInitialMessage.ts deleted file mode 100644 index 360eab9d0..000000000 --- a/apps/builder/src/features/preview/api/sendWhatsAppInitialMessage.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { authenticatedProcedure } from '@/helpers/server/trpc' -import { z } from 'zod' -import got, { HTTPError } from 'got' -import { getViewerUrl } from '@typebot.io/lib/getViewerUrl' -import prisma from '@/lib/prisma' -import { TRPCError } from '@trpc/server' - -export const sendWhatsAppInitialMessage = authenticatedProcedure - .input( - z.object({ - to: z.string(), - typebotId: z.string(), - startGroupId: z.string().optional(), - }) - ) - .mutation( - async ({ input: { to, typebotId, startGroupId }, ctx: { user } }) => { - const apiToken = await prisma.apiToken.findFirst({ - where: { ownerId: user.id }, - select: { - token: true, - }, - }) - if (!apiToken) - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Api Token not found', - }) - try { - await got.post({ - method: 'POST', - url: `${getViewerUrl()}/api/v1/typebots/${typebotId}/whatsapp/start-preview`, - headers: { - Authorization: `Bearer ${apiToken.token}`, - }, - json: { to, isPreview: true, startGroupId }, - }) - } catch (error) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Request to viewer failed', - cause: error instanceof HTTPError ? error.response.body : error, - }) - } - - return { message: 'success' } - } - ) diff --git a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx index b3ffd1dc0..ca376dd1b 100644 --- a/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx +++ b/apps/builder/src/features/preview/components/WhatsAppPreviewInstructions.tsx @@ -32,7 +32,7 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => { const [hasMessageBeenSent, setHasMessageBeenSent] = useState(false) const { showToast } = useToast() - const { mutate } = trpc.sendWhatsAppInitialMessage.useMutation({ + const { mutate } = trpc.whatsApp.startWhatsAppPreview.useMutation({ onMutate: () => setIsSendingMessage(true), onSettled: () => setIsSendingMessage(false), onError: (error) => showToast({ description: error.message }), diff --git a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts index c4719cc85..7192e404c 100644 --- a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts +++ b/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts @@ -5,7 +5,7 @@ import { Typebot } from '@typebot.io/schemas' export const isReadTypebotForbidden = async ( typebot: Pick & { - collaborators: Pick[] + collaborators: Pick[] }, user: Pick ) => { diff --git a/apps/viewer/src/features/whatsApp/api/receiveMessagePreview.ts b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts similarity index 85% rename from apps/viewer/src/features/whatsApp/api/receiveMessagePreview.ts rename to apps/builder/src/features/whatsapp/receiveMessagePreview.ts index 7f42b8e55..1141e505f 100644 --- a/apps/viewer/src/features/whatsApp/api/receiveMessagePreview.ts +++ b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts @@ -1,7 +1,7 @@ import { publicProcedure } from '@/helpers/server/trpc' import { whatsAppWebhookRequestBodySchema } from '@typebot.io/schemas/features/whatsapp' import { z } from 'zod' -import { resumeWhatsAppFlow } from '../helpers/resumeWhatsAppFlow' +import { resumeWhatsAppFlow } from '@typebot.io/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow' import { isNotDefined } from '@typebot.io/lib' import { TRPCError } from '@trpc/server' import { env } from '@typebot.io/env' @@ -11,7 +11,8 @@ export const receiveMessagePreview = publicProcedure openapi: { method: 'POST', path: '/whatsapp/preview/webhook', - summary: 'WhatsApp', + summary: 'Message webhook', + tags: ['WhatsApp'], }, }) .input(whatsAppWebhookRequestBodySchema) @@ -30,8 +31,7 @@ export const receiveMessagePreview = publicProcedure if (isNotDefined(receivedMessage)) return { message: 'No message found' } const contactName = entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? '' - const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.metadata.display_phone_number ?? '' + const contactPhoneNumber = '+' + receivedMessage.from return resumeWhatsAppFlow({ receivedMessage, sessionId: `wa-${receivedMessage.from}-preview`, diff --git a/apps/builder/src/features/whatsapp/router.ts b/apps/builder/src/features/whatsapp/router.ts index e9c0114a5..6f4dd298c 100644 --- a/apps/builder/src/features/whatsapp/router.ts +++ b/apps/builder/src/features/whatsapp/router.ts @@ -3,10 +3,16 @@ import { getPhoneNumber } from './getPhoneNumber' import { getSystemTokenInfo } from './getSystemTokenInfo' import { verifyIfPhoneNumberAvailable } from './verifyIfPhoneNumberAvailable' import { generateVerificationToken } from './generateVerificationToken' +import { startWhatsAppPreview } from './startWhatsAppPreview' +import { subscribePreviewWebhook } from './subscribePreviewWebhook' +import { receiveMessagePreview } from './receiveMessagePreview' export const whatsAppRouter = router({ getPhoneNumber, getSystemTokenInfo, verifyIfPhoneNumberAvailable, generateVerificationToken, + startWhatsAppPreview, + subscribePreviewWebhook, + receiveMessagePreview, }) diff --git a/apps/viewer/src/features/whatsApp/api/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts similarity index 64% rename from apps/viewer/src/features/whatsApp/api/startWhatsAppPreview.ts rename to apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index a5e3186ce..02ea9f0d6 100644 --- a/apps/viewer/src/features/whatsApp/api/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -1,21 +1,24 @@ -import { publicProcedure } from '@/helpers/server/trpc' +import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' import { TRPCError } from '@trpc/server' -import { sendWhatsAppMessage } from '../helpers/sendWhatsAppMessage' -import { startSession } from '@/features/chat/helpers/startSession' -import { restartSession } from '@/features/chat/queries/restartSession' +import { sendWhatsAppMessage } from '@typebot.io/lib/whatsApp/sendWhatsAppMessage' +import { startSession } from '@typebot.io/viewer/src/features/chat/helpers/startSession' import { env } from '@typebot.io/env' import { HTTPError } from 'got' import prisma from '@/lib/prisma' -import { sendChatReplyToWhatsApp } from '../helpers/sendChatReplyToWhatsApp' -import { saveStateToDatabase } from '@/features/chat/helpers/saveStateToDatabase' +import { sendChatReplyToWhatsApp } from '@typebot.io/lib/whatsApp/sendChatReplyToWhatsApp' +import { saveStateToDatabase } from '@typebot.io/viewer/src/features/chat/helpers/saveStateToDatabase' +import { restartSession } from '@typebot.io/viewer/src/features/chat/queries/restartSession' +import { isReadTypebotForbidden } from '../typebot/helpers/isReadTypebotForbidden' +import { SessionState } from '@typebot.io/schemas' -export const startWhatsAppPreview = publicProcedure +export const startWhatsAppPreview = authenticatedProcedure .meta({ openapi: { method: 'POST', path: '/typebots/{typebotId}/whatsapp/start-preview', - summary: 'Start WhatsApp Preview', + summary: 'Start preview', + tags: ['WhatsApp'], protect: true, }, }) @@ -38,20 +41,35 @@ export const startWhatsAppPreview = publicProcedure async ({ input: { to, typebotId, startGroupId }, ctx: { user } }) => { if ( !env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID || - !env.META_SYSTEM_USER_TOKEN + !env.META_SYSTEM_USER_TOKEN || + !env.WHATSAPP_PREVIEW_TEMPLATE_NAME ) throw new TRPCError({ code: 'BAD_REQUEST', message: - 'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID and/or META_SYSTEM_USER_TOKEN env variables', - }) - if (!user) - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: - 'You need to authenticate your request in order to start a preview', + 'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID or META_SYSTEM_USER_TOKEN or WHATSAPP_PREVIEW_TEMPLATE_NAME env variables', }) + const existingTypebot = await prisma.typebot.findFirst({ + where: { + id: typebotId, + }, + select: { + id: true, + workspaceId: true, + collaborators: { + select: { + userId: true, + }, + }, + }, + }) + if ( + !existingTypebot?.id || + (await isReadTypebotForbidden(existingTypebot, user)) + ) + throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) + const sessionId = `wa-${to}-preview` const existingSession = await prisma.chatSession.findFirst({ @@ -60,6 +78,7 @@ export const startWhatsAppPreview = publicProcedure }, select: { updatedAt: true, + state: true, }, }) @@ -105,7 +124,11 @@ export const startWhatsAppPreview = publicProcedure }) } else { await restartSession({ - state: newSessionState, + state: { + ...newSessionState, + whatsApp: (existingSession?.state as SessionState | undefined) + ?.whatsApp, + }, id: `wa-${to}-preview`, }) try { @@ -115,9 +138,9 @@ export const startWhatsAppPreview = publicProcedure type: 'template', template: { language: { - code: 'en', + code: env.WHATSAPP_PREVIEW_TEMPLATE_LANG, }, - name: 'preview_initial_message', + name: env.WHATSAPP_PREVIEW_TEMPLATE_NAME, }, }, credentials: { diff --git a/apps/viewer/src/features/whatsApp/api/subscribePreviewWebhook.ts b/apps/builder/src/features/whatsapp/subscribePreviewWebhook.ts similarity index 92% rename from apps/viewer/src/features/whatsApp/api/subscribePreviewWebhook.ts rename to apps/builder/src/features/whatsapp/subscribePreviewWebhook.ts index 18c6fe761..b08d49af7 100644 --- a/apps/viewer/src/features/whatsApp/api/subscribePreviewWebhook.ts +++ b/apps/builder/src/features/whatsapp/subscribePreviewWebhook.ts @@ -8,7 +8,8 @@ export const subscribePreviewWebhook = publicProcedure openapi: { method: 'GET', path: '/whatsapp/preview/webhook', - summary: 'WhatsApp', + summary: 'Subscribe webhook', + tags: ['WhatsApp'], }, }) .input( diff --git a/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts b/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts index f7cb74838..0d5a597ef 100644 --- a/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts +++ b/apps/builder/src/helpers/server/routers/v1/trpcRouter.ts @@ -3,7 +3,6 @@ import { webhookRouter } from '@/features/blocks/integrations/webhook/api/router import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api/getLinkedTypebots' import { credentialsRouter } from '@/features/credentials/api/router' import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure' -import { sendWhatsAppInitialMessage } from '@/features/preview/api/sendWhatsAppInitialMessage' import { resultsRouter } from '@/features/results/api/router' import { processTelemetryEvent } from '@/features/telemetry/api/processTelemetryEvent' import { themeRouter } from '@/features/theme/api/router' @@ -23,7 +22,6 @@ export const trpcRouter = router({ processTelemetryEvent, getLinkedTypebots, analytics: analyticsRouter, - sendWhatsAppInitialMessage, workspace: workspaceRouter, typebot: typebotRouter, webhook: webhookRouter, diff --git a/apps/builder/tsconfig.json b/apps/builder/tsconfig.json index 84e4ae1b4..5ec9b33e2 100644 --- a/apps/builder/tsconfig.json +++ b/apps/builder/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": ["src/*", "../viewer/src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] diff --git a/apps/docs/docs/self-hosting/configuration.md b/apps/docs/docs/self-hosting/configuration.md index b0aae723f..4bfed6133 100644 --- a/apps/docs/docs/self-hosting/configuration.md +++ b/apps/docs/docs/self-hosting/configuration.md @@ -200,22 +200,54 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne

Requirements

-1. Make sure you have [created a WhatsApp Business Account](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#set-up-developer-assets). -2. Go to your [System users page](https://business.facebook.com/settings/system-users) and create a new system user that has access to the related. +### Create a Facebook Business account + +1. Head over to https://business.facebook.com and log in +2. Create a new business account on the left side bar + +:::note +It is possible that Meta directly restricts your newly created Business account. In that case, make sure to verify your identity to proceed. +::: + +### Create a Meta app + +1. Head over to https://developers.facebook.com/apps +2. Click on Create App +3. Give it any name and select `Business` type +4. Select your newly created Business Account +5. On the app page, set up the `WhatsApp` product + +### Get the System User token + +1. Go to your [System users page](https://business.facebook.com/settings/system-users) and create a new system user that has access to the related. - Token expiration: `Never` - Available Permissions: `whatsapp_business_messaging`, `whatsapp_business_management` -3. The generated token will be used as `META_SYSTEM_USER_TOKEN` in your viewer configuration. -4. Click on `Add assets`. Under `Apps`, look for your app, select it and check `Manage app` -5. Go to your WhatsApp Dev Console +2. The generated token will be used as `META_SYSTEM_USER_TOKEN` in your viewer configuration. +3. Click on `Add assets`. Under `Apps`, look for your app, select it and check `Manage app` + +### Get the phone number ID + +1. Go to your WhatsApp Dev Console WhatsApp dev console -6. Add your phone number by clicking on the `Add phone number` button. -7. Select the newly created phone number in the `From` dropdown list. This will be used as `WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID` in your viewer configuration. -8. Head over to `Quickstart > Configuration`. Edit the webhook URL to `$NEXT_PUBLIC_VIEWER_URL/api/v1/whatsapp/preview/webhook`. Set the Verify token to `$ENCRYPTION_SECRET` and click on `Verify and save`. -9. Add the `messages` webhook field. +2. Add your phone number by clicking on the `Add phone number` button. +3. Select the newly created phone number in the `From` dropdown list and you will see right below the associated `Phone number ID` This will be used as `WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID` in your viewer configuration. + +### Set up the webhook + +1. Head over to `Quickstart > Configuration`. Edit the webhook URL to `$NEXTAUTH_URL/api/v1/whatsapp/preview/webhook`. Set the Verify token to `$ENCRYPTION_SECRET` and click on `Verify and save`. +2. Add the `messages` webhook field. + +### Set up the message template + +1. Head over to `Messaging > Message Templates` and click on `Create Template` +2. Select the `Utility` category +3. Give it a name that corresponds to your `WHATSAPP_PREVIEW_TEMPLATE_NAME` configuration. +4. Select the language that corresponds to your `WHATSAPP_PREVIEW_TEMPLATE_LANG` configuration. +5. You can format it as you'd like. The user will just have to send a message to start the preview.

@@ -223,6 +255,8 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne | ------------------------------------- | ------- | ------------------------------------------------------- | | META_SYSTEM_USER_TOKEN | | The system user token used to send WhatsApp messages | | WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID | | The phone number ID from which the message will be sent | +| WHATSAPP_PREVIEW_TEMPLATE_NAME | | The preview start template message name | +| WHATSAPP_PREVIEW_TEMPLATE_LANG | en | The preview start template message name | ## Others diff --git a/apps/docs/openapi/builder/_spec_.json b/apps/docs/openapi/builder/_spec_.json index 77279272e..c216a1364 100644 --- a/apps/docs/openapi/builder/_spec_.json +++ b/apps/docs/openapi/builder/_spec_.json @@ -32433,63 +32433,6 @@ } } }, - "delete": { - "operationId": "customDomains-deleteCustomDomain", - "summary": "Delete custom domain", - "tags": [ - "Custom domains" - ], - "security": [ - { - "Authorization": [] - } - ], - "parameters": [ - { - "name": "workspaceId", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "name", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "enum": [ - "success" - ] - } - }, - "required": [ - "message" - ], - "additionalProperties": false - } - } - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - }, "get": { "operationId": "customDomains-listCustomDomains", "summary": "List custom domains", @@ -32554,6 +32497,205 @@ } } }, + "/custom-domains/{name}": { + "delete": { + "operationId": "customDomains-deleteCustomDomain", + "summary": "Delete custom domain", + "tags": [ + "Custom domains" + ], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "workspaceId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "enum": [ + "success" + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/custom-domains/{name}/verify": { + "get": { + "operationId": "customDomains-verifyCustomDomain", + "summary": "Verify domain config", + "tags": [ + "Custom domains" + ], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "workspaceId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Valid Configuration", + "Invalid Configuration", + "Domain Not Found", + "Pending Verification", + "Unknown Error" + ] + }, + "domainJson": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "apexName": { + "type": "string" + }, + "projectId": { + "type": "string" + }, + "redirect": { + "type": "string", + "nullable": true + }, + "redirectStatusCode": { + "type": "number", + "nullable": true + }, + "gitBranch": { + "type": "string", + "nullable": true + }, + "updatedAt": { + "type": "number", + "nullable": true + }, + "createdAt": { + "type": "number", + "nullable": true + }, + "verified": { + "type": "boolean" + }, + "verification": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "value": { + "type": "string" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "type", + "domain", + "value", + "reason" + ], + "additionalProperties": false + } + } + }, + "required": [ + "name", + "apexName", + "projectId", + "redirect", + "redirectStatusCode", + "gitBranch", + "updatedAt", + "createdAt", + "verified" + ], + "additionalProperties": false + } + }, + "required": [ + "status", + "domainJson" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/whatsapp/phoneNumber": { "get": { "operationId": "whatsApp-getPhoneNumber", @@ -32768,6 +32910,497 @@ } } }, + "/typebots/{typebotId}/whatsapp/start-preview": { + "post": { + "operationId": "whatsApp-startWhatsAppPreview", + "summary": "Start preview", + "tags": [ + "WhatsApp" + ], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "to": { + "type": "string", + "minLength": 1 + }, + "startGroupId": { + "type": "string" + } + }, + "required": [ + "to" + ], + "additionalProperties": false + } + } + } + }, + "parameters": [ + { + "name": "typebotId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/whatsapp/preview/webhook": { + "get": { + "operationId": "whatsApp-subscribePreviewWebhook", + "summary": "Subscribe webhook", + "tags": [ + "WhatsApp" + ], + "parameters": [ + { + "name": "hub.challenge", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "hub.verify_token", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "number" + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + }, + "post": { + "operationId": "whatsApp-receiveMessagePreview", + "summary": "Message webhook", + "tags": [ + "WhatsApp" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "entry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "changes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "profile": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "profile" + ], + "additionalProperties": false + } + }, + "messages": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "text" + ] + }, + "text": { + "type": "object", + "properties": { + "body": { + "type": "string" + } + }, + "required": [ + "body" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "text", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "button" + ] + }, + "button": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "payload": { + "type": "string" + } + }, + "required": [ + "text", + "payload" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "button", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "interactive" + ] + }, + "interactive": { + "type": "object", + "properties": { + "button_reply": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "id", + "title" + ], + "additionalProperties": false + } + }, + "required": [ + "button_reply" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "interactive", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "image" + ] + }, + "image": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "image", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "video" + ] + }, + "video": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "video", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "audio" + ] + }, + "audio": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "audio", + "timestamp" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "document" + ] + }, + "document": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "document", + "timestamp" + ], + "additionalProperties": false + } + ] + } + } + }, + "additionalProperties": false + } + }, + "required": [ + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "changes" + ], + "additionalProperties": false + } + } + }, + "required": [ + "entry" + ], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/openai/models": { "get": { "operationId": "openAI-listModels", diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index e8e9e671d..5adbd7f67 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -6496,435 +6496,6 @@ } } }, - "/whatsapp/preview/webhook": { - "get": { - "operationId": "whatsAppRouter-subscribePreviewWebhook", - "summary": "WhatsApp", - "parameters": [ - { - "name": "hub.challenge", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "hub.verify_token", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "number" - } - } - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - }, - "post": { - "operationId": "whatsAppRouter-receiveMessagePreview", - "summary": "WhatsApp", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "entry": { - "type": "array", - "items": { - "type": "object", - "properties": { - "changes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "value": { - "type": "object", - "properties": { - "contacts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "profile": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - } - }, - "required": [ - "profile" - ], - "additionalProperties": false - } - }, - "metadata": { - "type": "object", - "properties": { - "display_phone_number": { - "type": "string" - } - }, - "required": [ - "display_phone_number" - ], - "additionalProperties": false - }, - "messages": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "text" - ] - }, - "text": { - "type": "object", - "properties": { - "body": { - "type": "string" - } - }, - "required": [ - "body" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "text", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "button" - ] - }, - "button": { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "payload": { - "type": "string" - } - }, - "required": [ - "text", - "payload" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "button", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "interactive" - ] - }, - "interactive": { - "type": "object", - "properties": { - "button_reply": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "required": [ - "id", - "title" - ], - "additionalProperties": false - } - }, - "required": [ - "button_reply" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "interactive", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "image" - ] - }, - "image": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "image", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "video" - ] - }, - "video": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "video", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "audio" - ] - }, - "audio": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "audio", - "timestamp" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "from": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "document" - ] - }, - "document": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "additionalProperties": false - }, - "timestamp": { - "type": "string" - } - }, - "required": [ - "from", - "type", - "document", - "timestamp" - ], - "additionalProperties": false - } - ] - } - } - }, - "required": [ - "metadata" - ], - "additionalProperties": false - } - }, - "required": [ - "value" - ], - "additionalProperties": false - } - } - }, - "required": [ - "changes" - ], - "additionalProperties": false - } - } - }, - "required": [ - "entry" - ], - "additionalProperties": false - } - } - } - }, - "parameters": [], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "additionalProperties": false - } - } - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - } - }, "/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook": { "get": { "operationId": "whatsAppRouter-subscribeWebhook", @@ -7031,18 +6602,6 @@ "additionalProperties": false } }, - "metadata": { - "type": "object", - "properties": { - "display_phone_number": { - "type": "string" - } - }, - "required": [ - "display_phone_number" - ], - "additionalProperties": false - }, "messages": { "type": "array", "items": { @@ -7320,9 +6879,6 @@ } } }, - "required": [ - "metadata" - ], "additionalProperties": false } }, @@ -7391,74 +6947,6 @@ } } } - }, - "/typebots/{typebotId}/whatsapp/start-preview": { - "post": { - "operationId": "whatsAppRouter-startWhatsAppPreview", - "summary": "Start WhatsApp Preview", - "security": [ - { - "Authorization": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "to": { - "type": "string", - "minLength": 1 - }, - "startGroupId": { - "type": "string" - } - }, - "required": [ - "to" - ], - "additionalProperties": false - } - } - } - }, - "parameters": [ - { - "name": "typebotId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ], - "additionalProperties": false - } - } - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - } } }, "components": { diff --git a/apps/viewer/package.json b/apps/viewer/package.json index 80b7cc028..4838840ca 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -1,5 +1,5 @@ { - "name": "viewer", + "name": "@typebot.io/viewer", "license": "AGPL-3.0-or-later", "version": "0.1.0", "scripts": { diff --git a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts index d6295b9c6..1462e3185 100644 --- a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts +++ b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts @@ -79,7 +79,7 @@ const getExpressionToEvaluate = case 'Contact name': return state.whatsApp?.contact.name ?? '' case 'Phone number': - return state.whatsApp?.contact.phoneNumber ?? '' + return `"${state.whatsApp?.contact.phoneNumber}"` ?? '' case 'Now': case 'Today': return 'new Date().toISOString()' diff --git a/apps/viewer/src/features/whatsApp/api/receiveMessage.ts b/apps/viewer/src/features/whatsApp/api/receiveMessage.ts index 85bd64086..ff0907e0a 100644 --- a/apps/viewer/src/features/whatsApp/api/receiveMessage.ts +++ b/apps/viewer/src/features/whatsApp/api/receiveMessage.ts @@ -9,7 +9,8 @@ export const receiveMessage = publicProcedure openapi: { method: 'POST', path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook', - summary: 'Receive WhatsApp Message', + summary: 'Message webhook', + tags: ['WhatsApp'], }, }) .input( @@ -28,7 +29,7 @@ export const receiveMessage = publicProcedure const contactName = entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? '' const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.metadata.display_phone_number ?? '' + entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? '' return resumeWhatsAppFlow({ receivedMessage, sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`, diff --git a/apps/viewer/src/features/whatsApp/api/router.ts b/apps/viewer/src/features/whatsApp/api/router.ts index 579ace950..b9cd890ee 100644 --- a/apps/viewer/src/features/whatsApp/api/router.ts +++ b/apps/viewer/src/features/whatsApp/api/router.ts @@ -1,14 +1,8 @@ import { router } from '@/helpers/server/trpc' -import { receiveMessagePreview } from './receiveMessagePreview' -import { startWhatsAppPreview } from './startWhatsAppPreview' -import { subscribePreviewWebhook } from './subscribePreviewWebhook' import { subscribeWebhook } from './subscribeWebhook' import { receiveMessage } from './receiveMessage' export const whatsAppRouter = router({ - subscribePreviewWebhook, subscribeWebhook, - receiveMessagePreview, receiveMessage, - startWhatsAppPreview, }) diff --git a/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts b/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts index 46b9da984..b0e64b399 100644 --- a/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts +++ b/apps/viewer/src/features/whatsApp/api/subscribeWebhook.ts @@ -8,7 +8,8 @@ export const subscribeWebhook = publicProcedure openapi: { method: 'GET', path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook', - summary: 'Subscribe WhatsApp webhook', + summary: 'Subscribe webhook', + tags: ['WhatsApp'], protect: true, }, }) diff --git a/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts b/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts index 32fac7380..fcb14a824 100644 --- a/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts +++ b/apps/viewer/src/features/whatsApp/helpers/resumeWhatsAppFlow.ts @@ -11,7 +11,7 @@ import prisma from '@/lib/prisma' import { decrypt } from '@typebot.io/lib/api' import { downloadMedia } from './downloadMedia' import { env } from '@typebot.io/env' -import { sendChatReplyToWhatsApp } from './sendChatReplyToWhatsApp' +import { sendChatReplyToWhatsApp } from '@typebot.io/lib/whatsApp/sendChatReplyToWhatsApp' export const resumeWhatsAppFlow = async ({ receivedMessage, diff --git a/packages/env/env.ts b/packages/env/env.ts index bc460ef75..6f6abfdc9 100644 --- a/packages/env/env.ts +++ b/packages/env/env.ts @@ -230,6 +230,8 @@ const whatsAppEnv = { server: { META_SYSTEM_USER_TOKEN: z.string().min(1).optional(), WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID: z.string().min(1).optional(), + WHATSAPP_PREVIEW_TEMPLATE_NAME: z.string().min(1).optional(), + WHATSAPP_PREVIEW_TEMPLATE_LANG: z.string().min(1).optional().default('en'), }, } diff --git a/packages/lib/package.json b/packages/lib/package.json index 55479b010..d27e833b5 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -8,21 +8,24 @@ "devDependencies": { "@paralleldrive/cuid2": "2.2.1", "@playwright/test": "1.36.0", + "@typebot.io/env": "workspace:*", "@typebot.io/prisma": "workspace:*", "@typebot.io/schemas": "workspace:*", "@typebot.io/tsconfig": "workspace:*", "@types/nodemailer": "6.4.8", "next": "13.4.3", "nodemailer": "6.9.3", - "typescript": "5.1.6", - "@typebot.io/env": "workspace:*" + "typescript": "5.1.6" }, "peerDependencies": { "next": "13.0.0", "nodemailer": "6.7.8" }, "dependencies": { + "@sentry/nextjs": "7.66.0", + "@udecode/plate-common": "^21.1.5", "got": "12.6.0", - "minio": "7.1.3" + "minio": "7.1.3", + "remark-slate": "^1.8.6" } } diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 57c1b5237..ce77240db 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "@typebot.io/tsconfig/base.json", "include": ["**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], + "compilerOptions": { + "target": "ES2021" + } } diff --git a/apps/viewer/src/features/whatsApp/helpers/convertInputToWhatsAppMessage.ts b/packages/lib/whatsApp/convertInputToWhatsAppMessage.ts similarity index 98% rename from apps/viewer/src/features/whatsApp/helpers/convertInputToWhatsAppMessage.ts rename to packages/lib/whatsApp/convertInputToWhatsAppMessage.ts index be670dba8..bf17774d7 100644 --- a/apps/viewer/src/features/whatsApp/helpers/convertInputToWhatsAppMessage.ts +++ b/packages/lib/whatsApp/convertInputToWhatsAppMessage.ts @@ -1,4 +1,3 @@ -import { isDefined, isEmpty } from '@typebot.io/lib' import { BubbleBlockType, ButtonItem, @@ -7,6 +6,7 @@ import { } from '@typebot.io/schemas' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' +import { isDefined, isEmpty } from '../utils' export const convertInputToWhatsAppMessages = ( input: NonNullable, diff --git a/apps/viewer/src/features/whatsApp/helpers/convertMessageToWhatsAppMessage.ts b/packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts similarity index 98% rename from apps/viewer/src/features/whatsApp/helpers/convertMessageToWhatsAppMessage.ts rename to packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts index f4f9d89fb..e2ec2cf6f 100644 --- a/apps/viewer/src/features/whatsApp/helpers/convertMessageToWhatsAppMessage.ts +++ b/packages/lib/whatsApp/convertMessageToWhatsAppMessage.ts @@ -5,7 +5,7 @@ import { } from '@typebot.io/schemas' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' -import { isSvgSrc } from '@typebot.io/lib' +import { isSvgSrc } from '../utils' const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/ diff --git a/apps/viewer/src/features/whatsApp/helpers/convertRichTextToWhatsAppText.ts b/packages/lib/whatsApp/convertRichTextToWhatsAppText.ts similarity index 100% rename from apps/viewer/src/features/whatsApp/helpers/convertRichTextToWhatsAppText.ts rename to packages/lib/whatsApp/convertRichTextToWhatsAppText.ts diff --git a/apps/viewer/src/features/whatsApp/helpers/sendChatReplyToWhatsApp.ts b/packages/lib/whatsApp/sendChatReplyToWhatsApp.ts similarity index 97% rename from apps/viewer/src/features/whatsApp/helpers/sendChatReplyToWhatsApp.ts rename to packages/lib/whatsApp/sendChatReplyToWhatsApp.ts index 51b499ba6..b8d8cb584 100644 --- a/apps/viewer/src/features/whatsApp/helpers/sendChatReplyToWhatsApp.ts +++ b/packages/lib/whatsApp/sendChatReplyToWhatsApp.ts @@ -11,10 +11,10 @@ import { import { convertMessageToWhatsAppMessage } from './convertMessageToWhatsAppMessage' import { sendWhatsAppMessage } from './sendWhatsAppMessage' import { captureException } from '@sentry/nextjs' -import { isNotDefined } from '@typebot.io/lib/utils' import { HTTPError } from 'got' -import { computeTypingDuration } from '@typebot.io/lib/computeTypingDuration' import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage' +import { isNotDefined } from '../utils' +import { computeTypingDuration } from '../computeTypingDuration' // Media can take some time to be delivered. This make sure we don't send a message before the media is delivered. const messageAfterMediaTimeout = 5000 diff --git a/apps/viewer/src/features/whatsApp/helpers/sendWhatsAppMessage.ts b/packages/lib/whatsApp/sendWhatsAppMessage.ts similarity index 100% rename from apps/viewer/src/features/whatsApp/helpers/sendWhatsAppMessage.ts rename to packages/lib/whatsApp/sendWhatsAppMessage.ts diff --git a/packages/schemas/features/whatsapp.ts b/packages/schemas/features/whatsapp.ts index 77b3ecd5b..48860637a 100644 --- a/packages/schemas/features/whatsapp.ts +++ b/packages/schemas/features/whatsapp.ts @@ -36,9 +36,9 @@ const actionSchema = z.object({ }) const templateSchema = z.object({ - name: z.literal('preview_initial_message'), + name: z.string(), language: z.object({ - code: z.literal('en'), + code: z.string(), }), }) @@ -151,9 +151,6 @@ export const whatsAppWebhookRequestBodySchema = z.object({ }) ) .optional(), - metadata: z.object({ - display_phone_number: z.string(), - }), messages: z.array(incomingMessageSchema).optional(), }), }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fedfd7ab..398f37480 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: '@typebot.io/nextjs': specifier: workspace:* version: link:../../packages/embeds/nextjs + '@typebot.io/viewer': + specifier: workspace:* + version: link:../viewer '@udecode/plate-basic-marks': specifier: 21.1.5 version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) @@ -1118,12 +1121,21 @@ importers: packages/lib: dependencies: + '@sentry/nextjs': + specifier: 7.66.0 + version: 7.66.0(next@13.4.3)(react@18.2.0) + '@udecode/plate-common': + specifier: ^21.1.5 + version: 21.1.5(@babel/core@7.22.9)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-react@0.94.2)(slate@0.94.1) got: specifier: 12.6.0 version: 12.6.0 minio: specifier: 7.1.3 version: 7.1.3 + remark-slate: + specifier: ^1.8.6 + version: 1.8.6 devDependencies: '@paralleldrive/cuid2': specifier: 2.2.1