Compare commits

...

55 Commits

Author SHA1 Message Date
Hariom Balhara 421f601441 Merge remote-tracking branch 'aar2dee2/integromat-app' into integromat-app 2023-08-31 15:52:00 +05:30
Hariom Balhara 789b06af28 Fix inviteLink reading 2023-08-31 15:51:51 +05:30
aar2dee2 bb29b2070f use logger for console logs 2023-08-31 15:49:15 +05:30
aar2dee2 a236ecad02 use default installation handler to install app 2023-08-31 15:40:23 +05:30
Hariom Balhara 786ca13e2e
Update turbo.json 2023-08-31 15:33:52 +05:30
aar2dee2 e0a8cf9af2 fix listBookings handler 2023-08-31 13:22:12 +05:30
aar2dee2 c4bfe98798 fix param name in deleteSubcription 2023-08-31 13:14:34 +05:30
aar2dee2 d4bb915cc8 update app readme 2023-08-31 13:05:49 +05:30
Hariom Balhara 6ace33ebe8
Update .env.appStore.example 2023-08-31 12:15:51 +05:30
Hariom Balhara f21ae3beca
Update .env.appStore.example 2023-08-31 12:15:29 +05:30
Hariom Balhara 84d850466c
Update packages/prisma/seed-app-store.ts 2023-08-31 12:14:37 +05:30
Peer Richelsen 6bfabe41e5
Merge branch 'main' into integromat-app 2023-08-31 00:38:43 +02:00
Peer Richelsen 4058cbc50e
Merge branch 'main' into integromat-app 2023-08-30 14:49:52 +02:00
aar2dee2 cc252433d8 Merge branch 'main' into integromat-app 2023-08-29 19:55:26 +05:30
aar2dee2 2a3d58fc7b fix type errors 2023-08-25 23:45:17 +05:30
aar2dee2 28c570bc60 Merge branch 'main' into integromat-app 2023-08-25 23:25:13 +05:30
aar2dee2 2d7f05461c revert yarn.lock 2023-08-25 23:22:26 +05:30
aar2dee2 d92a6abe7e fix: update link in readme 2023-08-25 23:09:40 +05:30
aar2dee2 613b3ae943 chore: handle error, cleanup readme, address review comments 2023-08-25 23:09:02 +05:30
aar2dee2 398b6d0e59 get app invite link from env vars 2023-08-25 15:04:25 +05:30
aar2dee2 70c1f3eec3 delete unused template files 2023-08-25 15:00:51 +05:30
Carina Wollendorfer 230ebbd92f
Delete .gitkeep 2023-08-24 15:59:31 -04:00
aar2dee2 01ba41e21f Merge branch 'main' into integromat-app 2023-08-14 12:22:39 +05:30
aar2dee2 9536f33081 update app code 2023-08-14 12:06:17 +05:30
aar2dee2 1f85a272e0 fix type errors 2023-08-14 11:39:28 +05:30
aar2dee2 b696af81a2 merge main 2023-08-14 11:18:53 +05:30
aar2dee2 02e037e733 fix app description 2023-07-05 21:34:53 +05:30
aar2dee2 fb59ff01a3 add app screenshots 2023-07-05 21:32:47 +05:30
aar2dee2 3171b26700 update icon 2023-07-05 21:19:47 +05:30
aar2dee2 5c94655a3c revert settings.json 2023-07-05 21:00:42 +05:30
Peer Richelsen 1ef52c297e
Merge branch 'main' into integromat-app 2023-07-05 15:17:27 +02:00
aar2dee2 b0f89dfa89 fix lint errors 2023-07-04 16:07:28 +05:30
aar2dee2 a25f97ab6d Merge branch 'main' into integromat-app 2023-07-04 09:28:37 +05:30
Carina Wollendorfer f5911d8995
Merge branch 'main' into integromat-app 2023-07-03 09:26:02 -04:00
Peer Richelsen c41dbe65c3
Merge branch 'main' into integromat-app 2023-07-03 08:32:39 +02:00
aar2dee2 89c819333f Merge branch 'main' into integromat-app 2023-07-01 22:41:39 +05:30
aar2dee2 d7067a1d49 subscribe unsubscribe in zapier using common nodeScheduler 2023-07-01 22:27:49 +05:30
aar2dee2 486a24ca62 move delete subscription to scheduler 2023-07-01 22:21:34 +05:30
aar2dee2 e63430d48d Merge branch 'main' into integromat-app 2023-06-28 11:49:34 +05:30
aar2dee2 e208d9001e move add subscription to node scheduler 2023-06-06 12:21:47 +05:30
aar2dee2 f1120334f8 Merge branch 'main' into integromat-app 2023-06-06 10:43:04 +05:30
aar2dee2 2a5d2ea38d move scheduler to app-store utils 2023-06-05 17:19:28 +05:30
aar2dee2 ea6f486020 Merge branch 'main' into integromat-app 2023-06-05 10:01:36 +05:30
aar2dee2 c0ea35ff96 Merge branch 'main' into integromat-app 2023-05-27 18:40:13 +05:30
aar2dee2 c356f188ba update make readme 2023-05-27 18:34:59 +05:30
aar2dee2 684e231802 fix module import error 2023-05-25 19:51:05 +05:30
aar2dee2 993f8bb429 add app store imports 2023-05-25 18:30:57 +05:30
aar2dee2 cacd62b926 fix typo 2023-05-25 17:31:14 +05:30
aar2dee2 a47f36ac99 import setup route in app setups 2023-05-25 17:28:21 +05:30
aar2dee2 75a4731c80 update app metadata 2023-05-25 16:48:13 +05:30
aar2dee2 21f3c618dc add relevant env vars 2023-05-25 16:13:16 +05:30
aar2dee2 e639b8891d setup integration 2023-05-25 16:11:30 +05:30
aar2dee2 be0a13d407 Merge branch 'main' into integromat-app 2023-05-25 10:56:21 +05:30
aar2dee2 6d9c0fa84e Merge branch 'main' into integromat-app 2023-05-17 10:28:33 +05:30
aar2dee2 aa59464331 start make app integration 2023-05-15 13:48:19 +05:30
39 changed files with 791 additions and 271 deletions

