Skip to content

Commit

Permalink
⚡ (whatsapp) Improve whatsApp management and media collection
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno authored and jmgoncalves97 committed Jan 17, 2025
1 parent 6650d8b commit 45d92d1
Show file tree
Hide file tree
Showing 22 changed files with 479 additions and 426 deletions.
18 changes: 10 additions & 8 deletions apps/builder/src/features/credentials/api/createCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,16 @@ export const createCredentials = authenticatedProcedure
})
.input(
z.object({
credentials: z.discriminatedUnion('type', [
stripeCredentialsSchema.pick(inputShape),
smtpCredentialsSchema.pick(inputShape),
googleSheetsCredentialsSchema.pick(inputShape),
openAICredentialsSchema.pick(inputShape),
whatsAppCredentialsSchema.pick(inputShape),
zemanticAiCredentialsSchema.pick(inputShape),
]),
credentials: z
.discriminatedUnion('type', [
stripeCredentialsSchema.pick(inputShape),
smtpCredentialsSchema.pick(inputShape),
googleSheetsCredentialsSchema.pick(inputShape),
openAICredentialsSchema.pick(inputShape),
whatsAppCredentialsSchema.pick(inputShape),
zemanticAiCredentialsSchema.pick(inputShape),
])
.and(z.object({ id: z.string().cuid2().optional() })),
})
)
.output(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type UpdateTypebotPayload = Partial<
| 'customDomain'
| 'resultsTablePreferences'
| 'isClosed'
| 'whatsAppPhoneNumberId'
| 'whatsAppCredentialsId'
>
>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { env } from '@typebot.io/env'
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
import React, { useState } from 'react'
import { createId } from '@paralleldrive/cuid2'

const steps = [
{ title: 'Requirements' },
Expand All @@ -57,6 +58,8 @@ type Props = {
onNewCredentials: (id: string) => void
}

const credentialsId = createId()

export const WhatsAppCredentialsModal = ({
isOpen,
onClose,
Expand Down Expand Up @@ -115,6 +118,7 @@ export const WhatsAppCredentialsModal = ({
if (!workspace) return
mutate({
credentials: {
id: credentialsId,
type: 'whatsApp',
workspaceId: workspace.id,
name: phoneNumberName,
Expand Down Expand Up @@ -269,7 +273,7 @@ export const WhatsAppCredentialsModal = ({
<Webhook
appId={tokenInfoData?.appId}
verificationToken={verificationToken}
phoneNumberId={phoneNumberId}
credentialsId={credentialsId}
/>
)}
</ModalBody>
Expand Down Expand Up @@ -442,18 +446,16 @@ const PhoneNumber = ({
const Webhook = ({
appId,
verificationToken,
phoneNumberId,
credentialsId,
}: {
appId?: string
verificationToken: string
phoneNumberId: string
credentialsId: string
}) => {
const { workspace } = useWorkspace()
const webhookUrl = `${
env.NEXT_PUBLIC_VIEWER_INTERNAL_URL ?? getViewerUrl()
}/api/v1/workspaces/${
workspace?.id
}/whatsapp/phoneNumbers/${phoneNumberId}/webhook`
}/api/v1/workspaces/${workspace?.id}/whatsapp/${credentialsId}/webhook`

return (
<Stack spacing={6}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { PublishButton } from '../../../PublishButton'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
import { trpc } from '@/lib/trpc'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { isDefined } from '@typebot.io/lib/utils'
import { TableList } from '@/components/TableList'
import { Comparison, LogicalOperator } from '@typebot.io/schemas'
import { DropdownList } from '@/components/DropdownList'
Expand All @@ -51,36 +50,37 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {

const { data: phoneNumberData } = trpc.whatsApp.getPhoneNumber.useQuery(
{
credentialsId: whatsAppSettings?.credentialsId as string,
credentialsId: typebot?.whatsAppCredentialsId as string,
},
{
enabled: !!whatsAppSettings?.credentialsId,
enabled: !!typebot?.whatsAppCredentialsId,
}
)

const toggleEnableWhatsApp = (isChecked: boolean) => {
if (!phoneNumberData?.id) return
updateTypebot({
updates: { whatsAppPhoneNumberId: isChecked ? phoneNumberData.id : null },
save: true,
})
}

const updateCredentialsId = (credentialsId: string | undefined) => {
if (!typebot) return
if (!phoneNumberData?.id || !typebot) return
updateTypebot({
updates: {
settings: {
...typebot.settings,
whatsApp: {
...typebot.settings.whatsApp,
credentialsId,
isEnabled: isChecked,
},
},
},
})
}

const updateCredentialsId = (credentialsId: string | undefined) => {
if (!typebot) return
updateTypebot({
updates: {
whatsAppCredentialsId: credentialsId,
},
})
}

const updateStartConditionComparisons = (comparisons: Comparison[]) => {
if (!typebot) return
updateTypebot({
Expand Down Expand Up @@ -148,7 +148,9 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
<CredentialsDropdown
type="whatsApp"
workspaceId={workspace.id}
currentCredentialsId={whatsAppSettings?.credentialsId}
currentCredentialsId={
typebot?.whatsAppCredentialsId ?? undefined
}
onCredentialsSelect={updateCredentialsId}
onCreateNewClick={onOpen}
credentialsName="WA phone number"
Expand All @@ -158,7 +160,7 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
)}
</HStack>
</ListItem>
{typebot?.settings.whatsApp?.credentialsId && (
{typebot?.whatsAppCredentialsId && (
<>
<ListItem>
<Accordion allowToggle>
Expand Down Expand Up @@ -196,22 +198,22 @@ export const WhatsAppModal = ({ isOpen, onClose }: ModalProps): JSX.Element => {
</Accordion>
</ListItem>

<ListItem>
<HStack>
<Text>Publish your bot:</Text>
<PublishButton size="sm" isMoreMenuDisabled />
</HStack>
</ListItem>
<ListItem>
<SwitchWithLabel
label="Enable WhatsApp integration"
initialValue={
isDefined(typebot?.whatsAppPhoneNumberId) ? true : false
typebot?.settings.whatsApp?.isEnabled ?? false
}
onCheckChange={toggleEnableWhatsApp}
justifyContent="flex-start"
/>
</ListItem>
<ListItem>
<HStack>
<Text>Publish your bot:</Text>
<PublishButton size="sm" isMoreMenuDisabled />
</HStack>
</ListItem>
{phoneNumberData?.id && (
<ListItem>
<TextLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export const convertPublicTypebotToTypebot = (
resultsTablePreferences: existingTypebot.resultsTablePreferences,
selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId,
whatsAppPhoneNumberId: existingTypebot.whatsAppPhoneNumberId,
whatsAppCredentialsId: existingTypebot.whatsAppCredentialsId,
})
2 changes: 2 additions & 0 deletions apps/builder/src/features/typebot/api/updateTypebot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const updateTypebot = authenticatedProcedure
.pick({
isClosed: true,
whatsAppPhoneNumberId: true,
whatsAppCredentialsId: true,
})
.partial()
),
Expand Down Expand Up @@ -151,6 +152,7 @@ export const updateTypebot = authenticatedProcedure
typebot.customDomain === null ? null : typebot.customDomain,
isClosed: typebot.isClosed,
whatsAppPhoneNumberId: typebot.whatsAppPhoneNumberId ?? undefined,
whatsAppCredentialsId: typebot.whatsAppCredentialsId ?? undefined,
},
})

Expand Down
14 changes: 4 additions & 10 deletions apps/builder/src/features/whatsapp/getPhoneNumber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import prisma from '@typebot.io/lib/prisma'
import { decrypt } from '@typebot.io/lib/api'
import { TRPCError } from '@trpc/server'
import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp'
import { parsePhoneNumber } from 'libphonenumber-js'

const inputSchema = z.object({
credentialsId: z.string().optional(),
Expand Down Expand Up @@ -46,18 +45,13 @@ export const getPhoneNumber = authenticatedProcedure
display_phone_number: string
}

const parsedPhoneNumber = parsePhoneNumber(display_phone_number)

if (!parsedPhoneNumber.isValid())
throw new TRPCError({
code: 'BAD_REQUEST',
message:
"Phone number is not valid. Make sure you don't provide a WhatsApp test number.",
})
const formattedPhoneNumber = `${
display_phone_number.startsWith('+') ? '' : '+'
}${display_phone_number.replace(/\s-/g, '')}`

return {
id: credentials.phoneNumberId,
name: parsedPhoneNumber.formatInternational().replace(/\s/g, ''),
name: formattedPhoneNumber,
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const receiveMessagePreview = publicProcedure
return resumeWhatsAppFlow({
receivedMessage,
sessionId: `wa-${receivedMessage.from}-preview`,
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
contact: {
name: contactName,
phoneNumber: contactPhoneNumber,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 45d92d1

Please sign in to comment.