fix: Create separate calendar events to hide organizer (#21555)

* Add notes to `EventManager.create`

* Create `CalendarServiceEvent`

* Generate calendar description and remove attendees if event is private in `CalendarManager`

* Process event for reschedule method

* Refactor calendar services

* Clean up comment

* Type fixes

* Type fix

* Type fix
This commit is contained in:
Joe Au-Yeung
2025-05-27 20:46:26 -04:00
committed by GitHub
parent e5868837d5
commit 6f1eaaa02e
10 changed files with 121 additions and 94 deletions
@@ -1,9 +1,10 @@
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import { getLocation } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type {
Calendar,
CalendarServiceEvent,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
@@ -130,7 +131,7 @@ export default class FeishuCalendarService implements Calendar {
});
};
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
async createEvent(event: CalendarServiceEvent, credentialId: number): Promise<NewCalendarEventType> {
let eventId = "";
let eventRespData;
const mainHostDestinationCalendar = event.destinationCalendar
@@ -200,7 +201,7 @@ export default class FeishuCalendarService implements Calendar {
* @param event
* @returns
*/
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
async updateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId?: string) {
const eventId = uid;
let eventRespData;
const mainHostDestinationCalendar = event.destinationCalendar?.find(
@@ -372,10 +373,10 @@ export default class FeishuCalendarService implements Calendar {
}
};
private translateEvent = (event: CalendarEvent): FeishuEvent => {
private translateEvent = (event: CalendarServiceEvent): FeishuEvent => {
const feishuEvent: FeishuEvent = {
summary: event.title,
description: getRichDescription(event),
description: event.calendarDescription,
start_time: {
timestamp: parseEventTime2Timestamp(event.startTime),
timezone: event.organizer.timeZone,
@@ -12,13 +12,13 @@ import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cach
import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import { uniqueBy } from "@calcom/lib/array";
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
import prisma from "@calcom/prisma";
import type {
Calendar,
CalendarServiceEvent,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
@@ -168,75 +168,70 @@ export default class GoogleCalendarService implements Calendar {
}
async createEvent(
calEventRaw: CalendarEvent,
calEvent: CalendarServiceEvent,
credentialId: number,
externalCalendarId?: string
): Promise<NewCalendarEventType> {
this.log.debug("Creating event");
const formattedCalEvent = formatCalEvent(calEventRaw);
const payload: calendar_v3.Schema$Event = {
summary: formattedCalEvent.title,
description: getRichDescription(formattedCalEvent),
summary: calEvent.title,
description: calEvent.calendarDescription,
start: {
dateTime: formattedCalEvent.startTime,
timeZone: formattedCalEvent.organizer.timeZone,
dateTime: calEvent.startTime,
timeZone: calEvent.organizer.timeZone,
},
end: {
dateTime: formattedCalEvent.endTime,
timeZone: formattedCalEvent.organizer.timeZone,
dateTime: calEvent.endTime,
timeZone: calEvent.organizer.timeZone,
},
attendees: this.getAttendees({ event: formattedCalEvent, hostExternalCalendarId: externalCalendarId }),
attendees: this.getAttendees({ event: calEvent, hostExternalCalendarId: externalCalendarId }),
reminders: {
useDefault: true,
},
guestsCanSeeOtherGuests: !!formattedCalEvent.seatsPerTimeSlot
? formattedCalEvent.seatsShowAttendees
: true,
iCalUID: formattedCalEvent.iCalUID,
guestsCanSeeOtherGuests: !!calEvent.seatsPerTimeSlot ? calEvent.seatsShowAttendees : true,
iCalUID: calEvent.iCalUID,
};
if (calEventRaw.hideCalendarEventDetails) {
if (calEvent.hideCalendarEventDetails) {
payload.visibility = "private";
}
if (formattedCalEvent.location) {
payload["location"] = getLocation(formattedCalEvent);
if (calEvent.location) {
payload["location"] = getLocation(calEvent);
}
if (formattedCalEvent.recurringEvent) {
if (calEvent.recurringEvent) {
const rule = new RRule({
freq: formattedCalEvent.recurringEvent.freq,
interval: formattedCalEvent.recurringEvent.interval,
count: formattedCalEvent.recurringEvent.count,
freq: calEvent.recurringEvent.freq,
interval: calEvent.recurringEvent.interval,
count: calEvent.recurringEvent.count,
});
payload["recurrence"] = [rule.toString()];
}
if (formattedCalEvent.conferenceData && formattedCalEvent.location === MeetLocationType) {
payload["conferenceData"] = formattedCalEvent.conferenceData;
if (calEvent.conferenceData && calEvent.location === MeetLocationType) {
payload["conferenceData"] = calEvent.conferenceData;
}
const calendar = await this.authedCalendar();
// Find in formattedCalEvent.destinationCalendar the one with the same credentialId
const selectedCalendar =
externalCalendarId ??
(formattedCalEvent.destinationCalendar?.find((cal) => cal.credentialId === credentialId)?.externalId ||
(calEvent.destinationCalendar?.find((cal) => cal.credentialId === credentialId)?.externalId ||
"primary");
try {
let event: calendar_v3.Schema$Event | undefined;
let recurringEventId = null;
if (formattedCalEvent.existingRecurringEvent) {
recurringEventId = formattedCalEvent.existingRecurringEvent.recurringEventId;
if (calEvent.existingRecurringEvent) {
recurringEventId = calEvent.existingRecurringEvent.recurringEventId;
const recurringEventInstances = await calendar.events.instances({
calendarId: selectedCalendar,
eventId: formattedCalEvent.existingRecurringEvent.recurringEventId,
eventId: calEvent.existingRecurringEvent.recurringEventId,
});
if (recurringEventInstances.data.items) {
const calComEventStartTime = dayjs(formattedCalEvent.startTime)
.tz(formattedCalEvent.organizer.timeZone)
.format();
const calComEventStartTime = dayjs(calEvent.startTime).tz(calEvent.organizer.timeZone).format();
for (let i = 0; i < recurringEventInstances.data.items.length; i++) {
const instance = recurringEventInstances.data.items[i];
const instanceStartTime = dayjs(instance.start?.dateTime)
@@ -260,10 +255,8 @@ export default class GoogleCalendarService implements Calendar {
calendarId: selectedCalendar,
eventId: event.id || "",
requestBody: {
location: getLocation(formattedCalEvent),
description: getRichDescription({
...formattedCalEvent,
}),
location: getLocation(calEvent),
description: calEvent.calendarDescription,
},
});
}
@@ -290,7 +283,7 @@ export default class GoogleCalendarService implements Calendar {
eventId: event.id || "",
requestBody: {
description: getRichDescription({
...formattedCalEvent,
...calEvent,
additionalInformation: { hangoutLink: event.hangoutLink },
}),
},
@@ -340,43 +333,38 @@ export default class GoogleCalendarService implements Calendar {
}
}
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
const formattedCalEvent = formatCalEvent(event);
async updateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId: string): Promise<any> {
const payload: calendar_v3.Schema$Event = {
summary: formattedCalEvent.title,
description: getRichDescription(formattedCalEvent),
summary: event.title,
description: event.calendarDescription,
start: {
dateTime: formattedCalEvent.startTime,
timeZone: formattedCalEvent.organizer.timeZone,
dateTime: event.startTime,
timeZone: event.organizer.timeZone,
},
end: {
dateTime: formattedCalEvent.endTime,
timeZone: formattedCalEvent.organizer.timeZone,
dateTime: event.endTime,
timeZone: event.organizer.timeZone,
},
attendees: this.getAttendees({ event: formattedCalEvent, hostExternalCalendarId: externalCalendarId }),
attendees: this.getAttendees({ event, hostExternalCalendarId: externalCalendarId }),
reminders: {
useDefault: true,
},
guestsCanSeeOtherGuests: !!formattedCalEvent.seatsPerTimeSlot
? formattedCalEvent.seatsShowAttendees
: true,
guestsCanSeeOtherGuests: !!event.seatsPerTimeSlot ? event.seatsShowAttendees : true,
};
if (formattedCalEvent.location) {
payload["location"] = getLocation(formattedCalEvent);
if (event.location) {
payload["location"] = getLocation(event);
}
if (formattedCalEvent.conferenceData && formattedCalEvent.location === MeetLocationType) {
payload["conferenceData"] = formattedCalEvent.conferenceData;
if (event.conferenceData && event.location === MeetLocationType) {
payload["conferenceData"] = event.conferenceData;
}
const calendar = await this.authedCalendar();
const selectedCalendar =
(externalCalendarId
? formattedCalEvent.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)
?.externalId
? event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId
: undefined) || "primary";
try {
@@ -401,7 +389,7 @@ export default class GoogleCalendarService implements Calendar {
eventId: evt.data.id || "",
requestBody: {
description: getRichDescription({
...formattedCalEvent,
...event,
additionalInformation: { hangoutLink: evt.data.hangoutLink },
}),
},
@@ -423,7 +411,7 @@ export default class GoogleCalendarService implements Calendar {
} catch (error) {
this.log.error(
"There was an error updating event in google calendar: ",
safeStringify({ error, event: formattedCalEvent, uid })
safeStringify({ error, event, uid })
);
throw error;
}
@@ -1,9 +1,10 @@
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import { getLocation } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type {
Calendar,
CalendarServiceEvent,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
@@ -130,7 +131,7 @@ export default class LarkCalendarService implements Calendar {
});
};
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
async createEvent(event: CalendarServiceEvent, credentialId: number): Promise<NewCalendarEventType> {
let eventId = "";
let eventRespData;
const mainHostDestinationCalendar = event.destinationCalendar
@@ -200,7 +201,7 @@ export default class LarkCalendarService implements Calendar {
* @param event
* @returns
*/
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
async updateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId?: string) {
const eventId = uid;
let eventRespData;
const mainHostDestinationCalendar = event.destinationCalendar?.find(
@@ -372,10 +373,10 @@ export default class LarkCalendarService implements Calendar {
}
};
private translateEvent = (event: CalendarEvent): LarkEvent => {
private translateEvent = (event: CalendarServiceEvent): LarkEvent => {
const larkEvent: LarkEvent = {
summary: event.title,
description: getRichDescription(event),
description: event.calendarDescription,
start_time: {
timestamp: parseEventTime2Timestamp(event.startTime),
timezone: event.organizer.timeZone,
@@ -2,18 +2,17 @@ import type { Calendar as OfficeCalendar, User, Event } from "@microsoft/microso
import type { DefaultBodyType } from "msw";
import dayjs from "@calcom/dayjs";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import { getLocation } from "@calcom/lib/CalEventParser";
import {
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialConfigurationError,
} from "@calcom/lib/CalendarAppError";
import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors";
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
import logger from "@calcom/lib/logger";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type {
Calendar,
CalendarEvent,
CalendarServiceEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
@@ -240,7 +239,7 @@ export default class Office365CalendarService implements Calendar {
return azureUserId ? `/users/${this.azureUserId}` : "/me";
}
async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
async createEvent(event: CalendarServiceEvent, credentialId: number): Promise<NewCalendarEventType> {
const mainHostDestinationCalendar = event.destinationCalendar
? event.destinationCalendar.find((cal) => cal.credentialId === credentialId) ??
event.destinationCalendar[0]
@@ -250,10 +249,9 @@ export default class Office365CalendarService implements Calendar {
? `${await this.getUserEndpoint()}/calendars/${mainHostDestinationCalendar?.externalId}/events`
: `${await this.getUserEndpoint()}/calendar/events`;
const formattedEvent = formatCalEvent(event);
const response = await this.fetcher(eventsUrl, {
method: "POST",
body: JSON.stringify(this.translateEvent(formattedEvent)),
body: JSON.stringify(this.translateEvent(event)),
});
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
@@ -266,7 +264,7 @@ export default class Office365CalendarService implements Calendar {
}
}
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
async updateEvent(uid: string, event: CalendarServiceEvent): Promise<NewCalendarEventType> {
try {
const response = await this.fetcher(`${await this.getUserEndpoint()}/calendar/events/${uid}`, {
method: "PATCH",
@@ -403,12 +401,12 @@ export default class Office365CalendarService implements Calendar {
});
}
private translateEvent = (event: CalendarEvent) => {
private translateEvent = (event: CalendarServiceEvent) => {
const office365Event: Event = {
subject: event.title,
body: {
contentType: "text",
content: getRichDescription(event),
content: event.calendarDescription,
},
start: {
dateTime: dayjs(event.startTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
@@ -1,11 +1,12 @@
import { stringify } from "querystring";
import dayjs from "@calcom/dayjs";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import { getLocation } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type {
Calendar,
CalendarServiceEvent,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
@@ -112,7 +113,7 @@ export default class ZohoCalendarService implements Calendar {
return this.handleData(response, this.log);
};
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
async createEvent(event: CalendarServiceEvent): Promise<NewCalendarEventType> {
let eventId = "";
let eventRespData;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
@@ -158,7 +159,7 @@ export default class ZohoCalendarService implements Calendar {
* @param event
* @returns
*/
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
async updateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId?: string) {
const eventId = uid;
let eventRespData;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
@@ -457,10 +458,10 @@ export default class ZohoCalendarService implements Calendar {
return data;
}
private translateEvent = (event: CalendarEvent) => {
private translateEvent = (event: CalendarServiceEvent) => {
const zohoEvent = {
title: event.title,
description: getRichDescription(event),
description: event.calendarDescription,
dateandtime: {
start: dayjs(event.startTime).format("YYYYMMDDTHHmmssZZ"),
end: dayjs(event.endTime).format("YYYYMMDDTHHmmssZZ"),
@@ -2,6 +2,7 @@ import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { sendCancelledSeatEmailsAndSMS } from "@calcom/emails";
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import { getAllDelegationCredentialsForUserIncludeServiceAccountKey } from "@calcom/lib/delegationCredential/server";
import { getDelegationCredentialOrFindRegularCredential } from "@calcom/lib/delegationCredential/server";
import { HttpError } from "@calcom/lib/http-error";
@@ -96,6 +97,7 @@ async function cancelAttendeeSeat(
const updatedEvt = {
...evt,
attendees: evt.attendees.filter((evtAttendee) => attendee.email !== evtAttendee.email),
calendarDescription: getRichDescription(evt),
};
if (reference.type.includes("_video")) {
integrationsToUpdate.push(updateMeeting(credential, updatedEvt, reference));
+38 -6
View File
@@ -5,13 +5,17 @@ import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import getApps from "@calcom/app-store/utils";
import dayjs from "@calcom/dayjs";
import { getUid } from "@calcom/lib/CalEventParser";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import { CalendarAppDelegationCredentialError } from "@calcom/lib/CalendarAppError";
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
import { buildNonDelegationCredentials } from "@calcom/lib/delegationCredential/clientAndServer";
import { formatCalEvent } from "@calcom/lib/formatCalendarEvent";
import logger from "@calcom/lib/logger";
import { getPiiFreeCalendarEvent, getPiiFreeCredential } from "@calcom/lib/piiFreeData";
import { safeStringify } from "@calcom/lib/safeStringify";
import type {
CalendarEvent,
CalendarServiceEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
@@ -280,10 +284,12 @@ export const getBusyCalendarTimes = async (
export const createEvent = async (
credential: CredentialForCalendarService,
calEvent: CalendarEvent,
originalEvent: CalendarEvent,
externalId?: string
): Promise<EventResult<NewCalendarEventType>> => {
const uid: string = getUid(calEvent);
// Some calendar libraries may edit the original event so let's clone it
const formattedEvent = formatCalEvent(originalEvent);
const uid: string = getUid(formattedEvent);
const calendar = await getCalendar(credential);
let success = true;
let calError: string | undefined = undefined;
@@ -291,14 +297,16 @@ export const createEvent = async (
log.debug(
"Creating calendar event",
safeStringify({
calEvent: getPiiFreeCalendarEvent(calEvent),
calEvent: getPiiFreeCalendarEvent(formattedEvent),
})
);
// Check if the disabledNotes flag is set to true
if (calEvent.hideCalendarNotes) {
calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
if (formattedEvent.hideCalendarNotes) {
formattedEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
}
const calEvent = processEvent(formattedEvent);
const externalCalendarIdWhenDelegationCredentialIsChosen = credential.delegatedToId
? externalId
: undefined;
@@ -369,10 +377,12 @@ export const createEvent = async (
export const updateEvent = async (
credential: CredentialForCalendarService,
calEvent: CalendarEvent,
rawCalEvent: CalendarEvent,
bookingRefUid: string | null,
externalCalendarId: string | null
): Promise<EventResult<NewCalendarEventType>> => {
const formattedEvent = formatCalEvent(rawCalEvent);
const calEvent = processEvent(formattedEvent);
const uid = getUid(calEvent);
const calendar = await getCalendar(credential);
let success = false;
@@ -479,3 +489,25 @@ export const deleteEvent = async ({
return Promise.resolve({});
};
/**
* Process the calendar event by generating description and removing attendees if needed
*/
const processEvent = (calEvent: CalendarEvent): CalendarServiceEvent => {
// Generate the calendar event description
const calendarEvent: CalendarServiceEvent = {
...calEvent,
calendarDescription: getRichDescription(calEvent),
};
// Determine if the calendar event should include attendees
const isOrganizerExempt = ORGANIZER_EMAIL_EXEMPT_DOMAINS?.split(",")
.filter((domain) => domain.trim() !== "")
.some((domain) => calEvent.organizer.email.toLowerCase().endsWith(domain.toLowerCase()));
if (calEvent.hideOrganizerEmail && !isOrganizerExempt) {
calEvent.attendees = [];
}
return calendarEvent;
};
+4 -5
View File
@@ -21,6 +21,7 @@ import sanitizeCalendarObject from "@calcom/lib/sanitizeCalendarObject";
import type { Person as AttendeeInCalendarEvent } from "@calcom/types/Calendar";
import type {
Calendar,
CalendarServiceEvent,
CalendarEvent,
CalendarEventType,
EventBusyDate,
@@ -32,7 +33,6 @@ import type { CredentialPayload } from "@calcom/types/Credential";
import { getLocation, getRichDescription } from "./CalEventParser";
import { symmetricDecrypt } from "./crypto";
import { formatCalEvent } from "./formatCalendarEvent";
import logger from "./logger";
const TIMEZONE_FORMAT = "YYYY-MM-DDTHH:mm:ss[Z]";
@@ -140,10 +140,9 @@ export default abstract class BaseCalendarService implements Calendar {
return attendees;
}
async createEvent(eventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
async createEvent(event: CalendarServiceEvent, credentialId: number): Promise<NewCalendarEventType> {
try {
const calendars = await this.listCalendars(eventRaw);
const event = formatCalEvent(eventRaw);
const calendars = await this.listCalendars(event);
const uid = uuidv4();
// We create local ICS files
@@ -153,7 +152,7 @@ export default abstract class BaseCalendarService implements Calendar {
start: convertDate(event.startTime),
duration: getDuration(event.startTime, event.endTime),
title: event.title,
description: getRichDescription(event),
description: event.calendarDescription,
location: getLocation(event),
organizer: { email: event.organizer.email, name: event.organizer.name },
attendees: this.getAttendees(event),
+2 -1
View File
@@ -180,6 +180,7 @@ export default class EventManager {
* @param event
*/
public async create(event: CalendarEvent): Promise<CreateUpdateResult> {
// TODO this method shouldn't be modifying the event object that's passed in
const evt = processLocation(event);
// Fallback to cal video if no location is set
@@ -259,7 +260,7 @@ export default class EventManager {
return result.type.includes("_calendar");
};
const createdCRMEvents = await this.createAllCRMEvents(clonedCalEvent);
const createdCRMEvents = await this.createAllCRMEvents(evt);
results.push(...createdCRMEvents);
+6 -2
View File
@@ -256,17 +256,21 @@ export interface IntegrationCalendar extends Ensure<Partial<_SelectedCalendar>,
*/
export type SelectedCalendarEventTypeIds = (number | null)[];
export interface CalendarServiceEvent extends CalendarEvent {
calendarDescription: string;
}
export interface Calendar {
getCredentialId?(): number;
createEvent(
event: CalendarEvent,
event: CalendarServiceEvent,
credentialId: number,
externalCalendarId?: string
): Promise<NewCalendarEventType>;
updateEvent(
uid: string,
event: CalendarEvent,
event: CalendarServiceEvent,
externalCalendarId?: string | null
): Promise<NewCalendarEventType | NewCalendarEventType[]>;