From 51a8bafaa7862e0ca1fdfd9ec4b60d1d800ca30c Mon Sep 17 00:00:00 2001 From: nicolas Date: Wed, 16 Jun 2021 22:14:44 +0200 Subject: [PATCH] Full zoom integration (except availability check) --- lib/emails/confirm-booked.ts | 53 ++++++++++++++++++++++++++++-------- lib/videoClient.ts | 7 ++--- pages/[user]/book.tsx | 2 +- pages/api/book/[user].ts | 12 ++++++-- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/lib/emails/confirm-booked.ts b/lib/emails/confirm-booked.ts index 6d7898aa..00ab4514 100644 --- a/lib/emails/confirm-booked.ts +++ b/lib/emails/confirm-booked.ts @@ -10,21 +10,47 @@ dayjs.extend(localizedFormat); dayjs.extend(utc); dayjs.extend(timezone); -export default function createConfirmBookedEmail(calEvent: CalendarEvent, cancelLink: string, rescheduleLink: string, options: any = {}) { +export interface VideoCallData { + type: string; + id: string; + password: string; + url: string; +}; + +export function integrationTypeToName(type: string): string { + //TODO: When there are more complex integration type strings, we should consider using an extra field in the DB for that. + const nameProto = type.split("_")[0]; + return nameProto.charAt(0).toUpperCase() + nameProto.slice(1); +} + +export function formattedId(videoCallData: VideoCallData): string { + switch(videoCallData.type) { + case 'zoom_video': + const strId = videoCallData.id.toString(); + const part1 = strId.slice(0, 3); + const part2 = strId.slice(3, 7); + const part3 = strId.slice(7, 11); + return part1 + " " + part2 + " " + part3; + default: + return videoCallData.id.toString(); + } +} + +export default function createConfirmBookedEmail(calEvent: CalendarEvent, cancelLink: string, rescheduleLink: string, options: any = {}, videoCallData?: VideoCallData) { return sendEmail(calEvent, cancelLink, rescheduleLink, { provider: { transport: serverConfig.transport, from: serverConfig.from, }, ...options - }); + }, videoCallData); } const sendEmail = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: string, { provider, -}) => new Promise( (resolve, reject) => { +}, videoCallData?: VideoCallData) => new Promise((resolve, reject) => { - const { from, transport } = provider; + const {from, transport} = provider; const inviteeStart: Dayjs = dayjs(calEvent.startTime).tz(calEvent.attendees[0].timeZone); nodemailer.createTransport(transport).sendMail( @@ -33,8 +59,8 @@ const sendEmail = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: from: `${calEvent.organizer.name} <${from}>`, replyTo: calEvent.organizer.email, subject: `Confirmed: ${calEvent.type} with ${calEvent.organizer.name} on ${inviteeStart.format('dddd, LL')}`, - html: html(calEvent, cancelLink, rescheduleLink), - text: text(calEvent, cancelLink, rescheduleLink), + html: html(calEvent, cancelLink, rescheduleLink, videoCallData), + text: text(calEvent, cancelLink, rescheduleLink, videoCallData), }, (error, info) => { if (error) { @@ -46,7 +72,7 @@ const sendEmail = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: ) }); -const html = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: string) => { +const html = (calEvent: CalendarEvent, cancelLink, rescheduleLink: string, videoCallData?: VideoCallData) => { const inviteeStart: Dayjs = dayjs(calEvent.startTime).tz(calEvent.attendees[0].timeZone); return `
@@ -55,9 +81,14 @@ const html = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: strin Your ${calEvent.type} with ${calEvent.organizer.name} at ${inviteeStart.format('h:mma')} (${calEvent.attendees[0].timeZone}) on ${inviteeStart.format('dddd, LL')} is scheduled.

` + ( - calEvent.location ? `Location: ${calEvent.location}

` : '' - ) + - `Additional notes:
+ videoCallData ? `Video call provider: ${integrationTypeToName(videoCallData.type)}
+ Meeting ID: ${formattedId(videoCallData)}
+ Meeting Password: ${videoCallData.password}
+ Meeting URL: ${videoCallData.url}

` : '' + ) + ( + calEvent.location ? `Location: ${calEvent.location}

` : '' + ) + + `Additional notes:
${calEvent.description}

Need to change this event?
@@ -67,4 +98,4 @@ const html = (calEvent: CalendarEvent, cancelLink: string, rescheduleLink: strin `; }; -const text = (evt: CalendarEvent, cancelLink: string, rescheduleLink: string) => html(evt, cancelLink, rescheduleLink).replace('
', "\n").replace(/<[^>]+>/g, ''); \ No newline at end of file +const text = (evt: CalendarEvent, cancelLink: string, rescheduleLink: string, videoCallData?: VideoCallData) => html(evt, cancelLink, rescheduleLink, videoCallData).replace('
', "\n").replace(/<[^>]+>/g, ''); \ No newline at end of file diff --git a/lib/videoClient.ts b/lib/videoClient.ts index 62c653c6..4a8817df 100644 --- a/lib/videoClient.ts +++ b/lib/videoClient.ts @@ -97,8 +97,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, @@ -149,7 +149,6 @@ const ZoomVideo = (credential): VideoApiAdapter => { console.log(err); });*/ }, - //TODO Also add the client user to the meeting after creation createMeeting: (meeting: VideoMeeting) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/users/me/meetings', { method: 'POST', headers: { @@ -194,7 +193,7 @@ const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all( const createMeeting = (credential, meeting: VideoMeeting): Promise => { - //TODO Implement email template + //TODO Send email to event host /*createNewMeetingEmail( meeting, );*/ diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index f27eda37..4598adcd 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -76,7 +76,7 @@ export default function Book(props) { } ); - let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=1`; + let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=${!!rescheduleUid}`; if (payload['location']) { successUrl += "&location=" + encodeURIComponent(payload['location']); } diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 302bcd29..8f4a2a4f 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -1,7 +1,7 @@ import type {NextApiRequest, NextApiResponse} from 'next'; import prisma from '../../../lib/prisma'; import {CalendarEvent, createEvent, updateEvent} from '../../../lib/calendarClient'; -import createConfirmBookedEmail from "../../../lib/emails/confirm-booked"; +import createConfirmBookedEmail, {VideoCallData} from "../../../lib/emails/confirm-booked"; import async from 'async'; import {v5 as uuidv5} from 'uuid'; import short from 'short-uuid'; @@ -182,10 +182,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); + const videoResults = results.filter((res) => res.type.endsWith('_video')); + const videoCallData: VideoCallData = videoResults.length === 0 ? undefined : { + type: videoResults[0].type, + id: videoResults[0].response.id, + password: videoResults[0].response.password, + url: videoResults[0].response.join_url, + }; + // If one of the integrations allows email confirmations or no integrations are added, send it. if (currentUser.credentials.length === 0 || !results.every((result) => result.disableConfirmationEmail)) { await createConfirmBookedEmail( - evt, cancelLink, rescheduleLink + evt, cancelLink, rescheduleLink, {}, videoCallData ); }