mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
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>
This commit is contained in:
parent
f37d6ba03b
commit
f63710a892
|
@ -492,7 +492,6 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
* gzip (ignored - default already works)
|
||||
* resolveWithFullResponse (implemented elsewhere)
|
||||
*/
|
||||
|
||||
return axiosConfig;
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
|
|
58
packages/nodes-base/credentials/WhatsAppApi.credentials.ts
Normal file
58
packages/nodes-base/credentials/WhatsAppApi.credentials.ts
Normal file
|
@ -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
packages/nodes-base/nodes/WhatsApp/MediaDescription.ts
Normal file
184
packages/nodes-base/nodes/WhatsApp/MediaDescription.ts
Normal file
|
@ -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
packages/nodes-base/nodes/WhatsApp/MediaFunctions.ts
Normal file
44
packages/nodes-base/nodes/WhatsApp/MediaFunctions.ts
Normal file
|
@ -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;
|
||||
}
|
259
packages/nodes-base/nodes/WhatsApp/MessageFunctions.ts
Normal file
259
packages/nodes-base/nodes/WhatsApp/MessageFunctions.ts
Normal file
|
@ -0,0 +1,259 @@
|
|||
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 you’re 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;
|
||||
}
|
1508
packages/nodes-base/nodes/WhatsApp/MessagesDescription.ts
Normal file
1508
packages/nodes-base/nodes/WhatsApp/MessagesDescription.ts
Normal file
File diff suppressed because it is too large
Load diff
53
packages/nodes-base/nodes/WhatsApp/WhatsApp.node.ts
Normal file
53
packages/nodes-base/nodes/WhatsApp/WhatsApp.node.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import { messageFields, messageTypeFields } from './MessagesDescription';
|
||||
import { mediaFields, mediaTypeFields } from './MediaDescription';
|
||||
|
||||
export class WhatsApp implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'WhatsApp Business Cloud',
|
||||
name: 'whatsApp',
|
||||
icon: 'file:whatsapp.svg',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
subtitle: '={{ $parameter["resource"] + ": " + $parameter["operation"] }}',
|
||||
description: 'Access WhatsApp API',
|
||||
defaults: {
|
||||
name: 'WhatsApp',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'whatsAppApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
baseURL: 'https://graph.facebook.com/v13.0/',
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Message',
|
||||
value: 'message',
|
||||
},
|
||||
{
|
||||
name: 'Media',
|
||||
value: 'media',
|
||||
},
|
||||
],
|
||||
default: 'message',
|
||||
},
|
||||
...messageFields,
|
||||
...mediaFields,
|
||||
...messageTypeFields,
|
||||
...mediaTypeFields,
|
||||
],
|
||||
};
|
||||
}
|
7
packages/nodes-base/nodes/WhatsApp/whatsapp.svg
Normal file
7
packages/nodes-base/nodes/WhatsApp/whatsapp.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 48 48" width="60px" height="60px">
|
||||
<path fill="#fff" d="m4.868 43.303 2.694-9.835a18.941 18.941 0 0 1-2.535-9.489C5.032 13.514 13.548 5 24.014 5a18.867 18.867 0 0 1 13.43 5.566A18.866 18.866 0 0 1 43 23.994c-.004 10.465-8.522 18.98-18.986 18.98-.001 0 0 0 0 0h-.008a18.965 18.965 0 0 1-9.073-2.311l-10.065 2.64z"/>
|
||||
<path fill="#fff" d="M4.868 43.803a.499.499 0 0 1-.482-.631l2.639-9.636a19.48 19.48 0 0 1-2.497-9.556C4.532 13.238 13.273 4.5 24.014 4.5a19.367 19.367 0 0 1 13.784 5.713A19.362 19.362 0 0 1 43.5 23.994c-.004 10.741-8.746 19.48-19.486 19.48a19.535 19.535 0 0 1-9.144-2.277l-9.875 2.589a.457.457 0 0 1-.127.017z"/>
|
||||
<path fill="#cfd8dc" d="M24.014 5a18.867 18.867 0 0 1 13.43 5.566A18.866 18.866 0 0 1 43 23.994c-.004 10.465-8.522 18.98-18.986 18.98h-.008a18.965 18.965 0 0 1-9.073-2.311l-10.065 2.64 2.694-9.835a18.941 18.941 0 0 1-2.535-9.489C5.032 13.514 13.548 5 24.014 5m0-1C12.998 4 4.032 12.962 4.027 23.979a20.01 20.01 0 0 0 2.461 9.622L3.903 43.04a.998.998 0 0 0 1.219 1.231l9.687-2.54a20.026 20.026 0 0 0 9.197 2.244c11.024 0 19.99-8.963 19.995-19.98A19.856 19.856 0 0 0 38.153 9.86 19.869 19.869 0 0 0 24.014 4z"/>
|
||||
<path fill="#40c351" d="M35.176 12.832a15.673 15.673 0 0 0-11.157-4.626c-8.704 0-15.783 7.076-15.787 15.774a15.738 15.738 0 0 0 2.413 8.396l.376.597-1.595 5.821 5.973-1.566.577.342a15.75 15.75 0 0 0 8.032 2.199h.006c8.698 0 15.777-7.077 15.78-15.776a15.68 15.68 0 0 0-4.618-11.161z"/>
|
||||
<path fill="#fff" d="M19.268 16.045c-.355-.79-.729-.806-1.068-.82-.277-.012-.593-.011-.909-.011-.316 0-.83.119-1.265.594-.435.475-1.661 1.622-1.661 3.956 0 2.334 1.7 4.59 1.937 4.906.237.316 3.282 5.259 8.104 7.161 4.007 1.58 4.823 1.266 5.693 1.187.87-.079 2.807-1.147 3.202-2.255.395-1.108.395-2.057.277-2.255-.119-.198-.435-.316-.909-.554s-2.807-1.385-3.242-1.543c-.435-.158-.751-.237-1.068.238-.316.474-1.225 1.543-1.502 1.859-.277.317-.554.357-1.028.119s-2.002-.738-3.815-2.354c-1.41-1.257-2.362-2.81-2.639-3.285-.277-.474-.03-.731.208-.968.213-.213.474-.554.712-.831.237-.277.316-.475.474-.791.158-.317.079-.594-.04-.831-.117-.238-1.039-2.584-1.461-3.522z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -318,6 +318,7 @@
|
|||
"dist/credentials/WebflowApi.credentials.js",
|
||||
"dist/credentials/WebflowOAuth2Api.credentials.js",
|
||||
"dist/credentials/WekanApi.credentials.js",
|
||||
"dist/credentials/WhatsAppApi.credentials.js",
|
||||
"dist/credentials/WiseApi.credentials.js",
|
||||
"dist/credentials/WooCommerceApi.credentials.js",
|
||||
"dist/credentials/WordpressApi.credentials.js",
|
||||
|
@ -684,6 +685,7 @@
|
|||
"dist/nodes/Webflow/WebflowTrigger.node.js",
|
||||
"dist/nodes/Webhook/Webhook.node.js",
|
||||
"dist/nodes/Wekan/Wekan.node.js",
|
||||
"dist/nodes/WhatsApp/WhatsApp.node.js",
|
||||
"dist/nodes/Wise/Wise.node.js",
|
||||
"dist/nodes/Wise/WiseTrigger.node.js",
|
||||
"dist/nodes/WooCommerce/WooCommerce.node.js",
|
||||
|
@ -751,6 +753,7 @@
|
|||
"cheerio": "1.0.0-rc.6",
|
||||
"chokidar": "3.5.2",
|
||||
"cron": "~1.7.2",
|
||||
"currency-codes": "^2.1.0",
|
||||
"eventsource": "^2.0.2",
|
||||
"fast-glob": "^3.2.5",
|
||||
"fflate": "^0.7.0",
|
||||
|
|
|
@ -217,7 +217,12 @@ export class RoutingNode {
|
|||
returnData.push({ json: {}, error: error.message });
|
||||
continue;
|
||||
}
|
||||
throw new NodeApiError(this.node, error, { runIndex, itemIndex: i });
|
||||
throw new NodeApiError(this.node, error, {
|
||||
runIndex,
|
||||
itemIndex: i,
|
||||
message: error?.message,
|
||||
description: error?.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue