| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | import { Credential } from "@prisma/client"; | 
					
						
							|  |  |  | import async from "async"; | 
					
						
							| 
									
										
										
										
											2021-10-13 11:35:25 +00:00
										 |  |  | import merge from "lodash/merge"; | 
					
						
							| 
									
										
										
										
											2021-09-22 19:52:38 +00:00
										 |  |  | import { v5 as uuidv5 } from "uuid"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  | import { AdditionInformation, CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient"; | 
					
						
							| 
									
										
										
										
											2021-09-06 09:06:33 +00:00
										 |  |  | import EventAttendeeMail from "@lib/emails/EventAttendeeMail"; | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  | import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail"; | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  | import { DailyEventResult, FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter"; | 
					
						
							|  |  |  | import { ZoomEventResult } from "@lib/integrations/Zoom/ZoomVideoApiAdapter"; | 
					
						
							| 
									
										
										
										
											2021-09-22 19:52:38 +00:00
										 |  |  | import { LocationType } from "@lib/location"; | 
					
						
							|  |  |  | import prisma from "@lib/prisma"; | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  | import { Ensure } from "@lib/types/utils"; | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  | import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient"; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  | export type Event = AdditionInformation & { name: string; id: string; disableConfirmationEmail?: boolean } & ( | 
					
						
							|  |  |  |     | ZoomEventResult | 
					
						
							|  |  |  |     | DailyEventResult | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | export interface EventResult { | 
					
						
							|  |  |  |   type: string; | 
					
						
							|  |  |  |   success: boolean; | 
					
						
							|  |  |  |   uid: string; | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |   createdEvent?: Event; | 
					
						
							|  |  |  |   updatedEvent?: Event; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   originalEvent: CalendarEvent; | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |   videoCallData?: VideoCallData; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  | export interface CreateUpdateResult { | 
					
						
							|  |  |  |   results: Array<EventResult>; | 
					
						
							|  |  |  |   referencesToCreate: Array<PartialReference>; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | export interface PartialBooking { | 
					
						
							|  |  |  |   id: number; | 
					
						
							|  |  |  |   references: Array<PartialReference>; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface PartialReference { | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  |   id?: number; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   type: string; | 
					
						
							|  |  |  |   uid: string; | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |   meetingId?: string | null; | 
					
						
							|  |  |  |   meetingPassword?: string | null; | 
					
						
							|  |  |  |   meetingUrl?: string | null; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-25 12:19:49 +00:00
										 |  |  | interface GetLocationRequestFromIntegrationRequest { | 
					
						
							|  |  |  |   location: string; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | export default class EventManager { | 
					
						
							|  |  |  |   calendarCredentials: Array<Credential>; | 
					
						
							|  |  |  |   videoCredentials: Array<Credential>; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-24 20:30:14 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Takes an array of credentials and initializes a new instance of the EventManager. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param credentials | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   constructor(credentials: Array<Credential>) { | 
					
						
							|  |  |  |     this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar")); | 
					
						
							|  |  |  |     this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video")); | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     //for  Daily.co video, temporarily pushes a credential for the daily-video-client
 | 
					
						
							|  |  |  |     const hasDailyIntegration = process.env.DAILY_API_KEY; | 
					
						
							|  |  |  |     if (hasDailyIntegration) { | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |       this.videoCredentials.push(FAKE_DAILY_CREDENTIAL); | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-24 20:30:14 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Takes a CalendarEvent and creates all necessary integration entries for it. | 
					
						
							|  |  |  |    * When a video integration is chosen as the event's location, a video integration | 
					
						
							|  |  |  |    * event will be scheduled for it as well. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param event | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |   public async create(event: Ensure<CalendarEvent, "language">): Promise<CreateUpdateResult> { | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |     const evt = EventManager.processLocation(event); | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |     const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null; | 
					
						
							| 
									
										
										
										
											2021-07-20 18:07:59 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-29 08:21:32 +00:00
										 |  |  |     // First, create all calendar events. If this is a dedicated integration event, don't send a mail right here.
 | 
					
						
							|  |  |  |     const results: Array<EventResult> = await this.createAllCalendarEvents(evt, isDedicated); | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |     // If and only if event type is a dedicated meeting, create a dedicated video meeting.
 | 
					
						
							| 
									
										
										
										
											2021-08-01 21:29:15 +00:00
										 |  |  |     if (isDedicated) { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       const result = await this.createVideoEvent(evt); | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |       if (result.videoCallData) { | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |         evt.videoCallData = result.videoCallData; | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |       } | 
					
						
							|  |  |  |       results.push(result); | 
					
						
							| 
									
										
										
										
											2021-09-06 09:06:33 +00:00
										 |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       await EventManager.sendAttendeeMail("new", results, evt); | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |     const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       let uid = ""; | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |       if (result.createdEvent) { | 
					
						
							|  |  |  |         const isDailyResult = result.type === "daily_video"; | 
					
						
							|  |  |  |         if (isDailyResult) { | 
					
						
							|  |  |  |           uid = (result.createdEvent as DailyEventResult).name.toString(); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           uid = (result.createdEvent as ZoomEventResult).id.toString(); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       return { | 
					
						
							|  |  |  |         type: result.type, | 
					
						
							|  |  |  |         uid, | 
					
						
							|  |  |  |         meetingId: result.videoCallData?.id.toString(), | 
					
						
							|  |  |  |         meetingPassword: result.videoCallData?.password, | 
					
						
							|  |  |  |         meetingUrl: result.videoCallData?.url, | 
					
						
							|  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       results, | 
					
						
							|  |  |  |       referencesToCreate, | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-24 20:30:14 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Takes a calendarEvent and a rescheduleUid and updates the event that has the | 
					
						
							|  |  |  |    * given uid using the data delivered in the given CalendarEvent. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param event | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |   public async update( | 
					
						
							|  |  |  |     event: Ensure<CalendarEvent, "language">, | 
					
						
							|  |  |  |     rescheduleUid: string | 
					
						
							|  |  |  |   ): Promise<CreateUpdateResult> { | 
					
						
							|  |  |  |     const evt = EventManager.processLocation(event); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!rescheduleUid) { | 
					
						
							|  |  |  |       throw new Error("You called eventManager.update without an `rescheduleUid`. This should never happen."); | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-07-25 12:19:49 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  |     // Get details of existing booking.
 | 
					
						
							|  |  |  |     const booking = await prisma.booking.findFirst({ | 
					
						
							|  |  |  |       where: { | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |         uid: rescheduleUid, | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  |       }, | 
					
						
							|  |  |  |       select: { | 
					
						
							|  |  |  |         id: true, | 
					
						
							|  |  |  |         references: { | 
					
						
							|  |  |  |           select: { | 
					
						
							|  |  |  |             id: true, | 
					
						
							|  |  |  |             type: true, | 
					
						
							|  |  |  |             uid: true, | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |             meetingId: true, | 
					
						
							|  |  |  |             meetingPassword: true, | 
					
						
							|  |  |  |             meetingUrl: true, | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  |           }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |     if (!booking) { | 
					
						
							|  |  |  |       throw new Error("booking not found"); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |     const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null; | 
					
						
							| 
									
										
										
										
											2021-10-29 08:21:32 +00:00
										 |  |  |     // First, create all calendar events. If this is a dedicated integration event, don't send a mail right here.
 | 
					
						
							|  |  |  |     const results: Array<EventResult> = await this.updateAllCalendarEvents(evt, booking, isDedicated); | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |     // If and only if event type is a dedicated meeting, update the dedicated video meeting.
 | 
					
						
							| 
									
										
										
										
											2021-08-01 21:38:38 +00:00
										 |  |  |     if (isDedicated) { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       const result = await this.updateVideoEvent(evt, booking); | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |       if (result.videoCallData) { | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |         evt.videoCallData = result.videoCallData; | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |       } | 
					
						
							|  |  |  |       results.push(result); | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       await EventManager.sendAttendeeMail("reschedule", results, evt); | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  |     // Now we can delete the old booking and its references.
 | 
					
						
							|  |  |  |     const bookingReferenceDeletes = prisma.bookingReference.deleteMany({ | 
					
						
							|  |  |  |       where: { | 
					
						
							|  |  |  |         bookingId: booking.id, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     const attendeeDeletes = prisma.attendee.deleteMany({ | 
					
						
							|  |  |  |       where: { | 
					
						
							|  |  |  |         bookingId: booking.id, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |     const bookingDeletes = prisma.booking.delete({ | 
					
						
							|  |  |  |       where: { | 
					
						
							|  |  |  |         id: booking.id, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2021-07-24 20:24:00 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Wait for all deletions to be applied.
 | 
					
						
							|  |  |  |     await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       results, | 
					
						
							|  |  |  |       referencesToCreate: [...booking.references], | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Creates event entries for all calendar integrations given in the credentials. | 
					
						
							| 
									
										
										
										
											2021-07-20 18:07:59 +00:00
										 |  |  |    * When noMail is true, no mails will be sent. This is used when the event is | 
					
						
							|  |  |  |    * a video meeting because then the mail containing the video credentials will be | 
					
						
							|  |  |  |    * more important than the mails created for these bare calendar events. | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |    * | 
					
						
							| 
									
										
										
										
											2021-07-25 15:05:18 +00:00
										 |  |  |    * When the optional uid is set, it will be used instead of the auto generated uid. | 
					
						
							|  |  |  |    * | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |    * @param event | 
					
						
							| 
									
										
										
										
											2021-07-20 18:07:59 +00:00
										 |  |  |    * @param noMail | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-11 05:50:56 +00:00
										 |  |  |   private async createAllCalendarEvents( | 
					
						
							|  |  |  |     event: CalendarEvent, | 
					
						
							|  |  |  |     noMail: boolean | null | 
					
						
							|  |  |  |   ): Promise<Array<EventResult>> { | 
					
						
							|  |  |  |     const [firstCalendar] = this.calendarCredentials; | 
					
						
							|  |  |  |     if (!firstCalendar) { | 
					
						
							|  |  |  |       return []; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return [await createEvent(firstCalendar, event, noMail)]; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-24 20:30:14 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Checks which video integration is needed for the event's location and returns | 
					
						
							|  |  |  |    * credentials for that - if existing. | 
					
						
							|  |  |  |    * @param event | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   private getVideoCredential(event: CalendarEvent): Credential | undefined { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |     if (!event.location) { | 
					
						
							|  |  |  |       return undefined; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     const integrationName = event.location.replace("integrations:", ""); | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Creates a video event entry for the selected integration location. | 
					
						
							|  |  |  |    * | 
					
						
							| 
									
										
										
										
											2021-07-25 15:05:18 +00:00
										 |  |  |    * When optional uid is set, it will be used instead of the auto generated uid. | 
					
						
							|  |  |  |    * | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |    * @param event | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |   private createVideoEvent(event: Ensure<CalendarEvent, "language">): Promise<EventResult> { | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     const credential = this.getVideoCredential(event); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |     if (credential) { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       return createMeeting(credential, event); | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     } else { | 
					
						
							|  |  |  |       return Promise.reject("No suitable credentials given for the requested integration name."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-20 18:07:59 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Updates the event entries for all calendar integrations given in the credentials. | 
					
						
							|  |  |  |    * When noMail is true, no mails will be sent. This is used when the event is | 
					
						
							|  |  |  |    * a video meeting because then the mail containing the video credentials will be | 
					
						
							|  |  |  |    * more important than the mails created for these bare calendar events. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param event | 
					
						
							|  |  |  |    * @param booking | 
					
						
							|  |  |  |    * @param noMail | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   private updateAllCalendarEvents( | 
					
						
							|  |  |  |     event: CalendarEvent, | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |     booking: PartialBooking | null, | 
					
						
							|  |  |  |     noMail: boolean | null | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   ): Promise<Array<EventResult>> { | 
					
						
							|  |  |  |     return async.mapLimit(this.calendarCredentials, 5, async (credential) => { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       const bookingRefUid = booking | 
					
						
							|  |  |  |         ? booking.references.filter((ref) => ref.type === credential.type)[0]?.uid | 
					
						
							|  |  |  |         : null; | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |       return updateEvent(credential, event, noMail, bookingRefUid); | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-20 18:07:59 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Updates a single video event. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param event | 
					
						
							|  |  |  |    * @param booking | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) { | 
					
						
							|  |  |  |     const credential = this.getVideoCredential(event); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |     if (credential) { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       const bookingRef = booking ? booking.references.filter((ref) => ref.type === credential.type)[0] : null; | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |       const bookingRefUid = bookingRef ? bookingRef.uid : null; | 
					
						
							|  |  |  |       return updateMeeting(credential, event, bookingRefUid).then((returnVal: EventResult) => { | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |         // Some video integrations, such as Zoom, don't return any data about the booking when updating it.
 | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |         if (returnVal.videoCallData === undefined) { | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |           returnVal.videoCallData = EventManager.bookingReferenceToVideoCallData(bookingRef); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return returnVal; | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |     } else { | 
					
						
							|  |  |  |       return Promise.reject("No suitable credentials given for the requested integration name."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							| 
									
										
										
										
											2021-08-01 21:29:15 +00:00
										 |  |  |    * Returns true if the given location describes a dedicated integration that | 
					
						
							|  |  |  |    * delivers meeting credentials. Zoom, for example, is dedicated, because it | 
					
						
							|  |  |  |    * needs to be called independently from any calendar APIs to receive meeting | 
					
						
							|  |  |  |    * credentials. Google Meetings, in contrast, are not dedicated, because they | 
					
						
							|  |  |  |    * are created while scheduling a regular calendar event by simply adding some | 
					
						
							|  |  |  |    * attributes to the payload JSON. | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |    * | 
					
						
							|  |  |  |    * @param location | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-08-01 21:29:15 +00:00
										 |  |  |   private static isDedicatedIntegration(location: string): boolean { | 
					
						
							|  |  |  |     // Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't.
 | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |     return location === "integrations:zoom" || location === "integrations:daily"; | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-07-25 12:19:49 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Helper function for processLocation: Returns the conferenceData object to be merged | 
					
						
							|  |  |  |    * with the CalendarEvent. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param locationObj | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private static getLocationRequestFromIntegration(locationObj: GetLocationRequestFromIntegrationRequest) { | 
					
						
							|  |  |  |     const location = locationObj.location; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-07 16:12:39 +00:00
										 |  |  |     if ( | 
					
						
							|  |  |  |       location === LocationType.GoogleMeet.valueOf() || | 
					
						
							|  |  |  |       location === LocationType.Zoom.valueOf() || | 
					
						
							|  |  |  |       location === LocationType.Daily.valueOf() | 
					
						
							|  |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2021-07-25 12:19:49 +00:00
										 |  |  |       const requestId = uuidv5(location, uuidv5.URL); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         conferenceData: { | 
					
						
							|  |  |  |           createRequest: { | 
					
						
							|  |  |  |             requestId: requestId, | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         location, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Takes a CalendarEvent and adds a ConferenceData object to the event | 
					
						
							|  |  |  |    * if the event has an integration-related location. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param event | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |   private static processLocation<T extends CalendarEvent>(event: T): T { | 
					
						
							| 
									
										
										
										
											2021-07-25 12:19:49 +00:00
										 |  |  |     // If location is set to an integration location
 | 
					
						
							|  |  |  |     // Build proper transforms for evt object
 | 
					
						
							|  |  |  |     // Extend evt object with those transformations
 | 
					
						
							|  |  |  |     if (event.location?.includes("integration")) { | 
					
						
							|  |  |  |       const maybeLocationRequestObject = EventManager.getLocationRequestFromIntegration({ | 
					
						
							|  |  |  |         location: event.location, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       event = merge(event, maybeLocationRequestObject); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return event; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Accepts a PartialReference object and, if all data is complete, | 
					
						
							|  |  |  |    * returns a VideoCallData object containing the meeting information. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param reference | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |   private static bookingReferenceToVideoCallData( | 
					
						
							|  |  |  |     reference: PartialReference | null | 
					
						
							|  |  |  |   ): VideoCallData | undefined { | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |     let isComplete = true; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |     if (!reference) { | 
					
						
							|  |  |  |       throw new Error("missing reference"); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |     switch (reference.type) { | 
					
						
							|  |  |  |       case "zoom_video": | 
					
						
							|  |  |  |         // Zoom meetings in our system should always have an ID, a password and a join URL. In the
 | 
					
						
							|  |  |  |         // future, it might happen that we consider making passwords for Zoom meetings optional.
 | 
					
						
							|  |  |  |         // Then, this part below (where the password existence is checked) needs to be adapted.
 | 
					
						
							|  |  |  |         isComplete = | 
					
						
							| 
									
										
										
										
											2021-11-18 11:13:38 +00:00
										 |  |  |           reference.meetingId !== undefined && | 
					
						
							|  |  |  |           reference.meetingPassword !== undefined && | 
					
						
							|  |  |  |           reference.meetingUrl !== undefined; | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |         break; | 
					
						
							|  |  |  |       default: | 
					
						
							|  |  |  |         isComplete = true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (isComplete) { | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         type: reference.type, | 
					
						
							|  |  |  |         // The null coalescing operator should actually never be used here, because we checked if it's defined beforehand.
 | 
					
						
							|  |  |  |         id: reference.meetingId ?? "", | 
					
						
							|  |  |  |         password: reference.meetingPassword ?? "", | 
					
						
							|  |  |  |         url: reference.meetingUrl ?? "", | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       return undefined; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Conditionally sends an email to the attendee. | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * @param type | 
					
						
							|  |  |  |    * @param results | 
					
						
							|  |  |  |    * @param event | 
					
						
							|  |  |  |    * @private | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private static async sendAttendeeMail( | 
					
						
							|  |  |  |     type: "new" | "reschedule", | 
					
						
							|  |  |  |     results: Array<EventResult>, | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |     event: CalendarEvent | 
					
						
							| 
									
										
										
										
											2021-09-22 22:43:10 +00:00
										 |  |  |   ) { | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  |     if ( | 
					
						
							|  |  |  |       !results.length || | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       !results.some((eRes) => (eRes.createdEvent || eRes.updatedEvent)?.disableConfirmationEmail) | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  |       const metadata: AdditionInformation = {}; | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  |       if (results.length) { | 
					
						
							|  |  |  |         // TODO: Handle created event metadata more elegantly
 | 
					
						
							|  |  |  |         metadata.hangoutLink = results[0].createdEvent?.hangoutLink; | 
					
						
							|  |  |  |         metadata.conferenceData = results[0].createdEvent?.conferenceData; | 
					
						
							|  |  |  |         metadata.entryPoints = results[0].createdEvent?.entryPoints; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       event.additionInformation = metadata; | 
					
						
							| 
									
										
										
										
											2021-10-25 13:05:21 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  |       let attendeeMail; | 
					
						
							|  |  |  |       switch (type) { | 
					
						
							|  |  |  |         case "reschedule": | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |           attendeeMail = new EventAttendeeRescheduledMail(event); | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  |           break; | 
					
						
							|  |  |  |         case "new": | 
					
						
							| 
									
										
										
										
											2021-11-09 16:27:33 +00:00
										 |  |  |           attendeeMail = new EventAttendeeMail(event); | 
					
						
							| 
									
										
										
										
											2021-09-08 11:21:19 +00:00
										 |  |  |           break; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         await attendeeMail.sendEmail(); | 
					
						
							|  |  |  |       } catch (e) { | 
					
						
							|  |  |  |         console.error("attendeeMail.sendEmail failed", e); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-07-15 01:19:30 +00:00
										 |  |  | } |