n8n/packages/nodes-base/nodes/WhatsApp/MessageFunctions.ts
Valya f63710a892
feat(WhatsApp Business node): WhatsApp node (#3659)
* 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>
2022-09-29 20:17:46 -04:00

260 lines
7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import set from 'lodash.set';
import { BinaryDataManager } from 'n8n-core';
import {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
IN8nHttpFullResponse,
INodeExecutionData,
JsonObject,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
import FormData from 'form-data';
interface WhatsAppApiError {
error: {
message: string;
type: string;
code: number;
fbtrace_id: string;
};
}
export async function addTemplateComponents(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
) {
const params = this.getNodeParameter('templateParameters') as IDataObject;
if (!params?.parameter) {
return requestOptions;
}
const components = [
{
type: 'body',
parameters: params.parameter,
},
];
if (!requestOptions.body) {
requestOptions.body = {};
}
set(requestOptions.body as {}, 'template.components', components);
return requestOptions;
}
export async function setType(this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions) {
const operation = this.getNodeParameter('operation') as string;
const messageType = this.getNodeParameter('messageType', null) as string | null;
let actualType = messageType;
if (operation === 'sendTemplate') {
actualType = 'template';
}
Object.assign(requestOptions.body, { type: actualType });
return requestOptions;
}
export async function mediaUploadFromItem(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
) {
const mediaPropertyName = this.getNodeParameter('mediaPropertyName') as string;
if (this.getInputData().binary?.[mediaPropertyName] === undefined) {
throw new NodeOperationError(
this.getNode(),
`The binary property "${mediaPropertyName}" does not exist. So no file can be written!`,
);
}
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 data = new FormData();
data.append('file', await BinaryDataManager.getInstance().retrieveBinaryData(binaryFile), {
contentType: mimeType,
filename: mediaFileName || binaryFileName,
});
data.append('messaging_product', 'whatsapp');
const phoneNumberId = this.getNodeParameter('phoneNumberId') as string;
const result = (await this.helpers.httpRequestWithAuthentication.call(this, 'whatsAppApi', {
url: `/${phoneNumberId}/media`,
baseURL: requestOptions.baseURL,
method: 'POST',
body: data,
})) as IDataObject;
const operation = this.getNodeParameter('messageType') as string;
if (!requestOptions.body) {
requestOptions.body = {};
}
set(requestOptions.body as {}, `${operation}.id`, result.id);
if (operation === 'document') {
set(requestOptions.body as {}, `${operation}.filename`, mediaFileName || binaryFileName);
}
return requestOptions;
}
export async function templateInfo(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const template = this.getNodeParameter('template') as string;
const [name, language] = template.split('|');
if (!requestOptions.body) {
requestOptions.body = {};
}
set(requestOptions.body as {}, 'template.name', name);
set(requestOptions.body as {}, 'template.language.code', language);
return requestOptions;
}
export async function componentsRequest(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const components = this.getNodeParameter('components') as IDataObject;
const componentsRet: object[] = [];
if (!components?.component) {
return requestOptions;
}
for (const component of components.component as IDataObject[]) {
const comp: IDataObject = {
type: component.type,
};
if (component.type === 'body') {
comp.parameters = (((component.bodyParameters as IDataObject)!.parameter as IDataObject[]) ||
[])!.map((i: IDataObject) => {
if (i.type === 'text') {
return i;
} else if (i.type === 'currency') {
return {
type: 'currency',
currency: {
code: i.code,
fallback_value: i.fallback_value,
amount_1000: (i.amount_1000 as number) * 1000,
},
};
} else if (i.type === 'date_time') {
return {
type: 'date_time',
date_time: {
fallback_value: i.date_time,
},
};
}
});
} else if (component.type === 'button') {
comp.index = component.index?.toString();
comp.sub_type = component.sub_type;
comp.parameters = [(component.buttonParameters as IDataObject).parameter];
} else if (component.type === 'header') {
comp.parameters = (
(component.headerParameters as IDataObject).parameter as IDataObject[]
).map((i: IDataObject) => {
if (i.type === 'image') {
return {
type: 'image',
image: {
link: i.imageLink,
},
};
}
return i;
});
}
componentsRet.push(comp);
}
if (!requestOptions.body) {
requestOptions.body = {};
}
set(requestOptions.body as {}, 'template.components', componentsRet);
return requestOptions;
}
export async function cleanPhoneNumber(
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
let phoneNumber = this.getNodeParameter('recipientPhoneNumber') as string;
phoneNumber = phoneNumber.replace(/[\-\(\)\+]/g, '');
if (!requestOptions.body) {
requestOptions.body = {};
}
set(requestOptions.body as {}, 'to', phoneNumber);
return requestOptions;
}
export async function sendErrorPostReceive(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (response.statusCode === 500) {
throw new NodeApiError(
this.getNode(),
{},
{
message: 'Sending failed',
description:
'If youre sending to a new test number, try sending a message to it from within the Meta developer portal first.',
httpCode: '500',
},
);
} else if (response.statusCode === 400) {
const error = { ...(response.body as WhatsAppApiError).error };
error.message = error.message.replace(/^\(#\d+\) /, '');
const messageType = this.getNodeParameter('messageType', 'media');
if (error.message.endsWith('is not a valid whatsapp business account media attachment ID')) {
throw new NodeApiError(
this.getNode(),
{ error },
{
message: `Invalid ${messageType} ID`,
description: error.message,
httpCode: '400',
},
);
} else if (error.message.endsWith('is not a valid URI.')) {
throw new NodeApiError(
this.getNode(),
{ error },
{
message: `Invalid ${messageType} URL`,
description: error.message,
httpCode: '400',
},
);
}
throw new NodeApiError(
this.getNode(),
{ ...(response as unknown as JsonObject), body: { error } },
{},
);
} else if (response.statusCode > 399) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject);
}
return data;
}