2022-07-28 19:58:26 +00:00
import type { TFunction } from "next-i18next" ;
2023-06-23 06:23:00 +00:00
import { z } from "zod" ;
2022-07-28 19:58:26 +00:00
2023-01-18 22:30:25 +00:00
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData" ;
2022-08-26 00:48:50 +00:00
import logger from "@calcom/lib/logger" ;
2023-05-02 11:44:05 +00:00
import { BookingStatus } from "@calcom/prisma/enums" ;
2023-02-16 22:39:57 +00:00
import type { Ensure , Optional } from "@calcom/types/utils" ;
2022-08-26 00:48:50 +00:00
import type { EventLocationTypeFromAppMeta } from "../types/App" ;
export type DefaultEventLocationType = {
default : true ;
type : DefaultEventLocationTypeEnum ;
label : string ;
messageForOrganizer : string ;
2023-03-28 20:03:54 +00:00
category : "in person" | "conferencing" | "other" | "phone" ;
2022-08-26 00:48:50 +00:00
iconUrl : string ;
2023-02-14 13:19:45 +00:00
urlRegExp? : string ;
2022-08-26 00:48:50 +00:00
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
2023-03-28 20:03:54 +00:00
variable :
| "locationType"
| "locationAddress"
| "address"
| "locationLink"
| "locationPhoneNumber"
| "phone"
| "hostDefault" ;
defaultValueVariable : "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone" ;
2022-08-26 00:48:50 +00:00
} & (
| {
organizerInputType : "phone" | "text" | null ;
organizerInputPlaceholder? : string | null ;
attendeeInputType? : null ;
attendeeInputPlaceholder? : null ;
}
| {
2022-11-05 20:10:10 +00:00
attendeeInputType : "phone" | "attendeeAddress" | null ;
2022-08-26 00:48:50 +00:00
attendeeInputPlaceholder : string ;
organizerInputType? : null ;
organizerInputPlaceholder? : null ;
}
) ;
type EventLocationTypeFromApp = Ensure < EventLocationTypeFromAppMeta , " defaultValueVariable " | " variable " > ;
export type EventLocationType = DefaultEventLocationType | EventLocationTypeFromApp ;
export const DailyLocationType = "integrations:daily" ;
2023-01-10 02:01:57 +00:00
export const MeetLocationType = "integrations:google:meet" ;
2022-08-26 00:48:50 +00:00
export enum DefaultEventLocationTypeEnum {
2022-11-05 20:10:10 +00:00
/ * *
* Booker Address
* /
AttendeeInPerson = "attendeeInPerson" ,
/ * *
* Organizer Address
* /
2022-02-15 20:30:52 +00:00
InPerson = "inPerson" ,
2022-08-26 00:48:50 +00:00
/ * *
* Booker Phone
* /
2022-02-15 20:30:52 +00:00
Phone = "phone" ,
2022-08-26 00:48:50 +00:00
/ * *
* Organizer Phone
* /
2022-05-16 15:50:12 +00:00
UserPhone = "userPhone" ,
2022-03-13 15:56:56 +00:00
Link = "link" ,
2023-03-28 20:03:54 +00:00
Conferencing = "conferencing" ,
2022-04-05 18:03:22 +00:00
}
2022-08-26 00:48:50 +00:00
export const defaultLocations : DefaultEventLocationType [ ] = [
2022-11-05 20:10:10 +00:00
{
default : true ,
type : DefaultEventLocationTypeEnum . AttendeeInPerson ,
2023-06-08 13:37:54 +00:00
label : "in_person_attendee_address" ,
2022-11-05 20:10:10 +00:00
variable : "address" ,
organizerInputType : null ,
messageForOrganizer : "Cal will ask your invitee to enter an address before scheduling." ,
attendeeInputType : "attendeeAddress" ,
2023-01-04 15:31:50 +00:00
attendeeInputPlaceholder : "enter_address" ,
2022-11-05 20:10:10 +00:00
defaultValueVariable : "attendeeAddress" ,
iconUrl : "/map-pin.svg" ,
2022-11-24 11:53:29 +00:00
category : "in person" ,
2022-11-05 20:10:10 +00:00
} ,
2022-08-26 00:48:50 +00:00
{
default : true ,
type : DefaultEventLocationTypeEnum . InPerson ,
2023-03-07 22:37:56 +00:00
label : "in_person" ,
2022-08-26 00:48:50 +00:00
organizerInputType : "text" ,
messageForOrganizer : "Provide an Address or Place" ,
// HACK:
variable : "locationAddress" ,
defaultValueVariable : "address" ,
iconUrl : "/map-pin.svg" ,
2022-11-24 11:53:29 +00:00
category : "in person" ,
2022-08-26 00:48:50 +00:00
} ,
2023-03-28 20:03:54 +00:00
{
default : true ,
type : DefaultEventLocationTypeEnum . Conferencing ,
iconUrl : "/link.svg" ,
organizerInputType : null ,
label : "organizer_default_conferencing_app" ,
variable : "hostDefault" ,
defaultValueVariable : "hostDefault" ,
category : "conferencing" ,
messageForOrganizer : "" ,
} ,
2022-08-26 00:48:50 +00:00
{
default : true ,
type : DefaultEventLocationTypeEnum . Link ,
2023-03-07 22:37:56 +00:00
label : "link_meeting" ,
2022-08-26 00:48:50 +00:00
organizerInputType : "text" ,
variable : "locationLink" ,
messageForOrganizer : "Provide a Meeting Link" ,
defaultValueVariable : "link" ,
2022-11-22 12:04:06 +00:00
iconUrl : "/link.svg" ,
2022-11-24 11:53:29 +00:00
category : "other" ,
2022-08-26 00:48:50 +00:00
} ,
{
default : true ,
type : DefaultEventLocationTypeEnum . Phone ,
2023-03-07 22:37:56 +00:00
label : "attendee_phone_number" ,
2022-08-26 00:48:50 +00:00
variable : "phone" ,
organizerInputType : null ,
attendeeInputType : "phone" ,
attendeeInputPlaceholder : ` enter_phone_number ` ,
defaultValueVariable : "phone" ,
messageForOrganizer : "Cal will ask your invitee to enter a phone number before scheduling." ,
// This isn't inputType phone because organizer doesn't need to provide it.
// inputType: "phone"
iconUrl : "/phone.svg" ,
2022-11-24 11:53:29 +00:00
category : "phone" ,
2022-08-26 00:48:50 +00:00
} ,
{
default : true ,
type : DefaultEventLocationTypeEnum . UserPhone ,
2023-03-07 22:37:56 +00:00
label : "organizer_phone_number" ,
2022-08-26 00:48:50 +00:00
messageForOrganizer : "Provide your phone number" ,
organizerInputType : "phone" ,
variable : "locationPhoneNumber" ,
defaultValueVariable : "hostPhoneNumber" ,
iconUrl : "/phone.svg" ,
2022-11-24 11:53:29 +00:00
category : "phone" ,
2022-08-26 00:48:50 +00:00
} ,
] ;
2022-04-05 18:03:22 +00:00
2023-06-23 06:23:00 +00:00
const translateAbleKeys = [
"in_person_attendee_address" ,
"in_person" ,
"attendee_phone_number" ,
"link_meeting" ,
"organizer_phone_number" ,
] ;
2022-05-25 20:34:08 +00:00
export type LocationObject = {
2022-08-26 00:48:50 +00:00
type : string ;
2023-06-13 14:54:53 +00:00
address? : string ;
2022-05-25 20:34:08 +00:00
displayLocationPublicly? : boolean ;
2023-03-28 20:03:54 +00:00
} & Partial <
Record < "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone" , string >
> ;
2022-08-26 00:48:50 +00:00
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
export type BookingLocationValue = string ;
export const AppStoreLocationType : Record < string , string > = { } ;
const locationsFromApps : EventLocationTypeFromApp [ ] = [ ] ;
for ( const [ appName , meta ] of Object . entries ( appStoreMetadata ) ) {
const location = meta . appData ? . location ;
if ( location ) {
2023-01-18 22:30:25 +00:00
// TODO: This template variable replacement should happen once during app-store:build.
for ( const [ key , value ] of Object . entries ( location ) ) {
if ( typeof value === "string" ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
location [ key ] = value . replace ( /{SLUG}/g , meta . slug ) . replace ( /{TITLE}/g , meta . name ) ;
}
}
2022-08-26 00:48:50 +00:00
const newLocation = {
. . . location ,
messageForOrganizer : location.messageForOrganizer || ` Set ${ location . label } link ` ,
iconUrl : meta.logo ,
// For All event location apps, locationLink is where we store the input
// TODO: locationLink and link seems redundant. We can modify the code to keep just one of them.
variable : location.variable || "locationLink" ,
defaultValueVariable : location.defaultValueVariable || "link" ,
} ;
// Static links always require organizer to input
if ( newLocation . linkType === "static" ) {
newLocation . organizerInputType = location . organizerInputType || "text" ;
if ( newLocation . organizerInputPlaceholder ? . match ( /https?:\/\// ) ) {
// HACK: Translation ends up removing https? if it's in the beginning :(
newLocation . organizerInputPlaceholder = ` ${ newLocation . organizerInputPlaceholder } ` ;
}
} else {
newLocation . organizerInputType = null ;
}
AppStoreLocationType [ appName ] = newLocation . type ;
locationsFromApps . push ( {
. . . newLocation ,
} ) ;
}
}
const locationsTypes = [ . . . defaultLocations , . . . locationsFromApps ] ;
export const getStaticLinkBasedLocation = ( locationType : string ) = >
locationsFromApps . find ( ( l ) = > l . linkType === "static" && l . type === locationType ) ;
export const getEventLocationTypeFromApp = ( locationType : string ) = >
locationsFromApps . find ( ( l ) = > l . type === locationType ) ;
export const getEventLocationType = ( locationType : string | undefined | null ) = >
locationsTypes . find ( ( l ) = > l . type === locationType ) ;
export const getEventLocationTypeFromValue = ( value : string | undefined | null ) = > {
if ( ! value ) {
return null ;
}
return locationsTypes . find ( ( l ) = > {
if ( l . default || l . linkType == "dynamic" || ! l . urlRegExp ) {
return ;
}
return new RegExp ( l . urlRegExp ) . test ( value ) ;
} ) ;
2022-05-25 20:34:08 +00:00
} ;
2022-08-26 00:48:50 +00:00
export const guessEventLocationType = ( locationTypeOrValue : string | undefined | null ) = >
getEventLocationType ( locationTypeOrValue ) || getEventLocationTypeFromValue ( locationTypeOrValue ) ;
2022-05-25 20:34:08 +00:00
2022-08-26 00:48:50 +00:00
export const LocationType = { . . . DefaultEventLocationTypeEnum , . . . AppStoreLocationType } ;
type PrivacyFilteredLocationObject = Optional < LocationObject , " address " | " link " > ;
export const privacyFilteredLocations = ( locations : LocationObject [ ] ) : PrivacyFilteredLocationObject [ ] = > {
const locationsAfterPrivacyFilter = locations . map ( ( location ) = > {
const eventLocationType = getEventLocationType ( location . type ) ;
if ( ! eventLocationType ) {
logger . debug ( ` Couldn't find location type. App might be uninstalled: ${ location . type } ` ) ;
}
2022-05-25 20:34:08 +00:00
// Filter out locations that are not to be displayed publicly
// Display if the location can be set to public - and also display all locations like google meet etc
2022-08-26 00:48:50 +00:00
if ( location . displayLocationPublicly || ! eventLocationType ) {
return location ;
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { address : _1 , link : _2 , hostPhoneNumber : _3 , . . . privacyFilteredLocation } = location ;
logger . debug ( "Applied Privacy Filter" , location , privacyFilteredLocation ) ;
return privacyFilteredLocation ;
2022-05-25 20:34:08 +00:00
}
} ) ;
2022-08-26 00:48:50 +00:00
return locationsAfterPrivacyFilter ;
} ;
2022-07-28 19:58:26 +00:00
/ * *
* Use this function for translating event location to a readable string
* @param location
* @param t
* @returns string
* /
2022-08-26 00:48:50 +00:00
export const getMessageForOrganizer = ( location : string , t : TFunction ) = > {
const videoLocation = getEventLocationTypeFromApp ( location ) ;
const defaultLocation = defaultLocations . find ( ( l ) = > l . type === location ) ;
if ( defaultLocation ) {
return t ( defaultLocation . messageForOrganizer ) ;
2022-07-28 19:58:26 +00:00
}
2022-12-07 20:12:26 +00:00
if ( videoLocation && videoLocation . linkType !== "static" && videoLocation . type !== "integrations:zoom" ) {
2022-08-26 00:48:50 +00:00
return t ( ` Cal will provide a ${ videoLocation . label } URL. ` ) ;
}
return "" ;
} ;
/ * *
* Use this function to translate booking location value to a readable string
* @param linkValue
* @param translationFunction
* @returns
* /
export const getHumanReadableLocationValue = (
linkValue : string | undefined | null ,
translationFunction : TFunction
) : string = > {
if ( ! linkValue ) {
return translationFunction ( "no_location" ) ;
}
// Just in case linkValue is a `locationType.type`(for old bookings)
const eventLocationType = getEventLocationType ( linkValue ) ;
if ( eventLocationType ) {
// If we can find a video location based on linkValue then it means that the linkValue is something like integrations:google-meet and in that case we don't have the meeting URL to show.
// Show a generic message in that case.
return ` ${ eventLocationType . label } ` ;
}
// Otherwise just show the available link value which can be a Phone number, a URL or a physical address of a place.
return linkValue || "" ;
} ;
export const locationKeyToString = ( location : LocationObject ) = > {
const eventLocationType = getEventLocationType ( location . type ) ;
if ( ! eventLocationType ) {
return null ;
}
const defaultValueVariable = eventLocationType . defaultValueVariable ;
if ( ! defaultValueVariable ) {
console . error ( ` defaultValueVariable not set for ${ location . type } ` ) ;
return "" ;
}
return location [ defaultValueVariable ] || eventLocationType . label ;
} ;
export const getEventLocationWithType = (
locations : LocationObject [ ] ,
locationType : EventLocationType [ "type" ] | undefined
) = > {
const location = locations . find ( ( location ) = > location . type === locationType ) ;
return location ;
2022-07-28 19:58:26 +00:00
} ;
2022-08-26 00:48:50 +00:00
// FIXME: It assumes that type would be sent mostly now. If just in case a value and not type is sent(when old frontend sends requests to new backend), below forEach won't be able to find a match and thus bookingLocation would still be correct equal to reqBody.location
// We must handle the situation where frontend doesn't send us the value because it doesn't have it(displayLocationPublicly not set)
// But we want to store the actual location(except dynamic URL based location type) so that Emails, Calendars pick the value only.
// TODO: We must store both type as well as value so that we know the type of data that we are having. Is it an address or a phone number? This is to be done post v2.0
export const getLocationValueForDB = (
bookingLocationTypeOrValue : EventLocationType [ "type" ] ,
eventLocations : LocationObject [ ]
) = > {
let bookingLocation = bookingLocationTypeOrValue ;
eventLocations . forEach ( ( location ) = > {
if ( location . type === bookingLocationTypeOrValue ) {
const eventLocationType = getEventLocationType ( bookingLocationTypeOrValue ) ;
if ( ! eventLocationType ) {
return ;
}
if ( ! eventLocationType . default && eventLocationType . linkType === "dynamic" ) {
// Dynamic link based locations should still be saved as type. The beyond logic generates meeting URL based on the type.
// This difference can be avoided when we start storing both type and value of a location
return ;
}
bookingLocation = location [ eventLocationType . defaultValueVariable ] || bookingLocation ;
}
} ) ;
return bookingLocation ;
} ;
export const getEventLocationValue = ( eventLocations : LocationObject [ ] , bookingLocation : LocationObject ) = > {
const eventLocationType = getEventLocationType ( bookingLocation ? . type ) ;
if ( ! eventLocationType ) {
return "" ;
}
const defaultValueVariable = eventLocationType . defaultValueVariable ;
if ( ! defaultValueVariable ) {
console . error ( ` ${ defaultValueVariable } not set for ${ bookingLocation . type } ` ) ;
return "" ;
}
const eventLocation = getEventLocationWithType ( eventLocations , bookingLocation ? . type ) ;
if ( ! eventLocation ) {
console . error ( ` Could not find eventLocation for ${ bookingLocation } ` ) ;
return "" ;
}
// Must send .type here if value isn't available due to privacy setting.
// For Booker Phone Number, it would be a value always. For others, value is either autogenerated or provided by Organizer and thus it's possible that organizer doesn't want it to show
// Backend checks for `integration` to generate link
// TODO: use zodSchema to ensure the type of data is correct
return (
bookingLocation [ defaultValueVariable ] || eventLocation [ defaultValueVariable ] || eventLocationType . type
) ;
} ;
2022-09-15 01:27:46 +00:00
export function getSuccessPageLocationMessage (
location : EventLocationType [ "type" ] ,
t : TFunction ,
bookingStatus? : BookingStatus
) {
2022-08-26 00:48:50 +00:00
const eventLocationType = getEventLocationType ( location ) ;
let locationToDisplay = location ;
if ( eventLocationType && ! eventLocationType . default && eventLocationType . linkType === "dynamic" ) {
2022-09-15 01:27:46 +00:00
const isConfirmed = bookingStatus === BookingStatus . ACCEPTED ;
2022-08-26 00:48:50 +00:00
2022-09-15 01:27:46 +00:00
if ( bookingStatus === BookingStatus . CANCELLED || bookingStatus === BookingStatus . REJECTED ) {
2022-08-26 00:48:50 +00:00
locationToDisplay == t ( "web_conference" ) ;
} else if ( isConfirmed ) {
locationToDisplay =
getHumanReadableLocationValue ( location , t ) + ": " + t ( "meeting_url_in_conformation_email" ) ;
} else {
locationToDisplay = t ( "web_conferencing_details_to_follow" ) ;
}
}
return locationToDisplay ;
}
2023-06-23 06:23:00 +00:00
export const getTranslatedLocation = (
location : PrivacyFilteredLocationObject ,
eventLocationType : ReturnType < typeof getEventLocationType > ,
t : TFunction
) = > {
if ( ! eventLocationType ) return null ;
const locationKey = z . string ( ) . default ( "" ) . parse ( locationKeyToString ( location ) ) ;
const translatedLocation = location . type . startsWith ( "integrations:" )
? eventLocationType . label
: translateAbleKeys . includes ( locationKey )
? t ( locationKey )
: locationKey ;
return translatedLocation ;
} ;