@@ -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,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { User } from '@typebot.io/prisma'
|
||||
|
||||
export const mockedUser: User = {
|
||||
id: 'userId',
|
||||
name: 'John Doe',
|
||||
email: 'user@email.com',
|
||||
company: null,
|
||||
createdAt: new Date('2022-01-01'),
|
||||
emailVerified: null,
|
||||
graphNavigation: 'TRACKPAD',
|
||||
preferredAppAppearance: null,
|
||||
image: 'https://avatars.githubusercontent.com/u/16015833?v=4',
|
||||
lastActivityAt: new Date('2022-01-01'),
|
||||
onboardingCategories: [],
|
||||
updatedAt: new Date('2022-01-01'),
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
@@ -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 />,
|
||||
@@ -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,
|
||||
|
||||
@@ -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() ?? ''
|
||||
)
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
@@ -31,7 +31,7 @@ export const startWhatsAppPreview = authenticatedProcedure
|
||||
value.replace(/\s/g, '').replace(/\+/g, '').replace(/-/g, '')
|
||||
),
|
||||
typebotId: z.string(),
|
||||
startGroupId: z.string().optional(),
|
||||
startFrom: startFromSchema.optional(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
@@ -39,135 +39,133 @@ export const startWhatsAppPreview = authenticatedProcedure
|
||||
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,
|
||||
},
|
||||
userId: user.id,
|
||||
initialSessionState: {
|
||||
whatsApp: (existingSession?.state as SessionState | undefined)
|
||||
?.whatsApp,
|
||||
credentials: {
|
||||
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
|
||||
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
|
||||
},
|
||||
state: newSessionState,
|
||||
})
|
||||
|
||||
if (canSendDirectMessagesToUser) {
|
||||
await sendChatReplyToWhatsApp({
|
||||
await saveStateToDatabase({
|
||||
clientSideActions: [],
|
||||
input,
|
||||
logs,
|
||||
session: {
|
||||
id: sessionId,
|
||||
state: newSessionState,
|
||||
},
|
||||
visitedEdges,
|
||||
})
|
||||
} 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,
|
||||
} 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,
|
||||
})
|
||||
} else {
|
||||
await restartSession({
|
||||
state: newSessionState,
|
||||
id: sessionId,
|
||||
})
|
||||
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',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -624,7 +624,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -634,6 +636,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -3929,7 +3937,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -3939,6 +3949,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -8007,7 +8023,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -8017,6 +8035,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -11708,7 +11732,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -11718,6 +11744,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -15497,7 +15529,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -15507,6 +15541,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -19195,7 +19235,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -19205,6 +19247,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -22918,7 +22966,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -22928,6 +22978,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -26647,7 +26703,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -26657,6 +26715,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -30203,7 +30267,6 @@
|
||||
"version",
|
||||
"id",
|
||||
"name",
|
||||
"events",
|
||||
"groups",
|
||||
"edges",
|
||||
"variables",
|
||||
@@ -30425,7 +30488,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -30435,6 +30500,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -34213,7 +34284,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -34223,6 +34296,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -37878,7 +37957,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -37888,6 +37969,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -41678,7 +41765,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -41688,6 +41777,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -45166,10 +45261,6 @@
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"publicId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"resultsTablePreferences": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -45208,15 +45299,13 @@
|
||||
"required": [
|
||||
"version",
|
||||
"name",
|
||||
"events",
|
||||
"groups",
|
||||
"edges",
|
||||
"variables",
|
||||
"theme",
|
||||
"settings",
|
||||
"icon",
|
||||
"folderId",
|
||||
"publicId"
|
||||
"folderId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
@@ -45418,7 +45507,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -45428,6 +45519,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -48816,10 +48913,6 @@
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"publicId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"resultsTablePreferences": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -48865,8 +48958,7 @@
|
||||
"theme",
|
||||
"settings",
|
||||
"icon",
|
||||
"folderId",
|
||||
"publicId"
|
||||
"folderId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -49092,7 +49184,9 @@
|
||||
"enum": [
|
||||
"url",
|
||||
"youtube",
|
||||
"vimeo"
|
||||
"vimeo",
|
||||
"tiktok",
|
||||
"gumlet"
|
||||
]
|
||||
},
|
||||
"height": {
|
||||
@@ -49102,6 +49196,12 @@
|
||||
},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"aspectRatio": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxWidth": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -55954,8 +56054,47 @@
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"startGroupId": {
|
||||
"type": "string"
|
||||
"startFrom": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"group"
|
||||
]
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"groupId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"event"
|
||||
]
|
||||
},
|
||||
"eventId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"eventId"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
79
apps/viewer/src/features/chat/api/continueChat.ts
Normal file
79
apps/viewer/src/features/chat/api/continueChat.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import { continueChatResponseSchema } from '@typebot.io/schemas/features/chat/schema'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
|
||||
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
|
||||
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const continueChat = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/v1/sessions/{sessionId}/continueChat',
|
||||
summary: 'Continue chat',
|
||||
description:
|
||||
'To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.',
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
message: z.string().optional(),
|
||||
sessionId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(continueChatResponseSchema)
|
||||
.mutation(async ({ input: { sessionId, message } }) => {
|
||||
const session = await getSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Session not found.',
|
||||
})
|
||||
}
|
||||
|
||||
const isSessionExpired =
|
||||
session &&
|
||||
isDefined(session.state.expiryTimeout) &&
|
||||
session.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
|
||||
|
||||
if (isSessionExpired)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Session expired. You need to start a new session.',
|
||||
})
|
||||
|
||||
const {
|
||||
messages,
|
||||
input,
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
logs,
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
} = await continueBotFlow(message, { version: 2, state: session.state })
|
||||
|
||||
if (newSessionState)
|
||||
await saveStateToDatabase({
|
||||
session: {
|
||||
id: session.id,
|
||||
state: newSessionState,
|
||||
},
|
||||
input,
|
||||
logs,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
})
|
||||
|
||||
return {
|
||||
messages,
|
||||
input,
|
||||
clientSideActions,
|
||||
dynamicTheme: parseDynamicTheme(newSessionState),
|
||||
logs,
|
||||
lastMessageNewFormat,
|
||||
}
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import {
|
||||
chatReplySchema,
|
||||
sendMessageInputSchema,
|
||||
} from '@typebot.io/schemas/features/chat/schema'
|
||||
chatReplySchema,
|
||||
} from '@typebot.io/schemas/features/chat/legacy/schema'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
import { startSession } from '@typebot.io/bot-engine/startSession'
|
||||
@@ -16,10 +16,12 @@ export const sendMessageV1 = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/sendMessage',
|
||||
path: '/v1/sendMessage',
|
||||
summary: 'Send a message',
|
||||
description:
|
||||
'To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.',
|
||||
tags: ['Deprecated'],
|
||||
deprecated: true,
|
||||
},
|
||||
})
|
||||
.input(sendMessageInputSchema)
|
||||
@@ -60,8 +62,45 @@ export const sendMessageV1 = publicProcedure
|
||||
visitedEdges,
|
||||
} = await startSession({
|
||||
version: 1,
|
||||
startParams,
|
||||
userId: user?.id,
|
||||
startParams:
|
||||
startParams.isPreview || typeof startParams.typebot !== 'string'
|
||||
? {
|
||||
type: 'preview',
|
||||
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
|
||||
isStreamEnabled: startParams.isStreamEnabled,
|
||||
startFrom:
|
||||
'startGroupId' in startParams && startParams.startGroupId
|
||||
? {
|
||||
type: 'group',
|
||||
groupId: startParams.startGroupId,
|
||||
}
|
||||
: 'startEventId' in startParams &&
|
||||
startParams.startEventId
|
||||
? {
|
||||
type: 'event',
|
||||
eventId: startParams.startEventId,
|
||||
}
|
||||
: undefined,
|
||||
typebotId:
|
||||
typeof startParams.typebot === 'string'
|
||||
? startParams.typebot
|
||||
: startParams.typebot.id,
|
||||
typebot:
|
||||
typeof startParams.typebot === 'string'
|
||||
? undefined
|
||||
: startParams.typebot,
|
||||
message,
|
||||
userId: parseUserId(user?.id),
|
||||
}
|
||||
: {
|
||||
type: 'live',
|
||||
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
|
||||
isStreamEnabled: startParams.isStreamEnabled,
|
||||
publicId: startParams.typebot,
|
||||
prefilledVariables: startParams.prefilledVariables,
|
||||
resultId: startParams.resultId,
|
||||
message,
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
||||
@@ -133,3 +172,13 @@ export const sendMessageV1 = publicProcedure
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const parseUserId = (userId?: string): string => {
|
||||
if (!userId)
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'You need to be authenticated to perform this action',
|
||||
})
|
||||
|
||||
return userId
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import {
|
||||
chatReplySchema,
|
||||
sendMessageInputSchema,
|
||||
} from '@typebot.io/schemas/features/chat/schema'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
import { startSession } from '@typebot.io/bot-engine/startSession'
|
||||
@@ -11,15 +7,21 @@ import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
|
||||
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
|
||||
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import {
|
||||
chatReplySchema,
|
||||
sendMessageInputSchema,
|
||||
} from '@typebot.io/schemas/features/chat/legacy/schema'
|
||||
|
||||
export const sendMessageV2 = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/sendMessage',
|
||||
path: '/v2/sendMessage',
|
||||
summary: 'Send a message',
|
||||
description:
|
||||
'To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.',
|
||||
tags: ['Deprecated'],
|
||||
deprecated: true,
|
||||
},
|
||||
})
|
||||
.input(sendMessageInputSchema)
|
||||
@@ -60,8 +62,45 @@ export const sendMessageV2 = publicProcedure
|
||||
visitedEdges,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams,
|
||||
userId: user?.id,
|
||||
startParams:
|
||||
startParams.isPreview || typeof startParams.typebot !== 'string'
|
||||
? {
|
||||
type: 'preview',
|
||||
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
|
||||
isStreamEnabled: startParams.isStreamEnabled,
|
||||
startFrom:
|
||||
'startGroupId' in startParams && startParams.startGroupId
|
||||
? {
|
||||
type: 'group',
|
||||
groupId: startParams.startGroupId,
|
||||
}
|
||||
: 'startEventId' in startParams &&
|
||||
startParams.startEventId
|
||||
? {
|
||||
type: 'event',
|
||||
eventId: startParams.startEventId,
|
||||
}
|
||||
: undefined,
|
||||
typebotId:
|
||||
typeof startParams.typebot === 'string'
|
||||
? startParams.typebot
|
||||
: startParams.typebot.id,
|
||||
typebot:
|
||||
typeof startParams.typebot === 'string'
|
||||
? undefined
|
||||
: startParams.typebot,
|
||||
message,
|
||||
userId: parseUserId(user?.id),
|
||||
}
|
||||
: {
|
||||
type: 'live',
|
||||
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
|
||||
isStreamEnabled: startParams.isStreamEnabled,
|
||||
publicId: startParams.typebot,
|
||||
prefilledVariables: startParams.prefilledVariables,
|
||||
resultId: startParams.resultId,
|
||||
message,
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
||||
@@ -133,3 +172,13 @@ export const sendMessageV2 = publicProcedure
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const parseUserId = (userId?: string): string => {
|
||||
if (!userId)
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'You need to be authenticated to perform this action',
|
||||
})
|
||||
|
||||
return userId
|
||||
}
|
||||
63
apps/viewer/src/features/chat/api/saveClientLogs.ts
Normal file
63
apps/viewer/src/features/chat/api/saveClientLogs.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import { chatLogSchema } from '@typebot.io/schemas/features/chat/schema'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
import { z } from 'zod'
|
||||
import { saveLogs } from '@typebot.io/bot-engine/queries/saveLogs'
|
||||
import { formatLogDetails } from '@typebot.io/bot-engine/logs/helpers/formatLogDetails'
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
export const saveClientLogs = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/v1/sessions/{sessionId}/clientLogs',
|
||||
summary: 'Save client logs',
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
sessionId: z.string(),
|
||||
clientLogs: z.array(chatLogSchema),
|
||||
})
|
||||
)
|
||||
.output(z.object({ message: z.string() }))
|
||||
.mutation(async ({ input: { sessionId, clientLogs } }) => {
|
||||
const session = await getSession(sessionId)
|
||||
|
||||
if (!session) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Session not found.',
|
||||
})
|
||||
}
|
||||
|
||||
const resultId = session.state.typebotsQueue[0].resultId
|
||||
|
||||
if (!resultId) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Result not found.',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await saveLogs(
|
||||
clientLogs.map((log) => ({
|
||||
...log,
|
||||
resultId,
|
||||
details: formatLogDetails(log.details),
|
||||
}))
|
||||
)
|
||||
return {
|
||||
message: 'Logs successfully saved.',
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to save logs', e)
|
||||
Sentry.captureException(e)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to save logs.',
|
||||
})
|
||||
}
|
||||
})
|
||||
83
apps/viewer/src/features/chat/api/startChat.ts
Normal file
83
apps/viewer/src/features/chat/api/startChat.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import {
|
||||
startChatInputSchema,
|
||||
startChatResponseSchema,
|
||||
} from '@typebot.io/schemas/features/chat/schema'
|
||||
import { startSession } from '@typebot.io/bot-engine/startSession'
|
||||
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
|
||||
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
|
||||
|
||||
export const startChat = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/v1/typebots/{publicId}/startChat',
|
||||
summary: 'Start chat',
|
||||
},
|
||||
})
|
||||
.input(startChatInputSchema)
|
||||
.output(startChatResponseSchema)
|
||||
.mutation(
|
||||
async ({
|
||||
input: {
|
||||
message,
|
||||
isOnlyRegistering,
|
||||
publicId,
|
||||
isStreamEnabled,
|
||||
prefilledVariables,
|
||||
resultId: startResultId,
|
||||
},
|
||||
}) => {
|
||||
const {
|
||||
typebot,
|
||||
messages,
|
||||
input,
|
||||
resultId,
|
||||
dynamicTheme,
|
||||
logs,
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
visitedEdges,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams: {
|
||||
type: 'live',
|
||||
isOnlyRegistering,
|
||||
isStreamEnabled,
|
||||
publicId,
|
||||
prefilledVariables,
|
||||
resultId: startResultId,
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
||||
const session = isOnlyRegistering
|
||||
? await restartSession({
|
||||
state: newSessionState,
|
||||
})
|
||||
: await saveStateToDatabase({
|
||||
session: {
|
||||
state: newSessionState,
|
||||
},
|
||||
input,
|
||||
logs,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
})
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
typebot: {
|
||||
id: typebot.id,
|
||||
theme: typebot.theme,
|
||||
settings: typebot.settings,
|
||||
},
|
||||
messages,
|
||||
input,
|
||||
resultId,
|
||||
dynamicTheme,
|
||||
logs,
|
||||
clientSideActions,
|
||||
}
|
||||
}
|
||||
)
|
||||
83
apps/viewer/src/features/chat/api/startChatPreview.ts
Normal file
83
apps/viewer/src/features/chat/api/startChatPreview.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import {
|
||||
startPreviewChatInputSchema,
|
||||
startPreviewChatResponseSchema,
|
||||
} from '@typebot.io/schemas/features/chat/schema'
|
||||
import { startSession } from '@typebot.io/bot-engine/startSession'
|
||||
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
|
||||
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
|
||||
|
||||
export const startChatPreview = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/v1/typebots/{typebotId}/preview/startChat',
|
||||
summary: 'Start preview chat',
|
||||
},
|
||||
})
|
||||
.input(startPreviewChatInputSchema)
|
||||
.output(startPreviewChatResponseSchema)
|
||||
.mutation(
|
||||
async ({
|
||||
input: {
|
||||
message,
|
||||
isOnlyRegistering,
|
||||
isStreamEnabled,
|
||||
startFrom,
|
||||
typebotId,
|
||||
typebot: startTypebot,
|
||||
},
|
||||
ctx: { user },
|
||||
}) => {
|
||||
const {
|
||||
typebot,
|
||||
messages,
|
||||
input,
|
||||
dynamicTheme,
|
||||
logs,
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
visitedEdges,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams: {
|
||||
type: 'preview',
|
||||
isOnlyRegistering,
|
||||
isStreamEnabled,
|
||||
startFrom,
|
||||
typebotId,
|
||||
typebot: startTypebot,
|
||||
userId: user.id,
|
||||
},
|
||||
message,
|
||||
})
|
||||
|
||||
const session = isOnlyRegistering
|
||||
? await restartSession({
|
||||
state: newSessionState,
|
||||
})
|
||||
: await saveStateToDatabase({
|
||||
session: {
|
||||
state: newSessionState,
|
||||
},
|
||||
input,
|
||||
logs,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
})
|
||||
|
||||
return {
|
||||
sessionId: session.id,
|
||||
typebot: {
|
||||
id: typebot.id,
|
||||
theme: typebot.theme,
|
||||
settings: typebot.settings,
|
||||
},
|
||||
messages,
|
||||
input,
|
||||
dynamicTheme,
|
||||
logs,
|
||||
clientSideActions,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,4 +1,3 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
@@ -9,12 +8,13 @@ import {
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
|
||||
export const updateTypebotInSession = publicProcedure
|
||||
export const updateTypebotInSession = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/sessions/{sessionId}/updateTypebot',
|
||||
path: '/v1/sessions/{sessionId}/updateTypebot',
|
||||
summary: 'Update typebot in session',
|
||||
description:
|
||||
'Update chat session with latest typebot modifications. This is useful when you want to update the typebot in an ongoing session after making changes to it.',
|
||||
@@ -28,8 +28,6 @@ export const updateTypebotInSession = publicProcedure
|
||||
)
|
||||
.output(z.object({ message: z.literal('success') }))
|
||||
.mutation(async ({ input: { sessionId }, ctx: { user } }) => {
|
||||
if (!user)
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
|
||||
const session = await getSession(sessionId)
|
||||
if (!session)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' })
|
||||
|
||||
@@ -19,10 +19,11 @@ export const getUploadUrl = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/blocks/{blockId}/storage/upload-url',
|
||||
path: '/v1/typebots/{typebotId}/blocks/{blockId}/storage/upload-url',
|
||||
summary: 'Get upload URL for a file',
|
||||
description: 'Used for the web client to get the bucket upload file.',
|
||||
deprecated: true,
|
||||
tags: ['Deprecated'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
|
||||
@@ -13,7 +13,7 @@ export const generateUploadUrl = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/generate-upload-url',
|
||||
path: '/v1/generate-upload-url',
|
||||
summary: 'Generate upload URL',
|
||||
description: 'Used to upload anything from the client to S3 bucket',
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ export const receiveMessage = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
summary: 'Message webhook',
|
||||
tags: ['WhatsApp'],
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export const subscribeWebhook = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
summary: 'Subscribe webhook',
|
||||
tags: ['WhatsApp'],
|
||||
protect: true,
|
||||
|
||||
26
apps/viewer/src/helpers/server/appRouter.ts
Normal file
26
apps/viewer/src/helpers/server/appRouter.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { sendMessageV1 } from '@/features/chat/api/legacy/sendMessageV1'
|
||||
import { whatsAppRouter } from '@/features/whatsapp/api/router'
|
||||
import { router } from './trpc'
|
||||
import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession'
|
||||
import { getUploadUrl } from '@/features/fileUpload/api/deprecated/getUploadUrl'
|
||||
import { generateUploadUrl } from '@/features/fileUpload/api/generateUploadUrl'
|
||||
import { sendMessageV2 } from '@/features/chat/api/legacy/sendMessageV2'
|
||||
import { continueChat } from '@/features/chat/api/continueChat'
|
||||
import { saveClientLogs } from '@/features/chat/api/saveClientLogs'
|
||||
import { startChat } from '@/features/chat/api/startChat'
|
||||
import { startChatPreview } from '@/features/chat/api/startChatPreview'
|
||||
|
||||
export const appRouter = router({
|
||||
sendMessageV1,
|
||||
sendMessageV2,
|
||||
startChat,
|
||||
continueChat,
|
||||
startChatPreview: startChatPreview,
|
||||
getUploadUrl,
|
||||
generateUploadUrl,
|
||||
updateTypebotInSession,
|
||||
whatsAppRouter,
|
||||
saveClientLogs,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
@@ -3,6 +3,8 @@ import { inferAsyncReturnType } from '@trpc/server'
|
||||
import * as trpcNext from '@trpc/server/adapters/next'
|
||||
import { User } from '@typebot.io/prisma'
|
||||
import { NextApiRequest } from 'next'
|
||||
import { mockedUser } from '@typebot.io/lib/mockedUser'
|
||||
import { env } from '@typebot.io/env'
|
||||
|
||||
export async function createContext(opts: trpcNext.CreateNextContextOptions) {
|
||||
const user = await getAuthenticatedUser(opts.req)
|
||||
@@ -15,6 +17,7 @@ export async function createContext(opts: trpcNext.CreateNextContextOptions) {
|
||||
const getAuthenticatedUser = async (
|
||||
req: NextApiRequest
|
||||
): Promise<User | undefined> => {
|
||||
if (env.NEXT_PUBLIC_E2E_TEST) return mockedUser
|
||||
const bearerToken = extractBearerToken(req)
|
||||
if (!bearerToken) return
|
||||
return authenticateByToken(bearerToken)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { generateOpenApiDocument } from 'trpc-openapi'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { appRouter } from './routers/appRouterV2'
|
||||
import { appRouter } from './appRouter'
|
||||
|
||||
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||
title: 'Chat API',
|
||||
version: '2.0.0',
|
||||
baseUrl: 'https://typebot.io/api/v2',
|
||||
version: '3.0.0',
|
||||
baseUrl: 'https://typebot.io/api',
|
||||
docsUrl: 'https://docs.typebot.io/api',
|
||||
})
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { sendMessageV1 } from '@/features/chat/api/sendMessageV1'
|
||||
import { whatsAppRouter } from '@/features/whatsapp/api/router'
|
||||
import { router } from '../trpc'
|
||||
import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession'
|
||||
import { getUploadUrl } from '@/features/fileUpload/api/deprecated/getUploadUrl'
|
||||
import { generateUploadUrl } from '@/features/fileUpload/api/generateUploadUrl'
|
||||
|
||||
export const appRouter = router({
|
||||
sendMessageV1,
|
||||
getUploadUrl,
|
||||
generateUploadUrl,
|
||||
updateTypebotInSession,
|
||||
whatsAppRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
@@ -1,16 +0,0 @@
|
||||
import { sendMessageV2 } from '@/features/chat/api/sendMessageV2'
|
||||
import { whatsAppRouter } from '@/features/whatsapp/api/router'
|
||||
import { router } from '../trpc'
|
||||
import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession'
|
||||
import { getUploadUrl } from '@/features/fileUpload/api/deprecated/getUploadUrl'
|
||||
import { generateUploadUrl } from '@/features/fileUpload/api/generateUploadUrl'
|
||||
|
||||
export const appRouter = router({
|
||||
sendMessageV2,
|
||||
getUploadUrl,
|
||||
generateUploadUrl,
|
||||
updateTypebotInSession,
|
||||
whatsAppRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
@@ -1,4 +1,4 @@
|
||||
import { initTRPC } from '@trpc/server'
|
||||
import { TRPCError, initTRPC } from '@trpc/server'
|
||||
import { OpenApiMeta } from 'trpc-openapi'
|
||||
import superjson from 'superjson'
|
||||
import { Context } from './context'
|
||||
@@ -8,13 +8,23 @@ const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
|
||||
transformer: superjson,
|
||||
})
|
||||
|
||||
export const router = t.router
|
||||
|
||||
const sentryMiddleware = t.middleware(
|
||||
Sentry.Handlers.trpcMiddleware({
|
||||
attachRpcInput: true,
|
||||
})
|
||||
)
|
||||
|
||||
const injectUser = t.middleware(({ next, ctx }) => {
|
||||
export const publicProcedure = t.procedure.use(sentryMiddleware)
|
||||
|
||||
const isAuthed = t.middleware(({ next, ctx }) => {
|
||||
if (!ctx.user?.id) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'You need to be authenticated to perform this action',
|
||||
})
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
user: ctx.user,
|
||||
@@ -22,10 +32,6 @@ const injectUser = t.middleware(({ next, ctx }) => {
|
||||
})
|
||||
})
|
||||
|
||||
const finalMiddleware = sentryMiddleware.unstable_pipe(injectUser)
|
||||
|
||||
export const middleware = t.middleware
|
||||
|
||||
export const router = t.router
|
||||
|
||||
export const publicProcedure = t.procedure.use(finalMiddleware)
|
||||
export const authenticatedProcedure = t.procedure.use(
|
||||
sentryMiddleware.unstable_pipe(isAuthed)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { appRouter } from '@/helpers/server/routers/appRouterV2'
|
||||
import { appRouter } from '@/helpers/server/appRouter'
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { createOpenApiNextHandler } from 'trpc-openapi'
|
||||
import cors from 'nextjs-cors'
|
||||
@@ -1,23 +0,0 @@
|
||||
import { appRouter } from '@/helpers/server/routers/appRouterV1'
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { createOpenApiNextHandler } from 'trpc-openapi'
|
||||
import cors from 'nextjs-cors'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { createContext } from '@/helpers/server/context'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await cors(req, res)
|
||||
|
||||
return createOpenApiNextHandler({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
onError({ error }) {
|
||||
if (error.code === 'INTERNAL_SERVER_ERROR') {
|
||||
Sentry.captureException(error)
|
||||
console.error('Something went wrong', error)
|
||||
}
|
||||
},
|
||||
})(req, res)
|
||||
}
|
||||
|
||||
export default handler
|
||||
@@ -2,7 +2,6 @@ import { getTestAsset } from '@/test/utils/playwright'
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { SendMessageInput } from '@typebot.io/schemas'
|
||||
import {
|
||||
createWebhook,
|
||||
deleteTypebots,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
importTypebotInDatabase,
|
||||
} from '@typebot.io/lib/playwright/databaseActions'
|
||||
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
|
||||
import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas'
|
||||
|
||||
test.afterEach(async () => {
|
||||
await deleteWebhooks(['chat-webhook-id'])
|
||||
@@ -40,17 +40,18 @@ test('API chat execution should work on preview bot', async ({ request }) => {
|
||||
url: 'https://api.chucknorris.io/jokes/random',
|
||||
})
|
||||
|
||||
await test.step('Start the chat', async () => {
|
||||
let chatSessionId: string
|
||||
|
||||
await test.step('Can start and continue chat', async () => {
|
||||
const { sessionId, messages, input, resultId } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
await request.post(`/api/v1/typebots/${typebotId}/preview/startChat`, {
|
||||
data: {
|
||||
startParams: {
|
||||
typebot: typebotId,
|
||||
isPreview: true,
|
||||
},
|
||||
} satisfies SendMessageInput,
|
||||
isOnlyRegistering: false,
|
||||
isStreamEnabled: false,
|
||||
} satisfies Omit<StartPreviewChatInput, 'typebotId'>,
|
||||
})
|
||||
).json()
|
||||
chatSessionId = sessionId
|
||||
expect(resultId).toBeUndefined()
|
||||
expect(sessionId).toBeDefined()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
@@ -61,6 +62,38 @@ test('API chat execution should work on preview bot', async ({ request }) => {
|
||||
])
|
||||
expect(input.type).toBe('text input')
|
||||
})
|
||||
|
||||
await test.step('Can answer Name question', async () => {
|
||||
const { messages, input } = await (
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: {
|
||||
message: 'John',
|
||||
},
|
||||
})
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
{
|
||||
children: [
|
||||
{ text: 'Nice to meet you ' },
|
||||
{
|
||||
type: 'inline-variable',
|
||||
children: [
|
||||
{
|
||||
type: 'p',
|
||||
children: [
|
||||
{
|
||||
text: 'John',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
type: 'p',
|
||||
},
|
||||
])
|
||||
expect(input.type).toBe('number input')
|
||||
})
|
||||
})
|
||||
|
||||
test('API chat execution should work on published bot', async ({ request }) => {
|
||||
@@ -83,12 +116,11 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Start the chat', async () => {
|
||||
const { sessionId, messages, input, resultId } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
await request.post(`/api/v1/typebots/${publicId}/startChat`, {
|
||||
data: {
|
||||
startParams: {
|
||||
typebot: publicId,
|
||||
},
|
||||
} satisfies SendMessageInput,
|
||||
isOnlyRegistering: false,
|
||||
isStreamEnabled: false,
|
||||
} satisfies Omit<StartChatInput, 'publicId'>,
|
||||
})
|
||||
).json()
|
||||
chatSessionId = sessionId
|
||||
@@ -111,8 +143,8 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Answer Name question', async () => {
|
||||
const { messages, input } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: { message: 'John', sessionId: chatSessionId },
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: { message: 'John' },
|
||||
})
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
@@ -142,8 +174,8 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Answer Age question', async () => {
|
||||
const { messages, input } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: { message: '24', sessionId: chatSessionId },
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: { message: '24' },
|
||||
})
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
@@ -181,8 +213,8 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Answer Rating question', async () => {
|
||||
const { messages, input } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: { message: '8', sessionId: chatSessionId },
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: { message: '8' },
|
||||
})
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
@@ -196,8 +228,8 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Answer Email question with wrong input', async () => {
|
||||
const { messages, input } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: { message: 'invalid email', sessionId: chatSessionId },
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: { message: 'invalid email' },
|
||||
})
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
@@ -215,8 +247,8 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Answer Email question with valid input', async () => {
|
||||
const { messages, input } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: { message: 'typebot@email.com', sessionId: chatSessionId },
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: { message: 'typebot@email.com' },
|
||||
})
|
||||
).json()
|
||||
expect(messages.length).toBe(0)
|
||||
@@ -225,8 +257,8 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Answer URL question', async () => {
|
||||
const { messages, input } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: { message: 'https://typebot.io', sessionId: chatSessionId },
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: { message: 'https://typebot.io' },
|
||||
})
|
||||
).json()
|
||||
expect(messages.length).toBe(0)
|
||||
@@ -235,8 +267,8 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
|
||||
await test.step('Answer Buttons question with invalid choice', async () => {
|
||||
const { messages } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: { message: 'Yes', sessionId: chatSessionId },
|
||||
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
|
||||
data: { message: 'Yes' },
|
||||
})
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
@@ -263,14 +295,14 @@ test('API chat execution should work on published bot', async ({ request }) => {
|
||||
})
|
||||
await test.step('Starting with a message when typebot starts with input should proceed', async () => {
|
||||
const { messages } = await (
|
||||
await request.post(`/api/v2/sendMessage`, {
|
||||
data: {
|
||||
message: 'Hey',
|
||||
startParams: {
|
||||
typebot: 'starting-with-input-public',
|
||||
},
|
||||
} satisfies SendMessageInput,
|
||||
})
|
||||
await request.post(
|
||||
`/api/v1/typebots/starting-with-input-public/startChat`,
|
||||
{
|
||||
data: {
|
||||
message: 'Hey',
|
||||
} satisfies Omit<StartChatInput, 'publicId'>,
|
||||
}
|
||||
)
|
||||
).json()
|
||||
expect(messages[0].content.richText).toStrictEqual([
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
|
||||
|
||||
const [, response] = await Promise.all([
|
||||
page.goto(`/${typebotId}-public`),
|
||||
page.waitForResponse(/sendMessage/),
|
||||
page.waitForResponse(/startChat/),
|
||||
])
|
||||
const { resultId } = await response.json()
|
||||
expect(resultId).toBeDefined()
|
||||
@@ -38,7 +38,7 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
|
||||
|
||||
const [, secondResponse] = await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForResponse(/sendMessage/),
|
||||
page.waitForResponse(/startChat/),
|
||||
])
|
||||
const { resultId: secondResultId } = await secondResponse.json()
|
||||
expect(secondResultId).toBe(resultId)
|
||||
@@ -57,7 +57,7 @@ test.describe('Create result on page refresh enabled', () => {
|
||||
])
|
||||
const [, response] = await Promise.all([
|
||||
page.goto(`/${typebotId}-public`),
|
||||
page.waitForResponse(/sendMessage/),
|
||||
page.waitForResponse(/startChat/),
|
||||
])
|
||||
const { resultId } = await response.json()
|
||||
expect(resultId).toBeDefined()
|
||||
@@ -65,7 +65,7 @@ test.describe('Create result on page refresh enabled', () => {
|
||||
await expect(page.getByRole('textbox')).toBeVisible()
|
||||
const [, secondResponse] = await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForResponse(/sendMessage/),
|
||||
page.waitForResponse(/startChat/),
|
||||
])
|
||||
const { resultId: secondResultId } = await secondResponse.json()
|
||||
expect(secondResultId).not.toBe(resultId)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { generateOpenApiDocument } from 'trpc-openapi'
|
||||
import { writeFileSync } from 'fs'
|
||||
import { appRouter } from '@/helpers/server/routers/appRouterV2'
|
||||
import { appRouter } from '@/helpers/server/appRouter'
|
||||
|
||||
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||
title: 'Chat API',
|
||||
version: '2.0.0',
|
||||
baseUrl: 'https://typebot.io/api/v2',
|
||||
version: '3.0.0',
|
||||
baseUrl: 'https://typebot.io/api',
|
||||
docsUrl: 'https://docs.typebot.io/api',
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user