From daecc1e0e40a3086f3b693c12d65ebfe7c0b70a0 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 15 Jul 2021 03:19:30 +0200 Subject: [PATCH 01/22] 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) { From b146b807785be684ed72d5023250f884ac418ec5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 15 Jul 2021 23:34:55 +0200 Subject: [PATCH 02/22] Fixed signature --- lib/calendarClient.ts | 4 +++- pages/api/cancel.ts | 45 +++++++++++++++++++------------------------ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 41720971..f9c4dfd5 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -498,7 +498,9 @@ const calendars = (withCredentials): CalendarApiAdapter[] => .filter(Boolean); const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => - Promise.all(calendars(withCredentials).map((c) => c.getAvailability(selectedCalendars))).then((results) => { + Promise.all( + calendars(withCredentials).map((c) => c.getAvailability(dateFrom, dateTo, selectedCalendars)) + ).then((results) => { return results.reduce((acc, availability) => acc.concat(availability), []); }); diff --git a/pages/api/cancel.ts b/pages/api/cancel.ts index 90ef2f43..e3c96f98 100644 --- a/pages/api/cancel.ts +++ b/pages/api/cancel.ts @@ -1,7 +1,7 @@ -import prisma from '../../lib/prisma'; -import {deleteEvent} from "../../lib/calendarClient"; -import async from 'async'; -import {deleteMeeting} from "../../lib/videoClient"; +import prisma from "../../lib/prisma"; +import { deleteEvent } from "../../lib/calendarClient"; +import async from "async"; +import { deleteMeeting } from "../../lib/videoClient"; export default async function handler(req, res) { if (req.method == "POST") { @@ -15,36 +15,36 @@ export default async function handler(req, res) { id: true, user: { select: { - credentials: true - } + credentials: true, + }, }, attendees: true, references: { select: { uid: true, - type: true - } - } - } + type: true, + }, + }, + }, }); const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => { const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0].uid; - if(credential.type.endsWith("_calendar")) { + if (credential.type.endsWith("_calendar")) { return await deleteEvent(credential, bookingRefUid); - } else if(credential.type.endsWith("_video")) { + } else if (credential.type.endsWith("_video")) { return await deleteMeeting(credential, bookingRefUid); } }); const attendeeDeletes = prisma.attendee.deleteMany({ where: { - bookingId: bookingToDelete.id - } + bookingId: bookingToDelete.id, + }, }); const bookingReferenceDeletes = prisma.bookingReference.deleteMany({ where: { - bookingId: bookingToDelete.id - } + bookingId: bookingToDelete.id, + }, }); const bookingDeletes = prisma.booking.delete({ where: { @@ -52,17 +52,12 @@ export default async function handler(req, res) { }, }); - await Promise.all([ - apiDeletes, - attendeeDeletes, - bookingReferenceDeletes, - bookingDeletes - ]); + await Promise.all([apiDeletes, attendeeDeletes, bookingReferenceDeletes, bookingDeletes]); //TODO Perhaps send emails to user and client to tell about the cancellation - res.status(200).json({message: 'Booking successfully deleted.'}); + res.status(200).json({ message: "Booking successfully deleted." }); } else { - res.status(405).json({message: 'This endpoint only accepts POST requests.'}); + res.status(405).json({ message: "This endpoint only accepts POST requests." }); } -} \ No newline at end of file +} From 81e1287693da9767fbe5b6b3b1f1dd7f82bab829 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 18 Jul 2021 16:03:59 +0200 Subject: [PATCH 03/22] Fixed cancellation --- lib/calendarClient.ts | 24 +++++++++++++++++++++++- lib/events/EventManager.ts | 3 +-- pages/api/cancel.ts | 12 +++++++----- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 0b3f9102..f9c4dfd5 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -157,7 +157,29 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { optional.location = { displayName: event.location }; } - return toRet; + return { + subject: event.title, + body: { + contentType: "HTML", + content: event.description, + }, + start: { + dateTime: event.startTime, + timeZone: event.organizer.timeZone, + }, + end: { + dateTime: event.endTime, + timeZone: event.organizer.timeZone, + }, + attendees: event.attendees.map((attendee) => ({ + emailAddress: { + address: attendee.email, + name: attendee.name, + }, + type: "required", + })), + ...optional, + }; }; const integrationType = "office365_calendar"; diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 69e184df..05adebaa 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -33,9 +33,8 @@ export default class EventManager { } public async create(event: CalendarEvent): Promise> { - const results: Array = []; // First, create all calendar events. - results.concat(await this.createAllCalendarEvents(event)); + const results: Array = 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)) { diff --git a/pages/api/cancel.ts b/pages/api/cancel.ts index e3c96f98..3610f1da 100644 --- a/pages/api/cancel.ts +++ b/pages/api/cancel.ts @@ -29,11 +29,13 @@ export default async function handler(req, res) { }); const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => { - const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0].uid; - if (credential.type.endsWith("_calendar")) { - return await deleteEvent(credential, bookingRefUid); - } else if (credential.type.endsWith("_video")) { - return await deleteMeeting(credential, bookingRefUid); + const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0]?.uid; + if (bookingRefUid) { + if (credential.type.endsWith("_calendar")) { + return await deleteEvent(credential, bookingRefUid); + } else if (credential.type.endsWith("_video")) { + return await deleteMeeting(credential, bookingRefUid); + } } }); const attendeeDeletes = prisma.attendee.deleteMany({ From a40a5c04fe10709c9106c0b955d282b1b211740c Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 18 Jul 2021 22:17:18 +0200 Subject: [PATCH 04/22] Retain rescheduleUid when picking a date --- lib/events/EventManager.ts | 5 ++--- pages/[user]/[type].tsx | 14 +++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 05adebaa..5fbc58d4 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -45,9 +45,8 @@ export default class EventManager { } public async update(event: CalendarEvent, booking: PartialBooking): Promise> { - const results: Array = []; // First, update all calendar events. - results.concat(await this.updateAllCalendarEvents(event, booking)); + const results: Array = 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)) { @@ -95,7 +94,7 @@ export default class EventManager { booking: PartialBooking ): Promise> { return async.mapLimit(this.calendarCredentials, 5, async (credential) => { - const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid; + const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0]?.uid; return updateEvent(credential, bookingRefUid, event); }); } diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 04cdbd76..313ee133 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -48,9 +48,17 @@ export default function Type(props): Type { router.replace( { - query: { - date: formattedDate, - }, + query: Object.assign( + {}, + { + date: formattedDate, + }, + rescheduleUid + ? { + rescheduleUid: rescheduleUid, + } + : {} + ), }, undefined, { From 7aff32fb502bbefc7fc86fc2dfcbe05d54d434e8 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 20 Jul 2021 20:07:59 +0200 Subject: [PATCH 05/22] Only send single mail when booking zoom --- lib/calendarClient.ts | 73 +++++++++++++++++++++----------------- lib/events/EventManager.ts | 47 ++++++++++++++++++------ pages/api/book/[user].ts | 2 +- 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index f9c4dfd5..bfe0f472 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -509,7 +509,11 @@ 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, + noMail = false +): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const uid: string = parser.getUid(); const richEvent: CalendarEvent = parser.asRichEvent(); @@ -529,29 +533,31 @@ const createEvent = async (credential: Credential, calEvent: CalendarEvent): Pro const maybeEntryPoints = creationResult?.entryPoints; const maybeConferenceData = creationResult?.conferenceData; - const organizerMail = new EventOrganizerMail(calEvent, uid, { - hangoutLink: maybeHangoutLink, - conferenceData: maybeConferenceData, - entryPoints: maybeEntryPoints, - }); + if (!noMail) { + const organizerMail = new EventOrganizerMail(calEvent, uid, { + hangoutLink: maybeHangoutLink, + conferenceData: maybeConferenceData, + entryPoints: maybeEntryPoints, + }); - const attendeeMail = new EventAttendeeMail(calEvent, uid, { - hangoutLink: maybeHangoutLink, - conferenceData: maybeConferenceData, - entryPoints: maybeEntryPoints, - }); + const attendeeMail = new EventAttendeeMail(calEvent, uid, { + hangoutLink: maybeHangoutLink, + conferenceData: maybeConferenceData, + entryPoints: maybeEntryPoints, + }); - try { - await organizerMail.sendEmail(); - } catch (e) { - console.error("organizerMail.sendEmail failed", e); - } - - if (!creationResult || !creationResult.disableConfirmationEmail) { try { - await attendeeMail.sendEmail(); + await organizerMail.sendEmail(); } catch (e) { - console.error("attendeeMail.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); + } } } @@ -567,7 +573,8 @@ const createEvent = async (credential: Credential, calEvent: CalendarEvent): Pro const updateEvent = async ( credential: Credential, uidToUpdate: string, - calEvent: CalendarEvent + calEvent: CalendarEvent, + noMail: false ): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); const newUid: string = parser.getUid(); @@ -584,19 +591,21 @@ const updateEvent = async ( }) : 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); - } - - if (!updateResult || !updateResult.disableConfirmationEmail) { + if (!noMail) { + const organizerMail = new EventOrganizerRescheduledMail(calEvent, newUid); + const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); try { - await attendeeMail.sendEmail(); + await organizerMail.sendEmail(); } catch (e) { - console.error("attendeeMail.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); + } } } diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 5fbc58d4..6c254c3c 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -33,11 +33,13 @@ export default class EventManager { } public async create(event: CalendarEvent): Promise> { - // First, create all calendar events. - const results: Array = await this.createAllCalendarEvents(event); + const isVideo = EventManager.isIntegration(event.location); + + // First, create all calendar events. If this is a video event, don't send a mail right here. + const results: Array = await this.createAllCalendarEvents(event, isVideo); // If and only if event type is a video meeting, create a video meeting as well. - if (EventManager.isIntegration(event.location)) { + if (isVideo) { results.push(await this.createVideoEvent(event)); } @@ -45,11 +47,13 @@ export default class EventManager { } public async update(event: CalendarEvent, booking: PartialBooking): Promise> { - // First, update all calendar events. - const results: Array = await this.updateAllCalendarEvents(event, booking); + const isVideo = EventManager.isIntegration(event.location); + + // First, update all calendar events. If this is a video event, don't send a mail right here. + const results: Array = await this.updateAllCalendarEvents(event, booking, isVideo); // If and only if event type is a video meeting, update the video meeting as well. - if (EventManager.isIntegration(event.location)) { + if (isVideo) { results.push(await this.updateVideoEvent(event, booking)); } @@ -58,13 +62,17 @@ export default class EventManager { /** * Creates event entries for all calendar integrations given in the credentials. + * When noMail is true, no mails will be sent. This is used when the event is + * a video meeting because then the mail containing the video credentials will be + * more important than the mails created for these bare calendar events. * * @param event + * @param noMail * @private */ - private createAllCalendarEvents(event: CalendarEvent): Promise> { + private createAllCalendarEvents(event: CalendarEvent, noMail: boolean): Promise> { return async.mapLimit(this.calendarCredentials, 5, async (credential: Credential) => { - return createEvent(credential, event); + return createEvent(credential, event, noMail); }); } @@ -89,16 +97,35 @@ export default class EventManager { } } + /** + * Updates the event entries for all calendar integrations given in the credentials. + * When noMail is true, no mails will be sent. This is used when the event is + * a video meeting because then the mail containing the video credentials will be + * more important than the mails created for these bare calendar events. + * + * @param event + * @param booking + * @param noMail + * @private + */ private updateAllCalendarEvents( event: CalendarEvent, - booking: PartialBooking + booking: PartialBooking, + noMail: boolean ): 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); + return updateEvent(credential, bookingRefUid, event, noMail); }); } + /** + * Updates a single video event. + * + * @param event + * @param booking + * @private + */ private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) { const credential = this.getVideoCredential(event); diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index fef3fac8..e5863cb5 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -356,7 +356,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]); } else { // Use EventManager to conditionally use all needed integrations. - const results: Array = await eventManager.create(evt); + results = await eventManager.create(evt); if (results.length > 0 && results.every((res) => !res.success)) { const error = { From 3a18aa10ddab31ce4e3569b12ee88f2e0017c2b5 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 20 Jul 2021 20:24:05 +0200 Subject: [PATCH 06/22] Fixed linting errors --- next.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/next.config.js b/next.config.js index b35afdce..171dc100 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,5 @@ -import nextTranspileModules from "next-transpile-modules"; -const withTM = nextTranspileModules(["react-timezone-select"]); +/* eslint-disable */ +const withTM = require("next-transpile-modules")(["react-timezone-select"]); // TODO: Revisit this later with getStaticProps in App if (process.env.NEXTAUTH_URL) { From da64dae568db42efdab5bc07cbb5eb17b8e4c6a3 Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 20 Jul 2021 20:25:36 +0200 Subject: [PATCH 07/22] Fixed linting errors --- next.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.js b/next.config.js index 171dc100..bb567c76 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,4 @@ -/* eslint-disable */ +/* eslint-disable @typescript-eslint/no-var-requires */ const withTM = require("next-transpile-modules")(["react-timezone-select"]); // TODO: Revisit this later with getStaticProps in App From cf52df5662be503afe18b8d1eaa0e9823151298f Mon Sep 17 00:00:00 2001 From: nicolas Date: Tue, 20 Jul 2021 20:40:41 +0200 Subject: [PATCH 08/22] Use entrypoint to make zoom location more beautiful --- lib/emails/EventMail.ts | 5 +++-- lib/emails/VideoEventAttendeeMail.ts | 17 ++++++++++++----- lib/emails/VideoEventOrganizerMail.ts | 9 ++++++++- lib/videoClient.ts | 17 +++++++++++++++-- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/emails/EventMail.ts b/lib/emails/EventMail.ts index 90cee0b5..1c9253fc 100644 --- a/lib/emails/EventMail.ts +++ b/lib/emails/EventMail.ts @@ -4,7 +4,7 @@ import { CalendarEvent, ConferenceData } from "../calendarClient"; import { serverConfig } from "../serverConfig"; import nodemailer from "nodemailer"; -interface EntryPoint { +export interface EntryPoint { entryPointType?: string; uri?: string; label?: string; @@ -15,7 +15,7 @@ interface EntryPoint { password?: string; } -interface AdditionInformation { +export interface AdditionInformation { conferenceData?: ConferenceData; entryPoints?: EntryPoint[]; hangoutLink?: string; @@ -34,6 +34,7 @@ export default abstract class EventMail { * * @param calEvent * @param uid + * @param additionInformation */ constructor(calEvent: CalendarEvent, uid: string, additionInformation: AdditionInformation = null) { this.calEvent = calEvent; diff --git a/lib/emails/VideoEventAttendeeMail.ts b/lib/emails/VideoEventAttendeeMail.ts index 7855f36d..2a8aa58e 100644 --- a/lib/emails/VideoEventAttendeeMail.ts +++ b/lib/emails/VideoEventAttendeeMail.ts @@ -1,14 +1,21 @@ -import {CalendarEvent} from "../calendarClient"; +import { CalendarEvent } from "../calendarClient"; import EventAttendeeMail from "./EventAttendeeMail"; -import {getFormattedMeetingId, getIntegrationName} from "./helpers"; -import {VideoCallData} from "../videoClient"; +import { getFormattedMeetingId, getIntegrationName } from "./helpers"; +import { VideoCallData } from "../videoClient"; +import { AdditionInformation } from "@lib/emails/EventMail"; export default class VideoEventAttendeeMail extends EventAttendeeMail { videoCallData: VideoCallData; - constructor(calEvent: CalendarEvent, uid: string, videoCallData: VideoCallData) { + constructor( + calEvent: CalendarEvent, + uid: string, + videoCallData: VideoCallData, + additionInformation: AdditionInformation = null + ) { super(calEvent, uid); this.videoCallData = videoCallData; + this.additionInformation = additionInformation; } /** @@ -24,4 +31,4 @@ export default class VideoEventAttendeeMail extends EventAttendeeMail { Meeting URL: ${this.videoCallData.url}
`; } -} \ No newline at end of file +} diff --git a/lib/emails/VideoEventOrganizerMail.ts b/lib/emails/VideoEventOrganizerMail.ts index 1f5b2384..cfb11e05 100644 --- a/lib/emails/VideoEventOrganizerMail.ts +++ b/lib/emails/VideoEventOrganizerMail.ts @@ -2,13 +2,20 @@ import { CalendarEvent } from "../calendarClient"; import EventOrganizerMail from "./EventOrganizerMail"; import { VideoCallData } from "../videoClient"; import { getFormattedMeetingId, getIntegrationName } from "./helpers"; +import { AdditionInformation } from "@lib/emails/EventMail"; export default class VideoEventOrganizerMail extends EventOrganizerMail { videoCallData: VideoCallData; - constructor(calEvent: CalendarEvent, uid: string, videoCallData: VideoCallData) { + constructor( + calEvent: CalendarEvent, + uid: string, + videoCallData: VideoCallData, + additionInformation: AdditionInformation = null + ) { super(calEvent, uid); this.videoCallData = videoCallData; + this.additionInformation = additionInformation; } /** diff --git a/lib/videoClient.ts b/lib/videoClient.ts index ec1b48ce..faea7d0b 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -8,6 +8,8 @@ import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail" import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; import { EventResult } from "@lib/events/EventManager"; import logger from "@lib/logger"; +import { AdditionInformation, EntryPoint } from "@lib/emails/EventMail"; +import { getIntegrationName } from "@lib/emails/helpers"; const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] }); @@ -224,8 +226,19 @@ const createMeeting = async (credential, calEvent: CalendarEvent): Promise Date: Sat, 24 Jul 2021 22:30:14 +0200 Subject: [PATCH 09/22] Added JSDoc --- lib/events/EventManager.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 50087a22..28a7876a 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -33,11 +33,23 @@ export default class EventManager { calendarCredentials: Array; videoCredentials: Array; + /** + * Takes an array of credentials and initializes a new instance of the EventManager. + * + * @param credentials + */ constructor(credentials: Array) { this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar")); this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video")); } + /** + * Takes a CalendarEvent and creates all necessary integration entries for it. + * When a video integration is chosen as the event's location, a video integration + * event will be scheduled for it as well. + * + * @param event + */ public async create(event: CalendarEvent): Promise { const isVideo = EventManager.isIntegration(event.location); @@ -62,6 +74,13 @@ export default class EventManager { }; } + /** + * Takes a calendarEvent and a rescheduleUid and updates the event that has the + * given uid using the data delivered in the given CalendarEvent. + * + * @param event + * @param rescheduleUid + */ public async update(event: CalendarEvent, rescheduleUid: string): Promise { // Get details of existing booking. const booking = await prisma.booking.findFirst({ @@ -132,6 +151,12 @@ export default class EventManager { }); } + /** + * Checks which video integration is needed for the event's location and returns + * credentials for that - if existing. + * @param event + * @private + */ private getVideoCredential(event: CalendarEvent): Credential | undefined { const integrationName = event.location.replace("integrations:", ""); return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName)); From a97862d4b84dc18f03bea6120e0cc5a16921572f Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 14:19:49 +0200 Subject: [PATCH 10/22] Process event location in EventManager --- lib/events/EventManager.ts | 58 ++++++++++++++++++++++++++++++++++++++ pages/api/book/[user].ts | 56 ++---------------------------------- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 28a7876a..050f870d 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -3,6 +3,9 @@ import { Credential } from "@prisma/client"; import async from "async"; import { createMeeting, updateMeeting } from "@lib/videoClient"; import prisma from "@lib/prisma"; +import { LocationType } from "@lib/location"; +import { v5 as uuidv5 } from "uuid"; +import merge from "lodash.merge"; export interface EventResult { type: string; @@ -29,6 +32,10 @@ export interface PartialReference { uid: string; } +interface GetLocationRequestFromIntegrationRequest { + location: string; +} + export default class EventManager { calendarCredentials: Array; videoCredentials: Array; @@ -51,6 +58,7 @@ export default class EventManager { * @param event */ public async create(event: CalendarEvent): Promise { + event = EventManager.processLocation(event); const isVideo = EventManager.isIntegration(event.location); // First, create all calendar events. If this is a video event, don't send a mail right here. @@ -82,6 +90,8 @@ export default class EventManager { * @param rescheduleUid */ public async update(event: CalendarEvent, rescheduleUid: string): Promise { + event = EventManager.processLocation(event); + // Get details of existing booking. const booking = await prisma.booking.findFirst({ where: { @@ -227,4 +237,52 @@ export default class EventManager { private static isIntegration(location: string): boolean { return location.includes("integrations:"); } + + /** + * Helper function for processLocation: Returns the conferenceData object to be merged + * with the CalendarEvent. + * + * @param locationObj + * @private + */ + private static getLocationRequestFromIntegration(locationObj: GetLocationRequestFromIntegrationRequest) { + const location = locationObj.location; + + if (location === LocationType.GoogleMeet.valueOf() || location === LocationType.Zoom.valueOf()) { + const requestId = uuidv5(location, uuidv5.URL); + + return { + conferenceData: { + createRequest: { + requestId: requestId, + }, + }, + location, + }; + } + + return null; + } + + /** + * Takes a CalendarEvent and adds a ConferenceData object to the event + * if the event has an integration-related location. + * + * @param event + * @private + */ + private static processLocation(event: CalendarEvent): CalendarEvent { + // If location is set to an integration location + // Build proper transforms for evt object + // Extend evt object with those transformations + if (event.location?.includes("integration")) { + const maybeLocationRequestObject = EventManager.getLocationRequestFromIntegration({ + location: event.location, + }); + + event = merge(event, maybeLocationRequestObject); + } + + return event; + } } diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index f086bf3f..df756da3 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -6,8 +6,6 @@ import short from "short-uuid"; import { getBusyVideoTimes } from "@lib/videoClient"; import EventAttendeeMail from "../../../lib/emails/EventAttendeeMail"; 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, { CreateUpdateResult, EventResult } from "@lib/events/EventManager"; @@ -86,38 +84,6 @@ function isOutOfBounds( } } -interface GetLocationRequestFromIntegrationRequest { - location: string; -} - -const getLocationRequestFromIntegration = ({ location }: GetLocationRequestFromIntegrationRequest) => { - if (location === LocationType.GoogleMeet.valueOf()) { - const requestId = uuidv5(location, uuidv5.URL); - - return { - conferenceData: { - createRequest: { - requestId: requestId, - }, - }, - location, - }; - } else if (location === LocationType.Zoom.valueOf()) { - const requestId = uuidv5(location, uuidv5.URL); - - return { - conferenceData: { - createRequest: { - requestId: requestId, - }, - }, - location, - }; - } - - return null; -}; - export async function handleLegacyConfirmationMail( results: Array, selectedEventType: { requiresConfirmation: boolean }, @@ -235,9 +201,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - const rawLocation = req.body.location; - - let evt: CalendarEvent = { + const evt: CalendarEvent = { type: selectedEventType.title, title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName), description: req.body.notes, @@ -245,25 +209,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) endTime: req.body.end, organizer: { email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone }, attendees: [{ email: req.body.email, name: req.body.name, timeZone: req.body.timeZone }], + location: req.body.location, // Will be processed by the EventManager later. }; - // If phone or inPerson use raw location - // set evt.location to req.body.location - if (!rawLocation?.includes("integration")) { - evt.location = rawLocation; - } - - // If location is set to an integration location - // Build proper transforms for evt object - // Extend evt object with those transformations - if (rawLocation?.includes("integration")) { - const maybeLocationRequestObject = getLocationRequestFromIntegration({ - location: rawLocation, - }); - - evt = merge(evt, maybeLocationRequestObject); - } - const eventType = await prisma.eventType.findFirst({ where: { userId: currentUser.id, From df161d549822b691ef0f5912efae064ff9959afe Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 14:37:22 +0200 Subject: [PATCH 11/22] Added location to bookings table --- pages/api/book/[user].ts | 1 + pages/api/book/confirm.ts | 1 + .../20210725123357_add_location_to_booking/migration.sql | 2 ++ prisma/schema.prisma | 1 + 4 files changed, 5 insertions(+) create mode 100644 prisma/migrations/20210725123357_add_location_to_booking/migration.sql diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index df756da3..a7f10b15 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -336,6 +336,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) attendees: { create: evt.attendees, }, + location: evt.location, // This is the raw location that can be processed by the EventManager. confirmed: !selectedEventType.requiresConfirmation, }, }); diff --git a/pages/api/book/confirm.ts b/pages/api/book/confirm.ts index fb8b961a..36cfb95e 100644 --- a/pages/api/book/confirm.ts +++ b/pages/api/book/confirm.ts @@ -42,6 +42,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) endTime: true, confirmed: true, attendees: true, + location: true, userId: true, id: true, uid: true, diff --git a/prisma/migrations/20210725123357_add_location_to_booking/migration.sql b/prisma/migrations/20210725123357_add_location_to_booking/migration.sql new file mode 100644 index 00000000..f2874f5e --- /dev/null +++ b/prisma/migrations/20210725123357_add_location_to_booking/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Booking" ADD COLUMN "location" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 83aed724..d08335ac 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -132,6 +132,7 @@ model Booking { endTime DateTime attendees Attendee[] + location String? createdAt DateTime @default(now()) updatedAt DateTime? From 4dd8359a151ac5f73f0f9d388f739b74dac0cbf9 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 14:40:02 +0200 Subject: [PATCH 12/22] Made location optional --- lib/events/EventManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 050f870d..be68f58b 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -235,7 +235,7 @@ export default class EventManager { * @private */ private static isIntegration(location: string): boolean { - return location.includes("integrations:"); + return location?.includes("integrations:"); } /** From 4fb8e8285e8e86cee4233c162bf3058df9a3bd94 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 16:29:06 +0200 Subject: [PATCH 13/22] Added location to event --- pages/api/book/confirm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/api/book/confirm.ts b/pages/api/book/confirm.ts index 36cfb95e..65c60b91 100644 --- a/pages/api/book/confirm.ts +++ b/pages/api/book/confirm.ts @@ -64,6 +64,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) endTime: booking.endTime.toISOString(), organizer: { email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone }, attendees: booking.attendees, + location: booking.location, }; if (req.body.confirmed) { From b0ac65b0f6b64456e98048d837d99fa2eb37e6c8 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 17:05:18 +0200 Subject: [PATCH 14/22] Added maybeUid to createEvent and createMeeting --- lib/calendarClient.ts | 5 +++-- lib/events/EventManager.ts | 26 +++++++++++++++++++------- lib/videoClient.ts | 8 ++++++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index baa47349..6d59ff7a 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -512,10 +512,11 @@ const listCalendars = (withCredentials) => const createEvent = async ( credential: Credential, calEvent: CalendarEvent, - noMail = false + noMail = false, + maybeUid: string = null ): Promise => { const parser: CalEventParser = new CalEventParser(calEvent); - const uid: string = parser.getUid(); + const uid: string = maybeUid ?? parser.getUid(); const richEvent: CalendarEvent = parser.asRichEvent(); let success = true; diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index be68f58b..98987022 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -54,19 +54,21 @@ export default class EventManager { * Takes a CalendarEvent and creates all necessary integration entries for it. * When a video integration is chosen as the event's location, a video integration * event will be scheduled for it as well. + * An optional uid can be set to override the auto-generated uid. * * @param event + * @param maybeUid */ - public async create(event: CalendarEvent): Promise { + public async create(event: CalendarEvent, maybeUid: string = null): Promise { event = EventManager.processLocation(event); const isVideo = EventManager.isIntegration(event.location); // First, create all calendar events. If this is a video event, don't send a mail right here. - const results: Array = await this.createAllCalendarEvents(event, isVideo); + const results: Array = await this.createAllCalendarEvents(event, isVideo, maybeUid); // If and only if event type is a video meeting, create a video meeting as well. if (isVideo) { - results.push(await this.createVideoEvent(event)); + results.push(await this.createVideoEvent(event, maybeUid)); } const referencesToCreate: Array = results.map((result) => { @@ -151,13 +153,20 @@ export default class EventManager { * a video meeting because then the mail containing the video credentials will be * more important than the mails created for these bare calendar events. * + * When the optional uid is set, it will be used instead of the auto generated uid. + * * @param event * @param noMail + * @param maybeUid * @private */ - private createAllCalendarEvents(event: CalendarEvent, noMail: boolean): Promise> { + private createAllCalendarEvents( + event: CalendarEvent, + noMail: boolean, + maybeUid: string = null + ): Promise> { return async.mapLimit(this.calendarCredentials, 5, async (credential: Credential) => { - return createEvent(credential, event, noMail); + return createEvent(credential, event, noMail, maybeUid); }); } @@ -175,14 +184,17 @@ export default class EventManager { /** * Creates a video event entry for the selected integration location. * + * When optional uid is set, it will be used instead of the auto generated uid. + * * @param event + * @param maybeUid * @private */ - private createVideoEvent(event: CalendarEvent): Promise { + private createVideoEvent(event: CalendarEvent, maybeUid: string = null): Promise { const credential = this.getVideoCredential(event); if (credential) { - return createMeeting(credential, event); + return createMeeting(credential, event, maybeUid); } else { return Promise.reject("No suitable credentials given for the requested integration name."); } diff --git a/lib/videoClient.ts b/lib/videoClient.ts index faea7d0b..1f4e3d3d 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -201,8 +201,12 @@ const getBusyVideoTimes = (withCredentials) => results.reduce((acc, availability) => acc.concat(availability), []) ); -const createMeeting = async (credential, calEvent: CalendarEvent): Promise => { - const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); +const createMeeting = async ( + credential, + calEvent: CalendarEvent, + maybeUid: string = null +): Promise => { + const uid: string = maybeUid ?? translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); if (!credential) { throw new Error( From 65fd73375145ad4622f8e94a3d9a9a3a4c2cf51b Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 17:08:11 +0200 Subject: [PATCH 15/22] Use optional udi --- pages/api/book/confirm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/book/confirm.ts b/pages/api/book/confirm.ts index 65c60b91..54bc7552 100644 --- a/pages/api/book/confirm.ts +++ b/pages/api/book/confirm.ts @@ -69,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (req.body.confirmed) { const eventManager = new EventManager(currentUser.credentials); - const scheduleResult = await eventManager.create(evt); + const scheduleResult = await eventManager.create(evt, booking.uid); await handleLegacyConfirmationMail( scheduleResult.results, From 2b38638d8499f8a356c878f915d25b41cd0c6d0d Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 19:15:31 +0200 Subject: [PATCH 16/22] Added maybeUid to CalEventParser --- lib/CalEventParser.ts | 8 +++++--- lib/calendarClient.ts | 4 ++-- lib/emails/EventMail.ts | 2 +- lib/videoClient.ts | 4 +++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index 69724ccc..78292804 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -6,10 +6,12 @@ import { stripHtml } from "./emails/helpers"; const translator = short(); export default class CalEventParser { - calEvent: CalendarEvent; + protected calEvent: CalendarEvent; + protected maybeUid: string; - constructor(calEvent: CalendarEvent) { + constructor(calEvent: CalendarEvent, maybeUid: string = null) { this.calEvent = calEvent; + this.maybeUid = maybeUid; } /** @@ -30,7 +32,7 @@ export default class CalEventParser { * Returns a unique identifier for the given calendar event. */ public getUid(): string { - return translator.fromUUID(uuidv5(JSON.stringify(this.calEvent), uuidv5.URL)); + return this.maybeUid ?? translator.fromUUID(uuidv5(JSON.stringify(this.calEvent), uuidv5.URL)); } /** diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 6d59ff7a..7102a535 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -515,8 +515,8 @@ const createEvent = async ( noMail = false, maybeUid: string = null ): Promise => { - const parser: CalEventParser = new CalEventParser(calEvent); - const uid: string = maybeUid ?? parser.getUid(); + const parser: CalEventParser = new CalEventParser(calEvent, maybeUid); + const uid: string = parser.getUid(); const richEvent: CalendarEvent = parser.asRichEvent(); let success = true; diff --git a/lib/emails/EventMail.ts b/lib/emails/EventMail.ts index 1c9253fc..b3728872 100644 --- a/lib/emails/EventMail.ts +++ b/lib/emails/EventMail.ts @@ -39,7 +39,7 @@ export default abstract class EventMail { constructor(calEvent: CalendarEvent, uid: string, additionInformation: AdditionInformation = null) { this.calEvent = calEvent; this.uid = uid; - this.parser = new CalEventParser(calEvent); + this.parser = new CalEventParser(calEvent, uid); this.additionInformation = additionInformation; } diff --git a/lib/videoClient.ts b/lib/videoClient.ts index 1f4e3d3d..755c0cda 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -10,6 +10,7 @@ import { EventResult } from "@lib/events/EventManager"; import logger from "@lib/logger"; import { AdditionInformation, EntryPoint } from "@lib/emails/EventMail"; import { getIntegrationName } from "@lib/emails/helpers"; +import CalEventParser from "@lib/CalEventParser"; const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] }); @@ -206,7 +207,8 @@ const createMeeting = async ( calEvent: CalendarEvent, maybeUid: string = null ): Promise => { - const uid: string = maybeUid ?? translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); + const parser: CalEventParser = new CalEventParser(calEvent, maybeUid); + const uid: string = parser.getUid(); if (!credential) { throw new Error( From 47ee0334db780c11468fb768438e4b89220afa7d Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 25 Jul 2021 23:22:34 +0200 Subject: [PATCH 17/22] Use better Regex to strip down html --- lib/emails/helpers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/emails/helpers.ts b/lib/emails/helpers.ts index e5218a0a..5c07dbbe 100644 --- a/lib/emails/helpers.ts +++ b/lib/emails/helpers.ts @@ -25,5 +25,11 @@ export function getFormattedMeetingId(videoCallData: VideoCallData): string { } export function stripHtml(html: string): string { - return html.replace("
", "\n").replace(/<[^>]+>/g, ""); + const aMailToRegExp = /"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g; + const aLinkRegExp = /"]*)"[\s\w="_:#;]*>([^<>]*)<\/a>/g; + return html + .replace(//g, "\n") + .replace(aMailToRegExp, "$1") + .replace(aLinkRegExp, "$2: $1") + .replace(/<[^>]+>/g, ""); } From f948370bef6aa326a1c30fc04e2bb555ec50916d Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 28 Jul 2021 22:05:37 +0200 Subject: [PATCH 18/22] Fixed codacy issues --- lib/videoClient.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/videoClient.ts b/lib/videoClient.ts index 755c0cda..e4a5e686 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -11,6 +11,7 @@ import logger from "@lib/logger"; import { AdditionInformation, EntryPoint } from "@lib/emails/EventMail"; import { getIntegrationName } from "@lib/emails/helpers"; import CalEventParser from "@lib/CalEventParser"; +import { Credential } from "@prisma/client"; const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] }); @@ -86,7 +87,7 @@ interface VideoApiAdapter { updateMeeting(uid: string, event: CalendarEvent); - deleteMeeting(uid: string); + deleteMeeting(uid: string): Promise; getAvailability(dateFrom, dateTo): Promise; } @@ -197,13 +198,13 @@ const videoIntegrations = (withCredentials): VideoApiAdapter[] => }) .filter(Boolean); -const getBusyVideoTimes = (withCredentials) => +const getBusyVideoTimes: (withCredentials) => Promise = (withCredentials) => Promise.all(videoIntegrations(withCredentials).map((c) => c.getAvailability())).then((results) => results.reduce((acc, availability) => acc.concat(availability), []) ); const createMeeting = async ( - credential, + credential: Credential, calEvent: CalendarEvent, maybeUid: string = null ): Promise => { @@ -317,7 +318,7 @@ const updateMeeting = async ( }; }; -const deleteMeeting = (credential, uid: string): Promise => { +const deleteMeeting = (credential: Credential, uid: string): Promise => { if (credential) { return videoIntegrations([credential])[0].deleteMeeting(uid); } From 082281bdd04dc84c9e912e9e83e9fb282290d609 Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 28 Jul 2021 22:44:52 +0200 Subject: [PATCH 19/22] Added type for credential --- lib/videoClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/videoClient.ts b/lib/videoClient.ts index e4a5e686..aa4b6c5d 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -270,7 +270,7 @@ const createMeeting = async ( }; const updateMeeting = async ( - credential, + credential: Credential, uidToUpdate: string, calEvent: CalendarEvent ): Promise => { From 19374d38f71f0eda1fd5b5b6ac5408dda21d99f7 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 1 Aug 2021 23:29:15 +0200 Subject: [PATCH 20/22] Renamed isVideo to isDedicated; hard-coded logic for zoom meetings for now --- lib/events/EventManager.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index 98987022..cc3143a5 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -61,13 +61,13 @@ export default class EventManager { */ public async create(event: CalendarEvent, maybeUid: string = null): Promise { event = EventManager.processLocation(event); - const isVideo = EventManager.isIntegration(event.location); + const isDedicated = EventManager.isDedicatedIntegration(event.location); - // First, create all calendar events. If this is a video event, don't send a mail right here. - const results: Array = await this.createAllCalendarEvents(event, isVideo, maybeUid); + // First, create all calendar events. If this is a dedicated integration event, don't send a mail right here. + const results: Array = await this.createAllCalendarEvents(event, isDedicated, maybeUid); - // If and only if event type is a video meeting, create a video meeting as well. - if (isVideo) { + // If and only if event type is a dedicated meeting, create a dedicated video meeting as well. + if (isDedicated) { results.push(await this.createVideoEvent(event, maybeUid)); } @@ -111,7 +111,7 @@ export default class EventManager { }, }); - const isVideo = EventManager.isIntegration(event.location); + const isVideo = EventManager.isDedicatedIntegration(event.location); // First, update all calendar events. If this is a video event, don't send a mail right here. const results: Array = await this.updateAllCalendarEvents(event, booking, isVideo); @@ -241,13 +241,19 @@ export default class EventManager { } /** - * Returns true if the given location describes an integration that delivers meeting credentials. + * Returns true if the given location describes a dedicated integration that + * delivers meeting credentials. Zoom, for example, is dedicated, because it + * needs to be called independently from any calendar APIs to receive meeting + * credentials. Google Meetings, in contrast, are not dedicated, because they + * are created while scheduling a regular calendar event by simply adding some + * attributes to the payload JSON. * * @param location * @private */ - private static isIntegration(location: string): boolean { - return location?.includes("integrations:"); + private static isDedicatedIntegration(location: string): boolean { + // Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't. + return location === "integrations:zoom"; } /** From d2bc02e6fc5e3c471b114b522bfd313dd8c28697 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 1 Aug 2021 23:38:38 +0200 Subject: [PATCH 21/22] Further renaming --- lib/events/EventManager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/events/EventManager.ts b/lib/events/EventManager.ts index cc3143a5..f40fb49b 100644 --- a/lib/events/EventManager.ts +++ b/lib/events/EventManager.ts @@ -111,13 +111,13 @@ export default class EventManager { }, }); - const isVideo = EventManager.isDedicatedIntegration(event.location); + const isDedicated = EventManager.isDedicatedIntegration(event.location); - // First, update all calendar events. If this is a video event, don't send a mail right here. - const results: Array = await this.updateAllCalendarEvents(event, booking, isVideo); + // First, update all calendar events. If this is a dedicated event, don't send a mail right here. + const results: Array = await this.updateAllCalendarEvents(event, booking, isDedicated); - // If and only if event type is a video meeting, update the video meeting as well. - if (isVideo) { + // If and only if event type is a dedicated meeting, update the dedicated video meeting as well. + if (isDedicated) { results.push(await this.updateVideoEvent(event, booking)); } From 5a5e61739b5f1c784458696e2571bfd9f53b4253 Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 8 Aug 2021 21:41:02 +0200 Subject: [PATCH 22/22] Removed faulty check --- pages/api/book/[user].ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index b032c925..eeda903a 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -34,11 +34,6 @@ function isAvailable(busyTimes, time, length) { const startTime = dayjs(busyTime.start); const endTime = dayjs(busyTime.end); - // Check if start times are the same - if (dayjs(time).format("HH:mm") == startTime.format("HH:mm")) { - t = false; - } - // Check if time is between start and end times if (dayjs(time).isBetween(startTime, endTime)) { t = false;