n8n-nodes-whatsapp-multi-session
Version:
n8n node for WhatsApp Multi-Session API integration
1,053 lines (1,052 loc) • 48.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WhatsAppMultiSession = void 0;
class WhatsAppMultiSession {
constructor() {
this.description = {
displayName: 'WhatsApp Multi-Session',
name: 'whatsAppMultiSession',
icon: 'file:whatsapp.svg',
group: ['communication'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with WhatsApp Multi-Session API',
defaults: {
name: 'WhatsApp Multi-Session',
},
inputs: [{ displayName: '', type: "main" /* NodeConnectionType.Main */ }],
outputs: [{ displayName: '', type: "main" /* NodeConnectionType.Main */ }],
credentials: [
{
name: 'whatsAppMultiSessionApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Session',
value: 'session',
},
{
name: 'Message',
value: 'message',
},
{
name: 'Contact',
value: 'contact',
},
{
name: 'Typing',
value: 'typing',
},
{
name: 'Webhook',
value: 'webhook',
},
],
default: 'message',
},
// Session Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['session'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new WhatsApp session',
action: 'Create a session',
},
{
name: 'Get',
value: 'get',
description: 'Get session information',
action: 'Get a session',
},
{
name: 'List',
value: 'list',
description: 'List all sessions',
action: 'List sessions',
},
{
name: 'Connect',
value: 'connect',
description: 'Connect a session (get QR code)',
action: 'Connect a session',
},
{
name: 'Disconnect',
value: 'disconnect',
description: 'Disconnect a session',
action: 'Disconnect a session',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a session',
action: 'Delete a session',
},
],
default: 'list',
},
// Message Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['message'],
},
},
options: [
{
name: 'Send Text',
value: 'sendText',
description: 'Send a text message',
action: 'Send a text message',
},
{
name: 'Send Image',
value: 'sendImage',
description: 'Send an image message',
action: 'Send an image message',
},
{
name: 'Send Document',
value: 'sendDocument',
description: 'Send a document',
action: 'Send a document',
},
{
name: 'Send Location',
value: 'sendLocation',
description: 'Send a location',
action: 'Send a location',
},
{
name: 'Forward Message',
value: 'forwardMessage',
description: 'Forward an existing message',
action: 'Forward a message',
},
{
name: 'Reply to Message',
value: 'replyMessage',
description: 'Reply to a specific message',
action: 'Reply to a message',
},
{
name: 'Download Image',
value: 'downloadImage',
description: 'Download an image from a received message',
action: 'Download an image',
},
],
default: 'sendText',
},
// Contact Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['contact'],
},
},
options: [
{
name: 'List',
value: 'list',
description: 'List contacts',
action: 'List contacts',
},
{
name: 'Check',
value: 'check',
description: 'Check if number is on WhatsApp',
action: 'Check contact',
},
],
default: 'list',
},
// Typing Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['typing'],
},
},
options: [
{
name: 'Start Typing',
value: 'startTyping',
description: 'Show typing indicator',
action: 'Start typing indicator',
},
{
name: 'Stop Typing',
value: 'stopTyping',
description: 'Hide typing indicator',
action: 'Stop typing indicator',
},
],
default: 'startTyping',
},
// Webhook Operations
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['webhook'],
},
},
options: [
{
name: 'Set URL',
value: 'setUrl',
description: 'Set webhook URL for receiving messages',
action: 'Set webhook URL',
},
{
name: 'Get URL',
value: 'getUrl',
description: 'Get current webhook URL',
action: 'Get webhook URL',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove webhook URL',
action: 'Remove webhook',
},
],
default: 'setUrl',
},
// Session ID field (used by most operations)
{
displayName: 'Session ID',
name: 'sessionId',
type: 'string',
required: true,
displayOptions: {
hide: {
resource: ['session'],
operation: ['list'],
},
},
default: '',
placeholder: 'session_123',
description: 'The WhatsApp session ID',
},
// Session creation fields
{
displayName: 'Session Name',
name: 'sessionName',
type: 'string',
displayOptions: {
show: {
resource: ['session'],
operation: ['create'],
},
},
default: '',
placeholder: 'My WhatsApp Session',
description: 'Display name for the session',
},
// Message fields
{
displayName: 'Phone Number',
name: 'phoneNumber',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
},
},
default: '',
placeholder: '6281234567890',
description: 'Recipient phone number (with country code, no +)',
},
{
displayName: 'Message Text',
name: 'messageText',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['sendText'],
},
},
default: '',
placeholder: 'Hello from n8n!',
description: 'The text message to send',
},
{
displayName: 'Image URL',
name: 'imageUrl',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['sendImage'],
},
},
default: '',
placeholder: 'https://example.com/image.jpg',
description: 'URL of the image to send',
},
{
displayName: 'Caption',
name: 'caption',
type: 'string',
displayOptions: {
show: {
resource: ['message'],
operation: ['sendImage'],
},
},
default: '',
placeholder: 'Image caption',
description: 'Optional caption for the image',
},
{
displayName: 'Document URL',
name: 'documentUrl',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['sendDocument'],
},
},
default: '',
placeholder: 'https://example.com/document.pdf',
description: 'URL of the document to send',
},
{
displayName: 'Filename',
name: 'filename',
type: 'string',
displayOptions: {
show: {
resource: ['message'],
operation: ['sendDocument'],
},
},
default: '',
placeholder: 'document.pdf',
description: 'Optional filename for the document',
},
{
displayName: 'Latitude',
name: 'latitude',
type: 'number',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['sendLocation'],
},
},
default: 0,
placeholder: '-6.2088',
description: 'Latitude coordinate',
},
{
displayName: 'Longitude',
name: 'longitude',
type: 'number',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['sendLocation'],
},
},
default: 0,
placeholder: '106.8456',
description: 'Longitude coordinate',
},
{
displayName: 'Location Name',
name: 'locationName',
type: 'string',
displayOptions: {
show: {
resource: ['message'],
operation: ['sendLocation'],
},
},
default: '',
placeholder: 'Jakarta, Indonesia',
description: 'Optional name for the location',
},
// Forward message fields
{
displayName: 'Message ID to Forward',
name: 'forwardMessageId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['forwardMessage'],
},
},
default: '',
placeholder: '3EB0D136B13F32830F7B88',
description: 'ID of the message to forward (from webhook trigger data: {{$json.id}}). Must be from the same session.',
},
{
displayName: 'Message Text',
name: 'forwardMessageText',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['forwardMessage'],
},
},
default: '',
placeholder: 'Hello, this is the message content to forward',
description: 'The text content of the message to forward (from webhook trigger data: {{$json.message.text || $json.message.conversation}})',
},
// Reply message fields
{
displayName: 'Reply Text',
name: 'replyText',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['replyMessage'],
},
},
default: '',
placeholder: 'Thank you for your message!',
description: 'The reply message text',
},
{
displayName: 'Quoted Message ID',
name: 'quotedMessageId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['replyMessage'],
},
},
default: '',
placeholder: '3EB0D136B13F32830F7B88',
description: 'ID of the message to reply to (from webhook trigger data: {{$json.id}}). Must be from the same session.',
},
// Download Image fields
{
displayName: 'Media URL',
name: 'mediaUrl',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['message'],
operation: ['downloadImage'],
},
},
default: '',
placeholder: '/api/media/temp/filename.jpg?expires=1234567890',
description: 'Media URL from webhook trigger data ({{$json.media_url}}). This should be the temporary media URL.',
},
{
displayName: 'Output Format',
name: 'outputFormat',
type: 'options',
displayOptions: {
show: {
resource: ['message'],
operation: ['downloadImage'],
},
},
options: [
{
name: 'Base64',
value: 'base64',
description: 'Return image as base64 encoded string',
},
{
name: 'Binary',
value: 'binary',
description: 'Return image as binary data buffer',
},
{
name: 'File',
value: 'file',
description: 'Return image as downloadable file attachment',
},
],
default: 'base64',
description: 'Format for the downloaded image data',
},
{
displayName: 'Put Output File in Field',
name: 'outputFileField',
type: 'string',
displayOptions: {
show: {
resource: ['message'],
operation: ['downloadImage'],
outputFormat: ['file'],
},
},
default: 'data',
placeholder: 'data',
description: 'Name of the binary property to write the file to',
},
// Contact check field
{
displayName: 'Phone Number',
name: 'phoneNumber',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['contact'],
operation: ['check'],
},
},
default: '',
placeholder: '6281234567890',
description: 'Phone number to check (with country code, no +)',
},
// Typing fields
{
displayName: 'Phone Number',
name: 'phoneNumber',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['typing'],
},
},
default: '',
placeholder: '6281234567890',
description: 'Phone number to send typing indicator to (with country code, no +)',
},
// Webhook fields
{
displayName: 'Webhook URL',
name: 'webhookUrl',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['webhook'],
operation: ['setUrl'],
},
},
default: '',
placeholder: 'https://your-server.com/webhook',
description: 'URL to receive webhook notifications',
},
],
};
}
async execute() {
var _a, _b, _c;
const items = this.getInputData();
const returnData = [];
const credentials = await this.getCredentials('whatsAppMultiSessionApi');
const baseUrl = credentials.serverUrl;
// Use API key for authentication
const authHeaders = {
'Authorization': `Bearer ${credentials.apiKey}`,
'Content-Type': 'application/json',
};
for (let i = 0; i < items.length; i++) {
const resource = this.getNodeParameter('resource', i);
const operation = this.getNodeParameter('operation', i);
try {
if (resource === 'session') {
if (operation === 'list') {
const response = await this.helpers.request({
method: 'GET',
url: `${baseUrl}/api/sessions`,
headers: authHeaders,
json: true,
});
response.sessions.forEach((session) => {
returnData.push({ json: session });
});
}
else if (operation === 'create') {
const sessionName = this.getNodeParameter('sessionName', i);
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions`,
headers: authHeaders,
body: {
name: sessionName,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'get') {
const sessionId = this.getNodeParameter('sessionId', i);
const response = await this.helpers.request({
method: 'GET',
url: `${baseUrl}/api/sessions/${sessionId}`,
headers: authHeaders,
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'connect') {
const sessionId = this.getNodeParameter('sessionId', i);
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/connect`,
headers: authHeaders,
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'disconnect') {
const sessionId = this.getNodeParameter('sessionId', i);
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/disconnect`,
headers: authHeaders,
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'delete') {
const sessionId = this.getNodeParameter('sessionId', i);
const response = await this.helpers.request({
method: 'DELETE',
url: `${baseUrl}/api/sessions/${sessionId}`,
headers: authHeaders,
json: true,
});
returnData.push({ json: response });
}
}
else if (resource === 'message') {
const sessionId = this.getNodeParameter('sessionId', i);
const phoneNumber = this.getNodeParameter('phoneNumber', i);
if (operation === 'sendText') {
const messageText = this.getNodeParameter('messageText', i);
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/send`,
headers: authHeaders,
body: {
to: phoneNumber,
message: messageText,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'sendImage') {
const imageUrl = this.getNodeParameter('imageUrl', i);
const caption = this.getNodeParameter('caption', i, '');
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/send-image`,
headers: authHeaders,
body: {
to: phoneNumber,
image_url: imageUrl,
caption: caption,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'sendDocument') {
const documentUrl = this.getNodeParameter('documentUrl', i);
const filename = this.getNodeParameter('filename', i, '');
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/send-document`,
headers: authHeaders,
body: {
to: phoneNumber,
document_url: documentUrl,
filename: filename,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'sendLocation') {
const latitude = this.getNodeParameter('latitude', i);
const longitude = this.getNodeParameter('longitude', i);
const locationName = this.getNodeParameter('locationName', i, '');
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/send-location`,
headers: authHeaders,
body: {
to: phoneNumber,
latitude: latitude,
longitude: longitude,
name: locationName,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'forwardMessage') {
const forwardMessageId = this.getNodeParameter('forwardMessageId', i);
const forwardMessageText = this.getNodeParameter('forwardMessageText', i);
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/forward`,
headers: authHeaders,
body: {
to: phoneNumber,
message_id: forwardMessageId,
text: forwardMessageText,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'replyMessage') {
const replyText = this.getNodeParameter('replyText', i);
const quotedMessageId = this.getNodeParameter('quotedMessageId', i);
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/reply`,
headers: authHeaders,
body: {
to: phoneNumber,
message: replyText,
quoted_message_id: quotedMessageId,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'downloadImage') {
const mediaUrl = this.getNodeParameter('mediaUrl', i);
const outputFormat = this.getNodeParameter('outputFormat', i, 'base64');
// Construct full URL if mediaUrl is relative
const fullMediaUrl = mediaUrl.startsWith('http') ? mediaUrl : `${baseUrl}${mediaUrl}`;
try {
// Download the image with authentication
const response = await this.helpers.request({
method: 'GET',
url: fullMediaUrl,
headers: {
'Authorization': `Bearer ${credentials.apiKey}`,
},
encoding: null,
resolveWithFullResponse: true,
simple: false, // Don't throw on HTTP error status codes
});
// Handle HTTP error responses
if (response.statusCode !== 200) {
let errorMessage = `HTTP ${response.statusCode}`;
let errorDetails = '';
// Try to parse error response body
try {
const errorBody = ((_a = response.body) === null || _a === void 0 ? void 0 : _a.toString()) || '';
// Try to parse as JSON first
try {
const errorJson = JSON.parse(errorBody);
errorMessage = errorJson.message || errorJson.error || errorMessage;
errorDetails = errorJson.details || '';
}
catch (jsonError) {
// If not JSON, use plain text
errorMessage = errorBody.trim() || errorMessage;
}
}
catch (parseError) {
// If can't parse body, use status message
errorMessage = response.statusMessage || errorMessage;
}
// Handle specific error cases
if (response.statusCode === 410) {
errorMessage = 'Media link has expired';
errorDetails = 'The temporary media URL has expired. Please request a new media URL from the webhook.';
}
else if (response.statusCode === 404) {
errorMessage = 'Media file not found';
errorDetails = 'The requested media file could not be found on the server.';
}
else if (response.statusCode === 401) {
errorMessage = 'Authentication failed';
errorDetails = 'Invalid API key or insufficient permissions.';
}
else if (response.statusCode === 403) {
errorMessage = 'Access forbidden';
errorDetails = 'Access to the media file is forbidden.';
}
// Return structured error response instead of throwing
returnData.push({
json: {
success: false,
error: true,
errorCode: response.statusCode,
errorMessage: errorMessage,
errorDetails: errorDetails,
mediaUrl: mediaUrl,
format: outputFormat,
}
});
continue; // Continue to next item instead of throwing
}
// Extract filename from URL
const urlParts = mediaUrl.split('/');
const filenameWithParams = urlParts[urlParts.length - 1];
const filename = filenameWithParams.split('?')[0]; // Remove query parameters
// Determine content type from URL or default
const ext = (_b = filename.split('.').pop()) === null || _b === void 0 ? void 0 : _b.toLowerCase();
let mimeType = 'application/octet-stream';
if (ext === 'jpg' || ext === 'jpeg')
mimeType = 'image/jpeg';
else if (ext === 'png')
mimeType = 'image/png';
else if (ext === 'gif')
mimeType = 'image/gif';
else if (ext === 'pdf')
mimeType = 'application/pdf';
else if (ext === 'mp4')
mimeType = 'video/mp4';
else if (ext === 'mp3')
mimeType = 'audio/mpeg';
else if (ext === 'ogg')
mimeType = 'audio/ogg';
const responseBody = response.body;
if (outputFormat === 'base64') {
// Return as base64
const base64Data = Buffer.from(responseBody).toString('base64');
returnData.push({
json: {
success: true,
message: 'Image downloaded successfully',
data: base64Data,
size: responseBody.length,
mimeType: mimeType,
format: 'base64',
filename: filename,
}
});
}
else if (outputFormat === 'binary') {
// Return as binary buffer
returnData.push({
json: {
success: true,
message: 'Image downloaded successfully',
data: responseBody,
size: responseBody.length,
mimeType: mimeType,
format: 'binary',
filename: filename,
}
});
}
else if (outputFormat === 'file') {
// Return as n8n binary file attachment
const outputFileField = this.getNodeParameter('outputFileField', i, 'data');
const binaryData = {
data: Buffer.from(responseBody).toString('base64'),
mimeType: mimeType,
fileName: filename,
fileExtension: ext || '',
};
// Create the return item with binary data
returnData.push({
json: {
success: true,
message: 'Image downloaded successfully',
size: responseBody.length,
mimeType: mimeType,
format: 'file',
filename: filename,
},
binary: {
[outputFileField]: binaryData,
},
});
}
}
catch (downloadError) {
// Handle network errors and other exceptions
const error = downloadError;
let errorMessage = 'Network error or request failed';
let errorDetails = error.message || '';
// Try to extract more specific error information
if (error.code) {
errorMessage = `Network error: ${error.code}`;
}
else if ((_c = error.response) === null || _c === void 0 ? void 0 : _c.statusCode) {
errorMessage = `HTTP ${error.response.statusCode}`;
if (error.response.body) {
try {
const errorBody = error.response.body.toString();
const errorJson = JSON.parse(errorBody);
errorDetails = errorJson.message || errorJson.error || errorBody;
}
catch {
errorDetails = error.response.body.toString();
}
}
}
// Return structured error response instead of throwing
returnData.push({
json: {
success: false,
error: true,
errorMessage: errorMessage,
errorDetails: errorDetails,
mediaUrl: mediaUrl,
format: outputFormat,
}
});
}
}
}
else if (resource === 'contact') {
const sessionId = this.getNodeParameter('sessionId', i);
if (operation === 'list') {
const response = await this.helpers.request({
method: 'GET',
url: `${baseUrl}/api/sessions/${sessionId}/contacts`,
headers: authHeaders,
json: true,
});
response.contacts.forEach((contact) => {
returnData.push({ json: contact });
});
}
else if (operation === 'check') {
const phoneNumber = this.getNodeParameter('phoneNumber', i);
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/check-phone`,
headers: authHeaders,
body: {
to: phoneNumber,
},
json: true,
});
returnData.push({ json: response });
}
}
else if (resource === 'typing') {
const sessionId = this.getNodeParameter('sessionId', i);
const phoneNumber = this.getNodeParameter('phoneNumber', i);
if (operation === 'startTyping') {
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/typing`,
headers: authHeaders,
body: {
to: phoneNumber,
typing: true,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'stopTyping') {
const response = await this.helpers.request({
method: 'POST',
url: `${baseUrl}/api/sessions/${sessionId}/typing`,
headers: authHeaders,
body: {
to: phoneNumber,
typing: false,
},
json: true,
});
returnData.push({ json: response });
}
}
else if (resource === 'webhook') {
const sessionId = this.getNodeParameter('sessionId', i);
if (operation === 'setUrl') {
const webhookUrl = this.getNodeParameter('webhookUrl', i);
const response = await this.helpers.request({
method: 'PUT',
url: `${baseUrl}/api/sessions/${sessionId}/webhook`,
headers: authHeaders,
body: {
webhook_url: webhookUrl,
},
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'getUrl') {
const response = await this.helpers.request({
method: 'GET',
url: `${baseUrl}/api/sessions/${sessionId}/webhook`,
headers: authHeaders,
json: true,
});
returnData.push({ json: response });
}
else if (operation === 'remove') {
const response = await this.helpers.request({
method: 'DELETE',
url: `${baseUrl}/api/sessions/${sessionId}/webhook`,
headers: authHeaders,
json: true,
});
returnData.push({ json: response });
}
}
}
catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
}
else {
throw error;
}
}
}
return [returnData];
}
}
exports.WhatsAppMultiSession = WhatsAppMultiSession;