feat: Add WhatsApp Business Trigger Node (#8840)

Co-authored-by: Giulio Andreini <andreini@netseven.it>
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
Bram Kn 2024-04-18 14:45:07 +02:00 committed by GitHub
parent 53101960e6
commit 23a2dd08b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 511 additions and 0 deletions

View file

@ -0,0 +1,41 @@
import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
export class WhatsAppTriggerApi implements ICredentialType {
name = 'whatsAppTriggerApi';
displayName = 'WhatsApp API';
documentationUrl = 'whatsApp';
properties: INodeProperties[] = [
{
displayName: 'Client ID',
name: 'clientId',
type: 'string',
default: '',
required: true,
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string',
typeOptions: {
password: true,
},
default: '',
required: true,
},
];
test: ICredentialTestRequest = {
request: {
method: 'POST',
baseURL: 'https://graph.facebook.com/v19.0/oauth/access_token',
body: {
client_id: '={{$credentials.clientId}}',
client_secret: '={{$credentials.clientSecret}}',
grant_type: 'client_credentials',
},
},
};
}

View file

@ -63,6 +63,17 @@ export class FacebookTrigger implements INodeType {
default: '',
description: 'Facebook APP ID',
},
{
displayName: 'To watch Whatsapp business account events use the Whatsapp trigger node',
name: 'whatsappBusinessAccountNotice',
type: 'notice',
default: '',
displayOptions: {
show: {
object: ['whatsappBusinessAccount'],
},
},
},
{
displayName: 'Object',
name: 'object',

View file

@ -0,0 +1,103 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IWebhookFunctions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import type {
WhatsAppAppWebhookSubscriptionsResponse,
WhatsAppAppWebhookSubscription,
} from './types';
async function appAccessTokenRead(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
): Promise<{ access_token: string }> {
const credentials = await this.getCredentials('whatsAppTriggerApi');
const options: IHttpRequestOptions = {
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
method: 'POST',
body: {
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
grant_type: 'client_credentials',
},
url: 'https://graph.facebook.com/v19.0/oauth/access_token',
json: true,
};
try {
return await this.helpers.httpRequest.call(this, options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
async function whatsappApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
method: IHttpRequestMethods,
resource: string,
body?: { type: 'json'; payload: IDataObject } | { type: 'form'; payload: IDataObject },
qs: IDataObject = {},
): Promise<any> {
const tokenResponse = await appAccessTokenRead.call(this);
const appAccessToken = tokenResponse.access_token;
const options: IHttpRequestOptions = {
headers: {
accept: 'application/json',
authorization: `Bearer ${appAccessToken}`,
},
method,
qs,
body: body?.payload,
url: `https://graph.facebook.com/v19.0${resource}`,
json: true,
};
try {
return await this.helpers.httpRequest.call(this, options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function appWebhookSubscriptionList(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
appId: string,
): Promise<WhatsAppAppWebhookSubscription[]> {
const response = (await whatsappApiRequest.call(
this,
'GET',
`/${appId}/subscriptions`,
)) as WhatsAppAppWebhookSubscriptionsResponse;
return response.data;
}
export async function appWebhookSubscriptionCreate(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
appId: string,
subscription: IDataObject,
) {
return await whatsappApiRequest.call(this, 'POST', `/${appId}/subscriptions`, {
type: 'form',
payload: { ...subscription },
});
}
export async function appWebhookSubscriptionDelete(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
appId: string,
object: string,
) {
return await whatsappApiRequest.call(this, 'DELETE', `/${appId}/subscriptions`, {
type: 'form',
payload: { object },
});
}

View file

@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.whatsAppTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/credentials/whatsapp/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.whatsapptrigger/"
}
]
}
}

View file

@ -0,0 +1,185 @@
import { createHmac } from 'crypto';
import {
NodeOperationError,
type IDataObject,
type IHookFunctions,
type INodeType,
type INodeTypeDescription,
type IWebhookFunctions,
type IWebhookResponseData,
} 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: [],
outputs: ['main'],
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)],
};
}
}

