first commit
This commit is contained in:
10
calcom/packages/app-store/make/DESCRIPTION.md
Normal file
10
calcom/packages/app-store/make/DESCRIPTION.md
Normal file
@ -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.<br /><br />**After Installation:** Have you lost your API key? You can always generate a new key on the <a href="/apps/make/setup">**<ins>Make Setup Page</ins>**</a>
|
21
calcom/packages/app-store/make/README.md
Normal file
21
calcom/packages/app-store/make/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# 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.
|
20
calcom/packages/app-store/make/api/add.ts
Normal file
20
calcom/packages/app-store/make/api/add.ts
Normal file
@ -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, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
5
calcom/packages/app-store/make/api/index.ts
Normal file
5
calcom/packages/app-store/make/api/index.ts
Normal file
@ -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";
|
@ -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) }),
|
||||
});
|
@ -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) }),
|
||||
});
|
@ -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) }),
|
||||
});
|
35
calcom/packages/app-store/make/api/subscriptions/me.ts
Normal file
35
calcom/packages/app-store/make/api/subscriptions/me.ts
Normal file
@ -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." });
|
||||
}
|
||||
}
|
||||
}
|
19
calcom/packages/app-store/make/config.json
Normal file
19
calcom/packages/app-store/make/config.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Make",
|
||||
"slug": "make",
|
||||
"type": "make_automation",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://github.com/aar2dee2",
|
||||
"variant": "automation",
|
||||
"categories": ["automation"],
|
||||
"publisher": "aar2dee2",
|
||||
"email": "support@cal.com",
|
||||
"description": "From tasks and workflows to apps and systems, build and automate anything in one powerful visual platform.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"imageSrc": "icon.svg",
|
||||
"dirName": "make",
|
||||
"isOAuth": false
|
||||
}
|
1
calcom/packages/app-store/make/index.ts
Normal file
1
calcom/packages/app-store/make/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as api from "./api";
|
15
calcom/packages/app-store/make/package.json
Normal file
15
calcom/packages/app-store/make/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug";
|
||||
|
||||
export interface IMakeSetupProps {
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const notFound = { notFound: true } as const;
|
||||
|
||||
if (typeof ctx.params?.slug !== "string") return notFound;
|
||||
let inviteLink = "";
|
||||
const appKeys = await getAppKeysFromSlug("make");
|
||||
if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link;
|
||||
|
||||
return {
|
||||
props: {
|
||||
inviteLink,
|
||||
},
|
||||
};
|
||||
};
|
174
calcom/packages/app-store/make/pages/setup/index.tsx
Normal file
174
calcom/packages/app-store/make/pages/setup/index.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import type { InferGetServerSidePropsType } 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, Icon, showToast, Tooltip } from "@calcom/ui";
|
||||
|
||||
import type { getServerSideProps } from "./_getServerSideProps";
|
||||
|
||||
const MAKE = "make";
|
||||
|
||||
export default function MakeSetup({ inviteLink }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const [newApiKeys, setNewApiKeys] = useState<Record<string, string>>({});
|
||||
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
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.isPending) {
|
||||
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-emphasis flex h-screen">
|
||||
{showContent ? (
|
||||
<div className="bg-default m-auto max-w-[43em] overflow-auto rounded pb-10 md:p-10">
|
||||
<div className="md:flex md:flex-row">
|
||||
<div className="invisible md:visible">
|
||||
<img className="h-11" src="/api/app-store/make/icon.svg" alt="Make Logo" />
|
||||
</div>
|
||||
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
|
||||
<div className="text-default">{t("setting_up_make")}</div>
|
||||
|
||||
<>
|
||||
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
|
||||
{!teams ? (
|
||||
<Button color="secondary" onClick={() => createApiKey()} className="mb-4 mt-2">
|
||||
{t("generate_api_key")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-8 text-sm font-semibold">Your event types:</div>
|
||||
{!newApiKeys[""] ? (
|
||||
<Button color="secondary" onClick={() => generateApiKey()} className="mb-4 mt-2">
|
||||
{t("generate_api_key")}
|
||||
</Button>
|
||||
) : (
|
||||
<CopyApiKey apiKey={newApiKeys[""]} />
|
||||
)}
|
||||
{teams.map((team) => {
|
||||
return (
|
||||
<div key={team.name}>
|
||||
<div className="mt-2 text-sm font-semibold">{team.name}:</div>
|
||||
{!newApiKeys[team.id] ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => generateApiKey(team.id)}
|
||||
className="mb-4 mt-2">
|
||||
{t("generate_api_key")}
|
||||
</Button>
|
||||
) : (
|
||||
<CopyApiKey apiKey={newApiKeys[team.id]} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
<ol className="mb-5 ml-5 mt-5 list-decimal ltr:mr-5 rtl:ml-5">
|
||||
<Trans i18nKey="make_setup_instructions">
|
||||
<li>
|
||||
Go to
|
||||
<a href={inviteLink} className="ml-1 mr-1 text-orange-600 underline">
|
||||
Make Invite Link
|
||||
</a>
|
||||
and install the Cal.com app.
|
||||
</li>
|
||||
<li>Log into your Make account and create a new Scenario.</li>
|
||||
<li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li>
|
||||
<li>Choose your account and then enter your Unique API Key.</li>
|
||||
<li>Test your Trigger.</li>
|
||||
<li>You're set!</li>
|
||||
</Trans>
|
||||
</ol>
|
||||
<Link href="/apps/installed/automation?hl=make" passHref={true} legacyBehavior>
|
||||
<Button color="secondary">{t("done")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-5 mt-5">
|
||||
<div>{t("install_make_app")}</div>
|
||||
<div className="mt-3">
|
||||
<Link href="/apps/make" passHref={true} legacyBehavior>
|
||||
<Button>{t("go_to_app_store")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div>
|
||||
<div className="my-2 mt-3 flex-wrap sm:flex sm:flex-nowrap">
|
||||
<code className="bg-subtle h-full w-full whitespace-pre-wrap rounded-md py-[6px] pl-2 pr-2 sm:rounded-r-none sm:pr-5">
|
||||
{apiKey}
|
||||
</code>
|
||||
<Tooltip side="top" content={t("copy_to_clipboard")}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
showToast(t("api_key_copied"), "success");
|
||||
}}
|
||||
type="button"
|
||||
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
|
||||
<Icon name="clipboard" className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-subtle mb-5 mt-2 text-sm">{t("copy_somewhere_safe")}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
BIN
calcom/packages/app-store/make/static/1.jpeg
Normal file
BIN
calcom/packages/app-store/make/static/1.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
calcom/packages/app-store/make/static/2.jpeg
Normal file
BIN
calcom/packages/app-store/make/static/2.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
calcom/packages/app-store/make/static/3.jpeg
Normal file
BIN
calcom/packages/app-store/make/static/3.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
BIN
calcom/packages/app-store/make/static/4.jpeg
Normal file
BIN
calcom/packages/app-store/make/static/4.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
calcom/packages/app-store/make/static/5.jpeg
Normal file
BIN
calcom/packages/app-store/make/static/5.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
6
calcom/packages/app-store/make/static/icon.svg
Normal file
6
calcom/packages/app-store/make/static/icon.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<g id="Make-App-Icon-Circle" transform="translate(3757 -1767)">
|
||||
<circle id="Ellipse_10" data-name="Ellipse 10" cx="256" cy="256" r="256" transform="translate(-3757 1767)" fill="#6d00cc"/>
|
||||
<path id="Path_141560" data-name="Path 141560" d="M244.78,14.544a7.187,7.187,0,0,0-7.186,7.192V213.927a7.19,7.19,0,0,0,7.186,7.192h52.063a7.187,7.187,0,0,0,7.186-7.192V21.736a7.183,7.183,0,0,0-7.186-7.192ZM92.066,17.083,5.77,188.795a7.191,7.191,0,0,0,3.192,9.654l46.514,23.379a7.184,7.184,0,0,0,9.654-3.2l86.3-171.711a7.184,7.184,0,0,0-3.2-9.654L101.719,13.886a7.2,7.2,0,0,0-9.654,3.2m72.592.614L127.731,204.876a7.189,7.189,0,0,0,5.632,8.442l51.028,10.306a7.2,7.2,0,0,0,8.481-5.665L229.8,30.786a7.19,7.19,0,0,0-5.637-8.442L173.133,12.038a7.391,7.391,0,0,0-1.427-.144,7.194,7.194,0,0,0-7.048,5.8" transform="translate(-3676.356 1905.425)" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 952 B |
7
calcom/packages/app-store/make/zod.ts
Normal file
7
calcom/packages/app-store/make/zod.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const appDataSchema = z.object({});
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
invite_link: z.string().min(1),
|
||||
});
|
Reference in New Issue
Block a user