From daecc1e0e40a3086f3b693c12d65ebfe7c0b70a0 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 15 Jul 2021 03:19:30 +0200 Subject: [PATCH] Created EventManager in order to unify event CRUD logic --- lib/calendarClient.ts | 38 ++++- lib/events/EventManager.ts | 124 +++++++++++++++++ lib/videoClient.ts | 274 ++++++++++++++++++++++--------------- pages/api/book/[user].ts | 91 ++++-------- 4 files changed, 347 insertions(+), 180 deletions(-) create mode 100644 lib/events/EventManager.ts diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 3891feab..41720971 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -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 => { +const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise => { 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 => { +): Promise => { 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, }; }; diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts new file mode 100644 index 00000000..69e184df --- /dev/null +++ b/lib/events/EventManager.ts @@ -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; +} + +export interface PartialReference { + id: number; + type: string; + uid: string; +} + +export default class EventManager { + calendarCredentials: Array; + videoCredentials: Array; + + constructor(credentials: Array) { + this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar")); + this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video")); + } + + public async create(event: CalendarEvent): Promise> { + const results: Array = []; + // 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> { + const results: Array = []; + // 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> { + 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 { + 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> { + 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:"); + } +} diff --git a/lib/videoClient.ts b/lib/videoClient.ts index 0e171ac6..ec1b48ce 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -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; - updateMeeting(uid: String, event: CalendarEvent); + updateMeeting(uid: string, event: CalendarEvent); - deleteMeeting(uid: String); + deleteMeeting(uid: string); getAvailability(dateFrom, dateTo): Promise; } 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 => { +const createMeeting = async (credential, calEvent: CalendarEvent): Promise => { 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 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 => { +const updateMeeting = async ( + credential, + uidToUpdate: string, + calEvent: CalendarEvent +): Promise => { 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 => { +const deleteMeeting = (credential, uid: string): Promise => { if (credential) { return videoIntegrations([credential])[0].deleteMeeting(uid); } @@ -252,4 +306,4 @@ const deleteMeeting = (credential, uid: String): Promise => { return Promise.resolve({}); }; -export {getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting}; +export { getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting }; diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 89eb8ca8..44c1c505 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -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 = []; 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 = 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) {