Created EventManager in order to unify event CRUD logic

This commit is contained in:
nicolas 2021-07-15 03:19:30 +02:00
parent 36b258f4b7
commit daecc1e0e4
4 changed files with 347 additions and 180 deletions

View file

@ -5,6 +5,10 @@ import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"
import prisma from "./prisma"; import prisma from "./prisma";
import { Credential } from "@prisma/client"; import { Credential } from "@prisma/client";
import CalEventParser from "./CalEventParser"; 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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const { google } = require("googleapis"); const { google } = require("googleapis");
@ -494,9 +498,7 @@ const calendars = (withCredentials): CalendarApiAdapter[] =>
.filter(Boolean); .filter(Boolean);
const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) =>
Promise.all( Promise.all(calendars(withCredentials).map((c) => c.getAvailability(selectedCalendars))).then((results) => {
calendars(withCredentials).map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars))
).then((results) => {
return results.reduce((acc, availability) => acc.concat(availability), []); return results.reduce((acc, availability) => acc.concat(availability), []);
}); });
@ -505,12 +507,21 @@ const listCalendars = (withCredentials) =>
results.reduce((acc, calendars) => acc.concat(calendars), []) 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 parser: CalEventParser = new CalEventParser(calEvent);
const uid: string = parser.getUid(); const uid: string = parser.getUid();
const richEvent: CalendarEvent = parser.asRichEvent(); 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 maybeHangoutLink = creationResult?.hangoutLink;
const maybeEntryPoints = creationResult?.entryPoints; const maybeEntryPoints = creationResult?.entryPoints;
@ -543,8 +554,11 @@ const createEvent = async (credential: Credential, calEvent: CalendarEvent): Pro
} }
return { return {
type: credential.type,
success,
uid, uid,
createdEvent: creationResult, createdEvent: creationResult,
originalEvent: calEvent,
}; };
}; };
@ -552,13 +566,20 @@ const updateEvent = async (
credential: Credential, credential: Credential,
uidToUpdate: string, uidToUpdate: string,
calEvent: CalendarEvent calEvent: CalendarEvent
): Promise<unknown> => { ): Promise<EventResult> => {
const parser: CalEventParser = new CalEventParser(calEvent); const parser: CalEventParser = new CalEventParser(calEvent);
const newUid: string = parser.getUid(); const newUid: string = parser.getUid();
const richEvent: CalendarEvent = parser.asRichEvent(); const richEvent: CalendarEvent = parser.asRichEvent();
let success = true;
const updateResult = credential 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; : null;
const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid);
@ -578,8 +599,11 @@ const updateEvent = async (
} }
return { return {
type: credential.type,
success,
uid: newUid, uid: newUid,
updatedEvent: updateResult, updatedEvent: updateResult,
originalEvent: calEvent,
}; };
}; };

124
lib/events/EventManager.ts Normal file
View 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:");
}
}

View file

