Created EventManager in order to unify event CRUD logic

pull/375/head
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

@ -1,11 +1,15 @@
import prisma from "./prisma"; 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,63 +37,67 @@ 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', { headers: {
method: 'POST', Authorization: authHeader,
headers: { "Content-Type": "application/x-www-form-urlencoded",
'Authorization': authHeader, },
'Content-Type': 'application/x-www-form-urlencoded' body: new URLSearchParams({
}, refresh_token: refreshToken,
body: new URLSearchParams({ grant_type: "refresh_token",
'refresh_token': refreshToken, }),
'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.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 { 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) => {
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate // Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
return { return {
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?
@ -97,8 +105,8 @@ const ZoomVideo = (credential): VideoApiAdapter => {
settings: { settings: {
host_video: true, host_video: true,
participant_video: true, participant_video: true,
cn_meeting: false, // TODO: true if host meeting in China cn_meeting: false, // TODO: true if host meeting in China
in_meeting: false, // TODO: true if host meeting in India in_meeting: false, // TODO: true if host meeting in India
join_before_host: true, join_before_host: true,
mute_upon_entry: false, mute_upon_entry: false,
watermark: false, watermark: false,
@ -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
// TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled. .getToken()
(accessToken) => fetch('https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=300', { .then(
method: 'get', // TODO Possibly implement pagination for cases when there are more than 300 meetings already scheduled.
headers: { (accessToken) =>
'Authorization': 'Bearer ' + accessToken fetch("https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=300", {
} method: "get",
}) headers: {
.then(handleErrorsJson) Authorization: "Bearer " + accessToken,
.then(responseBody => { },
return responseBody.meetings.map((meeting) => ({ })
start: meeting.start_time, .then(handleErrorsJson)
end: (new Date((new Date(meeting.start_time)).getTime() + meeting.duration * 60000)).toISOString() .then((responseBody) => {
})) return responseBody.meetings.map((meeting) => ({
}) start: meeting.start_time,
).catch((err) => { end: new Date(
console.log(err); 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', { createMeeting: (event: CalendarEvent) =>
method: 'POST', auth.getToken().then((accessToken) =>
headers: { fetch("https://api.zoom.us/v2/users/me/meetings", {
'Authorization': 'Bearer ' + accessToken, method: "POST",
'Content-Type': 'application/json', headers: {
}, Authorization: "Bearer " + accessToken,
body: JSON.stringify(translateEvent(event)) "Content-Type": "application/json",
}).then(handleErrorsJson)), },
deleteMeeting: (uid: String) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, { body: JSON.stringify(translateEvent(event)),
method: 'DELETE', }).then(handleErrorsJson)
headers: { ),
'Authorization': 'Bearer ' + accessToken deleteMeeting: (uid: string) =>
} auth.getToken().then((accessToken) =>
}).then(handleErrorsRaw)), fetch("https://api.zoom.us/v2/meetings/" + uid, {
updateMeeting: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, { method: "DELETE",
method: 'PATCH', headers: {
headers: { Authorization: "Bearer " + accessToken,
'Authorization': 'Bearer ' + accessToken, },
'Content-Type': 'application/json' }).then(handleErrorsRaw)
}, ),
body: JSON.stringify(translateEvent(event)) updateMeeting: (uid: string, event: CalendarEvent) =>
}).then(handleErrorsRaw)), 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[] =>
switch (cred.type) { withCredentials
case 'zoom_video': .map((cred) => {
return ZoomVideo(cred); switch (cred.type) {
default: case "zoom_video":
return; // unknown credential, could be legacy? In any case, ignore return ZoomVideo(cred);
} default:
}).filter(Boolean); 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( const createMeeting = async (credential, calEvent: CalendarEvent): Promise<EventResult> => {
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 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);
} }
@ -252,4 +306,4 @@ const deleteMeeting = (credential, uid: String): Promise<any> => {
return Promise.resolve({}); return Promise.resolve({});
}; };
export {getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting}; export { getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting };

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) {