n8n-nodes-whaapy 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
- import { INodeType, INodeTypeDescription } from 'n8n-workflow';
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
2
  export declare class Whaapy implements INodeType {
3
3
  description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
4
5
  }
@@ -1,6 +1,82 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Whaapy = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ // Helper function to convert string to slug (for auto-generating IDs)
6
+ function slugify(text) {
7
+ return text
8
+ .toString()
9
+ .toLowerCase()
10
+ .trim()
11
+ .replace(/\s+/g, '_')
12
+ .replace(/[^\w\-]+/g, '')
13
+ .replace(/\-\-+/g, '_')
14
+ .replace(/^-+/, '')
15
+ .replace(/-+$/, '')
16
+ .substring(0, 256);
17
+ }
18
+ // Build interactive message payload from structured fields
19
+ function buildInteractivePayload(params) {
20
+ // If using raw JSON, return it directly
21
+ if (params.useRawJson && params.rawJson) {
22
+ return params.rawJson;
23
+ }
24
+ const interactive = {
25
+ type: params.interactiveType,
26
+ body: {
27
+ text: params.bodyText,
28
+ },
29
+ };
30
+ // Add header if specified
31
+ if (params.headerType && params.headerType !== 'none') {
32
+ if (params.headerType === 'text' && params.headerText) {
33
+ interactive.header = {
34
+ type: 'text',
35
+ text: params.headerText,
36
+ };
37
+ }
38
+ else if (['image', 'video', 'document'].includes(params.headerType) && params.headerMediaUrl) {
39
+ interactive.header = {
40
+ type: params.headerType,
41
+ [params.headerType]: {
42
+ link: params.headerMediaUrl,
43
+ },
44
+ };
45
+ }
46
+ }
47
+ // Add footer if specified
48
+ if (params.footerText) {
49
+ interactive.footer = {
50
+ text: params.footerText,
51
+ };
52
+ }
53
+ // Build action based on type
54
+ if (params.interactiveType === 'button' && params.buttons && params.buttons.length > 0) {
55
+ interactive.action = {
56
+ buttons: params.buttons.map((btn) => ({
57
+ type: 'reply',
58
+ reply: {
59
+ id: btn.id || slugify(btn.title),
60
+ title: btn.title.substring(0, 20),
61
+ },
62
+ })),
63
+ };
64
+ }
65
+ else if (params.interactiveType === 'list' && params.sections && params.sections.length > 0) {
66
+ interactive.action = {
67
+ button: params.listButtonText || 'Ver Opciones',
68
+ sections: params.sections.map((section) => ({
69
+ title: section.title || undefined,
70
+ rows: section.rows.map((row) => ({
71
+ id: row.id || slugify(row.title),
72
+ title: row.title.substring(0, 24),
73
+ description: row.description ? row.description.substring(0, 72) : undefined,
74
+ })),
75
+ })),
76
+ };
77
+ }
78
+ return interactive;
79
+ }
4
80
  class Whaapy {
5
81
  constructor() {
6
82
  this.description = {
@@ -279,19 +355,270 @@ class Whaapy {
279
355
  },
280
356
  ],
281
357
  },
282
- // Message: Send - Interactive content
358
+ // ===========================================
359
+ // INTERACTIVE MESSAGE FIELDS (Structured)
360
+ // ===========================================
361
+ // Interactive: Type selector (button or list)
283
362
  {
284
- displayName: 'Interactive Content',
285
- name: 'interactiveContent',
286
- type: 'json',
363
+ displayName: 'Interactive Type',
364
+ name: 'interactiveType',
365
+ type: 'options',
287
366
  required: true,
288
- default: '{}',
289
- description: 'Interactive message content (buttons, lists)',
367
+ options: [
368
+ { name: 'Buttons (Reply Buttons)', value: 'button' },
369
+ { name: 'List (Menu)', value: 'list' },
370
+ ],
371
+ default: 'button',
372
+ description: 'Type of interactive message. Buttons show up to 3 options, Lists show a menu with sections.',
290
373
  displayOptions: {
291
374
  show: { resource: ['message'], operation: ['send'], messageType: ['interactive'] },
292
375
  },
293
- routing: {
294
- send: { type: 'body', property: 'interactive' },
376
+ },
377
+ // Interactive: Body text (required)
378
+ {
379
+ displayName: 'Body Text',
380
+ name: 'interactiveBodyText',
381
+ type: 'string',
382
+ typeOptions: { rows: 3 },
383
+ required: true,
384
+ default: '',
385
+ placeholder: '¿Cómo podemos ayudarte hoy?',
386
+ description: 'Main text of the message. Max 1024 characters.',
387
+ displayOptions: {
388
+ show: { resource: ['message'], operation: ['send'], messageType: ['interactive'] },
389
+ },
390
+ },
391
+ // Interactive: Header type (optional)
392
+ {
393
+ displayName: 'Header Type',
394
+ name: 'interactiveHeaderType',
395
+ type: 'options',
396
+ default: 'none',
397
+ options: [
398
+ { name: 'None', value: 'none' },
399
+ { name: 'Text', value: 'text' },
400
+ { name: 'Image', value: 'image' },
401
+ { name: 'Video', value: 'video' },
402
+ { name: 'Document', value: 'document' },
403
+ ],
404
+ description: 'Optional header for the message',
405
+ displayOptions: {
406
+ show: { resource: ['message'], operation: ['send'], messageType: ['interactive'] },
407
+ },
408
+ },
409
+ // Interactive: Header text (if type=text)
410
+ {
411
+ displayName: 'Header Text',
412
+ name: 'interactiveHeaderText',
413
+ type: 'string',
414
+ default: '',
415
+ placeholder: '🍕 Pizzería Whaapy',
416
+ description: 'Header text. Max 60 characters.',
417
+ displayOptions: {
418
+ show: {
419
+ resource: ['message'],
420
+ operation: ['send'],
421
+ messageType: ['interactive'],
422
+ interactiveHeaderType: ['text'],
423
+ },
424
+ },
425
+ },
426
+ // Interactive: Header media URL (if type=image|video|document)
427
+ {
428
+ displayName: 'Header Media URL',
429
+ name: 'interactiveHeaderMediaUrl',
430
+ type: 'string',
431
+ default: '',
432
+ placeholder: 'https://example.com/image.jpg',
433
+ description: 'Public URL of the media file for header',
434
+ displayOptions: {
435
+ show: {
436
+ resource: ['message'],
437
+ operation: ['send'],
438
+ messageType: ['interactive'],
439
+ interactiveHeaderType: ['image', 'video', 'document'],
440
+ },
441
+ },
442
+ },
443
+ // Interactive: Footer text (optional)
444
+ {
445
+ displayName: 'Footer Text',
446
+ name: 'interactiveFooterText',
447
+ type: 'string',
448
+ default: '',
449
+ placeholder: 'Responde con una opción',
450
+ description: 'Optional footer text in gray. Max 60 characters.',
451
+ displayOptions: {
452
+ show: { resource: ['message'], operation: ['send'], messageType: ['interactive'] },
453
+ },
454
+ },
455
+ // Interactive: Buttons (if type=button)
456
+ {
457
+ displayName: 'Buttons',
458
+ name: 'interactiveButtons',
459
+ type: 'fixedCollection',
460
+ typeOptions: {
461
+ multipleValues: true,
462
+ maxValue: 3,
463
+ },
464
+ default: { buttonValues: [] },
465
+ description: 'Reply buttons (1-3). Users tap to respond.',
466
+ displayOptions: {
467
+ show: {
468
+ resource: ['message'],
469
+ operation: ['send'],
470
+ messageType: ['interactive'],
471
+ interactiveType: ['button'],
472
+ },
473
+ },
474
+ options: [
475
+ {
476
+ displayName: 'Button',
477
+ name: 'buttonValues',
478
+ values: [
479
+ {
480
+ displayName: 'Title',
481
+ name: 'title',
482
+ type: 'string',
483
+ required: true,
484
+ default: '',
485
+ placeholder: 'Ver Menú',
486
+ description: 'Button text visible to user. Max 20 characters.',
487
+ },
488
+ {
489
+ displayName: 'ID',
490
+ name: 'id',
491
+ type: 'string',
492
+ default: '',
493
+ placeholder: 'ver_menu (optional, auto-generated if empty)',
494
+ description: 'Unique ID returned in webhook when user clicks. If empty, generated from title.',
495
+ },
496
+ ],
497
+ },
498
+ ],
499
+ },
500
+ // Interactive: List button text (if type=list)
501
+ {
502
+ displayName: 'List Button Text',
503
+ name: 'interactiveListButtonText',
504
+ type: 'string',
505
+ required: true,
506
+ default: 'Ver Opciones',
507
+ placeholder: 'Ver Menú',
508
+ description: 'Text for the button that opens the list menu. Max 20 characters.',
509
+ displayOptions: {
510
+ show: {
511
+ resource: ['message'],
512
+ operation: ['send'],
513
+ messageType: ['interactive'],
514
+ interactiveType: ['list'],
515
+ },
516
+ },
517
+ },
518
+ // Interactive: Sections (if type=list)
519
+ {
520
+ displayName: 'Sections',
521
+ name: 'interactiveSections',
522
+ type: 'fixedCollection',
523
+ typeOptions: {
524
+ multipleValues: true,
525
+ maxValue: 10,
526
+ },
527
+ default: { sectionValues: [] },
528
+ description: 'Menu sections. Each section has a title and rows (options).',
529
+ displayOptions: {
530
+ show: {
531
+ resource: ['message'],
532
+ operation: ['send'],
533
+ messageType: ['interactive'],
534
+ interactiveType: ['list'],
535
+ },
536
+ },
537
+ options: [
538
+ {
539
+ displayName: 'Section',
540
+ name: 'sectionValues',
541
+ values: [
542
+ {
543
+ displayName: 'Section Title',
544
+ name: 'title',
545
+ type: 'string',
546
+ default: '',
547
+ placeholder: 'Pizzas',
548
+ description: 'Section title. Required if more than 1 section. Max 24 characters.',
549
+ },
550
+ {
551
+ displayName: 'Rows',
552
+ name: 'rows',
553
+ type: 'fixedCollection',
554
+ typeOptions: {
555
+ multipleValues: true,
556
+ maxValue: 10,
557
+ },
558
+ default: { rowValues: [] },
559
+ description: 'Options in this section',
560
+ options: [
561
+ {
562
+ displayName: 'Row',
563
+ name: 'rowValues',
564
+ values: [
565
+ {
566
+ displayName: 'Title',
567
+ name: 'title',
568
+ type: 'string',
569
+ required: true,
570
+ default: '',
571
+ placeholder: 'Margarita',
572
+ description: 'Row title. Max 24 characters.',
573
+ },
574
+ {
575
+ displayName: 'Description',
576
+ name: 'description',
577
+ type: 'string',
578
+ default: '',
579
+ placeholder: 'Tomate, mozzarella - $150',
580
+ description: 'Row description. Optional. Max 72 characters.',
581
+ },
582
+ {
583
+ displayName: 'ID',
584
+ name: 'id',
585
+ type: 'string',
586
+ default: '',
587
+ placeholder: 'pizza_margarita (optional)',
588
+ description: 'Unique ID returned in webhook. If empty, generated from title.',
589
+ },
590
+ ],
591
+ },
592
+ ],
593
+ },
594
+ ],
595
+ },
596
+ ],
597
+ },
598
+ // Interactive: Advanced JSON (fallback for complex cases)
599
+ {
600
+ displayName: 'Use Raw JSON',
601
+ name: 'interactiveUseRawJson',
602
+ type: 'boolean',
603
+ default: false,
604
+ description: 'Use raw JSON instead of structured fields (for advanced use cases)',
605
+ displayOptions: {
606
+ show: { resource: ['message'], operation: ['send'], messageType: ['interactive'] },
607
+ },
608
+ },
609
+ {
610
+ displayName: 'Interactive JSON',
611
+ name: 'interactiveRawJson',
612
+ type: 'json',
613
+ default: '{}',
614
+ description: 'Raw interactive content JSON. See docs.whaapy.com for structure.',
615
+ displayOptions: {
616
+ show: {
617
+ resource: ['message'],
618
+ operation: ['send'],
619
+ messageType: ['interactive'],
620
+ interactiveUseRawJson: [true],
621
+ },
295
622
  },
296
623
  },
297
624
  // Message: Send - Location
@@ -1785,5 +2112,624 @@ class Whaapy {
1785
2112
  ],
1786
2113
  };
1787
2114
  }
