Skip to content

Commit

Permalink
🧑‍💻 (chat) Introduce startChat and continueChat endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno authored and jmgoncalves97 committed Jan 17, 2025
1 parent a15007d commit 8edb909
Show file tree
Hide file tree
Showing 74 changed files with 28,647 additions and 866 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import * as Sentry from '@sentry/nextjs'
import { User } from '@typebot.io/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getServerSession } from 'next-auth'
import { mockedUser } from '../mockedUser'
import { env } from '@typebot.io/env'
import { mockedUser } from '@typebot.io/lib/mockedUser'

export const getAuthenticatedUser = async (
req: NextApiRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export const AudioBubbleNode = ({ url }: Props) => {
return isDefined(url) ? (
<audio src={url} controls />
) : (
<Text color={'gray.500'}>
{t('editor.blocks.bubbles.audio.node.clickToEdit.text')}
</Text>
<Text color={'gray.500'}>{t('clickToEdit')}</Text>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ type Props = {
export const EmbedBubbleContent = ({ block }: Props) => {
const { t } = useTranslate()
if (!block.content?.url)
return (
<Text color="gray.500">
{t('editor.blocks.bubbles.embed.node.clickToEdit.text')}
</Text>
)
return <Text color="gray.500">{t('clickToEdit')}</Text>
return <Text>{t('editor.blocks.bubbles.embed.node.show.text')}</Text>
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ export const ImageBubbleContent = ({ block }: Props) => {
const containsVariables =
block.content?.url?.includes('{{') && block.content.url.includes('}}')
return !block.content?.url ? (
<Text color={'gray.500'}>
{t('editor.blocks.bubbles.image.node.clickToEdit.text')}
</Text>
<Text color={'gray.500'}>{t('clickToEdit')}</Text>
) : (
<Box w="full">
<Image
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ type Props = {
export const VideoBubbleContent = ({ block }: Props) => {
const { t } = useTranslate()
if (!block.content?.url || !block.content.type)
return (
<Text color="gray.500">
{t('editor.blocks.bubbles.video.node.clickToEdit.text')}
</Text>
)
return <Text color="gray.500">{t('clickToEdit')}</Text>
const containsVariables =
block.content?.url?.includes('{{') && block.content.url.includes('}}')
switch (block.content.type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test('should be configurable', async ({ page }) => {
await expect(page.getByTestId('selected-item-label').first()).toHaveText(
'My link typebot 2'
)
await page.click('input[placeholder="Select a block"]')
await page.click('input[placeholder="Select a group"]')
await page.click('text=Group #2')

await page.click('text=Preview')
Expand Down
13 changes: 9 additions & 4 deletions apps/builder/src/features/preview/components/WebPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { useGraph } from '@/features/graph/providers/GraphProvider'
import { useToast } from '@/hooks/useToast'
import { Standard } from '@typebot.io/nextjs'
import { ChatReply } from '@typebot.io/schemas'
import { ContinueChatResponse } from '@typebot.io/schemas'

export const WebPreview = () => {
const { typebot } = useTypebot()
Expand All @@ -13,7 +13,7 @@ export const WebPreview = () => {

const { showToast } = useToast()

const handleNewLogs = (logs: ChatReply['logs']) => {
const handleNewLogs = (logs: ContinueChatResponse['logs']) => {
logs?.forEach((log) => {
showToast({
icon: <WebhookIcon />,
Expand All @@ -40,8 +40,13 @@ export const WebPreview = () => {
<Standard
key={`web-preview${startPreviewAtGroup ?? ''}`}
typebot={typebot}
startGroupId={startPreviewAtGroup}
startEventId={startPreviewAtEvent}
startFrom={
startPreviewAtGroup
? { type: 'group', groupId: startPreviewAtGroup }
: startPreviewAtEvent
? { type: 'event', eventId: startPreviewAtEvent }
: undefined
}
onNewInputBlock={(block) =>
setPreviewingBlock({
id: block.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { BuoyIcon, ExternalLinkIcon } from '@/components/icons'

export const WhatsAppPreviewInstructions = (props: StackProps) => {
const { typebot, save } = useTypebot()
const { startPreviewAtGroup } = useEditor()
const { startPreviewAtGroup, startPreviewAtEvent } = useEditor()
const [phoneNumber, setPhoneNumber] = useState(
getPhoneNumberFromLocalStorage() ?? ''
)
Expand Down Expand Up @@ -56,7 +56,11 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => {
mutate({
to: phoneNumber,
typebotId: typebot.id,
startGroupId: startPreviewAtGroup,
startFrom: startPreviewAtGroup
? { type: 'group', groupId: startPreviewAtGroup }
: startPreviewAtEvent
? { type: 'event', eventId: startPreviewAtEvent }
: undefined,
})
}

Expand Down
222 changes: 110 additions & 112 deletions apps/builder/src/features/whatsapp/startWhatsAppPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { sendChatReplyToWhatsApp } from '@typebot.io/bot-engine/whatsapp/sendChatReplyToWhatsApp'
import { sendWhatsAppMessage } from '@typebot.io/bot-engine/whatsapp/sendWhatsAppMessage'
import { isReadTypebotForbidden } from '../typebot/helpers/isReadTypebotForbidden'
import { SessionState } from '@typebot.io/schemas'
import { SessionState, startFromSchema } from '@typebot.io/schemas'

export const startWhatsAppPreview = authenticatedProcedure
.meta({
Expand All @@ -31,143 +31,141 @@ export const startWhatsAppPreview = authenticatedProcedure
value.replace(/\s/g, '').replace(/\+/g, '').replace(/-/g, '')
),
typebotId: z.string(),
startGroupId: z.string().optional(),
startFrom: startFromSchema.optional(),
})
)
.output(
z.object({
message: z.string(),
})
)
.mutation(
async ({ input: { to, typebotId, startGroupId }, ctx: { user } }) => {
if (
!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID ||
!env.META_SYSTEM_USER_TOKEN ||
!env.WHATSAPP_PREVIEW_TEMPLATE_NAME
)
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID or META_SYSTEM_USER_TOKEN or WHATSAPP_PREVIEW_TEMPLATE_NAME env variables',
})
.mutation(async ({ input: { to, typebotId, startFrom }, ctx: { user } }) => {
if (
!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID ||
!env.META_SYSTEM_USER_TOKEN ||
!env.WHATSAPP_PREVIEW_TEMPLATE_NAME
)
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID or META_SYSTEM_USER_TOKEN or WHATSAPP_PREVIEW_TEMPLATE_NAME env variables',
})

const existingTypebot = await prisma.typebot.findFirst({
where: {
id: typebotId,
},
select: {
id: true,
workspaceId: true,
collaborators: {
select: {
userId: true,
},
const existingTypebot = await prisma.typebot.findFirst({
where: {
id: typebotId,
},
select: {
id: true,
workspaceId: true,
collaborators: {
select: {
userId: true,
},
},
})
if (
!existingTypebot?.id ||
(await isReadTypebotForbidden(existingTypebot, user))
)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
},
})
if (
!existingTypebot?.id ||
(await isReadTypebotForbidden(existingTypebot, user))
)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })

const sessionId = `wa-preview-${to}`
const sessionId = `wa-preview-${to}`

const existingSession = await prisma.chatSession.findFirst({
where: {
id: sessionId,
},
select: {
updatedAt: true,
state: true,
},
})
const existingSession = await prisma.chatSession.findFirst({
where: {
id: sessionId,
},
select: {
updatedAt: true,
state: true,
},
})

// For users that did not interact with the bot in the last 24 hours, we need to send a template message.
const canSendDirectMessagesToUser =
(existingSession?.updatedAt.getTime() ?? 0) >
Date.now() - 24 * 60 * 60 * 1000
// For users that did not interact with the bot in the last 24 hours, we need to send a template message.
const canSendDirectMessagesToUser =
(existingSession?.updatedAt.getTime() ?? 0) >
Date.now() - 24 * 60 * 60 * 1000

const {
newSessionState,
const {
newSessionState,
messages,
input,
clientSideActions,
logs,
visitedEdges,
} = await startSession({
version: 2,
message: undefined,
startParams: {
isOnlyRegistering: !canSendDirectMessagesToUser,
type: 'preview',
typebotId,
startFrom,
userId: user.id,
},
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)
?.whatsApp,
},
})

if (canSendDirectMessagesToUser) {
await sendChatReplyToWhatsApp({
to,
typingEmulation: newSessionState.typingEmulation,
messages,
input,
clientSideActions,
logs,
visitedEdges,
} = await startSession({
version: 2,
message: undefined,
startParams: {
isOnlyRegistering: !canSendDirectMessagesToUser,
typebot: typebotId,
isPreview: true,
startGroupId,
credentials: {
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
},
userId: user.id,
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)
?.whatsApp,
state: newSessionState,
})
await saveStateToDatabase({
clientSideActions: [],
input,
logs,
session: {
id: sessionId,
state: newSessionState,
},
visitedEdges,
})

if (canSendDirectMessagesToUser) {
await sendChatReplyToWhatsApp({
} else {
await restartSession({
state: newSessionState,
id: sessionId,
})
try {
await sendWhatsAppMessage({
to,
typingEmulation: newSessionState.typingEmulation,
messages,
input,
clientSideActions,
message: {
type: 'template',
template: {
language: {
code: env.WHATSAPP_PREVIEW_TEMPLATE_LANG,
},
name: env.WHATSAPP_PREVIEW_TEMPLATE_NAME,
},
},
credentials: {
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
},
state: newSessionState,
})
await saveStateToDatabase({
clientSideActions: [],
input,
logs,
session: {
id: sessionId,
state: newSessionState,
},
visitedEdges,
})
} else {
await restartSession({
state: newSessionState,
id: sessionId,
} catch (err) {
if (err instanceof HTTPError) console.log(err.response.body)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Request to Meta to send preview message failed',
cause: err,
})
try {
await sendWhatsAppMessage({
to,
message: {
type: 'template',
template: {
language: {
code: env.WHATSAPP_PREVIEW_TEMPLATE_LANG,
},
name: env.WHATSAPP_PREVIEW_TEMPLATE_NAME,
},
},
credentials: {
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
},
})
} catch (err) {
if (err instanceof HTTPError) console.log(err.response.body)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Request to Meta to send preview message failed',
cause: err,
})
}
}
return {
message: 'success',
}
}
)
return {
message: 'success',
}
})
2 changes: 1 addition & 1 deletion apps/builder/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { customAdapter } from '../../../features/auth/api/customAdapter'
import { User } from '@typebot.io/prisma'
import { getAtPath, isDefined } from '@typebot.io/lib'
import { mockedUser } from '@/features/auth/mockedUser'
import { mockedUser } from '@typebot.io/lib/mockedUser'
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
import { Ratelimit } from '@upstash/ratelimit'
Expand Down
Loading

0 comments on commit 8edb909

Please sign in to comment.