mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* chore: Upgrade prisma to 6.7.0 * Build fixes * type fixes Signed-off-by: Omar López <[email protected]> * Update schema.prisma * Patching * Revert "Update schema.prisma" This reverts commit 47d8618bf89ef4d007b30084df766f17281e21a1. * Revert "Patching" This reverts commit a1d2e3040e71690a44d4324db95d73b4d68c6adb. * Revert schema changes Signed-off-by: Omar López <[email protected]> * WIP Signed-off-by: Omar López <[email protected]> * Update getPublicEvent.ts * Update imports Signed-off-by: Omar López <[email protected]> * Update gitignore Signed-off-by: Omar López <[email protected]> * update remaining imports Signed-off-by: Omar López <[email protected]> * Delete .cursor/config.json * Discard changes to packages/features/eventtypes/lib/getPublicEvent.ts * Update _get.ts * Update user.ts * Update .gitignore * update * Update WorkflowStepContainer.tsx * Update next-auth-custom-adapter.ts * Update getPublicEvent.ts * Update workflow.ts * Update next-auth-custom-adapter.ts * Update next-auth-options.ts * Update bookingScenario.ts * fix missing imports * upgrades prismock Signed-off-by: Omar López <[email protected]> * patches prismock Signed-off-by: Omar López <[email protected]> * Update reschedule.test.ts * Update prisma.ts * patch prismock Signed-off-by: Omar López <[email protected]> * fix enums imports Signed-off-by: Omar López <[email protected]> * Revert "Update prisma.ts" This reverts commit 64edcf8db54171ff4456c209d563b5d431d99619. * Revert "patch prismock" This reverts commit e95819113dc9d88e7130947aa120cd42710977c8. * fix patch * Fix test that overrun the boundary, it shouldn't test too much * Move prisma import to changeSMSLockState * Bring back broken test without illegal imports * Merge with main and fix filter hosts by same round robin host * Fixed buildDryRunBooking fn tests * Fix and move ooo create or update handler test * Fix packages/features/eventtypes/lib/isCurrentlyAvailable.test.ts * Fix packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts * Mock @calcom/prisma * Fix: verify-email.test.ts * fix: Moved WebhookService test and fixed default import mock * Fix: Added missing prisma mock, handleNewBooking uses that of course * We're not testing createContext here * fix: Prisma mock fix for listMembers.test.ts * More fixes to broken testcases * Forgot to remove borked test * Prevent the need to mock a lot of dependencies by moving out buildBaseWhereCondition to its own file * Temporarily skip getCalendarEvents, needs a rewrite * Fix: turns out you can access protected in testcases * fix further mocks * Added packages/features/insights/server/buildBaseWhereCondition.ts, types * Always great to have a mock and then not use it * And one less again. * fix: confirm.handler.test, didn't mock prisma * fix: Address minor nit by @eunjae & fix ImpersonationProvider test * Updated isPrismaAvailableCheck that doesn't crash on import * fix: Get Prisma directly from the client, it usually involves the Validator and does not need 'local' inclusion * Add zod-prisma-types without the generator enabled (commented out) * Uncomment and see what happens * Change method of import as imports did not work in Input Schemas * Remove custom 'zod' booking model, it does not belong with Prisma * Fix all other global Model imports * Rewrite most schema includes AND remove barrel file * Add bookingCreateBodySchema to features/bookings * Flurry of type fixes for compatibility with new zod gen * Refactor out the custom prisma type createEventTypeInput * Work around nullable eventTypeLocations * HandlePayment type fix * More fixes, final fix remaining is CompleteEventType * Should fix a bunch more booking related type errors * Missed one * Some props missing from BookingCreateBodySchema * Fix location type in handleChildrenEventTypes * Little bit hacky imo but it works * Final type error \o/ * Forgot to include Prisma * Do not include zod-utils in booker/types * Oops, was already including Booker/types * Fix membership type, also disallow updating createdAt/updatedAt, make part of patch/post * Fix api v1 type errors * Fix EventTypeDescription typings * Remove getParserWithGeneric, use userBodySchema with UserSchema * use centralized timeZoneSchema * Implement feedback by @zomars * Couple of WIP pushes * Fix tests * Type fixes in `handleChildrenEventTypes` test * Try and parse metadata before use * Change zod-prisma-types configuration for optimal performance * Fix prisma validator error in `prisma/selects/credential` * Disable seperate relations model, hits a bug * Import absolute - this makes rollup work in @platform/libraries * Attempt at removing resolutions override * Refactor using `Prisma.validator` to `satisfies` * Build atoms using @calcom/prisma/client * Build atoms using @calcom/prisma/client * fixes * Update eventTypeSelect.ts * Adjust `eventTypeMetaDataSchemaWithUntypedApps` from `unknown` to `record(any)` * `EventTypeDescription` rely on `descriptionAsSafeHTML` instead of `description` * Add `seatsPerTimeSlot` to event type public select * Fix typing in `users-public-view` getServerSide props * Add missing `schedulingType` to prop * chore: bump platform libraries * Function return type is illegal, not sure how this passed eslint (#21567) * Merged with main * Update updateTokenObject.ts * Update handleResponse.ts * Update index.ts * Update handleChildrenEventTypes.ts * Update booking-idempotency-key.ts * Update WebhookService.test.ts * Update events.test.ts * Update queued-response.test.ts * Update events.test.ts * Update getRoutedUrl.test.ts * fix: type checks Signed-off-by: Omar López <[email protected]> * fixes Signed-off-by: Omar López <[email protected]> * chore: bump platform libraries * Update yarn.lock * more fixes Signed-off-by: Omar López <[email protected]> * fixes Signed-off-by: Omar López <[email protected]> * biuld fixes * chore: bump platform libraries * Update conferencing.repository.ts * Update conferencing.repository.ts * Update getCalendarsEvents.test.ts * Update vite.config.js * chore: bump platform libraries * Update users.ts * Discard changes to docs/api-reference/v2/openapi.json * Update vite.config.ts * updated platform libraries * Update get.handler.test.ts * Update get.handler.test.ts * Update schema.prisma * Discard changes to docs/api-reference/v2/openapi.json * Update next-auth-custom-adapter.ts * Update team.ts * Flurry of type fixes * Fix majority of insight related type errors * Type fixes for unlink of account * Make user nullable again * Fixed a bunch of unit tests and one type error * Attempted mock fix * Attempted fix for Attribute type * Ensure default import becomes prisma, but not direct usage * Import default as prisma in prisma.module * Add attributeOption to attribute type * Fix calcom/prisma mock * Refactor Prisma client imports to @calcom/prisma/client Updated all imports from '@prisma/client' to '@calcom/prisma/client' across tests and repository files for consistency and to use the correct Prisma client package. This change improves maintainability and ensures the correct client is referenced throughout the codebase. * Undo removal of max-warnings=0 to get main to merge * Remove unit tests for e2e fixtures, provide new prisma mock * Mock @calcom/prisma in event manager * Mock @calcom/prisma in event manager * Add correct format even with --no-verify * Mock prisma in CalendarManager * Add mock for permission-check.service * Better injection in PrismaApiKeyRepository imports * More mock fixes :) * Fix listMembers.handler.test * Fix User import * Appropriately adjust all types to be imported as types, there were a lot of types imported as normal deps * Why was this a thing? * Strictly speaking; Not using prismock anymore * Ditched patch file for prismock * Fix output.service.ts platform type imports, need concrete for plainToClass * Better typing and tests for unlinkConnectedAccount.handler * Small type fix * Disable calendar cache tests as they are dependent on prismock * chore: bump platform lib * getRoutedUrl test remove of unused import * Extract select to external const on getEventTypesFromDB * Direct select of userSelect from selects/user * fix type error from merging 23653 * Fixed integration tests by removing hardcoded values that were possible due to mocking, but as its now directly hitting the db no longer * fix: vite config atoms prisma client type location * revert: example app prisma client * revert: example app prisma client * bump platform libs * fix: use class instead of type for DI of PlatformBookingsService * update platform libs * remove unused variable * chore: generate prisma client for api v2 * fix: api v2 e2e * fix: atoms e2e * fix: atoms e2e * fix: atoms e2e * fix: api v2 e2e * fix: tsconfig apiv2 enums * publish libraries * Simplify check for existence teamId --------- Signed-off-by: Omar López <[email protected]> Co-authored-by: Alex van Andel <[email protected]> Co-authored-by: Joe Au-Yeung <[email protected]> Co-authored-by: supalarry <[email protected]> Co-authored-by: cal.com <[email protected]> Co-authored-by: Morgan <[email protected]> Co-authored-by: Benny Joo <[email protected]>
307 lines
12 KiB
TypeScript
307 lines
12 KiB
TypeScript
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
|
/// <reference path="../../../types/ical.d.ts"/>
|
|
import ICAL from "ical.js";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
|
import type {
|
|
Calendar,
|
|
IntegrationCalendar,
|
|
EventBusyDate,
|
|
CalendarEvent,
|
|
NewCalendarEventType,
|
|
} from "@calcom/types/Calendar";
|
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
|
|
|
// for Apple's Travel Time feature only (for now)
|
|
const getTravelDurationInSeconds = (vevent: ICAL.Component) => {
|
|
const travelDuration: ICAL.Duration = vevent.getFirstPropertyValue("x-apple-travel-duration");
|
|
if (!travelDuration) return 0;
|
|
|
|
// we can't rely on this being a valid duration and it's painful to check, so just try and catch if anything throws
|
|
try {
|
|
const travelSeconds = travelDuration.toSeconds();
|
|
// integer validation as we can never be sure with ical.js
|
|
if (!Number.isInteger(travelSeconds)) return 0;
|
|
return travelSeconds;
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
const applyTravelDuration = (event: ICAL.Event, seconds: number) => {
|
|
if (seconds <= 0) return event;
|
|
// move event start date back by the specified travel time
|
|
event.startDate.second -= seconds;
|
|
return event;
|
|
};
|
|
|
|
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
|
|
|
export default class ICSFeedCalendarService implements Calendar {
|
|
private urls: string[] = [];
|
|
protected integrationName = "ics-feed_calendar";
|
|
|
|
constructor(credential: CredentialPayload) {
|
|
const { urls } = JSON.parse(symmetricDecrypt(credential.key as string, CALENDSO_ENCRYPTION_KEY));
|
|
this.urls = urls;
|
|
}
|
|
|
|
createEvent(_event: CalendarEvent, _credentialId: number): Promise<NewCalendarEventType> {
|
|
console.warn("createEvent called on ICS (read-only) feed");
|
|
return Promise.resolve({
|
|
uid: _event.uid || "",
|
|
type: this.integrationName,
|
|
id: "",
|
|
password: "",
|
|
url: "",
|
|
additionalInfo: { calWarnings: ["ICS feed is read-only"] },
|
|
});
|
|
}
|
|
|
|
deleteEvent(_uid: string, _event: CalendarEvent, _externalCalendarId?: string): Promise<unknown> {
|
|
console.warn("deleteEvent called on ICS (read-only) feed");
|
|
return Promise.resolve();
|
|
}
|
|
|
|
updateEvent(
|
|
_uid: string,
|
|
_event: CalendarEvent,
|
|
_externalCalendarId?: string
|
|
): Promise<NewCalendarEventType | NewCalendarEventType[]> {
|
|
console.warn("updateEvent called on ICS (read-only) feed");
|
|
return Promise.resolve({
|
|
uid: _event.uid || "",
|
|
type: this.integrationName,
|
|
id: "",
|
|
password: "",
|
|
url: "",
|
|
additionalInfo: { calWarnings: ["ICS feed is read-only"] },
|
|
});
|
|
}
|
|
|
|
fetchCalendars = async (): Promise<{ url: string; vcalendar: ICAL.Component }[]> => {
|
|
const reqPromises = await Promise.allSettled(this.urls.map((x) => fetch(x).then((y) => [x, y])));
|
|
const reqs = reqPromises
|
|
.filter((x) => x.status === "fulfilled")
|
|
.map((x) => (x as PromiseFulfilledResult<[string, Response]>).value);
|
|
const res = await Promise.all(reqs.map((x) => x[1].text().then((y) => [x[0], y])));
|
|
return res
|
|
.map((x) => {
|
|
try {
|
|
const jcalData = ICAL.parse(x[1]);
|
|
return {
|
|
url: x[0],
|
|
vcalendar: new ICAL.Component(jcalData),
|
|
};
|
|
} catch (e) {
|
|
console.error("Error parsing calendar object: ", e);
|
|
return null;
|
|
}
|
|
})
|
|
.filter((x) => x !== null) as { url: string; vcalendar: ICAL.Component }[];
|
|
};
|
|
|
|
/**
|
|
* getUserTimezoneFromDB() retrieves the timezone of a user from the database.
|
|
*
|
|
* @param {number} id - The user's unique identifier.
|
|
* @returns {Promise<string | undefined>} - A Promise that resolves to the user's timezone or "Europe/London" as a default value if the timezone is not found.
|
|
*/
|
|
getUserTimezoneFromDB = async (id: number): Promise<string | undefined> => {
|
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
|
const user = await prisma.user.findUnique({
|
|
where: {
|
|
id,
|
|
},
|
|
select: {
|
|
timeZone: true,
|
|
},
|
|
});
|
|
return user?.timeZone;
|
|
};
|
|
|
|
/**
|
|
* getUserId() extracts the user ID from the first calendar in an array of IntegrationCalendars.
|
|
*
|
|
* @param {IntegrationCalendar[]} selectedCalendars - An array of IntegrationCalendars.
|
|
* @returns {number | null} - The user ID associated with the first calendar in the array, or null if the array is empty or the user ID is not found.
|
|
*/
|
|
getUserId = (selectedCalendars: IntegrationCalendar[]): number | null => {
|
|
if (selectedCalendars.length === 0) {
|
|
return null;
|
|
}
|
|
return selectedCalendars[0].userId || null;
|
|
};
|
|
|
|
async getAvailability(
|
|
dateFrom: string,
|
|
dateTo: string,
|
|
selectedCalendars: IntegrationCalendar[]
|
|
): Promise<EventBusyDate[]> {
|
|
const startISOString = new Date(dateFrom).toISOString();
|
|
|
|
const calendars = await this.fetchCalendars();
|
|
|
|
const userId = this.getUserId(selectedCalendars);
|
|
// we use the userId from selectedCalendars to fetch the user's timeZone from the database primarily for all-day events without any timezone information
|
|
const userTimeZone = userId ? await this.getUserTimezoneFromDB(userId) : "Europe/London";
|
|
const events: { start: string; end: string }[] = [];
|
|
|
|
calendars.forEach(({ vcalendar }) => {
|
|
const vevents = vcalendar.getAllSubcomponents("vevent");
|
|
vevents.forEach((vevent) => {
|
|
// if event status is free or transparent, DON'T return (unlike usual getAvailability)
|
|
//
|
|
// commented out because a lot of public ICS feeds that describe stuff like
|
|
// public holidays have them marked as transparent. if that is explicitly
|
|
// added to cal.com as an ICS feed, it should probably not be ignored.
|
|
// if (vevent?.getFirstPropertyValue("transp") === "TRANSPARENT") return;
|
|
|
|
const event = new ICAL.Event(vevent);
|
|
const dtstartProperty = vevent.getFirstProperty("dtstart");
|
|
const tzidFromDtstart = dtstartProperty ? (dtstartProperty as any).jCal[1].tzid : undefined;
|
|
|
|
const dtstart: { [key: string]: string } | undefined = vevent?.getFirstPropertyValue("dtstart");
|
|
const timezone = dtstart ? dtstart["timezone"] : undefined;
|
|
// We check if the dtstart timezone is in UTC which is actually represented by Z instead, but not recognized as that in ICAL.js as UTC
|
|
const isUTC = timezone === "Z";
|
|
|
|
// Fix precedence: prioritize TZID from DTSTART property, then standalone TZID, then UTC, then fallback
|
|
const tzid: string | undefined =
|
|
tzidFromDtstart || vevent?.getFirstPropertyValue("tzid") || (isUTC ? "UTC" : timezone);
|
|
// In case of icalendar, when only tzid is available without vtimezone, we need to add vtimezone explicitly to take care of timezone diff
|
|
if (!vcalendar.getFirstSubcomponent("vtimezone")) {
|
|
const timezoneToUse = tzid || userTimeZone;
|
|
if (timezoneToUse) {
|
|
try {
|
|
const timezoneComp = new ICAL.Component("vtimezone");
|
|
timezoneComp.addPropertyWithValue("tzid", timezoneToUse);
|
|
const standard = new ICAL.Component("standard");
|
|
|
|
// get timezone offset
|
|
const tzoffsetfrom = dayjs(event.startDate.toJSDate()).tz(timezoneToUse).format("Z");
|
|
const tzoffsetto = dayjs(event.endDate.toJSDate()).tz(timezoneToUse).format("Z");
|
|
|
|
// set timezone offset
|
|
standard.addPropertyWithValue("tzoffsetfrom", tzoffsetfrom);
|
|
standard.addPropertyWithValue("tzoffsetto", tzoffsetto);
|
|
// provide a standard dtstart
|
|
standard.addPropertyWithValue("dtstart", "1601-01-01T00:00:00");
|
|
timezoneComp.addSubcomponent(standard);
|
|
vcalendar.addSubcomponent(timezoneComp);
|
|
} catch (e) {
|
|
// Adds try-catch to ensure the code proceeds when Apple Calendar provides non-standard TZIDs
|
|
console.log("error in adding vtimezone", e);
|
|
}
|
|
} else {
|
|
console.error("No timezone found");
|
|
}
|
|
}
|
|
|
|
let vtimezone = null;
|
|
if (tzid) {
|
|
const allVtimezones = vcalendar.getAllSubcomponents("vtimezone");
|
|
vtimezone = allVtimezones.find((vtz) => vtz.getFirstPropertyValue("tzid") === tzid);
|
|
}
|
|
|
|
if (!vtimezone) {
|
|
vtimezone = vcalendar.getFirstSubcomponent("vtimezone");
|
|
}
|
|
|
|
// mutate event to consider travel time
|
|
applyTravelDuration(event, getTravelDurationInSeconds(vevent));
|
|
|
|
if (event.isRecurring()) {
|
|
let maxIterations = 365;
|
|
if (["HOURLY", "SECONDLY", "MINUTELY"].includes(event.getRecurrenceTypes())) {
|
|
console.error(`Won't handle [${event.getRecurrenceTypes()}] recurrence`);
|
|
return;
|
|
}
|
|
|
|
const start = dayjs(dateFrom);
|
|
const end = dayjs(dateTo);
|
|
const startDate = ICAL.Time.fromDateTimeString(startISOString);
|
|
startDate.hour = event.startDate.hour;
|
|
startDate.minute = event.startDate.minute;
|
|
startDate.second = event.startDate.second;
|
|
const iterator = event.iterator(startDate);
|
|
let current: ICAL.Time;
|
|
let currentEvent;
|
|
let currentStart = null;
|
|
let currentError;
|
|
|
|
while (
|
|
maxIterations > 0 &&
|
|
(currentStart === null || currentStart.isAfter(end) === false) &&
|
|
// this iterator was poorly implemented, normally done is expected to be
|
|
// returned
|
|
(current = iterator.next())
|
|
) {
|
|
maxIterations -= 1;
|
|
|
|
try {
|
|
// @see https://github.com/mozilla-comm/ical.js/issues/514
|
|
currentEvent = event.getOccurrenceDetails(current);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message !== currentError) {
|
|
currentError = error.message;
|
|
}
|
|
}
|
|
if (!currentEvent) return;
|
|
// do not mix up caldav and icalendar! For the recurring events here, the timezone
|
|
// provided is relevant, not as pointed out in https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5
|
|
// where recurring events are always in utc (in caldav!). Thus, apply the time zone here.
|
|
if (vtimezone) {
|
|
const zone = new ICAL.Timezone(vtimezone);
|
|
currentEvent.startDate = currentEvent.startDate.convertToZone(zone);
|
|
currentEvent.endDate = currentEvent.endDate.convertToZone(zone);
|
|
}
|
|
currentStart = dayjs(currentEvent.startDate.toJSDate());
|
|
|
|
if (currentStart.isBetween(start, end) === true) {
|
|
events.push({
|
|
start: currentStart.toISOString(),
|
|
end: dayjs(currentEvent.endDate.toJSDate()).toISOString(),
|
|
});
|
|
}
|
|
}
|
|
if (maxIterations <= 0) {
|
|
console.warn("could not find any occurrence for recurring event in 365 iterations");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (vtimezone) {
|
|
const zone = new ICAL.Timezone(vtimezone);
|
|
event.startDate = event.startDate.convertToZone(zone);
|
|
event.endDate = event.endDate.convertToZone(zone);
|
|
}
|
|
|
|
const finalStartISO = dayjs(event.startDate.toJSDate()).toISOString();
|
|
const finalEndISO = dayjs(event.endDate.toJSDate()).toISOString();
|
|
return events.push({
|
|
start: finalStartISO,
|
|
end: finalEndISO,
|
|
});
|
|
});
|
|
});
|
|
|
|
return Promise.resolve(events);
|
|
}
|
|
|
|
async listCalendars(): Promise<IntegrationCalendar[]> {
|
|
const vcals = await this.fetchCalendars();
|
|
|
|
return vcals.map(({ url, vcalendar }) => {
|
|
const name: string = vcalendar.getFirstPropertyValue("x-wr-calname");
|
|
return {
|
|
name,
|
|
readOnly: true,
|
|
externalId: url,
|
|
integrationName: this.integrationName,
|
|
};
|
|
});
|
|
}
|
|
}
|