2115
+ async execute() {
2116
+ const items = this.getInputData();
2117
+ const returnData = [];
2118
+ for (let i = 0; i < items.length; i++) {
2119
+ try {
2120
+ const resource = this.getNodeParameter('resource', i);
2121
+ const operation = this.getNodeParameter('operation', i);
2122
+ const credentials = await this.getCredentials('whaapyApi');
2123
+ const baseUrl = credentials.baseUrl;
2124
+ const apiKey = credentials.apiKey;
2125
+ let response;
2126
+ // ===========================================
2127
+ // MESSAGE RESOURCE
2128
+ // ===========================================
2129
+ if (resource === 'message') {
2130
+ if (operation === 'send') {
2131
+ const to = this.getNodeParameter('to', i);
2132
+ const messageType = this.getNodeParameter('messageType', i);
2133
+ const body = { to, type: messageType };
2134
+ // Handle different message types
2135
+ if (messageType === 'text') {
2136
+ body.content = this.getNodeParameter('textContent', i);
2137
+ }
2138
+ else if (['image', 'video', 'audio', 'document', 'sticker'].includes(messageType)) {
2139
+ const mediaUrl = this.getNodeParameter('mediaUrl', i);
2140
+ body[messageType] = { link: mediaUrl };
2141
+ if (['image', 'video', 'document'].includes(messageType)) {
2142
+ const caption = this.getNodeParameter('caption', i, '');
2143
+ if (caption)
2144
+ body[messageType].caption = caption;
2145
+ }
2146
+ }
2147
+ else if (messageType === 'template') {
2148
+ body.templateName = this.getNodeParameter('templateName', i);
2149
+ body.language = this.getNodeParameter('templateLanguage', i);
2150
+ const templateOptions = this.getNodeParameter('templateOptions', i, {});
2151
+ if (templateOptions.parameters) {
2152
+ body.template_parameters = templateOptions.parameters.split(',').map((v) => v.trim());
2153
+ }
2154
+ if (templateOptions.headerMediaType && templateOptions.headerMediaUrl) {
2155
+ body.header_media = {
2156
+ type: templateOptions.headerMediaType,
2157
+ url: templateOptions.headerMediaUrl,
2158
+ };
2159
+ }
2160
+ }
2161
+ else if (messageType === 'interactive') {
2162
+ // Build interactive message from structured fields
2163
+ const useRawJson = this.getNodeParameter('interactiveUseRawJson', i, false);
2164
+ if (useRawJson) {
2165
+ const rawJson = this.getNodeParameter('interactiveRawJson', i, '{}');
2166
+ body.interactive = typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson;
2167
+ }
2168
+ else {
2169
+ const interactiveType = this.getNodeParameter('interactiveType', i);
2170
+ const bodyText = this.getNodeParameter('interactiveBodyText', i);
2171
+ const headerType = this.getNodeParameter('interactiveHeaderType', i, 'none');
2172
+ const headerText = this.getNodeParameter('interactiveHeaderText', i, '');
2173
+ const headerMediaUrl = this.getNodeParameter('interactiveHeaderMediaUrl', i, '');
2174
+ const footerText = this.getNodeParameter('interactiveFooterText', i, '');
2175
+ // Build buttons array
2176
+ let buttons = [];
2177
+ if (interactiveType === 'button') {
2178
+ const buttonsData = this.getNodeParameter('interactiveButtons', i, { buttonValues: [] });
2179
+ buttons = buttonsData.buttonValues || [];
2180
+ }
2181
+ // Build sections array
2182
+ let sections = [];
2183
+ let listButtonText = '';
2184
+ if (interactiveType === 'list') {
2185
+ listButtonText = this.getNodeParameter('interactiveListButtonText', i, 'Ver Opciones');
2186
+ const sectionsData = this.getNodeParameter('interactiveSections', i, { sectionValues: [] });
2187
+ if (sectionsData.sectionValues) {
2188
+ sections = sectionsData.sectionValues.map((section) => {
2189
+ var _a;
2190
+ return ({
2191
+ title: section.title,
2192
+ rows: ((_a = section.rows) === null || _a === void 0 ? void 0 : _a.rowValues) || [],
2193
+ });
2194
+ });
2195
+ }
2196
+ }
2197
+ body.interactive = buildInteractivePayload({
2198
+ interactiveType,
2199
+ bodyText,
2200
+ headerType,
2201
+ headerText,
2202
+ headerMediaUrl,
2203
+ footerText,
2204
+ buttons,
2205
+ listButtonText,
2206
+ sections,
2207
+ });
2208
+ }
2209
+ }
2210
+ else if (messageType === 'location') {
2211
+ body.location = {
2212
+ latitude: this.getNodeParameter('latitude', i),
2213
+ longitude: this.getNodeParameter('longitude', i),
2214
+ name: this.getNodeParameter('locationName', i, '') || undefined,
2215
+ };
2216
+ }
2217
+ else if (messageType === 'contacts') {
2218
+ const contactsData = this.getNodeParameter('contactsData', i);
2219
+ body.contacts = typeof contactsData === 'string' ? JSON.parse(contactsData) : contactsData;
2220
+ }
2221
+ else if (messageType === 'reaction') {
2222
+ body.reaction = {
2223
+ message_id: this.getNodeParameter('reactionMessageId', i),
2224
+ emoji: this.getNodeParameter('reactionEmoji', i),
2225
+ };
2226
+ }
2227
+ // Add additional fields
2228
+ const additionalFields = this.getNodeParameter('additionalFields', i, {});
2229
+ if (additionalFields.pauseAi) {
2230
+ body.ai = body.ai || {};
2231
+ body.ai.pause = additionalFields.pauseAi;
2232
+ }
2233
+ if (additionalFields.pauseDuration) {
2234
+ body.ai = body.ai || {};
2235
+ body.ai.pauseDuration = additionalFields.pauseDuration;
2236
+ }
2237
+ if (additionalFields.disableAi) {
2238
+ body.ai = body.ai || {};
2239
+ body.ai.disable = additionalFields.disableAi;
2240
+ }
2241
+ if (additionalFields.replyTo) {
2242
+ body.context = { message_id: additionalFields.replyTo };
2243
+ }
2244
+ if (additionalFields.createConversation !== undefined) {
2245
+ body.createConversation = additionalFields.createConversation;
2246
+ }
2247
+ if (additionalFields.metadata) {
2248
+ body.metadata = typeof additionalFields.metadata === 'string'
2249
+ ? JSON.parse(additionalFields.metadata)
2250
+ : additionalFields.metadata;
2251
+ }
2252
+ response = await this.helpers.request({
2253
+ method: 'POST',
2254
+ url: `${baseUrl}/messages/v1`,
2255
+ headers: {
2256
+ 'Authorization': `Bearer ${apiKey}`,
2257
+ 'Content-Type': 'application/json',
2258
+ },
2259
+ body,
2260
+ json: true,
2261
+ });
2262
+ }
2263
+ else if (operation === 'retry') {
2264
+ const messageId = this.getNodeParameter('messageId', i);
2265
+ response = await this.helpers.request({
2266
+ method: 'POST',
2267
+ url: `${baseUrl}/messages/v1/${messageId}/retry`,
2268
+ headers: {
2269
+ 'Authorization': `Bearer ${apiKey}`,
2270
+ 'Content-Type': 'application/json',
2271
+ },
2272
+ json: true,
2273
+ });
2274
+ }
2275
+ }
2276
+ // ===========================================
2277
+ // CONVERSATION RESOURCE
2278
+ // ===========================================
2279
+ else if (resource === 'conversation') {
2280
+ if (operation === 'list') {
2281
+ const filters = this.getNodeParameter('conversationFilters', i, {});
2282
+ const qs = {};
2283
+ if (filters.search)
2284
+ qs.search = filters.search;
2285
+ if (filters.status && filters.status !== 'all')
2286
+ qs.status = filters.status;
2287
+ if (filters.limit)
2288
+ qs.limit = filters.limit;
2289
+ if (filters.offset)
2290
+ qs.offset = filters.offset;
2291
+ response = await this.helpers.request({
2292
+ method: 'GET',
2293
+ url: `${baseUrl}/conversations/v1`,
2294
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2295
+ qs,
2296
+ json: true,
2297
+ });
2298
+ }
2299
+ else if (operation === 'get') {
2300
+ const conversationId = this.getNodeParameter('conversationId', i);
2301
+ response = await this.helpers.request({
2302
+ method: 'GET',
2303
+ url: `${baseUrl}/conversations/v1/${conversationId}`,
2304
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2305
+ json: true,
2306
+ });
2307
+ }
2308
+ else if (operation === 'getByPhone') {
2309
+ const phoneNumber = this.getNodeParameter('phoneNumber', i);
2310
+ response = await this.helpers.request({
2311
+ method: 'GET',
2312
+ url: `${baseUrl}/conversations/v1/by-phone/${encodeURIComponent(phoneNumber)}`,
2313
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2314
+ json: true,
2315
+ });
2316
+ }
2317
+ else if (operation === 'getMessages') {
2318
+ const conversationId = this.getNodeParameter('conversationId', i);
2319
+ const options = this.getNodeParameter('messagesOptions', i, {});
2320
+ const qs = {};
2321
+ if (options.limit)
2322
+ qs.limit = options.limit;
2323
+ if (options.cursor)
2324
+ qs.cursor = options.cursor;
2325
+ response = await this.helpers.request({
2326
+ method: 'GET',
2327
+ url: `${baseUrl}/conversations/v1/${conversationId}/messages`,
2328
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2329
+ qs,
2330
+ json: true,
2331
+ });
2332
+ }
2333
+ else if (operation === 'close') {
2334
+ const conversationId = this.getNodeParameter('conversationId', i);
2335
+ response = await this.helpers.request({
2336
+ method: 'POST',
2337
+ url: `${baseUrl}/conversations/v1/${conversationId}/close`,
2338
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2339
+ json: true,
2340
+ });
2341
+ }
2342
+ else if (operation === 'archive') {
2343
+ const conversationId = this.getNodeParameter('conversationId', i);
2344
+ response = await this.helpers.request({
2345
+ method: 'POST',
2346
+ url: `${baseUrl}/conversations/v1/${conversationId}/archive`,
2347
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2348
+ json: true,
2349
+ });
2350
+ }
2351
+ else if (operation === 'markRead') {
2352
+ const conversationId = this.getNodeParameter('conversationId', i);
2353
+ response = await this.helpers.request({
2354
+ method: 'PATCH',
2355
+ url: `${baseUrl}/conversations/v1/${conversationId}/mark-read`,
2356
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2357
+ json: true,
2358
+ });
2359
+ }
2360
+ else if (operation === 'setAi') {
2361
+ const conversationId = this.getNodeParameter('conversationId', i);
2362
+ const aiEnabled = this.getNodeParameter('aiEnabled', i);
2363
+ response = await this.helpers.request({
2364
+ method: 'PATCH',
2365
+ url: `${baseUrl}/conversations/v1/${conversationId}/ai`,
2366
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2367
+ body: { aiEnabled },
2368
+ json: true,
2369
+ });
2370
+ }
2371
+ else if (operation === 'pauseAi') {
2372
+ const conversationId = this.getNodeParameter('conversationId', i);
2373
+ const duration = this.getNodeParameter('pauseDurationConv', i);
2374
+ response = await this.helpers.request({
2375
+ method: 'POST',
2376
+ url: `${baseUrl}/conversations/v1/${conversationId}/ai/pause`,
2377
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2378
+ body: { duration },
2379
+ json: true,
2380
+ });
2381
+ }
2382
+ else if (operation === 'aiSuggest') {
2383
+ const conversationId = this.getNodeParameter('conversationId', i);
2384
+ response = await this.helpers.request({
2385
+ method: 'POST',
2386
+ url: `${baseUrl}/conversations/v1/${conversationId}/ai-suggest`,
2387
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2388
+ json: true,
2389
+ });
2390
+ }
2391
+ }
2392
+ // ===========================================
2393
+ // AGENT RESOURCE
2394
+ // ===========================================
2395
+ else if (resource === 'agent') {
2396
+ if (operation === 'toggle') {
2397
+ const enabled = this.getNodeParameter('agentEnabled', i);
2398
+ response = await this.helpers.request({
2399
+ method: 'POST',
2400
+ url: `${baseUrl}/agent/v1/toggle`,
2401
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2402
+ body: { enabled },
2403
+ json: true,
2404
+ });
2405
+ }
2406
+ else if (operation === 'pause') {
2407
+ const duration = this.getNodeParameter('agentPauseDuration', i);
2408
+ response = await this.helpers.request({
2409
+ method: 'POST',
2410
+ url: `${baseUrl}/agent/v1/pause`,
2411
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2412
+ body: { duration },
2413
+ json: true,
2414
+ });
2415
+ }
2416
+ }
2417
+ // ===========================================
2418
+ // TEMPLATE RESOURCE
2419
+ // ===========================================
2420
+ else if (resource === 'template') {
2421
+ if (operation === 'list') {
2422
+ const filters = this.getNodeParameter('templateFilters', i, {});
2423
+ const qs = {};
2424
+ if (filters.status)
2425
+ qs.status = filters.status;
2426
+ if (filters.limit)
2427
+ qs.limit = filters.limit;
2428
+ if (filters.offset)
2429
+ qs.offset = filters.offset;
2430
+ response = await this.helpers.request({
2431
+ method: 'GET',
2432
+ url: `${baseUrl}/templates/v1`,
2433
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2434
+ qs,
2435
+ json: true,
2436
+ });
2437
+ }
2438
+ else if (operation === 'get') {
2439
+ const templateId = this.getNodeParameter('templateId', i);
2440
+ response = await this.helpers.request({
2441
+ method: 'GET',
2442
+ url: `${baseUrl}/templates/v1/${templateId}`,
2443
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2444
+ json: true,
2445
+ });
2446
+ }
2447
+ else if (operation === 'getVariables') {
2448
+ response = await this.helpers.request({
2449
+ method: 'GET',
2450
+ url: `${baseUrl}/templates/v1/variables`,
2451
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2452
+ json: true,
2453
+ });
2454
+ }
2455
+ else if (operation === 'sync') {
2456
+ response = await this.helpers.request({
2457
+ method: 'POST',
2458
+ url: `${baseUrl}/templates/v1/sync`,
2459
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2460
+ json: true,
2461
+ });
2462
+ }
2463
+ }
2464
+ // ===========================================
2465
+ // MEDIA RESOURCE
2466
+ // ===========================================
2467
+ else if (resource === 'media') {
2468
+ if (operation === 'upload') {
2469
+ // Media upload requires special handling with binary data
2470
+ const mediaType = this.getNodeParameter('mediaType', i);
2471
+ const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
2472
+ // For now, just return an error - binary upload needs special handling
2473
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Media upload from n8n requires binary data handling. Use the HTTP Request node with binary data or upload via URL.', { itemIndex: i });
2474
+ }
2475
+ }
2476
+ // ===========================================
2477
+ // CONTACT RESOURCE
2478
+ // ===========================================
2479
+ else if (resource === 'contact') {
2480
+ if (operation === 'list') {
2481
+ const filters = this.getNodeParameter('contactFilters', i, {});
2482
+ const qs = {};
2483
+ Object.entries(filters).forEach(([key, value]) => {
2484
+ if (value !== undefined && value !== '') {
2485
+ qs[key === 'sortBy' ? 'sort_by' : key === 'sortOrder' ? 'sort_order' : key === 'funnelStageId' ? 'funnel_stage_id' : key] = value;
2486
+ }
2487
+ });
2488
+ response = await this.helpers.request({
2489
+ method: 'GET',
2490
+ url: `${baseUrl}/contacts/v1`,
2491
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2492
+ qs,
2493
+ json: true,
2494
+ });
2495
+ }
2496
+ else if (operation === 'get') {
2497
+ const contactId = this.getNodeParameter('contactId', i);
2498
+ response = await this.helpers.request({
2499
+ method: 'GET',
2500
+ url: `${baseUrl}/contacts/v1/${contactId}`,
2501
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2502
+ json: true,
2503
+ });
2504
+ }
2505
+ else if (operation === 'create') {
2506
+ const name = this.getNodeParameter('contactName', i);
2507
+ const phoneNumber = this.getNodeParameter('contactPhone', i);
2508
+ const additional = this.getNodeParameter('contactAdditional', i, {});
2509
+ const body = { name, phoneNumber };
2510
+ if (additional.email)
2511
+ body.email = additional.email;
2512
+ if (additional.tags)
2513
+ body.tags = additional.tags;
2514
+ if (additional.customFields) {
2515
+ body.customFields = typeof additional.customFields === 'string'
2516
+ ? JSON.parse(additional.customFields)
2517
+ : additional.customFields;
2518
+ }
2519
+ if (additional.metadata) {
2520
+ body.metadata = typeof additional.metadata === 'string'
2521
+ ? JSON.parse(additional.metadata)
2522
+ : additional.metadata;
2523
+ }
2524
+ response = await this.helpers.request({
2525
+ method: 'POST',
2526
+ url: `${baseUrl}/contacts/v1`,
2527
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2528
+ body,
2529
+ json: true,
2530
+ });
2531
+ }
2532
+ else if (operation === 'update') {
2533
+ const contactId = this.getNodeParameter('contactId', i);
2534
+ const updateFields = this.getNodeParameter('contactUpdateFields', i, {});
2535
+ const body = {};
2536
+ Object.entries(updateFields).forEach(([key, value]) => {
2537
+ if (value !== undefined && value !== '') {
2538
+ if (key === 'customFields') {
2539
+ body[key] = typeof value === 'string' ? JSON.parse(value) : value;
2540
+ }
2541
+ else {
2542
+ body[key] = value;
2543
+ }
2544
+ }
2545
+ });
2546
+ response = await this.helpers.request({
2547
+ method: 'PATCH',
2548
+ url: `${baseUrl}/contacts/v1/${contactId}`,
2549
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2550
+ body,
2551
+ json: true,
2552
+ });
2553
+ }
2554
+ else if (operation === 'delete') {
2555
+ const contactId = this.getNodeParameter('contactId', i);
2556
+ response = await this.helpers.request({
2557
+ method: 'DELETE',
2558
+ url: `${baseUrl}/contacts/v1/${contactId}`,
2559
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2560
+ json: true,
2561
+ });
2562
+ }
2563
+ else if (operation === 'search') {
2564
+ const query = this.getNodeParameter('searchQuery', i);
2565
+ const options = this.getNodeParameter('searchOptions', i, {});
2566
+ const body = { query };
2567
+ if (options.filters) {
2568
+ body.filters = typeof options.filters === 'string' ? JSON.parse(options.filters) : options.filters;
2569
+ }
2570
+ if (options.limit)
2571
+ body.limit = options.limit;
2572
+ if (options.cursor)
2573
+ body.cursor = options.cursor;
2574
+ response = await this.helpers.request({
2575
+ method: 'POST',
2576
+ url: `${baseUrl}/contacts/v1/search`,
2577
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2578
+ body,
2579
+ json: true,
2580
+ });
2581
+ }
2582
+ else if (operation === 'bulk') {
2583
+ const bulkOperation = this.getNodeParameter('bulkOperation', i);
2584
+ const contacts = this.getNodeParameter('bulkContacts', i);
2585
+ const data = this.getNodeParameter('bulkData', i, '{}');
2586
+ response = await this.helpers.request({
2587
+ method: 'POST',
2588
+ url: `${baseUrl}/contacts/v1/bulk`,
2589
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2590
+ body: {
2591
+ operation: bulkOperation,
2592
+ contacts: typeof contacts === 'string' ? JSON.parse(contacts) : contacts,
2593
+ data: typeof data === 'string' ? JSON.parse(data) : data,
2594
+ },
2595
+ json: true,
2596
+ });
2597
+ }
2598
+ else if (operation === 'merge') {
2599
+ const contactId = this.getNodeParameter('contactId', i);
2600
+ const mergeWith = this.getNodeParameter('mergeWithId', i);
2601
+ response = await this.helpers.request({
2602
+ method: 'POST',
2603
+ url: `${baseUrl}/contacts/v1/${contactId}/merge`,
2604
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2605
+ body: { mergeWith },
2606
+ json: true,
2607
+ });
2608
+ }
2609
+ else if (operation === 'getTags') {
2610
+ response = await this.helpers.request({
2611
+ method: 'GET',
2612
+ url: `${baseUrl}/contacts/v1/tags`,
2613
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2614
+ json: true,
2615
+ });
2616
+ }
2617
+ else if (operation === 'getFields') {
2618
+ response = await this.helpers.request({
2619
+ method: 'GET',
2620
+ url: `${baseUrl}/contacts/v1/fields`,
2621
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2622
+ json: true,
2623
+ });
2624
+ }
2625
+ }
2626
+ // ===========================================
2627
+ // FUNNEL RESOURCE
2628
+ // ===========================================
2629
+ else if (resource === 'funnel') {
2630
+ if (operation === 'listStages') {
2631
+ const options = this.getNodeParameter('stageListOptions', i, {});
2632
+ const qs = {};
2633
+ if (options.limit)
2634
+ qs.limit = options.limit;
2635
+ if (options.offset)
2636
+ qs.offset = options.offset;
2637
+ response = await this.helpers.request({
2638
+ method: 'GET',
2639
+ url: `${baseUrl}/funnel/v1/stages`,
2640
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2641
+ qs,
2642
+ json: true,
2643
+ });
2644
+ }
2645
+ else if (operation === 'getStage') {
2646
+ const stageId = this.getNodeParameter('stageId', i);
2647
+ response = await this.helpers.request({
2648
+ method: 'GET',
2649
+ url: `${baseUrl}/funnel/v1/stages/${stageId}`,
2650
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2651
+ json: true,
2652
+ });
2653
+ }
2654
+ else if (operation === 'createStage') {
2655
+ const name = this.getNodeParameter('stageName', i);
2656
+ const options = this.getNodeParameter('stageOptions', i, {});
2657
+ const body = { name };
2658
+ if (options.position !== undefined)
2659
+ body.position = options.position;
2660
+ if (options.color)
2661
+ body.color = options.color;
2662
+ if (options.description)
2663
+ body.description = options.description;
2664
+ response = await this.helpers.request({
2665
+ method: 'POST',
2666
+ url: `${baseUrl}/funnel/v1/stages`,
2667
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2668
+ body,
2669
+ json: true,
2670
+ });
2671
+ }
2672
+ else if (operation === 'updateStage') {
2673
+ const stageId = this.getNodeParameter('stageId', i);
2674
+ const updateFields = this.getNodeParameter('stageUpdateFields', i, {});
2675
+ const body = {};
2676
+ Object.entries(updateFields).forEach(([key, value]) => {
2677
+ if (value !== undefined && value !== '') {
2678
+ body[key] = value;
2679
+ }
2680
+ });
2681
+ response = await this.helpers.request({
2682
+ method: 'PATCH',
2683
+ url: `${baseUrl}/funnel/v1/stages/${stageId}`,
2684
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2685
+ body,
2686
+ json: true,
2687
+ });
2688
+ }
2689
+ else if (operation === 'deleteStage') {
2690
+ const stageId = this.getNodeParameter('stageId', i);
2691
+ response = await this.helpers.request({
2692
+ method: 'DELETE',
2693
+ url: `${baseUrl}/funnel/v1/stages/${stageId}`,
2694
+ headers: { 'Authorization': `Bearer ${apiKey}` },
2695
+ json: true,
2696
+ });
2697
+ }
2698
+ else if (operation === 'reorderStages') {
2699
+ const stages = this.getNodeParameter('stagesOrder', i);
2700
+ response = await this.helpers.request({
2701
+ method: 'PATCH',
2702
+ url: `${baseUrl}/funnel/v1/stages/reorder`,
2703
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2704
+ body: { stages: typeof stages === 'string' ? JSON.parse(stages) : stages },
2705
+ json: true,
2706
+ });
2707
+ }
2708
+ else if (operation === 'moveContact') {
2709
+ const contactId = this.getNodeParameter('contactIdFunnel', i);
2710
+ const stageId = this.getNodeParameter('targetStageId', i);
2711
+ response = await this.helpers.request({
2712
+ method: 'POST',
2713
+ url: `${baseUrl}/funnel/v1/contacts/${contactId}/move`,
2714
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
2715
+ body: { stageId },
2716
+ json: true,
2717
+ });
2718
+ }
2719
+ }
2720
+ if (response) {
2721
+ returnData.push({ json: response });
2722
+ }
2723
+ }
2724
+ catch (error) {
2725
+ if (this.continueOnFail()) {
2726
+ returnData.push({ json: { error: error.message } });
2727
+ continue;
2728
+ }
2729
+ throw error;
2730
+ }
2731
+ }
2732
+ return [returnData];
2733
+ }
1788
2734
  }
