2
0

🚸 (whatsapp) Avoid multiple replies to be sent concurently

Closes #972, closes #1453
This commit is contained in:
Baptiste Arnaud
2024-04-18 09:38:22 +02:00
parent 94539e8ed3
commit 7bec58e745
12 changed files with 84 additions and 21 deletions

View File

@ -15,7 +15,8 @@
"DATABASE_URL": "postgresql://postgres:typebot@127.0.0.1:5432/typebot", "DATABASE_URL": "postgresql://postgres:typebot@127.0.0.1:5432/typebot",
"NEXT_PUBLIC_VIEWER_URL": "http://localhost:3001", "NEXT_PUBLIC_VIEWER_URL": "http://localhost:3001",
"NEXTAUTH_URL": "http://localhost:3000", "NEXTAUTH_URL": "http://localhost:3000",
"ENCRYPTION_SECRET": "H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S" "ENCRYPTION_SECRET": "H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S",
"S3_ENDPOINT": "http://localhost:9000"
}, },
"[prisma]": { "[prisma]": {
"editor.defaultFormatter": "Prisma.prisma" "editor.defaultFormatter": "Prisma.prisma"

View File

@ -12473,10 +12473,18 @@
} }
} }
} }
},
"runtime": {
"type": "string",
"enum": [
"edge",
"nodejs"
]
} }
}, },
"required": [ "required": [
"messages" "messages",
"runtime"
] ]
}, },
"lastBubbleBlockId": { "lastBubbleBlockId": {
@ -12753,6 +12761,13 @@
true true
] ]
}, },
"runtime": {
"type": "string",
"enum": [
"edge",
"nodejs"
]
},
"lastBubbleBlockId": { "lastBubbleBlockId": {
"type": "string" "type": "string"
}, },
@ -12762,7 +12777,8 @@
}, },
"required": [ "required": [
"type", "type",
"stream" "stream",
"runtime"
], ],
"title": "Exec stream" "title": "Exec stream"
}, },

View File

@ -5,15 +5,18 @@ import { SessionState } from '@typebot.io/schemas'
type Props = { type Props = {
id?: string id?: string
state: SessionState state: SessionState
isReplying?: boolean
} }
export const createSession = ({ export const createSession = ({
id, id,
state, state,
isReplying,
}: Props): Prisma.PrismaPromise<any> => }: Props): Prisma.PrismaPromise<any> =>
prisma.chatSession.create({ prisma.chatSession.create({
data: { data: {
id, id,
state, state,
isReplying,
}, },
}) })

View File

