Created EventManager in order to unify event CRUD logic
This commit is contained in:
		
							parent
							
								
									36b258f4b7
								
							
						
					
					
						commit
						daecc1e0e4
					
				
					 4 changed files with 347 additions and 180 deletions
				
			
		|  | @ -5,6 +5,10 @@ import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail" | |||
| import prisma from "./prisma"; | ||||
| import { Credential } from "@prisma/client"; | ||||
| import CalEventParser from "./CalEventParser"; | ||||
| import { EventResult } from "@lib/events/EventManager"; | ||||
| import logger from "@lib/logger"; | ||||
| 
 | ||||
| const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] }); | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-var-requires
 | ||||
| const { google } = require("googleapis"); | ||||
|  | @ -494,9 +498,7 @@ const calendars = (withCredentials): CalendarApiAdapter[] => | |||
|     .filter(Boolean); | ||||
| 
 | ||||
| const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => | ||||
|   Promise.all( | ||||
|     calendars(withCredentials).map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars)) | ||||
|   ).then((results) => { | ||||
|   Promise.all(calendars(withCredentials).map((c) => c.getAvailability(selectedCalendars))).then((results) => { | ||||
|     return results.reduce((acc, availability) => acc.concat(availability), []); | ||||
|   }); | ||||
| 
 | ||||
|  | @ -505,12 +507,21 @@ const listCalendars = (withCredentials) => | |||
|     results.reduce((acc, calendars) => acc.concat(calendars), []) | ||||
|   ); | ||||
| 
 | ||||
| const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise<unknown> => { | ||||
| const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => { | ||||
|   const parser: CalEventParser = new CalEventParser(calEvent); | ||||
|   const uid: string = parser.getUid(); | ||||
|   const richEvent: CalendarEvent = parser.asRichEvent(); | ||||
| 
 | ||||
|   const creationResult = credential ? await calendars([credential])[0].createEvent(richEvent) : null; | ||||
|   let success = true; | ||||
| 
 | ||||
|   const creationResult = credential | ||||
|     ? await calendars([credential])[0] | ||||
|         .createEvent(richEvent) | ||||
|         .catch((e) => { | ||||
|           log.error("createEvent failed", e, calEvent); | ||||
|           success = false; | ||||
|         }) | ||||
|     : null; | ||||
| 
 | ||||
|   const maybeHangoutLink = creationResult?.hangoutLink; | ||||
|   const maybeEntryPoints = creationResult?.entryPoints; | ||||
|  | @ -543,8 +554,11 @@ const createEvent = async (credential: Credential, calEvent: CalendarEvent): Pro | |||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     type: credential.type, | ||||
|     success, | ||||
|     uid, | ||||
|     createdEvent: creationResult, | ||||
|     originalEvent: calEvent, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  | @ -552,13 +566,20 @@ const updateEvent = async ( | |||
|   credential: Credential, | ||||
|   uidToUpdate: string, | ||||
|   calEvent: CalendarEvent | ||||
| ): Promise<unknown> => { | ||||
| ): Promise<EventResult> => { | ||||
|   const parser: CalEventParser = new CalEventParser(calEvent); | ||||
|   const newUid: string = parser.getUid(); | ||||
|   const richEvent: CalendarEvent = parser.asRichEvent(); | ||||
| 
 | ||||
|   let success = true; | ||||
| 
 | ||||
|   const updateResult = credential | ||||
|     ? await calendars([credential])[0].updateEvent(uidToUpdate, richEvent) | ||||
|     ? await calendars([credential])[0] | ||||
|         .updateEvent(uidToUpdate, richEvent) | ||||
|         .catch((e) => { | ||||
|           log.error("updateEvent failed", e, calEvent); | ||||
|           success = false; | ||||
|         }) | ||||
|     : null; | ||||
| 
 | ||||
|   const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); | ||||
|  | @ -578,8 +599,11 @@ const updateEvent = async ( | |||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     type: credential.type, | ||||
|     success, | ||||
|     uid: newUid, | ||||
|     updatedEvent: updateResult, | ||||
|     originalEvent: calEvent, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										124
									
								
								lib/events/EventManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								lib/events/EventManager.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,124 @@ | |||
| import { CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient"; | ||||
| import { Credential } from "@prisma/client"; | ||||
| import async from "async"; | ||||
| import { createMeeting, updateMeeting } from "@lib/videoClient"; | ||||
| 
 | ||||
| export interface EventResult { | ||||
|   type: string; | ||||
|   success: boolean; | ||||
|   uid: string; | ||||
|   createdEvent?: unknown; | ||||
|   updatedEvent?: unknown; | ||||
|   originalEvent: CalendarEvent; | ||||
| } | ||||
| 
 | ||||
| export interface PartialBooking { | ||||
|   id: number; | ||||
|   references: Array<PartialReference>; | ||||
| } | ||||
| 
 | ||||
| export interface PartialReference { | ||||
|   id: number; | ||||
|   type: string; | ||||
|   uid: string; | ||||
| } | ||||
| 
 | ||||
| export default class EventManager { | ||||
|   calendarCredentials: Array<Credential>; | ||||
|   videoCredentials: Array<Credential>; | ||||
| 
 | ||||
|   constructor(credentials: Array<Credential>) { | ||||
|     this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar")); | ||||
|     this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video")); | ||||
|   } | ||||
| 
 | ||||
|   public async create(event: CalendarEvent): Promise<Array<EventResult>> { | ||||
|     const results: Array<EventResult> = []; | ||||
|     // First, create all calendar events.
 | ||||
|     results.concat(await this.createAllCalendarEvents(event)); | ||||
| 
 | ||||
|     // If and only if event type is a video meeting, create a video meeting as well.
 | ||||
|     if (EventManager.isIntegration(event.location)) { | ||||
|       results.push(await this.createVideoEvent(event)); | ||||
|     } | ||||
| 
 | ||||
|     return results; | ||||
|   } | ||||
| 
 | ||||
|   public async update(event: CalendarEvent, booking: PartialBooking): Promise<Array<EventResult>> { | ||||
|     const results: Array<EventResult> = []; | ||||
|     // First, update all calendar events.
 | ||||
|     results.concat(await this.updateAllCalendarEvents(event, booking)); | ||||
| 
 | ||||
|     // If and only if event type is a video meeting, update the video meeting as well.
 | ||||
|     if (EventManager.isIntegration(event.location)) { | ||||
|       results.push(await this.updateVideoEvent(event, booking)); | ||||
|     } | ||||
| 
 | ||||
|     return results; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Creates event entries for all calendar integrations given in the credentials. | ||||
|    * | ||||
|    * @param event | ||||
|    * @private | ||||
|    */ | ||||
|   private createAllCalendarEvents(event: CalendarEvent): Promise<Array<EventResult>> { | ||||
|     return async.mapLimit(this.calendarCredentials, 5, async (credential: Credential) => { | ||||
|       return createEvent(credential, event); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private getVideoCredential(event: CalendarEvent): Credential | undefined { | ||||
|     const integrationName = event.location.replace("integrations:", ""); | ||||
|     return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Creates a video event entry for the selected integration location. | ||||
|    * | ||||
|    * @param event | ||||
|    * @private | ||||
|    */ | ||||
|   private createVideoEvent(event: CalendarEvent): Promise<EventResult> { | ||||
|     const credential = this.getVideoCredential(event); | ||||
| 
 | ||||
|     if (credential) { | ||||
|       return createMeeting(credential, event); | ||||
|     } else { | ||||
|       return Promise.reject("No suitable credentials given for the requested integration name."); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private updateAllCalendarEvents( | ||||
|     event: CalendarEvent, | ||||
|     booking: PartialBooking | ||||
|   ): Promise<Array<EventResult>> { | ||||
|     return async.mapLimit(this.calendarCredentials, 5, async (credential) => { | ||||
|       const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid; | ||||
|       return updateEvent(credential, bookingRefUid, event); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) { | ||||
|     const credential = this.getVideoCredential(event); | ||||
| 
 | ||||
|     if (credential) { | ||||
|       const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid; | ||||
|       return updateMeeting(credential, bookingRefUid, event); | ||||
|     } else { | ||||
|       return Promise.reject("No suitable credentials given for the requested integration name."); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns true if the given location describes an integration that delivers meeting credentials. | ||||
|    * | ||||
|    * @param location | ||||
|    * @private | ||||
|    */ | ||||
|   private static isIntegration(location: string): boolean { | ||||
|     return location.includes("integrations:"); | ||||
|   } | ||||
| } | ||||
|  | @ -1,11 +1,15 @@ | |||
| import prisma from "./prisma"; | ||||
| import {CalendarEvent} from "./calendarClient"; | ||||
| import { CalendarEvent } from "./calendarClient"; | ||||
| import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail"; | ||||
| import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail"; | ||||
| import {v5 as uuidv5} from 'uuid'; | ||||
| import short from 'short-uuid'; | ||||
| import { v5 as uuidv5 } from "uuid"; | ||||
| import short from "short-uuid"; | ||||
| import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; | ||||
| import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; | ||||
| import { EventResult } from "@lib/events/EventManager"; | ||||
| import logger from "@lib/logger"; | ||||
| 
 | ||||
| const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] }); | ||||
| 
 | ||||
| const translator = short(); | ||||
| 
 | ||||
|  | @ -33,63 +37,67 @@ function handleErrorsRaw(response) { | |||
| } | ||||
| 
 | ||||
| const zoomAuth = (credential) => { | ||||
|   const isExpired = (expiryDate) => expiryDate < +new Date(); | ||||
|   const authHeader = | ||||
|     "Basic " + | ||||
|     Buffer.from(process.env.ZOOM_CLIENT_ID + ":" + process.env.ZOOM_CLIENT_SECRET).toString("base64"); | ||||
| 
 | ||||
|   const isExpired = (expiryDate) => expiryDate < +(new Date()); | ||||
|   const authHeader = 'Basic ' + Buffer.from(process.env.ZOOM_CLIENT_ID + ':' + process.env.ZOOM_CLIENT_SECRET).toString('base64'); | ||||
| 
 | ||||
|   const refreshAccessToken = (refreshToken) => fetch('https://zoom.us/oauth/token', { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|       'Authorization': authHeader, | ||||
|       'Content-Type': 'application/x-www-form-urlencoded' | ||||
|     }, | ||||
|     body: new URLSearchParams({ | ||||
|       'refresh_token': refreshToken, | ||||
|       'grant_type': 'refresh_token', | ||||
|   const refreshAccessToken = (refreshToken) => | ||||
|     fetch("https://zoom.us/oauth/token", { | ||||
|       method: "POST", | ||||
|       headers: { | ||||
|         Authorization: authHeader, | ||||
|         "Content-Type": "application/x-www-form-urlencoded", | ||||
|       }, | ||||
|       body: new URLSearchParams({ | ||||
|         refresh_token: refreshToken, | ||||
|         grant_type: "refresh_token", | ||||
|       }), | ||||
|     }) | ||||
|   }) | ||||
|     .then(handleErrorsJson) | ||||
|     .then(async (responseBody) => { | ||||
|       // Store new tokens in database.
 | ||||
|       await prisma.credential.update({ | ||||
|         where: { | ||||
|           id: credential.id | ||||
|         }, | ||||
|         data: { | ||||
|           key: responseBody | ||||
|         } | ||||
|       .then(handleErrorsJson) | ||||
|       .then(async (responseBody) => { | ||||
|         // Store new tokens in database.
 | ||||
|         await prisma.credential.update({ | ||||
|           where: { | ||||
|             id: credential.id, | ||||
|           }, | ||||
|           data: { | ||||
|             key: responseBody, | ||||
|           }, | ||||
|         }); | ||||
|         credential.key.access_token = responseBody.access_token; | ||||
|         credential.key.expires_in = Math.round(+new Date() / 1000 + responseBody.expires_in); | ||||
|         return credential.key.access_token; | ||||
|       }); | ||||
|       credential.key.access_token = responseBody.access_token; | ||||
|       credential.key.expires_in = Math.round((+(new Date()) / 1000) + responseBody.expires_in); | ||||
|       return credential.key.access_token; | ||||
|     }) | ||||
| 
 | ||||
|   return { | ||||
|     getToken: () => !isExpired(credential.key.expires_in) ? Promise.resolve(credential.key.access_token) : refreshAccessToken(credential.key.refresh_token) | ||||
|     getToken: () => | ||||
|       !isExpired(credential.key.expires_in) | ||||
|         ? Promise.resolve(credential.key.access_token) | ||||
|         : refreshAccessToken(credential.key.refresh_token), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| interface VideoApiAdapter { | ||||
|   createMeeting(event: CalendarEvent): Promise<any>; | ||||
| 
 | ||||
|   updateMeeting(uid: String, event: CalendarEvent); | ||||
|   updateMeeting(uid: string, event: CalendarEvent); | ||||
| 
 | ||||
|   deleteMeeting(uid: String); | ||||
|   deleteMeeting(uid: string); | ||||
| 
 | ||||
|   getAvailability(dateFrom, dateTo): Promise<any>; | ||||
| } | ||||
| 
 | ||||
| const ZoomVideo = (credential): VideoApiAdapter => { | ||||
| 
 | ||||
|   const auth = zoomAuth(credential); | ||||
| 
 | ||||
|   const translateEvent = (event: CalendarEvent) => { | ||||
|     // Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
 | ||||
|     return { | ||||
|       topic: event.title, | ||||
|       type: 2,    // Means that this is a scheduled meeting
 | ||||
|       type: 2, // Means that this is a scheduled meeting
 | ||||
|       start_time: event.startTime, | ||||
|       duration: ((new Date(event.endTime)).getTime() - (new Date(event.startTime)).getTime()) / 60000, | ||||
|       duration: (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / 60000, | ||||
|       //schedule_for: "string",   TODO: Used when scheduling the meeting for someone else (needed?)
 | ||||
|       timezone: event.attendees[0].timeZone, | ||||
|       //password: "string",       TODO: Should we use a password? Maybe generate a random one?
 | ||||
|  | @ -97,8 +105,8 @@ const ZoomVideo = (credential): VideoApiAdapter => { | |||
|       settings: { | ||||
|         host_video: true, | ||||
|         participant_video: true, | ||||
|         cn_meeting: false,  // TODO: true if host meeting in China
 | ||||
|         in_meeting: false,  // TODO: true if host meeting in India
 | ||||
|         cn_meeting: false, // TODO: true if host meeting in China
 | ||||
|         in_meeting: false, // TODO: true if host meeting in India
 | ||||
|         join_before_host: true, | ||||
|         mute_upon_entry: false, | ||||
|         watermark: false, | ||||
|  | @ -107,82 +115,107 @@ const ZoomVideo = (credential): VideoApiAdapter => { | |||
|         audio: "both", | ||||
|         auto_recording: "none", | ||||
|         enforce_login: false, | ||||
|         registrants_email_notification: true | ||||
|       } | ||||
|         registrants_email_notification: true, | ||||
|       }, | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   return { | ||||
|     getAvailability: (dateFrom, dateTo) => { | ||||
|       return auth.getToken().then( | ||||
|         // TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled.
 | ||||
|         (accessToken) => fetch('https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=300', { | ||||
|           method: 'get', | ||||
|           headers: { | ||||
|             'Authorization': 'Bearer ' + accessToken | ||||
|           } | ||||
|         }) | ||||
|           .then(handleErrorsJson) | ||||
|           .then(responseBody => { | ||||
|             return responseBody.meetings.map((meeting) => ({ | ||||
|               start: meeting.start_time, | ||||
|               end: (new Date((new Date(meeting.start_time)).getTime() + meeting.duration * 60000)).toISOString() | ||||
|             })) | ||||
|           }) | ||||
|       ).catch((err) => { | ||||
|         console.log(err); | ||||
|       }); | ||||
|     getAvailability: () => { | ||||
|       return auth | ||||
|         .getToken() | ||||
|         .then( | ||||
|           // TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled.
 | ||||
|           (accessToken) => | ||||
|             fetch("https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=300", { | ||||
|               method: "get", | ||||
|               headers: { | ||||
|                 Authorization: "Bearer " + accessToken, | ||||
|               }, | ||||
|             }) | ||||
|               .then(handleErrorsJson) | ||||
|               .then((responseBody) => { | ||||
|                 return responseBody.meetings.map((meeting) => ({ | ||||
|                   start: meeting.start_time, | ||||
|                   end: new Date( | ||||
|                     new Date(meeting.start_time).getTime() + meeting.duration * 60000 | ||||
|                   ).toISOString(), | ||||
|                 })); | ||||
|               }) | ||||
|         ) | ||||
|         .catch((err) => { | ||||
|           console.log(err); | ||||
|         }); | ||||
|     }, | ||||
|     createMeeting: (event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/users/me/meetings', { | ||||
|       method: 'POST', | ||||
|       headers: { | ||||
|         'Authorization': 'Bearer ' + accessToken, | ||||
|         'Content-Type': 'application/json', | ||||
|       }, | ||||
|       body: JSON.stringify(translateEvent(event)) | ||||
|     }).then(handleErrorsJson)), | ||||
|     deleteMeeting: (uid: String) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, { | ||||
|       method: 'DELETE', | ||||
|       headers: { | ||||
|         'Authorization': 'Bearer ' + accessToken | ||||
|       } | ||||
|     }).then(handleErrorsRaw)), | ||||
|     updateMeeting: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, { | ||||
|       method: 'PATCH', | ||||
|       headers: { | ||||
|         'Authorization': 'Bearer ' + accessToken, | ||||
|         'Content-Type': 'application/json' | ||||
|       }, | ||||
|       body: JSON.stringify(translateEvent(event)) | ||||
|     }).then(handleErrorsRaw)), | ||||
|   } | ||||
|     createMeeting: (event: CalendarEvent) => | ||||
|       auth.getToken().then((accessToken) => | ||||
|         fetch("https://api.zoom.us/v2/users/me/meetings", { | ||||
|           method: "POST", | ||||
|           headers: { | ||||
|             Authorization: "Bearer " + accessToken, | ||||
|             "Content-Type": "application/json", | ||||
|           }, | ||||
|           body: JSON.stringify(translateEvent(event)), | ||||
|         }).then(handleErrorsJson) | ||||
|       ), | ||||
|     deleteMeeting: (uid: string) => | ||||
|       auth.getToken().then((accessToken) => | ||||
|         fetch("https://api.zoom.us/v2/meetings/" + uid, { | ||||
|           method: "DELETE", | ||||
|           headers: { | ||||
|             Authorization: "Bearer " + accessToken, | ||||
|           }, | ||||
|         }).then(handleErrorsRaw) | ||||
|       ), | ||||
|     updateMeeting: (uid: string, event: CalendarEvent) => | ||||
|       auth.getToken().then((accessToken) => | ||||
|         fetch("https://api.zoom.us/v2/meetings/" + uid, { | ||||
|           method: "PATCH", | ||||
|           headers: { | ||||
|             Authorization: "Bearer " + accessToken, | ||||
|             "Content-Type": "application/json", | ||||
|           }, | ||||
|           body: JSON.stringify(translateEvent(event)), | ||||
|         }).then(handleErrorsRaw) | ||||
|       ), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| // factory
 | ||||
| const videoIntegrations = (withCredentials): VideoApiAdapter[] => withCredentials.map((cred) => { | ||||
|   switch (cred.type) { | ||||
|     case 'zoom_video': | ||||
|       return ZoomVideo(cred); | ||||
|     default: | ||||
|       return; // unknown credential, could be legacy? In any case, ignore
 | ||||
|   } | ||||
| }).filter(Boolean); | ||||
| const videoIntegrations = (withCredentials): VideoApiAdapter[] => | ||||
|   withCredentials | ||||
|     .map((cred) => { | ||||
|       switch (cred.type) { | ||||
|         case "zoom_video": | ||||
|           return ZoomVideo(cred); | ||||
|         default: | ||||
|           return; // unknown credential, could be legacy? In any case, ignore
 | ||||
|       } | ||||
|     }) | ||||
|     .filter(Boolean); | ||||
| 
 | ||||
| const getBusyVideoTimes = (withCredentials) => | ||||
|   Promise.all(videoIntegrations(withCredentials).map((c) => c.getAvailability())).then((results) => | ||||
|     results.reduce((acc, availability) => acc.concat(availability), []) | ||||
|   ); | ||||
| 
 | ||||
| const getBusyVideoTimes = (withCredentials, dateFrom, dateTo) => Promise.all( | ||||
|   videoIntegrations(withCredentials).map(c => c.getAvailability(dateFrom, dateTo)) | ||||
| ).then( | ||||
|   (results) => results.reduce((acc, availability) => acc.concat(availability), []) | ||||
| ); | ||||
| 
 | ||||
| const createMeeting = async (credential, calEvent: CalendarEvent): Promise<any> => { | ||||
| const createMeeting = async (credential, calEvent: CalendarEvent): Promise<EventResult> => { | ||||
|   const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); | ||||
| 
 | ||||
|   if (!credential) { | ||||
|     throw new Error("Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."); | ||||
|     throw new Error( | ||||
|       "Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set." | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   const creationResult = await videoIntegrations([credential])[0].createMeeting(calEvent); | ||||
|   let success = true; | ||||
| 
 | ||||
|   const creationResult = await videoIntegrations([credential])[0] | ||||
|     .createMeeting(calEvent) | ||||
|     .catch((e) => { | ||||
|       log.error("createMeeting failed", e, calEvent); | ||||
|       success = false; | ||||
|     }); | ||||
| 
 | ||||
|   const videoCallData: VideoCallData = { | ||||
|     type: credential.type, | ||||
|  | @ -196,55 +229,76 @@ const createMeeting = async (credential, calEvent: CalendarEvent): Promise<any> | |||
|   try { | ||||
|     await organizerMail.sendEmail(); | ||||
|   } catch (e) { | ||||
|     console.error("organizerMail.sendEmail failed", e) | ||||
|     console.error("organizerMail.sendEmail failed", e); | ||||
|   } | ||||
| 
 | ||||
|   if (!creationResult || !creationResult.disableConfirmationEmail) { | ||||
|     try { | ||||
|       await attendeeMail.sendEmail(); | ||||
|     } catch (e) { | ||||
|       console.error("attendeeMail.sendEmail failed", e) | ||||
|       console.error("attendeeMail.sendEmail failed", e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     type: credential.type, | ||||
|     success, | ||||
|     uid, | ||||
|     createdEvent: creationResult | ||||
|     createdEvent: creationResult, | ||||
|     originalEvent: calEvent, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const updateMeeting = async (credential, uidToUpdate: String, calEvent: CalendarEvent): Promise<any> => { | ||||
| const updateMeeting = async ( | ||||
|   credential, | ||||
|   uidToUpdate: string, | ||||
|   calEvent: CalendarEvent | ||||
| ): Promise<EventResult> => { | ||||
|   const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); | ||||
| 
 | ||||
|   if (!credential) { | ||||
|     throw new Error("Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."); | ||||
|     throw new Error( | ||||
|       "Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set." | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   const updateResult = credential ? await videoIntegrations([credential])[0].updateMeeting(uidToUpdate, calEvent) : null; | ||||
|   let success = true; | ||||
| 
 | ||||
|   const updateResult = credential | ||||
|     ? await videoIntegrations([credential])[0] | ||||
|         .updateMeeting(uidToUpdate, calEvent) | ||||
|         .catch((e) => { | ||||
|           log.error("updateMeeting failed", e, calEvent); | ||||
|           success = false; | ||||
|         }) | ||||
|     : null; | ||||
| 
 | ||||
|   const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); | ||||
|   const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); | ||||
|   try { | ||||
|     await organizerMail.sendEmail(); | ||||
|   } catch (e) { | ||||
|     console.error("organizerMail.sendEmail failed", e) | ||||
|     console.error("organizerMail.sendEmail failed", e); | ||||
|   } | ||||
| 
 | ||||
|   if (!updateResult || !updateResult.disableConfirmationEmail) { | ||||
|     try { | ||||
|       await attendeeMail.sendEmail(); | ||||
|     } catch (e) { | ||||
|       console.error("attendeeMail.sendEmail failed", e) | ||||
|       console.error("attendeeMail.sendEmail failed", e); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     type: credential.type, | ||||
|     success, | ||||
|     uid: newUid, | ||||
|     updatedEvent: updateResult | ||||
|     updatedEvent: updateResult, | ||||
|     originalEvent: calEvent, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const deleteMeeting = (credential, uid: String): Promise<any> => { | ||||
| const deleteMeeting = (credential, uid: string): Promise<any> => { | ||||
|   if (credential) { | ||||
|     return videoIntegrations([credential])[0].deleteMeeting(uid); | ||||
|   } | ||||
|  | @ -252,4 +306,4 @@ const deleteMeeting = (credential, uid: String): Promise<any> => { | |||
|   return Promise.resolve({}); | ||||
| }; | ||||
| 
 | ||||
| export {getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting}; | ||||
| export { getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting }; | ||||
|  |  | |||
|  | @ -1,16 +1,17 @@ | |||
| import type { NextApiRequest, NextApiResponse } from "next"; | ||||
| import prisma from "../../../lib/prisma"; | ||||
| import { CalendarEvent, createEvent, getBusyCalendarTimes, updateEvent } from "../../../lib/calendarClient"; | ||||
| import async from "async"; | ||||
| import { CalendarEvent, getBusyCalendarTimes } from "@lib/calendarClient"; | ||||
| import { v5 as uuidv5 } from "uuid"; | ||||
| import short from "short-uuid"; | ||||
| import { createMeeting, getBusyVideoTimes, updateMeeting } from "../../../lib/videoClient"; | ||||
| import { getBusyVideoTimes } from "@lib/videoClient"; | ||||
| import EventAttendeeMail from "../../../lib/emails/EventAttendeeMail"; | ||||
| import { getEventName } from "../../../lib/event"; | ||||
| import { LocationType } from "../../../lib/location"; | ||||
| import { getEventName } from "@lib/event"; | ||||
| import { LocationType } from "@lib/location"; | ||||
| import merge from "lodash.merge"; | ||||
| import dayjs from "dayjs"; | ||||
| import logger from "../../../lib/logger"; | ||||
| import EventManager, { EventResult } from "@lib/events/EventManager"; | ||||
| import { User } from "@prisma/client"; | ||||
| 
 | ||||
| const translator = short(); | ||||
| const log = logger.getChildLogger({ prefix: ["[api] book:user"] }); | ||||
|  | @ -63,6 +64,18 @@ const getLocationRequestFromIntegration = ({ location }: GetLocationRequestFromI | |||
|           requestId: requestId, | ||||
|         }, | ||||
|       }, | ||||
|       location, | ||||
|     }; | ||||
|   } else if (location === LocationType.Zoom.valueOf()) { | ||||
|     const requestId = uuidv5(location, uuidv5.URL); | ||||
| 
 | ||||
|     return { | ||||
|       conferenceData: { | ||||
|         createRequest: { | ||||
|           requestId: requestId, | ||||
|         }, | ||||
|       }, | ||||
|       location, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|  | @ -88,7 +101,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|       return res.status(400).json(error); | ||||
|     } | ||||
| 
 | ||||
|     let currentUser = await prisma.user.findFirst({ | ||||
|     let currentUser: User = await prisma.user.findFirst({ | ||||
|       where: { | ||||
|         username: user, | ||||
|       }, | ||||
|  | @ -107,10 +120,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     // Split credentials up into calendar credentials and video credentials
 | ||||
|     let calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")); | ||||
|     let videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video")); | ||||
| 
 | ||||
|     const hasCalendarIntegrations = | ||||
|       currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0; | ||||
|     const hasVideoIntegrations = | ||||
|  | @ -152,9 +161,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|         name: true, | ||||
|       }, | ||||
|     }); | ||||
|     calendarCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")); | ||||
|     videoCredentials = currentUser.credentials.filter((cred) => cred.type.endsWith("_video")); | ||||
| 
 | ||||
|     // Initialize EventManager with credentials
 | ||||
|     const eventManager = new EventManager(currentUser.credentials); | ||||
|     const rescheduleUid = req.body.rescheduleUid; | ||||
| 
 | ||||
|     const selectedEventType = await prisma.eventType.findFirst({ | ||||
|  | @ -228,7 +237,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|       return res.status(400).json(error); | ||||
|     } | ||||
| 
 | ||||
|     let results = []; | ||||
|     let results: Array<EventResult> = []; | ||||
|     let referencesToCreate = []; | ||||
| 
 | ||||
|     if (rescheduleUid) { | ||||
|  | @ -249,30 +258,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       // Use all integrations
 | ||||
|       results = results.concat( | ||||
|         await async.mapLimit(calendarCredentials, 5, async (credential) => { | ||||
|           const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid; | ||||
|           return updateEvent(credential, bookingRefUid, evt) | ||||
|             .then((response) => ({ type: credential.type, success: true, response })) | ||||
|             .catch((e) => { | ||||
|               log.error("updateEvent failed", e, evt); | ||||
|               return { type: credential.type, success: false }; | ||||
|             }); | ||||
|         }) | ||||
|       ); | ||||
| 
 | ||||
|       results = results.concat( | ||||
|         await async.mapLimit(videoCredentials, 5, async (credential) => { | ||||
|           const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid; | ||||
|           return updateMeeting(credential, bookingRefUid, evt) | ||||
|             .then((response) => ({ type: credential.type, success: true, response })) | ||||
|             .catch((e) => { | ||||
|               log.error("updateMeeting failed", e, evt); | ||||
|               return { type: credential.type, success: false }; | ||||
|             }); | ||||
|         }) | ||||
|       ); | ||||
|       // Use EventManager to conditionally use all needed integrations.
 | ||||
|       results = await eventManager.update(evt, booking); | ||||
| 
 | ||||
|       if (results.length > 0 && results.every((res) => !res.success)) { | ||||
|         const error = { | ||||
|  | @ -306,28 +293,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
| 
 | ||||
|       await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]); | ||||
|     } else { | ||||
|       // Schedule event
 | ||||
|       results = results.concat( | ||||
|         await async.mapLimit(calendarCredentials, 5, async (credential) => { | ||||
|           return createEvent(credential, evt) | ||||
|             .then((response) => ({ type: credential.type, success: true, response })) | ||||
|             .catch((e) => { | ||||
|               log.error("createEvent failed", e, evt); | ||||
|               return { type: credential.type, success: false }; | ||||
|             }); | ||||
|         }) | ||||
|       ); | ||||
| 
 | ||||
|       results = results.concat( | ||||
|         await async.mapLimit(videoCredentials, 5, async (credential) => { | ||||
|           return createMeeting(credential, evt) | ||||
|             .then((response) => ({ type: credential.type, success: true, response })) | ||||
|             .catch((e) => { | ||||
|               log.error("createMeeting failed", e, evt); | ||||
|               return { type: credential.type, success: false }; | ||||
|             }); | ||||
|         }) | ||||
|       ); | ||||
|       // Use EventManager to conditionally use all needed integrations.
 | ||||
|       const results: Array<EventResult> = await eventManager.create(evt); | ||||
| 
 | ||||
|       if (results.length > 0 && results.every((res) => !res.success)) { | ||||
|         const error = { | ||||
|  | @ -342,15 +309,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
|       referencesToCreate = results.map((result) => { | ||||
|         return { | ||||
|           type: result.type, | ||||
|           uid: result.response.createdEvent.id.toString(), | ||||
|           uid: result.createdEvent.id.toString(), | ||||
|         }; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     const hashUID = | ||||
|       results.length > 0 | ||||
|         ? results[0].response.uid | ||||
|         : translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL)); | ||||
|       results.length > 0 ? results[0].uid : translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL)); | ||||
|     // TODO Should just be set to the true case as soon as we have a "bare email" integration class.
 | ||||
|     // UID generation should happen in the integration itself, not here.
 | ||||
|     if (results.length === 0) { | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 nicolas
						nicolas