Compare commits

...

23 Commits

Author SHA1 Message Date
Joe Au-Yeung 6c1aa1a0ac Add ICAL_UID_DOMAIN to .env 2023-10-31 14:16:16 -04:00
Joe Au-Yeung 30bd23406a Typo fix 2023-10-31 13:30:25 -04:00
Joe Au-Yeung 8ef5e6f256 Add booking action enum to emails 2023-10-31 13:23:43 -04:00
Joe Au-Yeung 929e76020f Add booking actions 2023-10-31 12:55:44 -04:00
Joe Au-Yeung a6f881c087 Add cancel status to cancelled booking 2023-10-30 16:52:21 -04:00
Joe Au-Yeung 7ed31c441c Add status as param 2023-10-30 16:46:56 -04:00
Joe Au-Yeung 397f6eba01 Use `generateIcsString` for rescheduled emails 2023-10-30 16:33:11 -04:00
Joe Au-Yeung 006b9fc4e2 Remove rewriting iCalUID when rescheduling 2023-10-30 16:32:49 -04:00
Joe Au-Yeung 6e81dc9c12 Refactor to single generateIcsString function 2023-10-30 16:32:05 -04:00
Joe Au-Yeung a4d48f81e0 Clean up unused organizer parameters 2023-10-30 13:30:55 -04:00
Joe Au-Yeung 1c79b5532f Only include app status to email 2023-10-30 13:18:54 -04:00
Joe Au-Yeung 49706b0bd4 Create `generateIcsString` function 2023-10-30 13:14:31 -04:00
Joe Au-Yeung 674c0e2b54 Clean up 2023-10-30 10:51:05 -04:00
Joe Au-Yeung 833d3a25bf Fix updating iCalUID 2023-10-27 16:35:08 -04:00
Joe Au-Yeung 83cb60b0cc Update iCalSequence on normal booking 2023-10-27 15:40:05 -04:00
Joe Au-Yeung 4813b34feb On reschedule update sequence 2023-10-27 14:59:00 -04:00
Joe Au-Yeung 757c878200 When rescheduling, keep same iCalUID as original booking 2023-10-27 13:54:49 -04:00
Joe Au-Yeung 25b83c3978 Update migration to include iCalSequence 2023-10-27 12:58:13 -04:00
Joe Au-Yeung f050f0c869 Request reschedule send correct ics file 2023-10-26 16:57:11 -04:00
Joe Au-Yeung 8371a311b1 GCal set iCalUID 2023-10-26 16:56:45 -04:00
Joe Au-Yeung e401431e60 Add iCalUID to seeder 2023-10-26 16:48:59 -04:00
Joe Au-Yeung 11e1d6fd00 Write iCalUID to booking 2023-10-26 16:48:42 -04:00
Joe Au-Yeung b045e996b4 Add iCalUID to booking 2023-10-26 16:48:15 -04:00
21 changed files with 265 additions and 132 deletions

View File

@ -257,3 +257,5 @@ E2E_TEST_OIDC_USER_EMAIL=
E2E_TEST_OIDC_USER_PASSWORD= E2E_TEST_OIDC_USER_PASSWORD=
# *********************************************************************************************************** # ***********************************************************************************************************
ICAL_UID_DOMAIN="cal.com"

View File

