Introduced EventOwnerMail and VideoEventOwnerMail as class based implementations
This commit is contained in:
parent
51a8bafaa7
commit
e37dd017c8
5 changed files with 231 additions and 81 deletions
|
@ -1,5 +1,6 @@
|
|||
import EventOwnerMail from "./emails/EventOwnerMail";
|
||||
|
||||
const {google} = require('googleapis');
|
||||
import createNewEventEmail from "./emails/new-event";
|
||||
|
||||
const googleAuth = () => {
|
||||
const {client_secret, client_id, redirect_uris} = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web;
|
||||
|
@ -323,17 +324,16 @@ const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all(
|
|||
(results) => results.reduce((acc, availability) => acc.concat(availability), [])
|
||||
);
|
||||
|
||||
const createEvent = (credential, calEvent: CalendarEvent): Promise<any> => {
|
||||
const createEvent = async (credential, calEvent: CalendarEvent): Promise<any> => {
|
||||
const mail = new EventOwnerMail(calEvent);
|
||||
const sentMail = await mail.sendEmail();
|
||||
|
||||
createNewEventEmail(
|
||||
calEvent,
|
||||
);
|
||||
const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null;
|
||||
|
||||
if (credential) {
|
||||
return calendars([credential])[0].createEvent(calEvent);
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
return {
|
||||
createdEvent: creationResult,
|
||||
sentMail: sentMail
|
||||
};
|
||||
};
|
||||
|
||||
const updateEvent = (credential, uid: String, calEvent: CalendarEvent): Promise<any> => {
|
||||
|
|
150
lib/emails/EventOwnerMail.ts
Normal file
150
lib/emails/EventOwnerMail.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import {CalendarEvent} from "../calendarClient";
|
||||
import {createEvent} from "ics";
|
||||
import dayjs, {Dayjs} from "dayjs";
|
||||
import {serverConfig} from "../serverConfig";
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export default class EventOwnerMail {
|
||||
calEvent: CalendarEvent;
|
||||
|
||||
/**
|
||||
* An EventOwnerMail always consists of a CalendarEvent
|
||||
* that stores the very basic data of the event (like date, title etc).
|
||||
*
|
||||
* @param calEvent
|
||||
*/
|
||||
constructor(calEvent: CalendarEvent) {
|
||||
this.calEvent = calEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance's event as an iCal event in string representation.
|
||||
* @protected
|
||||
*/
|
||||
protected getiCalEventAsString(): string {
|
||||
const icsEvent = createEvent({
|
||||
start: dayjs(this.calEvent.startTime).utc().toArray().slice(0, 6),
|
||||
startInputType: 'utc',
|
||||
productId: 'calendso/ics',
|
||||
title: `${this.calEvent.type} with ${this.calEvent.attendees[0].name}`,
|
||||
description: this.calEvent.description + this.stripHtml(this.getAdditionalBody()),
|
||||
duration: {minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), 'minute')},
|
||||
organizer: {name: this.calEvent.organizer.name, email: this.calEvent.organizer.email},
|
||||
attendees: this.calEvent.attendees.map((attendee: any) => ({name: attendee.name, email: attendee.email})),
|
||||
status: "CONFIRMED",
|
||||
});
|
||||
if (icsEvent.error) {
|
||||
throw icsEvent.error;
|
||||
}
|
||||
return icsEvent.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email text as HTML representation.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getHtmlRepresentation(): string {
|
||||
return `
|
||||
<div>
|
||||
Hi ${this.calEvent.organizer.name},<br />
|
||||
<br />
|
||||
A new event has been scheduled.<br />
|
||||
<br />
|
||||
<strong>Event Type:</strong><br />
|
||||
${this.calEvent.type}<br />
|
||||
<br />
|
||||
<strong>Invitee Email:</strong><br />
|
||||
<a href="mailto:${this.calEvent.attendees[0].email}">${this.calEvent.attendees[0].email}</a><br />
|
||||
<br />` + this.getAdditionalBody() +
|
||||
(
|
||||
this.calEvent.location ? `
|
||||
<strong>Location:</strong><br />
|
||||
${this.calEvent.location}<br />
|
||||
<br />
|
||||
` : ''
|
||||
) +
|
||||
`<strong>Invitee Time Zone:</strong><br />
|
||||
${this.calEvent.attendees[0].timeZone}<br />
|
||||
<br />
|
||||
<strong>Additional notes:</strong><br />
|
||||
${this.calEvent.description}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email text in a plain text representation
|
||||
* by stripping off the HTML tags.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getPlainTextRepresentation(): string {
|
||||
return this.stripHtml(this.getHtmlRepresentation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips off all HTML tags and leaves plain text.
|
||||
*
|
||||
* @param html
|
||||
* @protected
|
||||
*/
|
||||
protected stripHtml(html: string): string {
|
||||
return html
|
||||
.replace('<br />', "\n")
|
||||
.replace(/<[^>]+>/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the email to the event attendant and returns a Promise.
|
||||
*/
|
||||
public sendEmail(): Promise<any> {
|
||||
const options = this.getMailerOptions();
|
||||
const {transport, from} = options;
|
||||
const organizerStart: Dayjs = <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
|
||||
|
||||
return new Promise((resolve, reject) => nodemailer.createTransport(transport).sendMail(
|
||||
{
|
||||
icalEvent: {
|
||||
filename: 'event.ics',
|
||||
content: this.getiCalEventAsString(),
|
||||
},
|
||||
from: `Calendso <${from}>`,
|
||||
to: this.calEvent.organizer.email,
|
||||
subject: `New event: ${this.calEvent.attendees[0].name} - ${organizerStart.format('LT dddd, LL')} - ${this.calEvent.type}`,
|
||||
html: this.getHtmlRepresentation(),
|
||||
text: this.getPlainTextRepresentation(),
|
||||
},
|
||||
(error, info) => {
|
||||
if (error) {
|
||||
console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error);
|
||||
reject(new Error(error));
|
||||
} else {
|
||||
resolve(info);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers the required provider information from the config.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getMailerOptions(): any {
|
||||
return {
|
||||
transport: serverConfig.transport,
|
||||
from: serverConfig.from,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to include additional HTML or plain text
|
||||
* content into the mail body and calendar event description.
|
||||
* Leave it to an empty string if not desired.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getAdditionalBody(): string {
|
||||
return "";
|
||||
}
|
||||
}
|
27
lib/emails/VideoEventOwnerMail.ts
Normal file
27
lib/emails/VideoEventOwnerMail.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {CalendarEvent} from "../calendarClient";
|
||||
import EventOwnerMail from "./EventOwnerMail";
|
||||
import {formattedId, integrationTypeToName, VideoCallData} from "./confirm-booked";
|
||||
|
||||
export default class VideoEventOwnerMail extends EventOwnerMail {
|
||||
videoCallData: VideoCallData;
|
||||
|
||||
constructor(calEvent: CalendarEvent, videoCallData: VideoCallData) {
|
||||
super(calEvent);
|
||||
this.videoCallData = videoCallData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the video call information to the mail body
|
||||
* and calendar event description.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
protected getAdditionalBody(): string {
|
||||
return `
|
||||
<strong>Video call provider:</strong> ${integrationTypeToName(this.videoCallData.type)}<br />
|
||||
<strong>Meeting ID:</strong> ${formattedId(this.videoCallData)}<br />
|
||||
<strong>Meeting Password:</strong> ${this.videoCallData.password}<br />
|
||||
<strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br />
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
import prisma from "./prisma";
|
||||
import {VideoCallData} from "./emails/confirm-booked";
|
||||
import {CalendarEvent} from "./calendarClient";
|
||||
import VideoEventOwnerMail from "./emails/VideoEventOwnerMail";
|
||||
|
||||
function handleErrorsJson(response) {
|
||||
if (!response.ok) {
|
||||
|
@ -53,26 +56,10 @@ const zoomAuth = (credential) => {
|
|||
};
|
||||
};
|
||||
|
||||
interface Person {
|
||||
name?: string,
|
||||
email: string,
|
||||
timeZone: string
|
||||
}
|
||||
|
||||
interface VideoMeeting {
|
||||
title: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
description?: string;
|
||||
timezone: string;
|
||||
organizer: Person;
|
||||
attendees: Person[];
|
||||
}
|
||||
|
||||
interface VideoApiAdapter {
|
||||
createMeeting(meeting: VideoMeeting): Promise<any>;
|
||||
createMeeting(event: CalendarEvent): Promise<any>;
|
||||
|
||||
updateMeeting(uid: String, meeting: VideoMeeting);
|
||||
updateMeeting(uid: String, event: CalendarEvent);
|
||||
|
||||
deleteMeeting(uid: String);
|
||||
|
||||
|
@ -83,17 +70,17 @@ const ZoomVideo = (credential): VideoApiAdapter => {
|
|||
|
||||
const auth = zoomAuth(credential);
|
||||
|
||||
const translateMeeting = (meeting: VideoMeeting) => {
|
||||
const translateEvent = (event: CalendarEvent) => {
|
||||
// Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
|
||||
const meet = {
|
||||
topic: meeting.title,
|
||||
return {
|
||||
topic: event.title,
|
||||
type: 2, // Means that this is a scheduled meeting
|
||||
start_time: meeting.startTime,
|
||||
duration: ((new Date(meeting.endTime)).getTime() - (new Date(meeting.startTime)).getTime()) / 60000,
|
||||
start_time: event.startTime,
|
||||
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: meeting.timezone,
|
||||
timezone: event.attendees[0].timeZone,
|
||||
//password: "string", TODO: Should we use a password? Maybe generate a random one?
|
||||
agenda: meeting.description,
|
||||
agenda: event.description,
|
||||
settings: {
|
||||
host_video: true,
|
||||
participant_video: true,
|
||||
|
@ -110,8 +97,6 @@ const ZoomVideo = (credential): VideoApiAdapter => {
|
|||
registrants_email_notification: true
|
||||
}
|
||||
};
|
||||
|
||||
return meet;
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -149,13 +134,13 @@ const ZoomVideo = (credential): VideoApiAdapter => {
|
|||
console.log(err);
|
||||
});*/
|
||||
},
|
||||
createMeeting: (meeting: VideoMeeting) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/users/me/meetings', {
|
||||
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(translateMeeting(meeting))
|
||||
body: JSON.stringify(translateEvent(event))
|
||||
}).then(handleErrorsJson)),
|
||||
deleteMeeting: (uid: String) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, {
|
||||
method: 'DELETE',
|
||||
|
@ -163,13 +148,13 @@ const ZoomVideo = (credential): VideoApiAdapter => {
|
|||
'Authorization': 'Bearer ' + accessToken
|
||||
}
|
||||
}).then(handleErrorsRaw)),
|
||||
updateMeeting: (uid: String, meeting: VideoMeeting) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, {
|
||||
updateMeeting: (uid: String, event: CalendarEvent) => auth.getToken().then(accessToken => fetch('https://api.zoom.us/v2/meetings/' + uid, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(translateMeeting(meeting))
|
||||
body: JSON.stringify(translateEvent(event))
|
||||
}).then(handleErrorsRaw)),
|
||||
}
|
||||
};
|
||||
|
@ -191,23 +176,32 @@ const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all(
|
|||
(results) => results.reduce((acc, availability) => acc.concat(availability), [])
|
||||
);
|
||||
|
||||
const createMeeting = (credential, meeting: VideoMeeting): Promise<any> => {
|
||||
|
||||
//TODO Send email to event host
|
||||
/*createNewMeetingEmail(
|
||||
meeting,
|
||||
);*/
|
||||
|
||||
if (credential) {
|
||||
return videoIntegrations([credential])[0].createMeeting(meeting);
|
||||
const createMeeting = async (credential, calEvent: CalendarEvent): Promise<any> => {
|
||||
if(!credential) {
|
||||
throw new Error("Credentials must be set! Video platforms are optional, so this method shouldn't even be called.");
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
const creationResult = await videoIntegrations([credential])[0].createMeeting(calEvent);
|
||||
|
||||
const videoCallData: VideoCallData = {
|
||||
type: credential.type,
|
||||
id: creationResult.id,
|
||||
password: creationResult.password,
|
||||
url: creationResult.join_url,
|
||||
};
|
||||
|
||||
const updateMeeting = (credential, uid: String, meeting: VideoMeeting): Promise<any> => {
|
||||
const mail = new VideoEventOwnerMail(calEvent, videoCallData);
|
||||
const sentMail = await mail.sendEmail();
|
||||
|
||||
return {
|
||||
createdEvent: creationResult,
|
||||
sentMail: sentMail
|
||||
};
|
||||
};
|
||||
|
||||
const updateMeeting = (credential, uid: String, event: CalendarEvent): Promise<any> => {
|
||||
if (credential) {
|
||||
return videoIntegrations([credential])[0].updateMeeting(uid, meeting);
|
||||
return videoIntegrations([credential])[0].updateMeeting(uid, event);
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
|
@ -221,4 +215,4 @@ const deleteMeeting = (credential, uid: String): Promise<any> => {
|
|||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
export {getBusyTimes, createMeeting, updateMeeting, deleteMeeting, VideoMeeting};
|
||||
export {getBusyTimes, createMeeting, updateMeeting, deleteMeeting};
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import type {NextApiRequest, NextApiResponse} from 'next';
|
||||
import prisma from '../../../lib/prisma';
|
||||
import {CalendarEvent, createEvent, updateEvent} from '../../../lib/calendarClient';
|
||||
import createConfirmBookedEmail, {VideoCallData} from "../../../lib/emails/confirm-booked";
|
||||
import async from 'async';
|
||||
import {v5 as uuidv5} from 'uuid';
|
||||
import short from 'short-uuid';
|
||||
import {createMeeting, updateMeeting, VideoMeeting} from "../../../lib/videoClient";
|
||||
import {createMeeting, updateMeeting} from "../../../lib/videoClient";
|
||||
|
||||
const translator = short();
|
||||
|
||||
|
@ -44,18 +43,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
]
|
||||
};
|
||||
|
||||
//TODO Only create meeting if integration exists.
|
||||
const meeting: VideoMeeting = {
|
||||
attendees: [
|
||||
{email: req.body.email, name: req.body.name, timeZone: req.body.timeZone}
|
||||
],
|
||||
endTime: req.body.end,
|
||||
organizer: {email: currentUser.email, name: currentUser.name, timeZone: currentUser.timeZone},
|
||||
startTime: req.body.start,
|
||||
timezone: currentUser.timeZone,
|
||||
title: req.body.eventName + ' with ' + req.body.name,
|
||||
};
|
||||
|
||||
const hashUID: string = translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL));
|
||||
const cancelLink: string = process.env.BASE_URL + '/cancel/' + hashUID;
|
||||
const rescheduleLink:string = process.env.BASE_URL + '/reschedule/' + hashUID;
|
||||
|
@ -108,7 +95,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
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, meeting) // TODO Maybe append links?
|
||||
return await updateMeeting(credential, bookingRefUid, evt) // TODO Maybe append links?
|
||||
}));
|
||||
|
||||
// Clone elements
|
||||
|
@ -147,7 +134,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}));
|
||||
|
||||
results = results.concat(await async.mapLimit(videoCredentials, 5, async (credential) => {
|
||||
const response = await createMeeting(credential, meeting);
|
||||
const response = await createMeeting(credential, evt);
|
||||
return {
|
||||
type: credential.type,
|
||||
response
|
||||
|
@ -157,7 +144,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
referencesToCreate = results.map((result => {
|
||||
return {
|
||||
type: result.type,
|
||||
uid: result.response.id.toString()
|
||||
uid: result.response.createdEvent.id.toString()
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
@ -182,20 +169,12 @@ 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)) {
|
||||
/*if (currentUser.credentials.length === 0 || !results.every((result) => result.disableConfirmationEmail)) {
|
||||
await createConfirmBookedEmail(
|
||||
evt, cancelLink, rescheduleLink, {}, videoCallData
|
||||
);
|
||||
}
|
||||
}*/
|
||||
|
||||
res.status(200).json(results);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue