Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,8 @@
"event_limit_tab_description": "How often you can be booked",
"event_advanced_tab_description": "Calendar settings & more...",
"event_advanced_tab_title": "Advanced",
"optional_guest_team_members": "Add team members as optional guests",
"optional_guest_team_members_description": "Adding team members as an optional guest will always send an optional invite, but not check their availability.",
"event_payments_tab_description": "Set up payments for event",
"event_setup_multiple_duration_error": "Event Setup: Multiple durations requires at least 1 option.",
"event_setup_multiple_duration_default_error": "Event Setup: Please select a valid default duration.",
Expand Down
47 changes: 42 additions & 5 deletions packages/app-store/exchange2013calendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,29 @@ export default class ExchangeCalendarService implements Calendar {
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
// Create a set of optional guest emails for easy lookup.
const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);

// Add the main booker as required
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}

// Add team members as required, ONLY if they aren't optional
if (event.team?.members) {
event.team.members.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
event.team.members
.filter((member) => member.email && !optionalGuestEmails.has(member.email.toLowerCase()))
.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
});
}

// Add optional members to the optional list
if (event.optionalGuestTeamMembers) {
event.optionalGuestTeamMembers.forEach((member) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}

Expand All @@ -86,7 +101,7 @@ export default class ExchangeCalendarService implements Calendar {
uid: appointment.Id.UniqueId,
id: appointment.Id.UniqueId,
password: "",
type: "",
type: "exchange2013_calendar", // Added type for clarity
url: "",
additionalInfo: {},
};
Expand All @@ -109,14 +124,36 @@ export default class ExchangeCalendarService implements Calendar {
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content

// Clear old attendees before adding new ones
appointment.RequiredAttendees.Clear();
appointment.OptionalAttendees.Clear();

const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);

// Add the main booker as required
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}

// Add team members as required, ONLY if they aren't optional
if (event.team?.members) {
event.team.members.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
event.team.members
.filter((member) => member.email && !optionalGuestEmails.has(member.email.toLowerCase()))
.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
});
}

// Add optional members to the optional list
if (event.optionalGuestTeamMembers) {
event.optionalGuestTeamMembers.forEach((member) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}

appointment.Update(
ConflictResolutionMode.AlwaysOverwrite,
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
Expand Down
46 changes: 41 additions & 5 deletions packages/app-store/exchange2016calendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,29 @@ export default class ExchangeCalendarService implements Calendar {
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
// Create a set of optional guest emails for easy lookup.
const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);
Comment on lines +73 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden optional guest email normalization (avoid runtime TypeError) and add typing.

If any optionalGuestTeamMembers entries are missing email, .toLowerCase() will throw. Also, type the Set to string to keep TS strictness.

Apply this diff:

-      // Create a set of optional guest emails for easy lookup.
-      const optionalGuestEmails = new Set(
-        event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
-      );
+      // Create a set of optional guest emails for easy lookup.
+      const optionalGuestEmails = new Set<string>(
+        (event.optionalGuestTeamMembers ?? [])
+          .map((guest) => guest?.email?.trim())
+          .filter((e): e is string => !!e)
+          .map((e) => e.toLowerCase())
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create a set of optional guest emails for easy lookup.
const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);
// Create a set of optional guest emails for easy lookup.
const optionalGuestEmails = new Set<string>(
(event.optionalGuestTeamMembers ?? [])
.map((guest) => guest?.email?.trim())
.filter((e): e is string => !!e)
.map((e) => e.toLowerCase())
);
🤖 Prompt for AI Agents
In packages/app-store/exchange2016calendar/lib/CalendarService.ts around lines
73 to 76, the code builds optionalGuestEmails without typing and calls
.toLowerCase() on possibly undefined emails which can throw; change the Set to
be typed Set<string> and only include defined emails after normalizing to
lowercase. Specifically, replace the map with a guarded transform such as
filtering out entries without an email (or using flatMap) and then pushing
guest.email.toLowerCase() into the Set so TypeScript knows the values are
strings and runtime errors are avoided.


// Add the main booker as required
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}

// Add team members as required, ONLY if they aren't optional
if (event.team?.members) {
event.team.members.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
event.team.members
.filter((member) => member.email && !optionalGuestEmails.has(member.email.toLowerCase()))
.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
});
}