@ -200,6 +200,7 @@ export default class GoogleCalendarService implements Calendar {
useDefault: true, useDefault: true,
}, },
guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true, guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true,
iCalUID: calEventRaw.iCalUID,
}; };
if (calEventRaw.location) { if (calEventRaw.location) {
@ -248,7 +249,6 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar", type: "google_calendar",
password: "", password: "",
url: "", url: "",
iCalUID: event.data.iCalUID,
}; };
} catch (error) { } catch (error) {
console.error("There was an error contacting google calendar service: ", error); console.error("There was an error contacting google calendar service: ", error);

View File

@ -0,0 +1,132 @@
import type { DateArray, ParticipationStatus, ParticipationRole, EventStatus } from "ics";
import { createEvent } from "ics";
import type { TFunction } from "next-i18next";
import { RRule } from "rrule";
import dayjs from "@calcom/dayjs";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import { getWhen } from "@calcom/lib/CalEventParser";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
export enum BookingAction {
Create = "create",
Cancel = "cancel",
Reschedule = "reschedule",
RequestReschedule = "request_reschedule",
}
const generateIcsString = ({
event,
t,
role,
bookingAction,
}: {
event: CalendarEvent;
t: TFunction;
role: "attendee" | "organizer";
bookingAction: BookingAction;
}) => {
let title: string, subtitle: string, status: EventStatus;
// Taking care of recurrence rule
let recurrenceRule: string | undefined = undefined;
const partstat: ParticipationStatus = "ACCEPTED";
const icsRole: ParticipationRole = "REQ-PARTICIPANT";
if (event.recurringEvent?.count) {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(event.recurringEvent).toString().replace("RRULE:", "");
}
switch (bookingAction) {
case BookingAction.Create:
if (role === "organizer") {
title = event.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled";
} else if (role === "attendee") {
title = event.recurringEvent?.count
? "your_event_has_been_scheduled_recurring"
: "your_event_has_been_scheduled";
}
status = "CONFIRMED";
break;
case BookingAction.Cancel:
title = "event_request_cancelled";
status = "CANCELLED";
break;
case BookingAction.Reschedule:
title = "event_has_been_rescheduled";
status = "CONFIRMED";
break;
case BookingAction.RequestReschedule:
if (role === "organizer") {
title = t("request_reschedule_title_organizer", {
attendee: event.attendees[0].name,
});
subtitle = t("request_reschedule_subtitle_organizer", {
attendee: event.attendees[0].name,
});
} else if (role === "attendee") {
title = "request_reschedule_booking";
subtitle = t("request_reschedule_subtitle", {
organizer: event.organizer.name,
});
}
status = "CANCELLED";
}
const getTextBody = (title = "", subtitle = "emailed_you_and_any_other_attendees"): string => {
if (BookingAction.RequestReschedule && role === "attendee") {
return `
${t(title)}
${getWhen(event, t)}
${t(subtitle)}`;
}
return `
${t(title)}
${t(subtitle)}
${getRichDescription(event, t)}
`.trim();
};
const icsEvent = createEvent({
uid: event.iCalUID || event.uid!,
sequence: event.iCalSequence || 0,
start: dayjs(event.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calcom/ics",
title: event.title,
description: getTextBody(title, subtitle),
duration: { minutes: dayjs(event.endTime).diff(dayjs(event.startTime), "minute") },
organizer: { name: event.organizer.name, email: event.organizer.email },
...{ recurrenceRule },
attendees: [
...event.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
partstat,
role: icsRole,
rsvp: true,
})),
...(event.team?.members
? event.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
partstat,
role: icsRole,
rsvp: true,
}))
: []),
],
method: "REQUEST",
status,
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
};
export default generateIcsString;

View File

@ -1,9 +1,20 @@
import { renderEmail } from "../"; import { renderEmail } from "../";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email"; import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail { export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> { protected getNodeMailerPayload(): Record<string, unknown> {
return { return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "attendee",
bookingAction: BookingAction.Cancel,
}),
method: "REQUEST",
},
to: `${this.attendee.name} <${this.attendee.email}>`, to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email, replyTo: this.calEvent.organizer.email,

View File

@ -1,4 +1,5 @@
import { renderEmail } from "../"; import { renderEmail } from "../";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email"; import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail { export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
@ -6,7 +7,13 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
return { return {
icalEvent: { icalEvent: {
filename: "event.ics", filename: "event.ics",
content: this.getiCalEventAsString(), content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "attendee",
bookingAction: BookingAction.Reschedule,
}),
method: "REQUEST",
}, },
to: `${this.attendee.name} <${this.attendee.email}>`, to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,

View File