@ -4,7 +4,7 @@ import { sessionStateSchema } from '@typebot.io/schemas'
export const getSession = async (sessionId: string) => { export const getSession = async (sessionId: string) => {
const session = await prisma.chatSession.findUnique({ const session = await prisma.chatSession.findUnique({
where: { id: sessionId }, where: { id: sessionId },
select: { id: true, state: true, updatedAt: true }, select: { id: true, state: true, updatedAt: true, isReplying: true },
}) })
if (!session) return null if (!session) return null
return { ...session, state: sessionStateSchema.parse(session.state) } return { ...session, state: sessionStateSchema.parse(session.state) }

View File

@ -0,0 +1,20 @@
import prisma from '@typebot.io/lib/prisma'
type Props = {
existingSessionId: string | undefined
newSessionId: string
}
export const setChatSessionHasReplying = async ({
existingSessionId,
newSessionId,
}: Props) => {
if (existingSessionId) {
return prisma.chatSession.updateMany({
where: { id: existingSessionId },
data: { isReplying: true },
})
}
return prisma.chatSession.createMany({
data: { id: newSessionId, isReplying: true, state: {} },
})
}

View File

@ -5,15 +5,18 @@ import { SessionState } from '@typebot.io/schemas'
type Props = { type Props = {
id: string id: string
state: SessionState state: SessionState
isReplying: boolean
} }
export const updateSession = ({ export const updateSession = ({
id, id,
state, state,
isReplying,
}: Props): Prisma.PrismaPromise<any> => }: Props): Prisma.PrismaPromise<any> =>
prisma.chatSession.updateMany({ prisma.chatSession.updateMany({
where: { id }, where: { id },
data: { data: {
state, state,
isReplying,
}, },
}) })

View File

@ -16,7 +16,6 @@ type Props = {
logs: ContinueChatResponse['logs'] logs: ContinueChatResponse['logs']
clientSideActions: ContinueChatResponse['clientSideActions'] clientSideActions: ContinueChatResponse['clientSideActions']
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
forceCreateSession?: boolean
hasCustomEmbedBubble?: boolean hasCustomEmbedBubble?: boolean
} }
@ -25,7 +24,6 @@ export const saveStateToDatabase = async ({
input, input,
logs, logs,
clientSideActions, clientSideActions,
forceCreateSession,
visitedEdges, visitedEdges,
hasCustomEmbedBubble, hasCustomEmbedBubble,
}: Props) => { }: Props) => {
@ -43,13 +41,12 @@ export const saveStateToDatabase = async ({
if (id) { if (id) {
if (isCompleted && resultId) queries.push(deleteSession(id)) if (isCompleted && resultId) queries.push(deleteSession(id))
else queries.push(updateSession({ id, state })) else queries.push(updateSession({ id, state, isReplying: false }))
} }
const session = const session = id
id && !forceCreateSession ? { state, id }
? { state, id } : await createSession({ id, state, isReplying: false })
: await createSession({ id, state })
if (!resultId) { if (!resultId) {
if (queries.length > 0) await prisma.$transaction(queries) if (queries.length > 0) await prisma.$transaction(queries)

View File

@ -13,6 +13,7 @@ import { saveStateToDatabase } from '../saveStateToDatabase'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { isDefined } from '@typebot.io/lib/utils' import { isDefined } from '@typebot.io/lib/utils'
import { Reply } from '../types' import { Reply } from '../types'
import { setChatSessionHasReplying } from '../queries/setChatSessionHasReplying'
type Props = { type Props = {
receivedMessage: WhatsAppIncomingMessage receivedMessage: WhatsAppIncomingMessage
@ -67,6 +68,18 @@ export const resumeWhatsAppFlow = async ({
const session = await getSession(sessionId) const session = await getSession(sessionId)
if (session?.isReplying) {
console.log('Is currently replying, skipping...')
return {
message: 'Message received',
}
}
await setChatSessionHasReplying({
existingSessionId: session?.id,
newSessionId: sessionId,
})
const isSessionExpired = const isSessionExpired =
session && session &&
isDefined(session.state.expiryTimeout) && isDefined(session.state.expiryTimeout) &&
@ -116,7 +129,6 @@ export const resumeWhatsAppFlow = async ({
}) })
await saveStateToDatabase({ await saveStateToDatabase({
forceCreateSession: !session && isDefined(input),
clientSideActions: [], clientSideActions: [],
input, input,
logs, logs,

View File

@ -349,10 +349,11 @@ model ClaimableCustomPlan {
} }
model ChatSession { model ChatSession {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
state Json state Json
isReplying Boolean?
} }
model ThemeTemplate { model ThemeTemplate {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ChatSession" ADD COLUMN "isReplying" BOOLEAN;

View File

@ -328,10 +328,11 @@ model ClaimableCustomPlan {
} }
model ChatSession { model ChatSession {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
state Json state Json
isReplying Boolean?
} }
model ThemeTemplate { model ThemeTemplate {

View File

@ -28,13 +28,20 @@ import { preprocessTypebot } from '../typebot/helpers/preprocessTypebot'
import { typebotV5Schema, typebotV6Schema } from '../typebot/typebot' import { typebotV5Schema, typebotV6Schema } from '../typebot/typebot'
import { BubbleBlockType } from '../blocks/bubbles/constants' import { BubbleBlockType } from '../blocks/bubbles/constants'
import { clientSideActionSchema } from './clientSideAction' import { clientSideActionSchema } from './clientSideAction'
import { ChatSession as ChatSessionFromPrisma } from '@typebot.io/prisma'
const chatSessionSchema = z.object({ const chatSessionSchema = z.object({
id: z.string(), id: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
state: sessionStateSchema, state: sessionStateSchema,
}) isReplying: z
.boolean()
.nullable()
.describe(
'Used in WhatsApp runtime to avoid concurrent replies from the bot'
),
}) satisfies z.ZodType<ChatSessionFromPrisma, z.ZodTypeDef, unknown>
export type ChatSession = z.infer<typeof chatSessionSchema> export type ChatSession = z.infer<typeof chatSessionSchema>
const textMessageSchema = z const textMessageSchema = z