From 869ba9b97c71a7a61c9aa00135d800eda7a7605c Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 17 Jun 2021 02:44:13 +0200 Subject: [PATCH] Implemented reschedule mail and fixed bug that rescheduling weren't saved --- lib/calendarClient.ts | 593 +++++++++++---------- lib/emails/EventAttendeeMail.ts | 2 +- lib/emails/EventAttendeeRescheduledMail.ts | 40 ++ lib/emails/EventOwnerRescheduledMail.ts | 64 +++ lib/videoClient.ts | 25 +- pages/api/book/[user].ts | 13 +- 6 files changed, 440 insertions(+), 297 deletions(-) create mode 100644 lib/emails/EventAttendeeRescheduledMail.ts create mode 100644 lib/emails/EventOwnerRescheduledMail.ts diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index 786a47d5..5e670a95 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -2,366 +2,379 @@ import EventOwnerMail from "./emails/EventOwnerMail"; import EventAttendeeMail from "./emails/EventAttendeeMail"; import {v5 as uuidv5} from 'uuid'; import short from 'short-uuid'; +import EventOwnerRescheduledMail from "./emails/EventOwnerRescheduledMail"; +import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; const translator = short(); const {google} = require('googleapis'); const googleAuth = () => { - const {client_secret, client_id, redirect_uris} = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web; - return new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + const {client_secret, client_id, redirect_uris} = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web; + return new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); }; function handleErrorsJson(response) { - if (!response.ok) { - response.json().then(console.log); - throw Error(response.statusText); - } - return response.json(); + if (!response.ok) { + response.json().then(console.log); + throw Error(response.statusText); + } + return response.json(); } function handleErrorsRaw(response) { - if (!response.ok) { - response.text().then(console.log); - throw Error(response.statusText); - } - return response.text(); + if (!response.ok) { + response.text().then(console.log); + throw Error(response.statusText); + } + return response.text(); } const o365Auth = (credential) => { - const isExpired = (expiryDate) => expiryDate < +(new Date()); + const isExpired = (expiryDate) => expiryDate < +(new Date()); - const refreshAccessToken = (refreshToken) => fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { - method: 'POST', - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: new URLSearchParams({ - 'scope': 'User.Read Calendars.Read Calendars.ReadWrite', - 'client_id': process.env.MS_GRAPH_CLIENT_ID, - 'refresh_token': refreshToken, - 'grant_type': 'refresh_token', - 'client_secret': process.env.MS_GRAPH_CLIENT_SECRET, - }) + const refreshAccessToken = (refreshToken) => fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { + method: 'POST', + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + body: new URLSearchParams({ + 'scope': 'User.Read Calendars.Read Calendars.ReadWrite', + 'client_id': process.env.MS_GRAPH_CLIENT_ID, + 'refresh_token': refreshToken, + 'grant_type': 'refresh_token', + 'client_secret': process.env.MS_GRAPH_CLIENT_SECRET, + }) + }) + .then(handleErrorsJson) + .then((responseBody) => { + credential.key.access_token = responseBody.access_token; + credential.key.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in); + return credential.key.access_token; }) - .then(handleErrorsJson) - .then((responseBody) => { - credential.key.access_token = responseBody.access_token; - credential.key.expiry_date = Math.round((+(new Date()) / 1000) + responseBody.expires_in); - return credential.key.access_token; - }) - return { - getToken: () => !isExpired(credential.key.expiry_date) ? Promise.resolve(credential.key.access_token) : refreshAccessToken(credential.key.refresh_token) - }; + return { + getToken: () => !isExpired(credential.key.expiry_date) ? Promise.resolve(credential.key.access_token) : refreshAccessToken(credential.key.refresh_token) + }; }; interface Person { - name?: string, - email: string, - timeZone: string + name?: string, + email: string, + timeZone: string } interface CalendarEvent { - type: string; - title: string; - startTime: string; - endTime: string; - description?: string; - location?: string; - organizer: Person; - attendees: Person[]; + type: string; + title: string; + startTime: string; + endTime: string; + description?: string; + location?: string; + organizer: Person; + attendees: Person[]; }; interface CalendarApiAdapter { - createEvent(event: CalendarEvent): Promise; + createEvent(event: CalendarEvent): Promise; - updateEvent(uid: String, event: CalendarEvent); + updateEvent(uid: String, event: CalendarEvent); - deleteEvent(uid: String); + deleteEvent(uid: String); - getAvailability(dateFrom, dateTo): Promise; + getAvailability(dateFrom, dateTo): Promise; } const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { - const auth = o365Auth(credential); + const auth = o365Auth(credential); - const translateEvent = (event: CalendarEvent) => { + const translateEvent = (event: CalendarEvent) => { - let optional = {}; - if (event.location) { - optional.location = {displayName: event.location}; - } - - 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 - } - }; + let optional = {}; + if (event.location) { + optional.location = {displayName: event.location}; + } return { - getAvailability: (dateFrom, dateTo) => { - const payload = { - schedules: [credential.key.email], - startTime: { - dateTime: dateFrom, - timeZone: 'UTC', - }, - endTime: { - dateTime: dateTo, - timeZone: 'UTC', - }, - availabilityViewInterval: 60 - }; - - return auth.getToken().then( - (accessToken) => fetch('https://graph.microsoft.com/v1.0/me/calendar/getSchedule', { - method: 'post', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }) - .then(handleErrorsJson) - .then(responseBody => { - return responseBody.value[0].scheduleItems.map((evt) => ({ - start: evt.start.dateTime + 'Z', - end: evt.end.dateTime + 'Z' - })) - }) - ).catch((err) => { - console.log(err); - }); + 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 }, - createEvent: (event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events', { - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(translateEvent(event)) - }).then(handleErrorsJson).then((responseBody) => ({ - ...responseBody, - disableConfirmationEmail: true, - }))), - deleteEvent: (uid: String) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { - method: 'DELETE', - headers: { - 'Authorization': 'Bearer ' + accessToken - } - }).then(handleErrorsRaw)), - updateEvent: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { - method: 'PATCH', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(translateEvent(event)) - }).then(handleErrorsRaw)), + type: "required" + })), + ...optional } + }; + + return { + getAvailability: (dateFrom, dateTo) => { + const payload = { + schedules: [credential.key.email], + startTime: { + dateTime: dateFrom, + timeZone: 'UTC', + }, + endTime: { + dateTime: dateTo, + timeZone: 'UTC', + }, + availabilityViewInterval: 60 + }; + + return auth.getToken().then( + (accessToken) => fetch('https://graph.microsoft.com/v1.0/me/calendar/getSchedule', { + method: 'post', + headers: { + 'Authorization': 'Bearer ' + accessToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + .then(handleErrorsJson) + .then(responseBody => { + return responseBody.value[0].scheduleItems.map((evt) => ({ + start: evt.start.dateTime + 'Z', + end: evt.end.dateTime + 'Z' + })) + }) + ).catch((err) => { + console.log(err); + }); + }, + createEvent: (event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + accessToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(translateEvent(event)) + }).then(handleErrorsJson).then((responseBody) => ({ + ...responseBody, + disableConfirmationEmail: true, + }))), + deleteEvent: (uid: String) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { + method: 'DELETE', + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }).then(handleErrorsRaw)), + updateEvent: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendar/events/' + uid, { + method: 'PATCH', + headers: { + 'Authorization': 'Bearer ' + accessToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(translateEvent(event)) + }).then(handleErrorsRaw)), + } }; const GoogleCalendar = (credential): CalendarApiAdapter => { - const myGoogleAuth = googleAuth(); - myGoogleAuth.setCredentials(credential.key); - return { - getAvailability: (dateFrom, dateTo) => new Promise((resolve, reject) => { - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.calendarList - .list() - .then(cals => { - calendar.freebusy.query({ - requestBody: { - timeMin: dateFrom, - timeMax: dateTo, - items: cals.data.items - } - }, (err, apires) => { - if (err) { - reject(err); - } - resolve( - Object.values(apires.data.calendars).flatMap( - (item) => item["busy"] - ) - ) - }); - }) - .catch((err) => { - reject(err); - }); - - }), - createEvent: (event: CalendarEvent) => new Promise((resolve, reject) => { - const payload = { - summary: event.title, - description: event.description, - start: { - dateTime: event.startTime, - timeZone: event.organizer.timeZone, - }, - end: { - dateTime: event.endTime, - timeZone: event.organizer.timeZone, - }, - attendees: event.attendees, - reminders: { - useDefault: false, - overrides: [ - {'method': 'email', 'minutes': 60} - ], - }, - }; - - if (event.location) { - payload['location'] = event.location; + const myGoogleAuth = googleAuth(); + myGoogleAuth.setCredentials(credential.key); + return { + getAvailability: (dateFrom, dateTo) => new Promise((resolve, reject) => { + const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); + calendar.calendarList + .list() + .then(cals => { + calendar.freebusy.query({ + requestBody: { + timeMin: dateFrom, + timeMax: dateTo, + items: cals.data.items } - - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.events.insert({ - auth: myGoogleAuth, - calendarId: 'primary', - resource: payload, - }, function (err, event) { - if (err) { - console.log('There was an error contacting the Calendar service: ' + err); - return reject(err); - } - return resolve(event.data); - }); - }), - updateEvent: (uid: String, event: CalendarEvent) => new Promise((resolve, reject) => { - const payload = { - summary: event.title, - description: event.description, - start: { - dateTime: event.startTime, - timeZone: event.organizer.timeZone, - }, - end: { - dateTime: event.endTime, - timeZone: event.organizer.timeZone, - }, - attendees: event.attendees, - reminders: { - useDefault: false, - overrides: [ - {'method': 'email', 'minutes': 60} - ], - }, - }; - - if (event.location) { - payload['location'] = event.location; + }, (err, apires) => { + if (err) { + reject(err); } - - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.events.update({ - auth: myGoogleAuth, - calendarId: 'primary', - eventId: uid, - sendNotifications: true, - sendUpdates: 'all', - resource: payload - }, function (err, event) { - if (err) { - console.log('There was an error contacting the Calendar service: ' + err); - return reject(err); - } - return resolve(event.data); - }); - }), - deleteEvent: (uid: String) => new Promise( (resolve, reject) => { - const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); - calendar.events.delete({ - auth: myGoogleAuth, - calendarId: 'primary', - eventId: uid, - sendNotifications: true, - sendUpdates: 'all', - }, function (err, event) { - if (err) { - console.log('There was an error contacting the Calendar service: ' + err); - return reject(err); - } - return resolve(event.data); - }); + resolve( + Object.values(apires.data.calendars).flatMap( + (item) => item["busy"] + ) + ) + }); }) - }; + .catch((err) => { + reject(err); + }); + + }), + createEvent: (event: CalendarEvent) => new Promise((resolve, reject) => { + const payload = { + summary: event.title, + description: event.description, + start: { + dateTime: event.startTime, + timeZone: event.organizer.timeZone, + }, + end: { + dateTime: event.endTime, + timeZone: event.organizer.timeZone, + }, + attendees: event.attendees, + reminders: { + useDefault: false, + overrides: [ + {'method': 'email', 'minutes': 60} + ], + }, + }; + + if (event.location) { + payload['location'] = event.location; + } + + const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); + calendar.events.insert({ + auth: myGoogleAuth, + calendarId: 'primary', + resource: payload, + }, function (err, event) { + if (err) { + console.log('There was an error contacting the Calendar service: ' + err); + return reject(err); + } + return resolve(event.data); + }); + }), + updateEvent: (uid: String, event: CalendarEvent) => new Promise((resolve, reject) => { + const payload = { + summary: event.title, + description: event.description, + start: { + dateTime: event.startTime, + timeZone: event.organizer.timeZone, + }, + end: { + dateTime: event.endTime, + timeZone: event.organizer.timeZone, + }, + attendees: event.attendees, + reminders: { + useDefault: false, + overrides: [ + {'method': 'email', 'minutes': 60} + ], + }, + }; + + if (event.location) { + payload['location'] = event.location; + } + + const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); + calendar.events.update({ + auth: myGoogleAuth, + calendarId: 'primary', + eventId: uid, + sendNotifications: true, + sendUpdates: 'all', + resource: payload + }, function (err, event) { + if (err) { + console.log('There was an error contacting the Calendar service: ' + err); + return reject(err); + } + return resolve(event.data); + }); + }), + deleteEvent: (uid: String) => new Promise((resolve, reject) => { + const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); + calendar.events.delete({ + auth: myGoogleAuth, + calendarId: 'primary', + eventId: uid, + sendNotifications: true, + sendUpdates: 'all', + }, function (err, event) { + if (err) { + console.log('There was an error contacting the Calendar service: ' + err); + return reject(err); + } + return resolve(event.data); + }); + }) + }; }; // factory const calendars = (withCredentials): CalendarApiAdapter[] => withCredentials.map((cred) => { - switch (cred.type) { - case 'google_calendar': - return GoogleCalendar(cred); - case 'office365_calendar': - return MicrosoftOffice365Calendar(cred); - default: - return; // unknown credential, could be legacy? In any case, ignore - } + switch (cred.type) { + case 'google_calendar': + return GoogleCalendar(cred); + case 'office365_calendar': + return MicrosoftOffice365Calendar(cred); + default: + return; // unknown credential, could be legacy? In any case, ignore + } }).filter(Boolean); const getBusyCalendarTimes = (withCredentials, dateFrom, dateTo) => Promise.all( - calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo)) + calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo)) ).then( - (results) => results.reduce((acc, availability) => acc.concat(availability), []) + (results) => results.reduce((acc, availability) => acc.concat(availability), []) ); const createEvent = async (credential, calEvent: CalendarEvent): Promise => { - const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); + const uid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); - const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; + const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; - const ownerMail = new EventOwnerMail(calEvent, uid); - const attendeeMail = new EventAttendeeMail(calEvent, uid); - await ownerMail.sendEmail(); + const ownerMail = new EventOwnerMail(calEvent, uid); + const attendeeMail = new EventAttendeeMail(calEvent, uid); + await ownerMail.sendEmail(); - if(!creationResult || !creationResult.disableConfirmationEmail) { - await attendeeMail.sendEmail(); - } + if (!creationResult || !creationResult.disableConfirmationEmail) { + await attendeeMail.sendEmail(); + } - return { - uid, - createdEvent: creationResult - }; + return { + uid, + createdEvent: creationResult + }; }; -const updateEvent = (credential, uid: String, calEvent: CalendarEvent): Promise => { - if (credential) { - return calendars([credential])[0].updateEvent(uid, calEvent); - } +const updateEvent = async (credential, uidToUpdate: String, calEvent: CalendarEvent): Promise => { + const newUid: string = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); - return Promise.resolve({}); + const updateResult = credential ? await calendars([credential])[0].updateEvent(uidToUpdate, calEvent) : null; + + const ownerMail = new EventOwnerRescheduledMail(calEvent, newUid); + const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); + await ownerMail.sendEmail(); + + if (!updateResult || !updateResult.disableConfirmationEmail) { + await attendeeMail.sendEmail(); + } + + return { + uid: newUid, + updatedEvent: updateResult + }; }; const deleteEvent = (credential, uid: String): Promise => { - if (credential) { - return calendars([credential])[0].deleteEvent(uid); - } + if (credential) { + return calendars([credential])[0].deleteEvent(uid); + } - return Promise.resolve({}); + return Promise.resolve({}); }; export {getBusyCalendarTimes, createEvent, updateEvent, deleteEvent, CalendarEvent}; diff --git a/lib/emails/EventAttendeeMail.ts b/lib/emails/EventAttendeeMail.ts index 265104d8..b8bef1ba 100644 --- a/lib/emails/EventAttendeeMail.ts +++ b/lib/emails/EventAttendeeMail.ts @@ -49,7 +49,7 @@ export default class EventAttendeeMail extends EventMail { * * @private */ - private getInviteeStart(): Dayjs { + protected getInviteeStart(): Dayjs { return dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone); } } \ No newline at end of file diff --git a/lib/emails/EventAttendeeRescheduledMail.ts b/lib/emails/EventAttendeeRescheduledMail.ts new file mode 100644 index 00000000..760aa040 --- /dev/null +++ b/lib/emails/EventAttendeeRescheduledMail.ts @@ -0,0 +1,40 @@ +import EventAttendeeMail from "./EventAttendeeMail"; + +export default class EventAttendeeRescheduledMail extends EventAttendeeMail { + /** + * Returns the email text as HTML representation. + * + * @protected + */ + protected getHtmlRepresentation(): string { + return ` +
+ Hi ${this.calEvent.attendees[0].name},
+
+ Your ${this.calEvent.type} with ${this.calEvent.organizer.name} has been rescheduled to ${this.getInviteeStart().format('h:mma')} + (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format('dddd, LL')}.
+ ` + this.getAdditionalFooter() + ` +
+ `; + } + + /** + * Returns the payload object for the nodemailer. + * + * @protected + */ + protected getNodeMailerPayload(): Object { + return { + to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`, + from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, + replyTo: this.calEvent.organizer.email, + subject: `Rescheduled: ${this.calEvent.type} with ${this.calEvent.organizer.name} on ${this.getInviteeStart().format('dddd, LL')}`, + html: this.getHtmlRepresentation(), + text: this.getPlainTextRepresentation(), + }; + } + + protected printNodeMailerError(error: string): void { + console.error("SEND_RESCHEDULE_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error); + } +} \ No newline at end of file diff --git a/lib/emails/EventOwnerRescheduledMail.ts b/lib/emails/EventOwnerRescheduledMail.ts new file mode 100644 index 00000000..1b9ac9ca --- /dev/null +++ b/lib/emails/EventOwnerRescheduledMail.ts @@ -0,0 +1,64 @@ +import dayjs, {Dayjs} from "dayjs"; +import EventOwnerMail from "./EventOwnerMail"; + +export default class EventOwnerRescheduledMail extends EventOwnerMail { + /** + * Returns the email text as HTML representation. + * + * @protected + */ + protected getHtmlRepresentation(): string { + return ` +
+ Hi ${this.calEvent.organizer.name},
+
+ Your event has been rescheduled.
+
+ Event Type:
+ ${this.calEvent.type}
+
+ Invitee Email:
+ ${this.calEvent.attendees[0].email}
+
` + this.getAdditionalBody() + + ( + this.calEvent.location ? ` + Location:
+ ${this.calEvent.location}
+
+ ` : '' + ) + + `Invitee Time Zone:
+ ${this.calEvent.attendees[0].timeZone}
+
+ Additional notes:
+ ${this.calEvent.description} + ` + this.getAdditionalFooter() + ` +
+ `; + } + + /** + * Returns the payload object for the nodemailer. + * + * @protected + */ + protected getNodeMailerPayload(): Object { + const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); + + return { + icalEvent: { + filename: 'event.ics', + content: this.getiCalEventAsString(), + }, + from: `Calendso <${this.getMailerOptions().from}>`, + to: this.calEvent.organizer.email, + subject: `Rescheduled event: ${this.calEvent.attendees[0].name} - ${organizerStart.format('LT dddd, LL')} - ${this.calEvent.type}`, + html: this.getHtmlRepresentation(), + text: this.getPlainTextRepresentation(), + }; + } + + protected printNodeMailerError(error: string): void { + console.error("SEND_RESCHEDULE_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error); + } +} \ No newline at end of file diff --git a/lib/videoClient.ts b/lib/videoClient.ts index c3e3bcae..6397f971 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -4,6 +4,8 @@ import VideoEventOwnerMail from "./emails/VideoEventOwnerMail"; import VideoEventAttendeeMail from "./emails/VideoEventAttendeeMail"; import {v5 as uuidv5} from 'uuid'; import short from 'short-uuid'; +import EventAttendeeRescheduledMail from "./emails/EventAttendeeRescheduledMail"; +import EventOwnerRescheduledMail from "./emails/EventOwnerRescheduledMail"; const translator = short(); @@ -203,12 +205,27 @@ const createMeeting = async (credential, calEvent: CalendarEvent): Promise }; }; -const updateMeeting = (credential, uid: String, event: CalendarEvent): Promise => { - if (credential) { - return videoIntegrations([credential])[0].updateMeeting(uid, event); +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."); } - return Promise.resolve({}); + const updateResult = credential ? await videoIntegrations([credential])[0].updateMeeting(uidToUpdate, calEvent) : null; + + const ownerMail = new EventOwnerRescheduledMail(calEvent, newUid); + const attendeeMail = new EventAttendeeRescheduledMail(calEvent, newUid); + await ownerMail.sendEmail(); + + if (!updateResult || !updateResult.disableConfirmationEmail) { + await attendeeMail.sendEmail(); + } + + return { + uid: newUid, + updatedEvent: updateResult + }; }; const deleteMeeting = (credential, uid: String): Promise => { diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 2f40276b..83707bf2 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -78,12 +78,21 @@ 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 await updateEvent(credential, bookingRefUid, evt) + const response = await updateEvent(credential, bookingRefUid, evt); + + return { + type: credential.type, + response + }; })); results = results.concat(await async.mapLimit(videoCredentials, 5, async (credential) => { const bookingRefUid = booking.references.filter((ref) => ref.type === credential.type)[0].uid; - return await updateMeeting(credential, bookingRefUid, evt) + const response = await updateMeeting(credential, bookingRefUid, evt); + return { + type: credential.type, + response + }; })); // Clone elements