@ -2,10 +2,14 @@ import prisma from "./prisma";
import { CalendarEvent } from "./calendarClient"; import { CalendarEvent } from "./calendarClient";
import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail"; import VideoEventOrganizerMail from "./emails/VideoEventOrganizerMail";
import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail"; import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail";
import {v5 as uuidv5} from 'uuid'; import { v5 as uuidv5 } from "uuid";
import short from 'short-uuid'; import short from "short-uuid";
import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail";
import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; 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(); const translator = short();
@ -33,54 +37,58 @@ function handleErrorsRaw(response) {
} }
const zoomAuth = (credential) => { 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 refreshAccessToken = (refreshToken) =>
const authHeader = 'Basic ' + Buffer.from(process.env.ZOOM_CLIENT_ID + ':' + process.env.ZOOM_CLIENT_SECRET).toString('base64'); fetch("https://zoom.us/oauth/token", {
method: "POST",
const refreshAccessToken = (refreshToken) => fetch('https://zoom.us/oauth/token', {
method: 'POST',
headers: { headers: {
'Authorization': authHeader, Authorization: authHeader,
'Content-Type': 'application/x-www-form-urlencoded' "Content-Type": "application/x-www-form-urlencoded",
}, },
body: new URLSearchParams({ body: new URLSearchParams({
'refresh_token': refreshToken, refresh_token: refreshToken,
'grant_type': 'refresh_token', grant_type: "refresh_token",
}) }),
}) })
.then(handleErrorsJson) .then(handleErrorsJson)
.then(async (responseBody) => { .then(async (responseBody) => {
// Store new tokens in database. // Store new tokens in database.
await prisma.credential.update({ await prisma.credential.update({
where: { where: {
id: credential.id id: credential.id,
}, },
data: { data: {
key: responseBody key: responseBody,
} },
}); });
credential.key.access_token = responseBody.access_token; credential.key.access_token = responseBody.access_token;
credential.key.expires_in = Math.round((+(new Date()) / 1000) + responseBody.expires_in); credential.key.expires_in = Math.round(+new Date() / 1000 + responseBody.expires_in);
return credential.key.access_token; return credential.key.access_token;
}) });
return { 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 { interface VideoApiAdapter {
createMeeting(event: CalendarEvent): Promise<any>; 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>; getAvailability(dateFrom, dateTo): Promise<any>;
} }
const ZoomVideo = (credential): VideoApiAdapter => { const ZoomVideo = (credential): VideoApiAdapter => {
const auth = zoomAuth(credential); const auth = zoomAuth(credential);
const translateEvent = (event: CalendarEvent) => { const translateEvent = (event: CalendarEvent) => {
@ -89,7 +97,7 @@ const ZoomVideo = (credential): VideoApiAdapter => {
topic: event.title, 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, 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?) //schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
timezone: event.attendees[0].timeZone, timezone: event.attendees[0].timeZone,
//password: "string", TODO: Should we use a password? Maybe generate a random one? //password: "string", TODO: Should we use a password? Maybe generate a random one?
@ -107,82 +115,107 @@ const ZoomVideo = (credential): VideoApiAdapter => {
audio: "both", audio: "both",
auto_recording: "none", auto_recording: "none",
enforce_login: false, enforce_login: false,
registrants_email_notification: true registrants_email_notification: true,
} },
}; };
}; };
return { return {
getAvailability: (dateFrom, dateTo) => { getAvailability: () => {
return auth.getToken().then( return auth
.getToken()
.then(
// TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled. // 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', { (accessToken) =>
method: 'get', fetch("https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=300", {
method: "get",
headers: { headers: {
'Authorization': 'Bearer ' + accessToken Authorization: "Bearer " + accessToken,
} },
}) })
.then(handleErrorsJson) .then(handleErrorsJson)
.then(responseBody => { .then((responseBody) => {
return responseBody.meetings.map((meeting) => ({ return responseBody.meetings.map((meeting) => ({
start: meeting.start_time, start: meeting.start_time,
end: (new Date((new Date(meeting.start_time)).getTime() + meeting.duration * 60000)).toISOString() end: new Date(
})) new Date(meeting.start_time).getTime() + meeting.duration * 60000
).toISOString(),
}));
}) })
).catch((err) => { )
.catch((err) => {
console.log(err); console.log(err);
}); });
}, },
createMeeting: (event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/users/me/meetings', { createMeeting: (event: CalendarEvent) =>
method: 'POST', auth.getToken().then((accessToken) =>
fetch("https://api.zoom.us/v2/users/me/meetings", {
method: "POST",
headers: { headers: {
'Authorization': 'Bearer ' + accessToken, Authorization: "Bearer " + accessToken,
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify(translateEvent(event)) body: JSON.stringify(translateEvent(event)),
}).then(handleErrorsJson)), }).then(handleErrorsJson)
deleteMeeting: (uid: String) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, { ),
method: 'DELETE', deleteMeeting: (uid: string) =>
auth.getToken().then((accessToken) =>
fetch("https://api.zoom.us/v2/meetings/" + uid, {
method: "DELETE",
headers: { headers: {
'Authorization': 'Bearer ' + accessToken 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)
}).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 // factory
const videoIntegrations = (withCredentials): VideoApiAdapter[] => withCredentials.map((cred) => { const videoIntegrations = (withCredentials): VideoApiAdapter[] =>
withCredentials
.map((cred) => {
switch (cred.type) { switch (cred.type) {
case 'zoom_video': case "zoom_video":
return ZoomVideo(cred); return ZoomVideo(cred);
default: default:
return; // unknown credential, could be legacy? In any case, ignore return; // unknown credential, could be legacy? In any case, ignore
} }
}).filter(Boolean); })
.filter(Boolean);
const getBusyVideoTimes = (withCredentials) =>
const getBusyVideoTimes = (withCredentials, dateFrom, dateTo) => Promise.all( Promise.all(videoIntegrations(withCredentials).map((c) => c.getAvailability())).then((results) =>
videoIntegrations(withCredentials).map(c => c.getAvailability(dateFrom, dateTo)) results.reduce((acc, availability) => acc.concat(availability), [])
).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)); const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
if (!credential) { 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 = { const videoCallData: VideoCallData = {
type: credential.type, type: credential.type,
@ -196,55 +229,76 @@ const createMeeting = async (credential, calEvent: CalendarEvent): Promise<any>
try { try {
await organizerMail.sendEmail(); await organizerMail.sendEmail();
} catch (e) { } catch (e) {
console.error("organizerMail.sendEmail failed", e) console.error("organizerMail.sendEmail failed", e);
} }
if (!creationResult || !creationResult.disableConfirmationEmail) { if (!creationResult || !creationResult.disableConfirmationEmail) {
try { try {
await attendeeMail.sendEmail(); await attendeeMail.sendEmail();
} catch (e) { } catch (e) {
console.error("attendeeMail.sendEmail failed", e) console.error("attendeeMail.sendEmail failed", e);
} }
} }
return { return {
type: credential.type,
success,
uid, 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)); const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
if (!credential) { 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 organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid);
const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid);
try { try {
await organizerMail.sendEmail(); await organizerMail.sendEmail();
} catch (e) { } catch (e) {
console.error("organizerMail.sendEmail failed", e) console.error("organizerMail.sendEmail failed", e);
} }
if (!updateResult || !updateResult.disableConfirmationEmail) { if (!updateResult || !updateResult.disableConfirmationEmail) {
try { try {
await attendeeMail.sendEmail(); await attendeeMail.sendEmail();
} catch (e) { } catch (e) {
console.error("attendeeMail.sendEmail failed", e) console.error("attendeeMail.sendEmail failed", e);
} }
} }
return { return {
type: credential.type,
success,
uid: newUid, uid: newUid,
updatedEvent: updateResult updatedEvent: updateResult,
originalEvent: calEvent,
}; };
}; };
const deleteMeeting = (credential, uid: String): Promise<any> => { const deleteMeeting = (credential, uid: string): Promise<any> => {
if (credential) { if (credential) {
return videoIntegrations([credential])[0].deleteMeeting(uid); return videoIntegrations([credential])[0].deleteMeeting(uid);
} }

View file

@ -1,16 +1,17 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "../../../lib/prisma"; import prisma from "../../../lib/prisma";
import { CalendarEvent, createEvent, getBusyCalendarTimes, updateEvent } from "../../../lib/calendarClient"; import { CalendarEvent, getBusyCalendarTimes } from "@lib/calendarClient";
import async from "async";
import { v5 as uuidv5 } from "uuid"; import { v5 as uuidv5 } from "uuid";
import short from "short-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 EventAttendeeMail from "../../../lib/emails/EventAttendeeMail";
import { getEventName } from "../../../lib/event"; import { getEventName } from "@lib/event";
import { LocationType } from "../../../lib/location"; import { LocationType } from "@lib/location";
import merge from "lodash.merge"; import merge from "lodash.merge";
import dayjs from "dayjs"; import dayjs from "dayjs";
import logger from "../../../lib/logger"; import logger from "../../../lib/logger";
import EventManager, { EventResult } from "@lib/events/EventManager";
import { User } from "@prisma/client";
const translator = short(); const translator = short();
const log = logger.getChildLogger({ prefix: ["[api] book:user"] }); const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
@ -63,6 +64,18 @@ const getLocationRequestFromIntegration = ({ location }: GetLocationRequestFromI
requestId: requestId, 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); return res.status(400).json(error);
} }
let currentUser = await prisma.user.findFirst({ let currentUser: User = await prisma.user.findFirst({
where: { where: {
username: user, 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 = const hasCalendarIntegrations =
currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0; currentUser.credentials.filter((cred) => cred.type.endsWith("_calendar")).length > 0;
const hasVideoIntegrations = const hasVideoIntegrations =
@ -152,9 +161,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
name: true, 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 rescheduleUid = req.body.rescheduleUid;
const selectedEventType = await prisma.eventType.findFirst({ 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); return res.status(400).json(error);
} }
let results = []; let results: Array<EventResult> = [];
let referencesToCreate = []; let referencesToCreate = [];
if (rescheduleUid) { if (rescheduleUid) {
@ -249,30 +258,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
}); });
// Use all integrations // Use EventManager to conditionally use all needed integrations.
results = results.concat( results = await eventManager.update(evt, booking);
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 };
});
})
);
if (results.length > 0 && results.every((res) => !res.success)) { if (results.length > 0 && results.every((res) => !res.success)) {
const error = { const error = {
@ -306,28 +293,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]); await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
} else { } else {
// Schedule event // Use EventManager to conditionally use all needed integrations.
results = results.concat( const results: Array<EventResult> = await eventManager.create(evt);
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 };
});
})
);
if (results.length > 0 && results.every((res) => !res.success)) { if (results.length > 0 && results.every((res) => !res.success)) {
const error = { const error = {
@ -342,15 +309,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
referencesToCreate = results.map((result) => { referencesToCreate = results.map((result) => {
return { return {
type: result.type, type: result.type,
uid: result.response.createdEvent.id.toString(), uid: result.createdEvent.id.toString(),
}; };
}); });
} }
const hashUID = const hashUID =
results.length > 0 results.length > 0 ? results[0].uid : translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
? results[0].response.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. // 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. // UID generation should happen in the integration itself, not here.
if (results.length === 0) { if (results.length === 0) {