Skip to content

Commit

Permalink
revert: "feat: Populate gCal calendar cache via webhooks (#11928)"" (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
zomars authored Nov 18, 2024
1 parent 50df229 commit 7c0ee9f
Show file tree
Hide file tree
Showing 38 changed files with 915 additions and 235 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ GOOGLE_LOGIN_ENABLED=false
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
GOOGLE_API_CREDENTIALS=
# Token to verify incoming webhooks from Google Calendar
GOOGLE_WEBHOOK_TOKEN=
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
GOOGLE_WEBHOOK_URL=

# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
Expand Down
7 changes: 5 additions & 2 deletions apps/web/cron-tester.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { CronJob } from "cron";
import dotEnv from "dotenv";

dotEnv.config({ path: "../../.env" });

async function fetchCron(endpoint: string) {
const apiKey = process.env.CRON_API_KEY;

const res = await fetch(`http://localhost:3000/api${endpoint}?${apiKey}`, {
const res = await fetch(`http://localhost:3000/api${endpoint}?apiKey=${apiKey}`, {
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${process.env.CRON_SECRET}`,
Expand All @@ -20,7 +23,7 @@ try {
"*/5 * * * * *",
async function () {
await Promise.allSettled([
fetchCron("/tasks/cron"),
fetchCron("/calendar-cache/cron"),
// fetchCron("/cron/calVideoNoShowWebhookTriggers"),
//
// fetchCron("/tasks/cleanup"),
Expand Down
146 changes: 70 additions & 76 deletions apps/web/pages/api/availability/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,98 +3,92 @@ import { z } from "zod";

import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
import { HttpError } from "@calcom/lib/http-error";
import notEmpty from "@calcom/lib/notEmpty";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import { UserRepository } from "@calcom/lib/server/repository/user";

const selectedCalendarSelectSchema = z.object({
integration: z.string(),
externalId: z.string(),
credentialId: z.number().optional(),
credentialId: z.coerce.number(),
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession({ req, res });
/** Shared authentication middleware for GET, DELETE and POST requests */
async function authMiddleware(req: CustomNextApiRequest) {
const session = await getServerSession({ req });

if (!session?.user?.id) {
res.status(401).json({ message: "Not authenticated" });
return;
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
}

const userWithCredentials = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: {
credentials: {
select: credentialForCalendarServiceSelect,
},
timeZone: true,
id: true,
selectedCalendars: true,
},
});
const userWithCredentials = await UserRepository.findUserWithCredentials({ id: session.user.id });

if (!userWithCredentials) {
res.status(401).json({ message: "Not authenticated" });
return;
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
}
const { credentials, ...user } = userWithCredentials;
req.userWithCredentials = userWithCredentials;
return userWithCredentials;
}

if (req.method === "POST") {
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
await prisma.selectedCalendar.upsert({
where: {
userId_integration_externalId: {
userId: user.id,
integration,
externalId,
},
},
create: {
userId: user.id,
integration,
externalId,
credentialId,
},
// already exists
update: {},
});
res.status(200).json({ message: "Calendar Selection Saved" });
}
type CustomNextApiRequest = NextApiRequest & {
userWithCredentials?: Awaited<ReturnType<typeof authMiddleware>>;
};

if (req.method === "DELETE") {
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.query);
await prisma.selectedCalendar.delete({
where: {
userId_integration_externalId: {
userId: user.id,
externalId,
integration,
},
},
});
async function postHandler(req: CustomNextApiRequest) {
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
const user = req.userWithCredentials;
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.body);
await SelectedCalendarRepository.upsert({
userId: user.id,
integration,
externalId,
credentialId,
});

res.status(200).json({ message: "Calendar Selection Saved" });
}
return { message: "Calendar Selection Saved" };
}

if (req.method === "GET") {
const selectedCalendarIds = await prisma.selectedCalendar.findMany({
where: {
userId: user.id,
},
select: {
externalId: true,
},
});
async function deleteHandler(req: CustomNextApiRequest) {
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
const user = req.userWithCredentials;
const { integration, externalId, credentialId } = selectedCalendarSelectSchema.parse(req.query);
const calendarCacheRepository = await CalendarCache.initFromCredentialId(credentialId);
await calendarCacheRepository.unwatchCalendar({ calendarId: externalId });
await SelectedCalendarRepository.delete({
userId: user.id,
externalId,
integration,
});

// get user's credentials + their connected integrations
const calendarCredentials = getCalendarCredentials(credentials);
// get all the connected integrations' calendars (from third party)
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
const selectableCalendars = calendars.map((cal) => {
return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal };
});
res.status(200).json(selectableCalendars);
}
return { message: "Calendar Selection Saved" };
}

async function getHandler(req: CustomNextApiRequest) {
if (!req.userWithCredentials) throw new HttpError({ statusCode: 401, message: "Not authenticated" });
const user = req.userWithCredentials;
const selectedCalendarIds = await SelectedCalendarRepository.findMany({
where: { userId: user.id },
select: { externalId: true },
});
// get user's credentials + their connected integrations
const calendarCredentials = getCalendarCredentials(user.credentials);
// get all the connected integrations' calendars (from third party)
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
const selectableCalendars = calendars.map((cal) => {
return { selected: selectedCalendarIds.findIndex((s) => s.externalId === cal.externalId) > -1, ...cal };
});
return selectableCalendars;
}

export default defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => {
await authMiddleware(req);
return defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
DELETE: Promise.resolve({ default: defaultResponder(deleteHandler) }),
})(req, res);
});
1 change: 1 addition & 0 deletions apps/web/pages/api/calendar-cache/cron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "@calcom/features/calendar-cache/api/cron";
6 changes: 6 additions & 0 deletions apps/web/pages/api/cron/calendar-cache-cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
res.status(401).json({ message: "Not authenticated" });
return;
}

const deleted = await prisma.calendarCache.deleteMany({
where: {
// Delete all cache entries that expired before now
Expand Down
4 changes: 4 additions & 0 deletions apps/web/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"path": "/api/tasks/cron",
"schedule": "* * * * *"
},
{
"path": "/api/calendar-cache/cron",
"schedule": "* * * * *"
},
{
"path": "/api/tasks/cleanup",
"schedule": "0 0 * * *"
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/googlecalendar/api/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
// Wrapping in a try/catch to reduce chance of race conditions-
// also this improves performance for most of the happy-paths.
try {
await GoogleRepository.createSelectedCalendar({
await GoogleRepository.upsertSelectedCalendar({
credentialId: gcalCredential.id,
externalId: selectedCalendarWhereUnique.externalId,
userId: selectedCalendarWhereUnique.userId,
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/googlecalendar/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as add } from "./add";
export { default as callback } from "./callback";
export { default as webhook } from "./webhook";
41 changes: 41 additions & 0 deletions packages/app-store/googlecalendar/api/webhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { NextApiRequest } from "next";

import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";

import { getCalendar } from "../../_utils/getCalendar";

async function postHandler(req: NextApiRequest) {
if (req.headers["x-goog-channel-token"] !== process.env.GOOGLE_WEBHOOK_TOKEN) {
throw new HttpError({ statusCode: 403, message: "Invalid API key" });
}
if (typeof req.headers["x-goog-channel-id"] !== "string") {
throw new HttpError({ statusCode: 403, message: "Missing Channel ID" });
}

const selectedCalendar = await SelectedCalendarRepository.findByGoogleChannelId(
req.headers["x-goog-channel-id"]
);

if (!selectedCalendar) {
throw new HttpError({
statusCode: 200,
message: `No selected calendar found for googleChannelId: ${req.headers["x-goog-channel-id"]}`,
});
}
const { credential } = selectedCalendar;
if (!credential)
throw new HttpError({
statusCode: 200,
message: `No credential found for selected calendar for googleChannelId: ${req.headers["x-goog-channel-id"]}`,
});
const { selectedCalendars } = credential;
const calendar = await getCalendar(credential);
await calendar?.fetchAvailabilityAndSetCache?.(selectedCalendars);
return { message: "ok" };
}

export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});
Loading

0 comments on commit 7c0ee9f

Please sign in to comment.