2021-12-09 15:51:37 +00:00
import { Credential , DestinationCalendar } from "@prisma/client" ;
2021-07-15 01:19:30 +00:00
import async from "async" ;
2021-10-13 11:35:25 +00:00
import merge from "lodash/merge" ;
2021-09-22 19:52:38 +00:00
import { v5 as uuidv5 } from "uuid" ;
2022-06-04 17:23:56 +00:00
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter" ;
2022-03-23 22:00:30 +00:00
import getApps from "@calcom/app-store/utils" ;
import prisma from "@calcom/prisma" ;
2022-06-06 19:49:00 +00:00
import type { AdditionalInformation , CalendarEvent } from "@calcom/types/Calendar" ;
2022-03-23 22:00:30 +00:00
import type {
CreateUpdateResult ,
EventResult ,
PartialBooking ,
PartialReference ,
} from "@calcom/types/EventManager" ;
import type { VideoCallData } from "@calcom/types/VideoApiAdapter" ;
import { createEvent , updateEvent } from "./CalendarManager" ;
2022-04-05 18:03:22 +00:00
import { LocationType } from "./location" ;
2022-03-23 22:00:30 +00:00
import { createMeeting , updateMeeting } from "./videoClient" ;
2021-07-15 01:19:30 +00:00
2022-06-06 19:49:00 +00:00
export type Event = AdditionalInformation & VideoCallData ;
2021-10-26 16:17:24 +00:00
2021-11-26 11:03:43 +00:00
export const isZoom = ( location : string ) : boolean = > {
return location === "integrations:zoom" ;
} ;
export const isDaily = ( location : string ) : boolean = > {
return location === "integrations:daily" ;
} ;
2022-02-03 11:59:02 +00:00
export const isHuddle01 = ( location : string ) : boolean = > {
return location === "integrations:huddle01" ;
} ;
2022-02-04 18:30:52 +00:00
export const isTandem = ( location : string ) : boolean = > {
return location === "integrations:tandem" ;
} ;
2022-03-23 22:00:30 +00:00
export const isTeams = ( location : string ) : boolean = > {
return location === "integrations:office365_video" ;
} ;
2022-03-03 09:54:19 +00:00
export const isJitsi = ( location : string ) : boolean = > {
return location === "integrations:jitsi" ;
} ;
2021-11-26 11:03:43 +00:00
export const isDedicatedIntegration = ( location : string ) : boolean = > {
2022-03-03 09:54:19 +00:00
return (
2022-03-23 22:00:30 +00:00
isZoom ( location ) ||
isDaily ( location ) ||
isHuddle01 ( location ) ||
isTandem ( location ) ||
isJitsi ( location ) ||
isTeams ( location )
2022-03-03 09:54:19 +00:00
) ;
2021-11-26 11:03:43 +00:00
} ;
export const getLocationRequestFromIntegration = ( location : string ) = > {
if (
2022-03-23 22:00:30 +00:00
/** TODO: Handle this dynamically */
2021-11-26 11:03:43 +00:00
location === LocationType . GoogleMeet . valueOf ( ) ||
location === LocationType . Zoom . valueOf ( ) ||
2022-02-03 11:59:02 +00:00
location === LocationType . Daily . valueOf ( ) ||
2022-02-08 22:12:28 +00:00
location === LocationType . Jitsi . valueOf ( ) ||
2022-02-04 18:30:52 +00:00
location === LocationType . Huddle01 . valueOf ( ) ||
2022-03-23 22:00:30 +00:00
location === LocationType . Tandem . valueOf ( ) ||
location === LocationType . Teams . valueOf ( )
2021-11-26 11:03:43 +00:00
) {
const requestId = uuidv5 ( location , uuidv5 . URL ) ;
return {
conferenceData : {
createRequest : {
requestId : requestId ,
} ,
} ,
location ,
} ;
}
return null ;
} ;
export const processLocation = ( event : CalendarEvent ) : CalendarEvent = > {
// If location is set to an integration location
// Build proper transforms for evt object
// Extend evt object with those transformations
if ( event . location ? . includes ( "integration" ) ) {
const maybeLocationRequestObject = getLocationRequestFromIntegration ( event . location ) ;
event = merge ( event , maybeLocationRequestObject ) ;
}
return event ;
} ;
2021-07-25 12:19:49 +00:00
2021-12-09 15:51:37 +00:00
type EventManagerUser = {
credentials : Credential [ ] ;
destinationCalendar : DestinationCalendar | null ;
} ;
2022-03-23 22:00:30 +00:00
2021-07-15 01:19:30 +00:00
export default class EventManager {
2021-12-09 15:51:37 +00:00
calendarCredentials : Credential [ ] ;
videoCredentials : Credential [ ] ;
2021-07-15 01:19:30 +00:00
2021-07-24 20:30:14 +00:00
/ * *
* Takes an array of credentials and initializes a new instance of the EventManager .
*
2022-03-23 22:00:30 +00:00
* @param user
2021-07-24 20:30:14 +00:00
* /
2021-12-09 15:51:37 +00:00
constructor ( user : EventManagerUser ) {
2022-03-23 22:00:30 +00:00
const appCredentials = getApps ( user . credentials ) . flatMap ( ( app ) = > app . credentials ) ;
this . calendarCredentials = appCredentials . filter ( ( cred ) = > cred . type . endsWith ( "_calendar" ) ) ;
this . videoCredentials = appCredentials . filter ( ( cred ) = > cred . type . endsWith ( "_video" ) ) ;
2021-07-15 01:19:30 +00:00
}
2021-07-24 20:30:14 +00:00
/ * *
* Takes a CalendarEvent and creates all necessary integration entries for it .
* When a video integration is chosen as the event ' s location , a video integration
* event will be scheduled for it as well .
*
* @param event
* /
2022-01-27 20:32:53 +00:00
public async create ( event : CalendarEvent ) : Promise < CreateUpdateResult > {
2021-11-26 11:03:43 +00:00
const evt = processLocation ( event ) ;
const isDedicated = evt . location ? isDedicatedIntegration ( evt . location ) : null ;
2021-07-20 18:07:59 +00:00
2021-12-02 17:18:17 +00:00
const results : Array < EventResult > = [ ] ;
2021-09-22 22:43:10 +00:00
// If and only if event type is a dedicated meeting, create a dedicated video meeting.
2021-08-01 21:29:15 +00:00
if ( isDedicated ) {
2021-10-25 13:05:21 +00:00
const result = await this . createVideoEvent ( evt ) ;
2021-11-26 11:03:43 +00:00
if ( result . createdEvent ) {
evt . videoCallData = result . createdEvent ;
2021-09-22 22:43:10 +00:00
}
2021-12-02 17:18:17 +00:00
2021-09-22 22:43:10 +00:00
results . push ( result ) ;
2021-07-15 01:19:30 +00:00
}
2021-12-02 17:18:17 +00:00
// Create the calendar event with the proper video call data
results . push ( . . . ( await this . createAllCalendarEvents ( evt ) ) ) ;
2021-09-22 22:43:10 +00:00
const referencesToCreate : Array < PartialReference > = results . map ( ( result : EventResult ) = > {
2021-10-25 13:05:21 +00:00
return {
type : result . type ,
2021-11-26 11:03:43 +00:00
uid : result.createdEvent?.id.toString ( ) ? ? "" ,
meetingId : result.createdEvent?.id.toString ( ) ,
meetingPassword : result.createdEvent?.password ,
meetingUrl : result.createdEvent?.url ,
2022-05-16 20:20:09 +00:00
externalCalendarId : evt.destinationCalendar?.externalId ,
2021-10-25 13:05:21 +00:00
} ;
2021-07-24 20:24:00 +00:00
} ) ;
return {
results ,
referencesToCreate ,
} ;
2021-07-15 01:19:30 +00:00
}
2021-07-24 20:30:14 +00:00
/ * *
* Takes a calendarEvent and a rescheduleUid and updates the event that has the
* given uid using the data delivered in the given CalendarEvent .
*
* @param event
* /
2022-04-14 21:25:24 +00:00
public async update (
event : CalendarEvent ,
rescheduleUid : string ,
2022-05-30 19:40:29 +00:00
newBookingId? : number ,
rescheduleReason? : string
2022-04-14 21:25:24 +00:00
) : Promise < CreateUpdateResult > {
2021-11-26 11:03:43 +00:00
const evt = processLocation ( event ) ;
2021-11-09 16:27:33 +00:00
if ( ! rescheduleUid ) {
throw new Error ( "You called eventManager.update without an `rescheduleUid`. This should never happen." ) ;
2021-10-25 13:05:21 +00:00
}
2021-07-25 12:19:49 +00:00
2021-07-24 20:24:00 +00:00
// Get details of existing booking.
const booking = await prisma . booking . findFirst ( {
where : {
2021-11-09 16:27:33 +00:00
uid : rescheduleUid ,
2021-07-24 20:24:00 +00:00
} ,
select : {
id : true ,
references : {
2022-03-24 23:29:32 +00:00
// NOTE: id field removed from select as we don't require for deletingMany
// but was giving error on recreate for reschedule, probably because promise.all() didn't finished
2021-07-24 20:24:00 +00:00
select : {
type : true ,
uid : true ,
2021-09-22 22:43:10 +00:00
meetingId : true ,
meetingPassword : true ,
meetingUrl : true ,
2022-05-16 20:20:09 +00:00
externalCalendarId : true ,
2021-07-24 20:24:00 +00:00
} ,
} ,
2021-12-09 15:51:37 +00:00
destinationCalendar : true ,
2022-04-14 21:25:24 +00:00
payment : true ,
2021-07-24 20:24:00 +00:00
} ,
} ) ;
2021-10-25 13:05:21 +00:00
if ( ! booking ) {
throw new Error ( "booking not found" ) ;
}
2022-05-30 19:40:29 +00:00
// Add reschedule reason to new booking
await prisma . booking . update ( {
where : {
id : newBookingId ,
} ,
data : {
cancellationReason : rescheduleReason ,
} ,
} ) ;
2021-11-26 11:03:43 +00:00
const isDedicated = evt . location ? isDedicatedIntegration ( evt . location ) : null ;
2021-12-02 21:41:09 +00:00
const results : Array < EventResult > = [ ] ;
2021-09-22 22:43:10 +00:00
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
2021-08-01 21:38:38 +00:00
if ( isDedicated ) {
2021-10-25 13:05:21 +00:00
const result = await this . updateVideoEvent ( evt , booking ) ;
2022-02-10 10:44:46 +00:00
const [ updatedEvent ] = Array . isArray ( result . updatedEvent ) ? result . updatedEvent : [ result . updatedEvent ] ;
if ( updatedEvent ) {
evt . videoCallData = updatedEvent ;
evt . location = updatedEvent . url ;
2021-09-22 22:43:10 +00:00
}
results . push ( result ) ;
2021-07-15 01:19:30 +00:00
}
2021-11-26 11:03:43 +00:00
2021-12-02 21:41:09 +00:00
// Update all calendar events.
results . push ( . . . ( await this . updateAllCalendarEvents ( evt , booking ) ) ) ;
2022-04-14 21:25:24 +00:00
const bookingPayment = booking ? . payment ;
// Updating all payment to new
if ( bookingPayment && newBookingId ) {
const paymentIds = bookingPayment . map ( ( payment ) = > payment . id ) ;
await prisma . payment . updateMany ( {
where : {
id : {
in : paymentIds ,
} ,
} ,
data : {
bookingId : newBookingId ,
} ,
} ) ;
}
2021-07-24 20:24:00 +00:00
// Now we can delete the old booking and its references.
const bookingReferenceDeletes = prisma . bookingReference . deleteMany ( {
where : {
bookingId : booking.id ,
} ,
} ) ;
const attendeeDeletes = prisma . attendee . deleteMany ( {
where : {
bookingId : booking.id ,
} ,
} ) ;
2021-10-25 13:05:21 +00:00
2021-11-09 16:27:33 +00:00
const bookingDeletes = prisma . booking . delete ( {
where : {
id : booking.id ,
} ,
} ) ;
2021-07-24 20:24:00 +00:00
// Wait for all deletions to be applied.
await Promise . all ( [ bookingReferenceDeletes , attendeeDeletes , bookingDeletes ] ) ;
return {
results ,
referencesToCreate : [ . . . booking . references ] ,
} ;
2021-07-15 01:19:30 +00:00
}
/ * *
* Creates event entries for all calendar integrations given in the credentials .
2021-07-20 18:07:59 +00:00
* When noMail is true , no mails will be sent . This is used when the event is
* a video meeting because then the mail containing the video credentials will be
* more important than the mails created for these bare calendar events .
2021-07-15 01:19:30 +00:00
*
2021-07-25 15:05:18 +00:00
* When the optional uid is set , it will be used instead of the auto generated uid .
*
2021-07-15 01:19:30 +00:00
* @param event
2021-07-20 18:07:59 +00:00
* @param noMail
2021-07-15 01:19:30 +00:00
* @private
* /
2021-11-26 11:03:43 +00:00
private async createAllCalendarEvents ( event : CalendarEvent ) : Promise < Array < EventResult > > {
2021-12-09 15:51:37 +00:00
/** Can I use destinationCalendar here? */
/* How can I link a DC to a cred? */
if ( event . destinationCalendar ) {
const destinationCalendarCredentials = this . calendarCredentials . filter (
( c ) = > c . type === event . destinationCalendar ? . integration
) ;
return Promise . all ( destinationCalendarCredentials . map ( async ( c ) = > await createEvent ( c , event ) ) ) ;
}
2022-01-21 21:35:31 +00:00
/ * *
* Not ideal but , if we don ' t find a destination calendar ,
* fallback to the first connected calendar
* /
2021-12-09 15:51:37 +00:00
const [ credential ] = this . calendarCredentials ;
if ( ! credential ) {
2021-11-11 05:50:56 +00:00
return [ ] ;
}
2021-12-09 15:51:37 +00:00
return [ await createEvent ( credential , event ) ] ;
2021-07-15 01:19:30 +00:00
}
2021-07-24 20:30:14 +00:00
/ * *
* Checks which video integration is needed for the event ' s location and returns
* credentials for that - if existing .
* @param event
* @private
* /
2021-10-07 16:12:39 +00:00
2021-07-15 01:19:30 +00:00
private getVideoCredential ( event : CalendarEvent ) : Credential | undefined {
2021-10-25 13:05:21 +00:00
if ( ! event . location ) {
return undefined ;
}
2022-05-02 20:39:35 +00:00
/** @fixme potential bug since Google Meet are saved as `integrations:google:meet` and there are no `google:meet` type in our DB */
2021-07-15 01:19:30 +00:00
const integrationName = event . location . replace ( "integrations:" , "" ) ;
2022-06-04 17:23:56 +00:00
let videoCredential = this . videoCredentials . find ( ( credential : Credential ) = >
credential . type . includes ( integrationName )
) ;
2021-10-07 16:12:39 +00:00
2022-06-04 17:23:56 +00:00
/ * *
* This might happen if someone tries to use a location with a missing credential , so we fallback to Cal Video .
* @todo remove location from event types that has missing credentials
* * /
if ( ! videoCredential ) videoCredential = FAKE_DAILY_CREDENTIAL ;
return videoCredential ;
2021-07-15 01:19:30 +00:00
}
/ * *
* Creates a video event entry for the selected integration location .
*
2021-07-25 15:05:18 +00:00
* When optional uid is set , it will be used instead of the auto generated uid .
*
2021-07-15 01:19:30 +00:00
* @param event
* @private
* /
2022-01-27 20:32:53 +00:00
private createVideoEvent ( event : CalendarEvent ) : Promise < EventResult > {
2021-07-15 01:19:30 +00:00
const credential = this . getVideoCredential ( event ) ;
2021-10-26 16:17:24 +00:00
if ( credential ) {
2021-10-25 13:05:21 +00:00
return createMeeting ( credential , event ) ;
2021-07-15 01:19:30 +00:00
} else {
2022-04-26 11:31:57 +00:00
return Promise . reject (
` No suitable credentials given for the requested integration name: ${ event . location } `
) ;
2021-07-15 01:19:30 +00:00
}
}
2021-07-20 18:07:59 +00:00
/ * *
* Updates the event entries for all calendar integrations given in the credentials .
* When noMail is true , no mails will be sent . This is used when the event is
* a video meeting because then the mail containing the video credentials will be
* more important than the mails created for these bare calendar events .
*
* @param event
* @param booking
* @private
* /
2021-07-15 01:19:30 +00:00
private updateAllCalendarEvents (
event : CalendarEvent ,
2021-11-26 11:03:43 +00:00
booking : PartialBooking
2021-07-15 01:19:30 +00:00
) : Promise < Array < EventResult > > {
2021-11-26 11:03:43 +00:00
return async . mapLimit ( this . calendarCredentials , 5 , async ( credential : Credential ) = > {
2022-05-12 09:33:15 +00:00
// HACK:
// Right now if two calendars are connected and a booking is created it has two bookingReferences, one is having uid null and the other is having valid uid.
// I don't know why yet - But we should work on fixing that. But even after the fix as there can be multiple references in an existing booking the following ref.uid check would still be required
// We should ignore the one with uid null, the other one is valid.
// Also, we should store(if not already) that which is the calendarCredential for the valid bookingReference, instead of going through all credentials one by one
2021-10-25 13:05:21 +00:00
const bookingRefUid = booking
2022-05-12 09:33:15 +00:00
? booking . references . filter ( ( ref ) = > ref . type === credential . type && ! ! ref . uid ) [ 0 ] ? . uid
2021-10-25 13:05:21 +00:00
: null ;
2021-11-26 11:03:43 +00:00
2022-05-16 20:20:09 +00:00
const bookingExternalCalendarId = booking . references
? booking . references . filter ( ( ref ) = > ref . type === credential . type ) [ 0 ] . externalCalendarId
: null ;
return updateEvent ( credential , event , bookingRefUid , bookingExternalCalendarId ! ) ;
2021-07-15 01:19:30 +00:00
} ) ;
}
2021-07-20 18:07:59 +00:00
/ * *
* Updates a single video event .
*
* @param event
* @param booking
* @private
* /
2021-07-15 01:19:30 +00:00
private updateVideoEvent ( event : CalendarEvent , booking : PartialBooking ) {
const credential = this . getVideoCredential ( event ) ;
2021-10-26 16:17:24 +00:00
if ( credential ) {
2021-10-25 13:05:21 +00:00
const bookingRef = booking ? booking . references . filter ( ( ref ) = > ref . type === credential . type ) [ 0 ] : null ;
2021-11-26 11:03:43 +00:00
return updateMeeting ( credential , event , bookingRef ) ;
2021-07-15 01:19:30 +00:00
} else {
2022-04-26 11:31:57 +00:00
return Promise . reject (
` No suitable credentials given for the requested integration name: ${ event . location } `
) ;
2021-07-15 01:19:30 +00:00
}
}
2022-04-14 21:25:24 +00:00
/ * *
* Update event to set a cancelled event placeholder on users calendar
* remove if virtual calendar is already done and user availability its read from there
* and not only in their calendars
* @param event
* @param booking
* @public
* /
public async updateAndSetCancelledPlaceholder ( event : CalendarEvent , booking : PartialBooking ) {
await this . updateAllCalendarEvents ( event , booking ) ;
}
2021-07-15 01:19:30 +00:00
}