node-red-contrib-whatsapp-api 0.1.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/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ UNLICENSED
2
+
3
+ All rights reserved.
4
+
5
+ This package is not licensed for public reuse, modification, or redistribution
6
+ unless the copyright holder grants permission separately.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # node-red-contrib-whatsapp-api
2
+
3
+ Node-RED nodes for driving a WhatsApp Web session with [Baileys](https://github.com/WhiskeySockets/Baileys).
4
+
5
+ ## What it includes
6
+
7
+ - `whatsapp-api-config`: stores reconnect settings and the local runtime data directory
8
+ - `whatsapp-api-in`: emits normalized incoming WhatsApp messages
9
+ - `whatsapp-api-send`: sends text or media to a chat
10
+ - `whatsapp-api-history`: reads recent messages from the local persisted store
11
+
12
+ ## Runtime requirements
13
+
14
+ - Node.js 20+
15
+ - Node-RED 4+
16
+ - A phone that can scan the WhatsApp Web QR code
17
+
18
+ ## Install locally
19
+
20
+ ```bash
21
+ npm install
22
+ npm run build
23
+ cd ~/.node-red
24
+ npm install /absolute/path/to/node-red-contrib-whatsapp-api-0.1.0.tgz
25
+ ```
26
+
27
+ Then restart Node-RED and add the `whatsapp-api` nodes from the palette.
28
+
29
+ The packed module ships as a bundled runtime, so target installs do not need to rebuild or reshuffle the Baileys dependency tree on the Node-RED host.
30
+
31
+ ## Install from npm
32
+
33
+ After the package has been published:
34
+
35
+ ```bash
36
+ cd ~/.node-red
37
+ npm install node-red-contrib-whatsapp-api
38
+ ```
39
+
40
+ ## Login flow
41
+
42
+ This palette uses a deploy-first QR flow.
43
+
44
+ 1. Add a `whatsapp-api-config` node and deploy the flow.
45
+ 2. Open the config node again in the editor.
46
+ 3. Click `Connect`.
47
+ 4. Scan the QR code with WhatsApp on your phone.
48
+ 5. Wait for the status to show `Connected`.
49
+
50
+ The runtime keeps its auth state on disk and now attempts to reuse it automatically after every Node-RED restart. Use `Disconnect` to clear the stored session and force a fresh QR login.
51
+
52
+ ## Local message history
53
+
54
+ `whatsapp-api-history` reads from a local JSON store that is updated by the connected runtime. It returns messages the session has already synced or seen. It is not a server-side fetch of arbitrary WhatsApp chat history.
55
+
56
+ The history node can also return only the newest incoming messages that the linked WhatsApp account still has unread for the selected chat.
57
+
58
+ ## Example Node-RED flow
59
+
60
+ Import this JSON from the Node-RED editor (`Menu -> Import -> Clipboard`) to get a starter flow with:
61
+
62
+ - one `whatsapp-api-config` node
63
+ - one `whatsapp-api-send` example
64
+ - one `whatsapp-api-in` listener
65
+ - one `whatsapp-api-history` example
66
+
67
+ Replace the sample peer with a real WhatsApp JID such as `393331234567@s.whatsapp.net`.
68
+
69
+ ```json
70
+ [
71
+ {
72
+ "id": "a1f4d7c2e9b00101",
73
+ "type": "tab",
74
+ "label": "WhatsApp Example",
75
+ "disabled": false,
76
+ "info": ""
77
+ },
78
+ {
79
+ "id": "b2f4d7c2e9b00102",
80
+ "type": "whatsapp-api-config",
81
+ "name": "My WhatsApp",
82
+ "reconnectMinMs": "2000",
83
+ "reconnectMaxMs": "30000",
84
+ "dataDir": ""
85
+ },
86
+ {
87
+ "id": "c3f4d7c2e9b00103",
88
+ "type": "inject",
89
+ "z": "a1f4d7c2e9b00101",
90
+ "name": "Send test message",
91
+ "props": [
92
+ {
93
+ "p": "payload"
94
+ }
95
+ ],
96
+ "payload": "hello from Node-RED",
97
+ "payloadType": "str",
98
+ "x": 170,
99
+ "y": 100,
100
+ "wires": [
101
+ [
102
+ "d4f4d7c2e9b00104"
103
+ ]
104
+ ]
105
+ },
106
+ {
107
+ "id": "d4f4d7c2e9b00104",
108
+ "type": "whatsapp-api-send",
109
+ "z": "a1f4d7c2e9b00101",
110
+ "name": "Send WhatsApp",
111
+ "account": "b2f4d7c2e9b00102",
112
+ "peer": "393331234567@s.whatsapp.net",
113
+ "x": 450,
114
+ "y": 100,
115
+ "wires": [
116
+ [
117
+ "e5f4d7c2e9b00105"
118
+ ]
119
+ ]
120
+ },
121
+ {
122
+ "id": "e5f4d7c2e9b00105",
123
+ "type": "debug",
124
+ "z": "a1f4d7c2e9b00101",
125
+ "name": "Send result",
126
+ "active": true,
127
+ "tosidebar": true,
128
+ "complete": "true",
129
+ "targetType": "full",
130
+ "x": 720,
131
+ "y": 100,
132
+ "wires": []
133
+ },
134
+ {
135
+ "id": "f6f4d7c2e9b00106",
136
+ "type": "whatsapp-api-in",
137
+ "z": "a1f4d7c2e9b00101",
138
+ "name": "Incoming messages",
139
+ "account": "b2f4d7c2e9b00102",
140
+ "includeRaw": false,
141
+ "x": 190,
142
+ "y": 200,
143
+ "wires": [
144
+ [
145
+ "07f4d7c2e9b00107"
146
+ ]
147
+ ]
148
+ },
149
+ {
150
+ "id": "07f4d7c2e9b00107",
151
+ "type": "debug",
152
+ "z": "a1f4d7c2e9b00101",
153
+ "name": "Incoming debug",
154
+ "active": true,
155
+ "tosidebar": true,
156
+ "complete": "true",
157
+ "targetType": "full",
158
+ "x": 470,
159
+ "y": 200,
160
+ "wires": []
161
+ },
162
+ {
163
+ "id": "18f4d7c2e9b00108",
164
+ "type": "inject",
165
+ "z": "a1f4d7c2e9b00101",
166
+ "name": "Read last 5",
167
+ "props": [
168
+ {
169
+ "p": "payload"
170
+ }
171
+ ],
172
+ "payload": "",
173
+ "payloadType": "date",
174
+ "x": 150,
175
+ "y": 300,
176
+ "wires": [
177
+ [
178
+ "29f4d7c2e9b00109"
179
+ ]
180
+ ]
181
+ },
182
+ {
183
+ "id": "29f4d7c2e9b00109",
184
+ "type": "whatsapp-api-history",
185
+ "z": "a1f4d7c2e9b00101",
186
+ "name": "Recent Chat History",
187
+ "account": "b2f4d7c2e9b00102",
188
+ "peer": "393331234567@s.whatsapp.net",
189
+ "limit": "5",
190
+ "includeRaw": false,
191
+ "x": 450,
192
+ "y": 300,
193
+ "wires": [
194
+ [
195
+ "3af4d7c2e9b0010a"
196
+ ]
197
+ ]
198
+ },
199
+ {
200
+ "id": "3af4d7c2e9b0010a",
201
+ "type": "debug",
202
+ "z": "a1f4d7c2e9b00101",
203
+ "name": "History debug",
204
+ "active": true,
205
+ "tosidebar": true,
206
+ "complete": "true",
207
+ "targetType": "full",
208
+ "x": 730,
209
+ "y": 300,
210
+ "wires": []
211
+ }
212
+ ]
213
+ ```
214
+
215
+ ## Message contract
216
+
217
+ Incoming and action nodes use this shape:
218
+
219
+ - `msg.payload`: the main payload for the node
220
+ - `msg.whatsapp`: normalized WhatsApp metadata
221
+
222
+ Common metadata fields:
223
+
224
+ - `msg.whatsapp.peer`: `{ id, type, title, ref }`
225
+ - `msg.whatsapp.chatId`
226
+ - `msg.whatsapp.senderId`
227
+ - `msg.whatsapp.messageId`
228
+ - `msg.whatsapp.media`
229
+ - `msg.whatsapp.raw` when the node is configured with `Include Raw`
230
+
231
+ ## Send node input patterns
232
+
233
+ Text only:
234
+
235
+ ```json
236
+ {
237
+ "payload": "hello from Node-RED",
238
+ "whatsapp": {
239
+ "peer": "393331234567@s.whatsapp.net"
240
+ }
241
+ }
242
+ ```
243
+
244
+ Media with a file path:
245
+
246
+ ```json
247
+ {
248
+ "payload": "daily report",
249
+ "whatsapp": {
250
+ "peer": "393331234567@s.whatsapp.net",
251
+ "mediaPath": "/tmp/report.pdf"
252
+ }
253
+ }
254
+ ```
255
+
256
+ Media with a Buffer:
257
+
258
+ ```js
259
+ msg.payload = Buffer.from("file-bytes");
260
+ msg.whatsapp = {
261
+ peer: "393331234567@s.whatsapp.net",
262
+ fileName: "image.jpg",
263
+ caption: "generated by a flow",
264
+ mimeType: "image/jpeg"
265
+ };
266
+ return msg;
267
+ ```
@@ -0,0 +1,358 @@
1
+ <script type="text/html" data-template-name="whatsapp-api-config">
2
+ <div class="form-row">
3
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-config-input-name" placeholder="WhatsApp account" />
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-config-input-reconnectMinMs"><i class="fa fa-repeat"></i> Reconnect Min</label>
8
+ <input type="number" id="node-config-input-reconnectMinMs" placeholder="2000" />
9
+ </div>
10
+ <div class="form-row">
11
+ <label for="node-config-input-reconnectMaxMs"><i class="fa fa-repeat"></i> Reconnect Max</label>
12
+ <input type="number" id="node-config-input-reconnectMaxMs" placeholder="30000" />
13
+ </div>
14
+ <div class="form-row">
15
+ <label for="node-config-input-dataDir"><i class="fa fa-folder-open"></i> Data Dir</label>
16
+ <input type="text" id="node-config-input-dataDir" placeholder="Default under Node-RED userDir" />
17
+ </div>
18
+
19
+ <div class="form-row whatsapp-api-auth-panel">
20
+ <label><i class="fa fa-whatsapp"></i> Session</label>
21
+ <div class="whatsapp-api-auth-body">
22
+ <div id="whatsapp-api-auth-status" class="form-tips">Save and deploy before connecting.</div>
23
+ <div class="whatsapp-api-auth-actions">
24
+ <a href="#" class="editor-button" id="whatsapp-api-connect">Connect</a>
25
+ <a href="#" class="editor-button" id="whatsapp-api-test">Test Connection</a>
26
+ <a href="#" class="editor-button" id="whatsapp-api-disconnect">Disconnect</a>
27
+ </div>
28
+ <div id="whatsapp-api-qr-panel" class="whatsapp-api-qr-panel" style="display:none">
29
+ <img id="whatsapp-api-qr-image" alt="WhatsApp QR code" />
30
+ </div>
31
+ <div id="whatsapp-api-session-summary" class="form-tips">
32
+ Deploy the config node, then use Connect to start the WhatsApp QR login flow.
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </script>
37
+
38
+ <script type="text/html" data-help-name="whatsapp-api-config">
39
+ <p>Stores the runtime settings for the WhatsApp Web session used by the other nodes in this palette.</p>
40
+ <p>Save and deploy first, then use <b>Connect</b> in the editor to start the QR login flow against the deployed runtime.</p>
41
+ </script>
42
+
43
+ <script type="text/html" data-template-name="whatsapp-api-in">
44
+ <div class="form-row">
45
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
46
+ <input type="text" id="node-input-name" />
47
+ </div>
48
+ <div class="form-row">
49
+ <label for="node-input-account"><i class="fa fa-user"></i> Account</label>
50
+ <input type="text" id="node-input-account" />
51
+ </div>
52
+ <div class="form-row">
53
+ <label for="node-input-includeRaw"><i class="fa fa-code"></i> Include Raw</label>
54
+ <input type="checkbox" id="node-input-includeRaw" style="width:auto" />
55
+ </div>
56
+ </script>
57
+
58
+ <script type="text/html" data-help-name="whatsapp-api-in">
59
+ <p>Emits normalized incoming WhatsApp messages from the configured session.</p>
60
+ <p><code>msg.payload</code> contains the message body. <code>msg.whatsapp</code> carries normalized WhatsApp metadata.</p>
61
+ </script>
62
+
63
+ <script type="text/html" data-template-name="whatsapp-api-send">
64
+ <div class="form-row">
65
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
66
+ <input type="text" id="node-input-name" />
67
+ </div>
68
+ <div class="form-row">
69
+ <label for="node-input-account"><i class="fa fa-user"></i> Account</label>
70
+ <input type="text" id="node-input-account" />
71
+ </div>
72
+ <div class="form-row">
73
+ <label for="node-input-peer"><i class="fa fa-comment"></i> Default Chat</label>
74
+ <input type="text" id="node-input-peer" placeholder="393331234567 or 393331234567@s.whatsapp.net" />
75
+ </div>
76
+ </script>
77
+
78
+ <script type="text/html" data-help-name="whatsapp-api-send">
79
+ <p>Sends a WhatsApp text or media message.</p>
80
+ <p>Use <code>msg.payload</code> for text, or provide a Buffer/path through <code>msg.payload</code> and <code>msg.whatsapp.mediaPath</code>.</p>
81
+ </script>
82
+
83
+ <script type="text/html" data-template-name="whatsapp-api-history">
84
+ <div class="form-row">
85
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
86
+ <input type="text" id="node-input-name" />
87
+ </div>
88
+ <div class="form-row">
89
+ <label for="node-input-account"><i class="fa fa-user"></i> Account</label>
90
+ <input type="text" id="node-input-account" />
91
+ </div>
92
+ <div class="form-row">
93
+ <label for="node-input-peer"><i class="fa fa-comment"></i> Default Chat</label>
94
+ <input type="text" id="node-input-peer" placeholder="393331234567@s.whatsapp.net" />
95
+ </div>
96
+ <div class="form-row">
97
+ <label for="node-input-limit"><i class="fa fa-list-ol"></i> Limit</label>
98
+ <input type="number" id="node-input-limit" placeholder="10" />
99
+ </div>
100
+ <div class="form-row">
101
+ <label for="node-input-includeRaw"><i class="fa fa-code"></i> Include Raw</label>
102
+ <input type="checkbox" id="node-input-includeRaw" style="width:auto" />
103
+ </div>
104
+ <div class="form-row">
105
+ <label for="node-input-unreadOnly"><i class="fa fa-envelope"></i> Unread Only</label>
106
+ <input type="checkbox" id="node-input-unreadOnly" style="width:auto" />
107
+ </div>
108
+ </script>
109
+
110
+ <script type="text/html" data-help-name="whatsapp-api-history">
111
+ <p>Reads recent messages from the local WhatsApp message store maintained by the connected session.</p>
112
+ <p>The node outputs an array of normalized messages in <code>msg.payload</code>.</p>
113
+ <p>When <code>Unread Only</code> is enabled, the node returns the newest incoming messages that WhatsApp still marks as unread for that chat.</p>
114
+ </script>
115
+
116
+ <style>
117
+ .whatsapp-api-auth-panel .whatsapp-api-auth-body {
118
+ width: calc(100% - 110px);
119
+ margin-left: 110px;
120
+ }
121
+
122
+ .whatsapp-api-auth-actions {
123
+ display: flex;
124
+ gap: 8px;
125
+ margin-top: 8px;
126
+ }
127
+
128
+ .whatsapp-api-qr-panel {
129
+ margin-top: 12px;
130
+ }
131
+
132
+ .whatsapp-api-qr-panel img {
133
+ background: #fff;
134
+ border: 1px solid #d3d7db;
135
+ border-radius: 8px;
136
+ display: block;
137
+ max-width: 240px;
138
+ padding: 8px;
139
+ }
140
+ </style>
141
+
142
+ <script type="text/javascript">
143
+ (function() {
144
+ function apiRequest(method, url, data) {
145
+ return $.ajax({
146
+ contentType: "application/json; charset=utf-8",
147
+ data: data ? JSON.stringify(data) : undefined,
148
+ dataType: "json",
149
+ type: method,
150
+ url: url
151
+ });
152
+ }
153
+
154
+ function showError(xhr) {
155
+ var message = xhr && xhr.responseJSON && xhr.responseJSON.error ? xhr.responseJSON.error : "WhatsApp request failed.";
156
+ RED.notify(message, "error");
157
+ }
158
+
159
+ function updateAuthUi(status, hasNodeId) {
160
+ var state = status && status.state ? status.state : "disconnected";
161
+ var label = status && status.label ? status.label : "Disconnected";
162
+ var qrDataUrl = status && status.qrDataUrl ? status.qrDataUrl : null;
163
+
164
+ $("#whatsapp-api-auth-status").text(label);
165
+
166
+ if (qrDataUrl && state === "awaiting_qr") {
167
+ $("#whatsapp-api-qr-image").attr("src", qrDataUrl);
168
+ $("#whatsapp-api-qr-panel").show();
169
+ $("#whatsapp-api-session-summary").text("Scan the QR code with WhatsApp on your phone to finish linking this runtime session.");
170
+ return;
171
+ }
172
+
173
+ $("#whatsapp-api-qr-image").attr("src", "");
174
+ $("#whatsapp-api-qr-panel").hide();
175
+
176
+ if (!hasNodeId) {
177
+ $("#whatsapp-api-session-summary").text("Deploy the config node before starting the WhatsApp QR login flow.");
178
+ } else if (state === "connected") {
179
+ $("#whatsapp-api-session-summary").text("WhatsApp is connected. The runtime will reuse the stored auth state after restart.");
180
+ } else if (state === "connecting") {
181
+ $("#whatsapp-api-session-summary").text("Starting the WhatsApp session.");
182
+ } else if (state === "disconnected") {
183
+ $("#whatsapp-api-session-summary").text("Use Connect to start a fresh WhatsApp QR login flow.");
184
+ } else if (state === "error") {
185
+ $("#whatsapp-api-session-summary").text("The WhatsApp session hit an error. You can try Test Connection or Connect again.");
186
+ }
187
+ }
188
+
189
+ function registerSimpleNode(type, options) {
190
+ RED.nodes.registerType(type, $.extend(true, {
191
+ align: "right",
192
+ category: "whatsapp-api",
193
+ color: "#c8f2cf",
194
+ defaults: {
195
+ account: { required: true, type: "whatsapp-api-config" },
196
+ name: { value: "" }
197
+ }
198
+ }, options));
199
+ }
200
+
201
+ RED.nodes.registerType("whatsapp-api-config", {
202
+ category: "config",
203
+ defaults: {
204
+ dataDir: { value: "" },
205
+ name: { value: "" },
206
+ reconnectMaxMs: { value: 30000, validate: RED.validators.number() },
207
+ reconnectMinMs: { value: 2000, validate: RED.validators.number() }
208
+ },
209
+ label: function() {
210
+ return this.name || "whatsapp-api";
211
+ },
212
+ oneditprepare: function() {
213
+ var node = this;
214
+ var pollingHandle = null;
215
+
216
+ function stopPolling() {
217
+ if (pollingHandle) {
218
+ clearInterval(pollingHandle);
219
+ pollingHandle = null;
220
+ }
221
+ }
222
+
223
+ function pollRuntimeStatus() {
224
+ if (!node.id) {
225
+ updateAuthUi({ label: "Disconnected", state: "disconnected" }, false);
226
+ return;
227
+ }
228
+
229
+ apiRequest("GET", "whatsapp-api/config/" + encodeURIComponent(node.id) + "/status")
230
+ .done(function(response) {
231
+ updateAuthUi(response.status, true);
232
+ })
233
+ .fail(function(xhr) {
234
+ if (xhr && xhr.status !== 404) {
235
+ showError(xhr);
236
+ }
237
+ });
238
+ }
239
+
240
+ function startPolling() {
241
+ stopPolling();
242
+ pollingHandle = setInterval(pollRuntimeStatus, 1500);
243
+ }
244
+
245
+ $("#whatsapp-api-connect").on("click", function(event) {
246
+ event.preventDefault();
247
+
248
+ if (!node.id) {
249
+ RED.notify("Deploy the config node before connecting WhatsApp.", "warning");
250
+ return;
251
+ }
252
+
253
+ apiRequest("POST", "whatsapp-api/config/" + encodeURIComponent(node.id) + "/connect")
254
+ .done(function(response) {
255
+ updateAuthUi(response.status, true);
256
+ startPolling();
257
+ })
258
+ .fail(showError);
259
+ });
260
+
261
+ $("#whatsapp-api-test").on("click", function(event) {
262
+ event.preventDefault();
263
+
264
+ if (!node.id) {
265
+ RED.notify("Deploy the config node before testing the WhatsApp session.", "warning");
266
+ return;
267
+ }
268
+
269
+ apiRequest("POST", "whatsapp-api/config/" + encodeURIComponent(node.id) + "/test")
270
+ .done(function(response) {
271
+ updateAuthUi(response.status, true);
272
+ })
273
+ .fail(showError);
274
+ });
275
+
276
+ $("#whatsapp-api-disconnect").on("click", function(event) {
277
+ event.preventDefault();
278
+
279
+ if (!node.id) {
280
+ updateAuthUi({ label: "Disconnected", state: "disconnected" }, false);
281
+ return;
282
+ }
283
+
284
+ apiRequest("POST", "whatsapp-api/config/" + encodeURIComponent(node.id) + "/disconnect")
285
+ .done(function(response) {
286
+ updateAuthUi(response.status, true);
287
+ })
288
+ .fail(showError);
289
+ });
290
+
291
+ updateAuthUi({ label: "Disconnected", state: "disconnected" }, Boolean(node.id));
292
+ pollRuntimeStatus();
293
+ startPolling();
294
+
295
+ this._whatsappApiCleanup = function() {
296
+ stopPolling();
297
+ };
298
+ },
299
+ oneditsave: function() {
300
+ if (this._whatsappApiCleanup) {
301
+ this._whatsappApiCleanup();
302
+ this._whatsappApiCleanup = null;
303
+ }
304
+ },
305
+ oneditcancel: function() {
306
+ if (this._whatsappApiCleanup) {
307
+ this._whatsappApiCleanup();
308
+ this._whatsappApiCleanup = null;
309
+ }
310
+ }
311
+ });
312
+
313
+ registerSimpleNode("whatsapp-api-in", {
314
+ defaults: {
315
+ account: { required: true, type: "whatsapp-api-config" },
316
+ includeRaw: { value: false },
317
+ name: { value: "" }
318
+ },
319
+ icon: "font-awesome/fa-inbox",
320
+ inputs: 0,
321
+ label: function() {
322
+ return this.name || "whatsapp-api in";
323
+ },
324
+ outputs: 1
325
+ });
326
+
327
+ registerSimpleNode("whatsapp-api-send", {
328
+ defaults: {
329
+ account: { required: true, type: "whatsapp-api-config" },
330
+ name: { value: "" },
331
+ peer: { value: "" }
332
+ },
333
+ icon: "font-awesome/fa-paper-plane",
334
+ inputs: 1,
335
+ label: function() {
336
+ return this.name || "whatsapp-api send";
337
+ },
338
+ outputs: 1
339
+ });
340
+
341
+ registerSimpleNode("whatsapp-api-history", {
342
+ defaults: {
343
+ account: { required: true, type: "whatsapp-api-config" },
344
+ includeRaw: { value: false },
345
+ limit: { value: 10, validate: RED.validators.number() },
346
+ name: { value: "" },
347
+ peer: { value: "" },
348
+ unreadOnly: { value: false }
349
+ },
350
+ icon: "font-awesome/fa-history",
351
+ inputs: 1,
352
+ label: function() {
353
+ return this.name || "whatsapp-api history";
354
+ },
355
+ outputs: 1
356
+ });
357
+ })();
358
+ </script>