diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index f252a96d2f..b539629874 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1053,12 +1053,15 @@ "how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?", "choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.", "setting_up_zapier": "Setting up your Zapier integration", + "setting_up_make": "Setting up your Make integration", "generate_api_key": "Generate API key", "generate_api_key_description": "Generate an API key to use with {{appName}} at", "your_unique_api_key": "Your unique API key", "copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.", "zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.<1>Select Cal.com as your Trigger app. Also choose a Trigger event.<2>Choose your account and then enter your Unique API Key.<3>Test your Trigger.<4>You're set!", + "make_setup_instructions": "<0>Log into your Make account and create a new Scenario.<1>Select Cal.com as your Trigger app. Also choose a Trigger event.<2>Choose your account and then enter your Unique API Key.<3>Test your Trigger.<4>You're set!", "install_zapier_app": "Please first install the Zapier App in the app store.", + "install_make_app": "Please first install the Make App in the app store.", "connect_apple_server": "Connect to Apple Server", "calendar_url": "Calendar URL", "apple_server_generate_password": "Generate an app specific password to use with {{appName}} at", @@ -1089,6 +1092,7 @@ "or_lowercase": "or", "nevermind": "Nevermind", "go_to": "Go to: ", + "invite_link_make": "
  • Go toMake Invite Link and install Cal.com appp
  • ", "zapier_invite_link": "Zapier Invite Link", "meeting_url_provided_after_confirmed": "A Meeting URL will be created once the event is confirmed.", "dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee", diff --git a/packages/app-store/_pages/setup/_getStaticProps.tsx b/packages/app-store/_pages/setup/_getStaticProps.tsx index 5e1f963ef6..3a10131c9d 100644 --- a/packages/app-store/_pages/setup/_getStaticProps.tsx +++ b/packages/app-store/_pages/setup/_getStaticProps.tsx @@ -2,6 +2,7 @@ import type { GetStaticPropsContext } from "next"; export const AppSetupPageMap = { zapier: import("../../zapier/pages/setup/_getStaticProps"), + make: import("../../make/pages/setup/_getStaticProps"), }; export const getStaticProps = async (ctx: GetStaticPropsContext) => { diff --git a/packages/app-store/_pages/setup/index.tsx b/packages/app-store/_pages/setup/index.tsx index 22331872a6..6438b04a29 100644 --- a/packages/app-store/_pages/setup/index.tsx +++ b/packages/app-store/_pages/setup/index.tsx @@ -9,6 +9,7 @@ export const AppSetupMap = { "exchange2016-calendar": dynamic(() => import("../../exchange2016calendar/pages/setup")), "caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")), zapier: dynamic(() => import("../../zapier/pages/setup")), + make: dynamic(() => import("../../make/pages/setup")), closecom: dynamic(() => import("../../closecom/pages/setup")), sendgrid: dynamic(() => import("../../sendgrid/pages/setup")), paypal: dynamic(() => import("../../paypal/pages/setup")), diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 019da1d5b9..b3b679d8ed 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -12,6 +12,7 @@ import { appKeysSchema as gtm_zod_ts } from "./gtm/zod"; import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; +import { appKeysSchema as make_zod_ts } from "./make/zod"; import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod"; import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod"; import { appKeysSchema as office365video_zod_ts } from "./office365video/zod"; @@ -43,6 +44,7 @@ export const appKeysSchemas = { hubspot: hubspot_zod_ts, jitsivideo: jitsivideo_zod_ts, larkcalendar: larkcalendar_zod_ts, + make: make_zod_ts, metapixel: metapixel_zod_ts, office365calendar: office365calendar_zod_ts, office365video: office365video_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 3b7308219e..c9c05fa170 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -29,6 +29,7 @@ import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata"; import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata"; +import make_config_json from "./make/config.json"; import metapixel_config_json from "./metapixel/config.json"; import mirotalk_config_json from "./mirotalk/config.json"; import n8n_config_json from "./n8n/config.json"; @@ -99,6 +100,7 @@ export const appStoreMetadata = { huddle01video: huddle01video__metadata_ts, jitsivideo: jitsivideo__metadata_ts, larkcalendar: larkcalendar__metadata_ts, + make: make_config_json, metapixel: metapixel_config_json, mirotalk: mirotalk_config_json, n8n: n8n_config_json, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 49ff7731ec..1801493d85 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -12,6 +12,7 @@ import { appDataSchema as gtm_zod_ts } from "./gtm/zod"; import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; +import { appDataSchema as make_zod_ts } from "./make/zod"; import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod"; import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod"; import { appDataSchema as office365video_zod_ts } from "./office365video/zod"; @@ -43,6 +44,7 @@ export const appDataSchemas = { hubspot: hubspot_zod_ts, jitsivideo: jitsivideo_zod_ts, larkcalendar: larkcalendar_zod_ts, + make: make_zod_ts, metapixel: metapixel_zod_ts, office365calendar: office365calendar_zod_ts, office365video: office365video_zod_ts, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 6eb15be4f0..e37b6e7113 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -29,6 +29,7 @@ export const apiHandlers = { huddle01video: import("./huddle01video/api"), jitsivideo: import("./jitsivideo/api"), larkcalendar: import("./larkcalendar/api"), + make: import("./make/api"), metapixel: import("./metapixel/api"), mirotalk: import("./mirotalk/api"), n8n: import("./n8n/api"), diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 8982003f00..10046d5393 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -26,6 +26,7 @@ const appStore = { webexvideo: () => import("./webex"), giphy: () => import("./giphy"), zapier: () => import("./zapier"), + make: () => import("./make"), exchange2013calendar: () => import("./exchange2013calendar"), exchange2016calendar: () => import("./exchange2016calendar"), exchangecalendar: () => import("./exchangecalendar"), diff --git a/packages/app-store/make/DESCRIPTION.md b/packages/app-store/make/DESCRIPTION.md new file mode 100644 index 0000000000..ff99243ef5 --- /dev/null +++ b/packages/app-store/make/DESCRIPTION.md @@ -0,0 +1,10 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg + - 4.jpeg + - 5.jpeg +--- + +Workflow automation for everyone. Use the Cal.com app in Make to automate your workflows when a booking is created, rescheduled, cancelled or when a meeting has ended. You can also get all your booking with the 'List Bookings' module.

    **After Installation:** Have you lost your API key? You can always generate a new key on the **Make Setup Page** \ No newline at end of file diff --git a/packages/app-store/make/README.md b/packages/app-store/make/README.md new file mode 100644 index 0000000000..e3de1d11c7 --- /dev/null +++ b/packages/app-store/make/README.md @@ -0,0 +1,22 @@ +# Setting up Make Integration + + +1. Install the app from the Cal app store and generate an API key. Copy the API key. +2. Go to `/admin/apps/automation` in Cal and set the `invite_link` for Make to `https://www.make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510` to use the app. +3. Create a [Make account](https://www.make.com/en/login), if you don't have one. +4. Go to `Scenarios` in the sidebar and click on **Create a new scenario**. +5. Search for `Cal.com` in the apps list and select from the list of triggers - Booking Created, Booking Deleted, Booking Rescheduled, Meeting Ended +6. To create a **connection** you will need your Cal deployment url and the app API Key generated above. You only need to create a **connection** once, all webhooks can use that connection. +7. Setup the webhook for the desired event in Make. +8. To delete a webhook, go to `Webhooks` in the left sidebar in Make, pick the webhook you want to delete and click **delete**. + +## Localhost or Self-hosting + +Localhost urls can not be used as the base URL for api endpoints + +Possible solution: using [https://ngrok.com/](https://ngrok.com/) + +1. Create Account +2. [Download](https://ngrok.com/download) ngrok and start a tunnel to your running localhost + - Use forwarding url as your baseUrl for the URL endpoints +3. Use the ngrok url as your Cal deployment url when creating the **Connection** in Make. \ No newline at end of file diff --git a/packages/app-store/make/api/add.ts b/packages/app-store/make/api/add.ts new file mode 100644 index 0000000000..ac5e8078c3 --- /dev/null +++ b/packages/app-store/make/api/add.ts @@ -0,0 +1,20 @@ +import { createDefaultInstallation } from "@calcom/app-store/_utils/installation"; +import type { AppDeclarativeHandler } from "@calcom/types/AppHandler"; + +import appConfig from "../config.json"; + +const handler: AppDeclarativeHandler = { + appType: appConfig.type, + variant: appConfig.variant, + slug: appConfig.slug, + supportsMultipleInstalls: false, + handlerType: "add", + redirect: { + newTab: true, + url: "/apps/make/setup", + }, + createCredential: ({ appType, user, slug, teamId }) => + createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }), +}; + +export default handler; diff --git a/packages/app-store/make/api/index.ts b/packages/app-store/make/api/index.ts new file mode 100644 index 0000000000..d32a6d07b0 --- /dev/null +++ b/packages/app-store/make/api/index.ts @@ -0,0 +1,5 @@ +export { default as add } from "./add"; +export { default as listBookings } from "./subscriptions/listBookings"; +export { default as deleteSubscription } from "./subscriptions/deleteSubscription"; +export { default as addSubscription } from "./subscriptions/addSubscription"; +export { default as me } from "./subscriptions/me"; diff --git a/packages/app-store/make/api/subscriptions/addSubscription.ts b/packages/app-store/make/api/subscriptions/addSubscription.ts new file mode 100644 index 0000000000..3bfd90da16 --- /dev/null +++ b/packages/app-store/make/api/subscriptions/addSubscription.ts @@ -0,0 +1,38 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; +import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const apiKey = req.query.apiKey as string; + + if (!apiKey) { + return res.status(401).json({ message: "No API key provided" }); + } + + const validKey = await findValidApiKey(apiKey, "make"); + + if (!validKey) { + return res.status(401).json({ message: "API key not valid" }); + } + + const { subscriberUrl, triggerEvent } = req.body; + + const createAppSubscription = await addSubscription({ + appApiKey: validKey, + triggerEvent: triggerEvent, + subscriberUrl: subscriberUrl, + appId: "make", + }); + + if (!createAppSubscription) { + return res.status(500).json({ message: "Could not create subscription." }); + } + + res.status(200).json(createAppSubscription); +} + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/make/api/subscriptions/deleteSubscription.ts b/packages/app-store/make/api/subscriptions/deleteSubscription.ts new file mode 100644 index 0000000000..20b55e7714 --- /dev/null +++ b/packages/app-store/make/api/subscriptions/deleteSubscription.ts @@ -0,0 +1,40 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import z from "zod"; + +import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; +import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +const querySchema = z.object({ + apiKey: z.string(), + id: z.string(), +}); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { apiKey, id } = querySchema.parse(req.query); + + if (!apiKey) { + return res.status(401).json({ message: "No API key provided" }); + } + + const validKey = await findValidApiKey(apiKey, "make"); + + if (!validKey) { + return res.status(401).json({ message: "API key not valid" }); + } + + const deleteEventSubscription = await deleteSubscription({ + appApiKey: validKey, + webhookId: id, + appId: "make", + }); + + if (!deleteEventSubscription) { + return res.status(500).json({ message: "Could not delete subscription." }); + } + res.status(204).json({ message: "Subscription is deleted." }); +} + +export default defaultHandler({ + DELETE: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/make/api/subscriptions/listBookings.ts b/packages/app-store/make/api/subscriptions/listBookings.ts new file mode 100644 index 0000000000..b4204fbe94 --- /dev/null +++ b/packages/app-store/make/api/subscriptions/listBookings.ts @@ -0,0 +1,35 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; +import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const apiKey = req.query.apiKey as string; + + if (!apiKey) { + return res.status(401).json({ message: "No API key provided" }); + } + + const validKey = await findValidApiKey(apiKey, "make"); + + if (!validKey) { + return res.status(401).json({ message: "API key not valid" }); + } + const bookings = await listBookings(validKey); + + if (!bookings) { + return res.status(500).json({ message: "Unable to get bookings." }); + } + if (bookings.length === 0) { + const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId; + return res.status(404).json({ + message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``, + }); + } + res.status(201).json(bookings); +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/make/api/subscriptions/me.ts b/packages/app-store/make/api/subscriptions/me.ts new file mode 100644 index 0000000000..f02a84c9d2 --- /dev/null +++ b/packages/app-store/make/api/subscriptions/me.ts @@ -0,0 +1,35 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const apiKey = req.query.apiKey as string; + + if (!apiKey) { + return res.status(401).json({ message: "No API key provided" }); + } + + const validKey = await findValidApiKey(apiKey, "make"); + + if (!validKey) { + return res.status(401).json({ message: "API key not valid" }); + } + + if (req.method === "GET") { + try { + const user = await prisma.user.findFirst({ + where: { + id: validKey.userId, + }, + select: { + username: true, + }, + }); + res.status(201).json(user); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Unable to get User." }); + } + } +} diff --git a/packages/app-store/make/config.json b/packages/app-store/make/config.json new file mode 100644 index 0000000000..b88840f47d --- /dev/null +++ b/packages/app-store/make/config.json @@ -0,0 +1,18 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Make", + "slug": "make", + "type": "make_automation", + "logo": "icon.svg", + "url": "https://cal.com/apps/make", + "variant": "automation", + "categories": ["automation"], + "publisher": "aa2dee2", + "email": "support@cal.com", + "description": "Automate workflows", + "isTemplate": false, + "__createdUsingCli": true, + "__template": "basic", + "imageSrc": "icon.svg", + "dirName": "make" +} diff --git a/packages/app-store/make/index.ts b/packages/app-store/make/index.ts new file mode 100644 index 0000000000..d7f3602204 --- /dev/null +++ b/packages/app-store/make/index.ts @@ -0,0 +1 @@ +export * as api from "./api"; diff --git a/packages/app-store/make/package.json b/packages/app-store/make/package.json new file mode 100644 index 0000000000..ce3fc5fa9b --- /dev/null +++ b/packages/app-store/make/package.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/make", + "version": "0.0.0", + "main": "./index.ts", + "description": "Workflow automation for everyone. Use the Cal.com Make app to trigger your workflows when a booking is created, rescheduled, or cancelled, or after a meeting ends.", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*", + "@types/node-schedule": "^2.1.0" + } +} diff --git a/packages/app-store/make/pages/setup/_getStaticProps.tsx b/packages/app-store/make/pages/setup/_getStaticProps.tsx new file mode 100644 index 0000000000..1c6cac0efe --- /dev/null +++ b/packages/app-store/make/pages/setup/_getStaticProps.tsx @@ -0,0 +1,20 @@ +import type { GetStaticPropsContext } from "next"; + +import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug"; + +export interface IMakeSetupProps { + inviteLink: string; +} + +export const getStaticProps = async (ctx: GetStaticPropsContext) => { + if (typeof ctx.params?.slug !== "string") return { notFound: true } as const; + let inviteLink = ""; + const appKeys = await getAppKeysFromSlug("make"); + if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link; + + return { + props: { + inviteLink, + }, + }; +}; diff --git a/packages/app-store/make/pages/setup/index.tsx b/packages/app-store/make/pages/setup/index.tsx new file mode 100644 index 0000000000..7f097dd14e --- /dev/null +++ b/packages/app-store/make/pages/setup/index.tsx @@ -0,0 +1,178 @@ +import type { InferGetStaticPropsType } from "next"; +import { Trans } from "next-i18next"; +import Link from "next/link"; +import { useState } from "react"; +import { Toaster } from "react-hot-toast"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { Button, Tooltip, showToast } from "@calcom/ui"; +import { Clipboard } from "@calcom/ui/components/icon"; + +import type { getStaticProps } from "./_getStaticProps"; + +const MAKE = "make"; + +export default function MakeSetup({ inviteLink }: InferGetStaticPropsType) { + const [newApiKeys, setNewApiKeys] = useState>({}); + + const { t } = useLocale(); + const utils = trpc.useContext(); + const integrations = trpc.viewer.integrations.useQuery({ variant: "automation" }); + const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: MAKE }); + const teamsList = trpc.viewer.teams.listOwnedTeams.useQuery(undefined, { + refetchOnWindowFocus: false, + }); + const teams = teamsList.data?.map((team) => ({ id: team.id, name: team.name })); + const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation({ + onSuccess: () => { + utils.viewer.apiKeys.findKeyOfType.invalidate(); + }, + }); + const makeCredentials: { userCredentialIds: number[] } | undefined = integrations.data?.items.find( + (item: { type: string }) => item.type === "make_automation" + ); + const [credentialId] = makeCredentials?.userCredentialIds || [false]; + const showContent = integrations.data && integrations.isSuccess && credentialId; + + async function createApiKey(teamId?: number) { + const event = { note: "Make", expiresAt: null, appId: MAKE, teamId }; + const apiKey = await utils.client.viewer.apiKeys.create.mutate(event); + + if (oldApiKey.data) { + const oldKey = teamId + ? oldApiKey.data.find((key) => key.teamId === teamId) + : oldApiKey.data.find((key) => !key.teamId); + + if (oldKey) { + deleteApiKey.mutate({ + id: oldKey.id, + }); + } + } + + return apiKey; + } + + async function generateApiKey(teamId?: number) { + const apiKey = await createApiKey(teamId); + setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey }); + } + + if (integrations.isLoading) { + return
    ; + } + + return ( +
    + {showContent ? ( +
    +
    +
    + Make Logo +
    +
    +
    {t("setting_up_make")}
    + + <> +
    {t("generate_api_key")}:
    + {!teams ? ( + + ) : ( + <> +
    Your event types:
    + {!newApiKeys[""] ? ( + + ) : ( + + )} + {teams.map((team) => { + return ( +
    +
    {team.name}:
    + {!newApiKeys[team.id] ? ( + + ) : ( + + )} +
    + ); + })} + + )} + + +
      + +
    1. + Go to + + Make Invite Link + + and install the Cal.com app. +
    2. +
      + + +
    3. Log into your Make account and create a new Scenario.
    4. +
    5. Select Cal.com as your Trigger app. Also choose a Trigger event.
    6. +
    7. Choose your account and then enter your Unique API Key.
    8. +
    9. Test your Trigger.
    10. +
    11. You're set!
    12. +
      +
    + + + +
    +
    +
    + ) : ( +
    +
    {t("install_make_app")}
    +
    + + + +
    +
    + )} + +
    + ); +} + +const CopyApiKey = ({ apiKey }: { apiKey: string }) => { + const { t } = useLocale(); + return ( +
    +
    + + {apiKey} + + + + +
    +
    {t("copy_somewhere_safe")}
    +
    + ); +}; diff --git a/packages/app-store/make/static/1.jpeg b/packages/app-store/make/static/1.jpeg new file mode 100644 index 0000000000..54f811f918 Binary files /dev/null and b/packages/app-store/make/static/1.jpeg differ diff --git a/packages/app-store/make/static/2.jpeg b/packages/app-store/make/static/2.jpeg new file mode 100644 index 0000000000..c6637a01c8 Binary files /dev/null and b/packages/app-store/make/static/2.jpeg differ diff --git a/packages/app-store/make/static/3.jpeg b/packages/app-store/make/static/3.jpeg new file mode 100644 index 0000000000..15c25d425f Binary files /dev/null and b/packages/app-store/make/static/3.jpeg differ diff --git a/packages/app-store/make/static/4.jpeg b/packages/app-store/make/static/4.jpeg new file mode 100644 index 0000000000..6ae8a3cabc Binary files /dev/null and b/packages/app-store/make/static/4.jpeg differ diff --git a/packages/app-store/make/static/5.jpeg b/packages/app-store/make/static/5.jpeg new file mode 100644 index 0000000000..29667aff9a Binary files /dev/null and b/packages/app-store/make/static/5.jpeg differ diff --git a/packages/app-store/make/static/icon.svg b/packages/app-store/make/static/icon.svg new file mode 100644 index 0000000000..3332f2c81f --- /dev/null +++ b/packages/app-store/make/static/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/app-store/make/zod.ts b/packages/app-store/make/zod.ts new file mode 100644 index 0000000000..4a98d79116 --- /dev/null +++ b/packages/app-store/make/zod.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const appDataSchema = z.object({}); + +export const appKeysSchema = z.object({ + invite_link: z.string().min(1), +}); diff --git a/packages/app-store/zapier/api/subscriptions/addSubscription.ts b/packages/app-store/zapier/api/subscriptions/addSubscription.ts index 3bc3ecc2f4..675f161be1 100644 --- a/packages/app-store/zapier/api/subscriptions/addSubscription.ts +++ b/packages/app-store/zapier/api/subscriptions/addSubscription.ts @@ -1,12 +1,8 @@ -import type { Prisma } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { v4 } from "uuid"; import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; -import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; -import prisma from "@calcom/prisma"; -import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums"; async function handler(req: NextApiRequest, res: NextApiResponse) { const apiKey = req.query.apiKey as string; @@ -23,45 +19,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const { subscriberUrl, triggerEvent } = req.body; - try { - const createSubscription = await prisma.webhook.create({ - data: { - id: v4(), - userId: validKey.userId, - teamId: validKey.teamId, - eventTriggers: [triggerEvent], - subscriberUrl, - active: true, - appId: "zapier", - }, - }); + const createAppSubscription = await addSubscription({ + appApiKey: validKey, + triggerEvent: triggerEvent, + subscriberUrl: subscriberUrl, + appId: "zapier", + }); - if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) { - //schedule job for already existing bookings - const where: Prisma.BookingWhereInput = {}; - if (validKey.teamId) where.eventType = { teamId: validKey.teamId }; - else where.userId = validKey.userId; - const bookings = await prisma.booking.findMany({ - where: { - ...where, - startTime: { - gte: new Date(), - }, - status: BookingStatus.ACCEPTED, - }, - }); - - for (const booking of bookings) { - scheduleTrigger(booking, createSubscription.subscriberUrl, { - id: createSubscription.id, - appId: createSubscription.appId, - }); - } - } - res.status(200).json(createSubscription); - } catch (error) { + if (!createAppSubscription) { return res.status(500).json({ message: "Could not create subscription." }); } + + res.status(200).json(createAppSubscription); } export default defaultHandler({ diff --git a/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts b/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts index b0887ac21b..81469a30ff 100644 --- a/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts +++ b/packages/app-store/zapier/api/subscriptions/deleteSubscription.ts @@ -1,11 +1,9 @@ -import type { Prisma } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import z from "zod"; import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; +import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { defaultHandler, defaultResponder } from "@calcom/lib/server"; -import prisma from "@calcom/prisma"; -import { WebhookTriggerEvents } from "@calcom/prisma/enums"; const querySchema = z.object({ apiKey: z.string(), @@ -24,49 +22,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (!validKey) { return res.status(401).json({ message: "API key not valid" }); } - const webhook = await prisma.webhook.findFirst({ - where: { - id, - userId: validKey.userId, - teamId: validKey.teamId, - }, + + const deleteEventSubscription = await deleteSubscription({ + appApiKey: validKey, + webhookId: id, + appId: "zapier", }); - if (!webhook) { - return res.status(401).json({ message: "Not authorized to delete this webhook" }); + if (!deleteEventSubscription) { + return res.status(500).json({ message: "Could not delete subscription." }); } - if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) { - const where: Prisma.BookingWhereInput = {}; - if (validKey.teamId) where.eventType = { teamId: validKey.teamId }; - else where.userId = validKey.userId; - const bookingsWithScheduledJobs = await prisma.booking.findMany({ - where: { - ...where, - scheduledJobs: { - isEmpty: false, - }, - }, - }); - for (const booking of bookingsWithScheduledJobs) { - const updatedScheduledJobs = booking.scheduledJobs.filter( - (scheduledJob) => scheduledJob !== `zapier_${webhook.id}` - ); - await prisma.booking.update({ - where: { - id: booking.id, - }, - data: { - scheduledJobs: updatedScheduledJobs, - }, - }); - } - } - - await prisma.webhook.delete({ - where: { - id, - }, - }); res.status(204).json({ message: "Subscription is deleted." }); } diff --git a/packages/app-store/zapier/api/subscriptions/listBookings.ts b/packages/app-store/zapier/api/subscriptions/listBookings.ts index 7cf0510339..f3a59c690a 100644 --- a/packages/app-store/zapier/api/subscriptions/listBookings.ts +++ b/packages/app-store/zapier/api/subscriptions/listBookings.ts @@ -1,11 +1,8 @@ -import type { Prisma } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { getHumanReadableLocationValue } from "@calcom/core/location"; -import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; -import { defaultHandler, defaultResponder, getTranslation } from "@calcom/lib/server"; -import prisma from "@calcom/prisma"; +import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; async function handler(req: NextApiRequest, res: NextApiResponse) { const apiKey = req.query.apiKey as string; @@ -20,88 +17,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return res.status(401).json({ message: "API key not valid" }); } - try { - const where: Prisma.BookingWhereInput = {}; - if (validKey.teamId) { - where.eventType = { - OR: [{ teamId: validKey.teamId }, { parent: { teamId: validKey.teamId } }], - }; - } else { - where.userId = validKey.userId; - } + const bookings = await listBookings(validKey); - const bookings = await prisma.booking.findMany({ - take: 3, - where, - orderBy: { - id: "desc", - }, - select: { - title: true, - description: true, - customInputs: true, - responses: true, - startTime: true, - endTime: true, - location: true, - cancellationReason: true, - status: true, - user: { - select: { - username: true, - name: true, - email: true, - timeZone: true, - locale: true, - }, - }, - eventType: { - select: { - title: true, - description: true, - requiresConfirmation: true, - price: true, - currency: true, - length: true, - bookingFields: true, - team: true, - }, - }, - attendees: { - select: { - name: true, - email: true, - timeZone: true, - }, - }, - }, - }); - - if (bookings.length === 0) { - const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId; - return res.status(404).json({ - message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``, - }); - } - - const t = await getTranslation(bookings[0].user?.locale ?? "en", "common"); - - const updatedBookings = bookings.map((booking) => { - return { - ...booking, - ...getCalEventResponses({ - bookingFields: booking.eventType?.bookingFields ?? null, - booking, - }), - location: getHumanReadableLocationValue(booking.location || "", t), - }; - }); - - res.status(201).json(updatedBookings); - } catch (error) { - console.error(error); + if (!bookings) { return res.status(500).json({ message: "Unable to get bookings." }); } + if (bookings.length === 0) { + const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId; + return res.status(404).json({ + message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``, + }); + } + res.status(201).json(bookings); } export default defaultHandler({ diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts index 8b610d55a7..aa5409101b 100644 --- a/packages/features/webhooks/lib/scheduleTrigger.ts +++ b/packages/features/webhooks/lib/scheduleTrigger.ts @@ -1,5 +1,205 @@ +import type { Prisma } from "@prisma/client"; +import { v4 } from "uuid"; + +import { getHumanReadableLocationValue } from "@calcom/core/location"; +import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import logger from "@calcom/lib/logger"; +import { getTranslation } from "@calcom/lib/server"; import prisma from "@calcom/prisma"; -import { WebhookTriggerEvents } from "@calcom/prisma/enums"; +import type { ApiKey } from "@calcom/prisma/client"; +import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums"; + +const log = logger.getChildLogger({ prefix: ["[node-scheduler]"] }); + +export async function addSubscription({ + appApiKey, + triggerEvent, + subscriberUrl, + appId, +}: { + appApiKey: ApiKey; + triggerEvent: WebhookTriggerEvents; + subscriberUrl: string; + appId: string; +}) { + try { + const createSubscription = await prisma.webhook.create({ + data: { + id: v4(), + userId: appApiKey.userId, + teamId: appApiKey.teamId, + eventTriggers: [triggerEvent], + subscriberUrl, + active: true, + appId: appId, + }, + }); + + if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) { + //schedule job for already existing bookings + const where: Prisma.BookingWhereInput = {}; + if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId }; + else where.userId = appApiKey.userId; + const bookings = await prisma.booking.findMany({ + where: { + ...where, + startTime: { + gte: new Date(), + }, + status: BookingStatus.ACCEPTED, + }, + }); + + for (const booking of bookings) { + scheduleTrigger(booking, createSubscription.subscriberUrl, { + id: createSubscription.id, + appId: createSubscription.appId, + }); + } + } + + return createSubscription; + } catch (error) { + log.error(`Error creating subscription for user ${appApiKey.userId} and appId ${appApiKey.appId}.`); + } +} + +export async function deleteSubscription({ + appApiKey, + webhookId, + appId, +}: { + appApiKey: ApiKey; + webhookId: string; + appId: string; +}) { + try { + const webhook = await prisma.webhook.findFirst({ + where: { + id: webhookId, + }, + }); + + if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) { + const where: Prisma.BookingWhereInput = {}; + if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId }; + else where.userId = appApiKey.userId; + const bookingsWithScheduledJobs = await prisma.booking.findMany({ + where: { + ...where, + scheduledJobs: { + isEmpty: false, + }, + }, + }); + for (const booking of bookingsWithScheduledJobs) { + const updatedScheduledJobs = booking.scheduledJobs.filter( + (scheduledJob) => scheduledJob !== `${appId}_${webhook.id}` + ); + await prisma.booking.update({ + where: { + id: booking.id, + }, + data: { + scheduledJobs: updatedScheduledJobs, + }, + }); + } + } + + const deleteWebhook = await prisma.webhook.delete({ + where: { + id: webhookId, + }, + }); + if (!deleteWebhook) { + throw new Error(`Unable to delete webhook ${webhookId}`); + } + return deleteWebhook; + } catch (err) { + log.error( + `Error deleting subscription for user ${appApiKey.userId}, webhookId ${webhookId}, appId ${appId}` + ); + } +} + +export async function listBookings(appApiKey: ApiKey) { + try { + const where: Prisma.BookingWhereInput = {}; + if (appApiKey.teamId) { + where.eventType = { + OR: [{ teamId: appApiKey.teamId }, { parent: { teamId: appApiKey.teamId } }], + }; + } else { + where.userId = appApiKey.userId; + } + const bookings = await prisma.booking.findMany({ + take: 3, + where: where, + orderBy: { + id: "desc", + }, + select: { + title: true, + description: true, + customInputs: true, + responses: true, + startTime: true, + endTime: true, + location: true, + cancellationReason: true, + status: true, + user: { + select: { + username: true, + name: true, + email: true, + timeZone: true, + locale: true, + }, + }, + eventType: { + select: { + title: true, + description: true, + requiresConfirmation: true, + price: true, + currency: true, + length: true, + bookingFields: true, + team: true, + }, + }, + attendees: { + select: { + name: true, + email: true, + timeZone: true, + }, + }, + }, + }); + if (bookings.length === 0) { + return []; + } + const t = await getTranslation(bookings[0].user?.locale ?? "en", "common"); + + const updatedBookings = bookings.map((booking) => { + return { + ...booking, + ...getCalEventResponses({ + bookingFields: booking.eventType?.bookingFields ?? null, + booking, + }), + location: getHumanReadableLocationValue(booking.location || "", t), + }; + }); + + return updatedBookings; + } catch (err) { + log.error(`Error retrieving list of bookings for user ${appApiKey.userId} and appId ${appApiKey.appId}.`); + } +} export async function scheduleTrigger( booking: { id: number; endTime: Date; scheduledJobs: string[] }, diff --git a/packages/prisma/seed-app-store.ts b/packages/prisma/seed-app-store.ts index 56dd6e1133..d4bff45b70 100644 --- a/packages/prisma/seed-app-store.ts +++ b/packages/prisma/seed-app-store.ts @@ -337,7 +337,9 @@ export default async function main() { invite_link: process.env.ZAPIER_INVITE_LINK, }); } - + await createApp("make", "make", ["automation"], "make_automation", { + invite_link: "https://make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510", + }); await createApp("huddle01", "huddle01video", ["conferencing"], "huddle01_video"); // Payment apps