Introduced more classes for event mails
This commit is contained in:
		
							parent
							
								
									e37dd017c8
								
							
						
					
					
						commit
						04e0b55b51
					
				
					 9 changed files with 262 additions and 200 deletions
				
			
		|  | @ -324,8 +324,8 @@ const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all( | ||||||
|     (results) => results.reduce((acc, availability) => acc.concat(availability), []) |     (results) => results.reduce((acc, availability) => acc.concat(availability), []) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const createEvent = async (credential, calEvent: CalendarEvent): Promise<any> => { | const createEvent = async (credential, calEvent: CalendarEvent, hashUID: string): Promise<any> => { | ||||||
|     const mail = new EventOwnerMail(calEvent); |     const mail = new EventOwnerMail(calEvent, hashUID); | ||||||
|     const sentMail = await mail.sendEmail(); |     const sentMail = await mail.sendEmail(); | ||||||
| 
 | 
 | ||||||
|     const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; |     const creationResult = credential ? await calendars([credential])[0].createEvent(calEvent) : null; | ||||||
|  |  | ||||||
							
								
								
									
										55
									
								
								lib/emails/EventAttendeeMail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								lib/emails/EventAttendeeMail.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | ||||||
|  | import dayjs, {Dayjs} from "dayjs"; | ||||||
|  | import EventMail from "./EventMail"; | ||||||
|  | 
 | ||||||
|  | export default class EventAttendeeMail extends EventMail { | ||||||
|  |   /** | ||||||
|  |    * Returns the email text as HTML representation. | ||||||
|  |    * | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected getHtmlRepresentation(): string { | ||||||
|  |     return ` | ||||||
|  |     <div> | ||||||
|  |       Hi ${this.calEvent.attendees[0].name},<br /> | ||||||
|  |       <br /> | ||||||
|  |       Your ${this.calEvent.type} with ${this.calEvent.organizer.name} at ${this.getInviteeStart().format('h:mma')}  | ||||||
|  |       (${this.calEvent.attendees[0].timeZone}) on ${this.getInviteeStart().format('dddd, LL')} is scheduled.<br /> | ||||||
|  |       <br />` + this.getAdditionalBody() + (
 | ||||||
|  |         this.calEvent.location ? `<strong>Location:</strong> ${this.calEvent.location}<br /><br />` : '' | ||||||
|  |       ) + | ||||||
|  |       `<strong>Additional notes:</strong><br />
 | ||||||
|  |       ${this.calEvent.description} | ||||||
|  |       ` + this.getAdditionalFooter() + ` | ||||||
|  |     </div> | ||||||
|  |   `;
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 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: `Confirmed: ${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_BOOKING_CONFIRMATION_ERROR", this.calEvent.attendees[0].email, error); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the inviteeStart value used at multiple points. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   private getInviteeStart(): Dayjs { | ||||||
|  |     return <Dayjs>dayjs(this.calEvent.startTime).tz(this.calEvent.attendees[0].timeZone); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										135
									
								
								lib/emails/EventMail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								lib/emails/EventMail.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,135 @@ | ||||||
|  | import {CalendarEvent} from "../calendarClient"; | ||||||
|  | import {serverConfig} from "../serverConfig"; | ||||||
|  | import nodemailer from 'nodemailer'; | ||||||
|  | 
 | ||||||
|  | export default abstract class EventMail { | ||||||
|  |   calEvent: CalendarEvent; | ||||||
|  |   uid: string; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * An EventMail always consists of a CalendarEvent | ||||||
|  |    * that stores the very basic data of the event (like date, title etc). | ||||||
|  |    * It also needs the UID of the stored booking in our database. | ||||||
|  |    * | ||||||
|  |    * @param calEvent | ||||||
|  |    * @param uid | ||||||
|  |    */ | ||||||
|  |   constructor(calEvent: CalendarEvent, uid: string) { | ||||||
|  |     this.calEvent = calEvent; | ||||||
|  |     this.uid = uid; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the email text as HTML representation. | ||||||
|  |    * | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected abstract getHtmlRepresentation(): string; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * 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, ''); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the payload object for the nodemailer. | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected abstract getNodeMailerPayload(): Object; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Sends the email to the event attendant and returns a Promise. | ||||||
|  |    */ | ||||||
|  |   public sendEmail(): Promise<any> { | ||||||
|  |     return new Promise((resolve, reject) => nodemailer.createTransport(this.getMailerOptions().transport).sendMail( | ||||||
|  |       this.getNodeMailerPayload(), | ||||||
|  |       (error, info) => { | ||||||
|  |         if (error) { | ||||||
|  |           this.printNodeMailerError(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. Leave it to an empty | ||||||
|  |    * string if not desired. | ||||||
|  |    * | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected getAdditionalBody(): string { | ||||||
|  |     return ""; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Prints out the desired information when an error | ||||||
|  |    * occured while sending the mail. | ||||||
|  |    * @param error | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected abstract printNodeMailerError(error: string): void; | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns a link to reschedule the given booking. | ||||||
|  |    * | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected getRescheduleLink(): string { | ||||||
|  |     return process.env.BASE_URL + '/reschedule/' + this.uid; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns a link to cancel the given booking. | ||||||
|  |    * | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected getCancelLink(): string { | ||||||
|  |     return process.env.BASE_URL + '/cancel/' + this.uid; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Defines a footer that will be appended to the email. | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected getAdditionalFooter(): string { | ||||||
|  |     return ` | ||||||
|  |       <br/> | ||||||
|  |       Need to change this event?<br /> | ||||||
|  |       Cancel: <a href="${this.getCancelLink()}">${this.getCancelLink()}</a><br /> | ||||||
|  |       Reschedule: <a href="${this.getRescheduleLink()}">${this.getRescheduleLink()}</a> | ||||||
|  |     `;
 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -1,22 +1,8 @@ | ||||||
| import {CalendarEvent} from "../calendarClient"; |  | ||||||
| import {createEvent} from "ics"; | import {createEvent} from "ics"; | ||||||
| import dayjs, {Dayjs} from "dayjs"; | import dayjs, {Dayjs} from "dayjs"; | ||||||
| import {serverConfig} from "../serverConfig"; | import EventMail from "./EventMail"; | ||||||
| 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; |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|  | export default class EventOwnerMail extends EventMail { | ||||||
|   /** |   /** | ||||||
|    * Returns the instance's event as an iCal event in string representation. |    * Returns the instance's event as an iCal event in string representation. | ||||||
|    * @protected |    * @protected | ||||||
|  | @ -27,7 +13,7 @@ export default class EventOwnerMail { | ||||||
|       startInputType: 'utc', |       startInputType: 'utc', | ||||||
|       productId: 'calendso/ics', |       productId: 'calendso/ics', | ||||||
|       title: `${this.calEvent.type} with ${this.calEvent.attendees[0].name}`, |       title: `${this.calEvent.type} with ${this.calEvent.attendees[0].name}`, | ||||||
|       description: this.calEvent.description + this.stripHtml(this.getAdditionalBody()), |       description: this.calEvent.description + this.stripHtml(this.getAdditionalBody()) + this.stripHtml(this.getAdditionalFooter()), | ||||||
|       duration: {minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), 'minute')}, |       duration: {minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), 'minute')}, | ||||||
|       organizer: {name: this.calEvent.organizer.name, email: this.calEvent.organizer.email}, |       organizer: {name: this.calEvent.organizer.name, email: this.calEvent.organizer.email}, | ||||||
|       attendees: this.calEvent.attendees.map((attendee: any) => ({name: attendee.name, email: attendee.email})), |       attendees: this.calEvent.attendees.map((attendee: any) => ({name: attendee.name, email: attendee.email})), | ||||||
|  | @ -69,82 +55,33 @@ export default class EventOwnerMail { | ||||||
|         <br /> |         <br /> | ||||||
|         <strong>Additional notes:</strong><br /> |         <strong>Additional notes:</strong><br /> | ||||||
|         ${this.calEvent.description} |         ${this.calEvent.description} | ||||||
|  |       ` + this.getAdditionalFooter() + `    | ||||||
|       </div> |       </div> | ||||||
|     `;
 |     `;
 | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * Returns the email text in a plain text representation |    * Returns the payload object for the nodemailer. | ||||||
|    * by stripping off the HTML tags. |  | ||||||
|    * |    * | ||||||
|    * @protected |    * @protected | ||||||
|    */ |    */ | ||||||
|   protected getPlainTextRepresentation(): string { |   protected getNodeMailerPayload(): Object { | ||||||
|     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); |     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 { |     return { | ||||||
|       transport: serverConfig.transport, |       icalEvent: { | ||||||
|       from: serverConfig.from, |         filename: 'event.ics', | ||||||
|  |         content: this.getiCalEventAsString(), | ||||||
|  |       }, | ||||||
|  |       from: `Calendso <${this.getMailerOptions().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(), | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   protected printNodeMailerError(error: string): void { | ||||||
|    * Can be used to include additional HTML or plain text |     console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error); | ||||||
|    * content into the mail body and calendar event description. |  | ||||||
|    * Leave it to an empty string if not desired. |  | ||||||
|    * |  | ||||||
|    * @protected |  | ||||||
|    */ |  | ||||||
|   protected getAdditionalBody(): string { |  | ||||||
|     return ""; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										45
									
								
								lib/emails/VideoEventAttendeeMail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								lib/emails/VideoEventAttendeeMail.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | import {VideoCallData} from "./confirm-booked"; | ||||||
|  | import {CalendarEvent} from "../calendarClient"; | ||||||
|  | import EventAttendeeMail from "./EventAttendeeMail"; | ||||||
|  | 
 | ||||||
|  | export default class VideoEventAttendeeMail extends EventAttendeeMail { | ||||||
|  |   videoCallData: VideoCallData; | ||||||
|  | 
 | ||||||
|  |   constructor(calEvent: CalendarEvent, uid: string, videoCallData: VideoCallData) { | ||||||
|  |     super(calEvent, uid); | ||||||
|  |     this.videoCallData = videoCallData; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private getIntegrationName(): string { | ||||||
|  |     //TODO: When there are more complex integration type strings, we should consider using an extra field in the DB for that.
 | ||||||
|  |     const nameProto = this.videoCallData.type.split("_")[0]; | ||||||
|  |     return nameProto.charAt(0).toUpperCase() + nameProto.slice(1); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private getFormattedMeetingId(): string { | ||||||
|  |     switch(this.videoCallData.type) { | ||||||
|  |       case 'zoom_video': | ||||||
|  |         const strId = this.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 this.videoCallData.id.toString(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Adds the video call information to the mail body. | ||||||
|  |    * | ||||||
|  |    * @protected | ||||||
|  |    */ | ||||||
|  |   protected getAdditionalBody(): string { | ||||||
|  |     return ` | ||||||
|  |       <strong>Video call provider:</strong> ${this.getIntegrationName()}<br /> | ||||||
|  |       <strong>Meeting ID:</strong> ${this.getFormattedMeetingId()}<br /> | ||||||
|  |       <strong>Meeting Password:</strong> ${this.videoCallData.password}<br /> | ||||||
|  |       <strong>Meeting URL:</strong> <a href="${this.videoCallData.url}">${this.videoCallData.url}</a><br /> | ||||||
|  |     `;
 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -5,8 +5,8 @@ import {formattedId, integrationTypeToName, VideoCallData} from "./confirm-booke | ||||||
| export default class VideoEventOwnerMail extends EventOwnerMail { | export default class VideoEventOwnerMail extends EventOwnerMail { | ||||||
|   videoCallData: VideoCallData; |   videoCallData: VideoCallData; | ||||||
| 
 | 
 | ||||||
|   constructor(calEvent: CalendarEvent, videoCallData: VideoCallData) { |   constructor(calEvent: CalendarEvent, uid: string, videoCallData: VideoCallData) { | ||||||
|     super(calEvent); |     super(calEvent, uid); | ||||||
|     this.videoCallData = videoCallData; |     this.videoCallData = videoCallData; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,99 +0,0 @@ | ||||||
| 
 |  | ||||||
| import nodemailer from 'nodemailer'; |  | ||||||
| import dayjs, { Dayjs } from "dayjs"; |  | ||||||
| import localizedFormat from 'dayjs/plugin/localizedFormat'; |  | ||||||
| import utc from 'dayjs/plugin/utc'; |  | ||||||
| import timezone from 'dayjs/plugin/timezone'; |  | ||||||
| import toArray from 'dayjs/plugin/toArray'; |  | ||||||
| import { createEvent } from 'ics'; |  | ||||||
| import { CalendarEvent } from '../calendarClient'; |  | ||||||
| import { serverConfig } from '../serverConfig'; |  | ||||||
| 
 |  | ||||||
| dayjs.extend(localizedFormat); |  | ||||||
| dayjs.extend(utc); |  | ||||||
| dayjs.extend(timezone); |  | ||||||
| dayjs.extend(toArray); |  | ||||||
| 
 |  | ||||||
| export default function createNewEventEmail(calEvent: CalendarEvent, options: any = {}) { |  | ||||||
|   return sendEmail(calEvent, { |  | ||||||
|     provider: { |  | ||||||
|       transport: serverConfig.transport, |  | ||||||
|       from: serverConfig.from, |  | ||||||
|     }, |  | ||||||
|     ...options |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const icalEventAsString = (calEvent: CalendarEvent): string => { |  | ||||||
|   const icsEvent = createEvent({ |  | ||||||
|     start: dayjs(calEvent.startTime).utc().toArray().slice(0, 6), |  | ||||||
|     startInputType: 'utc', |  | ||||||
|     productId: 'calendso/ics', |  | ||||||
|     title: `${calEvent.type} with ${calEvent.attendees[0].name}`, |  | ||||||
|     description: calEvent.description, |  | ||||||
|     duration: { minutes: dayjs(calEvent.endTime).diff(dayjs(calEvent.startTime), 'minute') }, |  | ||||||
|     organizer: { name: calEvent.organizer.name, email: calEvent.organizer.email }, |  | ||||||
|     attendees: calEvent.attendees.map( (attendee: any) => ({ name: attendee.name, email: attendee.email }) ), |  | ||||||
|     status: "CONFIRMED", |  | ||||||
|   }); |  | ||||||
|   if (icsEvent.error) { |  | ||||||
|     throw icsEvent.error; |  | ||||||
|   } |  | ||||||
|   return icsEvent.value; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const sendEmail = (calEvent: CalendarEvent, { |  | ||||||
|   provider, |  | ||||||
| }) => new Promise( (resolve, reject) => { |  | ||||||
|   const { transport, from } = provider; |  | ||||||
|   const organizerStart: Dayjs = <Dayjs>dayjs(calEvent.startTime).tz(calEvent.organizer.timeZone); |  | ||||||
|   nodemailer.createTransport(transport).sendMail( |  | ||||||
|     { |  | ||||||
|       icalEvent: { |  | ||||||
|         filename: 'event.ics', |  | ||||||
|         content: icalEventAsString(calEvent), |  | ||||||
|       }, |  | ||||||
|       from: `Calendso <${from}>`, |  | ||||||
|       to: calEvent.organizer.email, |  | ||||||
|       subject: `New event: ${calEvent.attendees[0].name} - ${organizerStart.format('LT dddd, LL')} - ${calEvent.type}`, |  | ||||||
|       html: html(calEvent), |  | ||||||
|       text: text(calEvent), |  | ||||||
|     }, |  | ||||||
|     (error) => { |  | ||||||
|       if (error) { |  | ||||||
|         console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", calEvent.organizer.email, error); |  | ||||||
|         return reject(new Error(error)); |  | ||||||
|       } |  | ||||||
|       return resolve(); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const html = (evt: CalendarEvent) => ` |  | ||||||
|   <div> |  | ||||||
|     Hi ${evt.organizer.name},<br /> |  | ||||||
|     <br /> |  | ||||||
|     A new event has been scheduled.<br /> |  | ||||||
|     <br /> |  | ||||||
|     <strong>Event Type:</strong><br /> |  | ||||||
|     ${evt.type}<br /> |  | ||||||
|     <br /> |  | ||||||
|     <strong>Invitee Email:</strong><br /> |  | ||||||
|     <a href="mailto:${evt.attendees[0].email}">${evt.attendees[0].email}</a><br /> |  | ||||||
|     <br />` +
 |  | ||||||
|     ( |  | ||||||
|       evt.location ? ` |  | ||||||
|         <strong>Location:</strong><br /> |  | ||||||
|         ${evt.location}<br /> |  | ||||||
|         <br /> |  | ||||||
|       ` : ''
 |  | ||||||
|     ) + |  | ||||||
|     `<strong>Invitee Time Zone:</strong><br />
 |  | ||||||
|     ${evt.attendees[0].timeZone}<br /> |  | ||||||
|     <br /> |  | ||||||
|     <strong>Additional notes:</strong><br /> |  | ||||||
|     ${evt.description} |  | ||||||
|   </div> |  | ||||||
| `;
 |  | ||||||
| 
 |  | ||||||
| // just strip all HTML and convert <br /> to \n
 |  | ||||||
| const text = (evt: CalendarEvent) => html(evt).replace('<br />', "\n").replace(/<[^>]+>/g, ''); |  | ||||||
|  | @ -176,7 +176,7 @@ const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all( | ||||||
|     (results) => results.reduce((acc, availability) => acc.concat(availability), []) |     (results) => results.reduce((acc, availability) => acc.concat(availability), []) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| const createMeeting = async (credential, calEvent: CalendarEvent): Promise<any> => { | const createMeeting = async (credential, calEvent: CalendarEvent, hashUID: string): Promise<any> => { | ||||||
|     if(!credential) { |     if(!credential) { | ||||||
|         throw new Error("Credentials must be set! Video platforms are optional, so this method shouldn't even be called."); |         throw new Error("Credentials must be set! Video platforms are optional, so this method shouldn't even be called."); | ||||||
|     } |     } | ||||||
|  | @ -190,7 +190,7 @@ const createMeeting = async (credential, calEvent: CalendarEvent): Promise<any> | ||||||
|         url: creationResult.join_url, |         url: creationResult.join_url, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     const mail = new VideoEventOwnerMail(calEvent, videoCallData); |     const mail = new VideoEventOwnerMail(calEvent, hashUID, videoCallData); | ||||||
|     const sentMail = await mail.sendEmail(); |     const sentMail = await mail.sendEmail(); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|  |  | ||||||
|  | @ -44,17 +44,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const hashUID: string = translator.fromUUID(uuidv5(JSON.stringify(evt), uuidv5.URL)); |   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; |  | ||||||
|   const appendLinksToEvents = (event: CalendarEvent) => { |  | ||||||
|     const eventCopy = {...event}; |  | ||||||
|     eventCopy.description += "\n\n" |  | ||||||
|       + "Need to change this event?\n" |  | ||||||
|       + "Cancel: " + cancelLink + "\n" |  | ||||||
|       + "Reschedule:" + rescheduleLink; |  | ||||||
| 
 |  | ||||||
|     return eventCopy; |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   const eventType = await prisma.eventType.findFirst({ |   const eventType = await prisma.eventType.findFirst({ | ||||||
|     where: { |     where: { | ||||||
|  | @ -90,12 +79,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|     // Use all integrations
 |     // Use all integrations
 | ||||||
|     results = results.concat(await async.mapLimit(calendarCredentials, 5, async (credential) => { |     results = results.concat(await async.mapLimit(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 await updateEvent(credential, bookingRefUid, appendLinksToEvents(evt)) |       return await updateEvent(credential, bookingRefUid, evt) | ||||||
|     })); |     })); | ||||||
| 
 | 
 | ||||||
|     results = results.concat(await async.mapLimit(videoCredentials, 5, async (credential) => { |     results = results.concat(await async.mapLimit(videoCredentials, 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 await updateMeeting(credential, bookingRefUid, evt)  // TODO Maybe append links?
 |       return await updateMeeting(credential, bookingRefUid, evt) | ||||||
|     })); |     })); | ||||||
| 
 | 
 | ||||||
|     // Clone elements
 |     // Clone elements
 | ||||||
|  | @ -126,7 +115,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|   } else { |   } else { | ||||||
|     // Schedule event
 |     // Schedule event
 | ||||||
|     results = results.concat(await async.mapLimit(calendarCredentials, 5, async (credential) => { |     results = results.concat(await async.mapLimit(calendarCredentials, 5, async (credential) => { | ||||||
|       const response = await createEvent(credential, appendLinksToEvents(evt)); |       const response = await createEvent(credential, evt, hashUID); | ||||||
|       return { |       return { | ||||||
|         type: credential.type, |         type: credential.type, | ||||||
|         response |         response | ||||||
|  | @ -134,7 +123,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | ||||||
|     })); |     })); | ||||||
| 
 | 
 | ||||||
|     results = results.concat(await async.mapLimit(videoCredentials, 5, async (credential) => { |     results = results.concat(await async.mapLimit(videoCredentials, 5, async (credential) => { | ||||||
|       const response = await createMeeting(credential, evt); |       const response = await createMeeting(credential, evt, hashUID); | ||||||
|       return { |       return { | ||||||
|         type: credential.type, |         type: credential.type, | ||||||
|         response |         response | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 nicolas
						nicolas