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 13:13:57 -04:00 committed by GitHub
parent fd84b5754d
commit 2e0951d4dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 67 additions and 21 deletions

View File

@ -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);

View File

@ -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);

View File

@ -265,6 +265,7 @@ export const createEvent = async (
type: credential.type,
success,
uid,
iCalUID: creationResult?.iCalUID || undefined,
createdEvent: creationResult,
originalEvent: calEvent,
calError,

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() ?? "",

View File

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

View File

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

View File

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

View File

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

View File

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

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();

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;

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;