View file

@ -0,0 +1,53 @@
import type { INodeProperties } from 'n8n-workflow';
export const whatsappTriggerDescription: INodeProperties[] = [
{
displayName: 'Trigger On',
name: 'updates',
type: 'multiOptions',
required: true,
default: [],
options: [
{
name: 'Account Review Update',
value: 'account_review_update',
},
{
name: 'Account Update',
value: 'account_update',
},
{
name: 'Business Capability Update',
value: 'business_capability_update',
},
{
name: 'Message Template Quality Update',
value: 'message_template_quality_update',
},
{
name: 'Message Template Status Update',
value: 'message_template_status_update',
},
{
name: 'Messages',
value: 'messages',
},
{
name: 'Phone Number Name Update',
value: 'phone_number_name_update',
},
{
name: 'Phone Number Quality Update',
value: 'phone_number_quality_update',
},
{
name: 'Security',
value: 'security',
},
{
name: 'Template Category Update',
value: 'template_category_update',
},
],
},
];

View file

@ -0,0 +1,98 @@
import type { GenericValue, IDataObject } from 'n8n-workflow';
export type BaseFacebookResponse<TData> = { data: TData };
export type BasePaginatedFacebookResponse<TData> = BaseFacebookResponse<TData> & {
paging: { cursors: { before?: string; after?: string } };
};
export type WhatsAppAppWebhookSubscriptionsResponse = BaseFacebookResponse<
WhatsAppAppWebhookSubscription[]
>;
export interface WhatsAppAppWebhookSubscription {
object: string;
callback_url: string;
active: boolean;
fields: WhatsAppAppWebhookSubscriptionField[];
}
export interface WhatsAppAppWebhookSubscriptionField {
name: string;
version: string;
}
export interface CreateFacebookAppWebhookSubscription {
object: string;
callback_url: string;
fields: string[];
include_values: boolean;
verify_token: string;
}
export type FacebookPageListResponse = BasePaginatedFacebookResponse<FacebookPage[]>;
export type FacebookFormListResponse = BasePaginatedFacebookResponse<FacebookForm[]>;
export interface FacebookPage {
id: string;
name: string;
access_token: string;
category: string;
category_list: FacebookPageCategory[];
tasks: string[];
}
export interface FacebookPageCategory {
id: string;
name: string;
}
export interface FacebookFormQuestion {
id: string;
key: string;
label: string;
type: string;
}
export interface FacebookForm {
id: string;
name: string;
locale: string;
status: string;
page: {
id: string;
name: string;
};
questions: FacebookFormQuestion[];
}
export interface WhatsAppPageEvent {
object: 'whatsapp_business_account';
entry: WhatsAppEventEntry[];
}
export interface WhatsAppEventEntry {
id: string;
time: number;
changes: [
{
field: string;
value: IDataObject;
},
];
}
export interface FacebookFormLeadData {
id: string;
created_time: string;
ad_id: string;
ad_name: string;
adset_id: string;
adset_name: string;
form_id: string;
field_data: [
{
name: string;
values: GenericValue[];
},
];
}

View file

@ -368,6 +368,7 @@
"dist/credentials/WebflowOAuth2Api.credentials.js",
"dist/credentials/WekanApi.credentials.js",
"dist/credentials/WhatsAppApi.credentials.js",
"dist/credentials/WhatsAppTriggerApi.credentials.js",
"dist/credentials/WiseApi.credentials.js",
"dist/credentials/WooCommerceApi.credentials.js",
"dist/credentials/WordpressApi.credentials.js",
@ -776,6 +777,7 @@
"dist/nodes/Webflow/WebflowTrigger.node.js",
"dist/nodes/Webhook/Webhook.node.js",
"dist/nodes/Wekan/Wekan.node.js",
"dist/nodes/WhatsApp/WhatsAppTrigger.node.js",
"dist/nodes/WhatsApp/WhatsApp.node.js",
"dist/nodes/Wise/Wise.node.js",
"dist/nodes/Wise/WiseTrigger.node.js",