2
0

🧑‍💻 (chat) Introduce startChat and continueChat endpoints

Closes #1030
This commit is contained in:
Baptiste Arnaud
2023-11-13 15:27:36 +01:00
parent 63233eb7ee
commit 084588a086
74 changed files with 28426 additions and 645 deletions

View 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,
}
})

View File

@ -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
}

View File

@ -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
}

View 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.',
})
}
})

View 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,
}
}
)

View 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,
}
}
)

View File

@ -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' })

View File

@ -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(

View File

@ -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',
},

View File

@ -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'],
},

View File

@ -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,

View 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

View File

@ -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)

View File

@ -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',
})

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -1,4 +1,4 @@
import { appRouter } from '@/helpers/server/routers/appRouterV1'
import { appRouter } from '@/helpers/server/appRouter'
import * as Sentry from '@sentry/nextjs'
import { createOpenApiNextHandler } from 'trpc-openapi'
import cors from 'nextjs-cors'

View File

@ -1,23 +0,0 @@
import { appRouter } from '@/helpers/server/routers/appRouterV2'
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

View File

@ -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([
{

View File

@ -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)

View File

@ -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',
})