⚡ (whatsapp) Improve whatsApp management and media collection
Closes #796
This commit is contained in:
@ -31,14 +31,16 @@ export const createCredentials = authenticatedProcedure
|
|||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
credentials: z.discriminatedUnion('type', [
|
credentials: z
|
||||||
stripeCredentialsSchema.pick(inputShape),
|
.discriminatedUnion('type', [
|
||||||
smtpCredentialsSchema.pick(inputShape),
|
stripeCredentialsSchema.pick(inputShape),
|
||||||
googleSheetsCredentialsSchema.pick(inputShape),
|
smtpCredentialsSchema.pick(inputShape),
|
||||||
openAICredentialsSchema.pick(inputShape),
|
googleSheetsCredentialsSchema.pick(inputShape),
|
||||||
whatsAppCredentialsSchema.pick(inputShape),
|
openAICredentialsSchema.pick(inputShape),
|
||||||
zemanticAiCredentialsSchema.pick(inputShape),
|
whatsAppCredentialsSchema.pick(inputShape),
|
||||||
]),
|
zemanticAiCredentialsSchema.pick(inputShape),
|
||||||
|
])
|
||||||
|
.and(z.object({ id: z.string().cuid2().optional() })),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
|
@ -39,7 +39,7 @@ type UpdateTypebotPayload = Partial<
|
|||||||
| 'customDomain'
|
| 'customDomain'
|
||||||
| 'resultsTablePreferences'
|
| 'resultsTablePreferences'
|
||||||
| 'isClosed'
|
| 'isClosed'
|
||||||
| 'whatsAppPhoneNumberId'
|
| 'whatsAppCredentialsId'
|
||||||
>
|
>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ import { env } from '@typebot.io/env'
|
|||||||
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
|
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
|
||||||
import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
|
import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{ title: 'Requirements' },
|
{ title: 'Requirements' },
|
||||||
@ -57,6 +58,8 @@ type Props = {
|
|||||||
onNewCredentials: (id: string) => void
|
onNewCredentials: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const credentialsId = createId()
|
||||||
|
|
||||||
export const WhatsAppCredentialsModal = ({
|
export const WhatsAppCredentialsModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@ -115,6 +118,7 @@ export const WhatsAppCredentialsModal = ({
|
|||||||
if (!workspace) return
|
if (!workspace) return
|
||||||
mutate({
|
mutate({
|
||||||
credentials: {
|
credentials: {
|
||||||
|
id: credentialsId,
|
||||||
type: 'whatsApp',
|
type: 'whatsApp',
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
name: phoneNumberName,
|
name: phoneNumberName,
|
||||||
@ -269,7 +273,7 @@ export const WhatsAppCredentialsModal = ({
|
|||||||
<Webhook
|
<Webhook
|
||||||
appId={tokenInfoData?.appId}
|
appId={tokenInfoData?.appId}
|
||||||
verificationToken={verificationToken}
|
verificationToken={verificationToken}
|
||||||
phoneNumberId={phoneNumberId}
|
credentialsId={credentialsId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@ -442,18 +446,16 @@ const PhoneNumber = ({
|
|||||||
const Webhook = ({
|
const Webhook = ({
|
||||||
appId,
|
appId,
|
||||||
verificationToken,
|
verificationToken,
|
||||||
phoneNumberId,
|
credentialsId,
|
||||||
}: {
|
}: {
|
||||||
appId?: string
|
appId?: string
|
||||||
verificationToken: string
|
verificationToken: string
|
||||||
phoneNumberId: string
|
credentialsId: string
|
||||||
}) => {
|
}) => {
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
const webhookUrl = `${
|
const webhookUrl = `${
|
||||||
env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? getViewerUrl()
|
env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? getViewerUrl()
|
||||||
}/api/v1/workspaces/${
|
}/api/v1/workspaces/${workspace?.id}/whatsapp/${credentialsId}/webhook`
|
||||||
workspace?.id
|
|
||||||
}/whatsapp/phoneNumbers/${phoneNumberId}/webhook`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={6}>
|
<Stack spacing={6}>
|
||||||
|
@ -30,7 +30,6 @@ import { PublishButton } from '../../../PublishButton'
|
|||||||
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||||
import { isDefined } from '@typebot.io/lib/utils'
|
|
||||||
import { TableList } from '@/components/TableList'
|
import { TableList } from '@/components/TableList'
|
||||||
import { Comparison, LogicalOperator } from '@typebot.io/schemas'
|
import { Comparison, LogicalOperator } from '@typebot.io/schemas'
|
||||||
import { DropdownList } from '@/components/DropdownList'
|
import { DropdownList } from '@/components/DropdownList'
|
||||||
@ -51,18 +50,25 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
|||||||
|
|
||||||
const { data: phoneNumberData } = trpc.whatsApp.getPhoneNumber.useQuery(
|
const { data: phoneNumberData } = trpc.whatsApp.getPhoneNumber.useQuery(
|
||||||
{
|
{
|
||||||
credentialsId: whatsAppSettings?.credentialsId as string,
|
credentialsId: typebot?.whatsAppCredentialsId as string,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!whatsAppSettings?.credentialsId,
|
enabled: !!typebot?.whatsAppCredentialsId,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleEnableWhatsApp = (isChecked: boolean) => {
|
const toggleEnableWhatsApp = (isChecked: boolean) => {
|
||||||
if (!phoneNumberData?.id) return
|
if (!phoneNumberData?.id || !typebot) return
|
||||||
updateTypebot({
|
updateTypebot({
|
||||||
updates: { whatsAppPhoneNumberId: isChecked ? phoneNumberData.id : null },
|
updates: {
|
||||||
save: true,
|
settings: {
|
||||||
|
...typebot.settings,
|
||||||
|
whatsApp: {
|
||||||
|
...typebot.settings.whatsApp,
|
||||||
|
isEnabled: isChecked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,13 +76,7 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
|||||||
if (!typebot) return
|
if (!typebot) return
|
||||||
updateTypebot({
|
updateTypebot({
|
||||||
updates: {
|
updates: {
|
||||||
settings: {
|
whatsAppCredentialsId: credentialsId,
|
||||||
...typebot.settings,
|
|
||||||
whatsApp: {
|
|
||||||
...typebot.settings.whatsApp,
|
|
||||||
credentialsId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -148,7 +148,9 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
|||||||
<CredentialsDropdown
|
<CredentialsDropdown
|
||||||
type="whatsApp"
|
type="whatsApp"
|
||||||
workspaceId={workspace.id}
|
workspaceId={workspace.id}
|
||||||
currentCredentialsId={whatsAppSettings?.credentialsId}
|
currentCredentialsId={
|
||||||
|
typebot?.whatsAppCredentialsId ?? undefined
|
||||||
|
}
|
||||||
onCredentialsSelect={updateCredentialsId}
|
onCredentialsSelect={updateCredentialsId}
|
||||||
onCreateNewClick={onOpen}
|
onCreateNewClick={onOpen}
|
||||||
credentialsName="WA phone number"
|
credentialsName="WA phone number"
|
||||||
@ -158,7 +160,7 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
|||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{typebot?.settings.whatsApp?.credentialsId && (
|
{typebot?.whatsAppCredentialsId && (
|
||||||
<>
|
<>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<Accordion allowToggle>
|
<Accordion allowToggle>
|
||||||
@ -196,22 +198,22 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem>
|
|
||||||
<HStack>
|
|
||||||
<Text>Publish your bot:</Text>
|
|
||||||
<PublishButton size="sm" isMoreMenuDisabled />
|
|
||||||
</HStack>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
label="Enable WhatsApp integration"
|
label="Enable WhatsApp integration"
|
||||||
initialValue={
|
initialValue={
|
||||||
isDefined(typebot?.whatsAppPhoneNumberId) ? true : false
|
typebot?.settings.whatsApp?.isEnabled ?? false
|
||||||
}
|
}
|
||||||
onCheckChange={toggleEnableWhatsApp}
|
onCheckChange={toggleEnableWhatsApp}
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<HStack>
|
||||||
|
<Text>Publish your bot:</Text>
|
||||||
|
<PublishButton size="sm" isMoreMenuDisabled />
|
||||||
|
</HStack>
|
||||||
|
</ListItem>
|
||||||
{phoneNumberData?.id && (
|
{phoneNumberData?.id && (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<TextLink
|
<TextLink
|
||||||
|
@ -24,4 +24,5 @@ export const convertPublicTypebotToTypebot = (
|
|||||||
resultsTablePreferences: existingTypebot.resultsTablePreferences,
|
resultsTablePreferences: existingTypebot.resultsTablePreferences,
|
||||||
selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId,
|
selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId,
|
||||||
whatsAppPhoneNumberId: existingTypebot.whatsAppPhoneNumberId,
|
whatsAppPhoneNumberId: existingTypebot.whatsAppPhoneNumberId,
|
||||||
|
whatsAppCredentialsId: existingTypebot.whatsAppCredentialsId,
|
||||||
})
|
})
|
||||||
|
@ -31,6 +31,7 @@ export const updateTypebot = authenticatedProcedure
|
|||||||
.pick({
|
.pick({
|
||||||
isClosed: true,
|
isClosed: true,
|
||||||
whatsAppPhoneNumberId: true,
|
whatsAppPhoneNumberId: true,
|
||||||
|
whatsAppCredentialsId: true,
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
),
|
),
|
||||||
@ -151,6 +152,7 @@ export const updateTypebot = authenticatedProcedure
|
|||||||
typebot.customDomain === null ? null : typebot.customDomain,
|
typebot.customDomain === null ? null : typebot.customDomain,
|
||||||
isClosed: typebot.isClosed,
|
isClosed: typebot.isClosed,
|
||||||
whatsAppPhoneNumberId: typebot.whatsAppPhoneNumberId ?? undefined,
|
whatsAppPhoneNumberId: typebot.whatsAppPhoneNumberId ?? undefined,
|
||||||
|
whatsAppCredentialsId: typebot.whatsAppCredentialsId ?? undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import prisma from '@typebot.io/lib/prisma'
|
|||||||
import { decrypt } from '@typebot.io/lib/api'
|
import { decrypt } from '@typebot.io/lib/api'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp'
|
import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp'
|
||||||
import { parsePhoneNumber } from 'libphonenumber-js'
|
|
||||||
|
|
||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
credentialsId: z.string().optional(),
|
credentialsId: z.string().optional(),
|
||||||
@ -46,18 +45,13 @@ export const getPhoneNumber = authenticatedProcedure
|
|||||||
display_phone_number: string
|
display_phone_number: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPhoneNumber = parsePhoneNumber(display_phone_number)
|
const formattedPhoneNumber = `${
|
||||||
|
display_phone_number.startsWith('+') ? '' : '+'
|
||||||
if (!parsedPhoneNumber.isValid())
|
}${display_phone_number.replace(/\s-/g, '')}`
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message:
|
|
||||||
"Phone number is not valid. Make sure you don't provide a WhatsApp test number.",
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: credentials.phoneNumberId,
|
id: credentials.phoneNumberId,
|
||||||
name: parsedPhoneNumber.formatInternational().replace(/\s/g, ''),
|
name: formattedPhoneNumber,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@ export const receiveMessagePreview = publicProcedure
|
|||||||
return resumeWhatsAppFlow({
|
return resumeWhatsAppFlow({
|
||||||
receivedMessage,
|
receivedMessage,
|
||||||
sessionId: `wa-${receivedMessage.from}-preview`,
|
sessionId: `wa-${receivedMessage.from}-preview`,
|
||||||
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
|
|
||||||
contact: {
|
contact: {
|
||||||
name: contactName,
|
name: contactName,
|
||||||
phoneNumber: contactPhoneNumber,
|
phoneNumber: contactPhoneNumber,
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||||
|
import {
|
||||||
|
decrypt,
|
||||||
|
methodNotAllowed,
|
||||||
|
notAuthenticated,
|
||||||
|
notFound,
|
||||||
|
} from '@typebot.io/lib/api'
|
||||||
|
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
|
||||||
|
import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp'
|
||||||
|
import got from 'got'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const user = await getAuthenticatedUser(req, res)
|
||||||
|
if (!user) return notAuthenticated(res)
|
||||||
|
|
||||||
|
const typebotId = req.query.typebotId as string
|
||||||
|
|
||||||
|
const typebot = await prisma.typebot.findFirst({
|
||||||
|
where: {
|
||||||
|
id: typebotId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
whatsAppCredentialsId: true,
|
||||||
|
workspace: {
|
||||||
|
select: {
|
||||||
|
credentials: {
|
||||||
|
where: {
|
||||||
|
type: 'whatsApp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!typebot?.workspace || isReadWorkspaceFobidden(typebot.workspace, user))
|
||||||
|
return notFound(res, 'Workspace not found')
|
||||||
|
|
||||||
|
if (!typebot) return notFound(res, 'Typebot not found')
|
||||||
|
|
||||||
|
const mediaId = req.query.mediaId as string
|
||||||
|
const credentialsId = typebot.whatsAppCredentialsId
|
||||||
|
|
||||||
|
const credentials = typebot.workspace.credentials.find(
|
||||||
|
(credential) => credential.id === credentialsId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!credentials) return notFound(res, 'Credentials not found')
|
||||||
|
|
||||||
|
const credentialsData = (await decrypt(
|
||||||
|
credentials.data,
|
||||||
|
credentials.iv
|
||||||
|
)) as WhatsAppCredentials['data']
|
||||||
|
|
||||||
|
const { body } = await got.get({
|
||||||
|
url: `https://graph.facebook.com/v17.0/${mediaId}`,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${credentialsData.systemUserAccessToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsedBody = JSON.parse(body) as { url: string; mime_type: string }
|
||||||
|
|
||||||
|
const buffer = await got(parsedBody.url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${credentialsData.systemUserAccessToken}`,
|
||||||
|
},
|
||||||
|
}).buffer()
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', parsedBody.mime_type)
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400')
|
||||||
|
|
||||||
|
return res.send(buffer)
|
||||||
|
}
|
||||||
|
return methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler
|
@ -8633,8 +8633,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -12745,8 +12745,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -12877,6 +12877,10 @@
|
|||||||
"whatsAppPhoneNumberId": {
|
"whatsAppPhoneNumberId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"whatsAppCredentialsId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -12899,7 +12903,8 @@
|
|||||||
"resultsTablePreferences",
|
"resultsTablePreferences",
|
||||||
"isArchived",
|
"isArchived",
|
||||||
"isClosed",
|
"isClosed",
|
||||||
"whatsAppPhoneNumberId"
|
"whatsAppPhoneNumberId",
|
||||||
|
"whatsAppCredentialsId"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@ -16819,8 +16824,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -17018,6 +17023,10 @@
|
|||||||
"whatsAppPhoneNumberId": {
|
"whatsAppPhoneNumberId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"whatsAppCredentialsId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@ -20951,8 +20960,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -21083,6 +21092,10 @@
|
|||||||
"whatsAppPhoneNumberId": {
|
"whatsAppPhoneNumberId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"whatsAppCredentialsId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -21105,7 +21118,8 @@
|
|||||||
"resultsTablePreferences",
|
"resultsTablePreferences",
|
||||||
"isArchived",
|
"isArchived",
|
||||||
"isClosed",
|
"isClosed",
|
||||||
"whatsAppPhoneNumberId"
|
"whatsAppPhoneNumberId",
|
||||||
|
"whatsAppCredentialsId"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@ -25049,8 +25063,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -25181,6 +25195,10 @@
|
|||||||
"whatsAppPhoneNumberId": {
|
"whatsAppPhoneNumberId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"whatsAppCredentialsId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -25203,7 +25221,8 @@
|
|||||||
"resultsTablePreferences",
|
"resultsTablePreferences",
|
||||||
"isArchived",
|
"isArchived",
|
||||||
"isClosed",
|
"isClosed",
|
||||||
"whatsAppPhoneNumberId"
|
"whatsAppPhoneNumberId",
|
||||||
|
"whatsAppCredentialsId"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
@ -29206,8 +29225,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -30849,294 +30868,307 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"anyOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"type": "object",
|
"anyOf": [
|
||||||
"properties": {
|
{
|
||||||
"data": {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"live": {
|
"data": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"secretKey": {
|
"live": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretKey": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"publicKey": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"secretKey",
|
||||||
|
"publicKey"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretKey": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"publicKey": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"live",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"stripe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data",
|
||||||
|
"type",
|
||||||
|
"workspaceId",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"host": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"publicKey": {
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isTlsEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"from": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"port",
|
||||||
|
"from"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"smtp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data",
|
||||||
|
"type",
|
||||||
|
"workspaceId",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"refresh_token": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"expiry_date": {
|
||||||
|
"type": "number",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"token_type": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"google sheets"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data",
|
||||||
|
"type",
|
||||||
|
"workspaceId",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"apiKey": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"secretKey",
|
"apiKey"
|
||||||
"publicKey"
|
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"test": {
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"openai"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"data",
|
||||||
|
"type",
|
||||||
|
"workspaceId",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"secretKey": {
|
"systemUserAccessToken": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"publicKey": {
|
"phoneNumberId": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"required": [
|
||||||
|
"systemUserAccessToken",
|
||||||
|
"phoneNumberId"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"whatsApp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"live",
|
"data",
|
||||||
"test"
|
"type",
|
||||||
|
"workspaceId",
|
||||||
|
"name"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"type": {
|
{
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"stripe"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"data",
|
|
||||||
"type",
|
|
||||||
"workspaceId",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"host": {
|
"data": {
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"isTlsEnabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"port": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"from": {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"email": {
|
"apiKey": {
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"required": [
|
||||||
|
"apiKey"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
},
|
||||||
},
|
"type": {
|
||||||
"required": [
|
|
||||||
"port",
|
|
||||||
"from"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"smtp"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"data",
|
|
||||||
"type",
|
|
||||||
"workspaceId",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"refresh_token": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"enum": [
|
||||||
|
"zemanticAi"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"expiry_date": {
|
"workspaceId": {
|
||||||
"type": "number",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"access_token": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"token_type": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"id_token": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"scope": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
},
|
||||||
},
|
"name": {
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"google sheets"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"data",
|
|
||||||
"type",
|
|
||||||
"workspaceId",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"apiKey": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"apiKey"
|
"data",
|
||||||
|
"type",
|
||||||
|
"workspaceId",
|
||||||
|
"name"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"openai"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"required": [
|
|
||||||
"data",
|
|
||||||
"type",
|
|
||||||
"workspaceId",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"id": {
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"systemUserAccessToken": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"phoneNumberId": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"systemUserAccessToken",
|
|
||||||
"phoneNumberId"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"pattern": "^[a-z][a-z0-9]*$"
|
||||||
"whatsApp"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"required": [
|
|
||||||
"data",
|
|
||||||
"type",
|
|
||||||
"workspaceId",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"apiKey": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"apiKey"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"zemanticAi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"data",
|
|
||||||
"type",
|
|
||||||
"workspaceId",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3761,8 +3761,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -6172,8 +6172,8 @@
|
|||||||
"whatsApp": {
|
"whatsApp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"credentialsId": {
|
"isEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"startCondition": {
|
"startCondition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -6350,17 +6350,12 @@
|
|||||||
"presignedUrl": {
|
"presignedUrl": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"formData": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {}
|
|
||||||
},
|
|
||||||
"hasReachedStorageLimit": {
|
"hasReachedStorageLimit": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"presignedUrl",
|
"presignedUrl",
|
||||||
"formData",
|
|
||||||
"hasReachedStorageLimit"
|
"hasReachedStorageLimit"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@ -6508,7 +6503,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook": {
|
"/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "whatsAppRouter-subscribeWebhook",
|
"operationId": "whatsAppRouter-subscribeWebhook",
|
||||||
"summary": "Subscribe webhook",
|
"summary": "Subscribe webhook",
|
||||||
@ -6530,7 +6525,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phoneNumberId",
|
"name": "credentialsId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
@ -6932,7 +6927,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phoneNumberId",
|
"name": "credentialsId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
|
@ -8,14 +8,14 @@ export const receiveMessage = publicProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook',
|
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||||
summary: 'Message webhook',
|
summary: 'Message webhook',
|
||||||
tags: ['WhatsApp'],
|
tags: ['WhatsApp'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z
|
z
|
||||||
.object({ workspaceId: z.string(), phoneNumberId: z.string() })
|
.object({ workspaceId: z.string(), credentialsId: z.string() })
|
||||||
.merge(whatsAppWebhookRequestBodySchema)
|
.merge(whatsAppWebhookRequestBodySchema)
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
@ -23,7 +23,7 @@ export const receiveMessage = publicProcedure
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input: { entry, workspaceId, phoneNumberId } }) => {
|
.mutation(async ({ input: { entry, workspaceId, credentialsId } }) => {
|
||||||
const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0)
|
const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0)
|
||||||
if (isNotDefined(receivedMessage)) return { message: 'No message found' }
|
if (isNotDefined(receivedMessage)) return { message: 'No message found' }
|
||||||
const contactName =
|
const contactName =
|
||||||
@ -32,8 +32,8 @@ export const receiveMessage = publicProcedure
|
|||||||
entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ''
|
entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ''
|
||||||
return resumeWhatsAppFlow({
|
return resumeWhatsAppFlow({
|
||||||
receivedMessage,
|
receivedMessage,
|
||||||
sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`,
|
sessionId: `wa-${credentialsId}-${receivedMessage.from}`,
|
||||||
phoneNumberId,
|
credentialsId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
contact: {
|
contact: {
|
||||||
name: contactName,
|
name: contactName,
|
||||||
|
@ -7,7 +7,7 @@ export const subscribeWebhook = publicProcedure
|
|||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/workspaces/{workspaceId}/whatsapp/phoneNumbers/{phoneNumberId}/webhook',
|
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||||
summary: 'Subscribe webhook',
|
summary: 'Subscribe webhook',
|
||||||
tags: ['WhatsApp'],
|
tags: ['WhatsApp'],
|
||||||
protect: true,
|
protect: true,
|
||||||
@ -16,7 +16,7 @@ export const subscribeWebhook = publicProcedure
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
phoneNumberId: z.string(),
|
credentialsId: z.string(),
|
||||||
'hub.challenge': z.string(),
|
'hub.challenge': z.string(),
|
||||||
'hub.verify_token': z.string(),
|
'hub.verify_token': z.string(),
|
||||||
})
|
})
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
import got from 'got'
|
|
||||||
import { TRPCError } from '@trpc/server'
|
|
||||||
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
mediaId: string
|
|
||||||
systemUserToken: string
|
|
||||||
downloadPath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const downloadMedia = async ({
|
|
||||||
mediaId,
|
|
||||||
systemUserToken,
|
|
||||||
downloadPath,
|
|
||||||
}: Props) => {
|
|
||||||
const { body } = await got.get({
|
|
||||||
url: `https://graph.facebook.com/v17.0/${mediaId}`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${systemUserToken}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const parsedBody = JSON.parse(body) as { url: string; mime_type: string }
|
|
||||||
if (!parsedBody.url)
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'INTERNAL_SERVER_ERROR',
|
|
||||||
message: 'Request to Facebook failed. Could not find media url.',
|
|
||||||
cause: body,
|
|
||||||
})
|
|
||||||
const streamBuffer = await got(parsedBody.url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${systemUserToken}`,
|
|
||||||
},
|
|
||||||
}).buffer()
|
|
||||||
const typebotUrl = await uploadFileToBucket({
|
|
||||||
fileName: `public/${downloadPath}/${mediaId}`,
|
|
||||||
file: streamBuffer,
|
|
||||||
mimeType: parsedBody.mime_type,
|
|
||||||
})
|
|
||||||
await got.delete({
|
|
||||||
url: `https://graph.facebook.com/v17.0/${mediaId}`,
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${systemUserToken}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return typebotUrl
|
|
||||||
}
|
|
@ -6,7 +6,6 @@ import {
|
|||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import { sendChatReplyToWhatsApp } from './sendChatReplyToWhatsApp'
|
import { sendChatReplyToWhatsApp } from './sendChatReplyToWhatsApp'
|
||||||
import { startWhatsAppSession } from './startWhatsAppSession'
|
import { startWhatsAppSession } from './startWhatsAppSession'
|
||||||
import { downloadMedia } from './downloadMedia'
|
|
||||||
import { getSession } from '../queries/getSession'
|
import { getSession } from '../queries/getSession'
|
||||||
import { continueBotFlow } from '../continueBotFlow'
|
import { continueBotFlow } from '../continueBotFlow'
|
||||||
import { decrypt } from '@typebot.io/lib/api'
|
import { decrypt } from '@typebot.io/lib/api'
|
||||||
@ -17,12 +16,12 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
receivedMessage,
|
receivedMessage,
|
||||||
sessionId,
|
sessionId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
phoneNumberId,
|
credentialsId,
|
||||||
contact,
|
contact,
|
||||||
}: {
|
}: {
|
||||||
receivedMessage: WhatsAppIncomingMessage
|
receivedMessage: WhatsAppIncomingMessage
|
||||||
sessionId: string
|
sessionId: string
|
||||||
phoneNumberId: string
|
credentialsId?: string
|
||||||
workspaceId?: string
|
workspaceId?: string
|
||||||
contact: NonNullable<SessionState['whatsApp']>['contact']
|
contact: NonNullable<SessionState['whatsApp']>['contact']
|
||||||
}) => {
|
}) => {
|
||||||
@ -38,22 +37,14 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
|
|
||||||
const session = await getSession(sessionId)
|
const session = await getSession(sessionId)
|
||||||
|
|
||||||
const initialCredentials = session
|
const isPreview = workspaceId === undefined || credentialsId === undefined
|
||||||
? await getCredentials(phoneNumberId)(session.state)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const { typebot, resultId } = session?.state.typebotsQueue[0] ?? {}
|
const { typebot } = session?.state.typebotsQueue[0] ?? {}
|
||||||
const messageContent = await getIncomingMessageContent({
|
const messageContent = await getIncomingMessageContent({
|
||||||
message: receivedMessage,
|
message: receivedMessage,
|
||||||
systemUserToken: initialCredentials?.systemUserAccessToken,
|
typebotId: typebot?.id,
|
||||||
downloadPath:
|
|
||||||
typebot && resultId
|
|
||||||
? `typebots/${typebot.id}/results/${resultId}`
|
|
||||||
: undefined,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const isPreview = workspaceId === undefined
|
|
||||||
|
|
||||||
const sessionState =
|
const sessionState =
|
||||||
isPreview && session?.state
|
isPreview && session?.state
|
||||||
? ({
|
? ({
|
||||||
@ -64,6 +55,15 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
} satisfies SessionState)
|
} satisfies SessionState)
|
||||||
: session?.state
|
: session?.state
|
||||||
|
|
||||||
|
const credentials = await getCredentials({ credentialsId, isPreview })
|
||||||
|
|
||||||
|
if (!credentials) {
|
||||||
|
console.error('Could not find credentials')
|
||||||
|
return {
|
||||||
|
message: 'Message received',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resumeResponse = sessionState
|
const resumeResponse = sessionState
|
||||||
? await continueBotFlow(sessionState)(messageContent)
|
? await continueBotFlow(sessionState)(messageContent)
|
||||||
: workspaceId
|
: workspaceId
|
||||||
@ -71,7 +71,7 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
message: receivedMessage,
|
message: receivedMessage,
|
||||||
sessionId,
|
sessionId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
phoneNumberId,
|
credentials: { ...credentials, id: credentialsId as string },
|
||||||
contact,
|
contact,
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
@ -83,17 +83,6 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials =
|
|
||||||
initialCredentials ??
|
|
||||||
(await getCredentials(phoneNumberId)(resumeResponse.newSessionState))
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
console.error('Could not find credentials')
|
|
||||||
return {
|
|
||||||
message: 'Message received',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { input, logs, newSessionState, messages, clientSideActions } =
|
const { input, logs, newSessionState, messages, clientSideActions } =
|
||||||
resumeResponse
|
resumeResponse
|
||||||
|
|
||||||
@ -127,12 +116,10 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
|
|
||||||
const getIncomingMessageContent = async ({
|
const getIncomingMessageContent = async ({
|
||||||
message,
|
message,
|
||||||
systemUserToken,
|
typebotId,
|
||||||
downloadPath,
|
|
||||||
}: {
|
}: {
|
||||||
message: WhatsAppIncomingMessage
|
message: WhatsAppIncomingMessage
|
||||||
systemUserToken: string | undefined
|
typebotId?: string
|
||||||
downloadPath?: string
|
|
||||||
}): Promise<string | undefined> => {
|
}): Promise<string | undefined> => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'text':
|
case 'text':
|
||||||
@ -147,46 +134,52 @@ const getIncomingMessageContent = async ({
|
|||||||
return
|
return
|
||||||
case 'video':
|
case 'video':
|
||||||
case 'image':
|
case 'image':
|
||||||
if (!systemUserToken || !downloadPath) return ''
|
if (!typebotId) return
|
||||||
return downloadMedia({
|
const mediaId = 'video' in message ? message.video.id : message.image.id
|
||||||
mediaId: 'video' in message ? message.video.id : message.image.id,
|
return (
|
||||||
systemUserToken,
|
env.NEXTAUTH_URL +
|
||||||
downloadPath,
|
`/api/typebots/${typebotId}/whatsapp/media/${mediaId}`
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCredentials =
|
const getCredentials = async ({
|
||||||
(phoneNumberId: string) =>
|
credentialsId,
|
||||||
async (
|
isPreview,
|
||||||
state: SessionState
|
}: {
|
||||||
): Promise<WhatsAppCredentials['data'] | undefined> => {
|
credentialsId?: string
|
||||||
const isPreview = !state.typebotsQueue[0].resultId
|
isPreview: boolean
|
||||||
if (isPreview) {
|
}): Promise<WhatsAppCredentials['data'] | undefined> => {
|
||||||
if (!env.META_SYSTEM_USER_TOKEN) return
|
if (isPreview) {
|
||||||
return {
|
if (
|
||||||
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
|
!env.META_SYSTEM_USER_TOKEN ||
|
||||||
phoneNumberId,
|
!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID
|
||||||
}
|
)
|
||||||
}
|
return
|
||||||
if (!state.whatsApp) return
|
|
||||||
|
|
||||||
const credentials = await prisma.credentials.findUnique({
|
|
||||||
where: {
|
|
||||||
id: state.whatsApp.credentialsId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
data: true,
|
|
||||||
iv: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!credentials) return
|
|
||||||
const data = (await decrypt(
|
|
||||||
credentials.data,
|
|
||||||
credentials.iv
|
|
||||||
)) as WhatsAppCredentials['data']
|
|
||||||
return {
|
return {
|
||||||
systemUserAccessToken: data.systemUserAccessToken,
|
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
|
||||||
phoneNumberId,
|
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!credentialsId) return
|
||||||
|
|
||||||
|
const credentials = await prisma.credentials.findUnique({
|
||||||
|
where: {
|
||||||
|
id: credentialsId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
data: true,
|
||||||
|
iv: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!credentials) return
|
||||||
|
const data = (await decrypt(
|
||||||
|
credentials.data,
|
||||||
|
credentials.iv
|
||||||
|
)) as WhatsAppCredentials['data']
|
||||||
|
return {
|
||||||
|
systemUserAccessToken: data.systemUserAccessToken,
|
||||||
|
phoneNumberId: data.phoneNumberId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,21 +13,20 @@ import {
|
|||||||
WhatsAppIncomingMessage,
|
WhatsAppIncomingMessage,
|
||||||
} from '@typebot.io/schemas/features/whatsapp'
|
} from '@typebot.io/schemas/features/whatsapp'
|
||||||
import { isNotDefined } from '@typebot.io/lib/utils'
|
import { isNotDefined } from '@typebot.io/lib/utils'
|
||||||
import { decrypt } from '@typebot.io/lib/api/encryption'
|
|
||||||
import { startSession } from '../startSession'
|
import { startSession } from '../startSession'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
message: WhatsAppIncomingMessage
|
message: WhatsAppIncomingMessage
|
||||||
sessionId: string
|
sessionId: string
|
||||||
workspaceId?: string
|
workspaceId?: string
|
||||||
phoneNumberId: string
|
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
|
||||||
contact: NonNullable<SessionState['whatsApp']>['contact']
|
contact: NonNullable<SessionState['whatsApp']>['contact']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startWhatsAppSession = async ({
|
export const startWhatsAppSession = async ({
|
||||||
message,
|
message,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
phoneNumberId,
|
credentials,
|
||||||
contact,
|
contact,
|
||||||
}: Props): Promise<
|
}: Props): Promise<
|
||||||
| (ChatReply & {
|
| (ChatReply & {
|
||||||
@ -38,7 +37,7 @@ export const startWhatsAppSession = async ({
|
|||||||
const publicTypebotsWithWhatsAppEnabled =
|
const publicTypebotsWithWhatsAppEnabled =
|
||||||
(await prisma.publicTypebot.findMany({
|
(await prisma.publicTypebot.findMany({
|
||||||
where: {
|
where: {
|
||||||
typebot: { workspaceId, whatsAppPhoneNumberId: phoneNumberId },
|
typebot: { workspaceId, whatsAppCredentialsId: credentials.id },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
settings: true,
|
settings: true,
|
||||||
@ -55,7 +54,7 @@ export const startWhatsAppSession = async ({
|
|||||||
const botsWithWhatsAppEnabled = publicTypebotsWithWhatsAppEnabled.filter(
|
const botsWithWhatsAppEnabled = publicTypebotsWithWhatsAppEnabled.filter(
|
||||||
(publicTypebot) =>
|
(publicTypebot) =>
|
||||||
publicTypebot.typebot.publicId &&
|
publicTypebot.typebot.publicId &&
|
||||||
publicTypebot.settings.whatsApp?.credentialsId
|
publicTypebot.settings.whatsApp?.isEnabled
|
||||||
)
|
)
|
||||||
|
|
||||||
const publicTypebot =
|
const publicTypebot =
|
||||||
@ -70,19 +69,6 @@ export const startWhatsAppSession = async ({
|
|||||||
|
|
||||||
if (isNotDefined(publicTypebot)) return
|
if (isNotDefined(publicTypebot)) return
|
||||||
|
|
||||||
const encryptedCredentials = await prisma.credentials.findUnique({
|
|
||||||
where: {
|
|
||||||
id: publicTypebot.settings.whatsApp?.credentialsId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!encryptedCredentials) return
|
|
||||||
const credentials = (await decrypt(
|
|
||||||
encryptedCredentials?.data,
|
|
||||||
encryptedCredentials?.iv
|
|
||||||
)) as WhatsAppCredentials['data']
|
|
||||||
|
|
||||||
if (credentials.phoneNumberId !== phoneNumberId) return
|
|
||||||
|
|
||||||
const session = await startSession({
|
const session = await startSession({
|
||||||
startParams: {
|
startParams: {
|
||||||
typebot: publicTypebot.typebot.publicId as string,
|
typebot: publicTypebot.typebot.publicId as string,
|
||||||
@ -96,8 +82,7 @@ export const startWhatsAppSession = async ({
|
|||||||
...session.newSessionState,
|
...session.newSessionState,
|
||||||
whatsApp: {
|
whatsApp: {
|
||||||
contact,
|
contact,
|
||||||
credentialsId: publicTypebot?.settings.whatsApp
|
credentialsId: credentials.id,
|
||||||
?.credentialsId as string,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ export const parseTestTypebot = (
|
|||||||
isClosed: false,
|
isClosed: false,
|
||||||
resultsTablePreferences: null,
|
resultsTablePreferences: null,
|
||||||
whatsAppPhoneNumberId: null,
|
whatsAppPhoneNumberId: null,
|
||||||
|
whatsAppCredentialsId: null,
|
||||||
variables: [{ id: 'var1', name: 'var1' }],
|
variables: [{ id: 'var1', name: 'var1' }],
|
||||||
...partialTypebot,
|
...partialTypebot,
|
||||||
edges: [
|
edges: [
|
||||||
|
@ -199,6 +199,7 @@ model Typebot {
|
|||||||
isArchived Boolean @default(false)
|
isArchived Boolean @default(false)
|
||||||
isClosed Boolean @default(false)
|
isClosed Boolean @default(false)
|
||||||
whatsAppPhoneNumberId String?
|
whatsAppPhoneNumberId String?
|
||||||
|
whatsAppCredentialsId String?
|
||||||
|
|
||||||
@@index([workspaceId])
|
@@index([workspaceId])
|
||||||
@@index([folderId])
|
@@index([folderId])
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Typebot" ADD COLUMN "whatsAppCredentialsId" TEXT;
|
@ -183,6 +183,7 @@ model Typebot {
|
|||||||
isArchived Boolean @default(false)
|
isArchived Boolean @default(false)
|
||||||
isClosed Boolean @default(false)
|
isClosed Boolean @default(false)
|
||||||
whatsAppPhoneNumberId String?
|
whatsAppPhoneNumberId String?
|
||||||
|
whatsAppCredentialsId String?
|
||||||
|
|
||||||
@@index([workspaceId])
|
@@index([workspaceId])
|
||||||
@@index([isArchived, createdAt(sort: Desc)])
|
@@index([isArchived, createdAt(sort: Desc)])
|
||||||
|
@ -57,6 +57,7 @@ export const typebotSchema = z.preprocess(
|
|||||||
isArchived: z.boolean(),
|
isArchived: z.boolean(),
|
||||||
isClosed: z.boolean(),
|
isClosed: z.boolean(),
|
||||||
whatsAppPhoneNumberId: z.string().nullable(),
|
whatsAppPhoneNumberId: z.string().nullable(),
|
||||||
|
whatsAppCredentialsId: z.string().nullable(),
|
||||||
}) satisfies z.ZodType<TypebotPrisma, z.ZodTypeDef, unknown>
|
}) satisfies z.ZodType<TypebotPrisma, z.ZodTypeDef, unknown>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ const startConditionSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const whatsAppSettingsSchema = z.object({
|
export const whatsAppSettingsSchema = z.object({
|
||||||
credentialsId: z.string().optional(),
|
isEnabled: z.boolean().optional(),
|
||||||
startCondition: startConditionSchema.optional(),
|
startCondition: startConditionSchema.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user