Set GCal & Outlook iCalUID in .ics file (#8010)

* Add calendar UID to calendar event

* Add iCalUID to booking event

* On reschedule write to evt the iCalUID

* Add uid to ics file

* Remove console logs

* Pass iCalUID if available

* Remove generated app store files

* Rename product id to calcom

* Add UID to ics on reschedule

* Type fixes

* Type fixes

* Type fixes

* Remove comment

* Remove console.log

* Removed serverConfig block from this branch

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Joe Au-Yeung
2023-04-03 17:13:57 +00:00
committed by GitHub
co-authored by Alex van Andel
parent fd84b5754d
commit 2e0951d4dc
12 changed files with 67 additions and 21 deletions
@@ -165,6 +165,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar",
password: "",
url: "",
iCalUID: event.data.iCalUID,
});
}
);
@@ -201,6 +202,9 @@ export default class GoogleCalendarService implements Calendar {
id: String(event.organizer.id),
organizer: true,
responseStatus: "accepted",
email: event.destinationCalendar?.externalId
? event.destinationCalendar.externalId
: event.organizer.email,
},
// eslint-disable-next-line
...eventAttendees,
@@ -268,6 +272,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar",
password: "",
url: "",
iCalUID: evt.data.iCalUID,
});
}
return resolve(evt?.data);
@@ -73,7 +73,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)),
});
return handleErrorsJson(response);
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) {
this.log.error(error);
@@ -88,7 +90,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)),
});
return handleErrorsRaw(response);
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) {
this.log.error(error);
+1
View File
@@ -265,6 +265,7 @@ export const createEvent = async (
type: credential.type,
success,
uid,
iCalUID: creationResult?.iCalUID || undefined,
createdEvent: creationResult,
originalEvent: calEvent,
calError,
+16 -7
View File
@@ -10,15 +10,12 @@ import { MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma";
import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
import type { NewCalendarEventType } from "@calcom/types/Calendar";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { Event } from "@calcom/types/Event";
import type {
CreateUpdateResult,
EventResult,
PartialBooking,
PartialReference,
} from "@calcom/types/EventManager";
import type { EventResult } from "@calcom/types/EventManager";
import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager";
import { createEvent, updateEvent } from "./CalendarManager";
import { createMeeting, updateMeeting } from "./videoClient";
@@ -125,12 +122,24 @@ export default class EventManager {
// Create the calendar event with the proper video call data
results.push(...(await this.createAllCalendarEvents(clonedCalEvent)));
// Since the result can be a new calendar event or video event, we have to create a type guard
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
const isCalendarResult = (
result: (typeof results)[number]
): result is EventResult<NewCalendarEventType> => {
return result.type.includes("_calendar");
};
const referencesToCreate = results.map((result) => {
let createdEventObj: createdEventSchema | null = null;
if (typeof result?.createdEvent === "string") {
createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent));
}
if (isCalendarResult(result)) {
evt.iCalUID = result.iCalUID || undefined;
}
return {
type: result.type,
uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "",
@@ -37,21 +37,21 @@ export default class AttendeeScheduledEmail extends BaseEmail {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
const partstat: ParticipationStatus = "NEEDS-ACTION";
const partstat: ParticipationStatus = "ACCEPTED";
const role: ParticipationRole = "REQ-PARTICIPANT";
const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
productId: "calcom/ics",
title: this.calEvent.title,
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
@@ -47,7 +47,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
productId: "calcom/ics",
title: this.t("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
@@ -54,7 +54,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
productId: "calcom/ics",
title: this.t("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
@@ -35,13 +35,14 @@ export default class OrganizerScheduledEmail extends BaseEmail {
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
productId: "calcom/ics",
title: this.calEvent.title,
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
@@ -1057,6 +1057,10 @@ async function handler(
const results = updateManager.results;
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = calendarResult.updatedEvent.iCalUID || undefined;
if (results.length > 0 && results.some((res) => !res.success)) {
const error = {
errorCode: "BookingReschedulingMeetingFailed",
@@ -1183,7 +1187,15 @@ async function handler(
const copyEvent = cloneDeep(evt);
await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
const results = updateManager.results;
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
// TODO send reschedule emails to attendees of the old booking
await sendRescheduledEmails({
@@ -1266,7 +1278,15 @@ async function handler(
const copyEvent = cloneDeep(evt);
await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
const results = updateManager.results;
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person);
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
@@ -1575,7 +1595,7 @@ async function handler(
return prisma.booking.create(createBookingObj);
}
let results: EventResult<AdditionalInformation & { url?: string }>[] = [];
let results: EventResult<AdditionalInformation & { url?: string; iCalUID?: string }>[] = [];
let referencesToCreate: PartialReference[] = [];
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
@@ -1716,6 +1736,11 @@ async function handler(
log.error(`Booking ${organizerUser.name} failed`, error, results);
} else {
const metadata: AdditionalInformation = {};
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
if (results.length) {
// TODO: Handle created event metadata more elegantly
+1
View File
@@ -235,6 +235,7 @@ export const createdEventSchema = z
id: z.string(),
password: z.union([z.string(), z.undefined()]),
onlineMeetingUrl: z.string().nullable(),
iCalUID: z.string().optional(),
})
.passthrough();
+2
View File
@@ -61,6 +61,7 @@ export type NewCalendarEventType = {
password: string;
url: string;
additionalInfo: AdditionalInfo;
iCalUID?: string | null;
};
export type CalendarEventType = {
@@ -172,6 +173,7 @@ export interface CalendarEvent {
seatsShowAttendees?: boolean | null;
attendeeSeatId?: string;
seatsPerTimeSlot?: number | null;
iCalUID?: string | null;
// It has responses to all the fields(system + user)
responses?: CalEventResponses | null;
+1 -3
View File
@@ -1,7 +1,4 @@
import { DestinationCalendar } from "@prisma/client";
import type { CalendarEvent } from "./Calendar";
import type { Event } from "./Event";
export interface PartialReference {
id?: number;
@@ -19,6 +16,7 @@ export interface EventResult<T> {
appName: string;
success: boolean;
uid: string;
iCalUID?: string | null;
createdEvent?: T;
updatedEvent?: T | T[];
originalEvent: CalendarEvent;