2
0

🐛 (whatsapp) Fix WA preview not starting and accept audio and documents messages

This commit is contained in:
Baptiste Arnaud
2023-12-20 10:35:34 +01:00
parent a6536461e5
commit 780b4dee18
28 changed files with 619 additions and 192 deletions

View File

@ -10,15 +10,6 @@ import { OpenAI, ClientOptions } from 'openai'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
export const listModels = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/openai/models',
protect: true,
summary: 'List OpenAI models',
tags: ['OpenAI'],
},
})
.input(
z.object({
credentialsId: z.string(),
@ -28,11 +19,6 @@ export const listModels = authenticatedProcedure
type: z.enum(['gpt', 'tts']),
})
)
.output(
z.object({
models: z.array(z.string()),
})
)
.query(
async ({
input: { credentialsId, workspaceId, baseUrl, apiVersion, type },

View File

@ -8,31 +8,12 @@ import { ZemanticAiCredentials } from '@typebot.io/schemas/features/blocks/integ
import got from 'got'
export const listProjects = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/zemantic-ai/projects',
protect: true,
summary: 'List Zemantic AI projects',
tags: ['ZemanticAi'],
},
})
.input(
z.object({
credentialsId: z.string(),
workspaceId: z.string(),
})
)
.output(
z.object({
projects: z.array(
z.object({
label: z.string(),
value: z.string(),
})
),
})
)
.query(async ({ input: { credentialsId, workspaceId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId },

View File

@ -14,13 +14,6 @@ const inputShape = {
} as const
export const createCredentials = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/credentials',
protect: true,
},
})
.input(
z.object({
credentials: z.discriminatedUnion(
@ -31,11 +24,6 @@ export const createCredentials = authenticatedProcedure
),
})
)
.output(
z.object({
credentialsId: z.string(),
})
)
.mutation(async ({ input: { credentials }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {

View File

@ -11,11 +11,6 @@ export const deleteCredentials = authenticatedProcedure
workspaceId: z.string(),
})
)
.output(
z.object({
credentialsId: z.string(),
})
)
.mutation(
async ({ input: { credentialsId, workspaceId }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({

View File

@ -12,11 +12,6 @@ export const listCredentials = authenticatedProcedure
type: z.enum(enabledBlocks),
})
)
.output(
z.object({
credentials: z.array(z.object({ id: z.string(), name: z.string() })),
})
)
.query(async ({ input: { workspaceId, type }, ctx: { user } }) => {
const workspace = await prisma.workspace.findFirst({
where: {

View File

@ -1,10 +0,0 @@
import { router } from '@/helpers/server/trpc'
import { createCredentials } from './createCredentials'
import { deleteCredentials } from './deleteCredentials'
import { listCredentials } from './listCredentials'
export const forgedCredentialsRouter = router({
createCredentials,
listCredentials,
deleteCredentials,
})

View File

@ -15,13 +15,6 @@ export const fetchSelectItems = authenticatedProcedure
workspaceId: z.string(),
})
)
.output(
z.object({
items: z.array(
z.string().or(z.object({ label: z.string(), value: z.string() }))
),
})
)
.query(
async ({
input: { workspaceId, integrationId, fetcherId, options },

View File

@ -1,6 +1,12 @@
import { router } from '@/helpers/server/trpc'
import { fetchSelectItems } from './fetchSelectItems'
import { createCredentials } from './credentials/createCredentials'
import { deleteCredentials } from './credentials/deleteCredentials'
import { listCredentials } from './credentials/listCredentials'
export const integrationsRouter = router({
export const forgeRouter = router({
fetchSelectItems,
createCredentials,
listCredentials,
deleteCredentials,
})

View File

@ -54,7 +54,7 @@ export const ForgeSelectInput = ({
return fetchers.find((fetcher) => fetcher.id === fetcherId)
}, [baseFetcher, blockDef.actions, fetcherId])
const { data } = trpc.integrations.fetchSelectItems.useQuery(
const { data } = trpc.forge.fetchSelectItems.useQuery(
{
integrationId: blockDef.id,
options: pick(options, [

View File

@ -34,14 +34,13 @@ export const ForgedCredentialsDropdown = ({
const router = useRouter()
const { showToast } = useToast()
const { workspace, currentRole } = useWorkspace()
const { data, refetch, isLoading } =
trpc.integrationCredentials.listCredentials.useQuery(
{
workspaceId: workspace?.id as string,
type: blockDef.id,
},
{ enabled: !!workspace?.id }
)
const { data, refetch, isLoading } = trpc.forge.listCredentials.useQuery(
{
workspaceId: workspace?.id as string,
type: blockDef.id,
},
{ enabled: !!workspace?.id }
)
const [isDeleting, setIsDeleting] = useState<string>()
const { mutate } = trpc.credentials.deleteCredentials.useMutation({

View File

@ -42,7 +42,7 @@ export const ForgedCredentialsModal = ({
listCredentials: { refetch: refetchCredentials },
},
} = trpc.useContext()
const { mutate } = trpc.integrationCredentials.createCredentials.useMutation({
const { mutate } = trpc.forge.createCredentials.useMutation({
onMutate: () => setIsCreating(true),
onSettled: () => setIsCreating(false),
onError: (err) => {

View File

@ -100,12 +100,13 @@ export const WhatsAppCredentialsModal = ({
},
})
const { data: tokenInfoData } = trpc.whatsApp.getSystemTokenInfo.useQuery(
{
token: systemUserAccessToken,
},
{ enabled: isNotEmpty(systemUserAccessToken) }
)
const { data: tokenInfoData } =
trpc.whatsAppInternal.getSystemTokenInfo.useQuery(
{
token: systemUserAccessToken,
},
{ enabled: isNotEmpty(systemUserAccessToken) }
)
const resetForm = () => {
setActiveStep(0)
@ -133,7 +134,7 @@ export const WhatsAppCredentialsModal = ({
setIsVerifying(true)
try {
const { expiresAt, scopes } =
await trpcVanilla.whatsApp.getSystemTokenInfo.query({
await trpcVanilla.whatsAppInternal.getSystemTokenInfo.query({
token: systemUserAccessToken,
})
if (expiresAt !== 0) {
@ -167,16 +168,18 @@ export const WhatsAppCredentialsModal = ({
const isPhoneNumberAvailable = async () => {
setIsVerifying(true)
try {
const { name } = await trpcVanilla.whatsApp.getPhoneNumber.query({
const { name } = await trpcVanilla.whatsAppInternal.getPhoneNumber.query({
systemToken: systemUserAccessToken,
phoneNumberId,
})
setPhoneNumberName(name)
try {
const { message } =
await trpcVanilla.whatsApp.verifyIfPhoneNumberAvailable.query({
phoneNumberDisplayName: name,
})
await trpcVanilla.whatsAppInternal.verifyIfPhoneNumberAvailable.query(
{
phoneNumberDisplayName: name,
}
)
if (message === 'taken') {
setIsVerifying(false)
@ -186,7 +189,7 @@ export const WhatsAppCredentialsModal = ({
return false
}
const { verificationToken } =
await trpcVanilla.whatsApp.generateVerificationToken.mutate()
await trpcVanilla.whatsAppInternal.generateVerificationToken.mutate()
setVerificationToken(verificationToken)
} catch (err) {
console.error(err)

View File

@ -56,14 +56,15 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
const whatsAppSettings = typebot?.settings.whatsApp
const { data: phoneNumberData } = trpc.whatsApp.getPhoneNumber.useQuery(
{
credentialsId: typebot?.whatsAppCredentialsId as string,
},
{
enabled: !!typebot?.whatsAppCredentialsId,
}
)
const { data: phoneNumberData } =
trpc.whatsAppInternal.getPhoneNumber.useQuery(
{
credentialsId: typebot?.whatsAppCredentialsId as string,
},
{
enabled: !!typebot?.whatsAppCredentialsId,
}
)
const toggleEnableWhatsApp = (isChecked: boolean) => {
if (!phoneNumberData?.id || !typebot) return

View File

@ -42,22 +42,7 @@ export type FilePathUploadProps = z.infer<
>
export const generateUploadUrl = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/generate-upload-url',
summary: 'Generate upload URL',
description: 'Generate the needed URL to upload a file from the client',
},
})
.input(inputSchema)
.output(
z.object({
presignedUrl: z.string(),
formData: z.record(z.string(), z.any()),
fileUrl: z.string(),
})
)
.mutation(async ({ input: { filePathProps, fileType }, ctx: { user } }) => {
if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
throw new TRPCError({

View File

@ -1,23 +1,9 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import prisma from '@typebot.io/lib/prisma'
import { createId } from '@paralleldrive/cuid2'
export const generateVerificationToken = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/verficiationTokens',
protect: true,
},
})
.input(z.void())
.output(
z.object({
verificationToken: z.string(),
})
)
.mutation(async () => {
export const generateVerificationToken = authenticatedProcedure.mutation(
async () => {
const oneHourLater = new Date(Date.now() + 1000 * 60 * 60)
const verificationToken = await prisma.verificationToken.create({
data: {
@ -28,4 +14,5 @@ export const generateVerificationToken = authenticatedProcedure
})
return { verificationToken: verificationToken.token }
})
}
)

View File

@ -13,20 +13,7 @@ const inputSchema = z.object({
})
export const getPhoneNumber = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/whatsapp/phoneNumber',
protect: true,
},
})
.input(inputSchema)
.output(
z.object({
id: z.string(),
name: z.string(),
})
)
.query(async ({ input, ctx: { user } }) => {
const credentials = await getCredentials(user.id, input)
if (!credentials)

View File

@ -12,22 +12,7 @@ const inputSchema = z.object({
})
export const getSystemTokenInfo = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/whatsapp/systemToken',
protect: true,
},
})
.input(inputSchema)
.output(
z.object({
appId: z.string(),
appName: z.string(),
expiresAt: z.number(),
scopes: z.array(z.string()),
})
)
.query(async ({ input, ctx: { user } }) => {
if (!input.token && !input.credentialsId)
throw new TRPCError({

View File

@ -10,7 +10,7 @@ export const receiveMessagePreview = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/whatsapp/preview/webhook',
path: '/v1/whatsapp/preview/webhook',
summary: 'Message webhook',
tags: ['WhatsApp'],
},

View File

@ -7,11 +7,14 @@ import { startWhatsAppPreview } from './startWhatsAppPreview'
import { subscribePreviewWebhook } from './subscribePreviewWebhook'
import { receiveMessagePreview } from './receiveMessagePreview'
export const whatsAppRouter = router({
export const internalWhatsAppRouter = router({
getPhoneNumber,
getSystemTokenInfo,
verifyIfPhoneNumberAvailable,
generateVerificationToken,
})
export const publicWhatsAppRouter = router({
startWhatsAppPreview,
subscribePreviewWebhook,
receiveMessagePreview,

View File

@ -16,7 +16,7 @@ export const startWhatsAppPreview = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/typebots/{typebotId}/whatsapp/start-preview',
path: '/v1/typebots/{typebotId}/whatsapp/start-preview',
summary: 'Start preview',
tags: ['WhatsApp'],
protect: true,

View File

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

View File

@ -3,19 +3,7 @@ import { z } from 'zod'
import prisma from '@typebot.io/lib/prisma'
export const verifyIfPhoneNumberAvailable = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/whatsapp/phoneNumber/{phoneNumberDisplayName}/available',
protect: true,
},
})
.input(z.object({ phoneNumberDisplayName: z.string() }))
.output(
z.object({
message: z.enum(['available', 'taken']),
})
)
.query(async ({ input: { phoneNumberDisplayName } }) => {
const existingWhatsAppCredentials = await prisma.credentials.findFirst({
where: {

View File

@ -2,20 +2,18 @@ import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionPr
import { router } from '../trpc'
import { generateUploadUrl } from '@/features/upload/api/generateUploadUrl'
import { openAIRouter } from '@/features/blocks/integrations/openai/api/router'
import { whatsAppRouter } from '@/features/whatsapp/router'
import { internalWhatsAppRouter } from '@/features/whatsapp/router'
import { zemanticAiRouter } from '@/features/blocks/integrations/zemanticAi/api/router'
import { forgedCredentialsRouter } from '@/features/forge/api/credentials/router'
import { integrationsRouter } from '@/features/forge/api/router'
import { forgeRouter } from '@/features/forge/api/router'
import { googleSheetsRouter } from '@/features/blocks/integrations/googleSheets/api/router'
export const internalRouter = router({
getAppVersionProcedure,
generateUploadUrl,
whatsApp: whatsAppRouter,
whatsAppInternal: internalWhatsAppRouter,
openAI: openAIRouter,
zemanticAI: zemanticAiRouter,
integrationCredentials: forgedCredentialsRouter,
integrations: integrationsRouter,
forge: forgeRouter,
sheets: googleSheetsRouter,
})

View File

@ -11,6 +11,7 @@ import { analyticsRouter } from '@/features/analytics/api/router'
import { collaboratorsRouter } from '@/features/collaboration/api/router'
import { customDomainsRouter } from '@/features/customDomains/api/router'
import { processTelemetryEvent } from '@/features/telemetry/api/processTelemetryEvent'
import { publicWhatsAppRouter } from '@/features/whatsapp/router'
export const publicRouter = router({
getLinkedTypebots,
@ -25,6 +26,7 @@ export const publicRouter = router({
collaborators: collaboratorsRouter,
customDomains: customDomainsRouter,
processTelemetryEvent,
whatsApp: publicWhatsAppRouter,
})
export type PublicRouter = typeof publicRouter

View File

@ -56965,6 +56965,551 @@
}
}
}
},
"/v1/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
},
"startFrom": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"group"
]
},
"groupId": {
"type": "string"
}
},
"required": [
"type",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"event"
]
},
"eventId": {
"type": "string"
}
},
"required": [
"type",
"eventId"
],
"additionalProperties": false
}
]
}
},
"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"
}
}
}
},
"/v1/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": {
"metadata": {
"type": "object",
"properties": {
"phone_number_id": {
"type": "string"
}
},
"required": [
"phone_number_id"
],
"additionalProperties": false
},
"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
}
]
}
}
},
"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"
}
}
}
}
},
"components": {