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), [])
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const createEvent = async (credential, calEvent: CalendarEvent): Promise<any> => {
 | 
			
		||||
    const mail = new EventOwnerMail(calEvent);
 | 
			
		||||
const createEvent = async (credential, calEvent: CalendarEvent, hashUID: string): Promise<any> => {
 | 
			
		||||
    const mail = new EventOwnerMail(calEvent, hashUID);
 | 
			
		||||
    const sentMail = await mail.sendEmail();
 | 
			
		||||
 | 
			
		||||
    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 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;
 | 
			
		||||
  }
 | 
			
		||||
import EventMail from "./EventMail";
 | 
			
		||||
 | 
			
		||||
export default class EventOwnerMail extends EventMail {
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the instance's event as an iCal event in string representation.
 | 
			
		||||
   * @protected
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +13,7 @@ export default class EventOwnerMail {
 | 
			
		|||
      startInputType: 'utc',
 | 
			
		||||
      productId: 'calendso/ics',
 | 
			
		||||
      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')},
 | 
			
		||||
      organizer: {name: this.calEvent.organizer.name, email: this.calEvent.organizer.email},
 | 
			
		||||
      attendees: this.calEvent.attendees.map((attendee: any) => ({name: attendee.name, email: attendee.email})),
 | 
			
		||||
| 
						 | 
				
			
			@ -69,82 +55,33 @@ export default class EventOwnerMail {
 | 
			
		|||
        <br />
 | 
			
		||||
        <strong>Additional notes:</strong><br />
 | 
			
		||||
        ${this.calEvent.description}
 | 
			
		||||
      ` + this.getAdditionalFooter() + `   
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the email text in a plain text representation
 | 
			
		||||
   * by stripping off the HTML tags.
 | 
			
		||||
   * Returns the payload object for the nodemailer.
 | 
			
		||||
   *
 | 
			
		||||
   * @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;
 | 
			
		||||
  protected getNodeMailerPayload(): Object {
 | 
			
		||||
    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,
 | 
			
		||||
      icalEvent: {
 | 
			
		||||
        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(),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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 "";
 | 
			
		||||
  protected printNodeMailerError(error: string): void {
 | 
			
		||||
    console.error("SEND_NEW_EVENT_NOTIFICATION_ERROR", this.calEvent.organizer.email, error);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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 {
 | 
			
		||||
  videoCallData: VideoCallData;
 | 
			
		||||
 | 
			
		||||
  constructor(calEvent: CalendarEvent, videoCallData: VideoCallData) {
 | 
			
		||||
    super(calEvent);
 | 
			
		||||
  constructor(calEvent: CalendarEvent, uid: string, videoCallData: VideoCallData) {
 | 
			
		||||
    super(calEvent, uid);
 | 
			
		||||
    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), [])
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const createMeeting = async (credential, calEvent: CalendarEvent): Promise<any> => {
 | 
			
		||||
const createMeeting = async (credential, calEvent: CalendarEvent, hashUID: string): Promise<any> => {
 | 
			
		||||
    if(!credential) {
 | 
			
		||||
        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,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const mail = new VideoEventOwnerMail(calEvent, videoCallData);
 | 
			
		||||
    const mail = new VideoEventOwnerMail(calEvent, hashUID, videoCallData);
 | 
			
		||||
    const sentMail = await mail.sendEmail();
 | 
			
		||||
 | 
			
		||||
    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 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({
 | 
			
		||||
    where: {
 | 
			
		||||
| 
						 | 
				
			
			@ -90,12 +79,12 @@ 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, appendLinksToEvents(evt))
 | 
			
		||||
      return await updateEvent(credential, bookingRefUid, evt)
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    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)  // TODO Maybe append links?
 | 
			
		||||
      return await updateMeeting(credential, bookingRefUid, evt)
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    // Clone elements
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +115,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
 | 
			
		|||
  } else {
 | 
			
		||||
    // Schedule event
 | 
			
		||||
    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 {
 | 
			
		||||
        type: credential.type,
 | 
			
		||||
        response
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +123,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, evt);
 | 
			
		||||
      const response = await createMeeting(credential, evt, hashUID);
 | 
			
		||||
      return {
 | 
			
		||||
        type: credential.type,
 | 
			
		||||
        response
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue