@theotherwillembotha/node-red-whatsapp 0.0.55 → 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.
- package/README.md +33 -21
- package/build/GenerateNodes.js +5 -7
- package/build/Nodes.html +1451 -524
- package/build/Nodes.js +52397 -14
- package/build/Plugins.html +37 -0
- package/build/Plugins.js +45964 -55
- package/build/index.js +1 -0
- package/build/runtime/NodeManagerRuntime.js +140 -0
- package/build/whatsapp/node/WhatsappAccountConfigNode.js +7 -2
- package/build/whatsapp/node/WhatsappDynamicSendMessageNode.js +112 -0
- package/build/whatsapp/node/WhatsappGroupConfigNode.js +19 -8
- package/build/whatsapp/node/WhatsappReceiveMessageNode.js +2 -2
- package/build/whatsapp/node/WhatsappSendMessageNode.js +55 -33
- package/build/whatsapp/service/WhatsappClient.js +140 -14
- package/build/whatsapp/service/WhatsappService.js +85 -23
- package/build/whatsapp/service/WhatsappStore.js +10 -7
- package/package.json +10 -8
package/build/Nodes.html
CHANGED
|
@@ -7,173 +7,418 @@
|
|
|
7
7
|
You have been warned.
|
|
8
8
|
|
|
9
9
|
-->
|
|
10
|
-
|
|
11
10
|
<!-- WhatsappAccountConfigNode -->
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
.theotherwillembotha-compact-table li {
|
|
14
|
+
padding-top: 4px;
|
|
15
|
+
padding-bottom: 4px;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
<script type="text/javascript">
|
|
21
|
+
// a simple client for interfaceing with the WhatsappService on the backend. Note: this needs to be moved to its own file.
|
|
22
|
+
let waService = {
|
|
23
|
+
unlinkAccount : function(nodeID, localConnectionId){
|
|
24
|
+
return new Promise((resolve, fail) => {
|
|
25
|
+
|
|
26
|
+
let request = JSON.stringify({
|
|
27
|
+
nodeID: nodeID,
|
|
28
|
+
localConnectionId: localConnectionId
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
$.ajax({
|
|
32
|
+
url: "whatsapp/unlinkaccount",
|
|
33
|
+
type: "POST",
|
|
34
|
+
data: request,
|
|
35
|
+
contentType: "application/json; charset=utf-8",
|
|
36
|
+
success: (response) => {
|
|
37
|
+
resolve(response);
|
|
38
|
+
},
|
|
39
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
40
|
+
fail(errorThrown);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
getConnectionDetails:function(nodeID, localConnectionId){
|
|
47
|
+
return new Promise((resolve, fail) => {
|
|
48
|
+
let request = JSON.stringify({
|
|
49
|
+
nodeID: nodeID,
|
|
50
|
+
localConnectionId: localConnectionId
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
$.ajax({
|
|
54
|
+
url: "whatsapp/connectiondetails",
|
|
55
|
+
type: "POST",
|
|
56
|
+
data: request,
|
|
57
|
+
contentType: "application/json; charset=utf-8",
|
|
58
|
+
success: (response) => {
|
|
59
|
+
resolve(response);
|
|
60
|
+
},
|
|
61
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
62
|
+
fail(errorThrown);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
createGroup:function(nodeID, localConnectionId, name){
|
|
69
|
+
console.log("create group! " + name);
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve, fail) => {
|
|
72
|
+
let request = JSON.stringify({
|
|
73
|
+
nodeID: nodeID,
|
|
74
|
+
localConnectionId: localConnectionId,
|
|
75
|
+
groupName: name
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
$.ajax({
|
|
79
|
+
url: "whatsapp/creategroup",
|
|
80
|
+
type: "POST",
|
|
81
|
+
data: request,
|
|
82
|
+
contentType: "application/json; charset=utf-8",
|
|
83
|
+
success: (response) => {
|
|
84
|
+
resolve(response);
|
|
85
|
+
},
|
|
86
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
87
|
+
fail(errorThrown);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
getGroups:function(nodeID, localConnectionId){
|
|
94
|
+
return new Promise((resolve, fail) => {
|
|
95
|
+
let request = JSON.stringify({
|
|
96
|
+
nodeID: nodeID,
|
|
97
|
+
localConnectionId: localConnectionId
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
$.ajax({
|
|
101
|
+
url: "whatsapp/getgroups",
|
|
102
|
+
type: "POST",
|
|
103
|
+
data: request,
|
|
104
|
+
contentType: "application/json; charset=utf-8",
|
|
105
|
+
success: (response) => {
|
|
106
|
+
resolve(response);
|
|
107
|
+
},
|
|
108
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
109
|
+
fail(errorThrown);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
addUserToGroup:function(nodeID, localConnectionId, userId, groupId, role){
|
|
116
|
+
return new Promise((resolve, fail) => {
|
|
117
|
+
let request = JSON.stringify({
|
|
118
|
+
nodeID: nodeID,
|
|
119
|
+
localConnectionId: localConnectionId,
|
|
120
|
+
userId: userId,
|
|
121
|
+
groupId: groupId,
|
|
122
|
+
role:role
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
$.ajax({
|
|
126
|
+
url: "whatsapp/groupadduser",
|
|
127
|
+
type: "POST",
|
|
128
|
+
data: request,
|
|
129
|
+
contentType: "application/json; charset=utf-8",
|
|
130
|
+
success: (response) => {
|
|
131
|
+
resolve(response);
|
|
132
|
+
},
|
|
133
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
134
|
+
fail(errorThrown);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
updateUserInGroup:function(nodeID, localConnectionId, userId, groupId, role){
|
|
141
|
+
console.log("update user in group.");
|
|
142
|
+
return new Promise((resolve, fail) => {
|
|
143
|
+
let request = JSON.stringify({
|
|
144
|
+
nodeID: nodeID,
|
|
145
|
+
localConnectionId: localConnectionId,
|
|
146
|
+
userId: userId,
|
|
147
|
+
groupId: groupId,
|
|
148
|
+
role:role
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
$.ajax({
|
|
152
|
+
url: "whatsapp/groupupdateuser",
|
|
153
|
+
type: "POST",
|
|
154
|
+
data: request,
|
|
155
|
+
contentType: "application/json; charset=utf-8",
|
|
156
|
+
success: (response) => {
|
|
157
|
+
resolve(response);
|
|
158
|
+
},
|
|
159
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
160
|
+
fail(errorThrown);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
removeUserFromGroup:function(nodeID, localConnectionId, userId, groupId){
|
|
168
|
+
console.log("removeUserFromGroup", nodeID, localConnectionId, userId, groupId);
|
|
169
|
+
return new Promise((resolve, fail) => {
|
|
170
|
+
let request = JSON.stringify({
|
|
171
|
+
nodeID: nodeID,
|
|
172
|
+
localConnectionId: localConnectionId,
|
|
173
|
+
userId: userId,
|
|
174
|
+
groupId: groupId
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
$.ajax({
|
|
178
|
+
url: "whatsapp/groupremoveuser",
|
|
179
|
+
type: "POST",
|
|
180
|
+
data: request,
|
|
181
|
+
contentType: "application/json; charset=utf-8",
|
|
182
|
+
success: (response) => {
|
|
183
|
+
resolve(response);
|
|
184
|
+
},
|
|
185
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
186
|
+
fail(errorThrown);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
getAccounts: function() {
|
|
193
|
+
return new Promise((resolve, fail) => {
|
|
194
|
+
$.ajax({
|
|
195
|
+
url: "whatsapp/accounts",
|
|
196
|
+
type: "GET",
|
|
197
|
+
success: (response) => { resolve(response); },
|
|
198
|
+
error: (jqXHR, textStatus, errorThrown) => { fail(errorThrown); }
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
</script>
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
<!-- WhatsappGroupConfigNode -->
|
|
207
|
+
|
|
208
|
+
<style>
|
|
209
|
+
|
|
210
|
+
</style>
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
<!-- WhatsappSendMessageNode -->
|
|
214
|
+
|
|
215
|
+
<style>
|
|
216
|
+
.wa-send-action-fields .form-row { margin-bottom: 3px; }
|
|
217
|
+
</style>
|
|
218
|
+
<script>
|
|
219
|
+
var WA_SEND_TYPES = {
|
|
220
|
+
text: {
|
|
221
|
+
label: "Text",
|
|
222
|
+
icon: "fa-font",
|
|
223
|
+
fields: [
|
|
224
|
+
{ key: "value", label: "Text", types: ["msg", "flow", "global", "str"] }
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
image: {
|
|
228
|
+
label: "Image",
|
|
229
|
+
icon: "fa-image",
|
|
230
|
+
fields: [
|
|
231
|
+
{ key: "value", label: "Image", types: ["msg", "flow", "global"] }
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
video: {
|
|
235
|
+
label: "Video",
|
|
236
|
+
icon: "fa-video-camera",
|
|
237
|
+
fields: [
|
|
238
|
+
{ key: "value", label: "Video", types: ["msg", "flow", "global"] }
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
document: {
|
|
242
|
+
label: "Document",
|
|
243
|
+
icon: "fa-file",
|
|
244
|
+
fields: [
|
|
245
|
+
{ key: "value", label: "File", types: ["msg", "flow", "global"] },
|
|
246
|
+
{ key: "documentName", label: "Name", types: ["msg", "flow", "global", "str"] },
|
|
247
|
+
{ key: "documentMimetype", label: "Mime Type", types: ["msg", "flow", "global", "str"] }
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
</script>
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
<!-- logger -->
|
|
255
|
+
|
|
12
256
|
<style>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
257
|
+
.editorgroupborder {
|
|
258
|
+
border-width:1px;
|
|
259
|
+
border-style:solid;
|
|
260
|
+
border-color:lightgray;
|
|
261
|
+
padding: 3px;
|
|
262
|
+
margin-top: 2px;
|
|
263
|
+
margin-bottom: 2px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.editorsectionheading {
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
margin-bottom:1px !important;
|
|
269
|
+
margin-top:6px !important;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.nomargin {
|
|
273
|
+
margin-bottom:1px !important;
|
|
274
|
+
margin-top:1px !important;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.slidecontainer {
|
|
278
|
+
width: 100%;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.slider {
|
|
282
|
+
-webkit-appearance: none;
|
|
283
|
+
appearance: none;
|
|
284
|
+
width: 100%;
|
|
285
|
+
height: 25px;
|
|
286
|
+
background: #d3d3d3;
|
|
287
|
+
outline: none;
|
|
288
|
+
opacity: 0.7;
|
|
289
|
+
-webkit-transition: .2s;
|
|
290
|
+
transition: opacity .2s;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.slider:hover {
|
|
294
|
+
opacity: 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.slider::-webkit-slider-thumb {
|
|
298
|
+
-webkit-appearance: none;
|
|
299
|
+
appearance: none;
|
|
300
|
+
width: 25px;
|
|
301
|
+
height: 25px;
|
|
302
|
+
background: #04AA6D;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.slider::-moz-range-thumb {
|
|
307
|
+
width: 25px;
|
|
308
|
+
height: 25px;
|
|
309
|
+
background: #04AA6D;
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.towb_editorlabel {
|
|
314
|
+
width: 120px !important;
|
|
315
|
+
}
|
|
316
|
+
input.towb_editorfield, select.towb_editorfield, textarea.towb_editorfield {
|
|
317
|
+
width: 70% !important;
|
|
318
|
+
}
|
|
319
|
+
input.towb_editorfield_short, select.towb_editorfield_short {
|
|
320
|
+
width: 30% !important;
|
|
16
321
|
}
|
|
17
322
|
|
|
18
323
|
</style>
|
|
324
|
+
|
|
19
325
|
<script type="text/javascript">
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
326
|
+
|
|
327
|
+
// CSS
|
|
328
|
+
dropdownStyles = `
|
|
329
|
+
.loggertype-dropdown {
|
|
330
|
+
position: absolute;
|
|
331
|
+
background: white;
|
|
332
|
+
border: 1px solid #ccc;
|
|
333
|
+
border-radius: 4px;
|
|
334
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
335
|
+
max-height: 150px;
|
|
336
|
+
overflow-y: auto;
|
|
337
|
+
z-index: 1000;
|
|
338
|
+
min-width: 150px;
|
|
339
|
+
}
|
|
340
|
+
.loggertype-dropdown .dropdown-item {
|
|
341
|
+
padding: 6px 12px;
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
}
|
|
344
|
+
.loggertype-dropdown .dropdown-item:hover {
|
|
345
|
+
background: #f0f0f0;
|
|
346
|
+
}
|
|
347
|
+
`;
|
|
348
|
+
$('<style>').text(dropdownStyles).appendTo('head');
|
|
349
|
+
|
|
350
|
+
</script>
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
<!-- WhatsappDynamicSendMessageNode -->
|
|
356
|
+
|
|
357
|
+
<style>
|
|
358
|
+
.wa-dynamic-send-action-fields .form-row { margin-bottom: 3px; }
|
|
359
|
+
</style>
|
|
360
|
+
<script>
|
|
361
|
+
// WA_SEND_TYPES is defined by WhatsappSendMessageNode.html (included in the same page).
|
|
362
|
+
// Guard against load-order variation with a var declaration — var re-declarations are safe.
|
|
363
|
+
if (typeof WA_SEND_TYPES === "undefined") {
|
|
364
|
+
var WA_SEND_TYPES = {
|
|
365
|
+
text: {
|
|
366
|
+
label: "Text",
|
|
367
|
+
icon: "fa-font",
|
|
368
|
+
fields: [
|
|
369
|
+
{ key: "value", label: "Text", types: ["msg", "flow", "global", "str"] }
|
|
370
|
+
]
|
|
120
371
|
},
|
|
121
|
-
|
|
122
|
-
|
|
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);
|
|
372
|
+
image: {
|
|
373
|
+
label: "Image",
|
|
374
|
+
icon: "fa-image",
|
|
375
|
+
fields: [
|
|
376
|
+
{ key: "value", label: "Image", types: ["msg", "flow", "global"] }
|
|
377
|
+
]
|
|
144
378
|
},
|
|
145
|
-
|
|
146
|
-
|
|
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);
|
|
379
|
+
video: {
|
|
380
|
+
label: "Video",
|
|
381
|
+
icon: "fa-video-camera",
|
|
382
|
+
fields: [
|
|
383
|
+
{ key: "value", label: "Video", types: ["msg", "flow", "global"] }
|
|
384
|
+
]
|
|
167
385
|
},
|
|
168
|
-
|
|
169
|
-
|
|
386
|
+
document: {
|
|
387
|
+
label: "Document",
|
|
388
|
+
icon: "fa-file",
|
|
389
|
+
fields: [
|
|
390
|
+
{ key: "value", label: "File", types: ["msg", "flow", "global"] },
|
|
391
|
+
{ key: "documentName", label: "Name", types: ["msg", "flow", "global", "str"] },
|
|
392
|
+
{ key: "documentMimetype", label: "Mime Type", types: ["msg", "flow", "global", "str"] }
|
|
393
|
+
]
|
|
170
394
|
}
|
|
171
|
-
|
|
172
|
-
});
|
|
173
|
-
},
|
|
395
|
+
};
|
|
174
396
|
}
|
|
397
|
+
</script>
|
|
175
398
|
|
|
176
|
-
|
|
399
|
+
|
|
400
|
+
<!-- WhatsappReceiveMessageNode -->
|
|
401
|
+
|
|
402
|
+
<style>
|
|
403
|
+
</style>
|
|
404
|
+
<script>
|
|
405
|
+
const WA_ACCEPT_FIELDS = [
|
|
406
|
+
{ id: "Text", icon: "fa-font", label: "Text", defaultEnabled: true },
|
|
407
|
+
{ id: "ExtendedText", icon: "fa-align-left", label: "Extended Text", defaultEnabled: true },
|
|
408
|
+
{ id: "Image", icon: "fa-image", label: "Image", defaultEnabled: true },
|
|
409
|
+
{ id: "Video", icon: "fa-video-camera", label: "Video", defaultEnabled: true },
|
|
410
|
+
{ id: "Album", icon: "fa-th-large", label: "Album", defaultEnabled: true },
|
|
411
|
+
{ id: "Document", icon: "fa-file", label: "Document", defaultEnabled: false },
|
|
412
|
+
{ id: "Contact", icon: "fa-address-card", label: "Contact", defaultEnabled: false },
|
|
413
|
+
{ id: "Template", icon: "fa-th-list", label: "Template", defaultEnabled: false },
|
|
414
|
+
{ id: "Location", icon: "fa-map-marker", label: "Location", defaultEnabled: false },
|
|
415
|
+
{ id: "Event", icon: "fa-calendar", label: "Event", defaultEnabled: false },
|
|
416
|
+
{ id: "EventResponse", icon: "fa-calendar-check-o", label: "Event Response", defaultEnabled: false },
|
|
417
|
+
{ id: "Sticker", icon: "fa-sticky-note-o", label: "Sticker", defaultEnabled: false },
|
|
418
|
+
];
|
|
419
|
+
</script>
|
|
420
|
+
|
|
421
|
+
|
|
177
422
|
<script type="text/javascript">
|
|
178
423
|
RED.nodes.registerType('WhatsappAccountConfigNode', {
|
|
179
424
|
category: 'config',
|
|
@@ -199,8 +444,9 @@
|
|
|
199
444
|
{
|
|
200
445
|
let node = this;
|
|
201
446
|
let $btn = $("#wa-account-link-btn");
|
|
447
|
+
let isLinked = document.getElementById("node-config-input-accountLinked").checked;
|
|
202
448
|
// if the account is already linked, show the Unlink state
|
|
203
|
-
if (
|
|
449
|
+
if (isLinked) {
|
|
204
450
|
$btn.html('<i class="fa fa-unlink"></i> Unlink Account');
|
|
205
451
|
}
|
|
206
452
|
// if we don't have a local connectionId for this node, generate one.
|
|
@@ -208,10 +454,44 @@
|
|
|
208
454
|
$("#node-config-input-localConnectionId").val(crypto.randomUUID());
|
|
209
455
|
}
|
|
210
456
|
let localConnectionId = $("#node-config-input-localConnectionId").val();
|
|
211
|
-
|
|
457
|
+
// --- Claim Account section ---
|
|
458
|
+
// Only offer claiming if this node is not already linked.
|
|
459
|
+
if (!isLinked) {
|
|
460
|
+
waService.getAccounts().then(function(accounts) {
|
|
461
|
+
let unclaimed = accounts.filter(function(a) {
|
|
462
|
+
return !a.claimed;
|
|
463
|
+
});
|
|
464
|
+
if (unclaimed.length === 0) return;
|
|
465
|
+
let $claimSelect = $("#wa-claim-select");
|
|
466
|
+
$claimSelect.empty();
|
|
467
|
+
unclaimed.forEach(function(account) {
|
|
468
|
+
let label = account.name && account.phoneNumber ? account.name + " (+" + account.phoneNumber + ")" : account.name || account.phoneNumber || account.key;
|
|
469
|
+
if (!account.connected) label += " \u26a0 disconnected";
|
|
470
|
+
$('<option>').val(account.key).text(label).appendTo($claimSelect);
|
|
471
|
+
});
|
|
472
|
+
$("#wa-claim-section").show();
|
|
473
|
+
$("#wa-or-separator").show();
|
|
474
|
+
}).catch(function() {
|
|
475
|
+
/* backend not ready — ignore */ });
|
|
476
|
+
}
|
|
477
|
+
$("#wa-claim-btn").on("click", function() {
|
|
478
|
+
let claimedKey = $("#wa-claim-select").val();
|
|
479
|
+
if (!claimedKey) return;
|
|
480
|
+
// adopt the claimed account's localConnectionId and mark as linked
|
|
481
|
+
$("#node-config-input-localConnectionId").val(claimedKey);
|
|
482
|
+
$("#node-config-input-accountLinked").prop("checked", true);
|
|
483
|
+
$("#wa-claim-section").hide();
|
|
484
|
+
$("#wa-or-separator").hide();
|
|
485
|
+
$btn.html('<i class="fa fa-unlink"></i> Unlink Account');
|
|
486
|
+
$("#wa-account-qrcode").html("<p>\u2714 Account claimed. Deploy to activate.</p>");
|
|
487
|
+
});
|
|
488
|
+
// --- Link Account button ---
|
|
489
|
+
$btn.on("click", function() {
|
|
212
490
|
$btn.prop("disabled", true);
|
|
491
|
+
// re-read localConnectionId in case it was overridden by a claim
|
|
492
|
+
localConnectionId = $("#node-config-input-localConnectionId").val();
|
|
213
493
|
if (document.getElementById("node-config-input-accountLinked").checked) {
|
|
214
|
-
waService.unlinkAccount(node.id, localConnectionId).then(response
|
|
494
|
+
waService.unlinkAccount(node.id, localConnectionId).then(function(response) {
|
|
215
495
|
$btn.prop("disabled", false);
|
|
216
496
|
$btn.html('<i class="fa fa-link"></i> Link Account');
|
|
217
497
|
document.getElementById("node-config-input-accountLinked").checked = false;
|
|
@@ -283,6 +563,21 @@
|
|
|
283
563
|
<input type="checkbox" id="node-config-input-accountLinked">
|
|
284
564
|
</div>
|
|
285
565
|
|
|
566
|
+
<div class="form-row" id="wa-claim-section" style="display:none">
|
|
567
|
+
<label><i class="fa fa-mobile"></i> Claim</label>
|
|
568
|
+
<div style="width:70%; display:inline-flex; align-items:center;">
|
|
569
|
+
<select id="wa-claim-select" style="flex-grow:1; min-width:0;"></select>
|
|
570
|
+
<button type="button" id="wa-claim-btn" class="red-ui-button" style="margin-left:8px; flex-shrink:0;">
|
|
571
|
+
<i class="fa fa-check"></i> Claim
|
|
572
|
+
</button>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<div class="form-row" id="wa-or-separator" style="display:none">
|
|
577
|
+
<label></label>
|
|
578
|
+
<span style="color:#aaa; font-style:italic;">— or link a new account —</span>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
286
581
|
<div class="form-row">
|
|
287
582
|
<label></label>
|
|
288
583
|
<button type="button" id="wa-account-link-btn" class="red-ui-button">
|
|
@@ -318,9 +613,7 @@ To unlink the device, open the editor and click **Unlink Account**, then remove
|
|
|
318
613
|
|
|
319
614
|
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
615
|
</script>
|
|
321
|
-
|
|
322
|
-
<style>
|
|
323
|
-
</style>
|
|
616
|
+
|
|
324
617
|
<script type="text/javascript">
|
|
325
618
|
RED.nodes.registerType('WhatsappGroupConfigNode', {
|
|
326
619
|
category: 'config',
|
|
@@ -591,8 +884,9 @@ Credentials are persisted to `/data/whatsapp/<localConnectionId>/` if the `/data
|
|
|
591
884
|
options: []
|
|
592
885
|
}]
|
|
593
886
|
});
|
|
594
|
-
// set initial button state
|
|
887
|
+
// set initial button state and load groups for the already-selected account
|
|
595
888
|
setCreateButtonEnabled(accountConfigInput.val());
|
|
889
|
+
updateGroupList(accountConfigInput.val());
|
|
596
890
|
// show inline create section
|
|
597
891
|
$("#wa-group-new-btn").on("click", () => {
|
|
598
892
|
$("#wa-group-new-row").hide();
|
|
@@ -705,8 +999,6 @@ Credentials are persisted to `/data/whatsapp/<localConnectionId>/` if the `/data
|
|
|
705
999
|
</div>
|
|
706
1000
|
</div>
|
|
707
1001
|
|
|
708
|
-
|
|
709
|
-
|
|
710
1002
|
</div>
|
|
711
1003
|
</script>
|
|
712
1004
|
|
|
@@ -725,119 +1017,7 @@ References a WhatsApp group within a linked account. Used as the target for Send
|
|
|
725
1017
|
|
|
726
1018
|
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
1019
|
</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
1020
|
|
|
840
|
-
</script>
|
|
841
1021
|
<script type="text/javascript">
|
|
842
1022
|
RED.nodes.registerType('WhatsappSendMessageNode', {
|
|
843
1023
|
category: 'whatsapp',
|
|
@@ -865,10 +1045,8 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
|
|
|
865
1045
|
required: false,
|
|
866
1046
|
validate: function(value) {
|
|
867
1047
|
try {
|
|
868
|
-
|
|
869
|
-
return
|
|
870
|
-
return !p.enabled || (p.value !== undefined && p.value !== "");
|
|
871
|
-
});
|
|
1048
|
+
JSON.parse(value || "[]");
|
|
1049
|
+
return true;
|
|
872
1050
|
} catch (e) {
|
|
873
1051
|
return false;
|
|
874
1052
|
}
|
|
@@ -894,59 +1072,91 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
|
|
|
894
1072
|
required: true
|
|
895
1073
|
},
|
|
896
1074
|
metricsReference: {
|
|
897
|
-
|
|
898
|
-
|
|
1075
|
+
required: false,
|
|
1076
|
+
value: '',
|
|
1077
|
+
type: 'DelegatedConfigReferenceNode'
|
|
899
1078
|
},
|
|
900
1079
|
},
|
|
901
1080
|
oneditprepare: function() {
|
|
902
1081
|
// **** WhatsappSendMessageNode **** //
|
|
903
1082
|
{
|
|
904
1083
|
let node = this;
|
|
905
|
-
let
|
|
1084
|
+
let actionList = $("#wa-send-actions-list");
|
|
1085
|
+
actionList.editableList({
|
|
1086
|
+
sortable: true,
|
|
1087
|
+
removable: true,
|
|
1088
|
+
height: "auto",
|
|
1089
|
+
scrollOnAdd: true,
|
|
1090
|
+
addButton: "Add Message",
|
|
1091
|
+
addItem: function(container, i, action) {
|
|
1092
|
+
action = action || {};
|
|
1093
|
+
let type = action.type || "text";
|
|
1094
|
+
container.css({
|
|
1095
|
+
padding: "6px 4px"
|
|
1096
|
+
});
|
|
1097
|
+
// ── Type row ──────────────────────────────────────────────────────
|
|
1098
|
+
let typeRow = $('<div style="display:flex; align-items:center; gap:8px; margin-bottom:5px;">');
|
|
1099
|
+
let iconEl = $('<i style="width:14px; text-align:center; flex-shrink:0;">').addClass("fa " + (WA_SEND_TYPES[type] ? WA_SEND_TYPES[type].icon : "fa-comment"));
|
|
1100
|
+
let typeSelect = $('<select class="wa-send-action-type" style="flex:0 0 auto;">');
|
|
1101
|
+
Object.keys(WA_SEND_TYPES).forEach(function(key) {
|
|
1102
|
+
typeSelect.append($("<option>").val(key).text(WA_SEND_TYPES[key].label));
|
|
1103
|
+
});
|
|
1104
|
+
typeSelect.val(type);
|
|
1105
|
+
typeRow.append(iconEl).append(typeSelect);
|
|
1106
|
+
container.append(typeRow);
|
|
1107
|
+
// ── Fields container ──────────────────────────────────────────────
|
|
1108
|
+
let fieldsContainer = $('<div class="wa-send-action-fields">');
|
|
1109
|
+
container.append(fieldsContainer);
|
|
1110
|
+
let renderFields = function(selectedType) {
|
|
1111
|
+
fieldsContainer.empty();
|
|
1112
|
+
let typeDef = WA_SEND_TYPES[selectedType];
|
|
1113
|
+
if (!typeDef) return;
|
|
1114
|
+
iconEl.removeClass().addClass("fa " + typeDef.icon);
|
|
1115
|
+
typeDef.fields.forEach(function(field) {
|
|
1116
|
+
let fieldRow = $('<div class="form-row nomargin" style="display:flex; align-items:center; gap:6px;">');
|
|
1117
|
+
fieldRow.append($('<label style="width:70px; margin:0; font-size:0.85em; flex-shrink:0;">').text(field.label));
|
|
1118
|
+
let input = $('<input type="text">').addClass("wa-send-field-" + field.key);
|
|
1119
|
+
fieldRow.append(input);
|
|
1120
|
+
fieldsContainer.append(fieldRow);
|
|
1121
|
+
input.typedInput({
|
|
1122
|
+
types: field.types
|
|
1123
|
+
}).typedInput("type", action[field.key + "Type"] || field.types[0]).typedInput("value", action[field.key] || "");
|
|
1124
|
+
fieldRow.find(".red-ui-typedInput-container").css({
|
|
1125
|
+
flex: "1",
|
|
1126
|
+
"min-width": "0",
|
|
1127
|
+
width: ""
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
1131
|
+
renderFields(type);
|
|
1132
|
+
typeSelect.on("change", function() {
|
|
1133
|
+
// Reset saved values when the type changes so stale data isn't carried over.
|
|
1134
|
+
action = {
|
|
1135
|
+
type: $(this).val()
|
|
1136
|
+
};
|
|
1137
|
+
renderFields($(this).val());
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
let actions = [];
|
|
906
1142
|
try {
|
|
907
|
-
|
|
1143
|
+
actions = JSON.parse(node.payloads || "[]");
|
|
908
1144
|
} catch (e) {}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
});
|
|
1145
|
+
if (actions.length > 0) {
|
|
1146
|
+
actionList.editableList("addItems", actions);
|
|
1147
|
+
}
|
|
941
1148
|
}
|
|
942
1149
|
// **** logger **** //
|
|
943
1150
|
{
|
|
944
1151
|
let node = this;
|
|
1152
|
+
let templateConfig = {};
|
|
945
1153
|
// controls.
|
|
946
1154
|
let logEnabled = $("#node-input-logEnabled");
|
|
947
1155
|
let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
|
|
948
1156
|
let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
|
|
949
1157
|
let logEnabledSection = $("#section_logEnabled");
|
|
1158
|
+
let loggerSection = $("#section_loggerconfig");
|
|
1159
|
+
loggerSection.hide();
|
|
950
1160
|
// functions.
|
|
951
1161
|
let getLoggerTypes = async function() {
|
|
952
1162
|
let types = [];
|
|
@@ -1070,43 +1280,756 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
|
|
|
1070
1280
|
loggerEditButton.removeClass("disabled")
|
|
1071
1281
|
}
|
|
1072
1282
|
});
|
|
1073
|
-
//
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1283
|
+
// Show the logger section only if at least one logger type is registered.
|
|
1284
|
+
// If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
|
|
1285
|
+
getLoggerTypes().then(loggerTypes => {
|
|
1286
|
+
if (loggerTypes.length === 0) return;
|
|
1287
|
+
loggerSection.show();
|
|
1288
|
+
updateLoggerSelectorList().then(() => {
|
|
1289
|
+
loggerSelector.val(node.logger);
|
|
1290
|
+
loggerSelector.trigger('change');
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
// **** metrics **** //
|
|
1295
|
+
{
|
|
1296
|
+
let node = this;
|
|
1297
|
+
let templateConfig = {};
|
|
1298
|
+
let metricsEnabled = $("#node-input-metricsEnabled");
|
|
1299
|
+
let metricsSection = $("#section_metricsconfig");
|
|
1300
|
+
let metricsContainer = metricsEnabled.closest(".editorgroupborder");
|
|
1301
|
+
let metricsEnabledSection = metricsContainer.find("#section_metricsEnabled");
|
|
1302
|
+
metricsSection.hide();
|
|
1303
|
+
let getMetricsProviderTypes = async function() {
|
|
1304
|
+
let types = [];
|
|
1305
|
+
await $.ajax({
|
|
1306
|
+
url: "nodetypeservice/find?tag=MetricsProvider",
|
|
1307
|
+
type: "GET",
|
|
1308
|
+
contentType: "application/json; charset=utf-8",
|
|
1309
|
+
success: (response) => {
|
|
1310
|
+
types = response;
|
|
1311
|
+
},
|
|
1312
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
1313
|
+
console.log(jqXHR, textStatus, errorThrown);
|
|
1314
|
+
},
|
|
1315
|
+
});
|
|
1316
|
+
return types;
|
|
1317
|
+
};
|
|
1318
|
+
let getMetricsProviders = async () => {
|
|
1319
|
+
let providers = [];
|
|
1320
|
+
let providerTypes = (await getMetricsProviderTypes()).map(t => t.id);
|
|
1321
|
+
RED.nodes.eachConfig(cfg => {
|
|
1322
|
+
if (providerTypes.includes(cfg.type)) {
|
|
1323
|
+
providers.push({
|
|
1324
|
+
id: cfg.id,
|
|
1325
|
+
name: cfg.label(),
|
|
1326
|
+
type: cfg.type
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
return providers;
|
|
1331
|
+
};
|
|
1332
|
+
let updateMetricsSelectorList = async () => {
|
|
1333
|
+
metricsSelector.empty();
|
|
1334
|
+
(await getMetricsProviders()).forEach(provider => {
|
|
1335
|
+
$('<option value="' + provider.id + '">' + provider.name + '</option>').appendTo(metricsSelector);
|
|
1336
|
+
});
|
|
1337
|
+
$('<option value="_ADD_">none</option>').appendTo(metricsSelector);
|
|
1338
|
+
};
|
|
1339
|
+
metricsEnabled.on('change', function() {
|
|
1340
|
+
if (metricsEnabled.prop("checked")) {
|
|
1341
|
+
metricsEnabledSection.show();
|
|
1342
|
+
} else {
|
|
1343
|
+
metricsEnabledSection.hide();
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
let metricsSelector = metricsContainer.find("#metrics-selector");
|
|
1347
|
+
let metricsEditButton = metricsContainer.find("#metrics-edit-btn");
|
|
1348
|
+
let metricsAddButton = metricsContainer.find("#metrics-add-btn");
|
|
1349
|
+
metricsEditButton.on('click', async function(event) {
|
|
1350
|
+
event.stopPropagation();
|
|
1351
|
+
if (metricsEditButton.hasClass("disabled")) return;
|
|
1352
|
+
let selectedProvider = (await getMetricsProviders()).find(p => p.id === metricsSelector.val());
|
|
1353
|
+
if (selectedProvider) {
|
|
1354
|
+
RED.editor.editConfig("#metrics-selector", selectedProvider.type, selectedProvider.id);
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
metricsAddButton.on('click', function(event) {
|
|
1358
|
+
event.stopPropagation();
|
|
1359
|
+
let $btn = $(event.currentTarget);
|
|
1360
|
+
let btnOffset = $btn.offset();
|
|
1361
|
+
let btnH = $btn.outerHeight();
|
|
1362
|
+
let btnW = $btn.outerWidth();
|
|
1363
|
+
$('.metricstype-dropdown').remove();
|
|
1364
|
+
getMetricsProviderTypes().then(providerTypes => {
|
|
1365
|
+
let $dropdown = $('<div class="metricstype-dropdown red-ui-editor"></div>').css({
|
|
1366
|
+
position: 'absolute',
|
|
1367
|
+
zIndex: 1000
|
|
1368
|
+
});
|
|
1369
|
+
providerTypes.forEach(providerType => {
|
|
1370
|
+
$('<div class="dropdown-item"></div>').text(providerType.name).data('value', providerType.id).css({
|
|
1371
|
+
padding: '4px 8px',
|
|
1372
|
+
cursor: 'pointer'
|
|
1373
|
+
}).appendTo($dropdown);
|
|
1374
|
+
});
|
|
1375
|
+
$('body').append($dropdown);
|
|
1376
|
+
$dropdown.css({
|
|
1377
|
+
top: btnOffset.top + btnH,
|
|
1378
|
+
left: Math.max(0, btnOffset.left + btnW - $dropdown.outerWidth())
|
|
1379
|
+
});
|
|
1380
|
+
$(document).on('mousedown.metricsdropdown', function(e) {
|
|
1381
|
+
if (!$(e.target).closest('.metricstype-dropdown').length) {
|
|
1382
|
+
$dropdown.remove();
|
|
1383
|
+
$(document).off('mousedown.metricsdropdown');
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
$dropdown.on('click', '.dropdown-item', function(e) {
|
|
1387
|
+
e.stopPropagation();
|
|
1388
|
+
const selectedValue = $(this).data('value');
|
|
1389
|
+
$dropdown.remove();
|
|
1390
|
+
RED.editor.editConfig("#metrics-selector", selectedValue, "_ADD_");
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
});
|
|
1394
|
+
metricsSelector.on("focus", updateMetricsSelectorList);
|
|
1395
|
+
metricsSelector.on('change', function() {
|
|
1396
|
+
if ("_ADD_" === metricsSelector.val()) {
|
|
1397
|
+
metricsEditButton.addClass("disabled");
|
|
1398
|
+
} else {
|
|
1399
|
+
metricsEditButton.removeClass("disabled");
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
// Show the metrics section only if at least one metrics provider type is registered.
|
|
1403
|
+
// If nodered_prometheus (or any other provider) is not installed, the section stays hidden.
|
|
1404
|
+
getMetricsProviderTypes().then(providerTypes => {
|
|
1405
|
+
if (providerTypes.length === 0) return;
|
|
1406
|
+
metricsSection.show();
|
|
1407
|
+
updateMetricsSelectorList().then(() => {
|
|
1408
|
+
metricsSelector.val(node.metricsReference);
|
|
1409
|
+
metricsSelector.trigger('change');
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
oneditsave: function() {
|
|
1415
|
+
// **** WhatsappSendMessageNode **** //
|
|
1416
|
+
{
|
|
1417
|
+
let actions = [];
|
|
1418
|
+
$("#wa-send-actions-list").editableList("items").each(function() {
|
|
1419
|
+
let container = $(this);
|
|
1420
|
+
let type = container.find(".wa-send-action-type").val();
|
|
1421
|
+
let typeDef = WA_SEND_TYPES[type];
|
|
1422
|
+
if (!typeDef) return;
|
|
1423
|
+
let action = {
|
|
1424
|
+
type: type
|
|
1425
|
+
};
|
|
1426
|
+
typeDef.fields.forEach(function(field) {
|
|
1427
|
+
let input = container.find(".wa-send-field-" + field.key);
|
|
1428
|
+
if (input.length) {
|
|
1429
|
+
action[field.key] = input.typedInput("value");
|
|
1430
|
+
action[field.key + "Type"] = input.typedInput("type");
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
actions.push(action);
|
|
1434
|
+
});
|
|
1435
|
+
$("#node-input-payloads").val(JSON.stringify(actions));
|
|
1436
|
+
}
|
|
1437
|
+
// **** logger **** //
|
|
1438
|
+
{
|
|
1439
|
+
let node = this;
|
|
1440
|
+
let templateConfig = {};
|
|
1441
|
+
node = this;
|
|
1442
|
+
var selectedLogger = $("#logger-selector").val();
|
|
1443
|
+
node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
|
|
1444
|
+
node.logTemplateOverride = node.logTemplateOverrideEditor.getValue();
|
|
1445
|
+
node.logTemplateOverrideEditor.destroy();
|
|
1446
|
+
delete node.logTemplateOverrideEditor;
|
|
1447
|
+
}
|
|
1448
|
+
// **** metrics **** //
|
|
1449
|
+
{
|
|
1450
|
+
let node = this;
|
|
1451
|
+
let templateConfig = {};
|
|
1452
|
+
node = this;
|
|
1453
|
+
var selectedProvider = $("#metrics-selector").val();
|
|
1454
|
+
node.metricsReference = (selectedProvider && selectedProvider !== "_ADD_") ? selectedProvider : '';
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
oneditcancel: function() {},
|
|
1458
|
+
oneditdelete: function() {},
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
</script>
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
<script type="text/html" data-template-name='WhatsappSendMessageNode'>
|
|
1465
|
+
<div id='section_WhatsappSendMessageNode'>
|
|
1466
|
+
<div class="form-row">
|
|
1467
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
1468
|
+
<input type="text" id="node-input-name" />
|
|
1469
|
+
</div>
|
|
1470
|
+
|
|
1471
|
+
<div class="form-row nomargin">
|
|
1472
|
+
<label for="node-input-groupConfig" style="width:120px;"><i class="fa fa-users"></i> Group</label>
|
|
1473
|
+
<input type="text" id="node-input-groupConfig" placeholder="config">
|
|
1474
|
+
</div>
|
|
1475
|
+
|
|
1476
|
+
<div class="form-row editorsectionheading">
|
|
1477
|
+
<i class="w-16 fa fa-paper-plane"></i> <span>Messages</span>
|
|
1478
|
+
</div>
|
|
1479
|
+
|
|
1480
|
+
<div class="editorgroupborder">
|
|
1481
|
+
<ol id="wa-send-actions-list"></ol>
|
|
1482
|
+
</div>
|
|
1483
|
+
|
|
1484
|
+
<input type="hidden" id="node-input-payloads">
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
</div>
|
|
1488
|
+
|
|
1489
|
+
<div id='section_logger'>
|
|
1490
|
+
<div id="section_loggerconfig">
|
|
1491
|
+
<div class="form-row editorsectionheading">
|
|
1492
|
+
<i class="w-16 fa fa-eye"></i> <span>Logging</span>
|
|
1493
|
+
</div>
|
|
1494
|
+
<div class="editorgroupborder">
|
|
1495
|
+
<div class="form-row nomargin logEnableCheck">
|
|
1496
|
+
<input type="checkbox" id="node-input-logEnabled" placeholder="logEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;">
|
|
1497
|
+
<label style="width:auto" for="node-input-logEnabled"><span>Enable Logging</span></label>
|
|
1498
|
+
</div>
|
|
1499
|
+
|
|
1500
|
+
<div id="section_logEnabled">
|
|
1501
|
+
<div class="form-row nomargin">
|
|
1502
|
+
<label for="logger-selector">Logger</label>
|
|
1503
|
+
<div style="width: 70%; display: inline-flex;">
|
|
1504
|
+
<select id="logger-selector" style="flex-grow: 1;" class="">
|
|
1505
|
+
<option value="_ADD_">none</option>
|
|
1506
|
+
</select>
|
|
1507
|
+
<a id="logger-edit-btn" class="red-ui-button disabled" style="margin-left: 10px;">
|
|
1508
|
+
<i class="fa fa-pencil"></i>
|
|
1509
|
+
</a>
|
|
1510
|
+
<a id="logger-add-btn" class="red-ui-button" style="margin-left: 10px;">
|
|
1511
|
+
<i class="fa fa-plus"></i>
|
|
1512
|
+
</a>
|
|
1513
|
+
</div>
|
|
1514
|
+
</div>
|
|
1515
|
+
|
|
1516
|
+
<div class="form-row nomargin">
|
|
1517
|
+
<input type="checkbox" id="node-input-logTemplateOverrideEnabled" placeholder="" value="false" style="margin:8px 0 10px 102px; width:20px;">
|
|
1518
|
+
<label style="width:auto" for="node-input-logTemplateOverrideEnabled"><span>Override Template</span></label>
|
|
1519
|
+
</div>
|
|
1520
|
+
|
|
1521
|
+
<div id="section_logTemplateOverrideEnabled">
|
|
1522
|
+
<div class="form-row">
|
|
1523
|
+
<label class="towb_editorlabel" for="node-input-logTemplateOverrideEditor"><i class="fa fa-code"></i> Template</label>
|
|
1524
|
+
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-logTemplateOverrideEditor"></div>
|
|
1525
|
+
</div>
|
|
1526
|
+
</div>
|
|
1527
|
+
</div>
|
|
1528
|
+
</div>
|
|
1529
|
+
</div>
|
|
1530
|
+
|
|
1531
|
+
</div>
|
|
1532
|
+
|
|
1533
|
+
<div id='section_metrics'>
|
|
1534
|
+
<div id="section_metricsconfig">
|
|
1535
|
+
<div class="form-row editorsectionheading">
|
|
1536
|
+
<i class="w-16 fa fa-eye"></i> <span>Metrics</span>
|
|
1537
|
+
</div>
|
|
1538
|
+
<div class="editorgroupborder">
|
|
1539
|
+
<div class="form-row nomargin metricsEnableCheck">
|
|
1540
|
+
<input type="checkbox" id="node-input-metricsEnabled" value="false" style="margin:8px 0 10px 102px; width:20px;" />
|
|
1541
|
+
<label style="width:auto" for="node-input-metricsEnabled"><span>Enable Metrics</span></label>
|
|
1542
|
+
</div>
|
|
1543
|
+
|
|
1544
|
+
<div id="section_metricsEnabled">
|
|
1545
|
+
<div class="form-row nomargin">
|
|
1546
|
+
<label class="towb_editorlabel" for="metrics-selector">Metric Provider</label>
|
|
1547
|
+
<div style="width: 70%; display: inline-flex;">
|
|
1548
|
+
<select id="metrics-selector" style="flex-grow: 1; min-width: 0;">
|
|
1549
|
+
<option value="_ADD_">none</option>
|
|
1550
|
+
</select>
|
|
1551
|
+
<a id="metrics-edit-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;">
|
|
1552
|
+
<i class="fa fa-pencil"></i>
|
|
1553
|
+
</a>
|
|
1554
|
+
<a id="metrics-add-btn" class="red-ui-button" style="margin-left: 10px; flex-shrink: 0;">
|
|
1555
|
+
<i class="fa fa-plus"></i>
|
|
1556
|
+
</a>
|
|
1557
|
+
</div>
|
|
1558
|
+
</div>
|
|
1559
|
+
</div>
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
|
|
1563
|
+
</div>
|
|
1564
|
+
</script>
|
|
1565
|
+
|
|
1566
|
+
<script type="text/markdown" data-help-name='WhatsappSendMessageNode'>
|
|
1567
|
+
> **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
|
|
1568
|
+
|
|
1569
|
+
Sends one or more messages to a WhatsApp group when triggered.
|
|
1570
|
+
|
|
1571
|
+
### Properties
|
|
1572
|
+
|
|
1573
|
+
: *name* (string) : Display label for this node.
|
|
1574
|
+
: *group* (config) : The WhatsApp Group config node to send to.
|
|
1575
|
+
|
|
1576
|
+
### Messages
|
|
1577
|
+
|
|
1578
|
+
Each row in the **Messages** list is sent as a separate WhatsApp message, in order, when the node is triggered. Rows can be reordered by dragging and removed with the delete button.
|
|
1579
|
+
|
|
1580
|
+
Select the message type from the dropdown, then fill in the value fields:
|
|
1581
|
+
|
|
1582
|
+
- **Text** — a single text value (`msg`, `flow`, `global`, `str` with Handlebars support for `str`)
|
|
1583
|
+
- **Image** — a Buffer containing the image data (`msg`, `flow`, `global`)
|
|
1584
|
+
- **Video** — a Buffer containing the video data (`msg`, `flow`, `global`)
|
|
1585
|
+
- **Document** — three fields: the file Buffer, an optional filename, and an optional MIME type (defaults to `application/octet-stream`)
|
|
1586
|
+
|
|
1587
|
+
### Inputs
|
|
1588
|
+
|
|
1589
|
+
: *msg* : Trigger message. `msg`-type field values are resolved against this message.
|
|
1590
|
+
|
|
1591
|
+
### Logging
|
|
1592
|
+
: *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
|
|
1593
|
+
: *logger* (logconfig) : the endppoint that the node will log events to.
|
|
1594
|
+
: *override template* (logconfig) : if checked, a custom logging template can be provided for the log message.
|
|
1595
|
+
: *logger template* (mustache) : a template in mustache format to generate a message for logging purposes (see LoggerConfig node for more information)
|
|
1596
|
+
|
|
1597
|
+
### Metrics
|
|
1598
|
+
: *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
|
|
1599
|
+
: *metric provider* (metricsconfig) : the metrics backend that this node will report to.
|
|
1600
|
+
</script>
|
|
1601
|
+
|
|
1602
|
+
<script type="text/javascript">
|
|
1603
|
+
RED.nodes.registerType('WhatsappDynamicSendMessageNode', {
|
|
1604
|
+
category: 'whatsapp',
|
|
1605
|
+
icon: 'whatsapp.png',
|
|
1606
|
+
color: '#E6FFDA',
|
|
1607
|
+
label: function() {
|
|
1608
|
+
return this.name
|
|
1609
|
+
},
|
|
1610
|
+
paletteLabel: 'WhatsApp Dynamic Send',
|
|
1611
|
+
inputs: 1,
|
|
1612
|
+
inputLabels: (i) => 'message',
|
|
1613
|
+
outputs: 0,
|
|
1614
|
+
outputLabels: (i) => [][i],
|
|
1615
|
+
defaults: {
|
|
1616
|
+
name: {
|
|
1617
|
+
value: 'Dynamic Send WhatsApp Message',
|
|
1618
|
+
required: true
|
|
1619
|
+
},
|
|
1620
|
+
accountConfig: {
|
|
1621
|
+
type: 'WhatsappAccountConfigNode',
|
|
1622
|
+
required: true
|
|
1623
|
+
},
|
|
1624
|
+
recipient: {
|
|
1625
|
+
value: 'payload.sender.id'
|
|
1626
|
+
},
|
|
1627
|
+
recipientType: {
|
|
1628
|
+
value: 'msg'
|
|
1629
|
+
},
|
|
1630
|
+
payloads: {
|
|
1631
|
+
value: '[]',
|
|
1632
|
+
required: false,
|
|
1633
|
+
validate: function(value) {
|
|
1634
|
+
try {
|
|
1635
|
+
JSON.parse(value || "[]");
|
|
1636
|
+
return true;
|
|
1637
|
+
} catch (e) {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
},
|
|
1642
|
+
logEnabled: {
|
|
1643
|
+
value: false,
|
|
1644
|
+
required: true
|
|
1645
|
+
},
|
|
1646
|
+
logger: {
|
|
1647
|
+
required: false,
|
|
1648
|
+
value: '',
|
|
1649
|
+
type: 'DelegatedConfigReferenceNode'
|
|
1650
|
+
},
|
|
1651
|
+
logTemplateOverrideEnabled: {
|
|
1652
|
+
value: false
|
|
1653
|
+
},
|
|
1654
|
+
logTemplateOverride: {
|
|
1655
|
+
value: 'message:{{msg}}'
|
|
1656
|
+
},
|
|
1657
|
+
metricsEnabled: {
|
|
1658
|
+
value: false,
|
|
1659
|
+
required: true
|
|
1660
|
+
},
|
|
1661
|
+
metricsReference: {
|
|
1662
|
+
required: false,
|
|
1663
|
+
value: '',
|
|
1664
|
+
type: 'DelegatedConfigReferenceNode'
|
|
1665
|
+
},
|
|
1666
|
+
},
|
|
1667
|
+
oneditprepare: function() {
|
|
1668
|
+
// **** WhatsappDynamicSendMessageNode **** //
|
|
1669
|
+
{
|
|
1670
|
+
let node = this;
|
|
1671
|
+
// ── Recipient typed input ─────────────────────────────────────────────────
|
|
1672
|
+
$("#node-input-recipient").typedInput({
|
|
1673
|
+
types: ["msg", "flow", "global", "str"],
|
|
1674
|
+
typeField: "#node-input-recipientType"
|
|
1675
|
+
});
|
|
1676
|
+
// ── Messages editableList ─────────────────────────────────────────────────
|
|
1677
|
+
let actionList = $("#wa-dynamic-send-actions-list");
|
|
1678
|
+
actionList.editableList({
|
|
1679
|
+
sortable: true,
|
|
1680
|
+
removable: true,
|
|
1681
|
+
height: "auto",
|
|
1682
|
+
scrollOnAdd: true,
|
|
1683
|
+
addButton: "Add Message",
|
|
1684
|
+
addItem: function(container, i, action) {
|
|
1685
|
+
action = action || {};
|
|
1686
|
+
let type = action.type || "text";
|
|
1687
|
+
container.css({
|
|
1688
|
+
padding: "6px 4px"
|
|
1689
|
+
});
|
|
1690
|
+
// Type row
|
|
1691
|
+
let typeRow = $('<div style="display:flex; align-items:center; gap:8px; margin-bottom:5px;">');
|
|
1692
|
+
let iconEl = $('<i style="width:14px; text-align:center; flex-shrink:0;">').addClass("fa " + (WA_SEND_TYPES[type] ? WA_SEND_TYPES[type].icon : "fa-comment"));
|
|
1693
|
+
let typeSelect = $('<select class="wa-dynamic-send-action-type" style="flex:0 0 auto;">');
|
|
1694
|
+
Object.keys(WA_SEND_TYPES).forEach(function(key) {
|
|
1695
|
+
typeSelect.append($("<option>").val(key).text(WA_SEND_TYPES[key].label));
|
|
1696
|
+
});
|
|
1697
|
+
typeSelect.val(type);
|
|
1698
|
+
typeRow.append(iconEl).append(typeSelect);
|
|
1699
|
+
container.append(typeRow);
|
|
1700
|
+
// Fields container
|
|
1701
|
+
let fieldsContainer = $('<div class="wa-dynamic-send-action-fields">');
|
|
1702
|
+
container.append(fieldsContainer);
|
|
1703
|
+
let renderFields = function(selectedType) {
|
|
1704
|
+
fieldsContainer.empty();
|
|
1705
|
+
let typeDef = WA_SEND_TYPES[selectedType];
|
|
1706
|
+
if (!typeDef) return;
|
|
1707
|
+
iconEl.removeClass().addClass("fa " + typeDef.icon);
|
|
1708
|
+
typeDef.fields.forEach(function(field) {
|
|
1709
|
+
let fieldRow = $('<div class="form-row nomargin" style="display:flex; align-items:center; gap:6px;">');
|
|
1710
|
+
fieldRow.append($('<label style="width:70px; margin:0; font-size:0.85em; flex-shrink:0;">').text(field.label));
|
|
1711
|
+
let input = $('<input type="text">').addClass("wa-dynamic-send-field-" + field.key);
|
|
1712
|
+
fieldRow.append(input);
|
|
1713
|
+
fieldsContainer.append(fieldRow);
|
|
1714
|
+
input.typedInput({
|
|
1715
|
+
types: field.types
|
|
1716
|
+
}).typedInput("type", action[field.key + "Type"] || field.types[0]).typedInput("value", action[field.key] || "");
|
|
1717
|
+
fieldRow.find(".red-ui-typedInput-container").css({
|
|
1718
|
+
flex: "1",
|
|
1719
|
+
"min-width": "0",
|
|
1720
|
+
width: ""
|
|
1721
|
+
});
|
|
1722
|
+
});
|
|
1723
|
+
};
|
|
1724
|
+
renderFields(type);
|
|
1725
|
+
typeSelect.on("change", function() {
|
|
1726
|
+
action = {
|
|
1727
|
+
type: $(this).val()
|
|
1728
|
+
};
|
|
1729
|
+
renderFields($(this).val());
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
let actions = [];
|
|
1734
|
+
try {
|
|
1735
|
+
actions = JSON.parse(node.payloads || "[]");
|
|
1736
|
+
} catch (e) {}
|
|
1737
|
+
if (actions.length > 0) {
|
|
1738
|
+
actionList.editableList("addItems", actions);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
// **** logger **** //
|
|
1742
|
+
{
|
|
1743
|
+
let node = this;
|
|
1744
|
+
let templateConfig = {};
|
|
1745
|
+
// controls.
|
|
1746
|
+
let logEnabled = $("#node-input-logEnabled");
|
|
1747
|
+
let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
|
|
1748
|
+
let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
|
|
1749
|
+
let logEnabledSection = $("#section_logEnabled");
|
|
1750
|
+
let loggerSection = $("#section_loggerconfig");
|
|
1751
|
+
loggerSection.hide();
|
|
1752
|
+
// functions.
|
|
1753
|
+
let getLoggerTypes = async function() {
|
|
1754
|
+
let types = [];
|
|
1755
|
+
await $.ajax({
|
|
1756
|
+
url: "nodetypeservice/find?tag=LoggerType",
|
|
1757
|
+
type: "GET",
|
|
1758
|
+
contentType: "application/json; charset=utf-8",
|
|
1759
|
+
data: JSON.stringify({
|
|
1760
|
+
id: node.id
|
|
1761
|
+
}),
|
|
1762
|
+
success: (response) => {
|
|
1763
|
+
types = response;
|
|
1764
|
+
},
|
|
1765
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
1766
|
+
console.log(jqXHR, textStatus, errorThrown);
|
|
1767
|
+
},
|
|
1768
|
+
});
|
|
1769
|
+
return types;
|
|
1770
|
+
}
|
|
1771
|
+
let getLoggers = async () => {
|
|
1772
|
+
let loggers = [];
|
|
1773
|
+
// get the list of types again.
|
|
1774
|
+
let loggerTypes = (await getLoggerTypes()).map(type => type.id);
|
|
1775
|
+
// add all of the known loggers.
|
|
1776
|
+
RED.nodes.eachConfig(cfg => {
|
|
1777
|
+
if (loggerTypes.includes(cfg.type)) {
|
|
1778
|
+
loggers.push({
|
|
1779
|
+
id: cfg.id,
|
|
1780
|
+
name: cfg.label(),
|
|
1781
|
+
type: cfg.type
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
return loggers;
|
|
1786
|
+
};
|
|
1787
|
+
let updateLoggerSelectorList = async () => {
|
|
1788
|
+
loggerSelector.empty();
|
|
1789
|
+
(await getLoggers()).forEach(logger => {
|
|
1790
|
+
$('<option value="' + logger.id + '">' + logger.name + '</option>').appendTo(loggerSelector);
|
|
1791
|
+
});
|
|
1792
|
+
$('<option value="_ADD_">none</option>').appendTo(loggerSelector);
|
|
1793
|
+
}
|
|
1794
|
+
logEnabled.on('change', function() {
|
|
1795
|
+
if (logEnabled.prop("checked")) {
|
|
1796
|
+
logEnabledSection.show();
|
|
1797
|
+
node._def.defaults.logger.required = true;
|
|
1798
|
+
} else {
|
|
1799
|
+
logEnabledSection.hide()
|
|
1800
|
+
node._def.defaults.logger.required = false;
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
logTemplateEnabled.on('change', function() {
|
|
1804
|
+
if (logTemplateEnabled.prop("checked")) {
|
|
1805
|
+
logTemplateEnabledSection.show();
|
|
1806
|
+
node._def.defaults.logTemplateOverride.required = true;
|
|
1807
|
+
} else {
|
|
1808
|
+
logTemplateEnabledSection.hide()
|
|
1809
|
+
node._def.defaults.logTemplateOverride.required = false;
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
node.logTemplateOverrideEditor = RED.editor.createEditor({
|
|
1813
|
+
id: 'node-input-logTemplateOverrideEditor',
|
|
1814
|
+
mode: 'ace/mode/handlebars',
|
|
1815
|
+
value: node.logTemplateOverride
|
|
1816
|
+
});
|
|
1817
|
+
let loggerSelector = $("#logger-selector");
|
|
1818
|
+
let loggerEditButton = $("#logger-edit-btn");
|
|
1819
|
+
let loggerAddButton = $("#logger-add-btn");
|
|
1820
|
+
loggerEditButton.on('click', async function(event) {
|
|
1821
|
+
event.stopPropagation();
|
|
1822
|
+
if (loggerEditButton.hasClass("disabled")) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
// figure out what type of logger this is.
|
|
1826
|
+
let selectedLogger = (await getLoggers()).find(logger => logger.id === loggerSelector.val());
|
|
1827
|
+
RED.editor.editConfig("#logger-selector", selectedLogger.type, selectedLogger.id);
|
|
1828
|
+
});
|
|
1829
|
+
loggerAddButton.on('click', function(event) {
|
|
1830
|
+
event.stopPropagation();
|
|
1831
|
+
let $btn = $(event.target);
|
|
1832
|
+
// Remove any existing dropdown
|
|
1833
|
+
$('.loggertype-dropdown').remove();
|
|
1834
|
+
getLoggerTypes().then(loggerTypes => {
|
|
1835
|
+
let $dropdown = $('<div class="loggertype-dropdown red-ui-editor"></div>');
|
|
1836
|
+
loggerTypes.forEach(loggerType => {
|
|
1837
|
+
$('<div class="dropdown-item"></div>').text(loggerType.name).data('value', loggerType.id).css({
|
|
1838
|
+
padding: '4px 8px',
|
|
1839
|
+
cursor: 'pointer'
|
|
1840
|
+
}).appendTo($dropdown);
|
|
1841
|
+
});
|
|
1842
|
+
// Append first so outerWidth() is measurable, then position so the
|
|
1843
|
+
// right edge of the dropdown aligns with the right edge of the button.
|
|
1844
|
+
$('body').append($dropdown);
|
|
1845
|
+
$dropdown.css({
|
|
1846
|
+
top: $btn.offset().top + $btn.outerHeight(),
|
|
1847
|
+
left: $btn.offset().left + $btn.outerWidth() - $dropdown.outerWidth()
|
|
1848
|
+
});
|
|
1849
|
+
$(document).on('mousedown.dropdown', function(e) {
|
|
1850
|
+
if (!$(e.target).closest('.loggertype-dropdown').length) {
|
|
1851
|
+
$dropdown.remove();
|
|
1852
|
+
$(document).off('mousedown.dropdown'); // Clean up the listener
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
// Handle item selection
|
|
1856
|
+
$dropdown.on('click', '.dropdown-item', function(e) {
|
|
1857
|
+
e.stopPropagation();
|
|
1858
|
+
const selectedValue = $(this).data('value');
|
|
1859
|
+
const selectedText = $(this).text();
|
|
1860
|
+
// Do whatever you need with the selection
|
|
1861
|
+
console.log('Selected:', selectedValue, selectedText);
|
|
1862
|
+
$dropdown.remove();
|
|
1863
|
+
RED.editor.editConfig("#logger-selector", selectedValue, "_ADD_");
|
|
1864
|
+
});
|
|
1865
|
+
})
|
|
1866
|
+
});
|
|
1867
|
+
loggerSelector.on("focus", updateLoggerSelectorList);
|
|
1868
|
+
loggerSelector.on('change', function(event) {
|
|
1869
|
+
if ("_ADD_" === loggerSelector.val()) {
|
|
1870
|
+
loggerEditButton.addClass("disabled")
|
|
1871
|
+
} else {
|
|
1872
|
+
loggerEditButton.removeClass("disabled")
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
// Show the logger section only if at least one logger type is registered.
|
|
1876
|
+
// If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
|
|
1877
|
+
getLoggerTypes().then(loggerTypes => {
|
|
1878
|
+
if (loggerTypes.length === 0) return;
|
|
1879
|
+
loggerSection.show();
|
|
1880
|
+
updateLoggerSelectorList().then(() => {
|
|
1881
|
+
loggerSelector.val(node.logger);
|
|
1882
|
+
loggerSelector.trigger('change');
|
|
1883
|
+
});
|
|
1077
1884
|
});
|
|
1078
1885
|
}
|
|
1079
1886
|
// **** metrics **** //
|
|
1080
1887
|
{
|
|
1081
1888
|
let node = this;
|
|
1889
|
+
let templateConfig = {};
|
|
1082
1890
|
let metricsEnabled = $("#node-input-metricsEnabled");
|
|
1083
|
-
let
|
|
1891
|
+
let metricsSection = $("#section_metricsconfig");
|
|
1892
|
+
let metricsContainer = metricsEnabled.closest(".editorgroupborder");
|
|
1893
|
+
let metricsEnabledSection = metricsContainer.find("#section_metricsEnabled");
|
|
1894
|
+
metricsSection.hide();
|
|
1895
|
+
let getMetricsProviderTypes = async function() {
|
|
1896
|
+
let types = [];
|
|
1897
|
+
await $.ajax({
|
|
1898
|
+
url: "nodetypeservice/find?tag=MetricsProvider",
|
|
1899
|
+
type: "GET",
|
|
1900
|
+
contentType: "application/json; charset=utf-8",
|
|
1901
|
+
success: (response) => {
|
|
1902
|
+
types = response;
|
|
1903
|
+
},
|
|
1904
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
1905
|
+
console.log(jqXHR, textStatus, errorThrown);
|
|
1906
|
+
},
|
|
1907
|
+
});
|
|
1908
|
+
return types;
|
|
1909
|
+
};
|
|
1910
|
+
let getMetricsProviders = async () => {
|
|
1911
|
+
let providers = [];
|
|
1912
|
+
let providerTypes = (await getMetricsProviderTypes()).map(t => t.id);
|
|
1913
|
+
RED.nodes.eachConfig(cfg => {
|
|
1914
|
+
if (providerTypes.includes(cfg.type)) {
|
|
1915
|
+
providers.push({
|
|
1916
|
+
id: cfg.id,
|
|
1917
|
+
name: cfg.label(),
|
|
1918
|
+
type: cfg.type
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
return providers;
|
|
1923
|
+
};
|
|
1924
|
+
let updateMetricsSelectorList = async () => {
|
|
1925
|
+
metricsSelector.empty();
|
|
1926
|
+
(await getMetricsProviders()).forEach(provider => {
|
|
1927
|
+
$('<option value="' + provider.id + '">' + provider.name + '</option>').appendTo(metricsSelector);
|
|
1928
|
+
});
|
|
1929
|
+
$('<option value="_ADD_">none</option>').appendTo(metricsSelector);
|
|
1930
|
+
};
|
|
1084
1931
|
metricsEnabled.on('change', function() {
|
|
1085
1932
|
if (metricsEnabled.prop("checked")) {
|
|
1086
1933
|
metricsEnabledSection.show();
|
|
1087
|
-
node._def.defaults.metricsReference.required = true;
|
|
1088
1934
|
} else {
|
|
1089
|
-
metricsEnabledSection.hide()
|
|
1090
|
-
|
|
1935
|
+
metricsEnabledSection.hide();
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
let metricsSelector = metricsContainer.find("#metrics-selector");
|
|
1939
|
+
let metricsEditButton = metricsContainer.find("#metrics-edit-btn");
|
|
1940
|
+
let metricsAddButton = metricsContainer.find("#metrics-add-btn");
|
|
1941
|
+
metricsEditButton.on('click', async function(event) {
|
|
1942
|
+
event.stopPropagation();
|
|
1943
|
+
if (metricsEditButton.hasClass("disabled")) return;
|
|
1944
|
+
let selectedProvider = (await getMetricsProviders()).find(p => p.id === metricsSelector.val());
|
|
1945
|
+
if (selectedProvider) {
|
|
1946
|
+
RED.editor.editConfig("#metrics-selector", selectedProvider.type, selectedProvider.id);
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
metricsAddButton.on('click', function(event) {
|
|
1950
|
+
event.stopPropagation();
|
|
1951
|
+
let $btn = $(event.currentTarget);
|
|
1952
|
+
let btnOffset = $btn.offset();
|
|
1953
|
+
let btnH = $btn.outerHeight();
|
|
1954
|
+
let btnW = $btn.outerWidth();
|
|
1955
|
+
$('.metricstype-dropdown').remove();
|
|
1956
|
+
getMetricsProviderTypes().then(providerTypes => {
|
|
1957
|
+
let $dropdown = $('<div class="metricstype-dropdown red-ui-editor"></div>').css({
|
|
1958
|
+
position: 'absolute',
|
|
1959
|
+
zIndex: 1000
|
|
1960
|
+
});
|
|
1961
|
+
providerTypes.forEach(providerType => {
|
|
1962
|
+
$('<div class="dropdown-item"></div>').text(providerType.name).data('value', providerType.id).css({
|
|
1963
|
+
padding: '4px 8px',
|
|
1964
|
+
cursor: 'pointer'
|
|
1965
|
+
}).appendTo($dropdown);
|
|
1966
|
+
});
|
|
1967
|
+
$('body').append($dropdown);
|
|
1968
|
+
$dropdown.css({
|
|
1969
|
+
top: btnOffset.top + btnH,
|
|
1970
|
+
left: Math.max(0, btnOffset.left + btnW - $dropdown.outerWidth())
|
|
1971
|
+
});
|
|
1972
|
+
$(document).on('mousedown.metricsdropdown', function(e) {
|
|
1973
|
+
if (!$(e.target).closest('.metricstype-dropdown').length) {
|
|
1974
|
+
$dropdown.remove();
|
|
1975
|
+
$(document).off('mousedown.metricsdropdown');
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
$dropdown.on('click', '.dropdown-item', function(e) {
|
|
1979
|
+
e.stopPropagation();
|
|
1980
|
+
const selectedValue = $(this).data('value');
|
|
1981
|
+
$dropdown.remove();
|
|
1982
|
+
RED.editor.editConfig("#metrics-selector", selectedValue, "_ADD_");
|
|
1983
|
+
});
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
metricsSelector.on("focus", updateMetricsSelectorList);
|
|
1987
|
+
metricsSelector.on('change', function() {
|
|
1988
|
+
if ("_ADD_" === metricsSelector.val()) {
|
|
1989
|
+
metricsEditButton.addClass("disabled");
|
|
1990
|
+
} else {
|
|
1991
|
+
metricsEditButton.removeClass("disabled");
|
|
1091
1992
|
}
|
|
1092
1993
|
});
|
|
1994
|
+
// Show the metrics section only if at least one metrics provider type is registered.
|
|
1995
|
+
// If nodered_prometheus (or any other provider) is not installed, the section stays hidden.
|
|
1996
|
+
getMetricsProviderTypes().then(providerTypes => {
|
|
1997
|
+
if (providerTypes.length === 0) return;
|
|
1998
|
+
metricsSection.show();
|
|
1999
|
+
updateMetricsSelectorList().then(() => {
|
|
2000
|
+
metricsSelector.val(node.metricsReference);
|
|
2001
|
+
metricsSelector.trigger('change');
|
|
2002
|
+
});
|
|
2003
|
+
});
|
|
1093
2004
|
}
|
|
1094
2005
|
},
|
|
1095
2006
|
oneditsave: function() {
|
|
1096
|
-
// ****
|
|
2007
|
+
// **** WhatsappDynamicSendMessageNode **** //
|
|
1097
2008
|
{
|
|
1098
|
-
let
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
2009
|
+
let actions = [];
|
|
2010
|
+
$("#wa-dynamic-send-actions-list").editableList("items").each(function() {
|
|
2011
|
+
let container = $(this);
|
|
2012
|
+
let type = container.find(".wa-dynamic-send-action-type").val();
|
|
2013
|
+
let typeDef = WA_SEND_TYPES[type];
|
|
2014
|
+
if (!typeDef) return;
|
|
2015
|
+
let action = {
|
|
2016
|
+
type: type
|
|
1104
2017
|
};
|
|
2018
|
+
typeDef.fields.forEach(function(field) {
|
|
2019
|
+
let input = container.find(".wa-dynamic-send-field-" + field.key);
|
|
2020
|
+
if (input.length) {
|
|
2021
|
+
action[field.key] = input.typedInput("value");
|
|
2022
|
+
action[field.key + "Type"] = input.typedInput("type");
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
actions.push(action);
|
|
1105
2026
|
});
|
|
1106
|
-
$("#node-input-payloads").val(JSON.stringify(
|
|
2027
|
+
$("#node-input-payloads").val(JSON.stringify(actions));
|
|
1107
2028
|
}
|
|
1108
2029
|
// **** logger **** //
|
|
1109
2030
|
{
|
|
2031
|
+
let node = this;
|
|
2032
|
+
let templateConfig = {};
|
|
1110
2033
|
node = this;
|
|
1111
2034
|
var selectedLogger = $("#logger-selector").val();
|
|
1112
2035
|
node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
|
|
@@ -1115,7 +2038,13 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
|
|
|
1115
2038
|
delete node.logTemplateOverrideEditor;
|
|
1116
2039
|
}
|
|
1117
2040
|
// **** metrics **** //
|
|
1118
|
-
{
|
|
2041
|
+
{
|
|
2042
|
+
let node = this;
|
|
2043
|
+
let templateConfig = {};
|
|
2044
|
+
node = this;
|
|
2045
|
+
var selectedProvider = $("#metrics-selector").val();
|
|
2046
|
+
node.metricsReference = (selectedProvider && selectedProvider !== "_ADD_") ? selectedProvider : '';
|
|
2047
|
+
}
|
|
1119
2048
|
},
|
|
1120
2049
|
oneditcancel: function() {},
|
|
1121
2050
|
oneditdelete: function() {},
|
|
@@ -1124,23 +2053,36 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
|
|
|
1124
2053
|
</script>
|
|
1125
2054
|
|
|
1126
2055
|
|
|
1127
|
-
<script type="text/html" data-template-name='
|
|
1128
|
-
<div id='
|
|
2056
|
+
<script type="text/html" data-template-name='WhatsappDynamicSendMessageNode'>
|
|
2057
|
+
<div id='section_WhatsappDynamicSendMessageNode'>
|
|
1129
2058
|
<div class="form-row">
|
|
1130
2059
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
1131
2060
|
<input type="text" id="node-input-name" />
|
|
1132
2061
|
</div>
|
|
1133
2062
|
|
|
1134
|
-
<div class="form-row
|
|
1135
|
-
<
|
|
1136
|
-
|
|
2063
|
+
<div class="form-row editorsectionheading">
|
|
2064
|
+
<i class="w-16 fa fa-envelope"></i> <span>WhatsApp Account</span>
|
|
2065
|
+
</div>
|
|
2066
|
+
|
|
2067
|
+
<div class="editorgroupborder">
|
|
2068
|
+
<div class="form-row nomargin">
|
|
2069
|
+
<label for="node-input-accountConfig" style="width:120px;"><i class="fa fa-user"></i> Account</label>
|
|
2070
|
+
<input type="text" id="node-input-accountConfig" placeholder="config">
|
|
2071
|
+
</div>
|
|
2072
|
+
|
|
2073
|
+
<div class="form-row nomargin">
|
|
2074
|
+
<label for="node-input-recipient" style="width:120px;"><i class="fa fa-paper-plane"></i> Recipient</label>
|
|
2075
|
+
<input type="text" id="node-input-recipient" placeholder="payload.sender.id">
|
|
2076
|
+
<input type="hidden" id="node-input-recipientType">
|
|
2077
|
+
</div>
|
|
1137
2078
|
</div>
|
|
1138
2079
|
|
|
1139
2080
|
<div class="form-row editorsectionheading">
|
|
1140
|
-
<i class="w-16 fa fa-paper-plane"></i> <span>
|
|
2081
|
+
<i class="w-16 fa fa-paper-plane"></i> <span>Messages</span>
|
|
1141
2082
|
</div>
|
|
1142
2083
|
|
|
1143
|
-
<div class="editorgroupborder"
|
|
2084
|
+
<div class="editorgroupborder">
|
|
2085
|
+
<ol id="wa-dynamic-send-actions-list"></ol>
|
|
1144
2086
|
</div>
|
|
1145
2087
|
|
|
1146
2088
|
<input type="hidden" id="node-input-payloads">
|
|
@@ -1205,8 +2147,18 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
|
|
|
1205
2147
|
|
|
1206
2148
|
<div id="section_metricsEnabled">
|
|
1207
2149
|
<div class="form-row nomargin">
|
|
1208
|
-
<label class="towb_editorlabel" for="
|
|
1209
|
-
<
|
|
2150
|
+
<label class="towb_editorlabel" for="metrics-selector">Metric Provider</label>
|
|
2151
|
+
<div style="width: 70%; display: inline-flex;">
|
|
2152
|
+
<select id="metrics-selector" style="flex-grow: 1; min-width: 0;">
|
|
2153
|
+
<option value="_ADD_">none</option>
|
|
2154
|
+
</select>
|
|
2155
|
+
<a id="metrics-edit-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;">
|
|
2156
|
+
<i class="fa fa-pencil"></i>
|
|
2157
|
+
</a>
|
|
2158
|
+
<a id="metrics-add-btn" class="red-ui-button" style="margin-left: 10px; flex-shrink: 0;">
|
|
2159
|
+
<i class="fa fa-plus"></i>
|
|
2160
|
+
</a>
|
|
2161
|
+
</div>
|
|
1210
2162
|
</div>
|
|
1211
2163
|
</div>
|
|
1212
2164
|
</div>
|
|
@@ -1215,26 +2167,28 @@ Click **Create New Group** to create a new WhatsApp group from within Node-RED.
|
|
|
1215
2167
|
</div>
|
|
1216
2168
|
</script>
|
|
1217
2169
|
|
|
1218
|
-
<script type="text/markdown" data-help-name='
|
|
2170
|
+
<script type="text/markdown" data-help-name='WhatsappDynamicSendMessageNode'>
|
|
1219
2171
|
> **Early development** — this package is still maturing. Some features may be incomplete and diagnostic log output is intentionally verbose for now.
|
|
1220
2172
|
|
|
1221
|
-
Sends a
|
|
2173
|
+
Sends one or more WhatsApp messages directly to a recipient resolved at runtime — for example, replying privately to the sender of a group message.
|
|
1222
2174
|
|
|
1223
2175
|
### Properties
|
|
1224
2176
|
|
|
1225
|
-
: *name*
|
|
1226
|
-
: *
|
|
2177
|
+
: *name* (string) : Display label for this node.
|
|
2178
|
+
: *account* (config) : The WhatsApp Account config node to send from.
|
|
2179
|
+
: *recipient* (typed) : The target JID resolved from the incoming message, flow, global context, or a static string. Defaults to `msg.payload.sender.id` — the JID of whoever sent the triggering message.
|
|
1227
2180
|
|
|
1228
|
-
###
|
|
2181
|
+
### LID addressing
|
|
1229
2182
|
|
|
1230
|
-
|
|
2183
|
+
If the recipient JID ends with `@lid` (newer WhatsApp clients), the node automatically resolves it to the corresponding phone-number JID via the contact store before sending.
|
|
1231
2184
|
|
|
1232
|
-
|
|
1233
|
-
|
|
2185
|
+
### Messages
|
|
2186
|
+
|
|
2187
|
+
Each row in the **Messages** list is sent as a separate WhatsApp message, in order. Supports Text, Image, Video, and Document (with optional filename and MIME type).
|
|
1234
2188
|
|
|
1235
2189
|
### Inputs
|
|
1236
2190
|
|
|
1237
|
-
: *msg* : Trigger message.
|
|
2191
|
+
: *msg* : Trigger message. `msg`-type field values and the recipient are resolved against this message.
|
|
1238
2192
|
|
|
1239
2193
|
### Logging
|
|
1240
2194
|
: *enable logging* (boolean) : if checked, then the node will produce logging output to the specified logger.
|
|
@@ -1244,166 +2198,9 @@ Each field has a checkbox to enable it and a typed value. Supported value types
|
|
|
1244
2198
|
|
|
1245
2199
|
### Metrics
|
|
1246
2200
|
: *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
|
|
1247
|
-
: *metric
|
|
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
|
-
|
|
2201
|
+
: *metric provider* (metricsconfig) : the metrics backend that this node will report to.
|
|
1310
2202
|
</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
2203
|
|
|
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
2204
|
<script type="text/javascript">
|
|
1408
2205
|
RED.nodes.registerType('WhatsappReceiveMessageNode', {
|
|
1409
2206
|
category: 'whatsapp',
|
|
@@ -1458,8 +2255,9 @@ Each field has a checkbox to enable it and a typed value. Supported value types
|
|
|
1458
2255
|
required: true
|
|
1459
2256
|
},
|
|
1460
2257
|
metricsReference: {
|
|
1461
|
-
|
|
1462
|
-
|
|
2258
|
+
required: false,
|
|
2259
|
+
value: '',
|
|
2260
|
+
type: 'DelegatedConfigReferenceNode'
|
|
1463
2261
|
},
|
|
1464
2262
|
},
|
|
1465
2263
|
oneditprepare: function() {
|
|
@@ -1490,11 +2288,14 @@ Each field has a checkbox to enable it and a typed value. Supported value types
|
|
|
1490
2288
|
// **** logger **** //
|
|
1491
2289
|
{
|
|
1492
2290
|
let node = this;
|
|
2291
|
+
let templateConfig = {};
|
|
1493
2292
|
// controls.
|
|
1494
2293
|
let logEnabled = $("#node-input-logEnabled");
|
|
1495
2294
|
let logTemplateEnabled = $("#node-input-logTemplateOverrideEnabled");
|
|
1496
2295
|
let logTemplateEnabledSection = $("#section_logTemplateOverrideEnabled");
|
|
1497
2296
|
let logEnabledSection = $("#section_logEnabled");
|
|
2297
|
+
let loggerSection = $("#section_loggerconfig");
|
|
2298
|
+
loggerSection.hide();
|
|
1498
2299
|
// functions.
|
|
1499
2300
|
let getLoggerTypes = async function() {
|
|
1500
2301
|
let types = [];
|
|
@@ -1618,26 +2419,135 @@ Each field has a checkbox to enable it and a typed value. Supported value types
|
|
|
1618
2419
|
loggerEditButton.removeClass("disabled")
|
|
1619
2420
|
}
|
|
1620
2421
|
});
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
2422
|
+
// Show the logger section only if at least one logger type is registered.
|
|
2423
|
+
// If nodered_logging (or any other logger provider) is not installed, the section stays hidden.
|
|
2424
|
+
getLoggerTypes().then(loggerTypes => {
|
|
2425
|
+
if (loggerTypes.length === 0) return;
|
|
2426
|
+
loggerSection.show();
|
|
2427
|
+
updateLoggerSelectorList().then(() => {
|
|
2428
|
+
loggerSelector.val(node.logger);
|
|
2429
|
+
loggerSelector.trigger('change');
|
|
2430
|
+
});
|
|
1625
2431
|
});
|
|
1626
2432
|
}
|
|
1627
2433
|
// **** metrics **** //
|
|
1628
2434
|
{
|
|
1629
2435
|
let node = this;
|
|
2436
|
+
let templateConfig = {};
|
|
1630
2437
|
let metricsEnabled = $("#node-input-metricsEnabled");
|
|
1631
|
-
let
|
|
2438
|
+
let metricsSection = $("#section_metricsconfig");
|
|
2439
|
+
let metricsContainer = metricsEnabled.closest(".editorgroupborder");
|
|
2440
|
+
let metricsEnabledSection = metricsContainer.find("#section_metricsEnabled");
|
|
2441
|
+
metricsSection.hide();
|
|
2442
|
+
let getMetricsProviderTypes = async function() {
|
|
2443
|
+
let types = [];
|
|
2444
|
+
await $.ajax({
|
|
2445
|
+
url: "nodetypeservice/find?tag=MetricsProvider",
|
|
2446
|
+
type: "GET",
|
|
2447
|
+
contentType: "application/json; charset=utf-8",
|
|
2448
|
+
success: (response) => {
|
|
2449
|
+
types = response;
|
|
2450
|
+
},
|
|
2451
|
+
error: (jqXHR, textStatus, errorThrown) => {
|
|
2452
|
+
console.log(jqXHR, textStatus, errorThrown);
|
|
2453
|
+
},
|
|
2454
|
+
});
|
|
2455
|
+
return types;
|
|
2456
|
+
};
|
|
2457
|
+
let getMetricsProviders = async () => {
|
|
2458
|
+
let providers = [];
|
|
2459
|
+
let providerTypes = (await getMetricsProviderTypes()).map(t => t.id);
|
|
2460
|
+
RED.nodes.eachConfig(cfg => {
|
|
2461
|
+
if (providerTypes.includes(cfg.type)) {
|
|
2462
|
+
providers.push({
|
|
2463
|
+
id: cfg.id,
|
|
2464
|
+
name: cfg.label(),
|
|
2465
|
+
type: cfg.type
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
return providers;
|
|
2470
|
+
};
|
|
2471
|
+
let updateMetricsSelectorList = async () => {
|
|
2472
|
+
metricsSelector.empty();
|
|
2473
|
+
(await getMetricsProviders()).forEach(provider => {
|
|
2474
|
+
$('<option value="' + provider.id + '">' + provider.name + '</option>').appendTo(metricsSelector);
|
|
2475
|
+
});
|
|
2476
|
+
$('<option value="_ADD_">none</option>').appendTo(metricsSelector);
|
|
2477
|
+
};
|
|
1632
2478
|
metricsEnabled.on('change', function() {
|
|
1633
2479
|
if (metricsEnabled.prop("checked")) {
|
|
1634
2480
|
metricsEnabledSection.show();
|
|
1635
|
-
node._def.defaults.metricsReference.required = true;
|
|
1636
2481
|
} else {
|
|
1637
|
-
metricsEnabledSection.hide()
|
|
1638
|
-
|
|
2482
|
+
metricsEnabledSection.hide();
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
let metricsSelector = metricsContainer.find("#metrics-selector");
|
|
2486
|
+
let metricsEditButton = metricsContainer.find("#metrics-edit-btn");
|
|
2487
|
+
let metricsAddButton = metricsContainer.find("#metrics-add-btn");
|
|
2488
|
+
metricsEditButton.on('click', async function(event) {
|
|
2489
|
+
event.stopPropagation();
|
|
2490
|
+
if (metricsEditButton.hasClass("disabled")) return;
|
|
2491
|
+
let selectedProvider = (await getMetricsProviders()).find(p => p.id === metricsSelector.val());
|
|
2492
|
+
if (selectedProvider) {
|
|
2493
|
+
RED.editor.editConfig("#metrics-selector", selectedProvider.type, selectedProvider.id);
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
metricsAddButton.on('click', function(event) {
|
|
2497
|
+
event.stopPropagation();
|
|
2498
|
+
let $btn = $(event.currentTarget);
|
|
2499
|
+
let btnOffset = $btn.offset();
|
|
2500
|
+
let btnH = $btn.outerHeight();
|
|
2501
|
+
let btnW = $btn.outerWidth();
|
|
2502
|
+
$('.metricstype-dropdown').remove();
|
|
2503
|
+
getMetricsProviderTypes().then(providerTypes => {
|
|
2504
|
+
let $dropdown = $('<div class="metricstype-dropdown red-ui-editor"></div>').css({
|
|
2505
|
+
position: 'absolute',
|
|
2506
|
+
zIndex: 1000
|
|
2507
|
+
});
|
|
2508
|
+
providerTypes.forEach(providerType => {
|
|
2509
|
+
$('<div class="dropdown-item"></div>').text(providerType.name).data('value', providerType.id).css({
|
|
2510
|
+
padding: '4px 8px',
|
|
2511
|
+
cursor: 'pointer'
|
|
2512
|
+
}).appendTo($dropdown);
|
|
2513
|
+
});
|
|
2514
|
+
$('body').append($dropdown);
|
|
2515
|
+
$dropdown.css({
|
|
2516
|
+
top: btnOffset.top + btnH,
|
|
2517
|
+
left: Math.max(0, btnOffset.left + btnW - $dropdown.outerWidth())
|
|
2518
|
+
});
|
|
2519
|
+
$(document).on('mousedown.metricsdropdown', function(e) {
|
|
2520
|
+
if (!$(e.target).closest('.metricstype-dropdown').length) {
|
|
2521
|
+
$dropdown.remove();
|
|
2522
|
+
$(document).off('mousedown.metricsdropdown');
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
$dropdown.on('click', '.dropdown-item', function(e) {
|
|
2526
|
+
e.stopPropagation();
|
|
2527
|
+
const selectedValue = $(this).data('value');
|
|
2528
|
+
$dropdown.remove();
|
|
2529
|
+
RED.editor.editConfig("#metrics-selector", selectedValue, "_ADD_");
|
|
2530
|
+
});
|
|
2531
|
+
});
|
|
2532
|
+
});
|
|
2533
|
+
metricsSelector.on("focus", updateMetricsSelectorList);
|
|
2534
|
+
metricsSelector.on('change', function() {
|
|
2535
|
+
if ("_ADD_" === metricsSelector.val()) {
|
|
2536
|
+
metricsEditButton.addClass("disabled");
|
|
2537
|
+
} else {
|
|
2538
|
+
metricsEditButton.removeClass("disabled");
|
|
1639
2539
|
}
|
|
1640
2540
|
});
|
|
2541
|
+
// Show the metrics section only if at least one metrics provider type is registered.
|
|
2542
|
+
// If nodered_prometheus (or any other provider) is not installed, the section stays hidden.
|
|
2543
|
+
getMetricsProviderTypes().then(providerTypes => {
|
|
2544
|
+
if (providerTypes.length === 0) return;
|
|
2545
|
+
metricsSection.show();
|
|
2546
|
+
updateMetricsSelectorList().then(() => {
|
|
2547
|
+
metricsSelector.val(node.metricsReference);
|
|
2548
|
+
metricsSelector.trigger('change');
|
|
2549
|
+
});
|
|
2550
|
+
});
|
|
1641
2551
|
}
|
|
1642
2552
|
},
|
|
1643
2553
|
oneditsave: function() {
|
|
@@ -1653,6 +2563,8 @@ Each field has a checkbox to enable it and a typed value. Supported value types
|
|
|
1653
2563
|
}
|
|
1654
2564
|
// **** logger **** //
|
|
1655
2565
|
{
|
|
2566
|
+
let node = this;
|
|
2567
|
+
let templateConfig = {};
|
|
1656
2568
|
node = this;
|
|
1657
2569
|
var selectedLogger = $("#logger-selector").val();
|
|
1658
2570
|
node.logger = (selectedLogger && selectedLogger !== "_ADD_") ? selectedLogger : '';
|
|
@@ -1661,7 +2573,13 @@ Each field has a checkbox to enable it and a typed value. Supported value types
|
|
|
1661
2573
|
delete node.logTemplateOverrideEditor;
|
|
1662
2574
|
}
|
|
1663
2575
|
// **** metrics **** //
|
|
1664
|
-
{
|
|
2576
|
+
{
|
|
2577
|
+
let node = this;
|
|
2578
|
+
let templateConfig = {};
|
|
2579
|
+
node = this;
|
|
2580
|
+
var selectedProvider = $("#metrics-selector").val();
|
|
2581
|
+
node.metricsReference = (selectedProvider && selectedProvider !== "_ADD_") ? selectedProvider : '';
|
|
2582
|
+
}
|
|
1665
2583
|
},
|
|
1666
2584
|
oneditcancel: function() {},
|
|
1667
2585
|
oneditdelete: function() {},
|
|
@@ -1770,8 +2688,18 @@ Each field has a checkbox to enable it and a typed value. Supported value types
|
|
|
1770
2688
|
|
|
1771
2689
|
<div id="section_metricsEnabled">
|
|
1772
2690
|
<div class="form-row nomargin">
|
|
1773
|
-
<label class="towb_editorlabel" for="
|
|
1774
|
-
<
|
|
2691
|
+
<label class="towb_editorlabel" for="metrics-selector">Metric Provider</label>
|
|
2692
|
+
<div style="width: 70%; display: inline-flex;">
|
|
2693
|
+
<select id="metrics-selector" style="flex-grow: 1; min-width: 0;">
|
|
2694
|
+
<option value="_ADD_">none</option>
|
|
2695
|
+
</select>
|
|
2696
|
+
<a id="metrics-edit-btn" class="red-ui-button disabled" style="margin-left: 10px; flex-shrink: 0;">
|
|
2697
|
+
<i class="fa fa-pencil"></i>
|
|
2698
|
+
</a>
|
|
2699
|
+
<a id="metrics-add-btn" class="red-ui-button" style="margin-left: 10px; flex-shrink: 0;">
|
|
2700
|
+
<i class="fa fa-plus"></i>
|
|
2701
|
+
</a>
|
|
2702
|
+
</div>
|
|
1775
2703
|
</div>
|
|
1776
2704
|
</div>
|
|
1777
2705
|
</div>
|
|
@@ -1808,7 +2736,6 @@ Each message type can be individually enabled or disabled. By default Text, Exte
|
|
|
1808
2736
|
|
|
1809
2737
|
### Metrics
|
|
1810
2738
|
: *enable metrics* (boolean) : if checked, then the node will produce metrics output to the specified metrics provider.
|
|
1811
|
-
: *metric
|
|
2739
|
+
: *metric provider* (metricsconfig) : the metrics backend that this node will report to.
|
|
1812
2740
|
</script>
|
|
1813
|
-
|
|
1814
2741
|
|