2
0

(whatsapp) Improve WhatsApp preview management

Closes #800
This commit is contained in:
Baptiste Arnaud
2023-09-19 11:53:18 +02:00
parent 2ce63f5d06
commit f626c9867c
29 changed files with 830 additions and 681 deletions

View File

@ -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",

View File

@ -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' }
}
)

View File

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

View File

@ -5,7 +5,7 @@ import { Typebot } from '@typebot.io/schemas'
export const isReadTypebotForbidden = async (
typebot: Pick<Typebot, 'workspaceId'> & {
collaborators: Pick<CollaboratorsOnTypebots, 'userId' | 'type'>[]
collaborators: Pick<CollaboratorsOnTypebots, 'userId'>[]
},
user: Pick<User, 'email' | 'id'>
) => {

View File

@ -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`,

View File

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

View File

@ -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,19 +41,34 @@ 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',
'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID or META_SYSTEM_USER_TOKEN or WHATSAPP_PREVIEW_TEMPLATE_NAME env variables',
})
if (!user)
throw new TRPCError({
code: 'UNAUTHORIZED',
message:
'You need to authenticate your request in order to start a preview',
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`
@ -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: {

View File

@ -8,7 +8,8 @@ export const subscribePreviewWebhook = publicProcedure
openapi: {
method: 'GET',
path: '/whatsapp/preview/webhook',
summary: 'WhatsApp',
summary: 'Subscribe webhook',
tags: ['WhatsApp'],
},
})
.input(

View File

@ -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,

View File

@ -3,7 +3,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["src/*", "../viewer/src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]

View File

@ -200,22 +200,54 @@ In order to be able to test your bot on WhatsApp from the Preview drawer, you ne
<details><summary><h4>Requirements</h4></summary>
<p>
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
<img src="/img/whatsapp/dev-console.png" alt="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.
</p></details>
@ -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

View File

@ -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",

View File

@ -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": {

View File

@ -1,5 +1,5 @@
{
"name": "viewer",
"name": "@typebot.io/viewer",
"license": "AGPL-3.0-or-later",
"version": "0.1.0",
"scripts": {

View File

@ -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()'

View File

@ -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}`,

View File

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

View File

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

View File

@ -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,

2
packages/env/env.ts vendored
View File

@ -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'),
},
}

View File

@ -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"
}
}

View File

@ -1,5 +1,8 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
"compilerOptions": {
"target": "ES2021"
}
}

View File

@ -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<ChatReply['input']>,

View File

@ -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$/

View File

@ -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

View File

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

12
pnpm-lock.yaml generated
View File

@ -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