mirror of
https://github.com/hanzoai/calendar.git
synced 2026-09-24 22:42:24 +00:00
* refactor: apply biome formatting to packages/features (batch 1 - small subdirs) Format small subdirectories in packages/features: di, flags, holidays, oauth, settings, users, assignment-reason, selectedCalendar, hashedLink, host, form, form-builder, availability, data-table, pbac, schedules, troubleshooter, eventtypes, calendar-subscription, and root-level files. Also includes straggler apps/web BookEventForm.tsx. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 2 - medium subdirs) Format medium subdirectories in packages/features: auth, credentials, calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone, tasker, and webhooks. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 3 - bookings + insights) Format bookings and insights subdirectories in packages/features. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 4 - ee) Format packages/features/ee subdirectory covering billing, workflows, organizations, teams, managed-event-types, round-robin, dsync, integration-attribute-sync, and payments. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1) Format booking-audit di, actions, common, dto, repository, and types subdirectories in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2) Format booking-audit service subdirectory in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
217 lines
7.1 KiB
TypeScript
217 lines
7.1 KiB
TypeScript
import type { calendar_v3 } from "@googleapis/calendar";
|
|
import { v4 as uuid } from "uuid";
|
|
|
|
import { CalendarAuth } from "@calcom/app-store/googlecalendar/lib/CalendarAuth";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
|
|
import logger from "@calcom/lib/logger";
|
|
import type { SelectedCalendar } from "@calcom/prisma/client";
|
|
|
|
import type {
|
|
ICalendarSubscriptionPort,
|
|
CalendarSubscriptionResult,
|
|
CalendarSubscriptionEvent,
|
|
CalendarSubscriptionEventItem,
|
|
CalendarCredential,
|
|
} from "../lib/CalendarSubscriptionPort.interface";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["GoogleCalendarSubscriptionAdapter"] });
|
|
|
|
/**
|
|
* Google Calendar Subscription Adapter
|
|
*
|
|
* This adapter uses the Google Calendar API to create and manage calendar subscriptions
|
|
* @see https://developers.google.com/google-apps/calendar/quickstart/nodejs
|
|
*/
|
|
export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionPort {
|
|
private GOOGLE_WEBHOOK_TOKEN = process.env.GOOGLE_WEBHOOK_TOKEN;
|
|
private GOOGLE_WEBHOOK_URL = `${
|
|
process.env.GOOGLE_WEBHOOK_URL || process.env.NEXT_PUBLIC_WEBAPP_URL
|
|
}/api/webhooks/calendar-subscription/google_calendar`;
|
|
|
|
async validate(request: Request): Promise<boolean> {
|
|
const token = request?.headers?.get("X-Goog-Channel-Token");
|
|
if (!this.GOOGLE_WEBHOOK_TOKEN) {
|
|
log.warn("GOOGLE_WEBHOOK_TOKEN not configured");
|
|
return false;
|
|
}
|
|
if (token !== this.GOOGLE_WEBHOOK_TOKEN) {
|
|
log.warn("Invalid webhook token");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async extractChannelId(request: Request): Promise<string | null> {
|
|
const channelId = request?.headers?.get("X-Goog-Channel-ID");
|
|
if (!channelId) {
|
|
log.warn("Missing channel ID in webhook");
|
|
return null;
|
|
}
|
|
return channelId;
|
|
}
|
|
|
|
async subscribe(
|
|
selectedCalendar: SelectedCalendar,
|
|
credential: CalendarCredential
|
|
): Promise<CalendarSubscriptionResult> {
|
|
log.debug("Attempt to subscribe to Google Calendar", { externalId: selectedCalendar.externalId });
|
|
|
|
const MONTH_IN_SECONDS = 60 * 60 * 24 * 30;
|
|
|
|
const client = await this.getClient(credential);
|
|
const result = await client.events.watch({
|
|
calendarId: selectedCalendar.externalId,
|
|
requestBody: {
|
|
id: uuid(),
|
|
type: "web_hook",
|
|
address: this.GOOGLE_WEBHOOK_URL,
|
|
token: this.GOOGLE_WEBHOOK_TOKEN,
|
|
params: {
|
|
ttl: MONTH_IN_SECONDS.toFixed(0),
|
|
},
|
|
},
|
|
});
|
|
|
|
const e = result.data?.expiration;
|
|
const expiration = e ? new Date(/^\d+$/.test(e) ? +e : e) : null;
|
|
|
|
return {
|
|
provider: "google_calendar",
|
|
id: result.data.id,
|
|
resourceId: result.data.resourceId,
|
|
resourceUri: result.data.resourceUri,
|
|
expiration,
|
|
};
|
|
}
|
|
async unsubscribe(selectedCalendar: SelectedCalendar, credential: CalendarCredential): Promise<void> {
|
|
log.debug("Attempt to unsubscribe from Google Calendar", { externalId: selectedCalendar.externalId });
|
|
|
|
const client = await this.getClient(credential);
|
|
await client.channels
|
|
.stop({
|
|
requestBody: {
|
|
id: selectedCalendar.channelId as string,
|
|
resourceId: selectedCalendar.channelResourceId as string,
|
|
},
|
|
})
|
|
.catch((err) => {
|
|
log.error("Error unsubscribing from Google Calendar", err);
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
async fetchEvents(
|
|
selectedCalendar: SelectedCalendar,
|
|
credential: CalendarCredential
|
|
): Promise<CalendarSubscriptionEvent> {
|
|
log.info("Attempt to fetch events from Google Calendar", { externalId: selectedCalendar.externalId });
|
|
const client = await this.getClient(credential);
|
|
|
|
let syncToken = selectedCalendar.syncToken || undefined;
|
|
let pageToken;
|
|
|
|
const params: calendar_v3.Params$Resource$Events$List = {
|
|
calendarId: selectedCalendar.externalId,
|
|
pageToken,
|
|
singleEvents: true,
|
|
};
|
|
|
|
if (!syncToken) {
|
|
const now = dayjs().startOf("day");
|
|
// first sync or unsync (3 months)
|
|
const monthsAhead = now.add(CalendarCacheEventService.MONTHS_AHEAD, "month").endOf("day");
|
|
|
|
const timeMinISO = now.toISOString();
|
|
const timeMaxISO = monthsAhead.toISOString();
|
|
params.timeMin = timeMinISO;
|
|
params.timeMax = timeMaxISO;
|
|
} else {
|
|
// incremental sync
|
|
params.syncToken = syncToken;
|
|
}
|
|
|
|
const events: calendar_v3.Schema$Event[] = [];
|
|
do {
|
|
const { data }: { data: calendar_v3.Schema$Events } = await client.events.list(params);
|
|
|
|
syncToken = data.nextSyncToken || syncToken;
|
|
pageToken = data.nextPageToken ?? null;
|
|
if (pageToken) {
|
|
params.pageToken = pageToken;
|
|
}
|
|
|
|
events.push(...(data.items || []));
|
|
} while (pageToken);
|
|
|
|
return {
|
|
provider: "google_calendar",
|
|
syncToken: syncToken || null,
|
|
items: this.parseEvents(events),
|
|
};
|
|
}
|
|
|
|
private parseEvents(events: calendar_v3.Schema$Event[]): CalendarSubscriptionEventItem[] {
|
|
const now = dayjs().startOf("day");
|
|
const monthsAhead = now.add(CalendarCacheEventService.MONTHS_AHEAD, "month").endOf("day");
|
|
|
|
function filterEventsWithoutId(event: calendar_v3.Schema$Event) {
|
|
return typeof event.id === "string" && !!event.id;
|
|
}
|
|
|
|
function filterEventsByDateRange(event: calendar_v3.Schema$Event) {
|
|
const start = dayjs(event.start?.dateTime || event.start?.date);
|
|
return !start.isBefore(now) && !start.isAfter(monthsAhead);
|
|
}
|
|
|
|
return events
|
|
.filter(filterEventsWithoutId)
|
|
.filter(filterEventsByDateRange)
|
|
.map((event) => {
|
|
// empty or opaque is busy
|
|
const busy = !event.transparency || event.transparency === "opaque";
|
|
|
|
const start = event.start?.dateTime
|
|
? new Date(event.start.dateTime)
|
|
: event.start?.date
|
|
? new Date(event.start.date)
|
|
: new Date();
|
|
|
|
const end = event.end?.dateTime
|
|
? new Date(event.end.dateTime)
|
|
: event.end?.date
|
|
? new Date(event.end.date)
|
|
: new Date();
|
|
|
|
return {
|
|
id: event.id as string,
|
|
iCalUID: event.iCalUID ?? null,
|
|
start,
|
|
end,
|
|
busy,
|
|
summary: event.summary ?? null,
|
|
description: event.description ?? null,
|
|
location: event.location ?? null,
|
|
kind: event.kind ?? null,
|
|
etag: event.etag ?? null,
|
|
status: event.status ?? null,
|
|
isAllDay: typeof event.start?.date === "string" && !event.start?.dateTime ? true : false,
|
|
timeZone: event.start?.timeZone ?? null,
|
|
recurringEventId: event.recurringEventId ?? null,
|
|
originalStartDate: event.originalStartTime?.dateTime
|
|
? new Date(event.originalStartTime.dateTime)
|
|
: event.originalStartTime?.date
|
|
? new Date(event.originalStartTime.date)
|
|
: null,
|
|
createdAt: event.created ? new Date(event.created) : null,
|
|
updatedAt: event.updated ? new Date(event.updated) : null,
|
|
};
|
|
});
|
|
}
|
|
|
|
private async getClient(credential: CalendarCredential) {
|
|
const auth = new CalendarAuth(credential);
|
|
return await auth.getClient();
|
|
}
|
|
}
|