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-09-05 19:13:49 +00:00
import { z } from "zod" ;
2021-09-22 19:52:38 +00:00
2022-06-04 17:23:56 +00:00
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter" ;
2022-08-26 00:48:50 +00:00
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations" ;
2022-03-23 22:00:30 +00:00
import getApps from "@calcom/app-store/utils" ;
import prisma from "@calcom/prisma" ;
2022-09-05 19:13:49 +00:00
import { createdEventSchema } from "@calcom/prisma/zod-utils" ;
2022-06-17 18:34:41 +00:00
import type { AdditionalInformation , CalendarEvent , NewCalendarEventType } from "@calcom/types/Calendar" ;
import type { Event } from "@calcom/types/Event" ;
2022-07-18 15:37:47 +00:00
import type {
CreateUpdateResult ,
EventResult ,
PartialBooking ,
PartialReference ,
} from "@calcom/types/EventManager" ;
2022-03-23 22:00:30 +00:00
import { createEvent , updateEvent } from "./CalendarManager" ;
import { createMeeting , updateMeeting } from "./videoClient" ;
2021-07-15 01:19:30 +00:00
2021-11-26 11:03:43 +00:00
export const isDedicatedIntegration = ( location : string ) : boolean = > {
2022-08-26 00:48:50 +00:00
return location !== "integrations:google:meet" && location . includes ( "integrations:" ) ;
2021-11-26 11:03:43 +00:00
} ;
export const getLocationRequestFromIntegration = ( location : string ) = > {
2022-08-26 00:48:50 +00:00
const eventLocationType = getEventLocationTypeFromApp ( location ) ;
if ( eventLocationType ) {
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
2022-08-26 00:48:50 +00:00
// TODO: Rely on linkType:"dynamic" here. static links don't send their type. They send their URL directly.
2021-11-26 11:03:43 +00:00
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
2022-09-05 19:13:49 +00:00
type createdEventSchema = z . infer < typeof createdEventSchema > ;
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
2022-06-17 18:34:41 +00:00
const results : Array < EventResult < Exclude < Event , AdditionalInformation > >> = [ ] ;
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 ) ) ) ;
2022-06-17 18:34:41 +00:00
const referencesToCreate = results . map ( ( result ) = > {
2022-09-05 19:13:49 +00:00
let createdEventObj : createdEventSchema | null = null ;
if ( typeof result ? . createdEvent === "string" ) {
createdEventObj = createdEventSchema . parse ( JSON . parse ( result . createdEvent ) ) ;
}
2021-10-25 13:05:21 +00:00
return {
type : result . type ,
2022-09-05 19:13:49 +00:00
uid : createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString ( ) ? ? "" ,
meetingId : createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString ( ) ,
meetingPassword : createdEventObj ? createdEventObj.password : result.createdEvent?.password ,
meetingUrl : createdEventObj ? createdEventObj.onlineMeetingUrl : result.createdEvent?.url ,
2022-08-08 19:38:02 +00:00
externalCalendarId : evt.destinationCalendar?.externalId ,
credentialId : evt.destinationCalendar?.credentialId ,
} ;
} ) ;
return {
results ,
referencesToCreate ,
} ;
}
public async updateLocation ( event : CalendarEvent , booking : PartialBooking ) : Promise < CreateUpdateResult > {
const evt = processLocation ( event ) ;
const isDedicated = evt . location ? isDedicatedIntegration ( evt . location ) : null ;
const results : Array < EventResult < Exclude < Event , AdditionalInformation > >> = [ ] ;
// If and only if event type is a dedicated meeting, create a dedicated video meeting.
if ( isDedicated ) {
const result = await this . createVideoEvent ( evt ) ;
if ( result . createdEvent ) {
evt . videoCallData = result . createdEvent ;
}
results . push ( result ) ;
}
// Update the calendar event with the proper video call data
2022-08-12 20:46:40 +00:00
const calendarReference = booking . references . find ( ( reference ) = > reference . type . includes ( "_calendar" ) ) ;
if ( calendarReference ) {
results . push ( . . . ( await this . updateAllCalendarEvents ( evt , booking ) ) ) ;
}
2022-08-08 19:38:02 +00:00
const referencesToCreate = results . map ( ( result ) = > {
return {
type : result . type ,
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 ,
2022-07-18 15:37:47 +00:00
credentialId : evt.destinationCalendar?.credentialId ,
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-07-12 13:16:34 +00:00
public async reschedule (
2022-04-14 21:25:24 +00:00
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 ,
2022-07-18 15:37:47 +00:00
userId : true ,
2021-07-24 20:24:00 +00:00
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 ,
2022-07-18 15:37:47 +00:00
credentialId : 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 ;
2022-06-17 18:34:41 +00:00
const results : Array < EventResult < Event > > = [ ] ;
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
}
2022-07-12 13:16:34 +00:00
public async updateCalendarAttendees ( event : CalendarEvent , booking : PartialBooking ) {
await this . updateAllCalendarEvents ( event , booking ) ;
}
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
* /
2022-06-17 18:34:41 +00:00
private async createAllCalendarEvents ( event : CalendarEvent ) {
2021-12-09 15:51:37 +00:00
/** Can I use destinationCalendar here? */
/* How can I link a DC to a cred? */
2022-10-15 17:02:24 +00:00
let createdEvents : EventResult < NewCalendarEventType > [ ] = [ ] ;
2021-12-09 15:51:37 +00:00
if ( event . destinationCalendar ) {
2022-07-01 20:55:27 +00:00
if ( event . destinationCalendar . credentialId ) {
const credential = await prisma . credential . findFirst ( {
where : {
id : event.destinationCalendar.credentialId ,
} ,
} ) ;
if ( credential ) {
2022-10-15 17:02:24 +00:00
createdEvents . push ( await createEvent ( credential , event ) ) ;
2022-07-01 20:55:27 +00:00
}
2022-10-15 17:02:24 +00:00
} else {
const destinationCalendarCredentials = this . calendarCredentials . filter (
( c ) = > c . type === event . destinationCalendar ? . integration
) ;
createdEvents = createdEvents . concat (
await Promise . all ( destinationCalendarCredentials . map ( async ( c ) = > await createEvent ( c , event ) ) )
) ;
2022-07-01 20:55:27 +00:00
}
2022-10-15 17:02:24 +00:00
} else {
/ * *
* Not ideal but , if we don ' t find a destination calendar ,
* fallback to the first connected calendar
* /
const [ credential ] = this . calendarCredentials ;
if ( ! credential ) {
return [ ] ;
}
createdEvents . push ( await createEvent ( credential , event ) ) ;
2021-12-09 15:51:37 +00:00
}
2022-10-15 17:02:24 +00:00
// Taking care of non-traditional calendar integrations
createdEvents = createdEvents . concat (
await Promise . all (
this . calendarCredentials
. filter ( ( cred ) = > cred . type . includes ( "other_calendar" ) )
. map ( async ( cred ) = > await createEvent ( cred , event ) )
)
) ;
return createdEvents ;
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-07-06 14:43:57 +00:00
let videoCredential = this . videoCredentials
// Whenever a new video connection is added, latest credentials are added with the highest ID.
// Because you can't rely on having them in the higgest first order here, ensure this by sorting in DESC order
. sort ( ( a , b ) = > {
return b . id - a . id ;
} )
. 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-06-17 18:34:41 +00:00
private createVideoEvent ( event : CalendarEvent ) {
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
2022-06-17 18:34:41 +00:00
) : Promise < Array < EventResult < NewCalendarEventType > >> {
2022-07-18 15:37:47 +00:00
let calendarReference : PartialReference | undefined = undefined ,
credential ;
try {
// Bookings should only have one calendar reference
calendarReference = booking . references . filter ( ( reference ) = > reference . type . includes ( "_calendar" ) ) [ 0 ] ;
if ( ! calendarReference ) throw new Error ( "bookingRef" ) ;
const { uid : bookingRefUid , externalCalendarId : bookingExternalCalendarId } = calendarReference ;
if ( ! bookingExternalCalendarId ) throw new Error ( "externalCalendarId" ) ;
const result = [ ] ;
if ( calendarReference . credentialId ) {
credential = this . calendarCredentials . filter (
( credential ) = > credential . id === calendarReference ? . credentialId
) [ 0 ] ;
result . push ( updateEvent ( credential , event , bookingRefUid , bookingExternalCalendarId ) ) ;
} else {
const credentials = this . calendarCredentials . filter (
( credential ) = > credential . type === calendarReference ? . type
) ;
for ( const credential of credentials ) {
result . push ( updateEvent ( credential , event , bookingRefUid , bookingExternalCalendarId ) ) ;
}
}
return Promise . all ( result ) ;
} catch ( error ) {
let message = ` Tried to 'updateAllCalendarEvents' but there was no '{thing}' for ' ${ credential ? . type } ', userId: ' ${ credential ? . userId } ', bookingId: ' ${ booking ? . id } ' ` ;
if ( error instanceof Error ) message = message . replace ( "{thing}" , error . message ) ;
console . error ( message ) ;
return Promise . resolve ( [
{
type : calendarReference ? . type || "calendar" ,
2022-06-08 18:23:48 +00:00
success : false ,
uid : "" ,
originalEvent : event ,
2022-07-18 15:37:47 +00:00
} ,
] ) ;
}
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
}