@ -1,16 +1,13 @@
import type { DateArray, ParticipationStatus, ParticipationRole } from "ics";
import { createEvent } from "ics";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import type { TFunction } from "next-i18next"; import type { TFunction } from "next-i18next";
import { RRule } from "rrule";
import dayjs from "@calcom/dayjs";
import { getRichDescription } from "@calcom/lib/CalEventParser"; import { getRichDescription } from "@calcom/lib/CalEventParser";
import { TimeFormat } from "@calcom/lib/timeFormat"; import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../"; import { renderEmail } from "../";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import BaseEmail from "./_base-email"; import BaseEmail from "./_base-email";
export default class AttendeeScheduledEmail extends BaseEmail { export default class AttendeeScheduledEmail extends BaseEmail {
@ -32,65 +29,18 @@ export default class AttendeeScheduledEmail extends BaseEmail {
this.t = attendee.language.translate; this.t = attendee.language.translate;
} }
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule
let recurrenceRule: string | undefined = undefined;
if (this.calEvent.recurringEvent?.count) {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
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: "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,
email: attendee.email,
partstat,
role,
rsvp: true,
})),
...(this.calEvent.team?.members
? this.calEvent.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
partstat,
role,
rsvp: true,
}))
: []),
],
method: "REQUEST",
...{ recurrenceRule },
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
protected getNodeMailerPayload(): Record<string, unknown> { protected getNodeMailerPayload(): Record<string, unknown> {
const clonedCalEvent = cloneDeep(this.calEvent); const clonedCalEvent = cloneDeep(this.calEvent);
this.getiCalEventAsString();
return { return {
icalEvent: { icalEvent: {
filename: "event.ics", filename: "event.ics",
content: this.getiCalEventAsString(), content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "attendee",
bookingAction: BookingAction.Create,
}),
method: "REQUEST", method: "REQUEST",
}, },
to: `${this.attendee.name} <${this.attendee.email}>`, to: `${this.attendee.name} <${this.attendee.email}>`,
@ -114,7 +64,7 @@ ${this.t(
)} )}
${this.t(subtitle)} ${this.t(subtitle)}
${getRichDescription(this.calEvent, this.t)} ${getRichDescription(this.calEvent, this.t, true)}
`.trim(); `.trim();
} }

View File

@ -7,6 +7,7 @@ import { APP_NAME } from "@calcom/lib/constants";
import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from ".."; import { renderEmail } from "..";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email"; import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail { export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@ -22,7 +23,13 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
return { return {
icalEvent: { icalEvent: {
filename: "event.ics", filename: "event.ics",
content: this.getiCalEventAsString(), content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "attendee",
bookingAction: BookingAction.RequestReschedule,
}),
method: "REQUEST",
}, },
from: `${APP_NAME} <${this.getMailerOptions().from}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","), to: toAddresses.join(","),
@ -42,6 +49,8 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
// @OVERRIDE // @OVERRIDE
protected getiCalEventAsString(): string | undefined { protected getiCalEventAsString(): string | undefined {
const icsEvent = createEvent({ const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
sequence: 100,
start: dayjs(this.calEvent.startTime) start: dayjs(this.calEvent.startTime)
.utc() .utc()
.toArray() .toArray()
@ -61,7 +70,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
email: attendee.email, email: attendee.email,
})), })),
status: "CANCELLED", status: "CANCELLED",
method: "CANCEL", method: "REQUEST",
}); });
if (icsEvent.error) { if (icsEvent.error) {
throw icsEvent.error; throw icsEvent.error;

View File

@ -53,7 +53,7 @@ ${this.t(
)} )}
${this.t(subtitle)} ${this.t(subtitle)}
${extraInfo} ${extraInfo}
${getRichDescription(this.calEvent)} ${getRichDescription(this.calEvent, this.t, true)}
${callToAction} ${callToAction}
`.trim(); `.trim();
} }

View File

@ -1,6 +1,7 @@
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../"; import { renderEmail } from "../";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email"; import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
@ -8,6 +9,16 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return { return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "organizer",
bookingAction: BookingAction.Cancel,
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","), to: toAddresses.join(","),
subject: `${this.t("event_cancelled_subject", { subject: `${this.t("event_cancelled_subject", {

View File

@ -7,6 +7,7 @@ import { APP_NAME } from "@calcom/lib/constants";
import type { CalendarEvent } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from ".."; import { renderEmail } from "..";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email"; import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail { export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@ -21,7 +22,13 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
return { return {
icalEvent: { icalEvent: {
filename: "event.ics", filename: "event.ics",
content: this.getiCalEventAsString(), content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "organizer",
bookingAction: BookingAction.RequestReschedule,
}),
method: "REQUEST",
}, },
from: `${APP_NAME} <${this.getMailerOptions().from}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","), to: toAddresses.join(","),
@ -83,13 +90,11 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
} }
// @OVERRIDE // @OVERRIDE
protected getTextBody(title = "", subtitle = "", extraInfo = "", callToAction = ""): string { protected getTextBody(title = "", subtitle = ""): string {
return ` return `
${this.t(title)} ${this.t(title)}
${this.t(subtitle)} ${this.t(subtitle)}
${extraInfo} ${getRichDescription(this.calEvent, this.t, true)}
${getRichDescription(this.calEvent)}
${callToAction}
`.trim(); `.trim();
} }
} }