// Add optional members to the optional list
if (event.optionalGuestTeamMembers) {
event.optionalGuestTeamMembers.forEach((member) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}

Expand All @@ -87,7 +102,7 @@ export default class ExchangeCalendarService implements Calendar {
uid: appointment.Id.UniqueId,
id: appointment.Id.UniqueId,
password: "",
type: "",
type: "exchange2016_calendar", // Added type for clarity
url: "",
additionalInfo: {},
};
Expand All @@ -110,12 +125,33 @@ export default class ExchangeCalendarService implements Calendar {
appointment.End = DateTime.Parse(event.endTime); // moment string
appointment.Location = event.location || "Location not defined!";
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content

// Clear old attendees before adding new ones
appointment.RequiredAttendees.Clear();
appointment.OptionalAttendees.Clear();

const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);

Comment on lines +133 to +136
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Mirror the createEvent hardening: filter undefined emails and add Set typing.

Same risk here for .toLowerCase() on undefined.

Apply this diff:

-      const optionalGuestEmails = new Set(
-        event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
-      );
+      const optionalGuestEmails = new Set<string>(
+        (event.optionalGuestTeamMembers ?? [])
+          .map((guest) => guest?.email?.trim())
+          .filter((e): e is string => !!e)
+          .map((e) => e.toLowerCase())
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);
const optionalGuestEmails = new Set<string>(
(event.optionalGuestTeamMembers ?? [])
.map((guest) => guest?.email?.trim())
.filter((e): e is string => !!e)
.map((e) => e.toLowerCase())
);
🤖 Prompt for AI Agents
In packages/app-store/exchange2016calendar/lib/CalendarService.ts around lines
133 to 136, the current construction of optionalGuestEmails calls toLowerCase()
on guest.email without guarding against undefined; update this to mirror
createEvent hardening by first extracting emails, filtering out
undefined/null/empty values, then mapping to lower-case, and declare the
variable as a Set<string> to enforce typing; ensure the filter runs before
toLowerCase so no undefined is passed to the method.

// Add the main booker as required
for (let i = 0; i < event.attendees.length; i++) {
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
}

// Add team members as required, ONLY if they aren't optional
if (event.team?.members) {
event.team.members.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
event.team.members
.filter((member) => member.email && !optionalGuestEmails.has(member.email.toLowerCase()))
.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
});
}

