2
0

Add attachments option to text input (#1608)

Closes #854
This commit is contained in:
Baptiste Arnaud
2024-06-26 10:13:38 +02:00
committed by GitHub
parent 80da7af4f1
commit 6db0464fd7
88 changed files with 2959 additions and 735 deletions

View File

@@ -56,7 +56,6 @@
"@uiw/codemirror-theme-tokyo-night": "4.21.24",
"@uiw/react-codemirror": "4.21.24",
"@upstash/ratelimit": "0.4.3",
"@upstash/redis": "1.22.0",
"@use-gesture/react": "10.2.27",
"browser-image-compression": "2.0.2",
"canvas-confetti": "1.6.0",
@@ -69,6 +68,7 @@
"google-auth-library": "8.9.0",
"google-spreadsheet": "4.1.1",
"immer": "10.0.2",
"ioredis": "^5.4.1",
"isolated-vm": "4.7.2",
"jsonwebtoken": "9.0.1",
"ky": "1.2.4",

View File

@@ -8,15 +8,17 @@ import {
useColorModeValue,
Portal,
} from '@chakra-ui/react'
import React from 'react'
import React, { RefObject } from 'react'
import { EmojiOrImageIcon } from './EmojiOrImageIcon'
import { ImageUploadContent } from './ImageUploadContent'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
import { useTranslate } from '@tolgee/react'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
type Props = {
uploadFileProps: FilePathUploadProps
icon?: string | null
parentModalRef?: RefObject<HTMLElement | null> | undefined
onChangeIcon: (icon: string) => void
boxSize?: string
}
@@ -28,6 +30,7 @@ export const EditableEmojiOrImageIcon = ({
boxSize,
}: Props) => {
const { t } = useTranslate()
const { ref: parentModalRef } = useParentModal()
const bg = useColorModeValue('gray.100', 'gray.700')
return (
@@ -56,7 +59,7 @@ export const EditableEmojiOrImageIcon = ({
</PopoverTrigger>
</Flex>
</Tooltip>
<Portal>
<Portal containerRef={parentModalRef}>
<PopoverContent p="2">
<ImageUploadContent
uploadFileProps={uploadFileProps}

View File

@@ -24,7 +24,7 @@ test.describe('Date input block', () => {
'date'
)
await page.locator('[data-testid="from-date"]').fill('2021-01-01')
await page.locator('form').getByRole('button').click()
await page.getByLabel('Send').click()
await expect(page.locator('text="01/01/2021"')).toBeVisible()
await page.click(`text=Pick a date`)

View File

@@ -27,7 +27,9 @@ test('options should work', async ({ page }) => {
await page
.locator(`input[type="file"]`)
.setInputFiles([getTestAsset('avatar.jpg')])
await expect(page.locator(`text=File uploaded`)).toBeVisible()
await expect(
page.getByRole('img', { name: 'Attached image 1' })
).toBeVisible()
await page.click('text="Collect file"')
await page.click('text="Required?"')
await page.click('text="Allow multiple files?"')
@@ -46,9 +48,11 @@ test('options should work', async ({ page }) => {
getTestAsset('avatar.jpg'),
getTestAsset('avatar.jpg'),
])
await expect(page.locator(`text="3"`)).toBeVisible()
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(3)
await page.locator('text="Go"').click()
await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
await expect(
page.getByRole('img', { name: 'Attached image 1' })
).toBeVisible()
})
test.describe('Free workspace', () => {

View File

@@ -1,25 +1,48 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { Stack, Text } from '@chakra-ui/react'
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { TextInputBlock } from '@typebot.io/schemas'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { SetVariableLabel } from '@/components/SetVariableLabel'
type Props = {
options: TextInputBlock['options']
}
export const TextInputNodeContent = ({ options }: Props) => {
const { typebot } = useTypebot()
const attachmentVariableId =
typebot &&
options?.attachments?.isEnabled &&
options?.attachments.saveVariableId
if (options?.variableId)
return (
<WithVariableContent
variableId={options?.variableId}
h={options.isLong ? '100px' : 'auto'}
/>
<Stack w="calc(100% - 25px)">
<WithVariableContent
variableId={options?.variableId}
h={options.isLong ? '100px' : 'auto'}
/>
{attachmentVariableId && (
<SetVariableLabel
variables={typebot.variables}
variableId={attachmentVariableId}
/>
)}
</Stack>
)
return (
<Text color={'gray.500'} h={options?.isLong ? '100px' : 'auto'}>
{options?.labels?.placeholder ??
defaultTextInputOptions.labels.placeholder}
</Text>
<Stack>
<Text color={'gray.500'} h={options?.isLong ? '100px' : 'auto'}>
{options?.labels?.placeholder ??
defaultTextInputOptions.labels.placeholder}
</Text>
{attachmentVariableId && (
<SetVariableLabel
variables={typebot.variables}
variableId={attachmentVariableId}
/>
)}
</Stack>
)
}

View File

@@ -1,9 +1,12 @@
import { DropdownList } from '@/components/DropdownList'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { useTranslate } from '@tolgee/react'
import { TextInputBlock, Variable } from '@typebot.io/schemas'
import { fileVisibilityOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
import React from 'react'
@@ -14,21 +17,44 @@ type Props = {
export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
const { t } = useTranslate()
const handlePlaceholderChange = (placeholder: string) =>
const updatePlaceholder = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
const updateButtonLabel = (button: string) =>
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleLongChange = (isLong: boolean) =>
const updateIsLong = (isLong: boolean) =>
onOptionsChange({ ...options, isLong })
const handleVariableChange = (variable?: Variable) =>
const updateVariableId = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
const updateAttachmentsEnabled = (isEnabled: boolean) =>
onOptionsChange({
...options,
attachments: { ...options?.attachments, isEnabled },
})
const updateAttachmentsSaveVariableId = (variable?: Pick<Variable, 'id'>) =>
onOptionsChange({
...options,
attachments: { ...options?.attachments, saveVariableId: variable?.id },
})
const updateVisibility = (
visibility: (typeof fileVisibilityOptions)[number]
) =>
onOptionsChange({
...options,
attachments: { ...options?.attachments, visibility },
})
return (
<Stack spacing={4}>
<SwitchWithLabel
label={t('blocks.inputs.text.settings.longText.label')}
initialValue={options?.isLong ?? defaultTextInputOptions.isLong}
onCheckChange={handleLongChange}
onCheckChange={updateIsLong}
/>
<TextInput
label={t('blocks.inputs.settings.placeholder.label')}
@@ -36,22 +62,50 @@ export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
options?.labels?.placeholder ??
defaultTextInputOptions.labels.placeholder
}
onChange={handlePlaceholderChange}
onChange={updatePlaceholder}
/>
<TextInput
label={t('blocks.inputs.settings.button.label')}
defaultValue={
options?.labels?.button ?? defaultTextInputOptions.labels.button
}
onChange={handleButtonLabelChange}
onChange={updateButtonLabel}
/>
<SwitchWithRelatedSettings
label={'Allow attachments'}
initialValue={
options?.attachments?.isEnabled ??
defaultTextInputOptions.attachments.isEnabled
}
onCheckChange={updateAttachmentsEnabled}
>
<Stack>
<FormLabel mb="0" htmlFor="variable">
Save the URLs in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options?.attachments?.saveVariableId}
onSelectVariable={updateAttachmentsSaveVariableId}
/>
</Stack>
<DropdownList
label="Visibility:"
moreInfoTooltip='This setting determines who can see the uploaded files. "Public" means that anyone who has the link can see the files. "Private" means that only a members of this workspace can see the files.'
currentItem={
options?.attachments?.visibility ??
defaultTextInputOptions.attachments.visibility
}
onItemSelect={updateVisibility}
items={fileVisibilityOptions}
/>
</SwitchWithRelatedSettings>
<Stack>
<FormLabel mb="0" htmlFor="variable">
{t('blocks.inputs.settings.saveAnswer.label')}
</FormLabel>
<VariableSearchInput
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
onSelectVariable={updateVariableId}
/>
</Stack>
</Stack>

View File

@@ -4,6 +4,7 @@ import { parseDefaultGroupWithBlock } from '@typebot.io/playwright/databaseHelpe
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
import { getTestAsset } from '@/test/utils/playwright'
test.describe.parallel('Text input block', () => {
test('options should work', async ({ page }) => {
@@ -37,4 +38,48 @@ test.describe.parallel('Text input block', () => {
).toBeVisible()
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
})
test('hey boy', async ({ page }) => {
const typebotId = createId()
await createTypebots([
{
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
}),
},
])
await page.goto(`/typebots/${typebotId}/edit`)
await page.click(`text=${defaultTextInputOptions.labels.placeholder}`)
await page.getByText('Allow attachments').click()
await page.locator('[data-testid="variables-input"]').first().click()
await page.getByText('var1').click()
await page.getByRole('button', { name: 'Test' }).click()
await page
.getByPlaceholder('Type your answer...')
.fill('Help me with these')
await page.getByLabel('Add attachments').click()
await expect(page.getByRole('menuitem', { name: 'Document' })).toBeVisible()
await expect(
page.getByRole('menuitem', { name: 'Photos & videos' })
).toBeVisible()
await page
.locator('#document-upload')
.setInputFiles(getTestAsset('typebots/theme.json'))
await expect(page.getByText('theme.json')).toBeVisible()
await page
.locator('#photos-upload')
.setInputFiles([getTestAsset('avatar.jpg'), getTestAsset('avatar.jpg')])
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(2)
await page.getByRole('img', { name: 'avatar.jpg' }).first().hover()
await page.getByLabel('Remove attachment').first().click()
await expect(page.getByRole('img', { name: 'avatar.jpg' })).toHaveCount(1)
await page.getByLabel('Send').click()
await expect(
page.getByRole('img', { name: 'Attached image 1' })
).toBeVisible()
await expect(page.getByText('Help me with these')).toBeVisible()
})
})

View File

@@ -19,9 +19,9 @@ test.describe.parallel('Google sheets integration', () => {
await page.click('text=Add a value')
await page.click('text=Select a column')
await page.click('button >> text="Email"')
await page.getByRole('menuitem', { name: 'Email' }).click()
await page.click('[aria-label="Insert a variable"]')
await page.click('button >> text="Email" >> nth=1')
await page.getByRole('menuitem', { name: 'Email' }).last().click()
await page.click('text=Add a value')
await page.click('text=Select a column')
@@ -61,11 +61,11 @@ test.describe.parallel('Google sheets integration', () => {
await page.getByRole('button', { name: 'Row(s) to update' }).click()
await page.getByRole('button', { name: 'Add filter rule' }).click()
await page.click('text=Select a column')
await page.click('button >> text="Email"')
await page.getByRole('menuitem', { name: 'Email' }).click()
await page.getByRole('button', { name: 'Select an operator' }).click()
await page.getByRole('menuitem', { name: 'Equal to' }).click()
await page.click('[aria-label="Insert a variable"]')
await page.click('button >> text="Email" >> nth=1')
await page.getByRole('menuitem', { name: 'Email' }).last().click()
await page.getByRole('button', { name: 'Cells to update' }).click()
await page.click('text=Add a value')
@@ -106,11 +106,11 @@ test.describe.parallel('Google sheets integration', () => {
await page.getByRole('button', { name: 'Select row(s)' }).click()
await page.getByRole('button', { name: 'Add filter rule' }).click()
await page.click('text=Select a column')
await page.click('button >> text="Email"')
await page.getByRole('menuitem', { name: 'Email' }).click()
await page.getByRole('button', { name: 'Select an operator' }).click()
await page.getByRole('menuitem', { name: 'Equal to' }).click()
await page.click('[aria-label="Insert a variable"]')
await page.click('button >> text="Email" >> nth=1')
await page.getByRole('menuitem', { name: 'Email' }).last().click()
await page.getByRole('button', { name: 'Add filter rule' }).click()
await page.getByRole('button', { name: 'AND', exact: true }).click()

View File

@@ -20,7 +20,7 @@ test.describe('Condition block', () => {
'input[placeholder="Search for a variable"] >> nth=-1',
'Age'
)
await page.click('button:has-text("Age")')
await page.getByRole('menuitem', { name: 'Age' }).click()
await page.click('button:has-text("Select an operator")')
await page.click('button:has-text("Greater than")', { force: true })
await page.fill('input[placeholder="Type a number..."]', '80')
@@ -31,7 +31,7 @@ test.describe('Condition block', () => {
':nth-match(input[placeholder="Search for a variable"], 2)',
'Age'
)
await page.click('button:has-text("Age")')
await page.getByRole('menuitem', { name: 'Age' }).click()
await page.click('button:has-text("Select an operator")')
await page.click('button:has-text("Less than")', { force: true })
await page.fill(
@@ -44,7 +44,7 @@ test.describe('Condition block', () => {
'input[placeholder="Search for a variable"] >> nth=-1',
'Age'
)
await page.click('button:has-text("Age")')
await page.getByRole('menuitem', { name: 'Age' }).click()
await page.click('button:has-text("Select an operator")')
await page.click('button:has-text("Greater than")', { force: true })
await page.fill('input[placeholder="Type a number..."]', '20')

View File

@@ -23,10 +23,7 @@ test.describe('Set variable block', () => {
await page.click('text=Click to edit... >> nth = 0')
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
await page.getByRole('menuitem', { name: 'Create Total' }).click()
await page
.getByTestId('code-editor')
.getByRole('textbox')
.fill('1000 * {{Num}}')
await page.locator('textarea').fill('1000 * {{Num}}')
await page.click('text=Click to edit...', { force: true })
await expect(page.getByText('Save in results?')).toBeHidden()
@@ -39,10 +36,7 @@ test.describe('Set variable block', () => {
await expect(
page.getByRole('group').nth(1).locator('.chakra-switch')
).not.toHaveAttribute('data-checked')
await page
.getByTestId('code-editor')
.getByRole('textbox')
.fill('Custom value')
await page.locator('textarea').fill('Custom value')
await page.click('text=Click to edit...', { force: true })
await page.fill(
@@ -50,10 +44,7 @@ test.describe('Set variable block', () => {
'Addition'
)
await page.getByRole('menuitem', { name: 'Create Addition' }).click()
await page
.getByTestId('code-editor')
.getByRole('textbox')
.fill('1000 + {{Total}}')
await page.locator('textarea').fill('1000 + {{Total}}')
await page.click('text=Test')
await page
@@ -94,14 +85,14 @@ test.describe('Set variable block', () => {
await page.getByRole('button', { name: 'Test' }).click()
await page.getByRole('button', { name: 'There is a bug 🐛' }).click()
await page.getByTestId('textarea').fill('Hello!!')
await page.getByTestId('input').getByRole('button').click()
await page.getByLabel('Send').click()
await page
.locator('typebot-standard')
.getByRole('button', { name: 'Restart' })
.click()
await page.getByRole('button', { name: 'I have a question 💭' }).click()
await page.getByTestId('textarea').fill('How are you?')
await page.getByTestId('input').getByRole('button').click()
await page.getByLabel('Send').click()
await page.getByRole('button', { name: 'Transcription' }).click()
await expect(

View File

@@ -46,8 +46,8 @@ test('should be configurable', async ({ page }) => {
await page.getByLabel('Clear').click()
await page.click('text=Test')
await page.locator('typebot-standard').locator('input').fill('Hello there!')
await page.locator('typebot-standard').locator('input').press('Enter')
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
await page.getByPlaceholder('Type your answer...').press('Enter')
await expect(
page.locator('typebot-standard').locator('text=Hello there!')
).toBeVisible()

View File

@@ -9,6 +9,7 @@ import { useTranslate } from '@tolgee/react'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { WorkspaceDropdown } from '@/features/workspace/components/WorkspaceDropdown'
import { WorkspaceSettingsModal } from '@/features/workspace/components/WorkspaceSettingsModal'
import { ParentModalProvider } from '@/features/graph/providers/ParentModalProvider'
export const DashboardHeader = () => {
const { t } = useTranslate()
@@ -38,12 +39,14 @@ export const DashboardHeader = () => {
</Link>
<HStack>
{user && workspace && !workspace.isPastDue && (
<WorkspaceSettingsModal
isOpen={isOpen}
onClose={onClose}
user={user}
workspace={workspace}
/>
<ParentModalProvider>
<WorkspaceSettingsModal
isOpen={isOpen}
onClose={onClose}
user={user}
workspace={workspace}
/>
</ParentModalProvider>
)}
{!workspace?.isPastDue && (
<Button

View File

@@ -171,15 +171,10 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
const newBlock = { ...block }
const blockId = createId()
oldToNewIdsMapping.set(newBlock.id, blockId)
console.log(JSON.stringify(newBlock), blockHasOptions(newBlock))
if (blockHasOptions(newBlock) && newBlock.options) {
const variableIdsToReplace = extractVariableIdsFromObject(
newBlock.options
).filter((v) => oldToNewIdsMapping.has(v))
console.log(
JSON.stringify(newBlock.options),
variableIdsToReplace
)
if (variableIdsToReplace.length > 0) {
let optionsStr = JSON.stringify(newBlock.options)
variableIdsToReplace.forEach((variableId) => {

View File

@@ -43,7 +43,7 @@ export const parseReactBotProps = ({ typebot, apiHost }: BotProps) => {
}
export const typebotImportCode = isCloudProdInstance()
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'`
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.3/dist/web.js'`
: `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@${packageJson.version}/dist/web.js'`
export const parseInlineScript = (script: string) =>

View File

@@ -108,7 +108,6 @@ export const startWhatsAppPreview = authenticatedProcedure
setVariableHistory,
} = await startSession({
version: 2,
message: undefined,
startParams: {
isOnlyRegistering: !canSendDirectMessagesToUser,
type: 'preview',

View File

@@ -25,6 +25,7 @@ import { UserPreferencesForm } from '@/features/account/components/UserPreferenc
import { MyAccountForm } from '@/features/account/components/MyAccountForm'
import { BillingSettingsLayout } from '@/features/billing/components/BillingSettingsLayout'
import { useTranslate } from '@tolgee/react'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
type Props = {
isOpen: boolean
@@ -47,6 +48,7 @@ export const WorkspaceSettingsModal = ({
onClose,
}: Props) => {
const { t } = useTranslate()
const { ref } = useParentModal()
const { currentRole } = useWorkspace()
const [selectedTab, setSelectedTab] = useState<SettingsTab>('my-account')
@@ -55,7 +57,7 @@ export const WorkspaceSettingsModal = ({
return (
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
<ModalOverlay />
<ModalContent minH="600px" flexDir="row">
<ModalContent minH="600px" flexDir="row" ref={ref}>
<Stack
spacing={8}
w="180px"

View File

@@ -16,20 +16,36 @@ 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'
import { Redis } from '@upstash/redis/nodejs'
import ky from 'ky'
import { env } from '@typebot.io/env'
import * as Sentry from '@sentry/nextjs'
import { getIp } from '@typebot.io/lib/getIp'
import { trackEvents } from '@typebot.io/telemetry/trackEvents'
import Redis from 'ioredis'
const providers: Provider[] = []
let rateLimit: Ratelimit | undefined
let emailSignInRateLimiter: Ratelimit | undefined
if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) {
rateLimit = new Ratelimit({
redis: Redis.fromEnv(),
if (env.REDIS_URL) {
const redis = new Redis(env.REDIS_URL)
const rateLimitCompatibleRedis = {
sadd: <TData>(key: string, ...members: TData[]) =>
redis.sadd(key, ...members.map((m) => String(m))),
eval: async <TArgs extends unknown[], TData = unknown>(
script: string,
keys: string[],
args: TArgs
) =>
redis.eval(
script,
keys.length,
...keys,
...(args ?? []).map((a) => String(a))
) as Promise<TData>,
}
emailSignInRateLimiter = new Ratelimit({
redis: rateLimitCompatibleRedis,
limiter: Ratelimit.slidingWindow(1, '60 s'),
})
}
@@ -229,13 +245,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
let restricted: 'rate-limited' | undefined
if (
rateLimit &&
emailSignInRateLimiter &&
req.url?.startsWith('/api/auth/signin/email') &&
req.method === 'POST'
) {
const ip = getIp(req)
if (ip) {
const { success } = await rateLimit.limit(ip)
const { success } = await emailSignInRateLimiter.limit(ip)
if (!success) restricted = 'rate-limited'
}
}

View File

@@ -0,0 +1,62 @@
import prisma from '@typebot.io/lib/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
import {
methodNotAllowed,
notAuthenticated,
notFound,
} from '@typebot.io/lib/api'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { downloadMedia } from '@typebot.io/bot-engine/whatsapp/downloadMedia'
import { env } from '@typebot.io/env'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
if (!env.META_SYSTEM_USER_TOKEN)
return res
.status(400)
.json({ error: 'Meta system user token is not set' })
const user = await getAuthenticatedUser(req, res)
if (!user) return notAuthenticated(res)
const typebotId = req.query.typebotId as string
const typebot = await prisma.typebot.findFirst({
where: {
id: typebotId,
},
select: {
whatsAppCredentialsId: true,
workspace: {
select: {
members: {
select: {
userId: true,
},
},
},
},
},
})
if (!typebot?.workspace || isReadWorkspaceFobidden(typebot.workspace, user))
return notFound(res, 'Workspace not found')
if (!typebot) return notFound(res, 'Typebot not found')
const mediaId = req.query.mediaId as string
const { file, mimeType } = await downloadMedia({
mediaId,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
})
res.setHeader('Content-Type', mimeType)
res.setHeader('Cache-Control', 'public, max-age=86400')
return res.send(file)
}
return methodNotAllowed(res)
}
export default handler