Compare commits
23 Commits
main
...
refactor/g
Author | SHA1 | Date |
---|---|---|
Joe Au-Yeung | 6c1aa1a0ac | |
Joe Au-Yeung | 30bd23406a | |
Joe Au-Yeung | 8ef5e6f256 | |
Joe Au-Yeung | 929e76020f | |
Joe Au-Yeung | a6f881c087 | |
Joe Au-Yeung | 7ed31c441c | |
Joe Au-Yeung | 397f6eba01 | |
Joe Au-Yeung | 006b9fc4e2 | |
Joe Au-Yeung | 6e81dc9c12 | |
Joe Au-Yeung | a4d48f81e0 | |
Joe Au-Yeung | 1c79b5532f | |
Joe Au-Yeung | 49706b0bd4 | |
Joe Au-Yeung | 674c0e2b54 | |
Joe Au-Yeung | 833d3a25bf | |
Joe Au-Yeung | 83cb60b0cc | |
Joe Au-Yeung | 4813b34feb | |
Joe Au-Yeung | 757c878200 | |
Joe Au-Yeung | 25b83c3978 | |
Joe Au-Yeung | f050f0c869 | |
Joe Au-Yeung | 8371a311b1 | |
Joe Au-Yeung | e401431e60 | |
Joe Au-Yeung | 11e1d6fd00 | |
Joe Au-Yeung | b045e996b4 |
|
@ -257,3 +257,5 @@ E2E_TEST_OIDC_USER_EMAIL=
|
||||||
E2E_TEST_OIDC_USER_PASSWORD=
|
E2E_TEST_OIDC_USER_PASSWORD=
|
||||||
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
|
ICAL_UID_DOMAIN="cal.com"
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
@ -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,
|
||||||
|
|
|
@ -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}>`,
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(","),
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
@ -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])
|
||||||
|
|
|
@ -133,6 +133,7 @@ export async function createUserAndEventType({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: bookingInput.status,
|
status: bookingInput.status,
|
||||||
|
iCalUID: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in New Issue