1789
2735
  exports.Whaapy = Whaapy;
@@ -91,16 +91,8 @@ class WhaapyTrigger {
91
91
  type: 'collection',
92
92
  placeholder: 'Add Option',
93
93
  default: {},
94
- options: [
95
- {
96
- displayName: 'Webhook Secret',
97
- name: 'webhookSecret',
98
- type: 'string',
99
- typeOptions: { password: true },
100
- default: '',
101
- description: 'Secret to verify webhook signatures (optional)',
102
- },
103
- ],
94
+ description: 'The webhook secret is automatically generated by Whaapy and returned in the X-Webhook-Signature header',
95
+ options: [],
104
96
  },
105
97
  ],
106
98
  };
@@ -113,13 +105,13 @@ class WhaapyTrigger {
113
105
  try {
114
106
  const response = await this.helpers.request({
115
107
  method: 'GET',
116
- url: `${credentials.baseUrl}/webhooks/v1`,
108
+ url: `${credentials.baseUrl}/user-webhooks`,
117
109
  headers: {
118
110
  Authorization: `Bearer ${credentials.apiKey}`,
119
111
  },
120
112
  json: true,
121
113
  });
122
- const webhooks = response.webhooks || response.data || [];
114
+ const webhooks = response.data || response.webhooks || [];
123
115
  return webhooks.some((webhook) => {
124
116
  var _a;
125
117
  return webhook.url === webhookUrl &&
@@ -131,22 +123,21 @@ class WhaapyTrigger {
131
123
  }
132
124
  },
133
125
  async create() {
134
- var _a;
126
+ var _a, _b;
135
127
  const webhookUrl = this.getNodeWebhookUrl('default');
136
128
  const event = this.getNodeParameter('event');
137
- const options = this.getNodeParameter('options');
138
129
  const credentials = await this.getCredentials('whaapyApi');
130
+ // Generate a unique name for the webhook
131
+ const eventName = event === '*' ? 'all-events' : event.replace('.', '-');
139
132
  const body = {
133
+ name: `n8n-${eventName}-${Date.now()}`,
140
134
  url: webhookUrl,
141
135
  events: event === '*' ? ['*'] : [event],
142
136
  };
143
- if (options.webhookSecret) {
144
- body.secret = options.webhookSecret;
145
- }
146
137
  try {
147
138
  const response = await this.helpers.request({
148
139
  method: 'POST',
149
- url: `${credentials.baseUrl}/webhooks/v1`,
140
+ url: `${credentials.baseUrl}/user-webhooks`,
150
141
  headers: {
151
142
  Authorization: `Bearer ${credentials.apiKey}`,
152
143
  'Content-Type': 'application/json',
@@ -155,7 +146,7 @@ class WhaapyTrigger {
155
146
  json: true,
156
147
  });
157
148
  const webhookData = this.getWorkflowStaticData('node');
158
- webhookData.webhookId = response.id || ((_a = response.webhook) === null || _a === void 0 ? void 0 : _a.id);
149
+ webhookData.webhookId = ((_a = response.data) === null || _a === void 0 ? void 0 : _a.id) || response.id || ((_b = response.webhook) === null || _b === void 0 ? void 0 : _b.id);
159
150
  return true;
160
151
  }
161
152
  catch (error) {
@@ -171,7 +162,7 @@ class WhaapyTrigger {
171
162
  try {
172
163
  await this.helpers.request({
173
164
  method: 'DELETE',
174
- url: `${credentials.baseUrl}/webhooks/v1/${webhookData.webhookId}`,
165
+ url: `${credentials.baseUrl}/user-webhooks/${webhookData.webhookId}`,
175
166
  headers: {
176
167
  Authorization: `Bearer ${credentials.apiKey}`,
177
168
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-whaapy",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "n8n community node for Whaapy - WhatsApp Business API with AI",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",