// Add optional members to the optional list
if (event.optionalGuestTeamMembers) {
event.optionalGuestTeamMembers.forEach((member) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}
appointment.Update(
Expand Down
49 changes: 43 additions & 6 deletions packages/app-store/exchangecalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,29 @@ export default class ExchangeCalendarService implements Calendar {
appointment.End = DateTime.Parse(event.endTime);
appointment.Location = event.location || "";
appointment.Body = new MessageBody(event.description || "");
// Create a set of optional guest emails for easy lookup.
const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);

// Add the main booker as required
event.attendees.forEach((attendee: Person) => {
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
});

// Add team members as required, ONLY if they aren't optional
if (event.team?.members) {
event.team.members.forEach((member: Person) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
event.team.members
.filter((member) => member.email && !optionalGuestEmails.has(member.email.toLowerCase()))
.forEach((member: Person) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
});
}

// Add optional members to the optional list
if (event.optionalGuestTeamMembers) {
event.optionalGuestTeamMembers.forEach((member: { email: string }) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}
return appointment
Expand All @@ -75,7 +92,7 @@ export default class ExchangeCalendarService implements Calendar {
uid: appointment.Id.UniqueId,
id: appointment.Id.UniqueId,
password: "",
type: "",
type: "exchange_calendar",
url: "",
additionalInfo: {},
};
Expand All @@ -96,12 +113,32 @@ export default class ExchangeCalendarService implements Calendar {
appointment.End = DateTime.Parse(event.endTime);
appointment.Location = event.location || "";
appointment.Body = new MessageBody(event.description || "");

// Clear old attendees before adding new ones
appointment.RequiredAttendees.Clear();
appointment.OptionalAttendees.Clear();

const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);

// Add the main booker as required
event.attendees.forEach((attendee: Person) => {
appointment.RequiredAttendees.Add(new Attendee(attendee.email));
});
// Add team members as required, ONLY if they aren't optional
if (event.team?.members) {
event.team.members.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
event.team.members
.filter((member) => member.email && !optionalGuestEmails.has(member.email.toLowerCase()))
.forEach((member) => {
appointment.RequiredAttendees.Add(new Attendee(member.email));
});
}

// Add optional members to the optional list
if (event.optionalGuestTeamMembers) {
event.optionalGuestTeamMembers.forEach((member: { email: string }) => {
appointment.OptionalAttendees.Add(new Attendee(member.email));
});
}
return appointment
Expand All @@ -114,7 +151,7 @@ export default class ExchangeCalendarService implements Calendar {
uid: appointment.Id.UniqueId,
id: appointment.Id.UniqueId,
password: "",
type: "",
type: "exchange_calendar",
url: "",
additionalInfo: {},
};
Expand Down
50 changes: 34 additions & 16 deletions packages/app-store/feishucalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,27 +401,45 @@ export default class FeishuCalendarService implements Calendar {

private translateAttendees = (event: CalendarEvent): FeishuEventAttendee[] => {
const attendeeArray: FeishuEventAttendee[] = [];
event.attendees
.filter((att) => att.email)
.forEach((att) => {
const attendee: FeishuEventAttendee = {
// Use a Set to track all emails and prevent any duplicates in the final list
const addedEmails = new Set<string>();

// Helper function to add attendees if they haven't been added yet
const addUniqueAttendee = (email: string, is_optional: boolean) => {
if (email && !addedEmails.has(email.toLowerCase())) {
attendeeArray.push({
type: "third_party",
is_optional: false,
third_party_email: att.email,
};
attendeeArray.push(attendee);
});
is_optional,
third_party_email: email,
});
addedEmails.add(email.toLowerCase());
}
};

// 1. Create a Set of optional guest emails for easy lookup
const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);
Comment on lines +420 to +422
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against undefined emails to avoid runtime errors; also normalize/trim before dedup.

If any optionalGuestTeamMembers entry lacks email, calling toLowerCase() will throw. Additionally, trimming avoids duplicate entries that differ only by whitespace. Apply this small hardening:

-    const optionalGuestEmails = new Set(
-      event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
-    );
+    const optionalGuestEmails = new Set(
+      (event.optionalGuestTeamMembers ?? [])
+        .map((guest) => guest.email?.trim())
+        .filter((e): e is string => !!e)
+        .map((e) => e.toLowerCase())
+    );

And make addUniqueAttendee robust to stray whitespace and compute lowercased value once:

-    const addUniqueAttendee = (email: string, is_optional: boolean) => {
-      if (email && !addedEmails.has(email.toLowerCase())) {
-        attendeeArray.push({
-          type: "third_party",
-          is_optional,
-          third_party_email: email,
-        });
-        addedEmails.add(email.toLowerCase());
-      }
-    };
+    const addUniqueAttendee = (email: string, is_optional: boolean) => {
+      const trimmed = email?.trim();
+      const lc = trimmed?.toLowerCase();
+      if (lc && !addedEmails.has(lc)) {
+        attendeeArray.push({
+          type: "third_party",
+          is_optional,
+          third_party_email: trimmed,
+        });
+        addedEmails.add(lc);
+      }
+    };

Also applies to: 408-417

🤖 Prompt for AI Agents
In packages/app-store/feishucalendar/lib/CalendarService.ts around lines
408-422, guard against undefined/empty emails and normalize/trim before dedup:
when building optionalGuestEmails skip entries with no email, trim the email and
then compute a single lowercased value to insert into the Set (e.g. const e =
guest.email?.trim(); if (!e) continue;
optionalGuestEmails.add(e.toLowerCase())). Also update the addUniqueAttendee
helper to trim its input, skip empty strings, compute the lowercased key once,
and use that key for deduplication so stray whitespace or undefined values
cannot cause runtime errors or duplicate entries.


// 2. Add the main booker as a required attendee
event.attendees.forEach((attendee) => addUniqueAttendee(attendee.email, false));

// 3. Add the REQUIRED team members, filtering out optionals and the current user
event.team?.members.forEach((member) => {
if (member.email !== this.credential.user?.email) {
const attendee: FeishuEventAttendee = {
type: "third_party",
is_optional: false,
third_party_email: member.email,
};
attendeeArray.push(attendee);
if (
member.email &&
member.email !== this.credential.user?.email &&
!optionalGuestEmails.has(member.email.toLowerCase())
) {
addUniqueAttendee(member.email, false);
}
});

// 4. Add the OPTIONAL team members
event.optionalGuestTeamMembers?.forEach((guest) => {
addUniqueAttendee(guest.email, true);
});

return attendeeArray;
};
}
35 changes: 30 additions & 5 deletions packages/app-store/googlecalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,25 @@ export default class GoogleCalendarService implements Calendar {
id: String(event.organizer.id),
responseStatus: "accepted",
organizer: true,
// Tried changing the display name to the user but GCal will not let you do that. It will only display the name of the external calendar. Leaving this in just incase it works in the future.
displayName: event.organizer.name,
// We use || instead of ?? here to handle empty strings
email: hostExternalCalendarId || selectedHostDestinationCalendar?.externalId || event.organizer.email,
},
...(event.hideOrganizerEmail && !isOrganizerExempt ? [] : eventAttendees),
];

// Create a set of optional guest emails for easy lookup.
const optionalGuestEmails = new Set(
event.optionalGuestTeamMembers?.map((guest) => guest.email.toLowerCase()) ?? []
);

if (event.team?.members) {
// TODO: Check every other CalendarService for team members
const teamAttendeesWithoutCurrentUser = event.team.members
.filter((member) => member.email !== this.credential.user?.email)
// We now filter out the current user AND any member who is in the optional list.
.filter(
(member) =>
member.email !== this.credential.user?.email &&
!optionalGuestEmails.has(member.email.toLowerCase())
)
.map((m) => {
const teamMemberDestinationCalendar = event.destinationCalendar?.find(
(calendar) => calendar.integration === "google_calendar" && calendar.userId === m.id
Expand All @@ -127,9 +134,27 @@ export default class GoogleCalendarService implements Calendar {
attendees.push(...teamAttendeesWithoutCurrentUser);
}

if (event.optionalGuestTeamMembers) {
const optionalGuestMembers = event.optionalGuestTeamMembers
.map(({ email, name }) => ({
email,
displayName: name,
optional: true,
responseStatus: "needsAction",
}))
.filter((guest) => {
if (!guest.email) {
return false;
}
// This filter is still useful to prevent the booker from being added as optional, etc.
const guestEmail = guest.email.toLowerCase();
return !attendees.some((attendee) => attendee.email && attendee.email.toLowerCase() === guestEmail);
});
attendees.push(...optionalGuestMembers);
}

return attendees;
};

private async stopWatchingCalendarsInGoogle(
channels: { googleChannelResourceId: string | null; googleChannelId: string | null }[]
) {
Expand Down
Loading
Loading