2024-04-18 05:45:07 -07:00
import { createHmac } from 'crypto' ;
import {
NodeOperationError ,
type IDataObject ,
type IHookFunctions ,
type INodeType ,
type INodeTypeDescription ,
type IWebhookFunctions ,
type IWebhookResponseData ,
2024-08-29 06:55:53 -07:00
NodeConnectionType ,
2024-04-18 05:45:07 -07:00
} from 'n8n-workflow' ;
import {
appWebhookSubscriptionCreate ,
appWebhookSubscriptionDelete ,
appWebhookSubscriptionList ,
} from './GenericFunctions' ;
import type { WhatsAppPageEvent } from './types' ;
import { whatsappTriggerDescription } from './WhatsappDescription' ;
export class WhatsAppTrigger implements INodeType {
description : INodeTypeDescription = {
displayName : 'WhatsApp Trigger' ,
name : 'whatsAppTrigger' ,
icon : 'file:whatsapp.svg' ,
group : [ 'trigger' ] ,
version : 1 ,
subtitle : '={{$parameter["event"]}}' ,
description : 'Handle WhatsApp events via webhooks' ,
defaults : {
name : 'WhatsApp Trigger' ,
} ,
inputs : [ ] ,
2024-08-29 06:55:53 -07:00
outputs : [ NodeConnectionType . Main ] ,
2024-04-18 05:45:07 -07:00
credentials : [
{
name : 'whatsAppTriggerApi' ,
required : true ,
} ,
] ,
webhooks : [
{
name : 'setup' ,
httpMethod : 'GET' ,
responseMode : 'onReceived' ,
path : 'webhook' ,
} ,
{
name : 'default' ,
httpMethod : 'POST' ,
responseMode : 'onReceived' ,
path : 'webhook' ,
} ,
] ,
properties : [
{
displayName :
'Due to Facebook API limitations, you can use just one WhatsApp trigger for each Facebook App' ,
name : 'whatsAppNotice' ,
type : 'notice' ,
default : '' ,
} ,
. . . whatsappTriggerDescription ,
] ,
} ;
webhookMethods = {
default : {
async checkExists ( this : IHookFunctions ) : Promise < boolean > {
const webhookUrl = this . getNodeWebhookUrl ( 'default' ) as string ;
const credentials = await this . getCredentials ( 'whatsAppTriggerApi' ) ;
const updates = this . getNodeParameter ( 'updates' , [ ] ) as IDataObject [ ] ;
const subscribedEvents = updates . sort ( ) . join ( ',' ) ;
const appId = credentials . clientId as string ;
const webhooks = await appWebhookSubscriptionList . call ( this , appId ) ;
const subscription = webhooks . find (
( webhook ) = >
webhook . object === 'whatsapp_business_account' &&
webhook . fields
. map ( ( x ) = > x . name )
. sort ( )
. join ( ',' ) === subscribedEvents &&
webhook . active ,
) ;
if ( ! subscription ) {
return false ;
}
if ( subscription . callback_url !== webhookUrl ) {
throw new NodeOperationError (
this . getNode ( ) ,
` The WhatsApp App ID ${ appId } already has a webhook subscription. Delete it or use another App before executing the trigger. Due to WhatsApp API limitations, you can have just one trigger per App. ` ,
{ level : 'warning' } ,
) ;
}
if (
subscription ? . fields
. map ( ( x ) = > x . name )
. sort ( )
. join ( ',' ) !== subscribedEvents
) {
await appWebhookSubscriptionDelete . call ( this , appId , 'whatsapp_business_account' ) ;
return false ;
}
return true ;
} ,
async create ( this : IHookFunctions ) : Promise < boolean > {
const webhookUrl = this . getNodeWebhookUrl ( 'default' ) as string ;
const credentials = await this . getCredentials ( 'whatsAppTriggerApi' ) ;
const appId = credentials . clientId as string ;
const updates = this . getNodeParameter ( 'updates' , [ ] ) as IDataObject [ ] ;
const verifyToken = this . getNode ( ) . id ;
await appWebhookSubscriptionCreate . call ( this , appId , {
object : 'whatsapp_business_account' ,
callback_url : webhookUrl ,
verify_token : verifyToken ,
fields : JSON.stringify ( updates ) ,
include_values : true ,
} ) ;
return true ;
} ,
async delete ( this : IHookFunctions ) : Promise < boolean > {
const credentials = await this . getCredentials ( 'whatsAppTriggerApi' ) ;
const appId = credentials . clientId as string ;
await appWebhookSubscriptionDelete . call ( this , appId , 'whatsapp_business_account' ) ;
return true ;
} ,
} ,
} ;
async webhook ( this : IWebhookFunctions ) : Promise < IWebhookResponseData > {
const bodyData = this . getBodyData ( ) as unknown as WhatsAppPageEvent ;
const query = this . getQueryData ( ) as IDataObject ;
const res = this . getResponseObject ( ) ;
const req = this . getRequestObject ( ) ;
const headerData = this . getHeaderData ( ) as IDataObject ;
const credentials = await this . getCredentials ( 'whatsAppTriggerApi' ) ;
// Check if we're getting facebook's challenge request (https://developers.facebook.com/docs/graph-api/webhooks/getting-started)
if ( this . getWebhookName ( ) === 'setup' ) {
if ( query [ 'hub.challenge' ] ) {
if ( this . getNode ( ) . id !== query [ 'hub.verify_token' ] ) {
return { } ;
}
res . status ( 200 ) . send ( query [ 'hub.challenge' ] ) . end ( ) ;
return { noWebhookResponse : true } ;
}
}
const computedSignature = createHmac ( 'sha256' , credentials . clientSecret as string )
. update ( req . rawBody )
. digest ( 'hex' ) ;
if ( headerData [ 'x-hub-signature-256' ] !== ` sha256= ${ computedSignature } ` ) {
return { } ;
}
if ( bodyData . object !== 'whatsapp_business_account' ) {
return { } ;
}
const events = await Promise . all (
bodyData . entry
. map ( ( entry ) = > entry . changes )
. flat ( )
. map ( ( change ) = > ( { . . . change . value , field : change.field } ) ) ,
) ;
if ( events . length === 0 ) {
return { } ;
}
return {
workflowData : [ this . helpers . returnJsonArray ( events ) ] ,
} ;
}
}