Skip to content

Commit

Permalink
feat(WhatsApp Business node): WhatsApp node (#3659)
Browse files Browse the repository at this point in the history
* feat: base structure for whatsapp node with credentials

* feat: messages operation

* feat: create generic api call with credentials and test first operation

* fix: add missing template params

* fix: language code for template

* feat: media type and start of template components

* fix: remove provider name from media type

* lintfix

* fix: format

* feat: media operations w/o upload media type

* ♻️ Convert WhatsApp Business node to declarative style

* 🐛 form data not being sent with boundary in header

* ✨ add media operations to WhatsApp

* ✨ add credentials test to WhatsApp credentials

* ♻️ move preview url to optional collection in whatsapp message

* ♻️ renamed media operations in whatsapp node

* :refactor: move media file name to optional fields in whatsapp node

* ✨ add upload from n8n for whatsapp node message resource

* 🔥 remove other template component types in whatsapp node

* :speech_bubble: add specialised text for media types in WhatsApp node

* ⚡ Load dinamically phone number and template name

* ⚡ Add action property to all operations

* 🔥 Remove unnecessary imports

* ⚡ Use getBinaryDataBuffer helper

* ⚡ Add components property

* ✨ send components for whatsapp templates and template language

* 🏷️ fix WhatsApp node message function types

* 🏷️ fix any in whatsapp message functions

* 🔥 remove unused import

* ⚡ Improvements

* ⚡ Add send location

* ⚡ Add send contact

* ⚡ Small improvement

* ♻️ changes for review

* 🐛 fix presend error

* ♻️ change lat/long to numbers with proper clamping

* fix: bad merge

* refactor: changes for review

* update package-lock.json

* update package.-lock.json

* update

Co-authored-by: cxgarcia <schlaubitzcristobal@gmail.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
  • Loading branch information
3 people authored Sep 30, 2022
1 parent f37d6ba commit f63710a
Show file tree
Hide file tree
Showing 10 changed files with 2,126 additions and 3 deletions.
6 changes: 4 additions & 2 deletions packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,6 @@ async function parseRequestObject(requestObject: IDataObject) {
* gzip (ignored - default already works)
* resolveWithFullResponse (implemented elsewhere)
*/

return axiosConfig;
}

Expand Down Expand Up @@ -737,7 +736,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
// We are only setting content type headers if the user did
// not set it already manually. We're not overriding, even if it's wrong.
if (body instanceof FormData) {
axiosRequest.headers['Content-Type'] = 'multipart/form-data';
axiosRequest.headers = {
...axiosRequest.headers,
...body.getHeaders(),
};
} else if (body instanceof URLSearchParams) {
axiosRequest.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
Expand Down
58 changes: 58 additions & 0 deletions packages/nodes-base/credentials/WhatsAppApi.credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
IAuthenticateGeneric,
ICredentialDataDecryptedObject,
ICredentialTestRequest,
ICredentialType,
IHttpRequestOptions,
INodeProperties,
NodePropertyTypes,
} from 'n8n-workflow';

export class WhatsAppApi implements ICredentialType {
name = 'whatsAppApi';
displayName = 'WhatsApp API';
documentationUrl = 'whatsApp';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
type: 'string',
name: 'accessToken',
default: '',
required: true,
},
{
displayName: 'Bussiness Account ID',
type: 'string',
name: 'businessAccountId',
default: '',
required: true,
},
];

authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.accessToken}}',
},
},
};

test: ICredentialTestRequest = {
request: {
baseURL: 'https://graph.facebook.com/v13.0',
url: '/',
ignoreHttpStatusErrors: true,
},
rules: [
{
type: 'responseSuccessBody',
properties: {
key: 'error.type',
value: 'OAuthException',
message: 'Invalid access token',
},
},
],
};
}
184 changes: 184 additions & 0 deletions packages/nodes-base/nodes/WhatsApp/MediaDescription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { INodeProperties } from 'n8n-workflow';
import { setupUpload } from './MediaFunctions';

