Add WhatsApp integration beta test (#722)

Related to #401
This commit is contained in:
Baptiste Arnaud
2023-08-29 10:01:28 +02:00
parent 036b407a11
commit b852b4af0b
136 changed files with 6694 additions and 5383 deletions

View File

@@ -0,0 +1,31 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import prisma from '@/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 () => {
const oneHourLater = new Date(Date.now() + 1000 * 60 * 60)
const verificationToken = await prisma.verificationToken.create({
data: {
token: createId(),
expires: oneHourLater,
identifier: 'whatsapp webhook',
},
})
return { verificationToken: verificationToken.token }
})

View File

@@ -0,0 +1,78 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import got from 'got'
import prisma from '@/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(),
systemToken: z.string().optional(),
phoneNumberId: z.string().optional(),
})
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)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Credentials not found',
})
const { display_phone_number } = (await got(
`https://graph.facebook.com/v17.0/${credentials.phoneNumberId}`,
{
headers: {
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
},
}
).json()) as {
display_phone_number: string
}
return {
id: credentials.phoneNumberId,
name: parsePhoneNumber(display_phone_number)
.formatInternational()
.replace(/\s/g, ''),
}
})
const getCredentials = async (
userId: string,
input: z.infer<typeof inputSchema>
): Promise<WhatsAppCredentials['data'] | undefined> => {
if (input.systemToken && input.phoneNumberId)
return {
systemUserAccessToken: input.systemToken,
phoneNumberId: input.phoneNumberId,
}
if (!input.credentialsId) return
const credentials = await prisma.credentials.findUnique({
where: {
id: input.credentialsId,
workspace: { members: { some: { userId } } },
},
})
if (!credentials) return
return (await decrypt(
credentials.data,
credentials.iv
)) as WhatsAppCredentials['data']
}

View File

@@ -0,0 +1,88 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import got from 'got'
import { TRPCError } from '@trpc/server'
import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp'
import prisma from '@/lib/prisma'
import { decrypt } from '@typebot.io/lib/api/encryption'
const inputSchema = z.object({
token: z.string().optional(),
credentialsId: z.string().optional(),
})
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({
code: 'BAD_REQUEST',
message: 'Either token or credentialsId must be provided',
})
const credentials = await getCredentials(user.id, input)
if (!credentials)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Credentials not found',
})
const {
data: { expires_at, scopes, app_id, application },
} = (await got(
`https://graph.facebook.com/v17.0/debug_token?input_token=${credentials.systemUserAccessToken}`,
{
headers: {
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
},
}
).json()) as {
data: {
app_id: string
application: string
expires_at: number
scopes: string[]
}
}
return {
appId: app_id,
appName: application,
expiresAt: expires_at,
scopes,
}
})
const getCredentials = async (
userId: string,
input: z.infer<typeof inputSchema>
): Promise<Omit<WhatsAppCredentials['data'], 'phoneNumberId'> | undefined> => {
if (input.token)
return {
systemUserAccessToken: input.token,
}
const credentials = await prisma.credentials.findUnique({
where: {
id: input.credentialsId,
workspace: { members: { some: { userId } } },
},
})
if (!credentials) return
return (await decrypt(
credentials.data,
credentials.iv
)) as WhatsAppCredentials['data']
}

View File

@@ -0,0 +1,12 @@
import { router } from '@/helpers/server/trpc'
import { getPhoneNumber } from './getPhoneNumber'
import { getSystemTokenInfo } from './getSystemTokenInfo'
import { verifyIfPhoneNumberAvailable } from './verifyIfPhoneNumberAvailable'
import { generateVerificationToken } from './generateVerificationToken'
export const whatsAppRouter = router({
getPhoneNumber,
getSystemTokenInfo,
verifyIfPhoneNumberAvailable,
generateVerificationToken,
})

View File

@@ -0,0 +1,29 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import prisma from '@/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: {
type: 'whatsApp',
name: phoneNumberDisplayName,
},
})
if (existingWhatsAppCredentials) return { message: 'taken' }
return { message: 'available' }
})