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