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>pull/8087/head
parent
fd84b5754d
commit
2e0951d4dc
|
@ -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);
|
||||
|
||||
|
|
|
@ -265,6 +265,7 @@ export const createEvent = async (
|
|||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
iCalUID: creationResult?.iCalUID || undefined,
|
||||
createdEvent: creationResult,
|
||||
originalEvent: calEvent,
|
||||
calError,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,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;
|
||||
|
|
Loading…
Reference in New Issue