export const mediaFields: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
noDataExpression: true,
type: 'options',
placeholder: '',
options: [
{
name: 'Upload',
value: 'mediaUpload',
action: 'Upload media',
},
{
name: 'Download',
value: 'mediaUrlGet',
action: 'Download media',
},
{
name: 'Delete',
value: 'mediaDelete',
action: 'Delete media',
},
],
default: 'mediaUpload',
displayOptions: {
show: {
resource: ['media'],
},
},
// eslint-disable-next-line n8n-nodes-base/node-param-description-weak
description: 'The operation to perform on the media',
},
];

export const mediaTypeFields: INodeProperties[] = [
// ----------------------------------
// operation: mediaUpload
// ----------------------------------
{
displayName: 'Sender Phone Number (or ID)',
name: 'phoneNumberId',
type: 'options',
typeOptions: {
loadOptions: {
routing: {
request: {
url: '={{$credentials.businessAccountId}}/phone_numbers',
method: 'GET',
},
output: {
postReceive: [
{
type: 'rootProperty',
properties: {
property: 'data',
},
},
{
type: 'setKeyValue',
properties: {
name: '={{$responseItem.display_phone_number}} - {{$responseItem.verified_name}}',
value: '={{$responseItem.id}}',
},
},
{
type: 'sort',
properties: {
key: 'name',
},
},
],
},
},
},
},
default: '',
placeholder: '',
routing: {
request: {
method: 'POST',
url: '={{$value}}/media',
},
},
displayOptions: {
show: {
operation: ['mediaUpload'],
resource: ['media'],
},
},
required: true,
description: "The ID of the business account's phone number to store the media",
},
{
displayName: 'Property Name',
name: 'mediaPropertyName',
type: 'string',
default: 'data',
displayOptions: {
show: {
operation: ['mediaUpload'],
resource: ['media'],
},
},
required: true,
description: 'Name of the binary property which contains the data for the file to be uploaded',
routing: {
send: {
preSend: [setupUpload],
},
},
},
// ----------------------------------
// type: mediaUrlGet
// ----------------------------------
{
displayName: 'Media ID',
name: 'mediaGetId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['mediaUrlGet'],
resource: ['media'],
},
},
routing: {
request: {
method: 'GET',
url: '=/{{$value}}',
},
},
required: true,
description: 'The ID of the media',
},
// ----------------------------------
// type: mediaUrlGet
// ----------------------------------
{
displayName: 'Media ID',
name: 'mediaDeleteId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: ['mediaDelete'],
resource: ['media'],
},
},
routing: {
request: {
method: 'DELETE',
url: '=/{{$value}}',
},
},
required: true,
description: 'The ID of the media',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['media'],
operation: ['mediaUpload'],
},
},
options: [
{
displayName: 'Filename',
name: 'mediaFileName',
type: 'string',
default: '',
description: 'The name to use for the file',
},
],
},
];
44 changes: 44 additions & 0 deletions packages/nodes-base/nodes/WhatsApp/MediaFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
NodeOperationError,
} from 'n8n-workflow';

import FormData from 'form-data';

export async function setupUpload(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
) {
const mediaPropertyName = this.getNodeParameter('mediaPropertyName') as string;
if (!mediaPropertyName) {
return requestOptions;
}
if (this.getInputData().binary?.[mediaPropertyName] === undefined || !mediaPropertyName.trim()) {
throw new NodeOperationError(this.getNode(), 'Could not find file in node input data', {
description: `There’s no key called '${mediaPropertyName}' with binary data in it`,
});
}
const binaryFile = this.getInputData().binary![mediaPropertyName]!;
const mediaFileName = (this.getNodeParameter('additionalFields') as IDataObject).mediaFileName as
| string
| undefined;
const binaryFileName = binaryFile.fileName;
if (!mediaFileName && !binaryFileName) {
throw new NodeOperationError(this.getNode(), 'No file name given for media upload.');
}
const mimeType = binaryFile.mimeType;

const buffer = await this.helpers.getBinaryDataBuffer(mediaPropertyName);

const data = new FormData();
data.append('file', buffer, {
contentType: mimeType,
filename: mediaFileName || binaryFileName,
});
data.append('messaging_product', 'whatsapp');

requestOptions.body = data;
return requestOptions;
}
Loading

0 comments on commit f63710a

Please sign in to comment.