2020-03-08 19:39:20 -07:00
import {
IHookFunctions ,
IWebhookFunctions ,
} from 'n8n-core' ;
import {
2020-10-01 05:01:39 -07:00
IDataObject ,
2021-01-31 23:31:40 -08:00
ILoadOptionsFunctions ,
INodePropertyOptions ,
2020-03-08 19:39:20 -07:00
INodeType ,
2020-10-01 05:01:39 -07:00
INodeTypeDescription ,
2020-03-08 19:39:20 -07:00
IWebhookResponseData ,
2021-04-16 09:33:36 -07:00
NodeApiError ,
NodeOperationError ,
2020-03-08 19:39:20 -07:00
} from 'n8n-workflow' ;
import {
2021-01-31 23:31:40 -08:00
companyFields ,
contactFields ,
dealFields ,
2020-03-08 19:39:20 -07:00
hubspotApiRequest ,
2021-01-31 23:31:40 -08:00
propertyEvents ,
2020-03-08 19:39:20 -07:00
} from './GenericFunctions' ;
2020-04-07 21:35:50 -07:00
import {
createHash ,
2021-01-31 23:31:40 -08:00
} from 'crypto' ;
import {
capitalCase ,
} from 'change-case' ;
2020-03-08 19:39:20 -07:00
export class HubspotTrigger implements INodeType {
description : INodeTypeDescription = {
2020-07-24 03:56:41 -07:00
displayName : 'HubSpot Trigger' ,
2020-03-08 19:39:20 -07:00
name : 'hubspotTrigger' ,
2021-01-31 23:31:40 -08:00
icon : 'file:hubspot.svg' ,
2020-03-08 19:39:20 -07:00
group : [ 'trigger' ] ,
version : 1 ,
2020-07-24 03:56:41 -07:00
description : 'Starts the workflow when HubSpot events occur.' ,
2020-03-08 19:39:20 -07:00
defaults : {
name : 'Hubspot Trigger' ,
color : '#ff7f64' ,
} ,
inputs : [ ] ,
outputs : [ 'main' ] ,
credentials : [
{
name : 'hubspotDeveloperApi' ,
required : true ,
} ,
] ,
webhooks : [
{
name : 'default' ,
httpMethod : 'POST' ,
responseMode : 'onReceived' ,
path : 'webhook' ,
} ,
{
name : 'setup' ,
httpMethod : 'GET' ,
responseMode : 'onReceived' ,
path : 'webhook' ,
} ,
] ,
properties : [
{
2021-01-31 23:31:40 -08:00
displayName : 'Events' ,
name : 'eventsUi' ,
type : 'fixedCollection' ,
typeOptions : {
multipleValues : true ,
} ,
placeholder : 'Add Event' ,
default : { } ,
2020-03-08 19:39:20 -07:00
options : [
{
2021-01-31 23:31:40 -08:00
displayName : 'Event' ,
name : 'eventValues' ,
values : [
{
displayName : 'Name' ,
name : 'name' ,
type : 'options' ,
options : [
{
name : 'Contact Created' ,
value : 'contact.creation' ,
description : ` To get notified if any contact is created in a customer's account. ` ,
} ,
{
name : 'Contact Deleted' ,
value : 'contact.deletion' ,
description : ` To get notified if any contact is deleted in a customer's account. ` ,
} ,
{
name : 'Contact Privacy Deleted' ,
value : 'contact.privacyDeletion' ,
description : ` To get notified if a contact is deleted for privacy compliance reasons. ` ,
} ,
{
name : 'Contact Property Changed' ,
value : 'contact.propertyChange' ,
description : ` to get notified if a specified property is changed for any contact in a customer's account. ` ,
} ,
{
name : 'Company Created' ,
value : 'company.creation' ,
description : ` To get notified if any company is created in a customer's account. ` ,
} ,
{
name : 'Company Deleted' ,
value : 'company.deletion' ,
description : ` To get notified if any company is deleted in a customer's account. ` ,
} ,
{
name : 'Company Property Changed' ,
value : 'company.propertyChange' ,
description : ` To get notified if a specified property is changed for any company in a customer's account. ` ,
} ,
{
name : 'Deal Created' ,
value : 'deal.creation' ,
description : ` To get notified if any deal is created in a customer's account. ` ,
} ,
{
name : 'Deal Deleted' ,
value : 'deal.deletion' ,
description : ` To get notified if any deal is deleted in a customer's account. ` ,
} ,
{
name : 'Deal Property Changed' ,
value : 'deal.propertyChange' ,
description : ` To get notified if a specified property is changed for any deal in a customer's account. ` ,
} ,
] ,
default : 'contact.creation' ,
required : true ,
} ,
{
displayName : 'Property' ,
name : 'property' ,
type : 'options' ,
typeOptions : {
loadOptionsMethod : 'getContactProperties' ,
} ,
displayOptions : {
show : {
name : [
'contact.propertyChange' ,
] ,
} ,
} ,
default : '' ,
required : true ,
} ,
{
displayName : 'Property' ,
name : 'property' ,
type : 'options' ,
typeOptions : {
loadOptionsMethod : 'getCompanyProperties' ,
} ,
displayOptions : {
show : {
name : [
'company.propertyChange' ,
] ,
} ,
} ,
default : '' ,
required : true ,
} ,
{
displayName : 'Property' ,
name : 'property' ,
type : 'options' ,
typeOptions : {
loadOptionsMethod : 'getDealProperties' ,
} ,
displayOptions : {
show : {
name : [
'deal.propertyChange' ,
] ,
} ,
} ,
default : '' ,
required : true ,
} ,
2020-03-08 19:39:20 -07:00
] ,
} ,
2021-01-31 23:31:40 -08:00
] ,
2020-03-08 19:39:20 -07:00
} ,
{
displayName : 'Additional Fields' ,
name : 'additionalFields' ,
type : 'collection' ,
placeholder : 'Add Field' ,
default : { } ,
options : [
{
displayName : 'Max Concurrent Requests' ,
name : 'maxConcurrentRequests' ,
type : 'number' ,
typeOptions : {
minValue : 5 ,
} ,
default : 5 ,
} ,
] ,
} ,
] ,
2021-01-31 23:31:40 -08:00
} ;
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
methods = {
loadOptions : {
// Get all the available contacts to display them to user so that he can
// select them easily
async getContactProperties ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const returnData : INodePropertyOptions [ ] = [ ] ;
for ( const field of contactFields ) {
returnData . push ( {
name : capitalCase ( field . label ) ,
value : field.id ,
} ) ;
}
returnData . sort ( ( a , b ) = > {
if ( a . name < b . name ) { return - 1 ; }
if ( a . name > b . name ) { return 1 ; }
return 0 ;
} ) ;
return returnData ;
} ,
// Get all the available companies to display them to user so that he can
// select them easily
async getCompanyProperties ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const returnData : INodePropertyOptions [ ] = [ ] ;
for ( const field of companyFields ) {
returnData . push ( {
name : capitalCase ( field . label ) ,
value : field.id ,
} ) ;
}
returnData . sort ( ( a , b ) = > {
if ( a . name < b . name ) { return - 1 ; }
if ( a . name > b . name ) { return 1 ; }
return 0 ;
} ) ;
return returnData ;
} ,
// Get all the available deals to display them to user so that he can
// select them easily
async getDealProperties ( this : ILoadOptionsFunctions ) : Promise < INodePropertyOptions [ ] > {
const returnData : INodePropertyOptions [ ] = [ ] ;
for ( const field of dealFields ) {
returnData . push ( {
name : capitalCase ( field . label ) ,
value : field.id ,
} ) ;
}
returnData . sort ( ( a , b ) = > {
if ( a . name < b . name ) { return - 1 ; }
if ( a . name > b . name ) { return 1 ; }
return 0 ;
} ) ;
return returnData ;
} ,
} ,
2020-03-08 19:39:20 -07:00
} ;
// @ts-ignore (because of request)
webhookMethods = {
default : {
async checkExists ( this : IHookFunctions ) : Promise < boolean > {
// Check all the webhooks which exist already if it is identical to the
// one that is supposed to get created.
2021-01-31 23:31:40 -08:00
const currentWebhookUrl = this . getNodeWebhookUrl ( 'default' ) as string ;
const { appId } = this . getCredentials ( 'hubspotDeveloperApi' ) as IDataObject ;
try {
const { targetUrl } = await hubspotApiRequest . call ( this , 'GET' , ` /webhooks/v3/ ${ appId } /settings ` , { } ) ;
if ( targetUrl !== currentWebhookUrl ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , ` The APP ID ${ appId } already has a target url ${ targetUrl } . Delete it or use another APP ID before executing the trigger. Due to Hubspot API limitations, you can have just one trigger per APP. ` ) ;
2021-01-31 23:31:40 -08:00
}
} catch ( error ) {
if ( error . statusCode === 404 ) {
return false ;
2020-03-08 19:39:20 -07:00
}
}
2021-01-31 23:31:40 -08:00
// if the app is using the current webhook url. Delete everything and create it again with the current events
const { results : subscriptions } = await hubspotApiRequest . call ( this , 'GET' , ` /webhooks/v3/ ${ appId } /subscriptions ` , { } ) ;
// delete all subscriptions
for ( const subscription of subscriptions ) {
await hubspotApiRequest . call ( this , 'DELETE' , ` /webhooks/v3/ ${ appId } /subscriptions/ ${ subscription . id } ` , { } ) ;
}
await hubspotApiRequest . call ( this , 'DELETE' , ` /webhooks/v3/ ${ appId } /settings ` , { } ) ;
2020-03-08 19:39:20 -07:00
return false ;
} ,
async create ( this : IHookFunctions ) : Promise < boolean > {
const webhookUrl = this . getNodeWebhookUrl ( 'default' ) ;
2021-01-31 23:31:40 -08:00
const { appId } = this . getCredentials ( 'hubspotDeveloperApi' ) as IDataObject ;
const events = ( this . getNodeParameter ( 'eventsUi' ) as IDataObject || { } ) . eventValues as IDataObject [ ] || [ ] ;
2020-03-08 19:39:20 -07:00
const additionalFields = this . getNodeParameter ( 'additionalFields' ) as IDataObject ;
2021-01-31 23:31:40 -08:00
let endpoint = ` /webhooks/v3/ ${ appId } /settings ` ;
2020-03-13 04:09:09 -07:00
let body : IDataObject = {
2021-01-31 23:31:40 -08:00
targetUrl : webhookUrl ,
2020-03-08 19:39:20 -07:00
maxConcurrentRequests : additionalFields.maxConcurrentRequests || 5 ,
} ;
2021-01-31 23:31:40 -08:00
2020-03-08 19:39:20 -07:00
await hubspotApiRequest . call ( this , 'PUT' , endpoint , body ) ;
2021-01-31 23:31:40 -08:00
endpoint = ` /webhooks/v3/ ${ appId } /subscriptions ` ;
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
if ( Array . isArray ( events ) && events . length === 0 ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , ` You must define at least one event ` ) ;
2021-01-31 23:31:40 -08:00
}
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
for ( const event of events ) {
body = {
eventType : event.name ,
active : true ,
} ;
if ( propertyEvents . includes ( event . name as string ) ) {
const property = event . property ;
body . propertyName = property ;
}
await hubspotApiRequest . call ( this , 'POST' , endpoint , body ) ;
2020-03-08 19:39:20 -07:00
}
return true ;
} ,
async delete ( this : IHookFunctions ) : Promise < boolean > {
2021-01-31 23:31:40 -08:00
const { appId } = this . getCredentials ( 'hubspotDeveloperApi' ) as IDataObject ;
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
const { results : subscriptions } = await hubspotApiRequest . call ( this , 'GET' , ` /webhooks/v3/ ${ appId } /subscriptions ` , { } ) ;
2020-03-08 19:39:20 -07:00
2021-01-31 23:31:40 -08:00
for ( const subscription of subscriptions ) {
await hubspotApiRequest . call ( this , 'DELETE' , ` /webhooks/v3/ ${ appId } /subscriptions/ ${ subscription . id } ` , { } ) ;
}
try {
await hubspotApiRequest . call ( this , 'DELETE' , ` /webhooks/v3/ ${ appId } /settings ` , { } ) ;
2021-04-16 09:33:36 -07:00
} catch ( error ) {
2021-01-31 23:31:40 -08:00
return false ;
2020-03-08 19:39:20 -07:00
}
return true ;
} ,
} ,
} ;
async webhook ( this : IWebhookFunctions ) : Promise < IWebhookResponseData > {
2020-06-10 03:57:13 -07:00
2020-06-13 16:48:24 -07:00
const credentials = this . getCredentials ( 'hubspotDeveloperApi' ) as IDataObject ;
2020-06-10 03:57:13 -07:00
if ( credentials === undefined ) {
2021-04-16 09:33:36 -07:00
throw new NodeOperationError ( this . getNode ( ) , 'No credentials found!' ) ;
2020-06-10 03:57:13 -07:00
}
2020-03-13 04:09:09 -07:00
const req = this . getRequestObject ( ) ;
2020-03-08 19:39:20 -07:00
const bodyData = req . body ;
const headerData = this . getHeaderData ( ) ;
//@ts-ignore
2020-03-13 04:09:09 -07:00
if ( headerData [ 'x-hubspot-signature' ] === undefined ) {
2020-03-08 19:39:20 -07:00
return { } ;
}
2020-06-13 16:48:24 -07:00
// check signare if client secret is defined
if ( credentials . clientSecret !== '' ) {
const hash = ` ${ credentials ! . clientSecret } ${ JSON . stringify ( bodyData ) } ` ;
2021-01-31 23:31:40 -08:00
const signature = createHash ( 'sha256' ) . update ( hash ) . digest ( 'hex' ) ;
2020-06-13 16:48:24 -07:00
//@ts-ignore
if ( signature !== headerData [ 'x-hubspot-signature' ] ) {
return { } ;
}
2020-03-08 19:39:20 -07:00
}
2020-06-13 16:48:24 -07:00
2020-03-08 19:39:20 -07:00
for ( let i = 0 ; i < bodyData . length ; i ++ ) {
const subscriptionType = bodyData [ i ] . subscriptionType as string ;
if ( subscriptionType . includes ( 'contact' ) ) {
bodyData [ i ] . contactId = bodyData [ i ] . objectId ;
}
if ( subscriptionType . includes ( 'company' ) ) {
bodyData [ i ] . companyId = bodyData [ i ] . objectId ;
}
if ( subscriptionType . includes ( 'deal' ) ) {
bodyData [ i ] . dealId = bodyData [ i ] . objectId ;
}
delete bodyData [ i ] . objectId ;
}
return {
workflowData : [
this . helpers . returnJsonArray ( bodyData ) ,
] ,
} ;
}
}