diff --git a/.github/workflows/deploy-partykit.yml b/.github/workflows/deploy-partykit.yml new file mode 100644 index 0000000000..c1e094844d --- /dev/null +++ b/.github/workflows/deploy-partykit.yml @@ -0,0 +1,44 @@ +name: Deploy Partykit server + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + # https://github.com/vercel/turborepo/issues/6348 + - name: Run Turbo ignore + id: check_changes + run: | + echo "Running script..." + + # Enable error handling + set +e + + bunx turbo-ignore @typebot.io/partykit -task=build exit_code=$? + + # Disable error handling + set -e + + echo "Exit code: $exit_code" + + if I $exit_code -eq 0 ]; then + echo "hasClientApiHostChanged=0" >> $GITHUB_OUTPUT + # echo "::save-state name=project_changed: :0" + # # echo "::set-output name=project_changed:: 0" + else + echo "hasClientApiHostChanged=1" ยป $GITHUB_OUTPUT + # echo "::set-output name=project_changed: : 1" + fi + - run: bun install + - run: bunx partykit deploy --domain ${PARTYKIT_DOMAIN} + working-directory: ./packages/partykit + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + PARTYKIT_DOMAIN: ${{ secrets.PARTYKIT_DOMAIN }} diff --git a/.gitignore b/.gitignore index f2fc961a71..97b388f012 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ snapshots .tsup .env.docker + +.partykit diff --git a/apps/builder/package.json b/apps/builder/package.json index 1739867abe..c94cb8cc1b 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -9,6 +9,7 @@ "test:ui": "dotenv -e ./.env -e ../../.env -- playwright test --ui" }, "dependencies": { + "partysocket": "1.0.2", "@braintree/sanitize-url": "7.0.1", "@chakra-ui/anatomy": "2.2.2", "@chakra-ui/react": "2.8.2", diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts b/apps/builder/src/features/blocks/integrations/httpRequest/api/getResultExample.ts similarity index 91% rename from apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts rename to apps/builder/src/features/blocks/integrations/httpRequest/api/getResultExample.ts index cc6fe1dd05..ddbd3f6b3d 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts +++ b/apps/builder/src/features/blocks/integrations/httpRequest/api/getResultExample.ts @@ -2,7 +2,7 @@ import { fetchLinkedTypebots } from "@/features/blocks/logic/typebotLink/helpers import { canReadTypebots } from "@/helpers/databaseRules"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; -import { parseSampleResult } from "@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult"; +import { parseSampleResult } from "@typebot.io/bot-engine/blocks/integrations/httpRequest/parseSampleResult"; import { getBlockById } from "@typebot.io/groups/helpers"; import prisma from "@typebot.io/prisma"; import type { Typebot } from "@typebot.io/typebot/schemas/typebot"; @@ -16,8 +16,8 @@ export const getResultExample = authenticatedProcedure protect: true, summary: "Get result example", description: - 'Returns "fake" result for webhook block to help you anticipate how the webhook will behave.', - tags: ["Webhook"], + 'Returns "fake" result for http request block to help you anticipate how the webhook will behave.', + tags: ["HTTP request"], }, }) .input( diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts b/apps/builder/src/features/blocks/integrations/httpRequest/api/listHttpRequestBlocks.ts similarity index 78% rename from apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts rename to apps/builder/src/features/blocks/integrations/httpRequest/api/listHttpRequestBlocks.ts index 473cd6f3a1..f360e0b397 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/listWebhookBlocks.ts +++ b/apps/builder/src/features/blocks/integrations/httpRequest/api/listHttpRequestBlocks.ts @@ -1,7 +1,7 @@ import { canReadTypebots } from "@/helpers/databaseRules"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; import type { Block } from "@typebot.io/blocks-core/schemas/schema"; import { IntegrationBlockType } from "@typebot.io/blocks-integrations/constants"; import { parseGroups } from "@typebot.io/groups/schemas"; @@ -9,16 +9,16 @@ import { byId } from "@typebot.io/lib/utils"; import prisma from "@typebot.io/prisma"; import { z } from "@typebot.io/zod"; -export const listWebhookBlocks = authenticatedProcedure +export const listHttpRequestBlocks = authenticatedProcedure .meta({ openapi: { method: "GET", path: "/v1/typebots/{typebotId}/webhookBlocks", protect: true, - summary: "List webhook blocks", + summary: "List HTTP request blocks", description: - "Returns a list of all the webhook blocks that you can subscribe to.", - tags: ["Webhook"], + "Returns a list of all the HTTP request blocks that you can subscribe to.", + tags: ["HTTP request"], }, }) .input( @@ -32,7 +32,7 @@ export const listWebhookBlocks = authenticatedProcedure z.object({ id: z.string(), type: z.enum([ - IntegrationBlockType.WEBHOOK, + IntegrationBlockType.HTTP_REQUEST, IntegrationBlockType.ZAPIER, IntegrationBlockType.MAKE_COM, IntegrationBlockType.PABBLY_CONNECT, @@ -59,21 +59,21 @@ export const listWebhookBlocks = authenticatedProcedure typebotVersion: typebot.version, }); - const webhookBlocks = groups.reduce< + const httpRequestBlocks = groups.reduce< { id: string; label: string; url: string | undefined; type: - | IntegrationBlockType.WEBHOOK + | IntegrationBlockType.HTTP_REQUEST | IntegrationBlockType.ZAPIER | IntegrationBlockType.MAKE_COM | IntegrationBlockType.PABBLY_CONNECT; }[] - >((webhookBlocks, group) => { - const blocks = (group.blocks as Block[]).filter(isWebhookBlock); + >((httpRequestBlock, group) => { + const blocks = (group.blocks as Block[]).filter(isHttpRequestBlock); return [ - ...webhookBlocks, + ...httpRequestBlock, ...blocks.map((block) => ({ id: block.id, type: block.type, @@ -87,5 +87,5 @@ export const listWebhookBlocks = authenticatedProcedure ]; }, []); - return { webhookBlocks }; + return { webhookBlocks: httpRequestBlocks }; }); diff --git a/apps/builder/src/features/blocks/integrations/httpRequest/api/router.ts b/apps/builder/src/features/blocks/integrations/httpRequest/api/router.ts new file mode 100644 index 0000000000..62dbfedfcc --- /dev/null +++ b/apps/builder/src/features/blocks/integrations/httpRequest/api/router.ts @@ -0,0 +1,12 @@ +import { router } from "@/helpers/server/trpc"; +import { getResultExample } from "./getResultExample"; +import { listHttpRequestBlocks } from "./listHttpRequestBlocks"; +import { subscribeHttpRequest } from "./subscribeHttpRequest"; +import { unsubscribeHttpRequest } from "./unsubscribeHttpRequest"; + +export const httpRequestRouter = router({ + listHttpRequestBlocks, + getResultExample, + subscribeHttpRequest, + unsubscribeHttpRequest, +}); diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts b/apps/builder/src/features/blocks/integrations/httpRequest/api/subscribeHttpRequest.ts similarity index 73% rename from apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts rename to apps/builder/src/features/blocks/integrations/httpRequest/api/subscribeHttpRequest.ts index c746bdfd29..c7c6c6d7a2 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/subscribeWebhook.ts +++ b/apps/builder/src/features/blocks/integrations/httpRequest/api/subscribeHttpRequest.ts @@ -1,22 +1,22 @@ import { canWriteTypebots } from "@/helpers/databaseRules"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; import type { Block } from "@typebot.io/blocks-core/schemas/schema"; -import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/webhook/schema"; +import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/httpRequest/schema"; import { parseGroups } from "@typebot.io/groups/schemas"; import { byId } from "@typebot.io/lib/utils"; import prisma from "@typebot.io/prisma"; import { z } from "@typebot.io/zod"; -export const subscribeWebhook = authenticatedProcedure +export const subscribeHttpRequest = authenticatedProcedure .meta({ openapi: { method: "POST", path: "/v1/typebots/{typebotId}/webhookBlocks/{blockId}/subscribe", protect: true, - summary: "Subscribe to webhook block", - tags: ["Webhook"], + summary: "Subscribe to HTTP request block", + tags: ["HTTP request"], }, }) .input( @@ -48,30 +48,30 @@ export const subscribeWebhook = authenticatedProcedure typebotVersion: typebot.version, }); - const webhookBlock = groups + const httpRequestBlock = groups .flatMap((g) => g.blocks) .find(byId(blockId)) as HttpRequestBlock | null; - if (!webhookBlock || !isWebhookBlock(webhookBlock)) + if (!httpRequestBlock || !isHttpRequestBlock(httpRequestBlock)) throw new TRPCError({ code: "NOT_FOUND", - message: "Webhook block not found", + message: "HttpRequest block not found", }); - if (webhookBlock.options?.webhook || typebot.version === "6") { + if (httpRequestBlock.options?.webhook || typebot.version === "6") { const updatedGroups = groups.map((group) => - group.blocks.some((b) => b.id === webhookBlock.id) + group.blocks.some((b) => b.id === httpRequestBlock.id) ? { ...group, blocks: group.blocks.map((block) => - block.id !== webhookBlock.id + block.id !== httpRequestBlock.id ? block : { ...block, options: { - ...webhookBlock.options, + ...httpRequestBlock.options, webhook: { - ...webhookBlock.options?.webhook, + ...httpRequestBlock.options?.webhook, url, }, }, @@ -87,15 +87,15 @@ export const subscribeWebhook = authenticatedProcedure }, }); } else { - if ("webhookId" in webhookBlock) + if ("webhookId" in httpRequestBlock) await prisma.webhook.update({ - where: { id: webhookBlock.webhookId }, + where: { id: httpRequestBlock.webhookId }, data: { url }, }); else throw new TRPCError({ code: "NOT_FOUND", - message: "Webhook block not found", + message: "HttpRequest block not found", }); } diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts b/apps/builder/src/features/blocks/integrations/httpRequest/api/unsubscribeHttpRequest.ts similarity index 73% rename from apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts rename to apps/builder/src/features/blocks/integrations/httpRequest/api/unsubscribeHttpRequest.ts index bb70debc83..dfc54a8057 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/unsubscribeWebhook.ts +++ b/apps/builder/src/features/blocks/integrations/httpRequest/api/unsubscribeHttpRequest.ts @@ -1,22 +1,22 @@ import { canWriteTypebots } from "@/helpers/databaseRules"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; import type { Block } from "@typebot.io/blocks-core/schemas/schema"; -import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/webhook/schema"; +import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/httpRequest/schema"; import { parseGroups } from "@typebot.io/groups/schemas"; import { byId } from "@typebot.io/lib/utils"; import prisma from "@typebot.io/prisma"; import { z } from "@typebot.io/zod"; -export const unsubscribeWebhook = authenticatedProcedure +export const unsubscribeHttpRequest = authenticatedProcedure .meta({ openapi: { method: "POST", path: "/v1/typebots/{typebotId}/webhookBlocks/{blockId}/unsubscribe", protect: true, - summary: "Unsubscribe from webhook block", - tags: ["Webhook"], + summary: "Unsubscribe from HTTP request block", + tags: ["HTTP request"], }, }) .input( @@ -47,30 +47,30 @@ export const unsubscribeWebhook = authenticatedProcedure typebotVersion: typebot.version, }); - const webhookBlock = groups + const httpRequestBlock = groups .flatMap((g) => g.blocks) .find(byId(blockId)) as HttpRequestBlock | null; - if (!webhookBlock || !isWebhookBlock(webhookBlock)) + if (!httpRequestBlock || !isHttpRequestBlock(httpRequestBlock)) throw new TRPCError({ code: "NOT_FOUND", - message: "Webhook block not found", + message: "HTTP request block not found", }); - if (webhookBlock.options?.webhook || typebot.version === "6") { + if (httpRequestBlock.options?.webhook || typebot.version === "6") { const updatedGroups = groups.map((group) => - group.blocks.some((b) => b.id === webhookBlock.id) + group.blocks.some((b) => b.id === httpRequestBlock.id) ? { ...group, blocks: group.blocks.map((block) => - block.id !== webhookBlock.id + block.id !== httpRequestBlock.id ? block : { ...block, options: { - ...webhookBlock.options, + ...httpRequestBlock.options, webhook: { - ...webhookBlock.options?.webhook, + ...httpRequestBlock.options?.webhook, url: undefined, }, }, @@ -86,15 +86,15 @@ export const unsubscribeWebhook = authenticatedProcedure }, }); } else { - if ("webhookId" in webhookBlock) + if ("webhookId" in httpRequestBlock) await prisma.webhook.update({ - where: { id: webhookBlock.webhookId }, + where: { id: httpRequestBlock.webhookId }, data: { url: null }, }); else throw new TRPCError({ code: "NOT_FOUND", - message: "Webhook block not found", + message: "HTTP request block not found", }); } diff --git a/apps/builder/src/features/blocks/integrations/webhook/components/HttpRequestAdvancedConfigForm.tsx b/apps/builder/src/features/blocks/integrations/httpRequest/components/HttpRequestAdvancedConfigForm.tsx similarity index 85% rename from apps/builder/src/features/blocks/integrations/webhook/components/HttpRequestAdvancedConfigForm.tsx rename to apps/builder/src/features/blocks/integrations/httpRequest/components/HttpRequestAdvancedConfigForm.tsx index 18cc9c7929..943835d74f 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/components/HttpRequestAdvancedConfigForm.tsx +++ b/apps/builder/src/features/blocks/integrations/httpRequest/components/HttpRequestAdvancedConfigForm.tsx @@ -19,39 +19,39 @@ import { } from "@chakra-ui/react"; import { HttpMethod, + defaultHttpRequestAttributes, + defaultHttpRequestBlockOptions, defaultTimeout, - defaultWebhookAttributes, - defaultWebhookBlockOptions, maxTimeout, -} from "@typebot.io/blocks-integrations/webhook/constants"; +} from "@typebot.io/blocks-integrations/httpRequest/constants"; import type { HttpRequest, HttpRequestBlock, KeyValue, ResponseVariableMapping, VariableForTest, -} from "@typebot.io/blocks-integrations/webhook/schema"; +} from "@typebot.io/blocks-integrations/httpRequest/schema"; import { useMemo, useState } from "react"; import { convertVariablesForTestToVariables } from "../helpers/convertVariablesForTestToVariables"; import { getDeepKeys } from "../helpers/getDeepKeys"; -import { executeWebhook } from "../queries/executeWebhookQuery"; +import { executeHttpRequest } from "../queries/executeHttpRequestQuery"; import { HeadersInputs, QueryParamsInputs } from "./KeyValueInputs"; import { DataVariableInputs } from "./ResponseMappingInputs"; import { VariableForTestInputs } from "./VariableForTestInputs"; type Props = { blockId: string; - webhook: HttpRequest | undefined; + httpRequest: HttpRequest | undefined; options: HttpRequestBlock["options"]; - onWebhookChange: (webhook: HttpRequest) => void; + onHttpRequestChange: (httpRequest: HttpRequest) => void; onOptionsChange: (options: HttpRequestBlock["options"]) => void; }; export const HttpRequestAdvancedConfigForm = ({ blockId, - webhook, + httpRequest, options, - onWebhookChange, + onHttpRequestChange, onOptionsChange, }: Props) => { const { typebot, save } = useTypebot(); @@ -61,15 +61,16 @@ export const HttpRequestAdvancedConfigForm = ({ const { showToast } = useToast(); const updateMethod = (method: HttpMethod) => - onWebhookChange({ ...webhook, method }); + onHttpRequestChange({ ...httpRequest, method }); const updateQueryParams = (queryParams: KeyValue[]) => - onWebhookChange({ ...webhook, queryParams }); + onHttpRequestChange({ ...httpRequest, queryParams }); const updateHeaders = (headers: KeyValue[]) => - onWebhookChange({ ...webhook, headers }); + onHttpRequestChange({ ...httpRequest, headers }); - const updateBody = (body: string) => onWebhookChange({ ...webhook, body }); + const updateBody = (body: string) => + onHttpRequestChange({ ...httpRequest, body }); const updateVariablesForTest = (variablesForTest: VariableForTest[]) => onOptionsChange({ ...options, variablesForTest }); @@ -92,7 +93,7 @@ export const HttpRequestAdvancedConfigForm = ({ setIsTestResponseLoading(true); if (!options?.webhook) await save(); else await save(); - const { data, error } = await executeWebhook( + const { data, error } = await executeHttpRequest( typebot.id, convertVariablesForTestToVariables( options?.variablesForTest ?? [], @@ -119,7 +120,7 @@ export const HttpRequestAdvancedConfigForm = ({ ); const isCustomBody = - options?.isCustomBody ?? defaultWebhookBlockOptions.isCustomBody; + options?.isCustomBody ?? defaultHttpRequestBlockOptions.isCustomBody; return ( <> @@ -127,16 +128,16 @@ export const HttpRequestAdvancedConfigForm = ({ label="Advanced configuration" initialValue={ options?.isAdvancedConfig ?? - defaultWebhookBlockOptions.isAdvancedConfig + defaultHttpRequestBlockOptions.isAdvancedConfig } onCheckChange={updateAdvancedConfig} > @@ -144,7 +145,8 @@ export const HttpRequestAdvancedConfigForm = ({ Method: - initialItems={webhook?.queryParams} + initialItems={httpRequest?.queryParams} onItemsChange={updateQueryParams} addLabel="Add a param" > @@ -173,7 +175,7 @@ export const HttpRequestAdvancedConfigForm = ({ - initialItems={webhook?.headers} + initialItems={httpRequest?.headers} onItemsChange={updateHeaders} addLabel="Add a value" > @@ -194,7 +196,7 @@ export const HttpRequestAdvancedConfigForm = ({ /> {isCustomBody && ( - {webhook?.url && ( + {httpRequest?.url && ( + {websocketStatus === "opened" && ( + + + Waiting for an{" "} + + authenticated + {" "} + POST request to the Test URL... + + + )} + {receivedData && ( + + )} + {(receivedData || + (options?.responseVariableMapping && + options.responseVariableMapping.length > 0)) && ( + + + + Save in variables + + + + + initialItems={options?.responseVariableMapping} + onItemsChange={updateResponseVariableMapping} + addLabel="Add an entry" + > + {(props) => } + + + + + )} + + + + {typebot && ( + + + + + + + + + You can easily get the Result ID{" "} + + with a Set variable block + + . + + + )} + + + + ); +}; diff --git a/apps/builder/src/features/blocks/logic/webhook/webhook.spec.ts b/apps/builder/src/features/blocks/logic/webhook/webhook.spec.ts new file mode 100644 index 0000000000..d1a7a08600 --- /dev/null +++ b/apps/builder/src/features/blocks/logic/webhook/webhook.spec.ts @@ -0,0 +1,61 @@ +import { getTestAsset } from "@/test/utils/playwright"; +import { createId } from "@paralleldrive/cuid2"; +import test, { expect } from "@playwright/test"; +import { env } from "@typebot.io/env"; +import { importTypebotInDatabase } from "@typebot.io/playwright/databaseActions"; +import { apiToken } from "@typebot.io/playwright/databaseSetup"; + +const typebotId = createId(); + +test.describe("Wait block", () => { + test("wait should trigger", async ({ page, request }) => { + await importTypebotInDatabase(getTestAsset("typebots/logic/webhook.json"), { + id: typebotId, + }); + + await page.goto(`/typebots/${typebotId}/edit`); + await page.getByText("Listen for webhook").click(); + await page.getByRole("button", { name: "Listen for test event" }).click(); + await page.waitForTimeout(1000); + await request.post( + `${env.NEXT_PUBLIC_VIEWER_URL[0]}/api/v1/typebots/${typebotId}/blocks/webhook-block-id/web/executeTestWebhook`, + { + data: { + name: "John", + }, + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }, + ); + await expect(page.getByText('{ "data": { "name": "John')).toBeVisible(); + await page.getByRole("button", { name: "Save in variables" }).click(); + await page.getByRole("button", { name: "Add an entry" }).click(); + await page.getByPlaceholder("Select the data").click(); + await page.getByRole("menuitem", { name: "data.name" }).click(); + await page.getByTestId("variables-input").click(); + await page + .getByRole("menuitem", { name: "Name Rename variable Remove" }) + .click(); + await page.getByRole("button", { name: "Test", exact: true }).click(); + await expect( + page.locator("span").filter({ hasText: "Ok let's look into that..." }), + ).toBeVisible(); + await page.waitForTimeout(1000); + await request.post( + `${env.NEXT_PUBLIC_VIEWER_URL[0]}/api/v1/typebots/${typebotId}/blocks/webhook-block-id/web/executeTestWebhook`, + { + data: { + name: "John", + }, + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }, + ); + await expect( + page.getByText(", nice to see you again!", { exact: true }), + ).toBeVisible(); + await expect(page.getByText("John")).toBeVisible(); + }); +}); diff --git a/apps/builder/src/features/collaboration/api/getCollaborators.ts b/apps/builder/src/features/collaboration/api/getCollaborators.ts index cae07e8028..429f806e21 100644 --- a/apps/builder/src/features/collaboration/api/getCollaborators.ts +++ b/apps/builder/src/features/collaboration/api/getCollaborators.ts @@ -1,8 +1,8 @@ -import { isReadTypebotForbidden } from "@/features/typebot/helpers/isReadTypebotForbidden"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; import prisma from "@typebot.io/prisma"; import { collaboratorSchema } from "@typebot.io/schemas/features/collaborators"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import { z } from "@typebot.io/zod"; export const getCollaborators = authenticatedProcedure diff --git a/apps/builder/src/features/editor/components/BlockIcon.tsx b/apps/builder/src/features/editor/components/BlockIcon.tsx index 15b104b53d..dd0e96ceca 100644 --- a/apps/builder/src/features/editor/components/BlockIcon.tsx +++ b/apps/builder/src/features/editor/components/BlockIcon.tsx @@ -32,6 +32,7 @@ import { ScriptIcon } from "@/features/blocks/logic/script/components/ScriptIcon import { SetVariableIcon } from "@/features/blocks/logic/setVariable/components/SetVariableIcon"; import { TypebotLinkIcon } from "@/features/blocks/logic/typebotLink/components/TypebotLinkIcon"; import { WaitIcon } from "@/features/blocks/logic/wait/components/WaitIcon"; +import { WebhookIcon } from "@/features/blocks/logic/webhook/components/WebhookIcon"; import { ForgedBlockIcon } from "@/features/forge/ForgedBlockIcon"; import { type IconProps, useColorModeValue } from "@chakra-ui/react"; import { BubbleBlockType } from "@typebot.io/blocks-bubbles/constants"; @@ -98,11 +99,13 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => { return ; case LogicBlockType.AB_TEST: return ; + case LogicBlockType.WEBHOOK: + return ; case IntegrationBlockType.GOOGLE_SHEETS: return ; case IntegrationBlockType.GOOGLE_ANALYTICS: return ; - case IntegrationBlockType.WEBHOOK: + case IntegrationBlockType.HTTP_REQUEST: return ; case IntegrationBlockType.ZAPIER: return ; diff --git a/apps/builder/src/features/editor/components/BlockLabel.tsx b/apps/builder/src/features/editor/components/BlockLabel.tsx index 326684e352..651c108d1b 100644 --- a/apps/builder/src/features/editor/components/BlockLabel.tsx +++ b/apps/builder/src/features/editor/components/BlockLabel.tsx @@ -42,7 +42,7 @@ export const BlockLabel = ({ type, ...props }: Props): JSX.Element => { export const getBubbleBlockLabel = ( t: TFnType, -): Record => ({ +): { [key in BubbleBlockType]: string } => ({ [BubbleBlockType.TEXT]: t("editor.sidebarBlock.text.label"), [BubbleBlockType.IMAGE]: t("editor.sidebarBlock.image.label"), [BubbleBlockType.VIDEO]: t("editor.sidebarBlock.video.label"), @@ -52,7 +52,7 @@ export const getBubbleBlockLabel = ( export const getInputBlockLabel = ( t: TFnType, -): Record => ({ +): { [key in InputBlockType]: string } => ({ [InputBlockType.NUMBER]: t("editor.sidebarBlock.number.label"), [InputBlockType.EMAIL]: t("editor.sidebarBlock.email.label"), [InputBlockType.TEXT]: t("editor.sidebarBlock.text.label"), @@ -68,7 +68,7 @@ export const getInputBlockLabel = ( export const getLogicBlockLabel = ( t: TFnType, -): Record => ({ +): { [key in LogicBlockType]: string } => ({ [LogicBlockType.SET_VARIABLE]: t("editor.sidebarBlock.setVariable.label"), [LogicBlockType.CONDITION]: t("editor.sidebarBlock.condition.label"), [LogicBlockType.REDIRECT]: t("editor.sidebarBlock.redirect.label"), @@ -77,16 +77,17 @@ export const getLogicBlockLabel = ( [LogicBlockType.WAIT]: t("editor.sidebarBlock.wait.label"), [LogicBlockType.JUMP]: t("editor.sidebarBlock.jump.label"), [LogicBlockType.AB_TEST]: t("editor.sidebarBlock.abTest.label"), + [LogicBlockType.WEBHOOK]: "Webhook", }); export const getIntegrationBlockLabel = ( t: TFnType, -): Record => ({ +): { [key in IntegrationBlockType]: string } => ({ [IntegrationBlockType.GOOGLE_SHEETS]: t("editor.sidebarBlock.sheets.label"), [IntegrationBlockType.GOOGLE_ANALYTICS]: t( "editor.sidebarBlock.analytics.label", ), - [IntegrationBlockType.WEBHOOK]: "HTTP request", + [IntegrationBlockType.HTTP_REQUEST]: "HTTP request", [IntegrationBlockType.ZAPIER]: t("editor.sidebarBlock.zapier.label"), [IntegrationBlockType.MAKE_COM]: t("editor.sidebarBlock.makecom.label"), [IntegrationBlockType.PABBLY_CONNECT]: t("editor.sidebarBlock.pabbly.label"), diff --git a/apps/builder/src/features/editor/components/BlocksSideBar.tsx b/apps/builder/src/features/editor/components/BlocksSideBar.tsx index d9d0a53655..44451531f2 100644 --- a/apps/builder/src/features/editor/components/BlocksSideBar.tsx +++ b/apps/builder/src/features/editor/components/BlocksSideBar.tsx @@ -20,7 +20,9 @@ import type { BlockV6 } from "@typebot.io/blocks-core/schemas/schema"; import { InputBlockType } from "@typebot.io/blocks-inputs/constants"; import { IntegrationBlockType } from "@typebot.io/blocks-integrations/constants"; import { LogicBlockType } from "@typebot.io/blocks-logic/constants"; +import { env } from "@typebot.io/env"; import { forgedBlocks } from "@typebot.io/forge-repository/definitions"; +import { isDefined } from "@typebot.io/lib/utils"; import type React from "react"; import { useState } from "react"; import { useDebouncedCallback } from "use-debounce"; @@ -129,11 +131,17 @@ export const BlocksSideBar = () => { .includes(searchInput.toLowerCase()), ); - const filteredLogicBlockTypes = Object.values(LogicBlockType).filter((type) => - getLogicBlockLabel(t) - [type].toLowerCase() - .includes(searchInput.toLowerCase()), - ); + const filteredLogicBlockTypes = Object.values(LogicBlockType) + .filter((type) => + type === LogicBlockType.WEBHOOK + ? isDefined(env.NEXT_PUBLIC_PARTYKIT_HOST) + : true, + ) + .filter((type) => + getLogicBlockLabel(t) + [type].toLowerCase() + .includes(searchInput.toLowerCase()), + ); const filteredIntegrationBlockTypes = Object.values( IntegrationBlockType, diff --git a/apps/builder/src/features/editor/providers/typebotActions/blocks.ts b/apps/builder/src/features/editor/providers/typebotActions/blocks.ts index 6080a5c595..e1017f3bd4 100644 --- a/apps/builder/src/features/editor/providers/typebotActions/blocks.ts +++ b/apps/builder/src/features/editor/providers/typebotActions/blocks.ts @@ -6,7 +6,7 @@ import type { BlockIndices, BlockV6, } from "@typebot.io/blocks-core/schemas/schema"; -import type { HttpRequest } from "@typebot.io/blocks-integrations/webhook/schema"; +import type { HttpRequest } from "@typebot.io/blocks-integrations/httpRequest/schema"; import { byId } from "@typebot.io/lib/utils"; import type { Typebot, TypebotV6 } from "@typebot.io/typebot/schemas/typebot"; import { type Draft, produce } from "immer"; diff --git a/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx b/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx index 75443de59c..3e839acf01 100644 --- a/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx +++ b/apps/builder/src/features/graph/components/nodes/block/BlockNodeContent.tsx @@ -17,12 +17,12 @@ import { UrlNodeContent } from "@/features/blocks/inputs/url/components/UrlNodeC import { ChatwootNodeBody } from "@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody"; import { GoogleAnalyticsNodeBody } from "@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeBody"; import { GoogleSheetsNodeContent } from "@/features/blocks/integrations/googleSheets/components/GoogleSheetsNodeContent"; +import { HttpRequestNodeContent } from "@/features/blocks/integrations/httpRequest/components/HttpRequestNodeContent"; import { MakeComContent } from "@/features/blocks/integrations/makeCom/components/MakeComContent"; import { OpenAINodeBody } from "@/features/blocks/integrations/openai/components/OpenAINodeBody"; import { PabblyConnectContent } from "@/features/blocks/integrations/pabbly/components/PabblyConnectContent"; import { PixelNodeBody } from "@/features/blocks/integrations/pixel/components/PixelNodeBody"; import { SendEmailContent } from "@/features/blocks/integrations/sendEmail/components/SendEmailContent"; -import { WebhookContent } from "@/features/blocks/integrations/webhook/components/HttpRequestContent"; import { ZapierContent } from "@/features/blocks/integrations/zapier/components/ZapierContent"; import { AbTestNodeBody } from "@/features/blocks/logic/abTest/components/AbTestNodeBody"; import { JumpNodeBody } from "@/features/blocks/logic/jump/components/JumpNodeBody"; @@ -31,6 +31,7 @@ import { ScriptNodeContent } from "@/features/blocks/logic/script/components/Scr import { SetVariableContent } from "@/features/blocks/logic/setVariable/components/SetVariableContent"; import { TypebotLinkNode } from "@/features/blocks/logic/typebotLink/components/TypebotLinkNode"; import { WaitNodeContent } from "@/features/blocks/logic/wait/components/WaitNodeContent"; +import { WebhookNodeContent } from "@/features/blocks/logic/webhook/components/WebhookNodeContent"; import { ForgedBlockNodeContent } from "@/features/forge/components/ForgedBlockNodeContent"; import { BubbleBlockType } from "@typebot.io/blocks-bubbles/constants"; import type { @@ -123,14 +124,16 @@ export const BlockNodeContent = ({ return ; case LogicBlockType.CONDITION: return ; + case LogicBlockType.WEBHOOK: + return ; case IntegrationBlockType.GOOGLE_SHEETS: { return ; } case IntegrationBlockType.GOOGLE_ANALYTICS: { return ; } - case IntegrationBlockType.WEBHOOK: { - return ; + case IntegrationBlockType.HTTP_REQUEST: { + return ; } case IntegrationBlockType.ZAPIER: { return ; diff --git a/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx b/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx index a6e03a28ce..d86afc429c 100644 --- a/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx +++ b/apps/builder/src/features/graph/components/nodes/block/SettingsPopoverContent.tsx @@ -12,12 +12,12 @@ import { UrlInputSettings } from "@/features/blocks/inputs/url/components/UrlInp import { ChatwootSettings } from "@/features/blocks/integrations/chatwoot/components/ChatwootSettings"; import { GoogleAnalyticsSettings } from "@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsSettings"; import { GoogleSheetsSettings } from "@/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings"; +import { HttpRequestSettings } from "@/features/blocks/integrations/httpRequest/components/HttpRequestSettings"; import { MakeComSettings } from "@/features/blocks/integrations/makeCom/components/MakeComSettings"; import { OpenAISettings } from "@/features/blocks/integrations/openai/components/OpenAISettings"; import { PabblyConnectSettings } from "@/features/blocks/integrations/pabbly/components/PabblyConnectSettings"; import { PixelSettings } from "@/features/blocks/integrations/pixel/components/PixelSettings"; import { SendEmailSettings } from "@/features/blocks/integrations/sendEmail/components/SendEmailSettings"; -import { HttpRequestSettings } from "@/features/blocks/integrations/webhook/components/HttpRequestSettings"; import { ZapierSettings } from "@/features/blocks/integrations/zapier/components/ZapierSettings"; import { AbTestSettings } from "@/features/blocks/logic/abTest/components/AbTestSettings"; import { JumpSettings } from "@/features/blocks/logic/jump/components/JumpSettings"; @@ -26,6 +26,7 @@ import { ScriptSettings } from "@/features/blocks/logic/script/components/Script import { SetVariableSettings } from "@/features/blocks/logic/setVariable/components/SetVariableSettings"; import { TypebotLinkForm } from "@/features/blocks/logic/typebotLink/components/TypebotLinkForm"; import { WaitSettings } from "@/features/blocks/logic/wait/components/WaitSettings"; +import { WebhookSettings } from "@/features/blocks/logic/webhook/components/WebhookSettings"; import { useForgedBlock } from "@/features/forge/hooks/useForgedBlock"; import { VideoOnboardingPopover } from "@/features/onboarding/components/VideoOnboardingPopover"; import { hasOnboardingVideo } from "@/features/onboarding/helpers/hasOnboardingVideo"; @@ -309,7 +310,7 @@ export const BlockSettings = ({ ); } - case IntegrationBlockType.WEBHOOK: { + case IntegrationBlockType.HTTP_REQUEST: { return ( ); @@ -343,6 +344,14 @@ export const BlockSettings = ({ } case LogicBlockType.CONDITION: return null; + case LogicBlockType.WEBHOOK: + return ( + + ); default: { return ( diff --git a/apps/builder/src/features/graph/helpers/getHelpDocUrl.ts b/apps/builder/src/features/graph/helpers/getHelpDocUrl.ts index d3b8af7049..6fe3d994b0 100644 --- a/apps/builder/src/features/graph/helpers/getHelpDocUrl.ts +++ b/apps/builder/src/features/graph/helpers/getHelpDocUrl.ts @@ -51,8 +51,8 @@ export const getHelpDocUrl = ( return "https://docs.typebot.io/editor/blocks/integrations/zapier"; case IntegrationBlockType.PABBLY_CONNECT: return "https://docs.typebot.io/editor/blocks/integrations/pabbly"; - case IntegrationBlockType.WEBHOOK: - return "https://docs.typebot.io/editor/blocks/integrations/webhook"; + case IntegrationBlockType.HTTP_REQUEST: + return "https://docs.typebot.io/editor/blocks/integrations/http-request"; case InputBlockType.PICTURE_CHOICE: return "https://docs.typebot.io/editor/blocks/inputs/picture-choice"; case IntegrationBlockType.OPEN_AI: @@ -67,6 +67,8 @@ export const getHelpDocUrl = ( return "https://docs.typebot.io/editor/blocks/integrations/pixel"; case LogicBlockType.CONDITION: return "https://docs.typebot.io/editor/blocks/logic/condition"; + case LogicBlockType.WEBHOOK: + return "https://docs.typebot.io/editor/blocks/logic/webhook"; default: return blockDef?.docsUrl; } diff --git a/apps/builder/src/features/results/api/getResult.ts b/apps/builder/src/features/results/api/getResult.ts index 2d087f5f2f..f3a9817436 100644 --- a/apps/builder/src/features/results/api/getResult.ts +++ b/apps/builder/src/features/results/api/getResult.ts @@ -1,8 +1,8 @@ -import { isReadTypebotForbidden } from "@/features/typebot/helpers/isReadTypebotForbidden"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; import prisma from "@typebot.io/prisma"; import { resultWithAnswersSchema } from "@typebot.io/results/schemas/results"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import { z } from "@typebot.io/zod"; export const getResult = authenticatedProcedure diff --git a/apps/builder/src/features/results/api/getResultLogs.ts b/apps/builder/src/features/results/api/getResultLogs.ts index e1b77c2c12..ca13a25fb8 100644 --- a/apps/builder/src/features/results/api/getResultLogs.ts +++ b/apps/builder/src/features/results/api/getResultLogs.ts @@ -1,7 +1,7 @@ -import { isReadTypebotForbidden } from "@/features/typebot/helpers/isReadTypebotForbidden"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import prisma from "@typebot.io/prisma"; import { logSchema } from "@typebot.io/results/schemas/results"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import { z } from "@typebot.io/zod"; export const getResultLogs = authenticatedProcedure diff --git a/apps/builder/src/features/results/api/getResults.ts b/apps/builder/src/features/results/api/getResults.ts index d3dae4c471..70533cb443 100644 --- a/apps/builder/src/features/results/api/getResults.ts +++ b/apps/builder/src/features/results/api/getResults.ts @@ -6,11 +6,11 @@ import { parseFromDateFromTimeFilter, parseToDateFromTimeFilter, } from "@/features/analytics/helpers/parseDateFromTimeFilter"; -import { isReadTypebotForbidden } from "@/features/typebot/helpers/isReadTypebotForbidden"; import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; import prisma from "@typebot.io/prisma"; import { resultWithAnswersSchema } from "@typebot.io/results/schemas/results"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import { z } from "@typebot.io/zod"; const maxLimit = 100; diff --git a/apps/builder/src/features/typebot/api/getPublishedTypebot.ts b/apps/builder/src/features/typebot/api/getPublishedTypebot.ts index e30cb2dda5..0fb1735e22 100644 --- a/apps/builder/src/features/typebot/api/getPublishedTypebot.ts +++ b/apps/builder/src/features/typebot/api/getPublishedTypebot.ts @@ -1,6 +1,7 @@ import { authenticatedProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; import prisma from "@typebot.io/prisma"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import { migratePublicTypebot } from "@typebot.io/typebot/migrations/migrateTypebot"; import { publicTypebotSchema, @@ -9,7 +10,6 @@ import { } from "@typebot.io/typebot/schemas/publicTypebot"; import type { Typebot } from "@typebot.io/typebot/schemas/typebot"; import { z } from "@typebot.io/zod"; -import { isReadTypebotForbidden } from "../helpers/isReadTypebotForbidden"; export const getPublishedTypebot = authenticatedProcedure .meta({ diff --git a/apps/builder/src/features/typebot/api/getTypebot.ts b/apps/builder/src/features/typebot/api/getTypebot.ts index 8a70db5a2a..348641407f 100644 --- a/apps/builder/src/features/typebot/api/getTypebot.ts +++ b/apps/builder/src/features/typebot/api/getTypebot.ts @@ -3,10 +3,10 @@ import { TRPCError } from "@trpc/server"; import { env } from "@typebot.io/env"; import prisma from "@typebot.io/prisma"; import type { CollaborationType } from "@typebot.io/prisma/enum"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import { migrateTypebot } from "@typebot.io/typebot/migrations/migrateTypebot"; import { typebotSchema } from "@typebot.io/typebot/schemas/typebot"; import { z } from "@typebot.io/zod"; -import { isReadTypebotForbidden } from "../helpers/isReadTypebotForbidden"; export const getTypebot = publicProcedure .meta({ diff --git a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts index bb3f3e1bd7..38f076fba6 100644 --- a/apps/builder/src/features/whatsapp/receiveMessagePreview.ts +++ b/apps/builder/src/features/whatsapp/receiveMessagePreview.ts @@ -1,11 +1,15 @@ import { publicProcedure } from "@/helpers/server/trpc"; import { TRPCError } from "@trpc/server"; import { env } from "@typebot.io/env"; -import { isNotDefined } from "@typebot.io/lib/utils"; import { resumeWhatsAppFlow } from "@typebot.io/whatsapp/resumeWhatsAppFlow"; -import { whatsAppWebhookRequestBodySchema } from "@typebot.io/whatsapp/schemas"; +import { + type WhatsAppWebhookRequestBody, + whatsAppWebhookRequestBodySchema, +} from "@typebot.io/whatsapp/schemas"; import { z } from "@typebot.io/zod"; +const whatsAppPreviewSessionIdPrefix = "wa-preview-"; + export const receiveMessagePreview = publicProcedure .meta({ openapi: { @@ -22,23 +26,40 @@ export const receiveMessagePreview = publicProcedure }), ) .mutation(async ({ input: { entry } }) => { - if (!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID) - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID is not defined", - }); - const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0); - if (isNotDefined(receivedMessage)) return { message: "No message found" }; - const contactName = - entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ""; - const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ""; - return resumeWhatsAppFlow({ + assertEnv(); + + const { receivedMessage, contactName, contactPhoneNumber } = + extractMessageData(entry); + if (!receivedMessage) return { message: "No message found" }; + + await resumeWhatsAppFlow({ receivedMessage, - sessionId: `wa-preview-${receivedMessage.from}`, + sessionId: `${whatsAppPreviewSessionIdPrefix}${receivedMessage.from}`, contact: { name: contactName, phoneNumber: contactPhoneNumber, }, }); + + return { + message: "Message received", + }; }); + +const assertEnv = () => { + if (!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID) + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID is not defined", + }); +}; + +const extractMessageData = (entry: WhatsAppWebhookRequestBody["entry"]) => { + const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0); + const contactName = + entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ""; + const contactPhoneNumber = + entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ""; + + return { receivedMessage, contactName, contactPhoneNumber }; +}; diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index 87ee7d5772..6dc752b03b 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -7,11 +7,11 @@ import type { SessionState } from "@typebot.io/bot-engine/schemas/chatSession"; import { startSession } from "@typebot.io/bot-engine/startSession"; import { env } from "@typebot.io/env"; import prisma from "@typebot.io/prisma"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import { sendChatReplyToWhatsApp } from "@typebot.io/whatsapp/sendChatReplyToWhatsApp"; import { sendWhatsAppMessage } from "@typebot.io/whatsapp/sendWhatsAppMessage"; import { z } from "@typebot.io/zod"; import { HTTPError } from "ky"; -import { isReadTypebotForbidden } from "../typebot/helpers/isReadTypebotForbidden"; export const startWhatsAppPreview = authenticatedProcedure .meta({ @@ -127,7 +127,6 @@ export const startWhatsAppPreview = authenticatedProcedure if (canSendDirectMessagesToUser) { await sendChatReplyToWhatsApp({ to, - typingEmulation: newSessionState.typingEmulation, messages, input, clientSideActions, diff --git a/apps/builder/src/helpers/server/routers/publicRouter.ts b/apps/builder/src/helpers/server/routers/publicRouter.ts index a17205351a..64e08e4aa1 100644 --- a/apps/builder/src/helpers/server/routers/publicRouter.ts +++ b/apps/builder/src/helpers/server/routers/publicRouter.ts @@ -1,6 +1,6 @@ import { analyticsRouter } from "@/features/analytics/api/router"; import { billingRouter } from "@/features/billing/api/router"; -import { webhookRouter } from "@/features/blocks/integrations/webhook/api/router"; +import { httpRequestRouter } from "@/features/blocks/integrations/httpRequest/api/router"; import { getLinkedTypebots } from "@/features/blocks/logic/typebotLink/api/getLinkedTypebots"; import { collaboratorsRouter } from "@/features/collaboration/api/router"; import { credentialsRouter } from "@/features/credentials/api/router"; @@ -18,7 +18,7 @@ export const publicRouter = router({ analytics: analyticsRouter, workspace: workspaceRouter, typebot: typebotRouter, - webhook: webhookRouter, + httpRequest: httpRequestRouter, results: resultsRouter, billing: billingRouter, credentials: credentialsRouter, diff --git a/apps/builder/src/lib/theme.ts b/apps/builder/src/lib/theme.ts index 1526be70e2..ec95bae482 100644 --- a/apps/builder/src/lib/theme.ts +++ b/apps/builder/src/lib/theme.ts @@ -5,6 +5,7 @@ import { modalAnatomy, popoverAnatomy, switchAnatomy, + tabsAnatomy, } from "@chakra-ui/anatomy"; import { type StyleFunctionProps, @@ -188,6 +189,36 @@ const Switch = createMultiStyleConfigHelpers( }), }); +const Tabs = createMultiStyleConfigHelpers( + tabsAnatomy.keys, +).defineMultiStyleConfig({ + baseStyle: ({ colorMode }) => ({ + tablist: { + gap: 2, + }, + tab: { + px: "3", + borderRadius: "md", + fontWeight: "semibold", + _selected: { + bg: colorMode === "dark" ? "gray.800" : "gray.100", + }, + _hover: { + bg: colorMode === "dark" ? "gray.800" : "gray.100", + }, + _active: { + bg: colorMode === "dark" ? "gray.700" : "gray.200", + }, + }, + tabpanel: { + px: 0, + }, + }), + defaultProps: { + variant: "unstyled", + }, +}); + const components = { Modal, Popover, @@ -226,6 +257,7 @@ const components = { rounded: "md", }, }, + Tabs, }; const styles = { diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testWebhook.ts b/apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testHttpRequest.ts similarity index 91% rename from apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testWebhook.ts rename to apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testHttpRequest.ts index 22eb09b2b8..81b7a4b02d 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testWebhook.ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testHttpRequest.ts @@ -1,12 +1,12 @@ import { getAuthenticatedUser } from "@/features/auth/helpers/getAuthenticatedUser"; -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; import type { Block } from "@typebot.io/blocks-core/schemas/schema"; -import type { HttpRequest } from "@typebot.io/blocks-integrations/webhook/schema"; +import type { HttpRequest } from "@typebot.io/blocks-integrations/httpRequest/schema"; import { - executeWebhook, + executeHttpRequest, parseWebhookAttributes, -} from "@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock"; -import { parseSampleResult } from "@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult"; +} from "@typebot.io/bot-engine/blocks/integrations/httpRequest/executeHttpRequestBlock"; +import { parseSampleResult } from "@typebot.io/bot-engine/blocks/integrations/httpRequest/parseSampleResult"; import { fetchLinkedChildTypebots } from "@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots"; import { saveLog } from "@typebot.io/bot-engine/logs/saveLog"; import { getBlockById } from "@typebot.io/groups/helpers"; @@ -38,7 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const block = typebot.groups .flatMap((g) => g.blocks) .find(byId(blockId)); - if (!block || !isWebhookBlock(block)) + if (!block || !isHttpRequestBlock(block)) return notFound(res, "Webhook block not found"); const webhookId = "webhookId" in block ? block.webhookId : undefined; const webhook = @@ -82,7 +82,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { data: { message: `Couldn't parse webhook attributes` }, }); - const { response, logs } = await executeWebhook(parsedWebhook, { + const { response, logs } = await executeHttpRequest(parsedWebhook, { timeout: block.options?.timeout, }); diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/results/[resultId]/[fileName].ts b/apps/builder/src/pages/api/typebots/[typebotId]/results/[resultId]/[fileName].ts index 2a697210a1..81da93c63a 100644 --- a/apps/builder/src/pages/api/typebots/[typebotId]/results/[resultId]/[fileName].ts +++ b/apps/builder/src/pages/api/typebots/[typebotId]/results/[resultId]/[fileName].ts @@ -1,5 +1,4 @@ import { getAuthenticatedUser } from "@/features/auth/helpers/getAuthenticatedUser"; -import { isReadTypebotForbidden } from "@/features/typebot/helpers/isReadTypebotForbidden"; import { badRequest, methodNotAllowed, @@ -8,6 +7,7 @@ import { } from "@typebot.io/lib/api/utils"; import { getFileTempUrl } from "@typebot.io/lib/s3/getFileTempUrl"; import prisma from "@typebot.io/prisma"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; import type { NextApiRequest, NextApiResponse } from "next"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/apps/builder/src/test/assets/typebots/integrations/webhook.json b/apps/builder/src/test/assets/typebots/integrations/httpRequest.json similarity index 100% rename from apps/builder/src/test/assets/typebots/integrations/webhook.json rename to apps/builder/src/test/assets/typebots/integrations/httpRequest.json diff --git a/apps/builder/src/test/assets/typebots/logic/webhook.json b/apps/builder/src/test/assets/typebots/logic/webhook.json new file mode 100644 index 0000000000..3d26e8ceab --- /dev/null +++ b/apps/builder/src/test/assets/typebots/logic/webhook.json @@ -0,0 +1,92 @@ +{ + "version": "6", + "id": "cm1qmfbmd0001otew1vegx05q", + "name": "My typebotโ€โ€โ€Œโ€Œโ€Œโ€Œโ€โ€Œโ€Œโ€โ€Œโ€Œโ€Œโ€Œโ€โ€Œโ€Œโ€Œ", + "events": [ + { + "id": "aevmtgzc9rxzvxs2q5hvpwct", + "graphCoordinates": { "x": 0, "y": 0 }, + "type": "start", + "outgoingEdgeId": "l4t0scp44brhsaev2shrz1ww" + } + ], + "groups": [ + { + "id": "ad35ipsnb48e6ezkgdir18ti", + "graphCoordinates": { "x": 214.53, "y": 90.88 }, + "title": "Group #1", + "blocks": [ + { + "id": "ti3emvkuw16pno8hfoqhwohy", + "type": "text", + "content": { + "richText": [ + { + "type": "p", + "children": [{ "text": "Ok let's look into that..." }] + } + ] + } + }, + { + "id": "webhook-block-id", + "type": "webhook", + "outgoingEdgeId": "zmc9shyqackqkxqyjok5adip" + } + ] + }, + { + "id": "wt0gw2gk1bw8x7ajch6ppkkm", + "graphCoordinates": { "x": 607.12, "y": 92 }, + "title": "Group #2", + "blocks": [ + { + "id": "ysc1dd8tydgn810jzn5xkhep", + "type": "text", + "content": { + "richText": [ + { + "type": "p", + "children": [{ "text": "Hi {{Name}}, nice to see you again!" }] + } + ] + } + } + ] + } + ], + "edges": [ + { + "from": { "eventId": "aevmtgzc9rxzvxs2q5hvpwct" }, + "to": { "groupId": "ad35ipsnb48e6ezkgdir18ti" }, + "id": "l4t0scp44brhsaev2shrz1ww" + }, + { + "from": { "blockId": "n0i5queut7m0wa5am2gqa0ra" }, + "to": { "groupId": "wt0gw2gk1bw8x7ajch6ppkkm" }, + "id": "zmc9shyqackqkxqyjok5adip" + } + ], + "variables": [ + { + "id": "vzq187kjoyn9nt7283dkbs59n", + "name": "Name", + "isSessionVariable": true + } + ], + "theme": {}, + "selectedThemeTemplateId": null, + "settings": { "general": { "isBrandingEnabled": true } }, + "createdAt": "2024-10-01T15:56:19.477Z", + "updatedAt": "2024-10-01T15:56:19.477Z", + "icon": null, + "folderId": null, + "publicId": null, + "customDomain": null, + "workspaceId": "freeWorkspace", + "resultsTablePreferences": null, + "isArchived": false, + "isClosed": false, + "whatsAppCredentialsId": null, + "riskLevel": null +} diff --git a/apps/docs/editor/blocks/inputs/email.mdx b/apps/docs/editor/blocks/inputs/email.mdx index d93c355811..ddca0f6d48 100644 --- a/apps/docs/editor/blocks/inputs/email.mdx +++ b/apps/docs/editor/blocks/inputs/email.mdx @@ -28,4 +28,4 @@ The Email input block allows you to ask your user for an email. It will check if The retry message will be displayed whenever Typebot detected that the email is not properly formatted. -It won't check if the email address is **valid**. To do that, you will have to trigger a [Webhook block](/editor/blocks/integrations/webhook) and call an email validation service API. +It won't check if the email address is **valid**. To do that, you will have to trigger a [HTTP request block](/editor/blocks/integrations/http-request) and call an email validation service API. diff --git a/apps/docs/editor/blocks/inputs/website.mdx b/apps/docs/editor/blocks/inputs/website.mdx index f74375fef7..dacbf819c1 100644 --- a/apps/docs/editor/blocks/inputs/website.mdx +++ b/apps/docs/editor/blocks/inputs/website.mdx @@ -28,4 +28,4 @@ The Website input block allows you to ask your user for a URL. It will check if The retry message will be displayed whenever Typebot detected that the email is not properly formatted. -It won't check if the email address is **valid**. To do that, you will have to trigger a [Webhook block](/editor/blocks/integrations/webhook) and call an email validation service API. +It won't check if the email address is **valid**. To do that, you will have to trigger a [HTTP request block](/editor/blocks/integrations/http-request) and call an email validation service API. diff --git a/apps/docs/editor/blocks/integrations/webhook.mdx b/apps/docs/editor/blocks/integrations/http-request.mdx similarity index 91% rename from apps/docs/editor/blocks/integrations/webhook.mdx rename to apps/docs/editor/blocks/integrations/http-request.mdx index 253ccee6bb..dc59fd55fa 100644 --- a/apps/docs/editor/blocks/integrations/webhook.mdx +++ b/apps/docs/editor/blocks/integrations/http-request.mdx @@ -41,7 +41,7 @@ What I need in my case is instead of inserting "Star Wars", I'd like to insert a Variable in URL @@ -50,7 +50,7 @@ Then, we can set a test value for our variable (it will replace the variable wit Variable test values @@ -59,7 +59,7 @@ Hit the "Test the request" button and then we can save the result in multiple va Test request @@ -67,7 +67,7 @@ Hit the "Test the request" button and then we can save the result in multiple va Then we can use these variables to display dynamic content in the next bubbles: - Preview + Preview Possibilities are endless when it comes to API calls, you can litteraly call any API and fetch any data you want. @@ -82,7 +82,7 @@ You only have to paste this URL in the Webhook block and click on "Test the requ Simple Webhook POST diff --git a/apps/docs/editor/blocks/logic/wait.mdx b/apps/docs/editor/blocks/logic/wait.mdx index 9c66bdb06d..e5a00afade 100644 --- a/apps/docs/editor/blocks/logic/wait.mdx +++ b/apps/docs/editor/blocks/logic/wait.mdx @@ -7,11 +7,11 @@ The "Wait" block allows you to pause the conversation for a certain amount of se This can be useful if you want the bot to emphasize on what's been said or to wait before a redirection for example to make sure the user has read everything. - + This should be used wisely. If you want the bot to write slower or faster in a more general sense, you need to check the [Typing emulation settings](/settings/overview#typing-emulation) - + ## Pause the flow diff --git a/apps/docs/editor/blocks/logic/webhook.mdx b/apps/docs/editor/blocks/logic/webhook.mdx new file mode 100644 index 0000000000..b4c7a5e566 --- /dev/null +++ b/apps/docs/editor/blocks/logic/webhook.mdx @@ -0,0 +1,40 @@ +--- +title: Webhook +icon: webhook +--- + +The Webhook block allows you to pause the conversation until a provided webhook URL is called. + +This can be useful if you want to execute a long-running action that can take several minutes to complete. + +## URLs + +### Test + +- Web: + +``` +https://typebot.io/api/v1/typebots/{typebotId}/blocks/{blockId}/web/executeTestWebhook +``` + +- WhatsApp: + +``` +https://typebot.io/api/v1/typebots/{typebotId}/blocks/{blockId}/whatsapp/{phone}/executeTestWebhook +``` + +Where `{phone}` is your phone number on which you test the bot. For example, if you provided `+33 601 020304`, `{phone}` should be `33601020304`, no spaces, no `+`. + +### Production + +No matter the environment, the webhook URL will be the same: + +``` +https://typebot.io/api/v1/typebots/{typebotId}/blocks/{blockId}/results/{resultId}/executeWebhook +``` + +Where `{resultId}` is the current user result ID. You can get this ID directly in your typebot flow using a [Set variable](/editor/blocks/logic/set-variable) block with the `Result ID` value. + +## Authentication + +The Webhook URL needs to be authenticated in order to work. See [API authentication](/api-reference/authentication) for more information. diff --git a/apps/docs/images/blocks/integrations/webhook/preview.png b/apps/docs/images/blocks/integrations/http-request/preview.png similarity index 100% rename from apps/docs/images/blocks/integrations/webhook/preview.png rename to apps/docs/images/blocks/integrations/http-request/preview.png diff --git a/apps/docs/images/blocks/integrations/webhook/save-in-variable.png b/apps/docs/images/blocks/integrations/http-request/save-in-variable.png similarity index 100% rename from apps/docs/images/blocks/integrations/webhook/save-in-variable.png rename to apps/docs/images/blocks/integrations/http-request/save-in-variable.png diff --git a/apps/docs/images/blocks/integrations/webhook/simple-post.png b/apps/docs/images/blocks/integrations/http-request/simple-post.png similarity index 100% rename from apps/docs/images/blocks/integrations/webhook/simple-post.png rename to apps/docs/images/blocks/integrations/http-request/simple-post.png diff --git a/apps/docs/images/blocks/integrations/webhook/variable-test-value.png b/apps/docs/images/blocks/integrations/http-request/variable-test-value.png similarity index 100% rename from apps/docs/images/blocks/integrations/webhook/variable-test-value.png rename to apps/docs/images/blocks/integrations/http-request/variable-test-value.png diff --git a/apps/docs/images/blocks/integrations/webhook/variable-url.png b/apps/docs/images/blocks/integrations/http-request/variable-url.png similarity index 100% rename from apps/docs/images/blocks/integrations/webhook/variable-url.png rename to apps/docs/images/blocks/integrations/http-request/variable-url.png diff --git a/apps/docs/mint.json b/apps/docs/mint.json index e05c830061..9cf64c15d9 100644 --- a/apps/docs/mint.json +++ b/apps/docs/mint.json @@ -106,7 +106,8 @@ "editor/blocks/logic/typebot-link", "editor/blocks/logic/wait", "editor/blocks/logic/jump", - "editor/blocks/logic/ab-test" + "editor/blocks/logic/ab-test", + "editor/blocks/logic/webhook" ] }, { @@ -114,7 +115,7 @@ "pages": [ "editor/blocks/integrations/google-sheets", "editor/blocks/integrations/google-analytics", - "editor/blocks/integrations/webhook", + "editor/blocks/integrations/http-request", "editor/blocks/integrations/send-email", "editor/blocks/integrations/zapier", "editor/blocks/integrations/make-com", @@ -341,5 +342,11 @@ "discord": "https://typebot.io/discord", "twitter": "https://twitter.com/Typebot_io", "github": "https://github.com/baptisteArno/typebot.io" - } + }, + "redirects": [ + { + "source": "/editor/blocks/integrations/webhook", + "destination": "/editor/blocks/integrations/http-request" + } + ] } diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index b951f47ddf..633cb5c159 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -7952,11 +7952,11 @@ }, "/v1/typebots/{typebotId}/webhookBlocks": { "get": { - "operationId": "webhook-listWebhookBlocks", - "summary": "List webhook blocks", - "description": "Returns a list of all the webhook blocks that you can subscribe to.", + "operationId": "httpRequest-listHttpRequestBlocks", + "summary": "List HTTP request blocks", + "description": "Returns a list of all the HTTP request blocks that you can subscribe to.", "tags": [ - "Webhook" + "HTTP request" ], "security": [ { @@ -8075,11 +8075,11 @@ }, "/v1/typebots/{typebotId}/webhookBlocks/{blockId}/getResultExample": { "get": { - "operationId": "webhook-getResultExample", + "operationId": "httpRequest-getResultExample", "summary": "Get result example", - "description": "Returns \"fake\" result for webhook block to help you anticipate how the webhook will behave.", + "description": "Returns \"fake\" result for http request block to help you anticipate how the webhook will behave.", "tags": [ - "Webhook" + "HTTP request" ], "security": [ { @@ -8180,10 +8180,10 @@ }, "/v1/typebots/{typebotId}/webhookBlocks/{blockId}/subscribe": { "post": { - "operationId": "webhook-subscribeWebhook", - "summary": "Subscribe to webhook block", + "operationId": "httpRequest-subscribeHttpRequest", + "summary": "Subscribe to HTTP request block", "tags": [ - "Webhook" + "HTTP request" ], "security": [ { @@ -8295,10 +8295,10 @@ }, "/v1/typebots/{typebotId}/webhookBlocks/{blockId}/unsubscribe": { "post": { - "operationId": "webhook-unsubscribeWebhook", - "summary": "Unsubscribe from webhook block", + "operationId": "httpRequest-unsubscribeHttpRequest", + "summary": "Unsubscribe from HTTP request block", "tags": [ - "Webhook" + "HTTP request" ], "security": [ { @@ -13310,6 +13310,40 @@ "location", "timestamp" ] + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webhook" + ] + }, + "webhook": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + }, + "required": [ + "data" + ] + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "webhook", + "timestamp" + ] } ] } @@ -16026,6 +16060,9 @@ }, { "$ref": "#/components/schemas/abTestLogic" + }, + { + "$ref": "#/components/schemas/webhookLogic" } ], "discriminator": { @@ -16038,7 +16075,8 @@ "Wait": "#/components/schemas/waitLogic", "Jump": "#/components/schemas/jumpLogic", "Condition": "#/components/schemas/conditionLogic", - "AB test": "#/components/schemas/abTestLogic" + "AB test": "#/components/schemas/abTestLogic", + "webhook": "#/components/schemas/webhookLogic" } } }, @@ -17042,6 +17080,53 @@ ], "title": "AB Test" }, + "webhookLogic": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webhook" + ] + }, + "options": { + "type": "object", + "properties": { + "responseVariableMapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "bodyPath": { + "type": "string" + } + }, + "required": [ + "id" + ] + } + } + } + } + }, + "required": [ + "id", + "type" + ], + "title": "Webhook" + }, "googleSheetsBlock": { "type": "object", "properties": { diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 49e430f4c0..230b5e7c47 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -385,7 +385,7 @@ "$ref": "#/components/schemas/csaStreamOpenAiChatCompletion" }, { - "$ref": "#/components/schemas/csaExecWebhook" + "$ref": "#/components/schemas/csaHttpRequestToExecute" }, { "$ref": "#/components/schemas/csaInjectStartProps" @@ -398,6 +398,9 @@ }, { "$ref": "#/components/schemas/csaCodeToExecute" + }, + { + "$ref": "#/components/schemas/csaListenForWebhook" } ], "discriminator": { @@ -410,11 +413,12 @@ "wait": "#/components/schemas/csaWait", "setVariable": "#/components/schemas/csaSetVariable", "streamOpenAiChatCompletion": "#/components/schemas/csaStreamOpenAiChatCompletion", - "webhookToExecute": "#/components/schemas/csaExecWebhook", + "httpRequestToExecute": "#/components/schemas/csaHttpRequestToExecute", "startPropsToInject": "#/components/schemas/csaInjectStartProps", "pixel": "#/components/schemas/csaPixel", "stream": "#/components/schemas/csaStream", - "codeToExecute": "#/components/schemas/csaCodeToExecute" + "codeToExecute": "#/components/schemas/csaCodeToExecute", + "listenForWebhook": "#/components/schemas/csaListenForWebhook" } } } @@ -895,7 +899,7 @@ "$ref": "#/components/schemas/csaStreamOpenAiChatCompletion" }, { - "$ref": "#/components/schemas/csaExecWebhook" + "$ref": "#/components/schemas/csaHttpRequestToExecute" }, { "$ref": "#/components/schemas/csaInjectStartProps" @@ -908,6 +912,9 @@ }, { "$ref": "#/components/schemas/csaCodeToExecute" + }, + { + "$ref": "#/components/schemas/csaListenForWebhook" } ], "discriminator": { @@ -920,11 +927,12 @@ "wait": "#/components/schemas/csaWait", "setVariable": "#/components/schemas/csaSetVariable", "streamOpenAiChatCompletion": "#/components/schemas/csaStreamOpenAiChatCompletion", - "webhookToExecute": "#/components/schemas/csaExecWebhook", + "httpRequestToExecute": "#/components/schemas/csaHttpRequestToExecute", "startPropsToInject": "#/components/schemas/csaInjectStartProps", "pixel": "#/components/schemas/csaPixel", "stream": "#/components/schemas/csaStream", - "codeToExecute": "#/components/schemas/csaCodeToExecute" + "codeToExecute": "#/components/schemas/csaCodeToExecute", + "listenForWebhook": "#/components/schemas/csaListenForWebhook" } } } @@ -1337,7 +1345,7 @@ "$ref": "#/components/schemas/csaStreamOpenAiChatCompletion" }, { - "$ref": "#/components/schemas/csaExecWebhook" + "$ref": "#/components/schemas/csaHttpRequestToExecute" }, { "$ref": "#/components/schemas/csaInjectStartProps" @@ -1350,6 +1358,9 @@ }, { "$ref": "#/components/schemas/csaCodeToExecute" + }, + { + "$ref": "#/components/schemas/csaListenForWebhook" } ], "discriminator": { @@ -1362,11 +1373,12 @@ "wait": "#/components/schemas/csaWait", "setVariable": "#/components/schemas/csaSetVariable", "streamOpenAiChatCompletion": "#/components/schemas/csaStreamOpenAiChatCompletion", - "webhookToExecute": "#/components/schemas/csaExecWebhook", + "httpRequestToExecute": "#/components/schemas/csaHttpRequestToExecute", "startPropsToInject": "#/components/schemas/csaInjectStartProps", "pixel": "#/components/schemas/csaPixel", "stream": "#/components/schemas/csaStream", - "codeToExecute": "#/components/schemas/csaCodeToExecute" + "codeToExecute": "#/components/schemas/csaCodeToExecute", + "listenForWebhook": "#/components/schemas/csaListenForWebhook" } } }, @@ -1692,7 +1704,7 @@ "$ref": "#/components/schemas/csaStreamOpenAiChatCompletion" }, { - "$ref": "#/components/schemas/csaExecWebhook" + "$ref": "#/components/schemas/csaHttpRequestToExecute" }, { "$ref": "#/components/schemas/csaInjectStartProps" @@ -1705,6 +1717,9 @@ }, { "$ref": "#/components/schemas/csaCodeToExecute" + }, + { + "$ref": "#/components/schemas/csaListenForWebhook" } ], "discriminator": { @@ -1717,11 +1732,12 @@ "wait": "#/components/schemas/csaWait", "setVariable": "#/components/schemas/csaSetVariable", "streamOpenAiChatCompletion": "#/components/schemas/csaStreamOpenAiChatCompletion", - "webhookToExecute": "#/components/schemas/csaExecWebhook", + "httpRequestToExecute": "#/components/schemas/csaHttpRequestToExecute", "startPropsToInject": "#/components/schemas/csaInjectStartProps", "pixel": "#/components/schemas/csaPixel", "stream": "#/components/schemas/csaStream", - "codeToExecute": "#/components/schemas/csaCodeToExecute" + "codeToExecute": "#/components/schemas/csaCodeToExecute", + "listenForWebhook": "#/components/schemas/csaListenForWebhook" } } }, @@ -2154,7 +2170,7 @@ "$ref": "#/components/schemas/csaStreamOpenAiChatCompletion" }, { - "$ref": "#/components/schemas/csaExecWebhook" + "$ref": "#/components/schemas/csaHttpRequestToExecute" }, { "$ref": "#/components/schemas/csaInjectStartProps" @@ -2167,6 +2183,9 @@ }, { "$ref": "#/components/schemas/csaCodeToExecute" + }, + { + "$ref": "#/components/schemas/csaListenForWebhook" } ], "discriminator": { @@ -2179,11 +2198,12 @@ "wait": "#/components/schemas/csaWait", "setVariable": "#/components/schemas/csaSetVariable", "streamOpenAiChatCompletion": "#/components/schemas/csaStreamOpenAiChatCompletion", - "webhookToExecute": "#/components/schemas/csaExecWebhook", + "httpRequestToExecute": "#/components/schemas/csaHttpRequestToExecute", "startPropsToInject": "#/components/schemas/csaInjectStartProps", "pixel": "#/components/schemas/csaPixel", "stream": "#/components/schemas/csaStream", - "codeToExecute": "#/components/schemas/csaCodeToExecute" + "codeToExecute": "#/components/schemas/csaCodeToExecute", + "listenForWebhook": "#/components/schemas/csaListenForWebhook" } } }, @@ -3128,6 +3148,40 @@ "location", "timestamp" ] + }, + { + "type": "object", + "properties": { + "from": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webhook" + ] + }, + "webhook": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + }, + "required": [ + "data" + ] + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "from", + "type", + "webhook", + "timestamp" + ] } ] } @@ -3606,6 +3660,9 @@ }, { "$ref": "#/components/schemas/abTestLogic" + }, + { + "$ref": "#/components/schemas/webhookLogic" } ], "discriminator": { @@ -3618,7 +3675,8 @@ "Wait": "#/components/schemas/waitLogic", "Jump": "#/components/schemas/jumpLogic", "Condition": "#/components/schemas/conditionLogic", - "AB test": "#/components/schemas/abTestLogic" + "AB test": "#/components/schemas/abTestLogic", + "webhook": "#/components/schemas/webhookLogic" } } }, @@ -5754,6 +5812,53 @@ ], "title": "AB Test" }, + "webhookLogic": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "outgoingEdgeId": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "webhook" + ] + }, + "options": { + "type": "object", + "properties": { + "responseVariableMapping": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "variableId": { + "type": "string" + }, + "bodyPath": { + "type": "string" + } + }, + "required": [ + "id" + ] + } + } + } + } + }, + "required": [ + "id", + "type" + ], + "title": "Webhook" + }, "chatwootBlock": { "type": "object", "properties": { @@ -13359,16 +13464,16 @@ ], "title": "Stream OpenAI" }, - "csaExecWebhook": { + "csaHttpRequestToExecute": { "type": "object", "properties": { "type": { "type": "string", "enum": [ - "webhookToExecute" + "httpRequestToExecute" ] }, - "webhookToExecute": { + "httpRequestToExecute": { "type": "object", "properties": { "url": { @@ -13409,9 +13514,9 @@ }, "required": [ "type", - "webhookToExecute" + "httpRequestToExecute" ], - "title": "Execute webhook" + "title": "Execute HTTP request" }, "csaInjectStartProps": { "type": "object", @@ -13671,6 +13776,27 @@ ], "title": "Execute code" }, + "csaListenForWebhook": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "listenForWebhook" + ] + }, + "lastBubbleBlockId": { + "type": "string" + }, + "expectsDedicatedReply": { + "type": "boolean" + } + }, + "required": [ + "type" + ], + "title": "Listen to webhook" + }, "error.BAD_REQUEST": { "type": "object", "properties": { diff --git a/apps/viewer/package.json b/apps/viewer/package.json index e83b7a8d39..007ec214d1 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -10,6 +10,7 @@ "test:ui": "dotenv -e ./.env -e ../../.env -- playwright test --ui" }, "dependencies": { + "partysocket": "1.0.2", "@typebot.io/trpc-openapi": "workspace:*", "@planetscale/database": "1.8.0", "@sentry/nextjs": "7.77.0", diff --git a/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts b/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts index fe3df4ab89..ab060a5df2 100644 --- a/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts +++ b/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts @@ -140,7 +140,7 @@ export const sendMessageV1 = publicProcedure logs: allLogs, clientSideActions, visitedEdges, - hasEmbedBubbleWithWaitEvent: messages.some( + isWaitingForExternalEvent: messages.some( (message) => message.type === "custom-embed" || (message.type === BubbleBlockType.EMBED && @@ -209,7 +209,7 @@ export const sendMessageV1 = publicProcedure logs: allLogs, clientSideActions, visitedEdges, - hasEmbedBubbleWithWaitEvent: messages.some( + isWaitingForExternalEvent: messages.some( (message) => message.type === "custom-embed" || (message.type === BubbleBlockType.EMBED && diff --git a/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts b/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts index f8d412ffb7..52e3f93c3b 100644 --- a/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts +++ b/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts @@ -140,7 +140,7 @@ export const sendMessageV2 = publicProcedure logs: allLogs, clientSideActions, visitedEdges, - hasEmbedBubbleWithWaitEvent: messages.some( + isWaitingForExternalEvent: messages.some( (message) => message.type === "custom-embed" || (message.type === BubbleBlockType.EMBED && @@ -208,7 +208,7 @@ export const sendMessageV2 = publicProcedure logs: allLogs, clientSideActions, visitedEdges, - hasEmbedBubbleWithWaitEvent: messages.some( + isWaitingForExternalEvent: messages.some( (message) => message.type === "custom-embed" || (message.type === BubbleBlockType.EMBED && diff --git a/apps/viewer/src/features/whatsapp/api/receiveMessage.ts b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts index ef3ff9b68e..59991c3c9a 100644 --- a/apps/viewer/src/features/whatsapp/api/receiveMessage.ts +++ b/apps/viewer/src/features/whatsapp/api/receiveMessage.ts @@ -1,9 +1,13 @@ import { publicProcedure } from "@/helpers/server/trpc"; -import { isNotDefined } from "@typebot.io/lib/utils"; import { resumeWhatsAppFlow } from "@typebot.io/whatsapp/resumeWhatsAppFlow"; -import { whatsAppWebhookRequestBodySchema } from "@typebot.io/whatsapp/schemas"; +import { + type WhatsAppWebhookRequestBody, + whatsAppWebhookRequestBodySchema, +} from "@typebot.io/whatsapp/schemas"; import { z } from "@typebot.io/zod"; +const whatsAppSessionIdPrefix = "wa-"; + export const receiveMessage = publicProcedure .meta({ openapi: { @@ -24,18 +28,14 @@ export const receiveMessage = publicProcedure }), ) .mutation(async ({ input: { entry, credentialsId, workspaceId } }) => { - const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0); - if (isNotDefined(receivedMessage)) return { message: "No message found" }; - const contactName = - entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ""; - const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ""; - const phoneNumberId = entry.at(0)?.changes.at(0)?.value - .metadata.phone_number_id; - if (!phoneNumberId) return { message: "No phone number id found" }; - return resumeWhatsAppFlow({ + const { receivedMessage, contactName, contactPhoneNumber, phoneNumberId } = + extractMessageDetails(entry); + if (!receivedMessage) return { message: "No message found" }; + if (!phoneNumberId) return { message: "No phone number found" }; + + await resumeWhatsAppFlow({ receivedMessage, - sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`, + sessionId: `${whatsAppSessionIdPrefix}${phoneNumberId}-${receivedMessage.from}`, phoneNumberId, credentialsId, workspaceId, @@ -44,4 +44,19 @@ export const receiveMessage = publicProcedure phoneNumber: contactPhoneNumber, }, }); + + return { + message: "Message received", + }; }); + +const extractMessageDetails = (entry: WhatsAppWebhookRequestBody["entry"]) => { + const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0); + const contactName = + entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ""; + const contactPhoneNumber = + entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ""; + const phoneNumberId = entry.at(0)?.changes.at(0)?.value + .metadata.phone_number_id; + return { receivedMessage, contactName, contactPhoneNumber, phoneNumberId }; +}; diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index 6946b68af9..861f1d4fd8 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -1,12 +1,12 @@ import { authenticateUser } from "@/helpers/authenticateUser"; -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; import type { Block } from "@typebot.io/blocks-core/schemas/schema"; -import type { HttpRequest } from "@typebot.io/blocks-integrations/webhook/schema"; +import type { HttpRequest } from "@typebot.io/blocks-integrations/httpRequest/schema"; import { - executeWebhook, + executeHttpRequest, parseWebhookAttributes, -} from "@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock"; -import { parseSampleResult } from "@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult"; +} from "@typebot.io/bot-engine/blocks/integrations/httpRequest/executeHttpRequestBlock"; +import { parseSampleResult } from "@typebot.io/bot-engine/blocks/integrations/httpRequest/parseSampleResult"; import { fetchLinkedChildTypebots } from "@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots"; import { fetchLinkedParentTypebots } from "@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedParentTypebots"; import { saveLog } from "@typebot.io/bot-engine/logs/saveLog"; @@ -24,7 +24,6 @@ import type { PublicTypebot } from "@typebot.io/typebot/schemas/publicTypebot"; import type { Typebot } from "@typebot.io/typebot/schemas/typebot"; import type { Variable } from "@typebot.io/variables/schemas"; import Cors from "cors"; -/* eslint-disable @typescript-eslint/no-explicit-any */ import type { NextApiRequest, NextApiResponse } from "next"; const cors = initMiddleware(Cors()); @@ -51,7 +50,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const block = typebot.groups .flatMap((g) => g.blocks) .find(byId(blockId)); - if (!block || !isWebhookBlock(block)) + if (!block || !isHttpRequestBlock(block)) return notFound(res, "Webhook block not found"); const webhookId = "webhookId" in block ? block.webhookId : undefined; const webhook = @@ -117,7 +116,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { data: { message: `Couldn't parse webhook attributes` }, }); - const { response, logs } = await executeWebhook(parsedWebhook, { + const { response, logs } = await executeHttpRequest(parsedWebhook, { timeout: block.options?.timeout, }); diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts index 7de9f20ae5..590ba93367 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookBlocks.ts @@ -1,6 +1,6 @@ import { authenticateUser } from "@/helpers/authenticateUser"; -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; -import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/webhook/schema"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; +import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/httpRequest/schema"; import type { Group } from "@typebot.io/groups/schemas"; import { methodNotAllowed } from "@typebot.io/lib/api/utils"; import prisma from "@typebot.io/prisma"; @@ -22,7 +22,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { { blockId: string; name: string; url: string | undefined }[] >((emptyWebhookBlocks, group) => { const blocks = group.blocks.filter((block) => - isWebhookBlock(block), + isHttpRequestBlock(block), ) as HttpRequestBlock[]; return [ ...emptyWebhookBlocks, diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts index 00b3627408..56291caf32 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/webhookSteps.ts @@ -1,5 +1,5 @@ import { authenticateUser } from "@/helpers/authenticateUser"; -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; import type { Group } from "@typebot.io/groups/schemas"; import { methodNotAllowed } from "@typebot.io/lib/api/utils"; import { isNotDefined } from "@typebot.io/lib/utils"; @@ -23,7 +23,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { >((emptyWebhookBlocks, group) => { const blocks = group.blocks.filter( (block) => - isWebhookBlock(block) && + isHttpRequestBlock(block) && isNotDefined( typebot?.webhooks.find((w) => { if ("id" in w && "webhookId" in block) diff --git a/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/results/[resultId]/executeWebhook.ts b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/results/[resultId]/executeWebhook.ts new file mode 100644 index 0000000000..8f39bfec18 --- /dev/null +++ b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/results/[resultId]/executeWebhook.ts @@ -0,0 +1,132 @@ +import { authenticateUser } from "@/helpers/authenticateUser"; +import { LogicBlockType } from "@typebot.io/blocks-logic/constants"; +import { getSession } from "@typebot.io/bot-engine/queries/getSession"; +import { env } from "@typebot.io/env"; +import { parseGroups } from "@typebot.io/groups/schemas"; +import { + forbidden, + internalServerError, + methodNotAllowed, + notFound, +} from "@typebot.io/lib/api/utils"; +import { byId } from "@typebot.io/lib/utils"; +import prisma from "@typebot.io/prisma"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; +import { resumeWhatsAppFlow } from "@typebot.io/whatsapp/resumeWhatsAppFlow"; +import type { NextApiRequest, NextApiResponse } from "next"; +import PartySocket from "partysocket"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === "POST") { + if (!env.NEXT_PUBLIC_PARTYKIT_HOST) return notFound(res); + const user = await authenticateUser(req); + if (!user) return forbidden(res, "User not authenticated"); + const typebotId = req.query.typebotId as string; + const blockId = req.query.blockId as string; + const resultId = req.query.resultId as string; + const typebot = await prisma.typebot.findUnique({ + where: { id: typebotId }, + select: { + version: true, + groups: true, + settings: true, + whatsAppCredentialsId: true, + workspace: { + select: { + id: true, + isSuspended: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, + collaborators: { + select: { + userId: true, + }, + }, + }, + }); + if (!typebot || (await isReadTypebotForbidden(typebot, user))) + return notFound(res, "Typebot not found"); + if (!typebot) return notFound(res); + if (typebot.version !== "6") return internalServerError(res); + const block = parseGroups(typebot.groups, { + typebotVersion: typebot.version, + }) + .flatMap((g) => g.blocks) + .find(byId(blockId)); + if (!block || block.type !== LogicBlockType.WEBHOOK) + return notFound(res, "Webhook block not found"); + + const result = await prisma.result.findUnique({ + where: { + id: resultId, + }, + select: { + lastChatSessionId: true, + }, + }); + + if (!result?.lastChatSessionId) + return notFound(res, "No chat session found"); + + const chatSession = await getSession(result.lastChatSessionId); + + if (chatSession?.state.whatsApp) { + if (!typebot.whatsAppCredentialsId) + return internalServerError( + res, + "Found WA session but no credentialsId in typebot", + ); + const from = chatSession.id.split("-").at(-1); + if (!from) + return internalServerError( + res, + "Expected session ID to be in format: wa-{phoneNumberId}-{receivedMessage.from}", + ); + await resumeWhatsAppFlow({ + receivedMessage: { + from, + timestamp: new Date().toISOString(), + type: "webhook", + webhook: { + data: + typeof req.body === "string" + ? JSON.stringify({ data: JSON.parse(req.body) }) + : JSON.stringify({ data: req.body }, null, 2), + }, + }, + workspaceId: typebot.workspace.id, + sessionId: chatSession.id, + credentialsId: typebot.whatsAppCredentialsId, + origin: "webhook", + }); + return res.status(200).send("OK"); + } + + try { + await PartySocket.fetch( + { host: env.NEXT_PUBLIC_PARTYKIT_HOST, room: `${resultId}/webhooks` }, + { + method: "POST", + body: + typeof req.body === "string" + ? req.body + : JSON.stringify(req.body, null, 2), + }, + ); + } catch (error) { + console.error("PartySocket.fetch error:", error); + return internalServerError(res, "PartySocket.fetch error"); + } + + return res.status(200).send("OK"); + } + return methodNotAllowed(res); +}; + +export default handler; diff --git a/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/web/executeTestWebhook.ts b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/web/executeTestWebhook.ts new file mode 100644 index 0000000000..4d8b942ffb --- /dev/null +++ b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/web/executeTestWebhook.ts @@ -0,0 +1,82 @@ +import { authenticateUser } from "@/helpers/authenticateUser"; +import { LogicBlockType } from "@typebot.io/blocks-logic/constants"; +import { env } from "@typebot.io/env"; +import { parseGroups } from "@typebot.io/groups/schemas"; +import { + badRequest, + forbidden, + methodNotAllowed, + notFound, +} from "@typebot.io/lib/api/utils"; +import { byId } from "@typebot.io/lib/utils"; +import prisma from "@typebot.io/prisma"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; +import type { NextApiRequest, NextApiResponse } from "next"; +import PartySocket from "partysocket"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === "POST") { + if (!env.NEXT_PUBLIC_PARTYKIT_HOST) return notFound(res); + const user = await authenticateUser(req); + if (!user) return forbidden(res, "User not authenticated"); + const typebotId = req.query.typebotId as string; + const blockId = req.query.blockId as string; + const typebot = await prisma.typebot.findUnique({ + where: { id: typebotId }, + select: { + version: true, + groups: true, + workspace: { + select: { + isSuspended: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, + collaborators: { + select: { + userId: true, + }, + }, + }, + }); + if (!typebot || (await isReadTypebotForbidden(typebot, user))) + return notFound(res, "Typebot not found"); + if (typebot.version !== "6") return badRequest(res); + const block = parseGroups(typebot.groups, { + typebotVersion: typebot.version, + }) + .flatMap((g) => g.blocks) + .find(byId(blockId)); + if (!block || block.type !== LogicBlockType.WEBHOOK) + return notFound(res, "Webhook block not found"); + + try { + await PartySocket.fetch( + { + host: env.NEXT_PUBLIC_PARTYKIT_HOST, + room: `${user.id}/${typebotId}/webhooks`, + }, + { + method: "POST", + body: + typeof req.body === "string" + ? req.body + : JSON.stringify(req.body, null, 2), + }, + ); + } catch (error) { + console.error("PartySocket.fetch error:", error); + return res.status(500).send("PartySocket.fetch error"); + } + + return res.status(200).send("OK"); + } + return methodNotAllowed(res); +}; + +export default handler; diff --git a/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/whatsapp/[phone]/executeTestWebhook.ts b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/whatsapp/[phone]/executeTestWebhook.ts new file mode 100644 index 0000000000..86fa7afdf4 --- /dev/null +++ b/apps/viewer/src/pages/api/v1/typebots/[typebotId]/blocks/[blockId]/whatsapp/[phone]/executeTestWebhook.ts @@ -0,0 +1,79 @@ +import { authenticateUser } from "@/helpers/authenticateUser"; +import { LogicBlockType } from "@typebot.io/blocks-logic/constants"; +import { getSession } from "@typebot.io/bot-engine/queries/getSession"; +import { env } from "@typebot.io/env"; +import { parseGroups } from "@typebot.io/groups/schemas"; +import { badRequest, forbidden, notFound } from "@typebot.io/lib/api/utils"; +import { byId } from "@typebot.io/lib/utils"; +import prisma from "@typebot.io/prisma"; +import { isReadTypebotForbidden } from "@typebot.io/typebot/helpers/isReadTypebotForbidden"; +import { resumeWhatsAppFlow } from "@typebot.io/whatsapp/resumeWhatsAppFlow"; +import type { NextApiRequest, NextApiResponse } from "next"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === "POST") { + if (!env.NEXT_PUBLIC_PARTYKIT_HOST) return notFound(res); + const user = await authenticateUser(req); + if (!user) return forbidden(res, "User not authenticated"); + const typebotId = req.query.typebotId as string; + const blockId = req.query.blockId as string; + const phone = req.query.phone as string; + const typebot = await prisma.typebot.findUnique({ + where: { id: typebotId }, + select: { + version: true, + groups: true, + workspace: { + select: { + isSuspended: true, + isPastDue: true, + members: { + select: { + userId: true, + }, + }, + }, + }, + collaborators: { + select: { + userId: true, + }, + }, + }, + }); + if (!typebot || (await isReadTypebotForbidden(typebot, user))) + return notFound(res, "Typebot not found"); + if (typebot.version !== "6") return badRequest(res); + const block = parseGroups(typebot.groups, { + typebotVersion: typebot.version, + }) + .flatMap((g) => g.blocks) + .find(byId(blockId)); + if (!block || block.type !== LogicBlockType.WEBHOOK) + return notFound(res, "Webhook block not found"); + + const chatSession = await getSession(`wa-preview-${phone}`); + + if (!chatSession?.state.whatsApp) + return badRequest(res, "Expected whatsapp chat session"); + + await resumeWhatsAppFlow({ + receivedMessage: { + from: chatSession.id.split("-").at(-1)!, + timestamp: new Date().toISOString(), + type: "webhook", + webhook: { + data: + typeof req.body === "string" + ? JSON.stringify({ data: JSON.parse(req.body) }) + : JSON.stringify({ data: req.body }, null, 2), + }, + }, + sessionId: chatSession.id, + origin: "webhook", + }); + return res.status(200).send("OK"); + } +}; + +export default handler; diff --git a/apps/viewer/src/test/webhook.spec.ts b/apps/viewer/src/test/httpRequest.spec.ts similarity index 100% rename from apps/viewer/src/test/webhook.spec.ts rename to apps/viewer/src/test/httpRequest.spec.ts diff --git a/bun.lockb b/bun.lockb index 21e90c2547..f6318c3190 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 528f76e152..d7c38dc21a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "build-docker-compose-prod": "docker compose -f docker-compose.build.yml build", "start-docker-compose-prod": "docker compose -f docker-compose.yml up", "build": "turbo build --filter=builder... --filter=viewer... --filter=landing-page... --filter=docs...", - "dev": "turbo dev --filter=builder... --filter=viewer...", + "dev": "turbo dev --filter=builder... --filter=viewer... --filter=@typebot.io/partykit... --concurrency=11", "generate-change-log": "git fetch --all && pnpx gitmoji-changelog", "sync-locales": "tolgee sync --continue-on-warning --remove-unused --patterns './apps/builder/src/**/*.ts?(x)' --backup './.tolgee/backup'", "pull-locales": "tolgee pull && prettier --write ./apps/builder/src/i18n", diff --git a/packages/blocks/core/src/helpers.ts b/packages/blocks/core/src/helpers.ts index 875b912761..1c7f06d986 100644 --- a/packages/blocks/core/src/helpers.ts +++ b/packages/blocks/core/src/helpers.ts @@ -10,8 +10,8 @@ import type { PictureChoiceBlock } from "@typebot.io/blocks-inputs/pictureChoice import type { InputBlock } from "@typebot.io/blocks-inputs/schema"; import type { TextInputBlock } from "@typebot.io/blocks-inputs/text/schema"; import { IntegrationBlockType } from "@typebot.io/blocks-integrations/constants"; +import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/httpRequest/schema"; import type { IntegrationBlock } from "@typebot.io/blocks-integrations/schema"; -import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/webhook/schema"; import type { ConditionBlock } from "@typebot.io/blocks-logic/condition/schema"; import { LogicBlockType } from "@typebot.io/blocks-logic/constants"; import type { LogicBlock } from "@typebot.io/blocks-logic/schema"; @@ -65,9 +65,9 @@ export const isIntegrationBlock = (block: Block): block is IntegrationBlock => ) as any[] ).includes(block.type); -export const isWebhookBlock = (block: Block): block is HttpRequestBlock => +export const isHttpRequestBlock = (block: Block): block is HttpRequestBlock => [ - IntegrationBlockType.WEBHOOK, + IntegrationBlockType.HTTP_REQUEST, IntegrationBlockType.PABBLY_CONNECT, IntegrationBlockType.ZAPIER, IntegrationBlockType.MAKE_COM, diff --git a/packages/blocks/core/src/migrations/migrateWebhookBlock.ts b/packages/blocks/core/src/migrations/migrateWebhookBlock.ts index 952e1f4691..a1cd670165 100644 --- a/packages/blocks/core/src/migrations/migrateWebhookBlock.ts +++ b/packages/blocks/core/src/migrations/migrateWebhookBlock.ts @@ -1,16 +1,16 @@ import { HttpMethod, - defaultWebhookAttributes, -} from "@typebot.io/blocks-integrations/webhook/constants"; -import type { HttpRequest } from "@typebot.io/blocks-integrations/webhook/schema"; + defaultHttpRequestAttributes, +} from "@typebot.io/blocks-integrations/httpRequest/constants"; +import type { HttpRequest } from "@typebot.io/blocks-integrations/httpRequest/schema"; import type { Prisma } from "@typebot.io/prisma/types"; -import { isWebhookBlock } from "../helpers"; +import { isHttpRequestBlock } from "../helpers"; import type { BlockV5 } from "../schemas/schema"; export const migrateWebhookBlock = (webhooks: Prisma.Webhook[]) => (block: BlockV5): BlockV5 => { - if (!isWebhookBlock(block)) return block; + if (!isHttpRequestBlock(block)) return block; const webhook = webhooks.find( (webhook) => "webhookId" in block && webhook.id === block.webhookId, ); @@ -31,7 +31,7 @@ export const migrateWebhookBlock = body: webhook.body ?? undefined, } : { - ...defaultWebhookAttributes, + ...defaultHttpRequestAttributes, id: "webhookId" in block ? (block.webhookId ?? "") : "", }, }, diff --git a/packages/blocks/integrations/src/constants.ts b/packages/blocks/integrations/src/constants.ts index cce176ca92..f5b71415a5 100644 --- a/packages/blocks/integrations/src/constants.ts +++ b/packages/blocks/integrations/src/constants.ts @@ -2,7 +2,7 @@ export enum IntegrationBlockType { GOOGLE_SHEETS = "Google Sheets", OPEN_AI = "OpenAI", GOOGLE_ANALYTICS = "Google Analytics", - WEBHOOK = "Webhook", + HTTP_REQUEST = "Webhook", EMAIL = "Email", ZAPIER = "Zapier", MAKE_COM = "Make.com", diff --git a/packages/blocks/integrations/src/webhook/constants.ts b/packages/blocks/integrations/src/httpRequest/constants.ts similarity index 83% rename from packages/blocks/integrations/src/webhook/constants.ts rename to packages/blocks/integrations/src/httpRequest/constants.ts index a1b853f684..845dd6e541 100644 --- a/packages/blocks/integrations/src/webhook/constants.ts +++ b/packages/blocks/integrations/src/httpRequest/constants.ts @@ -12,11 +12,11 @@ export enum HttpMethod { TRACE = "TRACE", } -export const defaultWebhookAttributes = { +export const defaultHttpRequestAttributes = { method: HttpMethod.POST, } as const; -export const defaultWebhookBlockOptions = { +export const defaultHttpRequestBlockOptions = { isAdvancedConfig: false, isCustomBody: false, isExecutedOnClient: false, diff --git a/packages/blocks/integrations/src/webhook/schema.ts b/packages/blocks/integrations/src/httpRequest/schema.ts similarity index 98% rename from packages/blocks/integrations/src/webhook/schema.ts rename to packages/blocks/integrations/src/httpRequest/schema.ts index 80e252af41..87c498f236 100644 --- a/packages/blocks/integrations/src/webhook/schema.ts +++ b/packages/blocks/integrations/src/httpRequest/schema.ts @@ -69,7 +69,7 @@ const httpRequestOptionsSchemas = { const httpBlockV5Schema = blockBaseSchema.merge( z.object({ type: z - .enum([IntegrationBlockType.WEBHOOK]) + .enum([IntegrationBlockType.HTTP_REQUEST]) .describe("Legacy name for HTTP Request block"), options: httpRequestOptionsSchemas.v5.optional(), webhookId: z.string().optional(), diff --git a/packages/blocks/integrations/src/makeCom/schema.ts b/packages/blocks/integrations/src/makeCom/schema.ts index c1ef9f69d8..bf718248c5 100644 --- a/packages/blocks/integrations/src/makeCom/schema.ts +++ b/packages/blocks/integrations/src/makeCom/schema.ts @@ -1,6 +1,6 @@ import { z } from "@typebot.io/zod"; import { IntegrationBlockType } from "../constants"; -import { httpBlockSchemas } from "../webhook/schema"; +import { httpBlockSchemas } from "../httpRequest/schema"; export const makeComBlockSchemas = { v5: httpBlockSchemas.v5.merge( diff --git a/packages/blocks/integrations/src/pabblyConnect/schema.ts b/packages/blocks/integrations/src/pabblyConnect/schema.ts index e89731e3ca..f3fdc581b3 100644 --- a/packages/blocks/integrations/src/pabblyConnect/schema.ts +++ b/packages/blocks/integrations/src/pabblyConnect/schema.ts @@ -1,6 +1,6 @@ import { z } from "@typebot.io/zod"; import { IntegrationBlockType } from "../constants"; -import { httpBlockSchemas } from "../webhook/schema"; +import { httpBlockSchemas } from "../httpRequest/schema"; export const pabblyConnectBlockSchemas = { v5: httpBlockSchemas.v5.merge( diff --git a/packages/blocks/integrations/src/schema.ts b/packages/blocks/integrations/src/schema.ts index a88c11698e..88cdbbcca6 100644 --- a/packages/blocks/integrations/src/schema.ts +++ b/packages/blocks/integrations/src/schema.ts @@ -2,12 +2,12 @@ import { z } from "@typebot.io/zod"; import { chatwootBlockSchema } from "./chatwoot/schema"; import { googleAnalyticsBlockSchema } from "./googleAnalytics/schema"; import { googleSheetsBlockSchemas } from "./googleSheets/schema"; +import { httpBlockSchemas } from "./httpRequest/schema"; import { makeComBlockSchemas } from "./makeCom/schema"; import { openAIBlockSchema } from "./openai/schema"; import { pabblyConnectBlockSchemas } from "./pabblyConnect/schema"; import { pixelBlockSchema } from "./pixel/schema"; import { sendEmailBlockSchema } from "./sendEmail/schema"; -import { httpBlockSchemas } from "./webhook/schema"; import { zapierBlockSchemas } from "./zapier/schema"; const integrationBlockSchemas = [ diff --git a/packages/blocks/integrations/src/zapier/schema.ts b/packages/blocks/integrations/src/zapier/schema.ts index f7f81421a9..61aaa1fe44 100644 --- a/packages/blocks/integrations/src/zapier/schema.ts +++ b/packages/blocks/integrations/src/zapier/schema.ts @@ -1,6 +1,6 @@ import { z } from "@typebot.io/zod"; import { IntegrationBlockType } from "../constants"; -import { httpBlockSchemas } from "../webhook/schema"; +import { httpBlockSchemas } from "../httpRequest/schema"; export const zapierBlockSchemas = { v5: httpBlockSchemas.v5.merge( diff --git a/packages/blocks/logic/src/constants.ts b/packages/blocks/logic/src/constants.ts index 360d4b67a2..339a7d1299 100644 --- a/packages/blocks/logic/src/constants.ts +++ b/packages/blocks/logic/src/constants.ts @@ -7,4 +7,5 @@ export enum LogicBlockType { WAIT = "Wait", JUMP = "Jump", AB_TEST = "AB test", + WEBHOOK = "webhook", } diff --git a/packages/blocks/logic/src/schema.ts b/packages/blocks/logic/src/schema.ts index 51bb6b22d4..81e4110997 100644 --- a/packages/blocks/logic/src/schema.ts +++ b/packages/blocks/logic/src/schema.ts @@ -7,6 +7,7 @@ import { scriptBlockSchema } from "./script/schema"; import { setVariableBlockSchema } from "./setVariable/schema"; import { typebotLinkBlockSchema } from "./typebotLink/schema"; import { waitBlockSchema } from "./wait/schema"; +import { webhookBlockSchema } from "./webhook/schema"; const logicBlockSchemas = [ scriptBlockSchema, @@ -28,6 +29,7 @@ export const logicBlockV6Schema = z.discriminatedUnion("type", [ ...logicBlockSchemas, conditionBlockSchemas.v6, abTestBlockSchemas.v6, + webhookBlockSchema, ]); export type LogicBlockV6 = z.infer; diff --git a/packages/blocks/logic/src/webhook/schema.ts b/packages/blocks/logic/src/webhook/schema.ts new file mode 100644 index 0000000000..40dbe4d841 --- /dev/null +++ b/packages/blocks/logic/src/webhook/schema.ts @@ -0,0 +1,29 @@ +import { blockBaseSchema } from "@typebot.io/blocks-base/schemas"; +import { z } from "@typebot.io/zod"; +import { LogicBlockType } from "../constants"; + +export const waitOptionsSchema = z.object({ + responseVariableMapping: z + .array( + z.object({ + id: z.string(), + variableId: z.string().optional(), + bodyPath: z.string().optional(), + }), + ) + .optional(), +}); + +export const webhookBlockSchema = blockBaseSchema + .merge( + z.object({ + type: z.enum([LogicBlockType.WEBHOOK]), + options: waitOptionsSchema.optional(), + }), + ) + .openapi({ + title: "Webhook", + ref: "webhookLogic", + }); + +export type WebhookBlock = z.infer; diff --git a/packages/bot-engine/src/apiHandlers/continueChat.ts b/packages/bot-engine/src/apiHandlers/continueChat.ts index 25a1d517ed..f3c6dadc4f 100644 --- a/packages/bot-engine/src/apiHandlers/continueChat.ts +++ b/packages/bot-engine/src/apiHandlers/continueChat.ts @@ -79,7 +79,7 @@ export const continueChat = async ({ clientSideActions, visitedEdges, setVariableHistory, - hasEmbedBubbleWithWaitEvent: messages.some( + isWaitingForExternalEvent: messages.some( (message) => message.type === "custom-embed" || (message.type === BubbleBlockType.EMBED && diff --git a/packages/bot-engine/src/apiHandlers/startChat.ts b/packages/bot-engine/src/apiHandlers/startChat.ts index 2e77b71162..98c55ba3af 100644 --- a/packages/bot-engine/src/apiHandlers/startChat.ts +++ b/packages/bot-engine/src/apiHandlers/startChat.ts @@ -76,7 +76,7 @@ export const startChat = async ({ clientSideActions, visitedEdges, setVariableHistory, - hasEmbedBubbleWithWaitEvent: messages.some( + isWaitingForExternalEvent: messages.some( (message) => message.type === "custom-embed" || (message.type === BubbleBlockType.EMBED && diff --git a/packages/bot-engine/src/apiHandlers/startChatPreview.ts b/packages/bot-engine/src/apiHandlers/startChatPreview.ts index 6810b7bc44..fdffc793d1 100644 --- a/packages/bot-engine/src/apiHandlers/startChatPreview.ts +++ b/packages/bot-engine/src/apiHandlers/startChatPreview.ts @@ -70,7 +70,7 @@ export const startChatPreview = async ({ clientSideActions, visitedEdges, setVariableHistory, - hasEmbedBubbleWithWaitEvent: messages.some( + isWaitingForExternalEvent: messages.some( (message) => message.type === "custom-embed" || (message.type === BubbleBlockType.EMBED && diff --git a/packages/bot-engine/src/blocks/integrations/webhook/executeWebhookBlock.ts b/packages/bot-engine/src/blocks/integrations/httpRequest/executeHttpRequestBlock.ts similarity index 92% rename from packages/bot-engine/src/blocks/integrations/webhook/executeWebhookBlock.ts rename to packages/bot-engine/src/blocks/integrations/httpRequest/executeHttpRequestBlock.ts index 1ce3528155..508071bb2a 100644 --- a/packages/bot-engine/src/blocks/integrations/webhook/executeWebhookBlock.ts +++ b/packages/bot-engine/src/blocks/integrations/httpRequest/executeHttpRequestBlock.ts @@ -1,18 +1,18 @@ -import type { MakeComBlock } from "@typebot.io/blocks-integrations/makeCom/schema"; -import type { PabblyConnectBlock } from "@typebot.io/blocks-integrations/pabblyConnect/schema"; import { HttpMethod, + defaultHttpRequestAttributes, defaultTimeout, - defaultWebhookAttributes, maxTimeout, -} from "@typebot.io/blocks-integrations/webhook/constants"; +} from "@typebot.io/blocks-integrations/httpRequest/constants"; import type { ExecutableHttpRequest, HttpRequest, HttpRequestBlock, HttpResponse, KeyValue, -} from "@typebot.io/blocks-integrations/webhook/schema"; +} from "@typebot.io/blocks-integrations/httpRequest/schema"; +import type { MakeComBlock } from "@typebot.io/blocks-integrations/makeCom/schema"; +import type { PabblyConnectBlock } from "@typebot.io/blocks-integrations/pabblyConnect/schema"; import type { ZapierBlock } from "@typebot.io/blocks-integrations/zapier/schema"; import { env } from "@typebot.io/env"; import { JSONParse } from "@typebot.io/lib/JSONParse"; @@ -30,7 +30,7 @@ import type { TypebotInSession, } from "../../../schemas/chatSession"; import type { ExecuteIntegrationResponse } from "../../../types"; -import { resumeWebhookExecution } from "./resumeWebhookExecution"; +import { saveDataInResponseVariableMapping } from "./saveDataInResponseVariableMapping"; type ParsedWebhook = ExecutableHttpRequest & { basicAuth: { username?: string; password?: string }; @@ -50,7 +50,7 @@ export const webhookErrorDescription = `Webhook returned an error.`; type Params = { disableRequestTimeout?: boolean; timeout?: number }; -export const executeWebhookBlock = async ( +export const executeHttpRequestBlock = async ( state: SessionState, block: HttpRequestBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock, params: Params = {}, @@ -64,13 +64,13 @@ export const executeWebhookBlock = async ( })) as HttpRequest | null) : null); if (!webhook) return { outgoingEdgeId: block.outgoingEdgeId }; - const parsedWebhook = await parseWebhookAttributes({ + const parsedHttpRequest = await parseWebhookAttributes({ webhook, isCustomBody: block.options?.isCustomBody, typebot: state.typebotsQueue[0].typebot, answers: state.typebotsQueue[0].answers, }); - if (!parsedWebhook) { + if (!parsedHttpRequest) { logs.push({ status: "error", description: `Couldn't parse webhook attributes`, @@ -82,8 +82,8 @@ export const executeWebhookBlock = async ( outgoingEdgeId: block.outgoingEdgeId, clientSideActions: [ { - type: "webhookToExecute", - webhookToExecute: parsedWebhook, + type: "httpRequestToExecute", + httpRequestToExecute: parsedHttpRequest, expectsDedicatedReply: true, }, ], @@ -92,15 +92,18 @@ export const executeWebhookBlock = async ( response: webhookResponse, logs: executeWebhookLogs, startTimeShouldBeUpdated, - } = await executeWebhook(parsedWebhook, { + } = await executeHttpRequest(parsedHttpRequest, { ...params, timeout: block.options?.timeout, }); return { - ...resumeWebhookExecution({ + ...saveDataInResponseVariableMapping({ state, - block, + blockType: block.type, + blockId: block.id, + responseVariableMapping: block.options?.responseVariableMapping, + outgoingEdgeId: block.outgoingEdgeId, logs: executeWebhookLogs, response: webhookResponse, }), @@ -153,7 +156,7 @@ export const parseWebhookAttributes = async ({ variables: typebot.variables, isCustomBody, }); - const method = webhook.method ?? defaultWebhookAttributes.method; + const method = webhook.method ?? defaultHttpRequestAttributes.method; const { data: body, isJson } = bodyContent && method !== HttpMethod.GET ? safeJsonParse( @@ -175,7 +178,7 @@ export const parseWebhookAttributes = async ({ }; }; -export const executeWebhook = async ( +export const executeHttpRequest = async ( webhook: ParsedWebhook, params: Params = {}, ): Promise<{ diff --git a/packages/bot-engine/src/blocks/integrations/webhook/parseSampleResult.ts b/packages/bot-engine/src/blocks/integrations/httpRequest/parseSampleResult.ts similarity index 100% rename from packages/bot-engine/src/blocks/integrations/webhook/parseSampleResult.ts rename to packages/bot-engine/src/blocks/integrations/httpRequest/parseSampleResult.ts diff --git a/packages/bot-engine/src/blocks/integrations/webhook/resumeWebhookExecution.ts b/packages/bot-engine/src/blocks/integrations/httpRequest/saveDataInResponseVariableMapping.ts similarity index 61% rename from packages/bot-engine/src/blocks/integrations/webhook/resumeWebhookExecution.ts rename to packages/bot-engine/src/blocks/integrations/httpRequest/saveDataInResponseVariableMapping.ts index 571248b656..64c3225955 100644 --- a/packages/bot-engine/src/blocks/integrations/webhook/resumeWebhookExecution.ts +++ b/packages/bot-engine/src/blocks/integrations/httpRequest/saveDataInResponseVariableMapping.ts @@ -1,7 +1,5 @@ -import type { MakeComBlock } from "@typebot.io/blocks-integrations/makeCom/schema"; -import type { PabblyConnectBlock } from "@typebot.io/blocks-integrations/pabblyConnect/schema"; -import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/webhook/schema"; -import type { ZapierBlock } from "@typebot.io/blocks-integrations/zapier/schema"; +import type { IntegrationBlockType } from "@typebot.io/blocks-integrations/constants"; +import { LogicBlockType } from "@typebot.io/blocks-logic/constants"; import { byId } from "@typebot.io/lib/utils"; import { createHttpReqResponseMappingRunner } from "@typebot.io/variables/codeRunners"; import { parseVariables } from "@typebot.io/variables/parseVariables"; @@ -13,46 +11,65 @@ import { updateVariablesInSession } from "../../../updateVariablesInSession"; type Props = { state: SessionState; - block: HttpRequestBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock; + blockType: + | LogicBlockType.WEBHOOK + | IntegrationBlockType.HTTP_REQUEST + | IntegrationBlockType.ZAPIER + | IntegrationBlockType.MAKE_COM + | IntegrationBlockType.PABBLY_CONNECT; + blockId: string; + responseVariableMapping?: { + bodyPath?: string; + variableId?: string; + }[]; + outgoingEdgeId?: string; logs?: ChatLog[]; response: { - statusCode: number; + statusCode?: number; data?: unknown; }; }; -export const resumeWebhookExecution = ({ +export const saveDataInResponseVariableMapping = ({ state, - block, + blockType, + blockId, + responseVariableMapping, + outgoingEdgeId, logs = [], response, }: Props): ExecuteIntegrationResponse => { const { typebot } = state.typebotsQueue[0]; - const status = response.statusCode.toString(); - const isError = status.startsWith("4") || status.startsWith("5"); + const status = response.statusCode?.toString(); + const isError = status + ? status.startsWith("4") || status.startsWith("5") + : false; const responseFromClient = logs.length === 0; - if (responseFromClient) + if (responseFromClient) { + const blockLabel = + blockType === LogicBlockType.WEBHOOK ? "Webhook" : "HTTP request"; logs.push( isError ? { status: "error", - description: `Webhook returned error`, + description: `${blockLabel} returned error`, details: response.data, } : { status: "success", - description: `Webhook executed successfully!`, + description: `${blockLabel} executed successfully!`, details: response.data, }, ); + } - let run: (varMapping: string) => unknown; - if (block.options?.responseVariableMapping) { + let run: ((varMapping: string) => unknown) | undefined; + if (responseVariableMapping) { run = createHttpReqResponseMappingRunner(response); } - const newVariables = block.options?.responseVariableMapping?.reduce< + const newVariables = responseVariableMapping?.reduce< VariableWithUnknowValue[] >((newVariables, varMapping) => { if (!varMapping?.bodyPath || !varMapping.variableId || !run) @@ -75,10 +92,10 @@ export const resumeWebhookExecution = ({ const { updatedState, newSetVariableHistory } = updateVariablesInSession({ newVariables, state, - currentBlockId: block.id, + currentBlockId: blockId, }); return { - outgoingEdgeId: block.outgoingEdgeId, + outgoingEdgeId, newSessionState: updatedState, newSetVariableHistory, logs, @@ -86,7 +103,7 @@ export const resumeWebhookExecution = ({ } return { - outgoingEdgeId: block.outgoingEdgeId, + outgoingEdgeId, logs, }; }; diff --git a/packages/bot-engine/src/blocks/logic/webhook/executeWebhookBlock.ts b/packages/bot-engine/src/blocks/logic/webhook/executeWebhookBlock.ts new file mode 100644 index 0000000000..eb422fcbd6 --- /dev/null +++ b/packages/bot-engine/src/blocks/logic/webhook/executeWebhookBlock.ts @@ -0,0 +1,14 @@ +import type { WebhookBlock } from "@typebot.io/blocks-logic/webhook/schema"; +import type { ExecuteLogicResponse } from "../../../types"; + +export const executeWebhookBlock = ( + block: WebhookBlock, +): ExecuteLogicResponse => ({ + outgoingEdgeId: block.outgoingEdgeId, + clientSideActions: [ + { + type: "listenForWebhook", + expectsDedicatedReply: true, + }, + ], +}); diff --git a/packages/bot-engine/src/continueBotFlow.ts b/packages/bot-engine/src/continueBotFlow.ts index 5ea2cf70fd..54e90e989c 100644 --- a/packages/bot-engine/src/continueBotFlow.ts +++ b/packages/bot-engine/src/continueBotFlow.ts @@ -20,6 +20,7 @@ import type { ForgedBlock } from "@typebot.io/forge-repository/schemas"; import { getBlockById } from "@typebot.io/groups/helpers"; import type { Group } from "@typebot.io/groups/schemas"; import { isURL } from "@typebot.io/lib/isURL"; +import { stringifyError } from "@typebot.io/lib/stringifyError"; import { byId, isDefined } from "@typebot.io/lib/utils"; import type { Prisma } from "@typebot.io/prisma/types"; import type { AnswerInSessionState } from "@typebot.io/results/schemas/answers"; @@ -36,8 +37,8 @@ import { validateNumber } from "./blocks/inputs/number/validateNumber"; import { formatPhoneNumber } from "./blocks/inputs/phone/formatPhoneNumber"; import { parsePictureChoicesReply } from "./blocks/inputs/pictureChoice/parsePictureChoicesReply"; import { validateRatingReply } from "./blocks/inputs/rating/validateRatingReply"; +import { saveDataInResponseVariableMapping } from "./blocks/integrations/httpRequest/saveDataInResponseVariableMapping"; import { resumeChatCompletion } from "./blocks/integrations/legacy/openai/resumeChatCompletion"; -import { resumeWebhookExecution } from "./blocks/integrations/webhook/resumeWebhookExecution"; import { executeGroup, parseInput } from "./executeGroup"; import { getNextGroup } from "./getNextGroup"; import { saveAnswer } from "./queries/saveAnswer"; @@ -240,11 +241,31 @@ const processNonInputBlock = async ({ })(reply.text); newSessionState = result.newSessionState; } - } else if (reply && block.type === IntegrationBlockType.WEBHOOK) { - const result = resumeWebhookExecution({ + } else if ( + reply && + (block.type === IntegrationBlockType.HTTP_REQUEST || + block.type === LogicBlockType.WEBHOOK) + ) { + let response: { + statusCode?: number; + data?: unknown; + }; + try { + response = JSON.parse(reply.text); + } catch (err) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Provided response is not valid JSON", + cause: stringifyError(err), + }); + } + const result = saveDataInResponseVariableMapping({ state, - block, - response: JSON.parse(reply.text), + blockType: block.type, + blockId: block.id, + responseVariableMapping: block.options?.responseVariableMapping, + outgoingEdgeId: block.outgoingEdgeId, + response, }); if (result.newSessionState) newSessionState = result.newSessionState; } else if (isForgedBlockType(block.type)) { diff --git a/packages/bot-engine/src/executeIntegration.ts b/packages/bot-engine/src/executeIntegration.ts index 26c5c667d8..adcdec3df9 100644 --- a/packages/bot-engine/src/executeIntegration.ts +++ b/packages/bot-engine/src/executeIntegration.ts @@ -4,11 +4,11 @@ import { env } from "@typebot.io/env"; import { isNotDefined } from "@typebot.io/lib/utils"; import { executeChatwootBlock } from "./blocks/integrations/chatwoot/executeChatwootBlock"; import { executeGoogleSheetBlock } from "./blocks/integrations/googleSheets/executeGoogleSheetBlock"; +import { executeHttpRequestBlock } from "./blocks/integrations/httpRequest/executeHttpRequestBlock"; import { executeGoogleAnalyticsBlock } from "./blocks/integrations/legacy/googleAnalytics/executeGoogleAnalyticsBlock"; import { executeOpenAIBlock } from "./blocks/integrations/legacy/openai/executeOpenAIBlock"; import { executePixelBlock } from "./blocks/integrations/pixel/executePixelBlock"; import { executeSendEmailBlock } from "./blocks/integrations/sendEmail/executeSendEmailBlock"; -import { executeWebhookBlock } from "./blocks/integrations/webhook/executeWebhookBlock"; import { executeForgedBlock } from "./forge/executeForgedBlock"; import type { SessionState } from "./schemas/chatSession"; import type { ExecuteIntegrationResponse } from "./types"; @@ -32,14 +32,14 @@ export const executeIntegration = case IntegrationBlockType.MAKE_COM: case IntegrationBlockType.PABBLY_CONNECT: return { - ...(await executeWebhookBlock(state, block, { + ...(await executeHttpRequestBlock(state, block, { disableRequestTimeout: true, })), startTimeShouldBeUpdated: true, }; - case IntegrationBlockType.WEBHOOK: + case IntegrationBlockType.HTTP_REQUEST: return { - ...(await executeWebhookBlock(state, block, { + ...(await executeHttpRequestBlock(state, block, { disableRequestTimeout: isNotDefined(env.CHAT_API_TIMEOUT), })), }; diff --git a/packages/bot-engine/src/executeLogic.ts b/packages/bot-engine/src/executeLogic.ts index 0d928c739f..d8669e6246 100644 --- a/packages/bot-engine/src/executeLogic.ts +++ b/packages/bot-engine/src/executeLogic.ts @@ -9,6 +9,7 @@ import { executeScript } from "./blocks/logic/script/executeScript"; import { executeSetVariable } from "./blocks/logic/setVariable/executeSetVariable"; import { executeTypebotLink } from "./blocks/logic/typebotLink/executeTypebotLink"; import { executeWait } from "./blocks/logic/wait/executeWait"; +import { executeWebhookBlock } from "./blocks/logic/webhook/executeWebhookBlock"; import type { SessionState } from "./schemas/chatSession"; import type { ExecuteLogicResponse } from "./types"; @@ -35,5 +36,7 @@ export const executeLogic = return executeJumpBlock(state, block.options); case LogicBlockType.AB_TEST: return executeAbTest(state, block); + case LogicBlockType.WEBHOOK: + return executeWebhookBlock(block); } }; diff --git a/packages/bot-engine/src/logs/filterPotentiallySensitiveLogs.ts b/packages/bot-engine/src/logs/filterPotentiallySensitiveLogs.ts index ddf9753fe0..98a882d986 100644 --- a/packages/bot-engine/src/logs/filterPotentiallySensitiveLogs.ts +++ b/packages/bot-engine/src/logs/filterPotentiallySensitiveLogs.ts @@ -1,11 +1,11 @@ +import { + webhookErrorDescription, + webhookSuccessDescription, +} from "../blocks/integrations/httpRequest/executeHttpRequestBlock"; import { sendEmailErrorDescription, sendEmailSuccessDescription, } from "../blocks/integrations/sendEmail/executeSendEmailBlock"; -import { - webhookErrorDescription, - webhookSuccessDescription, -} from "../blocks/integrations/webhook/executeWebhookBlock"; export const filterPotentiallySensitiveLogs = (log: { status: string; diff --git a/packages/bot-engine/src/saveStateToDatabase.ts b/packages/bot-engine/src/saveStateToDatabase.ts index 97b07e5915..7ff84408c5 100644 --- a/packages/bot-engine/src/saveStateToDatabase.ts +++ b/packages/bot-engine/src/saveStateToDatabase.ts @@ -8,24 +8,24 @@ import { upsertResult } from "./queries/upsertResult"; import type { ChatSession, ContinueChatResponse } from "./schemas/api"; type Props = { - session: Pick & { id?: string }; + session: Pick & { id?: string; isReplying?: boolean }; input: ContinueChatResponse["input"]; logs: ContinueChatResponse["logs"]; clientSideActions: ContinueChatResponse["clientSideActions"]; visitedEdges: Prisma.VisitedEdge[]; setVariableHistory: SetVariableHistoryItem[]; - hasEmbedBubbleWithWaitEvent?: boolean; + isWaitingForExternalEvent?: boolean; initialSessionId?: string; }; export const saveStateToDatabase = async ({ - session: { state, id }, + session: { state, id, isReplying }, input, logs, clientSideActions, visitedEdges, setVariableHistory, - hasEmbedBubbleWithWaitEvent, + isWaitingForExternalEvent, initialSessionId, }: Props) => { const containsSetVariableClientSideAction = clientSideActions?.some( @@ -35,7 +35,7 @@ export const saveStateToDatabase = async ({ const isCompleted = Boolean( !input && !containsSetVariableClientSideAction && - !hasEmbedBubbleWithWaitEvent, + !isWaitingForExternalEvent, ); const queries: Prisma.PrismaPromise[] = []; @@ -44,12 +44,19 @@ export const saveStateToDatabase = async ({ if (id) { if (isCompleted && resultId) queries.push(deleteSession(id)); - else queries.push(updateSession({ id, state, isReplying: false })); + else + queries.push( + updateSession({ id, state, isReplying: isReplying ?? false }), + ); } const session = id ? { state, id } - : await createSession({ id: initialSessionId, state, isReplying: false }); + : await createSession({ + id: initialSessionId, + state, + isReplying: isReplying ?? false, + }); if (!resultId) { if (queries.length > 0) await prisma.$transaction(queries); diff --git a/packages/bot-engine/src/schemas/api.ts b/packages/bot-engine/src/schemas/api.ts index 665d108d6a..39e46090b5 100644 --- a/packages/bot-engine/src/schemas/api.ts +++ b/packages/bot-engine/src/schemas/api.ts @@ -76,7 +76,7 @@ const chatSessionSchema = z.object({ .describe( "Used in WhatsApp runtime to avoid concurrent replies from the bot", ), -}) satisfies z.ZodType; +}); export type ChatSession = z.infer; const textMessageSchema = z diff --git a/packages/bot-engine/src/schemas/clientSideAction.ts b/packages/bot-engine/src/schemas/clientSideAction.ts index 1e14d43a25..d586ca1464 100644 --- a/packages/bot-engine/src/schemas/clientSideAction.ts +++ b/packages/bot-engine/src/schemas/clientSideAction.ts @@ -1,7 +1,7 @@ import { googleAnalyticsOptionsSchema } from "@typebot.io/blocks-integrations/googleAnalytics/schema"; +import { executableHttpRequestSchema } from "@typebot.io/blocks-integrations/httpRequest/schema"; import { nativeMessageSchema } from "@typebot.io/blocks-integrations/openai/schema"; import { pixelOptionsSchema } from "@typebot.io/blocks-integrations/pixel/schema"; -import { executableHttpRequestSchema } from "@typebot.io/blocks-integrations/webhook/schema"; import { redirectOptionsSchema } from "@typebot.io/blocks-logic/redirect/schema"; import { listVariableValue } from "@typebot.io/variables/schemas"; import { z } from "@typebot.io/zod"; @@ -115,13 +115,13 @@ export const clientSideActionSchema = z.discriminatedUnion("type", [ }), z .object({ - type: z.literal("webhookToExecute"), - webhookToExecute: executableHttpRequestSchema, + type: z.literal("httpRequestToExecute"), + httpRequestToExecute: executableHttpRequestSchema, }) .merge(clientSideActionBaseSchema) .openapi({ - ref: "csaExecWebhook", - title: "Execute webhook", + ref: "csaHttpRequestToExecute", + title: "Execute HTTP request", }), z .object({ @@ -166,4 +166,14 @@ export const clientSideActionSchema = z.discriminatedUnion("type", [ ref: "csaCodeToExecute", title: "Execute code", }), + z + .object({ + type: z.literal("listenForWebhook"), + }) + .merge(clientSideActionBaseSchema) + .openapi({ + ref: "csaListenForWebhook", + title: "Listen to webhook", + }), ]); +export type ClientSideAction = z.infer; diff --git a/packages/deprecated/bot-engine/src/features/blocks/integrations/webhook/utils/executeWebhookBlock.ts b/packages/deprecated/bot-engine/src/features/blocks/integrations/webhook/utils/executeWebhookBlock.ts index f78f8498ad..33f24b4004 100644 --- a/packages/deprecated/bot-engine/src/features/blocks/integrations/webhook/utils/executeWebhookBlock.ts +++ b/packages/deprecated/bot-engine/src/features/blocks/integrations/webhook/utils/executeWebhookBlock.ts @@ -1,8 +1,8 @@ import { parseVariables } from "@/features/variables"; import type { IntegrationState } from "@/types"; +import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/httpRequest/schema"; import type { MakeComBlock } from "@typebot.io/blocks-integrations/makeCom/schema"; import type { PabblyConnectBlock } from "@typebot.io/blocks-integrations/pabblyConnect/schema"; -import type { HttpRequestBlock } from "@typebot.io/blocks-integrations/webhook/schema"; import type { ZapierBlock } from "@typebot.io/blocks-integrations/zapier/schema"; import { byId, sendRequest } from "@typebot.io/lib/utils"; import type { VariableWithUnknowValue } from "@typebot.io/variables/schemas"; diff --git a/packages/deprecated/bot-engine/src/utils/executeIntegration.ts b/packages/deprecated/bot-engine/src/utils/executeIntegration.ts index 4b1d350559..8d6946f6e1 100644 --- a/packages/deprecated/bot-engine/src/utils/executeIntegration.ts +++ b/packages/deprecated/bot-engine/src/utils/executeIntegration.ts @@ -22,7 +22,7 @@ export const executeIntegration = ({ case IntegrationBlockType.ZAPIER: case IntegrationBlockType.MAKE_COM: case IntegrationBlockType.PABBLY_CONNECT: - case IntegrationBlockType.WEBHOOK: + case IntegrationBlockType.HTTP_REQUEST: return executeWebhook(block, context); case IntegrationBlockType.EMAIL: return executeSendEmailBlock(block, context); diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 68c3ecd047..f206a0214e 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.3.17", + "version": "0.3.18", "description": "Javascript library to display typebots on your website", "license": "AGPL-3.0-or-later", "type": "module", @@ -39,7 +39,8 @@ "ky": "1.2.4", "marked": "9.0.3", "solid-element": "1.7.1", - "solid-js": "1.7.8" + "solid-js": "1.7.8", + "partysocket": "1.0.2" }, "devDependencies": { "@babel/preset-typescript": "7.24.7", diff --git a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx index 1adc1096f5..5c8f98a92f 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -284,6 +284,7 @@ export const ConversationContainer = (props: Props) => { context: { apiHost: props.context.apiHost, sessionId: props.initialChatReply.sessionId, + resultId: props.initialChatReply.resultId, }, onMessageStream: streamMessage, }); diff --git a/packages/embeds/js/src/features/blocks/integrations/webhook/executeWebhook.ts b/packages/embeds/js/src/features/blocks/integrations/httpRequest/executeHttpRequest.ts similarity index 75% rename from packages/embeds/js/src/features/blocks/integrations/webhook/executeWebhook.ts rename to packages/embeds/js/src/features/blocks/integrations/httpRequest/executeHttpRequest.ts index 813f90b565..4f9aec8ec7 100644 --- a/packages/embeds/js/src/features/blocks/integrations/webhook/executeWebhook.ts +++ b/packages/embeds/js/src/features/blocks/integrations/httpRequest/executeHttpRequest.ts @@ -1,9 +1,9 @@ -import type { ExecutableHttpRequest } from "@typebot.io/blocks-integrations/webhook/schema"; +import type { ExecutableHttpRequest } from "@typebot.io/blocks-integrations/httpRequest/schema"; -export const executeWebhook = async ( - webhookToExecute: ExecutableHttpRequest, +export const executeHttpRequest = async ( + httpRequestToExecute: ExecutableHttpRequest, ): Promise => { - const { url, method, body, headers } = webhookToExecute; + const { url, method, body, headers } = httpRequestToExecute; try { const response = await fetch(url, { method, diff --git a/packages/embeds/js/src/features/blocks/logic/script/executeScript.ts b/packages/embeds/js/src/features/blocks/logic/script/executeScript.ts index 8d9fd93403..f352b54666 100644 --- a/packages/embeds/js/src/features/blocks/logic/script/executeScript.ts +++ b/packages/embeds/js/src/features/blocks/logic/script/executeScript.ts @@ -15,7 +15,6 @@ export const executeScript = async ({ parseContent(content), ); await func(...args.map((arg) => arg.value)); - console.log(parseContent(content)); } catch (err) { console.log(err); return { diff --git a/packages/embeds/js/src/features/blocks/logic/webhook/listenForWebhook.ts b/packages/embeds/js/src/features/blocks/logic/webhook/listenForWebhook.ts new file mode 100644 index 0000000000..6915d4516a --- /dev/null +++ b/packages/embeds/js/src/features/blocks/logic/webhook/listenForWebhook.ts @@ -0,0 +1,45 @@ +import type { ChatLog } from "@typebot.io/bot-engine/schemas/api"; +import { getRuntimeVariable } from "@typebot.io/env/getRuntimeVariable"; +import PartySocket from "partysocket"; + +type Props = { + resultId?: string; + sessionId: string; +}; + +export const listenForWebhook = ({ sessionId, resultId }: Props) => { + const host = getRuntimeVariable("NEXT_PUBLIC_PARTYKIT_HOST"); + if (!host) return; + + const ws = new PartySocket({ + host, + room: getRoomName({ sessionId, resultId }), + }); + return new Promise<{ replyToSend: string | undefined; logs?: ChatLog[] }>( + (resolve) => { + ws.addEventListener("message", (event) => { + ws.close(); + resolve({ replyToSend: event.data }); + }); + + ws.addEventListener("error", (error) => { + resolve({ + logs: [ + { + status: "error", + description: "Websocket returned an error", + details: JSON.stringify(error, null, 2), + }, + ], + replyToSend: undefined, + }); + }); + }, + ); +}; + +const getRoomName = ({ sessionId, resultId }: Props) => { + if (resultId) return `${resultId}/webhooks`; + const [typebotId, userId] = sessionId.split("-"); + return `${userId}/${typebotId}/webhooks`; +}; diff --git a/packages/embeds/js/src/queries/startChatQuery.ts b/packages/embeds/js/src/queries/startChatQuery.ts index 5667c2258a..fa859e99de 100644 --- a/packages/embeds/js/src/queries/startChatQuery.ts +++ b/packages/embeds/js/src/queries/startChatQuery.ts @@ -49,65 +49,22 @@ export async function startChatQuery({ }) : undefined; if (paymentInProgressState) { - removePaymentInProgressFromStorage(); - - try { - const data = await ky - .post( - `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sessions/${ - paymentInProgressState.sessionId - }/continueChat`, - { - json: { - message: paymentInProgressState - ? stripeRedirectStatus === "failed" - ? "fail" - : "Success" - : undefined, - }, - timeout: false, - }, - ) - .json(); - - return { - data: { - ...data, - ...paymentInProgressState, - } satisfies StartChatResponse, - }; - } catch (error) { - return { error }; - } + return resumeChatAfterPaymentRedirect({ + apiHost, + stripeRedirectStatus, + paymentInProgressState, + }); } const typebotId = typeof typebot === "string" ? typebot : typebot.id; if (isPreview) { - try { - const data = await ky - .post( - `${ - isNotEmpty(apiHost) ? apiHost : guessApiHost() - }/api/v1/typebots/${typebotId}/preview/startChat`, - { - json: { - isStreamEnabled: true, - startFrom, - typebot, - prefilledVariables, - sessionId, - } satisfies Omit< - StartPreviewChatInput, - "typebotId" | "isOnlyRegistering" | "textBubbleContentFormat" - >, - timeout: false, - }, - ) - .json(); - - return { data }; - } catch (error) { - return { error }; - } + return startPreviewChat({ + apiHost, + typebotId, + startFrom, + typebot, + prefilledVariables, + sessionId, + }); } try { @@ -116,9 +73,7 @@ export async function startChatQuery({ ? new URL(document.referrer).origin : undefined; const response = await ky.post( - `${ - isNotEmpty(apiHost) ? apiHost : guessApiHost() - }/api/v1/typebots/${typebotId}/startChat`, + `${getApiHost(apiHost)}/api/v1/typebots/${typebotId}/startChat`, { headers: { "x-typebot-iframe-referrer-origin": iframeReferrerOrigin, @@ -151,3 +106,87 @@ export async function startChatQuery({ return { error }; } } + +const resumeChatAfterPaymentRedirect = async ({ + apiHost, + stripeRedirectStatus, + paymentInProgressState, +}: { + apiHost?: string; + stripeRedirectStatus?: string; + paymentInProgressState: { + sessionId: string; + typebot: BotContext["typebot"]; + }; +}) => { + removePaymentInProgressFromStorage(); + + try { + const data = await ky + .post( + `${getApiHost(apiHost)}/api/v1/sessions/${ + paymentInProgressState.sessionId + }/continueChat`, + { + json: { + message: stripeRedirectStatus === "failed" ? "fail" : "Success", + }, + timeout: false, + }, + ) + .json(); + + return { + data: { + ...data, + ...paymentInProgressState, + } as StartChatResponse, + }; + } catch (error) { + return { error }; + } +}; + +const startPreviewChat = async ({ + apiHost, + typebotId, + startFrom, + typebot, + prefilledVariables, + sessionId, +}: { + apiHost?: string; + typebotId: string; + startFrom?: StartFrom; + typebot: StartPreviewChatInput["typebot"]; + prefilledVariables?: Record; + sessionId?: string; +}) => { + try { + const data = await ky + .post( + `${getApiHost(apiHost)}/api/v1/typebots/${typebotId}/preview/startChat`, + { + json: { + isStreamEnabled: true, + startFrom, + typebot, + prefilledVariables, + sessionId, + } satisfies Omit< + StartPreviewChatInput, + "typebotId" | "isOnlyRegistering" | "textBubbleContentFormat" + >, + timeout: false, + }, + ) + .json(); + + return { data }; + } catch (error) { + return { error }; + } +}; + +const getApiHost = (apiHost?: string): string => + isNotEmpty(apiHost) ? apiHost : guessApiHost(); diff --git a/packages/embeds/js/src/types.ts b/packages/embeds/js/src/types.ts index 0531870e4f..eeda1c21d5 100644 --- a/packages/embeds/js/src/types.ts +++ b/packages/embeds/js/src/types.ts @@ -21,6 +21,7 @@ export type OutgoingLog = { export type ClientSideActionContext = { apiHost?: string; sessionId: string; + resultId?: string; }; export type ChatChunk = Pick< diff --git a/packages/embeds/js/src/utils/executeClientSideActions.ts b/packages/embeds/js/src/utils/executeClientSideActions.ts index e0c6388d58..e79911bf44 100644 --- a/packages/embeds/js/src/utils/executeClientSideActions.ts +++ b/packages/embeds/js/src/utils/executeClientSideActions.ts @@ -1,8 +1,8 @@ import { executeChatwoot } from "@/features/blocks/integrations/chatwoot/utils/executeChatwoot"; import { executeGoogleAnalyticsBlock } from "@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics"; +import { executeHttpRequest } from "@/features/blocks/integrations/httpRequest/executeHttpRequest"; import { streamChat } from "@/features/blocks/integrations/openai/streamChat"; import { executePixel } from "@/features/blocks/integrations/pixel/executePixel"; -import { executeWebhook } from "@/features/blocks/integrations/webhook/executeWebhook"; import { executeRedirect } from "@/features/blocks/logic/redirect/utils/executeRedirect"; import { executeCode, @@ -10,6 +10,7 @@ import { } from "@/features/blocks/logic/script/executeScript"; import { executeSetVariable } from "@/features/blocks/logic/setVariable/executeSetVariable"; import { executeWait } from "@/features/blocks/logic/wait/utils/executeWait"; +import { listenForWebhook } from "@/features/blocks/logic/webhook/listenForWebhook"; import type { ClientSideActionContext } from "@/types"; import type { ChatLog, @@ -78,8 +79,10 @@ export const executeClientSideAction = async ({ }; return { replyToSend: message }; } - if ("webhookToExecute" in clientSideAction) { - const response = await executeWebhook(clientSideAction.webhookToExecute); + if ("httpRequestToExecute" in clientSideAction) { + const response = await executeHttpRequest( + clientSideAction.httpRequestToExecute, + ); return { replyToSend: response }; } if ("startPropsToInject" in clientSideAction) { @@ -91,4 +94,10 @@ export const executeClientSideAction = async ({ if ("codeToExecute" in clientSideAction) { return executeCode(clientSideAction.codeToExecute); } + if (clientSideAction.type === "listenForWebhook") { + return listenForWebhook({ + sessionId: context.sessionId, + resultId: context.resultId, + }); + } }; diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index be61ffa51e..e070db1f63 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.3.17", + "version": "0.3.18", "license": "AGPL-3.0-or-later", "description": "Convenient library to display typebots on your Next.js website", "type": "module", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 79e80a16ac..3a2f58d763 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.3.17", + "version": "0.3.18", "description": "Convenient library to display typebots on your React app", "license": "AGPL-3.0-or-later", "type": "module", diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index 5b9384e8a5..8a5d5f7183 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -443,6 +443,15 @@ const keycloakEnv = { }, }; +const partykitEnv = { + client: { + NEXT_PUBLIC_PARTYKIT_HOST: z.string().min(1).optional(), + }, + runtimeEnv: { + NEXT_PUBLIC_PARTYKIT_HOST: getRuntimeVariable("NEXT_PUBLIC_PARTYKIT_HOST"), + }, +}; + export const env = createEnv({ server: { ...baseEnv.server, @@ -477,6 +486,7 @@ export const env = createEnv({ ...sentryEnv.client, ...posthogEnv.client, ...tolgeeEnv.client, + ...partykitEnv.client, }, experimental__runtimeEnv: { ...baseEnv.runtimeEnv, @@ -491,6 +501,7 @@ export const env = createEnv({ ...sentryEnv.runtimeEnv, ...posthogEnv.runtimeEnv, ...tolgeeEnv.runtimeEnv, + ...partykitEnv.runtimeEnv, }, skipValidation: process.env.SKIP_ENV_CHECK === "true" || diff --git a/packages/lib/src/api/utils.ts b/packages/lib/src/api/utils.ts index c9b44c9fd5..e74266e7cc 100644 --- a/packages/lib/src/api/utils.ts +++ b/packages/lib/src/api/utils.ts @@ -19,6 +19,12 @@ export const badRequest = (res: NextApiResponse, customMessage?: any) => export const forbidden = (res: NextApiResponse, customMessage?: string) => res.status(403).json({ message: customMessage ?? "Forbidden" }); +export const internalServerError = ( + res: NextApiResponse, + customMessage?: string, +) => + res.status(500).json({ message: customMessage ?? "Internal server error" }); + export const initMiddleware = ( handler: ( diff --git a/packages/partykit/package.json b/packages/partykit/package.json new file mode 100644 index 0000000000..59fba14f12 --- /dev/null +++ b/packages/partykit/package.json @@ -0,0 +1,11 @@ +{ + "name": "@typebot.io/partykit", + "scripts": { + "dev": "partykit dev --live", + "deploy": "partykit deploy" + }, + "devDependencies": { + "partykit": "0.0.110", + "@typebot.io/tsconfig": "workspace:*" + } +} diff --git a/packages/partykit/partykit.json b/packages/partykit/partykit.json new file mode 100644 index 0000000000..3509dd0fbb --- /dev/null +++ b/packages/partykit/partykit.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://www.partykit.io/schema.json", + "name": "typebot", + "main": "src/webhookServer.ts", + "compatibilityDate": "2024-10-01" +} diff --git a/packages/partykit/src/webhookServer.ts b/packages/partykit/src/webhookServer.ts new file mode 100644 index 0000000000..96e11b6443 --- /dev/null +++ b/packages/partykit/src/webhookServer.ts @@ -0,0 +1,36 @@ +import type * as Party from "partykit/server"; + +export default class Server implements Party.Server { + options: Party.ServerOptions = { + hibernate: true, + }; + + constructor(readonly room: Party.Room) {} + + async onRequest(request: Party.Request) { + if (request.method === "POST") { + let payload: unknown; + try { + payload = await request.json(); + } catch (err) { + return new Response("Invalid payload, please send JSON body", { + status: 400, + }); + } + if ( + payload === null || + typeof payload !== "object" || + Array.isArray(payload) + ) + return new Response("Invalid payload, please send JSON body", { + status: 400, + }); + this.room.broadcast(JSON.stringify({ data: payload })); + return new Response("OK"); + } + + return new Response("Method not allowed", { status: 405 }); + } +} + +Server satisfies Party.Worker; diff --git a/packages/partykit/tsconfig.json b/packages/partykit/tsconfig.json new file mode 100644 index 0000000000..f03bab32b7 --- /dev/null +++ b/packages/partykit/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@typebot.io/tsconfig/base.json", + "include": ["src/**/*.ts"] +} diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json index c25736d9ce..ab80a7dd59 100644 --- a/packages/tsconfig/base.json +++ b/packages/tsconfig/base.json @@ -2,12 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig", "display": "Default", "compilerOptions": { - "plugins": [ - { - "name": "ts-plugin-sort-import-suggestions", - "moveUpPatterns": ["@typebot.io/"] - } - ], "esModuleInterop": true, "skipLibCheck": true, "target": "es2022", diff --git a/packages/tsconfig/nextjs.json b/packages/tsconfig/nextjs.json index a3cb862e31..1701e7ac66 100644 --- a/packages/tsconfig/nextjs.json +++ b/packages/tsconfig/nextjs.json @@ -3,13 +3,7 @@ "display": "Next.js", "extends": "./base.json", "compilerOptions": { - "plugins": [ - { "name": "next" }, - { - "name": "ts-plugin-sort-import-suggestions", - "moveUpPatterns": ["@typebot.io/"] - } - ], + "plugins": [{ "name": "next" }], "jsx": "preserve", "allowJs": true, "incremental": true, diff --git a/apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts b/packages/typebot/src/helpers/isReadTypebotForbidden.ts similarity index 100% rename from apps/builder/src/features/typebot/helpers/isReadTypebotForbidden.ts rename to packages/typebot/src/helpers/isReadTypebotForbidden.ts diff --git a/packages/typebot/src/migrations/migrateTypebotFromV3ToV4.ts b/packages/typebot/src/migrations/migrateTypebotFromV3ToV4.ts index d0b9ebccae..2d6d565c04 100644 --- a/packages/typebot/src/migrations/migrateTypebotFromV3ToV4.ts +++ b/packages/typebot/src/migrations/migrateTypebotFromV3ToV4.ts @@ -1,4 +1,4 @@ -import { isWebhookBlock } from "@typebot.io/blocks-core/helpers"; +import { isHttpRequestBlock } from "@typebot.io/blocks-core/helpers"; import { migrateWebhookBlock } from "@typebot.io/blocks-core/migrations/migrateWebhookBlock"; import { isDefined } from "@typebot.io/lib/utils"; import prisma from "@typebot.io/prisma"; @@ -12,7 +12,7 @@ export const migrateTypebotFromV3ToV4 = async ( return typebot as Omit & { version: "4" }; const webhookBlocks = typebot.groups .flatMap((group) => group.blocks) - .filter(isWebhookBlock); + .filter(isHttpRequestBlock); const webhooks = await prisma.webhook.findMany({ where: { id: { diff --git a/packages/variables/src/codeRunners.ts b/packages/variables/src/codeRunners.ts index 0a6d7de0c2..fee38300b2 100644 --- a/packages/variables/src/codeRunners.ts +++ b/packages/variables/src/codeRunners.ts @@ -20,7 +20,13 @@ export const createCodeRunner = ({ variables }: { variables: Variable[] }) => { ); }; -export const createHttpReqResponseMappingRunner = (response: any) => { +export const createHttpReqResponseMappingRunner = (response: unknown) => { + if ( + response === null || + typeof response !== "object" || + Array.isArray(response) + ) + return; const isolate = new ivm.Isolate(); const context = isolate.createContextSync(); const jail = context.global; diff --git a/packages/whatsapp/src/receiveMessage.ts b/packages/whatsapp/src/receiveMessage.ts deleted file mode 100644 index 54b8f8dad7..0000000000 --- a/packages/whatsapp/src/receiveMessage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isNotDefined } from "@typebot.io/lib/utils"; -import { resumeWhatsAppFlow } from "./resumeWhatsAppFlow"; -import type { WhatsAppWebhookRequestBody } from "./schemas"; - -type Props = { - entry: WhatsAppWebhookRequestBody["entry"]; - credentialsId: string; - workspaceId: string; -}; - -export const receiveMessage = async ({ - entry, - credentialsId, - workspaceId, -}: Props) => { - const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0); - if (isNotDefined(receivedMessage)) return { message: "No message found" }; - const contactName = - entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ""; - const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ""; - const phoneNumberId = entry.at(0)?.changes.at(0)?.value - .metadata.phone_number_id; - if (!phoneNumberId) return { message: "No phone number id found" }; - return resumeWhatsAppFlow({ - receivedMessage, - sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`, - credentialsId, - workspaceId, - contact: { - name: contactName, - phoneNumber: contactPhoneNumber, - }, - }); -}; diff --git a/packages/whatsapp/src/receiveMessagePreview.ts b/packages/whatsapp/src/receiveMessagePreview.ts deleted file mode 100644 index c273328ce2..0000000000 --- a/packages/whatsapp/src/receiveMessagePreview.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TRPCError } from "@trpc/server"; -import { env } from "@typebot.io/env"; -import { isNotDefined } from "@typebot.io/lib/utils"; -import { resumeWhatsAppFlow } from "./resumeWhatsAppFlow"; -import type { WhatsAppWebhookRequestBody } from "./schemas"; - -type Props = { - entry: WhatsAppWebhookRequestBody["entry"]; -}; -export const receiveMessagePreview = ({ entry }: Props) => { - if (!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID) - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID is not defined", - }); - const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0); - if (isNotDefined(receivedMessage)) return { message: "No message found" }; - const contactName = - entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ""; - const contactPhoneNumber = - entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ""; - - return resumeWhatsAppFlow({ - receivedMessage, - sessionId: `wa-preview-${receivedMessage.from}`, - contact: { - name: contactName, - phoneNumber: contactPhoneNumber, - }, - }); -}; diff --git a/packages/whatsapp/src/resumeWhatsAppFlow.ts b/packages/whatsapp/src/resumeWhatsAppFlow.ts index fa57ceb569..3645a75e16 100644 --- a/packages/whatsapp/src/resumeWhatsAppFlow.ts +++ b/packages/whatsapp/src/resumeWhatsAppFlow.ts @@ -6,7 +6,10 @@ import { removeIsReplyingInChatSession } from "@typebot.io/bot-engine/queries/re import { setIsReplyingInChatSession } from "@typebot.io/bot-engine/queries/setIsReplyingInChatSession"; import { saveStateToDatabase } from "@typebot.io/bot-engine/saveStateToDatabase"; import type { Message } from "@typebot.io/bot-engine/schemas/api"; -import type { SessionState } from "@typebot.io/bot-engine/schemas/chatSession"; +import type { + ChatSession, + SessionState, +} from "@typebot.io/bot-engine/schemas/chatSession"; import { env } from "@typebot.io/env"; import { getBlockById } from "@typebot.io/groups/helpers"; import { decrypt } from "@typebot.io/lib/api/encryption/decrypt"; @@ -27,7 +30,13 @@ type Props = { credentialsId?: string; phoneNumberId?: string; workspaceId?: string; - contact: NonNullable["contact"]; + contact?: NonNullable["contact"]; + origin?: "webhook"; +}; + +const isMessageTooOld = (receivedMessage: WhatsAppIncomingMessage) => { + const messageSendDate = new Date(Number(receivedMessage.timestamp) * 1000); + return messageSendDate.getTime() < Date.now() - 180000; }; export const resumeWhatsAppFlow = async ({ @@ -37,50 +46,37 @@ export const resumeWhatsAppFlow = async ({ credentialsId, phoneNumberId, contact, -}: Props): Promise<{ message: string }> => { - const messageSendDate = new Date(Number(receivedMessage.timestamp) * 1000); - const messageSentBefore3MinutesAgo = - messageSendDate.getTime() < Date.now() - 180000; - if (messageSentBefore3MinutesAgo) { - console.log("Message is too old", messageSendDate.getTime()); - return { - message: "Message received", - }; +}: Props) => { + if (isMessageTooOld(receivedMessage)) { + console.log("Message is too old"); + return; } const isPreview = workspaceId === undefined || credentialsId === undefined; const credentials = await getCredentials({ credentialsId, isPreview }); - if (!credentials) { console.error("Could not find credentials"); - return { - message: "Message received", - }; + return; } - if (credentials.phoneNumberId !== phoneNumberId && !isPreview) { + if (phoneNumberId && credentials.phoneNumberId !== phoneNumberId) { console.error("Credentials point to another phone ID, skipping..."); - return { - message: "Message received", - }; + return; } const session = await getSession(sessionId); - const { incomingMessages, isReplyingWasSet } = + const aggregationResponse = await aggregateParallelMediaMessagesIfRedisEnabled({ receivedMessage, existingSessionId: session?.id, newSessionId: sessionId, }); - if (incomingMessages.length === 0) { - if (isReplyingWasSet) await removeIsReplyingInChatSession(sessionId); - - return { - message: "Message received", - }; + if (aggregationResponse.status === "found newer message") { + console.log("Found newer message, skipping this one"); + return; } const isSessionExpired = @@ -88,13 +84,11 @@ export const resumeWhatsAppFlow = async ({ isDefined(session.state.expiryTimeout) && session?.updatedAt.getTime() + session.state.expiryTimeout < Date.now(); - if (!isReplyingWasSet) { - if (session?.isReplying) { + if (aggregationResponse.status === "treat as unique message") { + if (session?.isReplying && origin !== "webhook") { if (!isSessionExpired) { console.log("Is currently replying, skipping..."); - return { - message: "Message received", - }; + return; } } else { await setIsReplyingInChatSession({ @@ -109,9 +103,8 @@ export const resumeWhatsAppFlow = async ({ (currentTypebot && session?.state.currentBlockId ? getBlockById(session.state.currentBlockId, currentTypebot.groups) : undefined) ?? {}; - - const reply = await getIncomingMessageContent({ - messages: incomingMessages, + const reply = await convertWhatsAppMessageToTypebotMessage({ + messages: aggregationResponse.incomingMessages, workspaceId, accessToken: credentials?.systemUserAccessToken, typebotId: currentTypebot?.id, @@ -119,51 +112,26 @@ export const resumeWhatsAppFlow = async ({ block, }); - const resumeResponse = - session && !isSessionExpired - ? await continueBotFlow(reply, { - version: 2, - state: { ...session.state, whatsApp: { contact } }, - textBubbleContentFormat: "richText", - }) - : workspaceId - ? await startWhatsAppSession({ - incomingMessage: reply, - workspaceId, - credentials: { ...credentials, id: credentialsId as string }, - contact, - }) - : { error: "workspaceId not found" }; - - if ("error" in resumeResponse) { - await removeIsReplyingInChatSession(sessionId); - console.log("Chat not starting:", resumeResponse.error); - return { - message: "Message received", - }; - } - const { input, logs, - newSessionState, - messages, - clientSideActions, visitedEdges, setVariableHistory, - } = resumeResponse; - - const isFirstChatChunk = (!session || isSessionExpired) ?? false; - await sendChatReplyToWhatsApp({ - to: receivedMessage.from, - messages, - input, - isFirstChatChunk, - typingEmulation: newSessionState.typingEmulation, - clientSideActions, - credentials, - state: newSessionState, - }); + newSessionState, + isWaitingForWebhook, + } = + (await resumeFlowAndSendWhatsAppMessages({ + to: receivedMessage.from, + credentials, + isSessionExpired, + reply, + session, + sessionId, + contact, + workspaceId, + credentialsId, + })) ?? {}; + if (!newSessionState || !visitedEdges || !setVariableHistory) return; await saveStateToDatabase({ clientSideActions: [], @@ -171,11 +139,16 @@ export const resumeWhatsAppFlow = async ({ logs, session: { id: sessionId, + isReplying: isWaitingForWebhook, state: { ...newSessionState, - currentBlockId: !input ? undefined : newSessionState.currentBlockId, + currentBlockId: + !input && !isWaitingForWebhook + ? undefined + : newSessionState.currentBlockId, }, }, + isWaitingForExternalEvent: isWaitingForWebhook, visitedEdges, setVariableHistory, }); @@ -185,7 +158,7 @@ export const resumeWhatsAppFlow = async ({ }; }; -const getIncomingMessageContent = async ({ +const convertWhatsAppMessageToTypebotMessage = async ({ messages, workspaceId, accessToken, @@ -289,6 +262,9 @@ const getIncomingMessageContent = async ({ else text = location; break; } + case "webhook": { + text = message.webhook.data; + } } } @@ -348,10 +324,19 @@ const aggregateParallelMediaMessagesIfRedisEnabled = async ({ receivedMessage: WhatsAppIncomingMessage; existingSessionId?: string; newSessionId: string; -}): Promise<{ - isReplyingWasSet: boolean; - incomingMessages: WhatsAppIncomingMessage[]; -}> => { +}): Promise< + | { + status: "treat as unique message"; + incomingMessages: [WhatsAppIncomingMessage]; + } + | { + status: "found newer message"; + } + | { + status: "ready to reply"; + incomingMessages: WhatsAppIncomingMessage[]; + } +> => { if (redis && ["document", "video", "image"].includes(receivedMessage.type)) { const redisKey = `wasession:${newSessionId}`; try { @@ -370,15 +355,13 @@ const aggregateParallelMediaMessagesIfRedisEnabled = async ({ const newMessagesResponse = await redis.lrange(redisKey, 0, -1); - if (!newMessagesResponse || newMessagesResponse.length > len) { - // Current message was aggregated with other messages another webhook handler. Skipping... - return { isReplyingWasSet: true, incomingMessages: [] }; - } + if (!newMessagesResponse || newMessagesResponse.length > len) + return { status: "found newer message" }; redis.del(redisKey).then(); return { - isReplyingWasSet: true, + status: "ready to reply", incomingMessages: newMessagesResponse.map((msgStr) => JSON.parse(msgStr), ), @@ -389,7 +372,103 @@ const aggregateParallelMediaMessagesIfRedisEnabled = async ({ } return { - isReplyingWasSet: false, + status: "treat as unique message", incomingMessages: [receivedMessage], }; }; + +const resumeFlowAndSendWhatsAppMessages = async (props: { + to: string; + session: Pick | null; + sessionId: string; + reply: Message | undefined; + contact?: NonNullable["contact"]; + credentials: WhatsAppCredentials["data"]; + isSessionExpired: boolean | null; + credentialsId?: string; + workspaceId?: string; +}) => { + const resumeResponse = await resumeFlow(props); + if ("error" in resumeResponse) { + await removeIsReplyingInChatSession(props.sessionId); + console.error(resumeResponse.error); + return; + } + + const { + input, + logs, + messages, + clientSideActions, + visitedEdges, + setVariableHistory, + newSessionState, + } = resumeResponse; + + const isFirstChatChunk = (!props.session || props.isSessionExpired) ?? false; + const result = await sendChatReplyToWhatsApp({ + to: props.to, + messages, + input, + isFirstChatChunk, + clientSideActions, + credentials: props.credentials, + state: resumeResponse.newSessionState, + }); + if (result?.type === "replyToSend") + return resumeFlowAndSendWhatsAppMessages({ + ...props, + reply: result.replyToSend + ? { + type: "text", + text: result.replyToSend, + } + : undefined, + }); + + return { + input, + logs, + visitedEdges, + setVariableHistory, + newSessionState, + isWaitingForWebhook: result?.type === "shouldWaitForWebhook", + }; +}; + +const resumeFlow = ({ + session, + isSessionExpired, + reply, + contact, + credentials, + credentialsId, + workspaceId, +}: { + reply: Message | undefined; + contact?: NonNullable["contact"]; + session: Pick | null; + credentials: WhatsAppCredentials["data"]; + isSessionExpired: boolean | null; + credentialsId?: string; + workspaceId?: string; +}) => { + if (session?.state && !isSessionExpired) + return continueBotFlow(reply, { + version: 2, + state: contact + ? { ...session.state, whatsApp: { contact } } + : session.state, + textBubbleContentFormat: "richText", + }); + if (!workspaceId || !contact) + return { + error: "Can't start WhatsApp session without workspaceId or contact", + }; + return startWhatsAppSession({ + incomingMessage: reply, + workspaceId, + credentials: { ...credentials, id: credentialsId as string }, + contact, + }); +}; diff --git a/packages/whatsapp/src/schemas.ts b/packages/whatsapp/src/schemas.ts index 168ae0a37f..6493f0cdf7 100644 --- a/packages/whatsapp/src/schemas.ts +++ b/packages/whatsapp/src/schemas.ts @@ -142,6 +142,14 @@ export const incomingMessageSchema = z.discriminatedUnion("type", [ }), timestamp: z.string(), }), + z.object({ + from: z.string(), + type: z.literal("webhook"), + webhook: z.object({ + data: z.string(), + }), + timestamp: z.string(), + }), ]); export const whatsAppWebhookRequestBodySchema = z.object({ diff --git a/packages/whatsapp/src/sendChatReplyToWhatsApp.ts b/packages/whatsapp/src/sendChatReplyToWhatsApp.ts index 7d4597fb4e..bfb01b1213 100644 --- a/packages/whatsapp/src/sendChatReplyToWhatsApp.ts +++ b/packages/whatsapp/src/sendChatReplyToWhatsApp.ts @@ -2,9 +2,9 @@ import * as Sentry from "@sentry/nextjs"; import { BubbleBlockType } from "@typebot.io/blocks-bubbles/constants"; import { InputBlockType } from "@typebot.io/blocks-inputs/constants"; import { computeTypingDuration } from "@typebot.io/bot-engine/computeTypingDuration"; -import { continueBotFlow } from "@typebot.io/bot-engine/continueBotFlow"; import type { ContinueChatResponse } from "@typebot.io/bot-engine/schemas/api"; import type { SessionState } from "@typebot.io/bot-engine/schemas/chatSession"; +import type { ClientSideAction } from "@typebot.io/bot-engine/schemas/clientSideAction"; import { isNotDefined } from "@typebot.io/lib/utils"; import { defaultSettings } from "@typebot.io/settings/constants"; import type { Settings } from "@typebot.io/settings/schemas"; @@ -20,21 +20,19 @@ const messageAfterMediaTimeout = 5000; type Props = { to: string; isFirstChatChunk: boolean; - typingEmulation: SessionState["typingEmulation"]; credentials: WhatsAppCredentials["data"]; state: SessionState; } & Pick; export const sendChatReplyToWhatsApp = async ({ to, - typingEmulation, isFirstChatChunk, messages, input, clientSideActions, credentials, state, -}: Props): Promise => { +}: Props): Promise => { const messagesBeforeInput = isLastMessageIncludedInInput( input, messages.at(-1), @@ -49,47 +47,23 @@ export const sendChatReplyToWhatsApp = async ({ isNotDefined(action.lastBubbleBlockId), ) ?? []; - for (const action of clientSideActionsBeforeMessages) { - const result = await executeClientSideAction({ to, credentials })(action); - if (!result) continue; - const { input, newSessionState, messages, clientSideActions } = - await continueBotFlow( - result.replyToSend - ? { type: "text", text: result.replyToSend } - : undefined, - { - version: 2, - state, - textBubbleContentFormat: "richText", - }, - ); + const result = await executeClientSideActions({ + clientSideActions: clientSideActionsBeforeMessages, + to, + credentials, + }); - return sendChatReplyToWhatsApp({ - to, - messages, - input, - isFirstChatChunk: false, - typingEmulation: newSessionState.typingEmulation, - clientSideActions, - credentials, - state: newSessionState, - }); - } + if (result) return result; let i = -1; for (const message of messagesBeforeInput) { i += 1; - if ( - i > 0 && - (typingEmulation?.delayBetweenBubbles ?? - defaultSettings.typingEmulation.delayBetweenBubbles) > 0 - ) { + const delayBetweenBubbles = + state.typingEmulation?.delayBetweenBubbles ?? + defaultSettings.typingEmulation.delayBetweenBubbles; + if (i > 0 && delayBetweenBubbles > 0) { await new Promise((resolve) => - setTimeout( - resolve, - (typingEmulation?.delayBetweenBubbles ?? - defaultSettings.typingEmulation.delayBetweenBubbles) * 1000, - ), + setTimeout(resolve, delayBetweenBubbles * 1000), ); } const whatsAppMessage = convertMessageToWhatsAppMessage(message); @@ -98,16 +72,17 @@ export const sendChatReplyToWhatsApp = async ({ sentMessages.at(-1)?.type ?? "", ); + const isTypingEmulationDisabled = + state.typingEmulation?.isDisabledOnFirstMessage ?? + defaultSettings.typingEmulation.isDisabledOnFirstMessage; + const typingDuration = lastSentMessageIsMedia ? messageAfterMediaTimeout - : isFirstChatChunk && - i === 0 && - (typingEmulation?.isDisabledOnFirstMessage ?? - defaultSettings.typingEmulation.isDisabledOnFirstMessage) + : isFirstChatChunk && i === 0 && isTypingEmulationDisabled ? 0 : getTypingDuration({ message: whatsAppMessage, - typingEmulation, + typingEmulation: state.typingEmulation, }); if ((typingDuration ?? 0) > 0) await new Promise((resolve) => setTimeout(resolve, typingDuration)); @@ -122,34 +97,6 @@ export const sendChatReplyToWhatsApp = async ({ clientSideActions?.filter( (action) => action.lastBubbleBlockId === message.id, ) ?? []; - for (const action of clientSideActionsAfterMessage) { - const result = await executeClientSideAction({ to, credentials })( - action, - ); - if (!result) continue; - const { input, newSessionState, messages, clientSideActions } = - await continueBotFlow( - result.replyToSend - ? { type: "text", text: result.replyToSend } - : undefined, - { - version: 2, - state, - textBubbleContentFormat: "richText", - }, - ); - - return sendChatReplyToWhatsApp({ - to, - messages, - input, - isFirstChatChunk: false, - typingEmulation: newSessionState.typingEmulation, - clientSideActions, - credentials, - state: newSessionState, - }); - } } catch (err) { Sentry.captureException(err, { extra: { message } }); console.log("Failed to send message:", JSON.stringify(message, null, 2)); @@ -160,6 +107,16 @@ export const sendChatReplyToWhatsApp = async ({ await err.response.text(), ); } + const clientSideActionsAfterMessage = + clientSideActions?.filter( + (action) => action.lastBubbleBlockId === message.id, + ) ?? []; + const result = await executeClientSideActions({ + clientSideActions: clientSideActionsAfterMessage, + to, + credentials, + }); + if (result) return result; } if (input) { @@ -176,7 +133,7 @@ export const sendChatReplyToWhatsApp = async ({ ? messageAfterMediaTimeout : getTypingDuration({ message, - typingEmulation, + typingEmulation: state.typingEmulation, }); if (typingDuration) await new Promise((resolve) => setTimeout(resolve, typingDuration)); @@ -240,13 +197,32 @@ const isLastMessageIncludedInInput = ( ); }; +const executeClientSideActions = async ({ + to, + credentials, + clientSideActions, +}: { + clientSideActions: ClientSideAction[]; + to: string; + credentials: WhatsAppCredentials["data"]; +}) => { + for (const action of clientSideActions) { + const result = await executeClientSideAction({ to, credentials })(action); + if (result) return result; + } +}; + +type ClientSideActionExecutionResult = + | { type: "replyToSend"; replyToSend: string | undefined } + | { type: "shouldWaitForWebhook" } + | undefined; const executeClientSideAction = (context: { to: string; credentials: WhatsAppCredentials["data"] }) => async ( clientSideAction: NonNullable< ContinueChatResponse["clientSideActions"] >[number], - ): Promise<{ replyToSend: string | undefined } | void> => { + ): Promise => { if ("wait" in clientSideAction) { await new Promise((resolve) => setTimeout( @@ -256,6 +232,7 @@ const executeClientSideAction = ); if (!clientSideAction.expectsDedicatedReply) return; return { + type: "replyToSend", replyToSend: undefined, }; } @@ -287,4 +264,8 @@ const executeClientSideAction = ); } } + if (clientSideAction.type === "listenForWebhook") + return { + type: "shouldWaitForWebhook", + }; }; diff --git a/yarn.lock b/yarn.lock index e7d5c89cf7..042876f0ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1,6 +1,6 @@ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # yarn lockfile v1 -# bun ./bun.lockb --hash: 85B5D8D351D5F01B-f8838cd62e30a7d5-CD59DB2A8A329FA4-6b5105405502e5bd +# bun ./bun.lockb --hash: DA56811BB46E16FF-a5770c5bfe19493c-05D940B437DC2C13-a0a97b695c31817a "@ai-sdk/anthropic@0.0.30": @@ -2169,6 +2169,36 @@ sisteransi "^1.0.5" is-unicode-supported "*" +"@cloudflare/workerd-darwin-64@1.20240718.0": + version "1.20240718.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240718.0.tgz" + integrity sha512-BsPZcSCgoGnufog2GIgdPuiKicYTNyO/Dp++HbpLRH+yQdX3x4aWx83M+a0suTl1xv76dO4g9aw7SIB6OSgIyQ== + +"@cloudflare/workerd-darwin-arm64@1.20240718.0": + version "1.20240718.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240718.0.tgz" + integrity sha512-nlr4gaOO5gcJerILJQph3+2rnas/nx/lYsuaot1ntHu4LAPBoQo1q/Pucj2cSIav4UiMzTbDmoDwPlls4Kteog== + +"@cloudflare/workerd-linux-64@1.20240718.0": + version "1.20240718.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240718.0.tgz" + integrity sha512-LJ/k3y47pBcjax0ee4K+6ZRrSsqWlfU4lbU8Dn6u5tSC9yzwI4YFNXDrKWInB0vd7RT3w4Yqq1S6ZEbfRrqVUg== + +"@cloudflare/workerd-linux-arm64@1.20240718.0": + version "1.20240718.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240718.0.tgz" + integrity sha512-zBEZvy88EcAMGRGfuVtS00Yl7lJdUM9sH7i651OoL+q0Plv9kphlCC0REQPwzxrEYT1qibSYtWcD9IxQGgx2/g== + +"@cloudflare/workerd-windows-64@1.20240718.0": + version "1.20240718.0" + resolved "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240718.0.tgz" + integrity sha512-YpCRvvT47XanFum7C3SedOZKK6BfVhqmwdAAVAQFyc4gsCdegZo0JkUkdloC/jwuWlbCACOG2HTADHOqyeolzQ== + +"@cloudflare/workers-types@4.20240718.0": + version "4.20240718.0" + resolved "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240718.0.tgz" + integrity sha512-7RqxXIM9HyhjfZ9ztXjITuc7mL0w4s+zXgypqKmMuvuObC3DgXutJ3bOYbQ+Ss5QbywrzWSNMlmGdL/ldg/yZg== + "@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.3.2", "@codemirror/autocomplete@^6.7.1": version "6.18.1" resolved "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz" @@ -2511,6 +2541,13 @@ style-mod "^4.1.0" w3c-keyname "^2.2.4" +"@cspotcode/source-map-support@0.8.1": + version "0.8.1" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@dnd-kit/accessibility@^3.0.0": version "3.1.0" resolved "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz" @@ -2746,6 +2783,11 @@ resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz" integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g== +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + "@esbuild/aix-ppc64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz" @@ -2761,6 +2803,11 @@ resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz" integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw== +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + "@esbuild/android-arm@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz" @@ -2776,6 +2823,11 @@ resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz" integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q== +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + "@esbuild/android-arm64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz" @@ -2791,6 +2843,11 @@ resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz" integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg== +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + "@esbuild/android-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz" @@ -2806,6 +2863,11 @@ resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz" integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ== +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + "@esbuild/darwin-arm64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz" @@ -2821,6 +2883,11 @@ resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz" integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g== +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + "@esbuild/darwin-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz" @@ -2836,6 +2903,11 @@ resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz" integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA== +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + "@esbuild/freebsd-arm64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz" @@ -2851,6 +2923,11 @@ resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz" integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw== +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + "@esbuild/freebsd-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz" @@ -2866,6 +2943,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz" integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q== +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + "@esbuild/linux-arm@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz" @@ -2881,6 +2963,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz" integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg== +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + "@esbuild/linux-arm64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz" @@ -2896,6 +2983,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz" integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA== +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + "@esbuild/linux-ia32@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz" @@ -2911,6 +3003,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz" integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg== +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + "@esbuild/linux-loong64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz" @@ -2926,6 +3023,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz" integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg== +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + "@esbuild/linux-mips64el@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz" @@ -2941,6 +3043,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz" integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA== +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + "@esbuild/linux-ppc64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz" @@ -2956,6 +3063,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz" integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ== +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + "@esbuild/linux-riscv64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz" @@ -2971,6 +3083,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz" integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q== +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + "@esbuild/linux-s390x@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz" @@ -2986,6 +3103,11 @@ resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz" integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA== +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + "@esbuild/linux-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz" @@ -3001,6 +3123,11 @@ resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz" integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ== +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + "@esbuild/netbsd-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz" @@ -3021,6 +3148,11 @@ resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz" integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw== +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + "@esbuild/openbsd-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz" @@ -3036,6 +3168,11 @@ resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz" integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ== +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + "@esbuild/sunos-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz" @@ -3051,6 +3188,11 @@ resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz" integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ== +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + "@esbuild/win32-arm64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz" @@ -3066,6 +3208,11 @@ resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz" integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg== +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + "@esbuild/win32-ia32@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz" @@ -3081,6 +3228,11 @@ resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz" integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw== +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + "@esbuild/win32-x64@0.23.1": version "0.23.1" resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz" @@ -3093,6 +3245,11 @@ dependencies: lodash.kebabcase "^4.1.1" +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + "@fix-webm-duration/fix@1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@fix-webm-duration/fix/-/fix-1.0.1.tgz" @@ -3572,7 +3729,7 @@ "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== @@ -3595,6 +3752,14 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" @@ -5425,6 +5590,7 @@ dompurify "3.0.6" ky "1.2.4" marked "9.0.3" + partysocket "1.0.2" solid-element "1.7.1" solid-js "1.7.8" @@ -5505,6 +5671,13 @@ ky "1.2.4" openai "4.52.7" +"@typebot.io/partykit@packages/partykit": + version "workspace:packages/partykit" + resolved "workspace:packages/partykit" + devDependencies: + "@typebot.io/tsconfig" "packages/tsconfig" + partykit "0.0.110" + "@typebot.io/playwright@packages/playwright": version "workspace:packages/playwright" resolved "workspace:packages/playwright" @@ -7364,7 +7537,7 @@ accepts@~1.3.4, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.0.0, acorn@^8.1.0, acorn@^8.10.0, acorn@^8.11.0, acorn@^8.11.2, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.0.0, acorn@^8.1.0, acorn@^8.10.0, acorn@^8.11.0, acorn@^8.11.2, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.12.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -7382,7 +7555,7 @@ acorn-jsx@^5.0.0, acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.0.2: +acorn-walk@^8.0.2, acorn-walk@^8.2.0: version "8.3.4" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== @@ -7589,6 +7762,13 @@ arrify@^2.0.0: resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== +as-table@^1.0.36: + version "1.0.55" + resolved "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz" + integrity sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ== + dependencies: + printable-characters "^1.0.42" + ast-types@^0.13.4: version "0.13.4" resolved "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz" @@ -8208,6 +8388,7 @@ buffer-from@^1.0.0: nuqs "^1.19.3" openai "4.52.7" papaparse "5.4.1" + partysocket "1.0.2" pexels "^1.4.0" prettier "2.8.8" qs "6.11.2" @@ -8361,6 +8542,14 @@ canvas-confetti@1.6.0: resolved "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.6.0.tgz" integrity sha512-ej+w/m8Jzpv9Z7W7uJZer14Ke8P2ogsjg4ZMGIuq4iqUOqY2Jq8BNW42iGmNfRwREaaEfFIczLuZZiEVSYNHAA== +capnp-ts@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/capnp-ts/-/capnp-ts-0.7.0.tgz" + integrity sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g== + dependencies: + debug "^4.3.1" + tslib "^2.2.0" + ccount@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" @@ -8557,6 +8746,15 @@ client-only@0.0.1, client-only@^0.0.1: resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +clipboardy@4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz" + integrity sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w== + dependencies: + execa "^8.0.1" + is-wsl "^3.1.0" + is64bit "^2.0.0" + cliui@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" @@ -9121,6 +9319,11 @@ csstype@3.1.3, csstype@^3.0.2, csstype@^3.1.0, csstype@^3.1.2, csstype@^3.1.3: resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +data-uri-to-buffer@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz" + integrity sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA== + data-uri-to-buffer@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz" @@ -9752,6 +9955,35 @@ esbuild@0.19.11: "@esbuild/linux-riscv64" "0.19.11" "@esbuild/linux-mips64el" "0.19.11" +esbuild@0.21.5: + version "0.21.5" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + esbuild@>=0.18, esbuild@^0.23.0: version "0.23.1" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz" @@ -9930,6 +10162,11 @@ event-target-shim@^5.0.0: resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +event-target-shim@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz" + integrity sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA== + eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" @@ -9966,11 +10203,31 @@ execa@^5.0.0, execa@^5.1.1: human-signals "^2.1.0" strip-final-newline "^2.0.0" +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + onetime "^6.0.0" + is-stream "^3.0.0" + get-stream "^8.0.1" + cross-spawn "^7.0.3" + signal-exit "^4.1.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + human-signals "^5.0.0" + strip-final-newline "^3.0.0" + exit@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +exit-hook@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz" + integrity sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw== + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" @@ -10329,7 +10586,7 @@ fsevents@2.3.2: resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@2.3.3, fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -10405,6 +10662,14 @@ get-port@^6.1.2: resolved "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz" integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== +get-source@^2.0.12: + version "2.0.12" + resolved "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz" + integrity sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w== + dependencies: + data-uri-to-buffer "^2.0.0" + source-map "^0.6.1" + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" @@ -10417,6 +10682,11 @@ get-stream@^6.0.0, get-stream@^6.0.1: resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-tsconfig@^4.7.5: version "4.8.1" resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz" @@ -10499,6 +10769,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + globals@^11.1.0: version "11.12.0" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" @@ -11134,6 +11409,11 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" @@ -11414,6 +11694,11 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + is-extendable@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" @@ -11458,6 +11743,13 @@ is-hotkey@^0.2.0: resolved "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz" integrity sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw== +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" @@ -11529,6 +11821,11 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-typed-array@^1.1.3: version "1.1.13" resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz" @@ -11563,6 +11860,20 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +is64bit@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz" + integrity sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw== + dependencies: + system-architecture "^0.1.0" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -13618,6 +13929,11 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" @@ -13628,6 +13944,24 @@ mimic-response@^4.0.0: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz" integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== +miniflare@3.20240718.0: + version "3.20240718.0" + resolved "https://registry.npmjs.org/miniflare/-/miniflare-3.20240718.0.tgz" + integrity sha512-TKgSeyqPBeT8TBLxbDJOKPWlq/wydoJRHjAyDdgxbw59N6wbP8JucK6AU1vXCfu21eKhrEin77ssXOpbfekzPA== + dependencies: + ws "^8.17.1" + zod "^3.22.3" + acorn "^8.8.0" + youch "^3.2.2" + undici "^5.28.4" + workerd "1.20240718.0" + capnp-ts "^0.7.0" + exit-hook "^2.2.1" + stoppable "^1.1.0" + acorn-walk "^8.2.0" + glob-to-regexp "^0.4.1" + "@cspotcode/source-map-support" "0.8.1" + minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -14083,6 +14417,11 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" @@ -14330,6 +14669,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + nprogress@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz" @@ -14405,6 +14751,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + open@^8.4.0: version "8.4.2" resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" @@ -14686,6 +15039,26 @@ parseurl@~1.3.3: resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +partykit@0.0.110: + version "0.0.110" + resolved "https://registry.npmjs.org/partykit/-/partykit-0.0.110.tgz" + integrity sha512-lAUeB/b6nZbMr6c789RKlSsGhbMtklNhjJZ0ri4zzaVEqhG2/k+cqTLCYTwHvfSI6eSa7cIF614R7CyMUrSLgA== + dependencies: + esbuild "0.21.5" + miniflare "3.20240718.0" + clipboardy "4.0.0" + yoga-wasm-web "0.3.3" + "@cloudflare/workers-types" "4.20240718.0" + optionalDependencies: + fsevents "2.3.3" + +partysocket@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/partysocket/-/partysocket-1.0.2.tgz" + integrity sha512-rAFOUKImaq+VBk2B+2RTBsWEvlnarEP53nchoUHzpVs8V6fG2/estihOTslTQUWHVuHEKDL5k8htG8K3TngyFA== + dependencies: + event-target-shim "^6.0.2" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -14706,6 +15079,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" @@ -15229,6 +15607,11 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" "@jest/schemas" "^29.6.3" +printable-characters@^1.0.42: + version "1.0.42" + resolved "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz" + integrity sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ== + prism-react-renderer@^1.3.5: version "1.3.5" resolved "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz" @@ -16420,7 +16803,7 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -16719,6 +17102,14 @@ stacktrace-parser@^0.1.10: dependencies: type-fest "^0.7.1" +stacktracey@^2.1.8: + version "2.1.8" + resolved "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz" + integrity sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw== + dependencies: + as-table "^1.0.36" + get-source "^2.0.12" + standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz" @@ -16741,6 +17132,11 @@ stdin-discarder@^0.1.0: dependencies: bl "^5.0.0" +stoppable@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz" + integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== + "stories@packages/embeds/stories": version "workspace:packages/embeds/stories" resolved "workspace:packages/embeds/stories" @@ -16865,6 +17261,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" @@ -17058,6 +17459,11 @@ symbol-tree@^3.2.4: resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +system-architecture@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz" + integrity sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA== + tabbable@^6.0.1, tabbable@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" @@ -17377,7 +17783,7 @@ tslib@2.4.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@*, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1: +tslib@*, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1: version "2.7.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== @@ -17526,6 +17932,13 @@ unbzip2-stream@^1.4.3: buffer "^5.2.1" through "^2.3.8" +undici@^5.28.4: + version "5.28.4" + resolved "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz" + integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== + dependencies: + "@fastify/busboy" "^2.0.0" + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" @@ -18068,6 +18481,7 @@ vfile-message@^4.0.0: nodemailer "6.9.15" openai "4.52.7" papaparse "5.4.1" + partysocket "1.0.2" react "18.2.0" react-dom "18.2.0" stripe "17.1.0" @@ -18275,6 +18689,17 @@ widest-line@^4.0.1: dependencies: string-width "^5.0.1" +workerd@1.20240718.0: + version "1.20240718.0" + resolved "https://registry.npmjs.org/workerd/-/workerd-1.20240718.0.tgz" + integrity sha512-w7lOLRy0XecQTg/ujTLWBiJJuoQvzB3CdQ6/8Wgex3QxFhV9Pbnh3UbwIuUfMw3OCCPQc4o7y+1P+mISAgp6yg== + optionalDependencies: + "@cloudflare/workerd-linux-64" "1.20240718.0" + "@cloudflare/workerd-darwin-64" "1.20240718.0" + "@cloudflare/workerd-windows-64" "1.20240718.0" + "@cloudflare/workerd-linux-arm64" "1.20240718.0" + "@cloudflare/workerd-darwin-arm64" "1.20240718.0" + wrap-ansi@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" @@ -18320,7 +18745,7 @@ ws@~8.17.1: resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== -ws@^8.11.0, ws@^8.18.0: +ws@^8.11.0, ws@^8.17.1, ws@^8.18.0: version "8.18.0" resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== @@ -18468,7 +18893,21 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zod@3.23.8, zod@^3.0.0, zod@^3.18.0, zod@^3.20.6, zod@^3.21.4, zod@^3.23.3: +yoga-wasm-web@0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz" + integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== + +youch@^3.2.2: + version "3.3.3" + resolved "https://registry.npmjs.org/youch/-/youch-3.3.3.tgz" + integrity sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA== + dependencies: + cookie "^0.5.0" + mustache "^4.2.0" + stacktracey "^2.1.8" + +zod@3.23.8, zod@^3.0.0, zod@^3.18.0, zod@^3.20.6, zod@^3.21.4, zod@^3.22.3, zod@^3.23.3: version "3.23.8" resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==