2
0

⚗️ Implement bot v2 MVP (#194)

Closes #190
This commit is contained in:
Baptiste Arnaud
2022-12-22 17:02:34 +01:00
committed by GitHub
parent e55823e011
commit 1a3869ae6d
202 changed files with 8060 additions and 1152 deletions

View File

@ -1,17 +1,10 @@
import { authenticateUser } from '@/features/auth/api'
import { checkChatsUsage } from '@/features/usage'
import prisma from '@/lib/prisma'
import { WorkspaceRole } from 'db'
import {
sendAlmostReachedChatsLimitEmail,
sendReachedChatsLimitEmail,
} from 'emails'
import { ResultWithAnswers } from 'models'
import { NextApiRequest, NextApiResponse } from 'next'
import { env, getChatsLimit, isDefined } from 'utils'
import { methodNotAllowed } from 'utils/api'
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
const user = await authenticateUser(req)
@ -47,125 +40,4 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
methodNotAllowed(res)
}
const checkChatsUsage = async (typebotId: string) => {
const typebot = await prisma.typebot.findUnique({
where: {
id: typebotId,
},
include: {
workspace: {
select: {
id: true,
plan: true,
additionalChatsIndex: true,
chatsLimitFirstEmailSentAt: true,
chatsLimitSecondEmailSentAt: true,
customChatsLimit: true,
},
},
},
})
const workspace = typebot?.workspace
if (!workspace) return false
const chatsLimit = getChatsLimit(workspace)
if (chatsLimit === -1) return
const now = new Date()
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
const chatsCount = await prisma.$transaction(async (tx) => {
const typebotIds = await tx.typebot.findMany({
where: {
workspaceId: workspace.id,
},
select: { id: true },
})
return tx.result.count({
where: {
typebotId: { in: typebotIds.map((typebot) => typebot.id) },
hasStarted: true,
createdAt: { gte: firstDayOfMonth, lte: firstDayOfNextMonth },
},
})
})
const hasSentFirstEmail =
workspace.chatsLimitFirstEmailSentAt !== null &&
workspace.chatsLimitFirstEmailSentAt < firstDayOfNextMonth &&
workspace.chatsLimitFirstEmailSentAt > firstDayOfMonth
const hasSentSecondEmail =
workspace.chatsLimitSecondEmailSentAt !== null &&
workspace.chatsLimitSecondEmailSentAt < firstDayOfNextMonth &&
workspace.chatsLimitSecondEmailSentAt > firstDayOfMonth
if (
chatsCount >= chatsLimit * LIMIT_EMAIL_TRIGGER_PERCENT &&
!hasSentFirstEmail &&
env('E2E_TEST') !== 'true'
)
await sendAlmostReachChatsLimitNotification({
workspaceId: workspace.id,
chatsLimit,
})
if (
chatsCount >= chatsLimit &&
!hasSentSecondEmail &&
env('E2E_TEST') !== 'true'
)
await sendReachedAlertNotification({
workspaceId: workspace.id,
chatsLimit,
})
return chatsCount >= chatsLimit
}
const sendAlmostReachChatsLimitNotification = async ({
workspaceId,
chatsLimit,
}: {
workspaceId: string
chatsLimit: number
}) => {
const members = await prisma.memberInWorkspace.findMany({
where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } },
})
await sendAlmostReachedChatsLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined),
chatsLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
})
await prisma.workspace.update({
where: { id: workspaceId },
data: { chatsLimitFirstEmailSentAt: new Date() },
})
}
const sendReachedAlertNotification = async ({
workspaceId,
chatsLimit,
}: {
workspaceId: string
chatsLimit: number
}) => {
const members = await prisma.memberInWorkspace.findMany({
where: { role: WorkspaceRole.ADMIN, workspaceId },
include: { user: { select: { email: true } } },
})
await sendReachedChatsLimitEmail({
to: members.map((member) => member.user.email).filter(isDefined),
chatsLimit,
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
})
await prisma.workspace.update({
where: { id: workspaceId },
data: { chatsLimitSecondEmailSentAt: new Date() },
})
}
export default handler

View File

@ -6,7 +6,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res, {
origin: 'https://docs.typebot.io',
origin: ['https://docs.typebot.io', 'http://localhost:3005'],
})
return createOpenApiNextHandler({

View File

@ -0,0 +1,85 @@
import { IncomingMessage } from 'http'
import { NotFoundPage } from '@/components/NotFoundPage'
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
import { env, getViewerUrl, isNotDefined } from 'utils'
import prisma from '@/lib/prisma'
import { TypebotPageV2, TypebotPageV2Props } from '@/components/TypebotPageV2'
import { ErrorPage } from '@/components/ErrorPage'
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { host, forwardedHost } = getHost(context.req)
const pathname = context.resolvedUrl.split('?')[0]
try {
if (!host) return { props: {} }
const viewerUrls = (getViewerUrl({ returnAll: true }) ?? '').split(',')
const isMatchingViewerUrl =
env('E2E_TEST') === 'true'
? true
: viewerUrls.some(
(url) =>
host.split(':')[0].includes(url.split('//')[1].split(':')[0]) ||
(forwardedHost &&
forwardedHost
.split(':')[0]
.includes(url.split('//')[1].split(':')[0]))
)
const typebot = isMatchingViewerUrl
? await getTypebotFromPublicId(context.query.publicId?.toString())
: null
if (!typebot)
console.log(
isMatchingViewerUrl
? `Couldn't find publicId: ${context.query.publicId?.toString()}`
: `Couldn't find customDomain`
)
return {
props: {
typebot,
url: `https://${forwardedHost ?? host}${pathname}`,
},
}
} catch (err) {
console.error(err)
}
return {
props: {},
url: `https://${forwardedHost ?? host}${pathname}`,
}
}
const getTypebotFromPublicId = async (
publicId?: string
): Promise<TypebotPageV2Props['typebot'] | null> => {
if (!publicId) return null
const typebot = (await prisma.typebot.findUnique({
where: { publicId },
select: {
id: true,
theme: true,
name: true,
settings: true,
isArchived: true,
isClosed: true,
},
})) as TypebotPageV2Props['typebot'] | null
if (isNotDefined(typebot)) return null
return typebot
}
const getHost = (
req?: IncomingMessage
): { host?: string; forwardedHost?: string } => ({
host: req?.headers ? req.headers.host : window.location.host,
forwardedHost: req?.headers['x-forwarded-host'] as string | undefined,
})
const App = ({ typebot, url }: TypebotPageV2Props) => {
if (!typebot || typebot.isArchived) return <NotFoundPage />
if (typebot.isClosed)
return <ErrorPage error={new Error('This bot is now closed')} />
return <TypebotPageV2 typebot={typebot} url={url} />
}
export default App