@@ -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 }
|
||||
})
|
||||
78
apps/builder/src/features/whatsapp/getPhoneNumber.ts
Normal file
78
apps/builder/src/features/whatsapp/getPhoneNumber.ts
Normal 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']
|
||||
}
|
||||
88
apps/builder/src/features/whatsapp/getSystemTokenInfo.ts
Normal file
88
apps/builder/src/features/whatsapp/getSystemTokenInfo.ts
Normal 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']
|
||||
}
|
||||
12
apps/builder/src/features/whatsapp/router.ts
Normal file
12
apps/builder/src/features/whatsapp/router.ts
Normal 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,
|
||||
})
|
||||
@@ -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' }
|
||||
})
|
||||
Reference in New Issue
Block a user