View File

@ -1052,12 +1052,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.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"make_setup_instructions": "<0>Log into your Make account and create a new Scenario.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"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 @@
"nevermind": "Nevermind",
"go_to": "Go to: ",
"zapier_invite_link": "Zapier Invite Link",
"make_invite_link": "Make 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",
"event_location": "Event's location",

View File

@ -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) => {

View File

@ -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")),

View File

@ -0,0 +1,299 @@
import type { Prisma } from "@prisma/client";
import schedule from "node-schedule";
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 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) {
return 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) {
return 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) {
return 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[] },
subscriberUrl: string,
subscriber: { id: string; appId: string | null }
) {
try {
//schedule job to call subscriber url at the end of meeting
// FIXME: in-process scheduling - job will vanish on server crash / restart
const job = schedule.scheduleJob(
`${subscriber.appId}_${subscriber.id}`,
booking.endTime,
async function () {
const body = JSON.stringify(booking);
await fetch(subscriberUrl, {
method: "POST",
body,
});
//remove scheduled job from bookings once triggered
const updatedScheduledJobs = booking.scheduledJobs.filter((scheduledJob) => {
return scheduledJob !== `${subscriber.appId}_${subscriber.id}`;
});
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: updatedScheduledJobs,
},
});
}
);
//add scheduled job name to booking
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: {
push: job.name,
},
},
});
} catch (error) {
log.error("Error cancelling scheduled jobs", error);
}
}
export async function cancelScheduledJobs(
booking: { uid: string; scheduledJobs?: string[] },
appId?: string | null,
isReschedule?: boolean
) {
if (!booking.scheduledJobs) return;
let scheduledJobs = booking.scheduledJobs || [];
const promises = booking.scheduledJobs.map(async (scheduledJob) => {
if (appId) {
if (scheduledJob.startsWith(appId)) {
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
}
} else {
//if no specific appId given, delete all scheduled jobs of booking
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = [];
}
if (!isReschedule) {
await prisma.booking.update({
where: {
uid: booking.uid,
},
data: {
scheduledJobs: scheduledJobs,
},
});
}
});
try {
await Promise.all(promises);
} catch (error) {
log.error("Error cancelling scheduled jobs", error);
}
}

View File

@ -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,

View File

@ -28,6 +28,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";
@ -97,6 +98,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,

View File

@ -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,

View File

@ -28,6 +28,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"),

View File

@ -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"),

View 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>

View File

@ -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.

View File

@ -0,0 +1,16 @@
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",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;

View 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";

View File

@ -0,0 +1,38 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { addSubscription } from "@calcom/app-store/_utils/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
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) }),
});

View File

@ -0,0 +1,40 @@
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { deleteSubscription } from "@calcom/app-store/_utils/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
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) }),
});

View File

@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { listBookings } from "@calcom/app-store/_utils/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
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) }),
});

View 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." });
}
}
}

View File

@ -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"
}

View File

@ -0,0 +1 @@
export * as api from "./api";

View 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"
}
}

View File

@ -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,
},
};
};

View File

