@jep182/n8n-nodes-whatsthat 0.3.0 → 0.4.3

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 CHANGED
@@ -17,8 +17,8 @@ It lets you:
17
17
 
18
18
  Use this node to:
19
19
 
20
- - create a session
21
20
  - connect a session
21
+ - wait for a session to become connected
22
22
  - list sessions
23
23
  - inspect session status
24
24
  - disconnect a session
@@ -27,6 +27,7 @@ Use this node to:
27
27
  When pairing is available, the node returns:
28
28
 
29
29
  - `pairingCode`
30
+ - `qrCodeUrl`
30
31
  - `qr`
31
32
  - `qrDataUrl`
32
33
 
@@ -85,14 +86,19 @@ WhatsThat embeds Baileys directly in n8n.
85
86
  ```
86
87
 
87
88
  3. Add `WhatsThat Session`.
88
- 4. Choose `Create Session`, then provide:
89
+ 4. Choose `Connect Session`, then provide:
89
90
  - `Session ID (Internal)`: a stable unique ID such as `main-phone`
90
91
  - `Label (Visible Name)`: a human-readable name such as `Luca personal phone`
91
92
  - optional `Phone Number For Pairing`: full number with country code, digits only, without `00` or `+`
92
- 5. Run `Connect Session`.
93
- 6. Use the returned `pairingCode` or `qrDataUrl` to connect the device.
94
- 7. Use `WhatsThat Targets` to discover and link chats/groups.
95
- 8. Use `WhatsThat Message` to send messages by alias or raw JID.
93
+ 5. Run the node and use the returned `pairingCode`, `qrCodeUrl`, or `qrDataUrl` to connect the device.
94
+ 6. Add another `WhatsThat Session` node with `Ensure Session`.
95
+ 7. Set `Return When` to `Connected` so the workflow waits until the already-started session finishes pairing.
96
+ 8. Use `WhatsThat Targets` to discover and link chats/groups.
97
+ 9. Use `WhatsThat Message` to send messages by alias or raw JID.
98
+
99
+ Example workflow:
100
+
101
+ - [`examples/register-number.workflow.json`](./examples/register-number.workflow.json)
96
102
 
97
103
  ## Media Delivery
98
104
 
@@ -13,7 +13,7 @@ class WhatsThatSession {
13
13
  icon: 'file:whatsthat.svg',
14
14
  group: ['transform'],
15
15
  version: 1,
16
- description: 'Manage embedded Baileys sessions for WhatsThat',
16
+ description: 'Create, connect, and manage embedded Baileys sessions for WhatsThat',
17
17
  defaults: { name: 'WhatsThat Session' },
18
18
  inputs: ['main'],
19
19
  outputs: ['main'],
@@ -23,10 +23,10 @@ class WhatsThatSession {
23
23
  displayName: 'Operation',
24
24
  name: 'operation',
25
25
  type: 'options',
26
- default: 'create',
26
+ default: 'connect',
27
27
  options: [
28
- { name: 'Create Session', value: 'create' },
29
28
  { name: 'Connect Session', value: 'connect' },
29
+ { name: 'Ensure Session', value: 'ensure' },
30
30
  { name: 'List Sessions', value: 'list' },
31
31
  { name: 'Get Session Status', value: 'status' },
32
32
  { name: 'Disconnect Session', value: 'disconnect' },
@@ -51,7 +51,7 @@ class WhatsThatSession {
51
51
  default: '',
52
52
  description: 'Human-readable name shown in results. Example: "Luca personal phone" or "Support number".',
53
53
  displayOptions: {
54
- show: { operation: ['create'] },
54
+ show: { operation: ['connect', 'ensure'] },
55
55
  },
56
56
  },
57
57
  {
@@ -61,7 +61,34 @@ class WhatsThatSession {
61
61
  default: '',
62
62
  description: 'Optional. Full phone number with country code, digits only, without 00 or +. Example: 393331234567.',
63
63
  displayOptions: {
64
- show: { operation: ['create'] },
64
+ show: { operation: ['connect', 'ensure'] },
65
+ },
66
+ },
67
+ {
68
+ displayName: 'Return When',
69
+ name: 'waitFor',
70
+ type: 'options',
71
+ default: 'pairing_or_connected',
72
+ options: [
73
+ { name: 'Pairing Is Ready Or Connected', value: 'pairing_or_connected' },
74
+ { name: 'Connected', value: 'connected' },
75
+ ],
76
+ description: 'Return as soon as pairing data is ready, or wait until the session is fully connected.',
77
+ displayOptions: {
78
+ show: { operation: ['ensure'] },
79
+ },
80
+ },
81
+ {
82
+ displayName: 'Timeout Seconds',
83
+ name: 'timeoutSeconds',
84
+ type: 'number',
85
+ typeOptions: {
86
+ minValue: 1,
87
+ },
88
+ default: 20,
89
+ description: 'Maximum time to wait before returning the latest known session status.',
90
+ displayOptions: {
91
+ show: { operation: ['ensure'] },
65
92
  },
66
93
  },
67
94
  ],
@@ -77,19 +104,33 @@ class WhatsThatSession {
77
104
  const rawSessionId = this.getNodeParameter('sessionId', itemIndex, '');
78
105
  const sessionId = operation === 'list' ? (0, validation_1.normalizeSessionId)(rawSessionId) : (0, validation_1.requireSessionId)(rawSessionId);
79
106
  let json;
80
- const label = this.getNodeParameter('label', itemIndex, '').trim();
81
- const phoneNumberForPairing = this.getNodeParameter('phoneNumberForPairing', itemIndex, '').trim();
82
107
  switch (operation) {
83
- case 'create':
84
- json = await runtime_1.registry.ensureSession(access.paths.root, access, {
108
+ case 'connect': {
109
+ const label = this.getNodeParameter('label', itemIndex, '').trim();
110
+ const phoneNumberForPairing = this.getNodeParameter('phoneNumberForPairing', itemIndex, '').trim();
111
+ await runtime_1.registry.ensureSession(access.paths.root, access, {
85
112
  sessionId,
86
113
  label: label || sessionId,
87
114
  phoneNumberForPairing,
88
115
  });
89
- break;
90
- case 'connect':
91
116
  json = await runtime_1.registry.connectSession(access.paths.root, access, sessionId);
92
117
  break;
118
+ }
119
+ case 'ensure': {
120
+ const label = this.getNodeParameter('label', itemIndex, '').trim();
121
+ const phoneNumberForPairing = this.getNodeParameter('phoneNumberForPairing', itemIndex, '').trim();
122
+ const waitFor = this.getNodeParameter('waitFor', itemIndex);
123
+ const timeoutSeconds = this.getNodeParameter('timeoutSeconds', itemIndex, 20);
124
+ json = await runtime_1.registry.ensureConnectedSession(access.paths.root, access, {
125
+ sessionId,
126
+ label: label || sessionId,
127
+ phoneNumberForPairing,
128
+ }, {
129
+ waitFor,
130
+ timeoutMs: timeoutSeconds * 1000,
131
+ });
132
+ break;
133
+ }
93
134
  case 'list':
94
135
  json = await runtime_1.registry.listSessions(access);
95
136
  break;
@@ -46,6 +46,14 @@ declare class WhatsThatRegistry extends EventEmitter {
46
46
  phoneNumberForPairing?: string;
47
47
  }): Promise<SessionRecord>;
48
48
  connectSession(storageRoot: string, access: DataAccess, sessionId: string): Promise<SessionRecord>;
49
+ ensureConnectedSession(_storageRoot: string, access: DataAccess, input: {
50
+ sessionId: string;
51
+ label: string;
52
+ phoneNumberForPairing?: string;
53
+ }, options?: {
54
+ waitFor?: 'pairing_or_connected' | 'connected';
55
+ timeoutMs?: number;
56
+ }): Promise<SessionRecord>;
49
57
  listSessions(access: DataAccess): Promise<SessionRecord[]>;
50
58
  getSession(access: DataAccess, sessionId: string): Promise<SessionRecord | undefined>;
51
59
  disconnectSession(access: DataAccess, sessionId: string): Promise<SessionRecord | undefined>;
@@ -61,7 +69,11 @@ declare class WhatsThatRegistry extends EventEmitter {
61
69
  private upsertSession;
62
70
  private upsertDiscoveredTarget;
63
71
  private emitRuntime;
72
+ private waitForSessionState;
73
+ private matchesWaitTarget;
74
+ waitForConnectedSession(access: DataAccess, sessionId: string, timeoutMs: number): Promise<SessionRecord>;
64
75
  private required;
76
+ private buildQrCodeUrl;
65
77
  }
66
78
  export declare const registry: WhatsThatRegistry;
67
79
  export declare function extractMessageText(message: proto.IMessage | null | undefined): string | undefined;
@@ -98,10 +98,12 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
98
98
  if (update.qr) {
99
99
  qrcode_terminal_1.default.generate(update.qr, { small: true });
100
100
  const qrDataUrl = await qrcode_1.default.toDataURL(update.qr);
101
+ const qrCodeUrl = this.buildQrCodeUrl(update.qr);
101
102
  const pairingRecord = {
102
103
  ...current,
103
104
  status: 'pairing',
104
105
  qr: update.qr,
106
+ qrCodeUrl,
105
107
  qrDataUrl,
106
108
  updatedAt: new Date().toISOString(),
107
109
  lastSeenAt: new Date().toISOString(),
@@ -128,6 +130,7 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
128
130
  status: 'connected',
129
131
  phone: socket.user?.id,
130
132
  qr: undefined,
133
+ qrCodeUrl: undefined,
131
134
  qrDataUrl: undefined,
132
135
  pairingCode: undefined,
133
136
  updatedAt: new Date().toISOString(),
@@ -216,6 +219,17 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
216
219
  this.sockets.set(sessionId, socket);
217
220
  return (await this.getSession(access, sessionId)) ?? starting;
218
221
  }
222
+ async ensureConnectedSession(_storageRoot, access, input, options) {
223
+ await this.ensureSession(_storageRoot, access, input);
224
+ const current = await this.getSession(access, input.sessionId);
225
+ if (!current) {
226
+ throw new Error(`Unknown session ${input.sessionId}`);
227
+ }
228
+ if (!this.sockets.has(input.sessionId) && current.status !== 'connected') {
229
+ throw new Error(`Session ${input.sessionId} is not active. Run Connect Session first and keep the n8n runtime alive until pairing completes.`);
230
+ }
231
+ return this.waitForSessionState(access, input.sessionId, options?.waitFor ?? 'pairing_or_connected', options?.timeoutMs ?? 20000);
232
+ }
219
233
  async listSessions(access) {
220
234
  const sessions = await access.listSessions();
221
235
  return sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
@@ -448,12 +462,71 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
448
462
  emitRuntime(event) {
449
463
  this.emit('event', event);
450
464
  }
465
+ async waitForSessionState(access, sessionId, waitFor, timeoutMs) {
466
+ const current = await this.getSession(access, sessionId);
467
+ if (!current) {
468
+ throw new Error(`Unknown session ${sessionId}`);
469
+ }
470
+ if (this.matchesWaitTarget(current, waitFor)) {
471
+ return current;
472
+ }
473
+ return new Promise((resolve) => {
474
+ let settled = false;
475
+ let timeout;
476
+ const cleanup = () => {
477
+ if (timeout)
478
+ clearTimeout(timeout);
479
+ this.off('event', handler);
480
+ };
481
+ const settle = async () => {
482
+ if (settled)
483
+ return;
484
+ settled = true;
485
+ cleanup();
486
+ resolve((await this.getSession(access, sessionId)) ?? current);
487
+ };
488
+ const handler = async (event) => {
489
+ if (event.sessionId !== sessionId)
490
+ return;
491
+ const latest = await this.getSession(access, sessionId);
492
+ if (latest && this.matchesWaitTarget(latest, waitFor)) {
493
+ await settle();
494
+ }
495
+ };
496
+ this.on('event', handler);
497
+ timeout = setTimeout(() => {
498
+ void settle();
499
+ }, timeoutMs);
500
+ });
501
+ }
502
+ matchesWaitTarget(record, waitFor) {
503
+ if (record.status === 'connected') {
504
+ return true;
505
+ }
506
+ if (waitFor === 'pairing_or_connected' && record.status === 'pairing') {
507
+ return true;
508
+ }
509
+ return false;
510
+ }
511
+ async waitForConnectedSession(access, sessionId, timeoutMs) {
512
+ const current = await this.getSession(access, sessionId);
513
+ if (!current) {
514
+ throw new Error(`Unknown session ${sessionId}`);
515
+ }
516
+ if (!this.sockets.has(sessionId) && current.status !== 'connected') {
517
+ throw new Error(`Session ${sessionId} is not active. Run Connect Session first and keep the n8n runtime alive until pairing completes.`);
518
+ }
519
+ return this.waitForSessionState(access, sessionId, 'connected', timeoutMs);
520
+ }
451
521
  required(value, field) {
452
522
  if (value === undefined || value === null || value === '') {
453
523
  throw new Error(`Missing required field ${field}`);
454
524
  }
455
525
  return value;
456
526
  }
527
+ buildQrCodeUrl(qr) {
528
+ return `https://quickchart.io/qr?text=${encodeURIComponent(qr)}`;
529
+ }
457
530
  }
458
531
  exports.registry = new WhatsThatRegistry();
459
532
  function extractMessageText(message) {
@@ -7,6 +7,7 @@ export interface SessionRecord {
7
7
  phone?: string;
8
8
  pairingCode?: string;
9
9
  qr?: string;
10
+ qrCodeUrl?: string;
10
11
  qrDataUrl?: string;
11
12
  phoneNumberForPairing?: string;
12
13
  lastSeenAt: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jep182/n8n-nodes-whatsthat",
3
- "version": "0.3.0",
3
+ "version": "0.4.3",
4
4
  "description": "n8n community nodes with embedded Baileys runtime for multi-session chat automation",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",