View File

@ -1,6 +1,7 @@
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../"; import { renderEmail } from "../";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email"; import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail { export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
@ -10,7 +11,13 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
return { return {
icalEvent: { icalEvent: {
filename: "event.ics", filename: "event.ics",
content: this.getiCalEventAsString(), content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "organizer",
bookingAction: BookingAction.Reschedule,
}),
method: "REQUEST",
}, },
from: `${APP_NAME} <${this.getMailerOptions().from}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","), to: toAddresses.join(","),

View File

@ -1,17 +1,14 @@
import type { DateArray } from "ics";
import { createEvent } from "ics";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import type { TFunction } from "next-i18next"; import type { TFunction } from "next-i18next";
import { RRule } from "rrule";
import dayjs from "@calcom/dayjs";
import { getRichDescription } from "@calcom/lib/CalEventParser"; import { getRichDescription } from "@calcom/lib/CalEventParser";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { TimeFormat } from "@calcom/lib/timeFormat"; import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../"; import { renderEmail } from "../";
import generateIcsString, { BookingAction } from "../lib/generateIcsString";
import BaseEmail from "./_base-email"; import BaseEmail from "./_base-email";
export default class OrganizerScheduledEmail extends BaseEmail { export default class OrganizerScheduledEmail extends BaseEmail {
@ -29,47 +26,6 @@ export default class OrganizerScheduledEmail extends BaseEmail {
this.teamMember = input.teamMember; this.teamMember = input.teamMember;
} }
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule
let recurrenceRule: string | undefined = undefined;
if (this.calEvent.recurringEvent?.count) {
// ics appends "RRULE:" already, so removing it from RRule generated string
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: "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 },
...{ recurrenceRule },
attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
...(this.calEvent.team?.members
? this.calEvent.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
}))
: []),
],
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
protected getNodeMailerPayload(): Record<string, unknown> { protected getNodeMailerPayload(): Record<string, unknown> {
const clonedCalEvent = cloneDeep(this.calEvent); const clonedCalEvent = cloneDeep(this.calEvent);
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
@ -77,7 +33,13 @@ export default class OrganizerScheduledEmail extends BaseEmail {
return { return {
icalEvent: { icalEvent: {
filename: "event.ics", filename: "event.ics",
content: this.getiCalEventAsString(), content: generateIcsString({
event: this.calEvent,
t: this.t,
role: "organizer",
bookingAction: BookingAction.Create,
}),
method: "REQUEST",
}, },
from: `${APP_NAME} <${this.getMailerOptions().from}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","), to: toAddresses.join(","),
@ -93,20 +55,13 @@ export default class OrganizerScheduledEmail extends BaseEmail {
}; };
} }
protected getTextBody( protected getTextBody(title = "", subtitle = "emailed_you_and_any_other_attendees"): string {
title = "",
subtitle = "emailed_you_and_any_other_attendees",
extraInfo = "",
callToAction = ""
): string {
return ` return `
${this.t( ${this.t(
title || this.calEvent.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled" title || this.calEvent.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled"
)} )}
${this.t(subtitle)} ${this.t(subtitle)}
${extraInfo} ${getRichDescription(this.calEvent, this.t, true)}
${getRichDescription(this.calEvent)}
${callToAction}
`.trim(); `.trim();
} }

