@theotherwillembotha/node-red-whatsapp 0.0.55

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.
@@ -0,0 +1,1814 @@
1
+
2
+ <!--
3
+
4
+ This file was automatically generated using the NODERED Core utility.
5
+ Any modifications to his file will be overwritten the next time the code is regenerated.
6
+
7
+ You have been warned.
8
+
9
+ -->
10
+
11
+ <!-- WhatsappAccountConfigNode -->
12
+ <style>
13
+ .theotherwillembotha-compact-table li {
14
+ padding-top: 4px;
15
+ padding-bottom: 4px;
16
+ }
17
+
18
+ </style>
19
+ <script type="text/javascript">
20
+ // a simple client for interfaceing with the WhatsappService on the backend. Note: this needs to be moved to its own file.
21
+ let waService = {
22
+ unlinkAccount: function(nodeID, localConnectionId) {
23
+ return new Promise((resolve, fail) => {
24
+ let request = JSON.stringify({
25
+ nodeID: nodeID,
26
+ localConnectionId: localConnectionId
27
+ });
28
+ $.ajax({
29
+ url: "whatsapp/unlinkaccount",
30
+ type: "POST",
31
+ data: request,
32
+ contentType: "application/json; charset=utf-8",
33
+ success: (response) => {
34
+ resolve(response);
35
+ },
36
+ error: (jqXHR, textStatus, errorThrown) => {
37
+ fail(errorThrown);
38
+ }
39
+ });
40
+ });
41
+ },
42
+ getConnectionDetails: function(nodeID, localConnectionId) {
43
+ return new Promise((resolve, fail) => {
44
+ let request = JSON.stringify({
45
+ nodeID: nodeID,
46
+ localConnectionId: localConnectionId
47
+ });
48
+ $.ajax({
49
+ url: "whatsapp/connectiondetails",
50
+ type: "POST",
51
+ data: request,
52
+ contentType: "application/json; charset=utf-8",
53
+ success: (response) => {
54
+ resolve(response);
55
+ },
56
+ error: (jqXHR, textStatus, errorThrown) => {
57
+ fail(errorThrown);
58
+ }
59
+ });
60
+ });
61
+ },
62
+ createGroup: function(nodeID, localConnectionId, name) {
63
+ console.log("create group! " + name);
64
+ return new Promise((resolve, fail) => {
65
+ let request = JSON.stringify({
66
+ nodeID: nodeID,
67
+ localConnectionId: localConnectionId,
68
+ groupName: name
69
+ });
70
+ $.ajax({
71
+ url: "whatsapp/creategroup",
72
+ type: "POST",
73
+ data: request,
74
+ contentType: "application/json; charset=utf-8",
75
+ success: (response) => {
76
+ resolve(response);
77
+ },
78
+ error: (jqXHR, textStatus, errorThrown) => {
79
+ fail(errorThrown);
80
+ }
81
+ });
82
+ });
83
+ },
84
+ getGroups: function(nodeID, localConnectionId) {
85
+ return new Promise((resolve, fail) => {
86
+ let request = JSON.stringify({
87
+ nodeID: nodeID,
88
+ localConnectionId: localConnectionId
89
+ });
90
+ $.ajax({
91
+ url: "whatsapp/getgroups",
92
+ type: "POST",
93
+ data: request,
94
+ contentType: "application/json; charset=utf-8",
95
+ success: (response) => {
96
+ resolve(response);
97
+ },
98
+ error: (jqXHR, textStatus, errorThrown) => {
99
+ fail(errorThrown);
100
+ }
101
+ });
102
+ });
103
+ },
104
+ addUserToGroup: function(nodeID, localConnectionId, userId, groupId, role) {
105
+ return new Promise((resolve, fail) => {
106
+ let request = JSON.stringify({
107
+ nodeID: nodeID,
108
+ localConnectionId: localConnectionId,
109
+ userId: userId,
110
+ groupId: groupId,
111
+ role: role
112
+ });
113
+ $.ajax({
114
+ url: "whatsapp/groupadduser",
115
+ type: "POST",
116
+ data: request,
117
+ contentType: "application/json; charset=utf-8",
118
+ success: (response) => {
119
+ resolve(response);
120
+ },
121
+ error: (jqXHR, textStatus, errorThrown) => {
122
+ fail(errorThrown);
123
+ }
124
+ });
125
+ });
126
+ },
127
+ updateUserInGroup: function(nodeID, localConnectionId, userId, groupId, role) {
128
+ console.log("update user in group.");
129
+ return new Promise((resolve, fail) => {
130
+ let request = JSON.stringify({
131
+ nodeID: nodeID,
132
+ localConnectionId: localConnectionId,
133
+ userId: userId,
134
+ groupId: groupId,
135
+ role: role
136
+ });
137
+ $.ajax({
138
+ url: "whatsapp/groupupdateuser",
139
+ type: "POST",
140
+ data: request,
141
+ contentType: "application/json; charset=utf-8",
142
+ success: (response) => {
143
+ resolve(response);
144
+ },
145
+ error: (jqXHR, textStatus, errorThrown) => {
146
+ fail(errorThrown);
147
+ }
148
+ });
149
+ });
150
+ },
151
+ removeUserFromGroup: function(nodeID, localConnectionId, userId, groupId) {
152
+ console.log("removeUserFromGroup", nodeID, localConnectionId, userId, groupId);
153
+ return new Promise((resolve, fail) => {
154
+ let request = JSON.stringify({
155
+ nodeID: nodeID,
156
+ localConnectionId: localConnectionId,
157
+ userId: userId,
158
+ groupId: groupId
159
+ });
160
+ $.ajax({
161
+ url: "whatsapp/groupremoveuser",
162
+ type: "POST",
163
+ data: request,
164
+ contentType: "application/json; charset=utf-8",
165
+ success: (response) => {
166
+ resolve(response);
167
+ },
168
+ error: (jqXHR, textStatus, errorThrown) => {
169
+ fail(errorThrown);
170
+ }
171
+ });
172
+ });
173
+ },
174
+ }
175
+
176
+ </script>
177
+ <script type="text/javascript">
178
+ RED.nodes.registerType('WhatsappAccountConfigNode', {
179
+ category: 'config',
180
+ label: function() {
181
+ return this.name
182
+ },
183
+ paletteLabel: 'WhatsApp Account',
184
+ defaults: {
185
+ name: {
186
+ value: 'WhatsApp Account',
187
+ required: true
188
+ },
189
+ accountLinked: {
190
+ value: false,
191
+ required: true
192
+ },
193
+ localConnectionId: {
194
+ required: true
195
+ },
196
+ },
197
+ oneditprepare: function() {
198
+ // **** WhatsappAccountConfigNode **** //
199
+ {
200
+ let node = this;
201
+ let $btn = $("#wa-account-link-btn");
202
+ // if the account is already linked, show the Unlink state
203
+ if (document.getElementById("node-config-input-accountLinked").checked) {
204
+ $btn.html('<i class="fa fa-unlink"></i> Unlink Account');
205
+ }
206
+ // if we don't have a local connectionId for this node, generate one.
207
+ if ($("#node-config-input-localConnectionId").val() === "") {
208
+ $("#node-config-input-localConnectionId").val(crypto.randomUUID());
209
+ }
210
+ let localConnectionId = $("#node-config-input-localConnectionId").val();
211
+ $btn.on("click", () => {
212
+ $btn.prop("disabled", true);
213
+ if (document.getElementById("node-config-input-accountLinked").checked) {
214
+ waService.unlinkAccount(node.id, localConnectionId).then(response => {
215
+ $btn.prop("disabled", false);
216
+ $btn.html('<i class="fa fa-link"></i> Link Account');
217
+ document.getElementById("node-config-input-accountLinked").checked = false;
218
+ $("#wa-account-qrcode").html("<p>Account unlinked. Remember to remove this device from WhatsApp on your phone.</p>");
219
+ });
220
+ return;
221
+ }
222
+ // register an event handler for QR codes and connection state updates.
223
+ let eventHandler = function(topic, payload) {
224
+ if (payload.type === "qrcode") {
225
+ let qrcode = "<p>Open WhatsApp on your phone, select <strong>Linked Devices</strong> and scan the QR code below.</p><br>" + payload.qrcode;
226
+ $("#wa-account-qrcode").html(qrcode);
227
+ return;
228
+ }
229
+ if (payload.type === "account_linked") {
230
+ $("#wa-account-qrcode").html("<p>\u2714 Account linked successfully.</p>");
231
+ RED.comms.unsubscribe("notification/whatsapp/" + localConnectionId, eventHandler);
232
+ $btn.prop("disabled", false);
233
+ $btn.html('<i class="fa fa-unlink"></i> Unlink Account');
234
+ document.getElementById("node-config-input-accountLinked").checked = true;
235
+ return;
236
+ }
237
+ if (payload.type === "account_link_timeout") {
238
+ $("#wa-account-qrcode").html("<p>\u2718 Link timed out. Click <em>Link Account</em> to try again.</p>");
239
+ $btn.prop("disabled", false);
240
+ RED.comms.unsubscribe("notification/whatsapp/" + localConnectionId, eventHandler);
241
+ return;
242
+ }
243
+ console.log("unhandled payload type in WhatsappConnectionConfig " + payload.type);
244
+ };
245
+ RED.comms.subscribe("notification/whatsapp/" + localConnectionId, eventHandler);
246
+ $.ajax({
247
+ url: "whatsapp/linkaccount",
248
+ type: "POST",
249
+ data: JSON.stringify({
250
+ nodeID: node.id,
251
+ localConnectionId: localConnectionId
252
+ }),
253
+ contentType: "application/json; charset=utf-8",
254
+ error: (jqXHR) => {
255
+ console.log("linkaccount error", jqXHR);
256
+ }
257
+ });
258
+ });
259
+ }
260
+ },
261
+ oneditsave: function() {
262
+ // **** WhatsappAccountConfigNode **** //
263
+ {
264
+ let node = this;
265
+ }
266
+ },
267
+ oneditcancel: function() {},
268
+ oneditdelete: function() {},
269
+ });
270
+
271
+ </script>
272
+
273
+
274
+ <script type="text/html" data-template-name='WhatsappAccountConfigNode'>
275
+ <div id='section_WhatsappAccountConfigNode'>
276
+ <div class="form-row">
277
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
278
+ <input type="text" id="node-config-input-name" />
279
+ </div>
280
+
281
+ <div style="display:none">
282
+ <input type="text" id="node-config-input-localConnectionId">
283
+ <input type="checkbox" id="node-config-input-accountLinked">
284
+ </div>
285
+
286
+ <div class="form-row">
287
+ <label></label>
288
+ <button type="button" id="wa-account-link-btn" class="red-ui-button">
289
+ <i class="fa fa-link"></i> Link Account
290
+ </button>
291
+ </div>
292
+
293
+ <div class="form-row">
294
+ <label></label>
295
+ <div id="wa-account-qrcode" style="max-width: 450px;"></div>
296
+ </div>
297
+
298
+
299
+ </div>
300
+ </script>
301
+
302
+ <script type="text/markdown" data-help-name='WhatsappAccountConfigNode'>
303
+ > **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
304
+
305
+ Manages a WhatsApp Web session. Handles QR-code pairing and persists session credentials so the connection survives Node-RED restarts.
306
+
307
+ ### Properties
308
+
309
+ : *name* (string) : Display label for this config node.
310
+
311
+ ### Linking an account
312
+
313
+ Open the node editor and click **Link Account**. A QR code is displayed — scan it using WhatsApp on your phone (Linked Devices). Once scanned, the session is saved and reconnected automatically on each restart.
314
+
315
+ To unlink the device, open the editor and click **Unlink Account**, then remove the linked device from WhatsApp on your phone.
316
+
317
+ ### Session storage
318
+
319
+ Credentials are persisted to `/data/whatsapp/<localConnectionId>/` if the `/data` directory exists (standard Node-RED Docker image), otherwise to `./whatsapp/<localConnectionId>/` relative to the Node-RED working directory.
320
+ </script>
321
+ <!-- WhatsappGroupConfigNode -->
322
+ <style>
323
+ </style>
324
+ <script type="text/javascript">
325
+ RED.nodes.registerType('WhatsappGroupConfigNode', {
326
+ category: 'config',
327
+ label: function() {
328
+ return this.name
329
+ },
330
+ paletteLabel: 'WhatsApp Group',
331
+ defaults: {
332
+ name: {
333
+ value: 'WhatsApp Group',
334
+ required: true
335
+ },
336
+ accountConfig: {
337
+ type: 'WhatsappAccountConfigNode',
338
+ required: true
339
+ },
340
+ groupId: {
341
+ value: '',
342
+ required: true
343
+ },
344
+ },
345
+ oneditprepare: function() {
346
+ // **** WhatsappGroupConfigNode **** //
347
+ {
348
+ let node = this;
349
+ let groupList = [];
350
+ let onGroupChanged = () => {
351
+ let selectedId = groupSelectionControl.typedInput("value");
352
+ let selectedGroup = groupList.find(g => g.id === selectedId);
353
+ if (selectedGroup) {
354
+ let accountNode;
355
+ RED.nodes.eachConfig(cfg => {
356
+ if (cfg.id === accountConfigInput.val()) accountNode = cfg;
357
+ });
358
+ let accountName = accountNode ? accountNode.name : "";
359
+ $("#node-config-input-name").val(accountName ? accountName + " - " + selectedGroup.subject : selectedGroup.subject);
360
+ }
361
+ };
362
+ let onGroupMembersLoad = () => {
363
+ let selectedId = groupSelectionControl.typedInput("value");
364
+ updateMemberList(selectedId);
365
+ };
366
+ let updateGroupList = (accountNodeId) => {
367
+ // map controls.
368
+ let groupSelectionControl = $("#node-config-input-groupId");
369
+ let newGroupButton = document.getElementById("wa-group-new-btn");
370
+ // get the localConnectionId.
371
+ let localConnectionId;
372
+ RED.nodes.eachConfig(cfg => {
373
+ if (cfg.id === accountNodeId) {
374
+ localConnectionId = cfg.localConnectionId;
375
+ }
376
+ });
377
+ // if we couldnt find a localConnectionId, it means that the connection does not exist or was not set.
378
+ if (!localConnectionId) {
379
+ newGroupButton.disabled = true;
380
+ groupList = [];
381
+ groupSelectionControl.typedInput('types', [{
382
+ value: "group",
383
+ options: []
384
+ }]);
385
+ }
386
+ if (localConnectionId) {
387
+ newGroupButton.disabled = false;
388
+ // issue request to backend to get the list of groups
389
+ waService.getGroups(node.id, localConnectionId).then(groupListResponse => {
390
+ groupList = groupListResponse;
391
+ // rebuild the list of groups...
392
+ let groups = groupList.map(groupMeta => {
393
+ return {
394
+ value: groupMeta.id,
395
+ label: groupMeta.subject
396
+ }
397
+ });
398
+ // detach change handlers before restoring saved value so the name field is not overwritten on open
399
+ groupSelectionControl.off("change", onGroupChanged);
400
+ groupSelectionControl.off("change", onGroupMembersLoad);
401
+ groupSelectionControl.typedInput('types', [{
402
+ value: "group",
403
+ options: groups
404
+ }]);
405
+ groupSelectionControl.typedInput('value', node.groupId);
406
+ groupSelectionControl.on("change", onGroupChanged);
407
+ groupSelectionControl.on("change", onGroupMembersLoad);
408
+ onGroupMembersLoad();
409
+ }).catch(error => {
410
+ console.log(`Error gettting groups: ${name}`, error);
411
+ })
412
+ }
413
+ };
414
+ let updateMemberList = (selectedGroupId) => {
415
+ let selectedGroup = groupList.find(g => g.id === selectedGroupId);
416
+ if (!selectedGroup) {
417
+ //console.log("no group selected");
418
+ return;
419
+ }
420
+ console.log("updating member list", selectedGroup);
421
+ let members = selectedGroup.participants ? selectedGroup.participants : [];
422
+ // determine if the linked account can manage roles (must be Admin or SuperAdmin in this group)
423
+ let me = members.find(m => m.isMe);
424
+ let canManage = me && (me.role === "Admin" || me.role === "SuperAdmin");
425
+ let groupMemberList = $("#node-input-groupMembers-container");
426
+ groupMemberList.editableList({
427
+ sortable: false,
428
+ removable: true,
429
+ height: 'auto',
430
+ scrollOnAdd: true,
431
+ addButton: 'Add Member',
432
+ header: $('<div>').append($.parseHTML(`
433
+ <div style='width:130px; margin-left:8px; display:inline-block;'>Number</div>
434
+ <div style='width:140px; display:inline-block;'>Name</div>
435
+ <div style='width:100px; display:inline-block;'>Role</div>
436
+ `)),
437
+ addItem: function(container, i, _member) {
438
+ let member = _member;
439
+ console.log("ADD MEMBER: ", member);
440
+ let saveMember = (number, role) => {
441
+ // get the localConnectionId from the selected account config.
442
+ let localConnectionId;
443
+ RED.nodes.eachConfig(cfg => {
444
+ if (cfg.id === accountConfigInput.val()) {
445
+ localConnectionId = cfg.localConnectionId;
446
+ }
447
+ });
448
+ if (!localConnectionId) return;
449
+ // format the phone number as a WhatsApp JID: strip leading + and append @s.whatsapp.net
450
+ let userId = number.replace(/^\+/, "") + "@s.whatsapp.net";
451
+ let groupId = groupSelectionControl.typedInput("value");
452
+ saveButton.prop("disabled", true);
453
+ waService.addUserToGroup(node.id, localConnectionId, userId, groupId, role).then(() => {
454
+ idField.val(userId);
455
+ member.id = userId;
456
+ phoneNumberField.prop("disabled", true);
457
+ saveButton.hide();
458
+ }).catch(err => {
459
+ console.log("Error adding member to group:", err);
460
+ }).finally(() => {
461
+ saveButton.prop("disabled", false);
462
+ });
463
+ }
464
+ let onRoleChanged = (role) => {
465
+ if (!member.id) return; // new unsaved member — role is submitted with the save button
466
+ let localConnectionId;
467
+ RED.nodes.eachConfig(cfg => {
468
+ if (cfg.id === accountConfigInput.val()) {
469
+ localConnectionId = cfg.localConnectionId;
470
+ }
471
+ });
472
+ if (!localConnectionId) return;
473
+ let groupId = groupSelectionControl.typedInput("value");
474
+ roleField.prop("disabled", true);
475
+ waService.updateUserInGroup(node.id, localConnectionId, member.id, groupId, role).catch(err => {
476
+ console.log("Error updating member role:", err);
477
+ }).finally(() => {
478
+ roleField.prop("disabled", false);
479
+ });
480
+ };
481
+ let row = $('<div/>', {
482
+ class: 'form-row',
483
+ style: 'display: flex; margin-bottom: 0;'
484
+ }).appendTo(container);
485
+ // Add the id column
486
+ let idField = $('<input/>', {
487
+ type: 'text',
488
+ name: i,
489
+ required: true,
490
+ style: 'display:none;',
491
+ class: 'node-input-member-id-value'
492
+ }).appendTo(row);
493
+ idField.val(member.id);
494
+ // Add the Number column
495
+ let phoneNumberField = $('<input/>', {
496
+ type: 'text',
497
+ name: i,
498
+ required: true,
499
+ disabled: (member.id),
500
+ style: 'margin-left: 5px; margin-bottom: 0; width:130px',
501
+ class: 'node-input-member-phonenumber-value'
502
+ }).appendTo(row);
503
+ if (member.id) {
504
+ phoneNumberField.val(member.id.substring(0, member.id.indexOf("@")));
505
+ }
506
+ // Add the Name column (read-only)
507
+ let nameField = $('<span/>', {
508
+ style: 'margin-left: 5px; margin-bottom: 0; width:140px; display:inline-block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; line-height:34px;',
509
+ class: 'node-input-member-name-value'
510
+ }).appendTo(row);
511
+ nameField.text(member.name ?? "");
512
+ let isSuperAdmin = member.role === "SuperAdmin";
513
+ let roleField = $('<select />', {
514
+ name: i,
515
+ required: false,
516
+ disabled: isSuperAdmin || !canManage,
517
+ style: 'margin-left: 5px; margin-bottom: 0; width:100px',
518
+ class: 'node-input-member-role-value'
519
+ });
520
+ // SuperAdmin is a fixed role — show it only for the current SuperAdmin, not as an assignable option
521
+ if (isSuperAdmin) {
522
+ roleField.append($("<option>").attr('value', "SuperAdmin").text("Super Admin"));
523
+ } else {
524
+ Object.entries({
525
+ Admin: "Admin",
526
+ Member: "Member"
527
+ }).forEach(([key, value]) => {
528
+ roleField.append($("<option>").attr('value', key).text(value));
529
+ });
530
+ }
531
+ roleField.val(member.role);
532
+ roleField.on("change", () => onRoleChanged(roleField.val()));
533
+ roleField.appendTo(row);
534
+ // column for the save button.
535
+ let saveButtonHolder = $(`<span id="middlePlaceholder" style="display: inline-block; width: 18px;"></span>`).appendTo(row)
536
+ let saveButton = $(`<a
537
+ href="#"
538
+ class="red-ui-editableList-item-approve red-ui-button red-ui-button-small"
539
+ style="margin-left: 5px; margin-bottom: 0;margin-top: 8px;">
540
+ <i class="fa fa-save"></i>
541
+ </a>`).appendTo(saveButtonHolder);
542
+ // if there is no member.id, show the save button because its a new member.
543
+ if (member.id) {
544
+ saveButton.hide();
545
+ } else {
546
+ saveButton.show();
547
+ }
548
+ saveButton.on("click", () => {
549
+ saveMember(phoneNumberField.val(), roleField.val())
550
+ })
551
+ },
552
+ removeItem: function(member) {
553
+ if (member.id) {
554
+ console.log("member removed", member);
555
+ // get the localConnectionId from the selected account config.
556
+ let localConnectionId;
557
+ RED.nodes.eachConfig(cfg => {
558
+ if (cfg.id === accountConfigInput.val()) {
559
+ localConnectionId = cfg.localConnectionId;
560
+ }
561
+ });
562
+ // get the groupId
563
+ let groupId = groupSelectionControl.typedInput("value");
564
+ // remove the user from the group.
565
+ waService.removeUserFromGroup(node.id, localConnectionId, member.id, groupId);
566
+ } else {
567
+ console.log("member id was not available when trying to remove member");
568
+ }
569
+ }
570
+ });
571
+ groupMemberList.empty();
572
+ groupMemberList.editableList('addItems', members);
573
+ }
574
+ let setCreateButtonEnabled = (accountNodeId) => {
575
+ let hasAccount = accountNodeId && accountNodeId !== "_ADD_";
576
+ $("#wa-group-new-btn").prop("disabled", !hasAccount);
577
+ };
578
+ // when the accountConfig changes, update the group list and button state.
579
+ let accountConfigInput = $("#node-config-input-accountConfig");
580
+ accountConfigInput.on("change", () => {
581
+ updateGroupList(accountConfigInput.val());
582
+ setCreateButtonEnabled(accountConfigInput.val());
583
+ });
584
+ // when the group changes, update the name field and member list separately
585
+ let groupSelectionControl = $("#node-config-input-groupId");
586
+ groupSelectionControl.on("change", onGroupChanged);
587
+ groupSelectionControl.on("change", onGroupMembersLoad);
588
+ groupSelectionControl.typedInput({
589
+ types: [{
590
+ value: "group",
591
+ options: []
592
+ }]
593
+ });
594
+ // set initial button state
595
+ setCreateButtonEnabled(accountConfigInput.val());
596
+ // show inline create section
597
+ $("#wa-group-new-btn").on("click", () => {
598
+ $("#wa-group-new-row").hide();
599
+ $("#wa-group-create-section").show();
600
+ $("#wa-new-group-name").val("").focus();
601
+ });
602
+ // cancel
603
+ $("#wa-group-create-cancel-btn").on("click", () => {
604
+ $("#wa-group-create-section").hide();
605
+ $("#wa-group-new-row").show();
606
+ $("#wa-new-group-name").val("");
607
+ });
608
+ // create
609
+ $("#wa-group-create-confirm-btn").on("click", () => {
610
+ let groupName = $("#wa-new-group-name").val().trim();
611
+ if (!groupName) return;
612
+ let localConnectionId;
613
+ RED.nodes.eachConfig(cfg => {
614
+ if (cfg.id === accountConfigInput.val()) localConnectionId = cfg.localConnectionId;
615
+ });
616
+ if (!localConnectionId) return;
617
+ let $btn = $("#wa-group-create-confirm-btn");
618
+ $btn.prop("disabled", true);
619
+ waService.createGroup(node.id, localConnectionId, groupName).then(newGroup => {
620
+ return waService.getGroups(node.id, localConnectionId).then(groupListResponse => {
621
+ groupList = groupListResponse;
622
+ let groups = groupList.map(g => ({
623
+ value: g.id,
624
+ label: g.subject
625
+ }));
626
+ groupSelectionControl.typedInput("types", [{
627
+ value: "group",
628
+ options: groups
629
+ }]);
630
+ groupSelectionControl.typedInput("value", newGroup.id);
631
+ $("#wa-group-create-section").hide();
632
+ $("#wa-group-new-row").show();
633
+ $("#wa-new-group-name").val("");
634
+ });
635
+ }).catch(err => {
636
+ console.log("Error creating group:", err);
637
+ }).finally(() => {
638
+ $btn.prop("disabled", false);
639
+ });
640
+ });
641
+ }
642
+ },
643
+ oneditsave: function() {
644
+ // **** WhatsappGroupConfigNode **** //
645
+ {
646
+ let node = this;
647
+ }
648
+ },
649
+ oneditcancel: function() {},
650
+ oneditdelete: function() {},
651
+ });
652
+
653
+ </script>
654
+
655
+
656
+ <script type="text/html" data-template-name='WhatsappGroupConfigNode'>
657
+ <div id='section_WhatsappGroupConfigNode'>
658
+ <div class="form-row">
659
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
660
+ <input type="text" id="node-config-input-name" />
661
+ </div>
662
+
663
+ <div class="editorgroupborder">
664
+
665
+ <div class="form-row nomargin">
666
+ <label for="node-config-input-accountConfig"><i class="fa fa-user"></i> Account</label>
667
+ <input type="text" id="node-config-input-accountConfig" placeholder="config">
668
+ </div>
669
+
670
+ <div class="form-row">
671
+ <label for="node-config-input-groupId"><i class="fa fa-users"></i> Group</label>
672
+ <input type="text" id="node-config-input-groupId">
673
+ </div>
674
+
675
+ <div class="form-row" id="wa-group-new-row">
676
+ <label></label>
677
+ <button type="button" id="wa-group-new-btn" class="red-ui-button" disabled>
678
+ <i class="fa fa-plus"></i> Create New Group
679
+ </button>
680
+ </div>
681
+
682
+ <div id="wa-group-create-section" style="display:none;">
683
+ <div class="form-row nomargin">
684
+ <label style="width:120px;"><i class="fa fa-tag"></i> Group Name</label>
685
+ <input type="text" id="wa-new-group-name" style="width:calc(100% - 200px);">
686
+ </div>
687
+ <div class="form-row">
688
+ <label></label>
689
+ <button type="button" id="wa-group-create-confirm-btn" class="red-ui-button">
690
+ <i class="fa fa-check"></i> Create
691
+ </button>
692
+ <button type="button" id="wa-group-create-cancel-btn" class="red-ui-button" style="margin-left:4px;">
693
+ <i class="fa fa-times"></i> Cancel
694
+ </button>
695
+ </div>
696
+ </div>
697
+
698
+ <div class="form-row" style="font-weight: 600;">
699
+ <i class="w-16 fa fa-eye"></i> <span>Group Members</span>
700
+ </div>
701
+ <div class="form-row">
702
+ <span>
703
+ <ol id="node-input-groupMembers-container"></ol>
704
+ </span>
705
+ </div>
706
+ </div>
707
+
708
+
709
+
710
+ </div>
711
+ </script>
712
+
713
+ <script type="text/markdown" data-help-name='WhatsappGroupConfigNode'>
714
+ > **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
715
+
716
+ References a WhatsApp group within a linked account. Used as the target for Send Message nodes and the source for Receive Message nodes.
717
+
718
+ ### Properties
719
+
720
+ : *name* (string) : Display label for this config node.
721
+ : *account* (config) : The WhatsApp Account config node that owns this group.
722
+ : *group* (select) : The WhatsApp group to use. The dropdown is populated from the groups available on the selected account.
723
+
724
+ ### Creating a new group
725
+
726
+ Click **Create New Group** to create a new WhatsApp group from within Node-RED. The new group will appear in the group dropdown once created.
727
+ </script>
728
+ <!-- WhatsappSendMessageNode -->
729
+ <style>
730
+ </style>
731
+ <script>
732
+ const WA_SEND_FIELDS = [{
733
+ id: "text",
734
+ icon: "fa-font",
735
+ label: "Send Text",
736
+ types: ["msg", "flow", "global", "str"]
737
+ }, {
738
+ id: "image",
739
+ icon: "fa-image",
740
+ label: "Send Image",
741
+ types: ["msg", "flow", "global"]
742
+ }, ];
743
+
744
+ </script>
745
+ <!-- logger -->
746
+ <style>
747
+ .editorgroupborder {
748
+ border-width: 1px;
749
+ border-style: solid;
750
+ border-color: lightgray;
751
+ padding: 3px;
752
+ margin-top: 2px;
753
+ margin-bottom: 2px;
754
+ }
755
+
756
+ .editorsectionheading {
757
+ font-weight: 600;
758
+ margin-bottom: 1px !important;
759
+ margin-top: 6px !important;
760
+ }
761
+
762
+ .nomargin {
763
+ margin-bottom: 1px !important;
764
+ margin-top: 1px !important;
765
+ }
766
+
767
+ .slidecontainer {
768
+ width: 100%;
769
+ }
770
+
771
+ .slider {
772
+ -webkit-appearance: none;
773
+ appearance: none;
774
+ width: 100%;
775
+ height: 25px;
776
+ background: #d3d3d3;
777
+ outline: none;
778
+ opacity: 0.7;
779
+ -webkit-transition: .2s;
780
+ transition: opacity .2s;
781
+ }
782
+
783
+ .slider:hover {
784
+ opacity: 1;
785
+ }
786
+
787
+ .slider::-webkit-slider-thumb {
788
+ -webkit-appearance: none;
789
+ appearance: none;
790
+ width: 25px;
791
+ height: 25px;
792
+ background: #04AA6D;
793
+ cursor: pointer;
794
+ }
795
+
796
+ .slider::-moz-range-thumb {
797
+ width: 25px;
798
+ height: 25px;
799
+ background: #04AA6D;
800
+ cursor: pointer;
801
+ }
802
+
803
+ .towb_editorlabel {
804
+ width: 120px !important;
805
+ }
806
+
807
+ .towb_editorfield {
808
+ width: 70% !important;
809
+ }
810
+
811
+ .towb_editorfield_short {
812
+ width: 30% !important;
813
+ }
814
+
815
+ </style>
816
+ <script type="text/javascript">
817
+ // CSS
818
+ dropdownStyles = `
819
+ .loggertype-dropdown {
820
+ position: absolute;
821
+ background: white;
822
+ border: 1px solid #ccc;
823
+ border-radius: 4px;
824
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
825
+ max-height: 150px;
826
+ overflow-y: auto;
827
+ z-index: 1000;
828
+ min-width: 150px;
829
+ }
830
+ .loggertype-dropdown .dropdown-item {
831
+ padding: 6px 12px;
832
+ cursor: pointer;
833
+ }
834
+ .loggertype-dropdown .dropdown-item:hover {
835
+ background: #f0f0f0;
836
+ }
837
+ `;
838
+ $('<style>').text(dropdownStyles).appendTo('head');
839
+
840
+ </script>
841
+ <script type="text/javascript">
842
+ RED.nodes.registerType('WhatsappSendMessageNode', {
843
+ category: 'whatsapp',
844
+ icon: 'whatsapp.png',
845
+ color: '#E6FFDA',
846
+ label: function() {
847
+ return this.name
848
+ },
849
+ paletteLabel: 'WhatsApp Send Message',
850
+ inputs: 1,
851
+ inputLabels: (i) => 'message',
852
+ outputs: 0,
853
+ outputLabels: (i) => [][i],
854
+ defaults: {
855
+ name: {
856
+ value: 'Send WhatsApp Message',
857
+ required: true
858
+ },
859
+ groupConfig: {
860
+ type: 'WhatsappGroupConfigNode',
861
+ required: true
862
+ },
863
+ payloads: {
864
+ value: '[]',
865
+ required: false,
866
+ validate: function(value) {
867
+ try {
868
+ var payloads = JSON.parse(value || "[]");
869
+ return payloads.every(function(p) {
870
+ return !p.enabled || (p.value !== undefined && p.value !== "");
871
+ });
872
+ } catch (e) {
873
+ return false;
874
+ }
875
+ }
876
+ },
877
+ logEnabled: {
878
+ value: false,
879
+ required: true
880
+ },
881
+ logger: {
882
+ required: false,
883
+ value: '',
884
+ type: 'DelegatedConfigReferenceNode'
885
+ },
886
+ logTemplateOverrideEnabled: {
887
+ value: false
888
+ },
889
+ logTemplateOverride: {
890
+ value: 'message:{{msg}}'
891
+ },
892
+ metricsEnabled: {
893
+ value: false,
894
+ required: true
895
+ },
896
+ metricsReference: {
897
+ type: 'MetricsConfigNode',
898
+ required: false
899
+ },
900
+ },
901
+ oneditprepare: function() {
902
+ // **** WhatsappSendMessageNode **** //
903
+ {
904
+ let node = this;
905
+ let payloads = [];
906
+ try {
907
+ payloads = JSON.parse(node.payloads || "[]");
908
+ } catch (e) {}
909
+ WA_SEND_FIELDS.forEach(function(field) {
910
+ let saved = payloads.find(function(p) {
911
+ return p.id === field.id;
912
+ }) || {
913
+ enabled: false,
914
+ type: field.types[0],
915
+ value: ""
916
+ };
917
+ let row = $('<div class="form-row nomargin">' + '<label style="width:120px;"><i class="fa ' + field.icon + '"></i> ' + field.label + '</label>' + '<div id="wa-send-row-' + field.id + '" style="display:inline-flex; align-items:center; gap:6px; vertical-align:middle; width:calc(100% - 128px);">' + '<input type="checkbox" id="wa-send-enabled-' + field.id + '" style="width:auto; margin:0; flex-shrink:0;">' + '<input type="text" id="wa-send-value-' + field.id + '">' + '</div>' + '</div>');
918
+ $("#wa-send-payload-rows").append(row);
919
+ $("#wa-send-value-" + field.id).typedInput({
920
+ types: field.types
921
+ }).typedInput("type", saved.type).typedInput("value", saved.value);
922
+ // typedInput inserts its container as a sibling of the (now hidden) input, making it
923
+ // a direct flex child — apply flex:1 so it expands to fill the remaining row width.
924
+ $("#wa-send-row-" + field.id + " .red-ui-typedInput-container").css({
925
+ "flex": "1",
926
+ "min-width": "0",
927
+ "width": ""
928
+ });
929
+ $("#wa-send-enabled-" + field.id).prop("checked", saved.enabled);
930
+ // live validation: highlight the typedInput when checkbox is checked but value is empty.
931
+ var checkFieldValid = function() {
932
+ var enabled = $("#wa-send-enabled-" + field.id).prop("checked");
933
+ var value = $("#wa-send-value-" + field.id).typedInput("value");
934
+ var invalid = enabled && (!value || value === "");
935
+ $("#wa-send-row-" + field.id + " .red-ui-typedInput-container").toggleClass("input-error", invalid);
936
+ };
937
+ $("#wa-send-enabled-" + field.id).on("change", checkFieldValid);
938
+ $("#wa-send-value-" + field.id).on("change", checkFieldValid);
939
+ checkFieldValid();
940
+ });
941
+ }
942
+ // **** logger **** //
943
+ {
944
+ let node = this;
945
+ // controls.
946
+ let logEnabled = $("#node-input-logEnabled");
947
+ let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
948
+ let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
949
+ let logEnabledSection = $("#section_logEnabled");
950
+ // functions.
951
+ let getLoggerTypes = async function() {
952
+ let types = [];
953
+ await $.ajax({
954
+ url: "nodetypeservice/find?tag=LoggerType",
955
+ type: "GET",
956
+ contentType: "application/json; charset=utf-8",
957
+ data: JSON.stringify({
958
+ id: node.id
959
+ }),
960
+ success: (response) => {
961
+ types = response;
962
+ },
963
+ error: (jqXHR, textStatus, errorThrown) => {
964
+ console.log(jqXHR, textStatus, errorThrown);
965
+ },
966
+ });
967
+ return types;
968
+ }
969
+ let getLoggers = async () => {
970
+ let loggers = [];
971
+ // get the list of types again.
972
+ let loggerTypes = (await getLoggerTypes()).map(type => type.id);
973
+ // add all of the known loggers.
974
+ RED.nodes.eachConfig(cfg => {
975
+ if (loggerTypes.includes(cfg.type)) {
976
+ loggers.push({
977
+ id: cfg.id,
978
+ name: cfg.label(),
979
+ type: cfg.type
980
+ });
981
+ }
982
+ });
983
+ return loggers;
984
+ };
985
+ let updateLoggerSelectorList = async () => {
986
+ loggerSelector.empty();
987
+ (await getLoggers()).forEach(logger => {
988
+ $('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
989
+ });
990
+ $('<option value="_ADD_">none</option>').appendTo(loggerSelector);
991
+ }
992
+ logEnabled.on('change', function() {
993
+ if (logEnabled.prop("checked")) {
994
+ logEnabledSection.show();
995
+ node._def.defaults.logger.required = true;
996
+ } else {
997
+ logEnabledSection.hide()
998
+ node._def.defaults.logger.required = false;
999
+ }
1000
+ });
1001
+ logTemplateEnabled.on('change', function() {
1002
+ if (logTemplateEnabled.prop("checked")) {
1003
+ logTemplateEnabledSection.show();
1004
+ node._def.defaults.logTemplateOverride.required = true;
1005
+ } else {
1006
+ logTemplateEnabledSection.hide()
1007
+ node._def.defaults.logTemplateOverride.required = false;
1008
+ }
1009
+ });
1010
+ node.logTemplateOverrideEditor = RED.editor.createEditor({
1011
+ id: 'node-input-logTemplateOverrideEditor',
1012
+ mode: 'ace/mode/handlebars',
1013
+ value: node.logTemplateOverride
1014
+ });
1015
+ let loggerSelector = $("#logger-selector");
1016
+ let loggerEditButton = $("#logger-edit-btn");
1017
+ let loggerAddButton = $("#logger-add-btn");
1018
+ loggerEditButton.on('click', async function(event) {
1019
+ event.stopPropagation();
1020
+ if (loggerEditButton.hasClass("disabled")) {
1021
+ return;
1022
+ }
1023
+ // figure out what type of logger this is.
1024
+ let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
1025
+ RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
1026
+ });
1027
+ loggerAddButton.on('click', function(event) {
1028
+ event.stopPropagation();
1029
+ let $btn = $(event.target);
1030
+ // Remove any existing dropdown
1031
+ $('.loggertype-dropdown').remove();
1032
+ getLoggerTypes().then(loggerTypes => {
1033
+ let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
1034
+ loggerTypes.forEach(loggerType => {
1035
+ $('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
1036
+ padding: '4px 8px',
1037
+ cursor: 'pointer'
1038
+ }).appendTo($dropdown);
1039
+ });
1040
+ // Append first so outerWidth() is measurable, then position so the
1041
+ // right edge of the dropdown aligns with the right edge of the button.
1042
+ $('body').append($dropdown);
1043
+ $dropdown.css({
1044
+ top: $btn.offset().top + $btn.outerHeight(),
1045
+ left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
1046
+ });
1047
+ $(document).on('mousedown.dropdown', function(e) {
1048
+ if (!$(e.target).closest('.loggertype-dropdown').length) {
1049
+ $dropdown.remove();
1050
+ $(document).off('mousedown.dropdown'); // Clean up the listener
1051
+ }
1052
+ });
1053
+ // Handle item selection
1054
+ $dropdown.on('click', '.dropdown-item', function(e) {
1055
+ e.stopPropagation();
1056
+ const selectedValue = $(this).data('value');
1057
+ const selectedText = $(this).text();
1058
+ // Do whatever you need with the selection
1059
+ console.log('Selected:', selectedValue, selectedText);
1060
+ $dropdown.remove();
1061
+ RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
1062
+ });
1063
+ })
1064
+ });
1065
+ loggerSelector.on("focus", updateLoggerSelectorList);
1066
+ loggerSelector.on('change', function(event) {
1067
+ if ("_ADD_" === loggerSelector.val()) {
1068
+ loggerEditButton.addClass("disabled")
1069
+ } else {
1070
+ loggerEditButton.removeClass("disabled")
1071
+ }
1072
+ });
1073
+ // lastly, make sure that we set the correct value on the logger selector.
1074
+ updateLoggerSelectorList().then(() => {
1075
+ loggerSelector.val(node.logger);
1076
+ loggerSelector.trigger('change');
1077
+ });
1078
+ }
1079
+ // **** metrics **** //
1080
+ {
1081
+ let node = this;
1082
+ let metricsEnabled = $("#node-input-metricsEnabled");
1083
+ let metricsEnabledSection = $("#section_metricsEnabled");
1084
+ metricsEnabled.on('change', function() {
1085
+ if (metricsEnabled.prop("checked")) {
1086
+ metricsEnabledSection.show();
1087
+ node._def.defaults.metricsReference.required = true;
1088
+ } else {
1089
+ metricsEnabledSection.hide()
1090
+ node._def.defaults.metricsReference.required = false;
1091
+ }
1092
+ });
1093
+ }
1094
+ },
1095
+ oneditsave: function() {
1096
+ // **** WhatsappSendMessageNode **** //
1097
+ {
1098
+ let payloads = WA_SEND_FIELDS.map(function(field) {
1099
+ return {
1100
+ id: field.id,
1101
+ enabled: $("#wa-send-enabled-" + field.id).prop("checked"),
1102
+ type: $("#wa-send-value-" + field.id).typedInput("type"),
1103
+ value: $("#wa-send-value-" + field.id).typedInput("value"),
1104
+ };
1105
+ });
1106
+ $("#node-input-payloads").val(JSON.stringify(payloads));
1107
+ }
1108
+ // **** logger **** //
1109
+ {
1110
+ node = this;
1111
+ var selectedLogger = $("#logger-selector").val();
1112
+ node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
1113
+ node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
1114
+ node.logTemplateOverrideEditor.destroy();
1115
+ delete node.logTemplateOverrideEditor;
1116
+ }
1117
+ // **** metrics **** //
1118
+ {}
1119
+ },
1120
+ oneditcancel: function() {},
1121
+ oneditdelete: function() {},
1122
+ });
1123
+
1124
+ </script>
1125
+
1126
+
1127
+ <script type="text/html" data-template-name='WhatsappSendMessageNode'>
1128
+ <div id='section_WhatsappSendMessageNode'>
1129
+ <div class="form-row">
1130
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1131
+ <input type="text" id="node-input-name" />
1132
+ </div>
1133
+
1134
+ <div class="form-row nomargin">
1135
+ <label for="node-input-groupConfig" style="width:120px;"><i class="fa fa-users"></i> Group</label>
1136
+ <input type="text" id="node-input-groupConfig" placeholder="config">
1137
+ </div>
1138
+
1139
+ <div class="form-row editorsectionheading">
1140
+ <i class="w-16 fa fa-paper-plane"></i> <span>Send</span>
1141
+ </div>
1142
+
1143
+ <div class="editorgroupborder" id="wa-send-payload-rows">
1144
+ </div>
1145
+
1146
+ <input type="hidden" id="node-input-payloads">
1147
+
1148
+
1149
+ </div>
1150
+
1151
+ <div id='section_logger'>
1152
+ <div id="section_loggerconfig">
1153
+ <div class="form-row editorsectionheading">
1154
+ <i class="w-16 fa fa-eye"></i> <span>Logging</span>
1155
+ </div>
1156
+ <div class="editorgroupborder">
1157
+ <div class="form-row nomargin logEnableCheck">
1158
+ <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
1159
+ <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
1160
+ </div>
1161
+
1162
+ <div id="section_logEnabled">
1163
+ <div class="form-row nomargin">
1164
+ <label for="logger-selector">Logger</label>
1165
+ <div style="width: 70%; display: inline-flex;">
1166
+ <select id="logger-selector" style="flex-grow: 1;" class="">
1167
+ <option value="_ADD_">none</option>
1168
+ </select>
1169
+ <a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
1170
+ <i class="fa fa-pencil"></i>
1171
+ </a>
1172
+ <a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
1173
+ <i class="fa fa-plus"></i>
1174
+ </a>
1175
+ </div>
1176
+ </div>
1177
+
1178
+ <div class="form-row nomargin">
1179
+ <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
1180
+ <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
1181
+ </div>
1182
+
1183
+ <div id="section_logTemplateOverrideEnabled">
1184
+ <div class="form-row">
1185
+ <label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
1186
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
1187
+ </div>
1188
+ </div>
1189
+ </div>
1190
+ </div>
1191
+ </div>
1192
+
1193
+ </div>
1194
+
1195
+ <div id='section_metrics'>
1196
+ <div id="section_metricsconfig">
1197
+ <div class="form-row editorsectionheading">
1198
+ <i class="w-16 fa fa-eye"></i> <span>Metrics</span>
1199
+ </div>
1200
+ <div class="editorgroupborder">
1201
+ <div class="form-row nomargin metricsEnableCheck">
1202
+ <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
1203
+ <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
1204
+ </div>
1205
+
1206
+ <div id="section_metricsEnabled">
1207
+ <div class="form-row nomargin">
1208
+ <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
1209
+ <input id="node-input-metricsReference" placeholder="metrics" />
1210
+ </div>
1211
+ </div>
1212
+ </div>
1213
+ </div>
1214
+
1215
+ </div>
1216
+ </script>
1217
+
1218
+ <script type="text/markdown" data-help-name='WhatsappSendMessageNode'>
1219
+ > **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
1220
+
1221
+ Sends a message to a WhatsApp group.
1222
+
1223
+ ### Properties
1224
+
1225
+ : *name* (string) : Display label for this node.
1226
+ : *group* (config) : The WhatsApp Group config node to send to.
1227
+
1228
+ ### Send fields
1229
+
1230
+ Each field has a checkbox to enable it and a typed value. Supported value types depend on the field:
1231
+
1232
+ - **Send Text** — `msg`, `flow`, `global`, `str`
1233
+ - **Send Image** — `msg`, `flow`, `global`
1234
+
1235
+ ### Inputs
1236
+
1237
+ : *msg* : Trigger message. Field values are resolved against this message (for `msg` type) or from context.
1238
+
1239
+ ### Logging
1240
+ : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
1241
+ : *logger* (logconfig) : the endppoint that the node will log events to.
1242
+ : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
1243
+ : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
1244
+
1245
+ ### Metrics
1246
+ : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
1247
+ : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
1248
+ </script>
1249
+ <!-- WhatsappReceiveMessageNode -->
1250
+ <style>
1251
+ </style>
1252
+ <script>
1253
+ const WA_ACCEPT_FIELDS = [{
1254
+ id: "Text",
1255
+ icon: "fa-font",
1256
+ label: "Text",
1257
+ defaultEnabled: true
1258
+ }, {
1259
+ id: "ExtendedText",
1260
+ icon: "fa-align-left",
1261
+ label: "Extended Text",
1262
+ defaultEnabled: true
1263
+ }, {
1264
+ id: "Image",
1265
+ icon: "fa-image",
1266
+ label: "Image",
1267
+ defaultEnabled: true
1268
+ }, {
1269
+ id: "Video",
1270
+ icon: "fa-video-camera",
1271
+ label: "Video",
1272
+ defaultEnabled: true
1273
+ }, {
1274
+ id: "Album",
1275
+ icon: "fa-th-large",
1276
+ label: "Album",
1277
+ defaultEnabled: true
1278
+ }, {
1279
+ id: "Document",
1280
+ icon: "fa-file",
1281
+ label: "Document",
1282
+ defaultEnabled: false
1283
+ }, {
1284
+ id: "Contact",
1285
+ icon: "fa-address-card",
1286
+ label: "Contact",
1287
+ defaultEnabled: false
1288
+ }, {
1289
+ id: "Template",
1290
+ icon: "fa-th-list",
1291
+ label: "Template",
1292
+ defaultEnabled: false
1293
+ }, {
1294
+ id: "Interactive",
1295
+ icon: "fa-hand-o-up",
1296
+ label: "Interactive",
1297
+ defaultEnabled: false
1298
+ }, {
1299
+ id: "Location",
1300
+ icon: "fa-map-marker",
1301
+ label: "Location",
1302
+ defaultEnabled: false
1303
+ }, {
1304
+ id: "LiveLocation",
1305
+ icon: "fa-location-arrow",
1306
+ label: "Live Location",
1307
+ defaultEnabled: false
1308
+ }, ];
1309
+
1310
+ </script>
1311
+ <!-- logger -->
1312
+ <style>
1313
+ .editorgroupborder {
1314
+ border-width: 1px;
1315
+ border-style: solid;
1316
+ border-color: lightgray;
1317
+ padding: 3px;
1318
+ margin-top: 2px;
1319
+ margin-bottom: 2px;
1320
+ }
1321
+
1322
+ .editorsectionheading {
1323
+ font-weight: 600;
1324
+ margin-bottom: 1px !important;
1325
+ margin-top: 6px !important;
1326
+ }
1327
+
1328
+ .nomargin {
1329
+ margin-bottom: 1px !important;
1330
+ margin-top: 1px !important;
1331
+ }
1332
+
1333
+ .slidecontainer {
1334
+ width: 100%;
1335
+ }
1336
+
1337
+ .slider {
1338
+ -webkit-appearance: none;
1339
+ appearance: none;
1340
+ width: 100%;
1341
+ height: 25px;
1342
+ background: #d3d3d3;
1343
+ outline: none;
1344
+ opacity: 0.7;
1345
+ -webkit-transition: .2s;
1346
+ transition: opacity .2s;
1347
+ }
1348
+
1349
+ .slider:hover {
1350
+ opacity: 1;
1351
+ }
1352
+
1353
+ .slider::-webkit-slider-thumb {
1354
+ -webkit-appearance: none;
1355
+ appearance: none;
1356
+ width: 25px;
1357
+ height: 25px;
1358
+ background: #04AA6D;
1359
+ cursor: pointer;
1360
+ }
1361
+
1362
+ .slider::-moz-range-thumb {
1363
+ width: 25px;
1364
+ height: 25px;
1365
+ background: #04AA6D;
1366
+ cursor: pointer;
1367
+ }
1368
+
1369
+ .towb_editorlabel {
1370
+ width: 120px !important;
1371
+ }
1372
+
1373
+ .towb_editorfield {
1374
+ width: 70% !important;
1375
+ }
1376
+
1377
+ .towb_editorfield_short {
1378
+ width: 30% !important;
1379
+ }
1380
+
1381
+ </style>
1382
+ <script type="text/javascript">
1383
+ // CSS
1384
+ dropdownStyles = `
1385
+ .loggertype-dropdown {
1386
+ position: absolute;
1387
+ background: white;
1388
+ border: 1px solid #ccc;
1389
+ border-radius: 4px;
1390
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1391
+ max-height: 150px;
1392
+ overflow-y: auto;
1393
+ z-index: 1000;
1394
+ min-width: 150px;
1395
+ }
1396
+ .loggertype-dropdown .dropdown-item {
1397
+ padding: 6px 12px;
1398
+ cursor: pointer;
1399
+ }
1400
+ .loggertype-dropdown .dropdown-item:hover {
1401
+ background: #f0f0f0;
1402
+ }
1403
+ `;
1404
+ $('<style>').text(dropdownStyles).appendTo('head');
1405
+
1406
+ </script>
1407
+ <script type="text/javascript">
1408
+ RED.nodes.registerType('WhatsappReceiveMessageNode', {
1409
+ category: 'whatsapp',
1410
+ icon: 'whatsapp.png',
1411
+ color: '#E6FFDA',
1412
+ label: function() {
1413
+ return this.name || "WhatsappReceiveMessage";
1414
+ },
1415
+ paletteLabel: 'WhatsApp Receive Message',
1416
+ inputs: 0,
1417
+ inputLabels: (i) => 'undefined',
1418
+ outputs: 1,
1419
+ outputLabels: (i) => ['message'][i],
1420
+ defaults: {
1421
+ name: {
1422
+ value: 'Receive Whatsapp Message',
1423
+ required: true
1424
+ },
1425
+ groupConfig: {
1426
+ type: 'WhatsappGroupConfigNode',
1427
+ required: true
1428
+ },
1429
+ eventOutputKey: {
1430
+ value: 'payload'
1431
+ },
1432
+ eventOutputType: {
1433
+ value: 'msg'
1434
+ },
1435
+ acceptOwnMessages: {
1436
+ value: false
1437
+ },
1438
+ accepts: {
1439
+ value: '[]'
1440
+ },
1441
+ logEnabled: {
1442
+ value: false,
1443
+ required: true
1444
+ },
1445
+ logger: {
1446
+ required: false,
1447
+ value: '',
1448
+ type: 'DelegatedConfigReferenceNode'
1449
+ },
1450
+ logTemplateOverrideEnabled: {
1451
+ value: false
1452
+ },
1453
+ logTemplateOverride: {
1454
+ value: 'message:{{msg}}'
1455
+ },
1456
+ metricsEnabled: {
1457
+ value: false,
1458
+ required: true
1459
+ },
1460
+ metricsReference: {
1461
+ type: 'MetricsConfigNode',
1462
+ required: false
1463
+ },
1464
+ },
1465
+ oneditprepare: function() {
1466
+ // **** WhatsappReceiveMessageNode **** //
1467
+ {
1468
+ let node = this;
1469
+ $("#node-input-eventOutputKey").typedInput({
1470
+ types: ["msg", "flow", "global"],
1471
+ typeField: "#node-input-eventOutputType"
1472
+ });
1473
+ let accepts = [];
1474
+ try {
1475
+ accepts = JSON.parse(node.accepts || "[]");
1476
+ } catch (e) {}
1477
+ let grid = $('<div style="display:grid; grid-template-columns:1fr 1fr; gap:4px 8px; padding:4px 0;"></div>');
1478
+ $("#wa-accept-rows").append(grid);
1479
+ WA_ACCEPT_FIELDS.forEach(function(field) {
1480
+ let saved = accepts.find(function(a) {
1481
+ return a.id === field.id;
1482
+ }) || {
1483
+ enabled: field.defaultEnabled
1484
+ };
1485
+ let cell = $('<div style="display:flex; align-items:center; gap:6px;">' + '<input type="checkbox" id="wa-accept-' + field.id + '" style="width:auto; margin:0; flex-shrink:0;">' + '<label for="wa-accept-' + field.id + '" style="margin:0; width:auto;"><i class="fa ' + field.icon + '"></i> ' + field.label + '</label>' + '</div>');
1486
+ grid.append(cell);
1487
+ $("#wa-accept-" + field.id).prop("checked", saved.enabled);
1488
+ });
1489
+ }
1490
+ // **** logger **** //
1491
+ {
1492
+ let node = this;
1493
+ // controls.
1494
+ let logEnabled = $("#node-input-logEnabled");
1495
+ let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
1496
+ let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
1497
+ let logEnabledSection = $("#section_logEnabled");
1498
+ // functions.
1499
+ let getLoggerTypes = async function() {
1500
+ let types = [];
1501
+ await $.ajax({
1502
+ url: "nodetypeservice/find?tag=LoggerType",
1503
+ type: "GET",
1504
+ contentType: "application/json; charset=utf-8",
1505
+ data: JSON.stringify({
1506
+ id: node.id
1507
+ }),
1508
+ success: (response) => {
1509
+ types = response;
1510
+ },
1511
+ error: (jqXHR, textStatus, errorThrown) => {
1512
+ console.log(jqXHR, textStatus, errorThrown);
1513
+ },
1514
+ });
1515
+ return types;
1516
+ }
1517
+ let getLoggers = async () => {
1518
+ let loggers = [];
1519
+ // get the list of types again.
1520
+ let loggerTypes = (await getLoggerTypes()).map(type => type.id);
1521
+ // add all of the known loggers.
1522
+ RED.nodes.eachConfig(cfg => {
1523
+ if (loggerTypes.includes(cfg.type)) {
1524
+ loggers.push({
1525
+ id: cfg.id,
1526
+ name: cfg.label(),
1527
+ type: cfg.type
1528
+ });
1529
+ }
1530
+ });
1531
+ return loggers;
1532
+ };
1533
+ let updateLoggerSelectorList = async () => {
1534
+ loggerSelector.empty();
1535
+ (await getLoggers()).forEach(logger => {
1536
+ $('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
1537
+ });
1538
+ $('<option value="_ADD_">none</option>').appendTo(loggerSelector);
1539
+ }
1540
+ logEnabled.on('change', function() {
1541
+ if (logEnabled.prop("checked")) {
1542
+ logEnabledSection.show();
1543
+ node._def.defaults.logger.required = true;
1544
+ } else {
1545
+ logEnabledSection.hide()
1546
+ node._def.defaults.logger.required = false;
1547
+ }
1548
+ });
1549
+ logTemplateEnabled.on('change', function() {
1550
+ if (logTemplateEnabled.prop("checked")) {
1551
+ logTemplateEnabledSection.show();
1552
+ node._def.defaults.logTemplateOverride.required = true;
1553
+ } else {
1554
+ logTemplateEnabledSection.hide()
1555
+ node._def.defaults.logTemplateOverride.required = false;
1556
+ }
1557
+ });
1558
+ node.logTemplateOverrideEditor = RED.editor.createEditor({
1559
+ id: 'node-input-logTemplateOverrideEditor',
1560
+ mode: 'ace/mode/handlebars',
1561
+ value: node.logTemplateOverride
1562
+ });
1563
+ let loggerSelector = $("#logger-selector");
1564
+ let loggerEditButton = $("#logger-edit-btn");
1565
+ let loggerAddButton = $("#logger-add-btn");
1566
+ loggerEditButton.on('click', async function(event) {
1567
+ event.stopPropagation();
1568
+ if (loggerEditButton.hasClass("disabled")) {
1569
+ return;
1570
+ }
1571
+ // figure out what type of logger this is.
1572
+ let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
1573
+ RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
1574
+ });
1575
+ loggerAddButton.on('click', function(event) {
1576
+ event.stopPropagation();
1577
+ let $btn = $(event.target);
1578
+ // Remove any existing dropdown
1579
+ $('.loggertype-dropdown').remove();
1580
+ getLoggerTypes().then(loggerTypes => {
1581
+ let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
1582
+ loggerTypes.forEach(loggerType => {
1583
+ $('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
1584
+ padding: '4px 8px',
1585
+ cursor: 'pointer'
1586
+ }).appendTo($dropdown);
1587
+ });
1588
+ // Append first so outerWidth() is measurable, then position so the
1589
+ // right edge of the dropdown aligns with the right edge of the button.
1590
+ $('body').append($dropdown);
1591
+ $dropdown.css({
1592
+ top: $btn.offset().top + $btn.outerHeight(),
1593
+ left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
1594
+ });
1595
+ $(document).on('mousedown.dropdown', function(e) {
1596
+ if (!$(e.target).closest('.loggertype-dropdown').length) {
1597
+ $dropdown.remove();
1598
+ $(document).off('mousedown.dropdown'); // Clean up the listener
1599
+ }
1600
+ });
1601
+ // Handle item selection
1602
+ $dropdown.on('click', '.dropdown-item', function(e) {
1603
+ e.stopPropagation();
1604
+ const selectedValue = $(this).data('value');
1605
+ const selectedText = $(this).text();
1606
+ // Do whatever you need with the selection
1607
+ console.log('Selected:', selectedValue, selectedText);
1608
+ $dropdown.remove();
1609
+ RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
1610
+ });
1611
+ })
1612
+ });
1613
+ loggerSelector.on("focus", updateLoggerSelectorList);
1614
+ loggerSelector.on('change', function(event) {
1615
+ if ("_ADD_" === loggerSelector.val()) {
1616
+ loggerEditButton.addClass("disabled")
1617
+ } else {
1618
+ loggerEditButton.removeClass("disabled")
1619
+ }
1620
+ });
1621
+ // lastly, make sure that we set the correct value on the logger selector.
1622
+ updateLoggerSelectorList().then(() => {
1623
+ loggerSelector.val(node.logger);
1624
+ loggerSelector.trigger('change');
1625
+ });
1626
+ }
1627
+ // **** metrics **** //
1628
+ {
1629
+ let node = this;
1630
+ let metricsEnabled = $("#node-input-metricsEnabled");
1631
+ let metricsEnabledSection = $("#section_metricsEnabled");
1632
+ metricsEnabled.on('change', function() {
1633
+ if (metricsEnabled.prop("checked")) {
1634
+ metricsEnabledSection.show();
1635
+ node._def.defaults.metricsReference.required = true;
1636
+ } else {
1637
+ metricsEnabledSection.hide()
1638
+ node._def.defaults.metricsReference.required = false;
1639
+ }
1640
+ });
1641
+ }
1642
+ },
1643
+ oneditsave: function() {
1644
+ // **** WhatsappReceiveMessageNode **** //
1645
+ {
1646
+ let accepts = WA_ACCEPT_FIELDS.map(function(field) {
1647
+ return {
1648
+ id: field.id,
1649
+ enabled: $("#wa-accept-" + field.id).prop("checked"),
1650
+ };
1651
+ });
1652
+ $("#node-input-accepts").val(JSON.stringify(accepts));
1653
+ }
1654
+ // **** logger **** //
1655
+ {
1656
+ node = this;
1657
+ var selectedLogger = $("#logger-selector").val();
1658
+ node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
1659
+ node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
1660
+ node.logTemplateOverrideEditor.destroy();
1661
+ delete node.logTemplateOverrideEditor;
1662
+ }
1663
+ // **** metrics **** //
1664
+ {}
1665
+ },
1666
+ oneditcancel: function() {},
1667
+ oneditdelete: function() {},
1668
+ });
1669
+
1670
+ </script>
1671
+
1672
+
1673
+ <script type="text/html" data-template-name='WhatsappReceiveMessageNode'>
1674
+ <div id='section_WhatsappReceiveMessageNode'>
1675
+ <div class="form-row">
1676
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1677
+ <input type="text" id="node-input-name" placeholder="Name">
1678
+ </div>
1679
+
1680
+ <div class="form-row editorsectionheading">
1681
+ <i class="w-16 fa fa-envelope"></i> <span>WhatsApp Message</span>
1682
+ </div>
1683
+
1684
+ <div class="editorgroupborder">
1685
+
1686
+ <div class="form-row nomargin">
1687
+ <label for="node-input-groupConfig" style="width:120px;"><i class="fa fa-users"></i> Group</label>
1688
+ <input type="text" id="node-input-groupConfig" placeholder="config">
1689
+ </div>
1690
+
1691
+ <div class="form-row nomargin">
1692
+ <label for="node-input-acceptOwnMessages" style="width:120px;"><i class="fa fa-reply"></i> Own Messages</label>
1693
+ <input type="checkbox" id="node-input-acceptOwnMessages" style="width:auto; margin:0; vertical-align:middle;">
1694
+ </div>
1695
+
1696
+ <div class="form-row nomargin">
1697
+ <label for="node-input-eventOutputKey" style="width:120px;"><i class="fa fa-arrow-right"></i> Output Path</label>
1698
+ <input type="text" id="node-input-eventOutputKey" placeholder="payload">
1699
+ <input type="hidden" id="node-input-eventOutputType">
1700
+ </div>
1701
+
1702
+ </div>
1703
+
1704
+ <div class="form-row editorsectionheading">
1705
+ <i class="w-16 fa fa-filter"></i> <span>Accept</span>
1706
+ </div>
1707
+
1708
+ <div class="editorgroupborder" id="wa-accept-rows">
1709
+ </div>
1710
+
1711
+ <input type="hidden" id="node-input-accepts">
1712
+
1713
+
1714
+ </div>
1715
+
1716
+ <div id='section_logger'>
1717
+ <div id="section_loggerconfig">
1718
+ <div class="form-row editorsectionheading">
1719
+ <i class="w-16 fa fa-eye"></i> <span>Logging</span>
1720
+ </div>
1721
+ <div class="editorgroupborder">
1722
+ <div class="form-row nomargin logEnableCheck">
1723
+ <input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
1724
+ <label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
1725
+ </div>
1726
+
1727
+ <div id="section_logEnabled">
1728
+ <div class="form-row nomargin">
1729
+ <label for="logger-selector">Logger</label>
1730
+ <div style="width: 70%; display: inline-flex;">
1731
+ <select id="logger-selector" style="flex-grow: 1;" class="">
1732
+ <option value="_ADD_">none</option>
1733
+ </select>
1734
+ <a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
1735
+ <i class="fa fa-pencil"></i>
1736
+ </a>
1737
+ <a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
1738
+ <i class="fa fa-plus"></i>
1739
+ </a>
1740
+ </div>
1741
+ </div>
1742
+
1743
+ <div class="form-row nomargin">
1744
+ <input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
1745
+ <label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
1746
+ </div>
1747
+
1748
+ <div id="section_logTemplateOverrideEnabled">
1749
+ <div class="form-row">
1750
+ <label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
1751
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
1752
+ </div>
1753
+ </div>
1754
+ </div>
1755
+ </div>
1756
+ </div>
1757
+
1758
+ </div>
1759
+
1760
+ <div id='section_metrics'>
1761
+ <div id="section_metricsconfig">
1762
+ <div class="form-row editorsectionheading">
1763
+ <i class="w-16 fa fa-eye"></i> <span>Metrics</span>
1764
+ </div>
1765
+ <div class="editorgroupborder">
1766
+ <div class="form-row nomargin metricsEnableCheck">
1767
+ <input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
1768
+ <label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
1769
+ </div>
1770
+
1771
+ <div id="section_metricsEnabled">
1772
+ <div class="form-row nomargin">
1773
+ <label class="towb_editorlabel" for="node-input-metricsReference">Metric Collector</label>
1774
+ <input id="node-input-metricsReference" placeholder="metrics" />
1775
+ </div>
1776
+ </div>
1777
+ </div>
1778
+ </div>
1779
+
1780
+ </div>
1781
+ </script>
1782
+
1783
+ <script type="text/markdown" data-help-name='WhatsappReceiveMessageNode'>
1784
+ > **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
1785
+
1786
+ Receives messages from a WhatsApp group and outputs them as Node-RED messages.
1787
+
1788
+ ### Properties
1789
+
1790
+ : *name* (string) : Display label for this node.
1791
+ : *group* (config) : The WhatsApp Group config node to receive from.
1792
+ : *own messages* (bool) : Whether to forward messages sent by this account.
1793
+ : *output path* (typed) : Where in the output `msg` to place the WhatsApp message, or which context variable to write to.
1794
+
1795
+ ### Accept filters
1796
+
1797
+ Each message type can be individually enabled or disabled. By default Text, Extended Text, Image, Video, and Album messages are accepted.
1798
+
1799
+ ### Outputs
1800
+
1801
+ : *msg* : Output message. The WhatsApp message is placed at the configured output path.
1802
+
1803
+ ### Logging
1804
+ : *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
1805
+ : *logger* (logconfig) : the endppoint that the node will log events to.
1806
+ : *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
1807
+ : *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
1808
+
1809
+ ### Metrics
1810
+ : *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
1811
+ : *metric collector* (metricconfig) : the collection point or grouping where this particular metric will be attached.
1812
+ </script>
1813
+
1814
+