@ -0,0 +1,176 @@
import type { getStaticProps } from "make/pages/setup/_getStaticProps";
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";
const MAKE = "make";
export default function MakeSetup({ inviteLink }: InferGetStaticPropsType<typeof getStaticProps>) {
const [newApiKeys, setNewApiKeys] = useState<Record<string, string>>({});
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;
const isCalDev = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.dev";
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 <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">
{isCalDev && (
<li>
{t("go_to")}
<a href={inviteLink} className="text-orange-600 underline">
{t("make_invite_link")}
</a>
</li>
)}
<Trans i18nKey="make_setup_instructions">
<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&apos;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">
<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>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View 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

View 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),
});

View File

@ -1,12 +1,8 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 } from "uuid";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { addSubscription } from "@calcom/app-store/_utils/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
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({

View File

@ -1,11 +1,9 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { deleteSubscription } from "@calcom/app-store/_utils/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
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." });
}

View File

@ -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 { listBookings } from "@calcom/app-store/_utils/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder, getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
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({

View File

@ -1,97 +0,0 @@
import schedule from "node-schedule";
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
export async function scheduleTrigger(
booking: { id: number; endTime: Date; scheduledJobs: string[] },
subscriberUrl: string,
subscriber: { id: string; appId: string | null }
) {
try {
//schedule job to call subscriber url at the end of meeting
// FIXME: in-process scheduling - job will vanish on server crash / restart
const job = schedule.scheduleJob(
`${subscriber.appId}_${subscriber.id}`,
booking.endTime,
async function () {
const body = JSON.stringify({ triggerEvent: WebhookTriggerEvents.MEETING_ENDED, ...booking });
await fetch(subscriberUrl, {
method: "POST",
body,
});
//remove scheduled job from bookings once triggered
const updatedScheduledJobs = booking.scheduledJobs.filter((scheduledJob) => {
return scheduledJob !== `${subscriber.appId}_${subscriber.id}`;
});
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: updatedScheduledJobs,
},
});
}
);
//add scheduled job name to booking
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: {
push: job.name,
},
},
});
} catch (error) {
console.error("Error cancelling scheduled jobs", error);
}
}
export async function cancelScheduledJobs(
booking: { uid: string; scheduledJobs?: string[] },
appId?: string | null,
isReschedule?: boolean
) {
if (!booking.scheduledJobs) return;
let scheduledJobs = booking.scheduledJobs || [];
const promises = booking.scheduledJobs.map(async (scheduledJob) => {
if (appId) {
if (scheduledJob.startsWith(appId)) {
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
}
} else {
//if no specific appId given, delete all scheduled jobs of booking
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = [];
}
if (!isReschedule) {
await prisma.booking.update({
where: {
uid: booking.uid,
},
data: {
scheduledJobs: scheduledJobs,
},
});
}
});
try {
await Promise.all(promises);
} catch (error) {
console.error("Error cancelling scheduled jobs", error);
}
}

View File

@ -3,9 +3,9 @@ import type { NextApiRequest } from "next";
import appStore from "@calcom/app-store";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { cancelScheduledJobs } from "@calcom/app-store/_utils/nodeScheduler";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { DailyLocationType } from "@calcom/app-store/locations";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { deleteMeeting, updateMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendCancelledSeatEmails } from "@calcom/emails";

View File

@ -1,6 +1,6 @@
import type { Prisma, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { scheduleTrigger } from "@calcom/app-store/_utils/nodeScheduler";
import type { EventManagerUser } from "@calcom/core/EventManager";
import EventManager from "@calcom/core/EventManager";
import { sendScheduledEmails } from "@calcom/emails";

View File

@ -10,6 +10,7 @@ import { v5 as uuidv5 } from "uuid";
import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/_utils/nodeScheduler";
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
import type { LocationObject } from "@calcom/app-store/locations";
import {
@ -19,7 +20,6 @@ import {
} from "@calcom/app-store/locations";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import { getAppFromSlug } from "@calcom/app-store/utils";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { getEventName } from "@calcom/core/event";
import { getUserAvailability } from "@calcom/core/getUserAvailability";

View File

@ -337,7 +337,6 @@ export default async function main() {
invite_link: process.env.ZAPIER_INVITE_LINK,
});
}
await createApp("huddle01", "huddle01video", ["conferencing"], "huddle01_video");
// Payment apps

View File

@ -1,7 +1,7 @@
import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { cancelScheduledJobs } from "@calcom/app-store/_utils/nodeScheduler";
import { DailyLocationType } from "@calcom/core/location";
import { sendCancelledEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";

View File

@ -2,7 +2,7 @@ import type { BookingReference, EventType } from "@prisma/client";
import type { TFunction } from "next-i18next";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { cancelScheduledJobs } from "@calcom/app-store/_utils/nodeScheduler";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
import { deleteMeeting } from "@calcom/core/videoClient";