View File

@ -110,6 +110,8 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
scheduledJobs: true, scheduledJobs: true,
seatsReferences: true, seatsReferences: true,
responses: true, responses: true,
iCalUID: true,
iCalSequence: true,
}, },
}); });
} }
@ -261,6 +263,8 @@ async function handler(req: CustomRequest) {
...(teamMembers && { team: { name: "", members: teamMembers } }), ...(teamMembers && { team: { name: "", members: teamMembers } }),
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot, seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees, seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
iCalUID: bookingToDelete.iCalUID,
iCalSequence: bookingToDelete.iCalSequence + 1,
}; };
const dataForWebhooks = { evt, webhooks, eventTypeInfo }; const dataForWebhooks = { evt, webhooks, eventTypeInfo };
@ -387,6 +391,7 @@ async function handler(req: CustomRequest) {
data: { data: {
status: BookingStatus.CANCELLED, status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason, cancellationReason: cancellationReason,
iCalSequence: evt.iCalSequence,
}, },
select: { select: {
startTime: true, startTime: true,

View File

@ -1099,6 +1099,14 @@ async function handler(
const calEventUserFieldsResponses = const calEventUserFieldsResponses =
"calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null; "calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null;
const iCalUID = originalRescheduledBooking?.iCalUID ?? `${uid}@${process.env.ICAL_UID_DOMAIN}`;
// For bookings made before introducing iCalSequence, assume that the sequence should start at 1. For new bookings start at 0.
const iCalSequence = originalRescheduledBooking?.iCalSequence
? originalRescheduledBooking.iCalSequence + 1
: originalRescheduledBooking
? 1
: 0;
let evt: CalendarEvent = { let evt: CalendarEvent = {
bookerUrl: await getBookerUrl(organizerUser), bookerUrl: await getBookerUrl(organizerUser),
type: eventType.title, type: eventType.title,
@ -1135,6 +1143,8 @@ async function handler(
seatsPerTimeSlot: eventType.seatsPerTimeSlot, seatsPerTimeSlot: eventType.seatsPerTimeSlot,
seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true, seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true,
schedulingType: eventType.schedulingType, schedulingType: eventType.schedulingType,
iCalUID,
iCalSequence,
}; };
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") { if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") {
@ -1963,6 +1973,8 @@ async function handler(
connect: { id: evt.destinationCalendar[0].id }, connect: { id: evt.destinationCalendar[0].id },
} }
: undefined, : undefined,
iCalUID: evt.iCalUID ?? "",
iCalSequence: evt.iCalSequence ?? 0,
}; };
if (reqBody.recurringEventId) { if (reqBody.recurringEventId) {
@ -2187,12 +2199,6 @@ async function handler(
`EventManager.create failure in some of the integrations ${organizerUser.username}`, `EventManager.create failure in some of the integrations ${organizerUser.username}`,
safeStringify({ error, results }) safeStringify({ error, results })
); );
} else {
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
} }
const { metadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({ const { metadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({
@ -2299,6 +2305,18 @@ async function handler(
evt.appsStatus = handleAppsStatus(results, booking); evt.appsStatus = handleAppsStatus(results, booking);
videoCallUrl = videoCallUrl =
metadata.hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl; metadata.hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl;
if (evt.iCalUID !== booking.iCalUID) {
// The eventManager and handleAppsStatus could change the iCalUID. At this point we can update the DB record
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
iCalUID: evt.iCalUID,
},
});
}
} }
if (noEmail !== true) { if (noEmail !== true) {
let isHostConfirmationEmailsDisabled = false; let isHostConfirmationEmailsDisabled = false;

View File

@ -176,7 +176,11 @@ export const getRescheduleLink = (calEvent: CalendarEvent): string => {
return `${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`; return `${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`;
}; };
export const getRichDescription = (calEvent: CalendarEvent, t_?: TFunction /*, attendee?: Person*/) => { export const getRichDescription = (
calEvent: CalendarEvent,
t_?: TFunction /*, attendee?: Person*/,
includeAppStatus = false
) => {
const t = t_ ?? calEvent.organizer.language.translate; const t = t_ ?? calEvent.organizer.language.translate;
return ` return `
@ -189,7 +193,7 @@ ${getLocation(calEvent)}
${getDescription(calEvent, t)} ${getDescription(calEvent, t)}
${getAdditionalNotes(calEvent, t)} ${getAdditionalNotes(calEvent, t)}
${getUserFieldsResponses(calEvent)} ${getUserFieldsResponses(calEvent)}
${getAppsStatus(calEvent, t)} ${includeAppStatus ? getAppsStatus(calEvent, t) : ""}
${ ${
// TODO: Only the original attendee can make changes to the event // TODO: Only the original attendee can make changes to the event
// Guests cannot // Guests cannot

View File

@ -0,0 +1,9 @@
/*
Warnings:
- Added the required column `iCalUID` to the `Booking` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "iCalSequence" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "iCalUID" TEXT NOT NULL;

View File

@ -417,6 +417,8 @@ model Booking {
/// @zod.custom(imports.bookingMetadataSchema) /// @zod.custom(imports.bookingMetadataSchema)
metadata Json? metadata Json?
isRecorded Boolean @default(false) isRecorded Boolean @default(false)
iCalUID String
iCalSequence Int @default(0)
@@index([eventTypeId]) @@index([eventTypeId])
@@index([userId]) @@index([userId])

View File

@ -133,6 +133,7 @@ export async function createUserAndEventType({
}, },
}, },
status: bookingInput.status, status: bookingInput.status,
iCalUID: "",
}, },
}); });
console.log( console.log(

View File

@ -62,6 +62,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
scheduledJobs: true, scheduledJobs: true,
workflowReminders: true, workflowReminders: true,
responses: true, responses: true,
iCalUID: true,
}, },
where: { where: {
uid: bookingId, uid: bookingId,
@ -176,6 +177,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
tAttendees tAttendees
), ),
organizer: userAsPeopleType, organizer: userAsPeopleType,
iCalUID: bookingToReschedule.iCalUID,
}); });
const director = new CalendarEventDirector(); const director = new CalendarEventDirector();
@ -246,6 +248,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
? [bookingToReschedule?.destinationCalendar] ? [bookingToReschedule?.destinationCalendar]
: [], : [],
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
iCalUID: bookingToReschedule?.iCalUID,
}; };
// Send webhook // Send webhook

View File

@ -184,6 +184,7 @@ export interface CalendarEvent {
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
schedulingType?: SchedulingType | null; schedulingType?: SchedulingType | null;
iCalUID?: string | null; iCalUID?: string | null;
iCalSequence?: number | null;
// It has responses to all the fields(system + user) // It has responses to all the fields(system + user)
responses?: CalEventResponses | null; responses?: CalEventResponses | null;

View File

@ -239,6 +239,7 @@
"HEROKU_APP_NAME", "HEROKU_APP_NAME",
"HUBSPOT_CLIENT_ID", "HUBSPOT_CLIENT_ID",
"HUBSPOT_CLIENT_SECRET", "HUBSPOT_CLIENT_SECRET",
"ICAL_UID_DOMAIN",
"INTEGRATION_TEST_MODE", "INTEGRATION_TEST_MODE",
"INTERCOM_SECRET", "INTERCOM_SECRET",
"INTERCOM_SECRET", "INTERCOM_SECRET",