mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
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:
parent
53101960e6
commit
23a2dd08b6
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
|
|
103
packages/nodes-base/nodes/WhatsApp/GenericFunctions.ts
Normal file
103
packages/nodes-base/nodes/WhatsApp/GenericFunctions.ts
Normal 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 },
|
||||
});
|
||||
}
|
18
packages/nodes-base/nodes/WhatsApp/WhatsAppTrigger.node.json
Normal file
18
packages/nodes-base/nodes/WhatsApp/WhatsAppTrigger.node.json
Normal 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/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
185
packages/nodes-base/nodes/WhatsApp/WhatsAppTrigger.node.ts
Normal file
185
packages/nodes-base/nodes/WhatsApp/WhatsAppTrigger.node.ts
Normal 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)],
|
||||
};
|
||||
}
|
||||
}
|
53
packages/nodes-base/nodes/WhatsApp/WhatsappDescription.ts
Normal file
53
packages/nodes-base/nodes/WhatsApp/WhatsappDescription.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
98
packages/nodes-base/nodes/WhatsApp/types.ts
Normal file
98
packages/nodes-base/nodes/WhatsApp/types.ts
Normal 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[];
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue