2023-03-15 08:58:50 +00:00
import type { Webhook } from "@prisma/client" ;
2022-06-16 16:21:48 +00:00
import { createHmac } from "crypto" ;
2021-11-22 11:37:07 +00:00
import { compile } from "handlebars" ;
2022-09-08 19:18:00 +00:00
import { getHumanReadableLocationValue } from "@calcom/app-store/locations" ;
2022-03-23 22:00:30 +00:00
import type { CalendarEvent } from "@calcom/types/Calendar" ;
2021-10-07 15:14:47 +00:00
2021-11-22 11:37:07 +00:00
type ContentType = "application/json" | "application/x-www-form-urlencoded" ;
2022-09-08 19:18:00 +00:00
export type EventTypeInfo = {
eventTitle? : string | null ;
eventDescription? : string | null ;
requiresConfirmation? : boolean | null ;
price? : number | null ;
currency? : string | null ;
length? : number | null ;
} ;
2023-05-30 15:35:05 +00:00
export type WebhookDataType = CalendarEvent &
2023-02-17 13:21:11 +00:00
EventTypeInfo & {
2023-09-06 19:40:27 +00:00
metadata ? : { [ key : string ] : string | number | boolean | null } ;
2023-02-17 13:21:11 +00:00
bookingId? : number ;
status? : string ;
smsReminderNumber? : string ;
rescheduleUid? : string ;
rescheduleStartTime? : string ;
rescheduleEndTime? : string ;
triggerEvent : string ;
createdAt : string ;
2023-05-10 14:56:31 +00:00
downloadLink? : string ;
2023-09-06 19:40:27 +00:00
paymentId? : number ;
2023-02-17 13:21:11 +00:00
} ;
2023-08-26 00:27:05 +00:00
function getZapierPayload (
data : CalendarEvent & EventTypeInfo & { status? : string ; createdAt : string }
) : string {
2022-09-08 19:18:00 +00:00
const attendees = data . attendees . map ( ( attendee ) = > {
return {
name : attendee.name ,
email : attendee.email ,
timeZone : attendee.timeZone ,
} ;
} ) ;
const t = data . organizer . language . translate ;
const location = getHumanReadableLocationValue ( data . location || "" , t ) ;
const body = {
title : data.title ,
description : data.description ,
customInputs : data.customInputs ,
2023-03-15 08:58:50 +00:00
responses : data.responses ,
2023-04-04 04:59:09 +00:00
userFieldsResponses : data.userFieldsResponses ,
2022-09-08 19:18:00 +00:00
startTime : data.startTime ,
endTime : data.endTime ,
location : location ,
status : data.status ,
2022-10-28 09:09:15 +00:00
cancellationReason : data.cancellationReason ,
user : {
username : data.organizer.username ,
name : data.organizer.name ,
email : data.organizer.email ,
timeZone : data.organizer.timeZone ,
locale : data.organizer.locale ,
} ,
2022-09-08 19:18:00 +00:00
eventType : {
title : data.eventTitle ,
description : data.eventDescription ,
requiresConfirmation : data.requiresConfirmation ,
price : data.price ,
currency : data.currency ,
length : data.length ,
} ,
attendees : attendees ,
2023-08-26 00:27:05 +00:00
createdAt : data.createdAt ,
2022-09-08 19:18:00 +00:00
} ;
return JSON . stringify ( body ) ;
}
2023-02-17 13:21:11 +00:00
function applyTemplate ( template : string , data : WebhookDataType , contentType : ContentType ) {
2023-09-04 18:01:12 +00:00
const compiled = compile ( template ) ( data ) . replace ( /"/g , '"' ) ;
2023-02-17 13:21:11 +00:00
2021-11-22 11:37:07 +00:00
if ( contentType === "application/json" ) {
2022-02-18 17:49:39 +00:00
return JSON . stringify ( jsonParse ( compiled ) ) ;
2021-11-22 11:37:07 +00:00
}
return compiled ;
}
function jsonParse ( jsonString : string ) {
try {
return JSON . parse ( jsonString ) ;
} catch ( e ) {
// don't do anything.
}
return false ;
}
const sendPayload = async (
2022-06-16 16:21:48 +00:00
secretKey : string | null ,
2021-10-07 15:14:47 +00:00
triggerEvent : string ,
createdAt : string ,
2022-05-03 23:16:59 +00:00
webhook : Pick < Webhook , " subscriberUrl " | " appId " | " payloadTemplate " > ,
2023-02-17 13:21:11 +00:00
data : Omit < WebhookDataType , " createdAt " | " triggerEvent " >
2021-11-22 11:37:07 +00:00
) = > {
2022-07-20 18:30:57 +00:00
const { appId , payloadTemplate : template } = webhook ;
2021-11-22 11:37:07 +00:00
const contentType =
! template || jsonParse ( template ) ? "application/json" : "application/x-www-form-urlencoded" ;
2022-05-03 23:16:59 +00:00
data . description = data . description || data . additionalNotes ;
let body ;
/* Zapier id is hardcoded in the DB, we send the raw data for this case */
if ( appId === "zapier" ) {
2023-08-26 00:27:05 +00:00
body = getZapierPayload ( { . . . data , createdAt } ) ;
2022-05-03 23:16:59 +00:00
} else if ( template ) {
2023-02-17 13:21:11 +00:00
body = applyTemplate ( template , { . . . data , triggerEvent , createdAt } , contentType ) ;
2022-05-03 23:16:59 +00:00
} else {
body = JSON . stringify ( {
triggerEvent : triggerEvent ,
createdAt : createdAt ,
payload : data ,
} ) ;
}
2021-11-22 11:37:07 +00:00
2023-09-04 21:04:57 +00:00
return _sendPayload ( secretKey , webhook , body , contentType ) ;
2022-07-20 18:30:57 +00:00
} ;
2023-09-04 21:04:57 +00:00
export const sendGenericWebhookPayload = async ( {
secretKey ,
triggerEvent ,
createdAt ,
webhook ,
data ,
rootData ,
} : {
secretKey : string | null ;
triggerEvent : string ;
createdAt : string ;
webhook : Pick < Webhook , " subscriberUrl " | " appId " | " payloadTemplate " > ;
data : Record < string , unknown > ;
rootData? : Record < string , unknown > ;
} ) = > {
const body = JSON . stringify ( {
// Added rootData props first so that using the known(i.e. triggerEvent, createdAt, payload) properties in rootData doesn't override the known properties
. . . rootData ,
triggerEvent : triggerEvent ,
createdAt : createdAt ,
payload : data ,
} ) ;
return _sendPayload ( secretKey , webhook , body , "application/json" ) ;
2022-07-20 18:30:57 +00:00
} ;
const _sendPayload = async (
secretKey : string | null ,
webhook : Pick < Webhook , " subscriberUrl " | " appId " | " payloadTemplate " > ,
body : string ,
contentType : "application/json" | "application/x-www-form-urlencoded"
) = > {
const { subscriberUrl } = webhook ;
if ( ! subscriberUrl || ! body ) {
throw new Error ( "Missing required elements to send webhook payload." ) ;
}
2022-06-16 16:21:48 +00:00
const secretSignature = secretKey
? createHmac ( "sha256" , secretKey ) . update ( ` ${ body } ` ) . digest ( "hex" )
: "no-secret-provided" ;
2021-11-22 11:37:07 +00:00
const response = await fetch ( subscriberUrl , {
method : "POST" ,
headers : {
"Content-Type" : contentType ,
2022-06-16 16:21:48 +00:00
"X-Cal-Signature-256" : secretSignature ,
2021-11-22 11:37:07 +00:00
} ,
2023-07-24 14:36:45 +00:00
redirect : "manual" ,
2021-11-22 11:37:07 +00:00
body ,
2021-10-07 15:14:47 +00:00
} ) ;
2021-11-22 11:37:07 +00:00
const text = await response . text ( ) ;
return {
ok : response.ok ,
status : response.status ,
2023-07-24 14:36:45 +00:00
. . . ( text
? {
message : text ,
}
: { } ) ,
2021-11-22 11:37:07 +00:00
} ;
} ;
2021-10-07 15:14:47 +00:00
export default sendPayload ;