@jep182/n8n-nodes-whatsthat 0.4.2 → 0.5.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 +154 -80
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/dist/nodes/WhatsThat/WhatsThat.node.d.ts +10 -0
- package/dist/nodes/WhatsThat/WhatsThat.node.js +647 -0
- package/dist/nodes/WhatsThatSession/WhatsThatSession.node.js +15 -3
- package/dist/nodes/WhatsThatTrigger/WhatsThatTrigger.node.js +46 -2
- package/dist/shared/context.d.ts +2 -2
- package/dist/shared/runtime.d.ts +5 -1
- package/dist/shared/runtime.js +37 -7
- package/dist/shared/validation.d.ts +2 -0
- package/dist/shared/validation.js +12 -0
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,124 +1,198 @@
|
|
|
1
1
|
# @jep182/n8n-nodes-whatsthat
|
|
2
2
|
|
|
3
|
-
WhatsThat
|
|
3
|
+
WhatsThat lets you connect one or more WhatsApp numbers inside n8n, link chats or groups with simple names, send messages, and react to incoming events.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You will mainly use:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- discover chats and groups
|
|
10
|
-
- link chats/groups to friendly aliases
|
|
11
|
-
- send text, media, documents, reactions, contacts, locations, and polls
|
|
12
|
-
- receive inbound events through a trigger node
|
|
7
|
+
- `WhatsThat`
|
|
8
|
+
- `WhatsThat Trigger`
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
You also need one credential:
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
- `WhatsThat Runtime`
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
## Before You Start
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
- connect a session
|
|
22
|
-
- list sessions
|
|
23
|
-
- inspect session status
|
|
24
|
-
- disconnect a session
|
|
25
|
-
- remove a session
|
|
16
|
+
Create `WhatsThat Runtime` credentials and choose a persistent storage path, for example:
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
```text
|
|
19
|
+
/home/node/.n8n/whatsthat
|
|
20
|
+
```
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
- `qrCodeUrl`
|
|
31
|
-
- `qr`
|
|
32
|
-
- `qrDataUrl`
|
|
22
|
+
This folder is used to store session files and local metadata.
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
## How To Connect Your Number
|
|
35
25
|
|
|
36
|
-
|
|
26
|
+
1. Add a `WhatsThat` node.
|
|
27
|
+
2. Set `Resource` to `Session`.
|
|
28
|
+
3. Set `Operation` to `Connect Session`.
|
|
29
|
+
4. Fill in:
|
|
30
|
+
- `Session Name`: a stable internal name like `main-phone`
|
|
31
|
+
- `Display Name`: a friendly label like `Luca phone`
|
|
32
|
+
- `WhatsApp Number`: optional, only if you want a pairing code instead of relying only on QR
|
|
33
|
+
5. Run the node.
|
|
34
|
+
6. In the output, open `qrCodeUrl`.
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
- list linked aliases
|
|
40
|
-
- link a target to an alias
|
|
41
|
-
- unlink an alias
|
|
36
|
+
Open `qrCodeUrl` as the standard and recommended way to connect the number.
|
|
42
37
|
|
|
43
|
-
|
|
38
|
+
Important:
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
- open `qrCodeUrl` whenever possible
|
|
41
|
+
- do not rely on `pairingCode`
|
|
42
|
+
- `pairingCode` is not stable and may fail or stop working depending on the session state and device behavior
|
|
43
|
+
- `qrDataUrl` is available only if you specifically need the raw embedded QR image data
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
- image
|
|
49
|
-
- video
|
|
50
|
-
- audio
|
|
51
|
-
- document
|
|
52
|
-
- reaction
|
|
53
|
-
- location
|
|
54
|
-
- contact
|
|
55
|
-
- poll
|
|
45
|
+
Example:
|
|
56
46
|
|
|
57
|
-
|
|
47
|
+
```text
|
|
48
|
+
Session Name: main-phone
|
|
49
|
+
Display Name: Luca phone
|
|
50
|
+
WhatsApp Number: 393331234567
|
|
51
|
+
```
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
- file/document attachment
|
|
53
|
+
## How To Wait Until The Number Is Fully Connected
|
|
61
54
|
|
|
62
|
-
|
|
55
|
+
After `Connect Session`, add another `WhatsThat` node:
|
|
63
56
|
|
|
64
|
-
|
|
57
|
+
1. Set `Resource` to `Session`
|
|
58
|
+
2. Set `Operation` to `Wait Until Connect`
|
|
59
|
+
3. Use the same `Session Name`
|
|
60
|
+
4. Choose how many seconds to wait in `Timeout Seconds`
|
|
65
61
|
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
This second node waits for the already-started session to become fully connected.
|
|
63
|
+
|
|
64
|
+
## How To Link A Group Or Chat Manually
|
|
65
|
+
|
|
66
|
+
1. Add a `WhatsThat` node
|
|
67
|
+
2. Set `Resource` to `Linked Chat`
|
|
68
|
+
3. Start with `Operation = List Discovered Chats`
|
|
69
|
+
4. Pick the chat or group you want to use
|
|
70
|
+
5. Change to `Operation = Link Chat`
|
|
71
|
+
6. Fill in:
|
|
72
|
+
- `Session Name`
|
|
73
|
+
- `Chat JID`
|
|
74
|
+
- `Linked Chat`: the simple name you want to use later, for example `support` or `team`
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
Example:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
Linked Chat: support
|
|
80
|
+
```
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
After that, you can send messages by choosing that linked chat instead of writing the raw JID every time.
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
- session metadata and linked targets are stored as local JSON files under the runtime storage path
|
|
84
|
+
## How To Link A Group Or Chat From WhatsApp
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
Use `WhatsThat Trigger`.
|
|
80
87
|
|
|
81
|
-
1.
|
|
82
|
-
2.
|
|
88
|
+
1. Add a `WhatsThat Trigger` node
|
|
89
|
+
2. Select the same `Session Name`
|
|
90
|
+
3. Set `Event` to `Link Chat Command`
|
|
91
|
+
4. Leave the default command or change it
|
|
92
|
+
|
|
93
|
+
By default, users can send a message like:
|
|
83
94
|
|
|
84
95
|
```text
|
|
85
|
-
/
|
|
96
|
+
/link-whatsthat support
|
|
86
97
|
```
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
4. Choose `Ensure Session`, then provide:
|
|
90
|
-
- `Session ID (Internal)`: a stable unique ID such as `main-phone`
|
|
91
|
-
- `Label (Visible Name)`: a human-readable name such as `Luca personal phone`
|
|
92
|
-
- optional `Phone Number For Pairing`: full number with country code, digits only, without `00` or `+`
|
|
93
|
-
5. Set `Return When` to `Pairing Is Ready Or Connected` for first-time pairing.
|
|
94
|
-
6. Use the returned `pairingCode`, `qrCodeUrl`, or `qrDataUrl` to connect the device.
|
|
95
|
-
7. Use `WhatsThat Targets` to discover and link chats/groups.
|
|
96
|
-
8. Use `WhatsThat Message` to send messages by alias or raw JID.
|
|
99
|
+
If that message is sent inside a group or chat, WhatsThat links that conversation with alias `support`.
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
This is useful when you want users to self-register a group without opening n8n.
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
## How To Send A Message To A Linked Chat
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
1. Add a `WhatsThat` node
|
|
106
|
+
2. Set `Resource` to `Send Message`
|
|
107
|
+
3. Set `Session Name`
|
|
108
|
+
4. Set `Send Message To` to `Linked Chat`
|
|
109
|
+
5. Choose a linked chat from the dropdown
|
|
110
|
+
6. Choose `Message Type`
|
|
111
|
+
7. Write the message
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
Example:
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
```text
|
|
116
|
+
Send Message To: Linked Chat
|
|
117
|
+
Linked Chat: support
|
|
118
|
+
Message Type: Text
|
|
119
|
+
Message: Hello from n8n
|
|
120
|
+
```
|
|
108
121
|
|
|
109
|
-
##
|
|
122
|
+
## How To Send A Message To A Number
|
|
110
123
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
1. Add a `WhatsThat` node
|
|
125
|
+
2. Set `Resource` to `Send Message`
|
|
126
|
+
3. Set `Send Message To` to `WhatsApp Number`
|
|
127
|
+
4. Enter the number with country code, digits only, without `00` and without `+`
|
|
114
128
|
|
|
115
|
-
|
|
129
|
+
Example:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
WhatsApp Number: 393331234567
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## How To Send A Message To Yourself
|
|
136
|
+
|
|
137
|
+
1. Add a `WhatsThat` node
|
|
138
|
+
2. Set `Resource` to `Send Message`
|
|
139
|
+
3. Set `Send Message To` to `Yourself`
|
|
116
140
|
|
|
117
|
-
|
|
141
|
+
WhatsThat uses the number already connected for that session.
|
|
118
142
|
|
|
119
|
-
|
|
143
|
+
This is useful for testing.
|
|
144
|
+
|
|
145
|
+
## How To Send Media
|
|
146
|
+
|
|
147
|
+
For images and videos you can choose:
|
|
148
|
+
|
|
149
|
+
- `Native Media`
|
|
150
|
+
- `As File`
|
|
151
|
+
|
|
152
|
+
Use `Media URL` for the file you want to send.
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
|
|
156
|
+
- send an image preview normally
|
|
157
|
+
- send a PDF as a document
|
|
158
|
+
- send a video as a file attachment
|
|
159
|
+
|
|
160
|
+
## How To Receive Events
|
|
161
|
+
|
|
162
|
+
Use `WhatsThat Trigger` when you want to react to:
|
|
163
|
+
|
|
164
|
+
- incoming messages
|
|
165
|
+
- your own sent messages
|
|
166
|
+
- pairing events
|
|
167
|
+
- connection events
|
|
168
|
+
- group updates
|
|
169
|
+
|
|
170
|
+
Common examples:
|
|
171
|
+
|
|
172
|
+
- start a workflow when a message arrives
|
|
173
|
+
- auto-link a chat with `/link-whatsthat support`
|
|
174
|
+
- continue a workflow when a session becomes connected
|
|
175
|
+
|
|
176
|
+
## Example Workflow
|
|
177
|
+
|
|
178
|
+
You can import this example:
|
|
179
|
+
|
|
180
|
+
- [`examples/register-number.workflow.json`](./examples/register-number.workflow.json)
|
|
181
|
+
|
|
182
|
+
It shows the basic flow:
|
|
183
|
+
|
|
184
|
+
1. `Connect Session`
|
|
185
|
+
2. `Wait Until Connect`
|
|
186
|
+
|
|
187
|
+
## Notes
|
|
188
|
+
|
|
189
|
+
- Use one persistent n8n instance as the owner of these sessions
|
|
190
|
+
- Keep the runtime storage path persistent
|
|
191
|
+
- If n8n restarts, active in-memory sockets are restarted from the saved session files
|
|
192
|
+
|
|
193
|
+
## Thanks
|
|
120
194
|
|
|
121
|
-
|
|
195
|
+
This project uses [Baileys](https://github.com/WhiskeySockets/Baileys).
|
|
122
196
|
|
|
123
197
|
## License
|
|
124
198
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
export * from './credentials/WhatsThatRuntime.credentials';
|
|
2
|
-
export * from './nodes/
|
|
3
|
-
export * from './nodes/WhatsThatTargets/WhatsThatTargets.node';
|
|
4
|
-
export * from './nodes/WhatsThatMessage/WhatsThatMessage.node';
|
|
2
|
+
export * from './nodes/WhatsThat/WhatsThat.node';
|
|
5
3
|
export * from './nodes/WhatsThatTrigger/WhatsThatTrigger.node';
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./credentials/WhatsThatRuntime.credentials"), exports);
|
|
18
|
-
__exportStar(require("./nodes/
|
|
19
|
-
__exportStar(require("./nodes/WhatsThatTargets/WhatsThatTargets.node"), exports);
|
|
20
|
-
__exportStar(require("./nodes/WhatsThatMessage/WhatsThatMessage.node"), exports);
|
|
18
|
+
__exportStar(require("./nodes/WhatsThat/WhatsThat.node"), exports);
|
|
21
19
|
__exportStar(require("./nodes/WhatsThatTrigger/WhatsThatTrigger.node"), exports);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class WhatsThat implements INodeType {
|
|
3
|
+
methods: {
|
|
4
|
+
loadOptions: {
|
|
5
|
+
getLinkedAliases(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
description: INodeTypeDescription;
|
|
9
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WhatsThat = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const access_1 = require("../../shared/access");
|
|
6
|
+
const runtime_1 = require("../../shared/runtime");
|
|
7
|
+
const validation_1 = require("../../shared/validation");
|
|
8
|
+
function buildPayload(context, itemIndex) {
|
|
9
|
+
const messageType = context.getNodeParameter('messageType', itemIndex);
|
|
10
|
+
const sendMessageTo = context.getNodeParameter('sendMessageTo', itemIndex);
|
|
11
|
+
const deliveryMode = context.getNodeParameter('deliveryMode', itemIndex, 'native');
|
|
12
|
+
const payload = {
|
|
13
|
+
sessionId: (0, validation_1.requireSessionId)(context.getNodeParameter('sessionId', itemIndex)),
|
|
14
|
+
type: messageType,
|
|
15
|
+
};
|
|
16
|
+
if (sendMessageTo === 'linkedChat') {
|
|
17
|
+
payload.channelAlias = context.getNodeParameter('linkedAlias', itemIndex);
|
|
18
|
+
}
|
|
19
|
+
else if (sendMessageTo === 'jid') {
|
|
20
|
+
payload.jid = context.getNodeParameter('jid', itemIndex);
|
|
21
|
+
}
|
|
22
|
+
else if (sendMessageTo === 'number') {
|
|
23
|
+
payload.phoneNumber = (0, validation_1.requireWhatsappNumber)(context.getNodeParameter('phoneNumber', itemIndex));
|
|
24
|
+
}
|
|
25
|
+
else if (sendMessageTo === 'yourself') {
|
|
26
|
+
payload.sendToSelf = true;
|
|
27
|
+
}
|
|
28
|
+
payload.message = context.getNodeParameter('message', itemIndex, '');
|
|
29
|
+
payload.replyToMessageId = context.getNodeParameter('replyToMessageId', itemIndex, '');
|
|
30
|
+
if (['image', 'video', 'audio', 'document'].includes(messageType)) {
|
|
31
|
+
payload.mediaUrl = context.getNodeParameter('mediaUrl', itemIndex);
|
|
32
|
+
payload.mimetype = context.getNodeParameter('mimetype', itemIndex, '');
|
|
33
|
+
payload.fileName = context.getNodeParameter('fileName', itemIndex, '');
|
|
34
|
+
}
|
|
35
|
+
if (['image', 'video'].includes(messageType)) {
|
|
36
|
+
payload.sendAsDocument = deliveryMode === 'document';
|
|
37
|
+
}
|
|
38
|
+
if (['image', 'video', 'document'].includes(messageType)) {
|
|
39
|
+
payload.caption = context.getNodeParameter('caption', itemIndex, '');
|
|
40
|
+
}
|
|
41
|
+
if (messageType === 'reaction') {
|
|
42
|
+
payload.reactionText = context.getNodeParameter('reactionText', itemIndex, '👍');
|
|
43
|
+
}
|
|
44
|
+
if (messageType === 'location') {
|
|
45
|
+
payload.location = {
|
|
46
|
+
degreesLatitude: context.getNodeParameter('latitude', itemIndex),
|
|
47
|
+
degreesLongitude: context.getNodeParameter('longitude', itemIndex),
|
|
48
|
+
name: context.getNodeParameter('locationName', itemIndex, ''),
|
|
49
|
+
address: context.getNodeParameter('locationAddress', itemIndex, ''),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (messageType === 'contact') {
|
|
53
|
+
payload.contact = {
|
|
54
|
+
displayName: context.getNodeParameter('contactName', itemIndex),
|
|
55
|
+
vcard: context.getNodeParameter('contactVcard', itemIndex),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (messageType === 'poll') {
|
|
59
|
+
const raw = context.getNodeParameter('pollOptions', itemIndex);
|
|
60
|
+
payload.poll = {
|
|
61
|
+
name: context.getNodeParameter('pollName', itemIndex),
|
|
62
|
+
values: raw.split(',').map((value) => value.trim()).filter(Boolean),
|
|
63
|
+
selectableCount: context.getNodeParameter('pollSelectableCount', itemIndex, 1),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return payload;
|
|
67
|
+
}
|
|
68
|
+
function formatSessionOutput(value) {
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
return value.map((item) => formatSessionOutput(item));
|
|
71
|
+
}
|
|
72
|
+
if (!value || typeof value !== 'object') {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
const record = value;
|
|
76
|
+
if (!('sessionId' in record) || !('status' in record)) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
qrCodeUrl: record.qrCodeUrl,
|
|
81
|
+
sessionId: record.sessionId,
|
|
82
|
+
status: record.status,
|
|
83
|
+
pairingCode: record.pairingCode,
|
|
84
|
+
qr: record.qr,
|
|
85
|
+
qrDataUrl: record.qrDataUrl,
|
|
86
|
+
phone: record.phone,
|
|
87
|
+
label: record.label,
|
|
88
|
+
phoneNumberForPairing: record.phoneNumberForPairing,
|
|
89
|
+
lastDisconnectReason: record.lastDisconnectReason,
|
|
90
|
+
createdAt: record.createdAt,
|
|
91
|
+
updatedAt: record.updatedAt,
|
|
92
|
+
lastSeenAt: record.lastSeenAt,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
class WhatsThat {
|
|
96
|
+
constructor() {
|
|
97
|
+
this.methods = {
|
|
98
|
+
loadOptions: {
|
|
99
|
+
async getLinkedAliases() {
|
|
100
|
+
const access = await (0, access_1.buildAccess)(this);
|
|
101
|
+
const sessionId = (0, validation_1.requireSessionId)(this.getNodeParameter('sessionId'));
|
|
102
|
+
const linked = await runtime_1.registry.listLinkedTargets(access, sessionId);
|
|
103
|
+
return linked.map((item) => ({
|
|
104
|
+
name: `${item.alias} (${item.displayName})`,
|
|
105
|
+
value: item.alias,
|
|
106
|
+
}));
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
this.description = {
|
|
111
|
+
displayName: 'WhatsThat',
|
|
112
|
+
name: 'whatsThat',
|
|
113
|
+
icon: 'file:../WhatsThatSession/whatsthat.svg',
|
|
114
|
+
group: ['transform'],
|
|
115
|
+
version: 1,
|
|
116
|
+
description: 'Manage sessions, chat links, and messages for WhatsThat',
|
|
117
|
+
defaults: { name: 'WhatsThat' },
|
|
118
|
+
inputs: ['main'],
|
|
119
|
+
outputs: ['main'],
|
|
120
|
+
credentials: [{ name: 'whatsThatRuntime', required: true }],
|
|
121
|
+
properties: [
|
|
122
|
+
{
|
|
123
|
+
displayName: 'Resource',
|
|
124
|
+
name: 'resource',
|
|
125
|
+
type: 'options',
|
|
126
|
+
default: 'session',
|
|
127
|
+
noDataExpression: true,
|
|
128
|
+
options: [
|
|
129
|
+
{ name: 'Session', value: 'session' },
|
|
130
|
+
{ name: 'Linked Chat', value: 'linkChat' },
|
|
131
|
+
{ name: 'Send Message', value: 'sendMessage' },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
displayName: 'Operation',
|
|
136
|
+
name: 'sessionOperation',
|
|
137
|
+
type: 'options',
|
|
138
|
+
default: 'connect',
|
|
139
|
+
displayOptions: {
|
|
140
|
+
show: { resource: ['session'] },
|
|
141
|
+
},
|
|
142
|
+
options: [
|
|
143
|
+
{ name: 'Connect Session', value: 'connect' },
|
|
144
|
+
{ name: 'Wait Until Connect', value: 'ensure' },
|
|
145
|
+
{ name: 'List Sessions', value: 'list' },
|
|
146
|
+
{ name: 'Get Session', value: 'status' },
|
|
147
|
+
{ name: 'Disconnect Session', value: 'disconnect' },
|
|
148
|
+
{ name: 'Remove Session', value: 'remove' },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
displayName: 'Operation',
|
|
153
|
+
name: 'linkChatOperation',
|
|
154
|
+
type: 'options',
|
|
155
|
+
default: 'listDiscovered',
|
|
156
|
+
displayOptions: {
|
|
157
|
+
show: { resource: ['linkChat'] },
|
|
158
|
+
},
|
|
159
|
+
options: [
|
|
160
|
+
{ name: 'List Discovered Chats', value: 'listDiscovered' },
|
|
161
|
+
{ name: 'List Linked Chats', value: 'listLinked' },
|
|
162
|
+
{ name: 'Link Chat', value: 'link' },
|
|
163
|
+
{ name: 'Unlink Chat', value: 'unlink' },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
displayName: 'Session Name',
|
|
168
|
+
name: 'sessionId',
|
|
169
|
+
type: 'string',
|
|
170
|
+
default: '',
|
|
171
|
+
required: true,
|
|
172
|
+
description: 'Required unique ID for this session. Use a stable internal value such as "main-phone" or "support-team".',
|
|
173
|
+
displayOptions: {
|
|
174
|
+
hide: {
|
|
175
|
+
resource: ['session'],
|
|
176
|
+
sessionOperation: ['list'],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
displayName: 'Display Name',
|
|
182
|
+
name: 'label',
|
|
183
|
+
type: 'string',
|
|
184
|
+
default: '',
|
|
185
|
+
description: 'Human-readable name shown in results. Example: "Luca personal phone" or "Support number".',
|
|
186
|
+
displayOptions: {
|
|
187
|
+
show: {
|
|
188
|
+
resource: ['session'],
|
|
189
|
+
sessionOperation: ['connect'],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
displayName: 'WhatsApp Number',
|
|
195
|
+
name: 'phoneNumberForPairing',
|
|
196
|
+
type: 'string',
|
|
197
|
+
default: '',
|
|
198
|
+
description: 'Optional. Full phone number with country code, digits only, without 00 or +. Example: 393331234567.',
|
|
199
|
+
displayOptions: {
|
|
200
|
+
show: {
|
|
201
|
+
resource: ['session'],
|
|
202
|
+
sessionOperation: ['connect'],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
displayName: 'Timeout Seconds',
|
|
208
|
+
name: 'timeoutSeconds',
|
|
209
|
+
type: 'number',
|
|
210
|
+
default: 300,
|
|
211
|
+
typeOptions: {
|
|
212
|
+
minValue: 1,
|
|
213
|
+
},
|
|
214
|
+
description: 'Maximum time to wait before returning the latest known session status.',
|
|
215
|
+
displayOptions: {
|
|
216
|
+
show: {
|
|
217
|
+
resource: ['session'],
|
|
218
|
+
sessionOperation: ['ensure'],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
displayName: 'Chat JID',
|
|
224
|
+
name: 'linkJid',
|
|
225
|
+
type: 'string',
|
|
226
|
+
default: '',
|
|
227
|
+
description: 'The raw WhatsApp JID to link. Usually taken from List Discovered Chats.',
|
|
228
|
+
displayOptions: {
|
|
229
|
+
show: {
|
|
230
|
+
resource: ['linkChat'],
|
|
231
|
+
linkChatOperation: ['link'],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
displayName: 'Linked Chat',
|
|
237
|
+
name: 'linkAlias',
|
|
238
|
+
type: 'string',
|
|
239
|
+
default: '',
|
|
240
|
+
description: 'Friendly name you will use later when sending messages by alias.',
|
|
241
|
+
displayOptions: {
|
|
242
|
+
show: {
|
|
243
|
+
resource: ['linkChat'],
|
|
244
|
+
linkChatOperation: ['link', 'unlink'],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
displayName: 'Send Message To',
|
|
250
|
+
name: 'sendMessageTo',
|
|
251
|
+
type: 'options',
|
|
252
|
+
default: 'linkedChat',
|
|
253
|
+
description: 'Choose how to resolve the destination chat.',
|
|
254
|
+
options: [
|
|
255
|
+
{ name: 'Linked Chat', value: 'linkedChat' },
|
|
256
|
+
{ name: 'WhatsApp Number', value: 'number' },
|
|
257
|
+
{ name: 'Raw JID', value: 'jid' },
|
|
258
|
+
{ name: 'Yourself', value: 'yourself' },
|
|
259
|
+
],
|
|
260
|
+
displayOptions: {
|
|
261
|
+
show: { resource: ['sendMessage'] },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
displayName: 'Linked Chat',
|
|
266
|
+
name: 'linkedAlias',
|
|
267
|
+
type: 'options',
|
|
268
|
+
default: '',
|
|
269
|
+
description: 'Choose one of the linked chats saved for this session.',
|
|
270
|
+
typeOptions: {
|
|
271
|
+
loadOptionsMethod: 'getLinkedAliases',
|
|
272
|
+
},
|
|
273
|
+
displayOptions: {
|
|
274
|
+
show: {
|
|
275
|
+
resource: ['sendMessage'],
|
|
276
|
+
sendMessageTo: ['linkedChat'],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
displayName: 'WhatsApp Number',
|
|
282
|
+
name: 'phoneNumber',
|
|
283
|
+
type: 'string',
|
|
284
|
+
default: '',
|
|
285
|
+
description: 'Full phone number with country code, digits only, without 00 or +. Example: 393331234567.',
|
|
286
|
+
displayOptions: {
|
|
287
|
+
show: {
|
|
288
|
+
resource: ['sendMessage'],
|
|
289
|
+
sendMessageTo: ['number'],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
displayName: 'JID',
|
|
295
|
+
name: 'jid',
|
|
296
|
+
type: 'string',
|
|
297
|
+
default: '',
|
|
298
|
+
description: 'Raw WhatsApp JID, for example 393331234567@s.whatsapp.net or a group JID.',
|
|
299
|
+
displayOptions: {
|
|
300
|
+
show: {
|
|
301
|
+
resource: ['sendMessage'],
|
|
302
|
+
sendMessageTo: ['jid'],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
displayName: 'Yourself',
|
|
308
|
+
name: 'yourselfNotice',
|
|
309
|
+
type: 'notice',
|
|
310
|
+
default: '',
|
|
311
|
+
description: 'Send the message to the WhatsApp number already connected for this session.',
|
|
312
|
+
displayOptions: {
|
|
313
|
+
show: {
|
|
314
|
+
resource: ['sendMessage'],
|
|
315
|
+
sendMessageTo: ['yourself'],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
displayName: 'Message Type',
|
|
321
|
+
name: 'messageType',
|
|
322
|
+
type: 'options',
|
|
323
|
+
default: 'text',
|
|
324
|
+
description: 'The kind of outbound message to send.',
|
|
325
|
+
displayOptions: {
|
|
326
|
+
show: { resource: ['sendMessage'] },
|
|
327
|
+
},
|
|
328
|
+
options: [
|
|
329
|
+
{ name: 'Text', value: 'text' },
|
|
330
|
+
{ name: 'Image', value: 'image' },
|
|
331
|
+
{ name: 'Video', value: 'video' },
|
|
332
|
+
{ name: 'Audio', value: 'audio' },
|
|
333
|
+
{ name: 'Document', value: 'document' },
|
|
334
|
+
{ name: 'Reaction', value: 'reaction' },
|
|
335
|
+
{ name: 'Location', value: 'location' },
|
|
336
|
+
{ name: 'Contact', value: 'contact' },
|
|
337
|
+
{ name: 'Poll', value: 'poll' },
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
displayName: 'Delivery Mode',
|
|
342
|
+
name: 'deliveryMode',
|
|
343
|
+
type: 'options',
|
|
344
|
+
default: 'native',
|
|
345
|
+
displayOptions: {
|
|
346
|
+
show: {
|
|
347
|
+
resource: ['sendMessage'],
|
|
348
|
+
messageType: ['image', 'video'],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
options: [
|
|
352
|
+
{ name: 'Native Media', value: 'native' },
|
|
353
|
+
{ name: 'As File', value: 'document' },
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
displayName: 'Message',
|
|
358
|
+
name: 'message',
|
|
359
|
+
type: 'string',
|
|
360
|
+
typeOptions: { rows: 4 },
|
|
361
|
+
default: '',
|
|
362
|
+
description: 'Main text body for the outbound message.',
|
|
363
|
+
displayOptions: {
|
|
364
|
+
show: { resource: ['sendMessage'] },
|
|
365
|
+
hide: { messageType: ['location', 'contact', 'poll'] },
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
displayName: 'Media URL',
|
|
370
|
+
name: 'mediaUrl',
|
|
371
|
+
type: 'string',
|
|
372
|
+
default: '',
|
|
373
|
+
description: 'Public URL or reachable file URL for the media to send.',
|
|
374
|
+
displayOptions: {
|
|
375
|
+
show: {
|
|
376
|
+
resource: ['sendMessage'],
|
|
377
|
+
messageType: ['image', 'video', 'audio', 'document'],
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
displayName: 'Caption',
|
|
383
|
+
name: 'caption',
|
|
384
|
+
type: 'string',
|
|
385
|
+
default: '',
|
|
386
|
+
displayOptions: {
|
|
387
|
+
show: {
|
|
388
|
+
resource: ['sendMessage'],
|
|
389
|
+
messageType: ['image', 'video', 'document'],
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
displayName: 'Mimetype',
|
|
395
|
+
name: 'mimetype',
|
|
396
|
+
type: 'string',
|
|
397
|
+
default: '',
|
|
398
|
+
displayOptions: {
|
|
399
|
+
show: {
|
|
400
|
+
resource: ['sendMessage'],
|
|
401
|
+
messageType: ['image', 'video', 'audio', 'document'],
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
displayName: 'File Name',
|
|
407
|
+
name: 'fileName',
|
|
408
|
+
type: 'string',
|
|
409
|
+
default: '',
|
|
410
|
+
displayOptions: {
|
|
411
|
+
show: {
|
|
412
|
+
resource: ['sendMessage'],
|
|
413
|
+
messageType: ['image', 'video', 'document'],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
displayName: 'Reply To Message ID',
|
|
419
|
+
name: 'replyToMessageId',
|
|
420
|
+
type: 'string',
|
|
421
|
+
default: '',
|
|
422
|
+
description: 'Optional. Reply or react to a specific WhatsApp message ID.',
|
|
423
|
+
displayOptions: {
|
|
424
|
+
show: { resource: ['sendMessage'] },
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
displayName: 'Reaction Text',
|
|
429
|
+
name: 'reactionText',
|
|
430
|
+
type: 'string',
|
|
431
|
+
default: '👍',
|
|
432
|
+
displayOptions: {
|
|
433
|
+
show: {
|
|
434
|
+
resource: ['sendMessage'],
|
|
435
|
+
messageType: ['reaction'],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
displayName: 'Latitude',
|
|
441
|
+
name: 'latitude',
|
|
442
|
+
type: 'number',
|
|
443
|
+
default: 0,
|
|
444
|
+
displayOptions: {
|
|
445
|
+
show: {
|
|
446
|
+
resource: ['sendMessage'],
|
|
447
|
+
messageType: ['location'],
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
displayName: 'Longitude',
|
|
453
|
+
name: 'longitude',
|
|
454
|
+
type: 'number',
|
|
455
|
+
default: 0,
|
|
456
|
+
displayOptions: {
|
|
457
|
+
show: {
|
|
458
|
+
resource: ['sendMessage'],
|
|
459
|
+
messageType: ['location'],
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
displayName: 'Location Name',
|
|
465
|
+
name: 'locationName',
|
|
466
|
+
type: 'string',
|
|
467
|
+
default: '',
|
|
468
|
+
displayOptions: {
|
|
469
|
+
show: {
|
|
470
|
+
resource: ['sendMessage'],
|
|
471
|
+
messageType: ['location'],
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
displayName: 'Location Address',
|
|
477
|
+
name: 'locationAddress',
|
|
478
|
+
type: 'string',
|
|
479
|
+
default: '',
|
|
480
|
+
displayOptions: {
|
|
481
|
+
show: {
|
|
482
|
+
resource: ['sendMessage'],
|
|
483
|
+
messageType: ['location'],
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
displayName: 'Contact Name',
|
|
489
|
+
name: 'contactName',
|
|
490
|
+
type: 'string',
|
|
491
|
+
default: '',
|
|
492
|
+
displayOptions: {
|
|
493
|
+
show: {
|
|
494
|
+
resource: ['sendMessage'],
|
|
495
|
+
messageType: ['contact'],
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
displayName: 'Contact VCard',
|
|
501
|
+
name: 'contactVcard',
|
|
502
|
+
type: 'string',
|
|
503
|
+
typeOptions: { rows: 4 },
|
|
504
|
+
default: '',
|
|
505
|
+
displayOptions: {
|
|
506
|
+
show: {
|
|
507
|
+
resource: ['sendMessage'],
|
|
508
|
+
messageType: ['contact'],
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
displayName: 'Poll Name',
|
|
514
|
+
name: 'pollName',
|
|
515
|
+
type: 'string',
|
|
516
|
+
default: '',
|
|
517
|
+
displayOptions: {
|
|
518
|
+
show: {
|
|
519
|
+
resource: ['sendMessage'],
|
|
520
|
+
messageType: ['poll'],
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
displayName: 'Poll Options',
|
|
526
|
+
name: 'pollOptions',
|
|
527
|
+
type: 'string',
|
|
528
|
+
default: '',
|
|
529
|
+
displayOptions: {
|
|
530
|
+
show: {
|
|
531
|
+
resource: ['sendMessage'],
|
|
532
|
+
messageType: ['poll'],
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
displayName: 'Selectable Count',
|
|
538
|
+
name: 'pollSelectableCount',
|
|
539
|
+
type: 'number',
|
|
540
|
+
default: 1,
|
|
541
|
+
displayOptions: {
|
|
542
|
+
show: {
|
|
543
|
+
resource: ['sendMessage'],
|
|
544
|
+
messageType: ['poll'],
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async execute() {
|
|
552
|
+
const items = this.getInputData();
|
|
553
|
+
const returnData = [];
|
|
554
|
+
const access = await (0, access_1.buildAccess)(this);
|
|
555
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
556
|
+
try {
|
|
557
|
+
const resource = this.getNodeParameter('resource', itemIndex);
|
|
558
|
+
const rawSessionId = this.getNodeParameter('sessionId', itemIndex, '');
|
|
559
|
+
let json;
|
|
560
|
+
if (resource === 'session') {
|
|
561
|
+
const operation = this.getNodeParameter('sessionOperation', itemIndex);
|
|
562
|
+
const sessionId = operation === 'list' ? (0, validation_1.normalizeSessionId)(rawSessionId) : (0, validation_1.requireSessionId)(rawSessionId);
|
|
563
|
+
switch (operation) {
|
|
564
|
+
case 'connect': {
|
|
565
|
+
const label = this.getNodeParameter('label', itemIndex, '').trim();
|
|
566
|
+
const phoneNumberForPairing = this.getNodeParameter('phoneNumberForPairing', itemIndex, '').trim();
|
|
567
|
+
await runtime_1.registry.ensureSession(access.paths.root, access, {
|
|
568
|
+
sessionId,
|
|
569
|
+
label: label || sessionId,
|
|
570
|
+
phoneNumberForPairing,
|
|
571
|
+
});
|
|
572
|
+
json = await runtime_1.registry.connectSession(access.paths.root, access, sessionId);
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case 'ensure': {
|
|
576
|
+
const label = this.getNodeParameter('label', itemIndex, '').trim();
|
|
577
|
+
const phoneNumberForPairing = this.getNodeParameter('phoneNumberForPairing', itemIndex, '').trim();
|
|
578
|
+
const timeoutSeconds = this.getNodeParameter('timeoutSeconds', itemIndex, 300);
|
|
579
|
+
json = await runtime_1.registry.ensureConnectedSession(access.paths.root, access, {
|
|
580
|
+
sessionId,
|
|
581
|
+
label: label || sessionId,
|
|
582
|
+
phoneNumberForPairing,
|
|
583
|
+
}, {
|
|
584
|
+
waitFor: 'connected',
|
|
585
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
586
|
+
});
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case 'list':
|
|
590
|
+
json = await runtime_1.registry.listSessions(access);
|
|
591
|
+
break;
|
|
592
|
+
case 'status':
|
|
593
|
+
json = (await runtime_1.registry.getSession(access, sessionId)) ?? null;
|
|
594
|
+
break;
|
|
595
|
+
case 'disconnect':
|
|
596
|
+
json = (await runtime_1.registry.disconnectSession(access, sessionId)) ?? null;
|
|
597
|
+
break;
|
|
598
|
+
case 'remove':
|
|
599
|
+
json = { removed: await runtime_1.registry.removeSession(access, sessionId) };
|
|
600
|
+
break;
|
|
601
|
+
default:
|
|
602
|
+
throw new Error(`Unsupported session operation ${operation}`);
|
|
603
|
+
}
|
|
604
|
+
json = formatSessionOutput(json);
|
|
605
|
+
}
|
|
606
|
+
else if (resource === 'linkChat') {
|
|
607
|
+
const operation = this.getNodeParameter('linkChatOperation', itemIndex);
|
|
608
|
+
const sessionId = (0, validation_1.requireSessionId)(rawSessionId);
|
|
609
|
+
switch (operation) {
|
|
610
|
+
case 'listDiscovered':
|
|
611
|
+
json = await runtime_1.registry.listTargets(access, sessionId);
|
|
612
|
+
break;
|
|
613
|
+
case 'listLinked':
|
|
614
|
+
json = await runtime_1.registry.listLinkedTargets(access, sessionId);
|
|
615
|
+
break;
|
|
616
|
+
case 'link':
|
|
617
|
+
json = await runtime_1.registry.connectTarget(access, sessionId, this.getNodeParameter('linkAlias', itemIndex), this.getNodeParameter('linkJid', itemIndex));
|
|
618
|
+
break;
|
|
619
|
+
case 'unlink':
|
|
620
|
+
json = {
|
|
621
|
+
removed: await runtime_1.registry.unlinkTarget(access, sessionId, this.getNodeParameter('linkAlias', itemIndex)),
|
|
622
|
+
};
|
|
623
|
+
break;
|
|
624
|
+
default:
|
|
625
|
+
throw new Error(`Unsupported link chat operation ${operation}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else if (resource === 'sendMessage') {
|
|
629
|
+
json = await runtime_1.registry.sendMessage(access, buildPayload(this, itemIndex));
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
throw new Error(`Unsupported resource ${resource}`);
|
|
633
|
+
}
|
|
634
|
+
returnData.push({ json: json, pairedItem: itemIndex });
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
if (this.continueOnFail()) {
|
|
638
|
+
returnData.push({ json: { error: error.message }, pairedItem: itemIndex });
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return [returnData];
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
exports.WhatsThat = WhatsThat;
|
|
@@ -23,8 +23,9 @@ class WhatsThatSession {
|
|
|
23
23
|
displayName: 'Operation',
|
|
24
24
|
name: 'operation',
|
|
25
25
|
type: 'options',
|
|
26
|
-
default: '
|
|
26
|
+
default: 'connect',
|
|
27
27
|
options: [
|
|
28
|
+
{ name: 'Connect Session', value: 'connect' },
|
|
28
29
|
{ name: 'Ensure Session', value: 'ensure' },
|
|
29
30
|
{ name: 'List Sessions', value: 'list' },
|
|
30
31
|
{ name: 'Get Session Status', value: 'status' },
|
|
@@ -50,7 +51,7 @@ class WhatsThatSession {
|
|
|
50
51
|
default: '',
|
|
51
52
|
description: 'Human-readable name shown in results. Example: "Luca personal phone" or "Support number".',
|
|
52
53
|
displayOptions: {
|
|
53
|
-
show: { operation: ['ensure'] },
|
|
54
|
+
show: { operation: ['connect', 'ensure'] },
|
|
54
55
|
},
|
|
55
56
|
},
|
|
56
57
|
{
|
|
@@ -60,7 +61,7 @@ class WhatsThatSession {
|
|
|
60
61
|
default: '',
|
|
61
62
|
description: 'Optional. Full phone number with country code, digits only, without 00 or +. Example: 393331234567.',
|
|
62
63
|
displayOptions: {
|
|
63
|
-
show: { operation: ['ensure'] },
|
|
64
|
+
show: { operation: ['connect', 'ensure'] },
|
|
64
65
|
},
|
|
65
66
|
},
|
|
66
67
|
{
|
|
@@ -104,6 +105,17 @@ class WhatsThatSession {
|
|
|
104
105
|
const sessionId = operation === 'list' ? (0, validation_1.normalizeSessionId)(rawSessionId) : (0, validation_1.requireSessionId)(rawSessionId);
|
|
105
106
|
let json;
|
|
106
107
|
switch (operation) {
|
|
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, {
|
|
112
|
+
sessionId,
|
|
113
|
+
label: label || sessionId,
|
|
114
|
+
phoneNumberForPairing,
|
|
115
|
+
});
|
|
116
|
+
json = await runtime_1.registry.connectSession(access.paths.root, access, sessionId);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
107
119
|
case 'ensure': {
|
|
108
120
|
const label = this.getNodeParameter('label', itemIndex, '').trim();
|
|
109
121
|
const phoneNumberForPairing = this.getNodeParameter('phoneNumberForPairing', itemIndex, '').trim();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WhatsThatTrigger = void 0;
|
|
4
|
+
const access_1 = require("../../shared/access");
|
|
4
5
|
const runtime_1 = require("../../shared/runtime");
|
|
5
6
|
const validation_1 = require("../../shared/validation");
|
|
6
7
|
class WhatsThatTrigger {
|
|
@@ -23,7 +24,7 @@ class WhatsThatTrigger {
|
|
|
23
24
|
type: 'string',
|
|
24
25
|
default: '',
|
|
25
26
|
required: true,
|
|
26
|
-
description: 'The unique session ID
|
|
27
|
+
description: 'The unique session ID used in the WhatsThat node resource Session.',
|
|
27
28
|
},
|
|
28
29
|
{
|
|
29
30
|
displayName: 'Event',
|
|
@@ -31,6 +32,7 @@ class WhatsThatTrigger {
|
|
|
31
32
|
type: 'options',
|
|
32
33
|
default: 'message.received',
|
|
33
34
|
options: [
|
|
35
|
+
{ name: 'Link Chat Command', value: 'link.chat.command' },
|
|
34
36
|
{ name: 'Message Received', value: 'message.received' },
|
|
35
37
|
{ name: 'Message From Me', value: 'message.from_me' },
|
|
36
38
|
{ name: 'Message Sent', value: 'message.sent' },
|
|
@@ -41,6 +43,18 @@ class WhatsThatTrigger {
|
|
|
41
43
|
{ name: 'Group Participants', value: 'group.participants' },
|
|
42
44
|
{ name: 'Any Event', value: '*' }
|
|
43
45
|
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
displayName: 'Link Command',
|
|
49
|
+
name: 'linkCommand',
|
|
50
|
+
type: 'string',
|
|
51
|
+
default: '/link-whatsthat',
|
|
52
|
+
description: 'Users must send this command followed by a space and the alias in the chat to link. Example: /link-whatsthat support',
|
|
53
|
+
displayOptions: {
|
|
54
|
+
show: {
|
|
55
|
+
eventName: ['link.chat.command'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
44
58
|
}
|
|
45
59
|
],
|
|
46
60
|
};
|
|
@@ -48,9 +62,39 @@ class WhatsThatTrigger {
|
|
|
48
62
|
async trigger() {
|
|
49
63
|
const sessionId = (0, validation_1.requireSessionId)(this.getNodeParameter('sessionId'));
|
|
50
64
|
const eventName = this.getNodeParameter('eventName');
|
|
51
|
-
const
|
|
65
|
+
const linkCommand = this.getNodeParameter('linkCommand', '');
|
|
66
|
+
const access = await (0, access_1.buildAccess)(this);
|
|
67
|
+
const handler = async (event) => {
|
|
52
68
|
if (event.sessionId !== sessionId)
|
|
53
69
|
return;
|
|
70
|
+
if (eventName === 'link.chat.command') {
|
|
71
|
+
if (event.event !== 'message.received')
|
|
72
|
+
return;
|
|
73
|
+
const data = event.data;
|
|
74
|
+
const text = (0, runtime_1.extractMessageText)(data.message)?.trim();
|
|
75
|
+
const prefix = linkCommand.trim();
|
|
76
|
+
if (!text || !prefix || !text.startsWith(`${prefix} `))
|
|
77
|
+
return;
|
|
78
|
+
const alias = text.slice(prefix.length).trim();
|
|
79
|
+
const jid = data.remoteJid;
|
|
80
|
+
if (!alias || !jid)
|
|
81
|
+
return;
|
|
82
|
+
const linked = await runtime_1.registry.connectTarget(access, sessionId, alias, jid);
|
|
83
|
+
this.emit([
|
|
84
|
+
[
|
|
85
|
+
{
|
|
86
|
+
json: {
|
|
87
|
+
event: 'link.chat.command',
|
|
88
|
+
sessionId,
|
|
89
|
+
alias,
|
|
90
|
+
jid,
|
|
91
|
+
linked,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
]);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
54
98
|
if (eventName !== '*' && event.event !== eventName)
|
|
55
99
|
return;
|
|
56
100
|
this.emit([[{ json: event }]]);
|
package/dist/shared/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { IDataObject, IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
|
|
2
|
-
export type NodeContext = IExecuteFunctions | ILoadOptionsFunctions;
|
|
1
|
+
import type { IDataObject, IExecuteFunctions, ILoadOptionsFunctions, ITriggerFunctions } from 'n8n-workflow';
|
|
2
|
+
export type NodeContext = IExecuteFunctions | ILoadOptionsFunctions | ITriggerFunctions;
|
|
3
3
|
export interface RuntimeConfig {
|
|
4
4
|
storagePath: string;
|
|
5
5
|
}
|
package/dist/shared/runtime.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ type SendRequest = {
|
|
|
13
13
|
sessionId: string;
|
|
14
14
|
channelAlias?: string;
|
|
15
15
|
jid?: string;
|
|
16
|
+
phoneNumber?: string;
|
|
17
|
+
sendToSelf?: boolean;
|
|
16
18
|
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'reaction' | 'location' | 'contact' | 'poll';
|
|
17
19
|
sendAsDocument?: boolean;
|
|
18
20
|
message?: string;
|
|
@@ -46,7 +48,7 @@ declare class WhatsThatRegistry extends EventEmitter {
|
|
|
46
48
|
phoneNumberForPairing?: string;
|
|
47
49
|
}): Promise<SessionRecord>;
|
|
48
50
|
connectSession(storageRoot: string, access: DataAccess, sessionId: string): Promise<SessionRecord>;
|
|
49
|
-
ensureConnectedSession(
|
|
51
|
+
ensureConnectedSession(_storageRoot: string, access: DataAccess, input: {
|
|
50
52
|
sessionId: string;
|
|
51
53
|
label: string;
|
|
52
54
|
phoneNumberForPairing?: string;
|
|
@@ -64,6 +66,7 @@ declare class WhatsThatRegistry extends EventEmitter {
|
|
|
64
66
|
unlinkTarget(access: DataAccess, sessionId: string, alias: string): Promise<boolean>;
|
|
65
67
|
sendMessage(access: DataAccess, request: SendRequest): Promise<Record<string, unknown>>;
|
|
66
68
|
private buildContent;
|
|
69
|
+
private resolveTargetJid;
|
|
67
70
|
private syncGroups;
|
|
68
71
|
private rememberTarget;
|
|
69
72
|
private upsertSession;
|
|
@@ -71,6 +74,7 @@ declare class WhatsThatRegistry extends EventEmitter {
|
|
|
71
74
|
private emitRuntime;
|
|
72
75
|
private waitForSessionState;
|
|
73
76
|
private matchesWaitTarget;
|
|
77
|
+
waitForConnectedSession(access: DataAccess, sessionId: string, timeoutMs: number): Promise<SessionRecord>;
|
|
74
78
|
private required;
|
|
75
79
|
private buildQrCodeUrl;
|
|
76
80
|
}
|
package/dist/shared/runtime.js
CHANGED
|
@@ -219,14 +219,14 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
|
|
|
219
219
|
this.sockets.set(sessionId, socket);
|
|
220
220
|
return (await this.getSession(access, sessionId)) ?? starting;
|
|
221
221
|
}
|
|
222
|
-
async ensureConnectedSession(
|
|
223
|
-
await this.ensureSession(
|
|
222
|
+
async ensureConnectedSession(_storageRoot, access, input, options) {
|
|
223
|
+
await this.ensureSession(_storageRoot, access, input);
|
|
224
224
|
const current = await this.getSession(access, input.sessionId);
|
|
225
225
|
if (!current) {
|
|
226
226
|
throw new Error(`Unknown session ${input.sessionId}`);
|
|
227
227
|
}
|
|
228
228
|
if (!this.sockets.has(input.sessionId) && current.status !== 'connected') {
|
|
229
|
-
|
|
229
|
+
throw new Error(`Session ${input.sessionId} is not active. Run Connect Session first and keep the n8n runtime alive until pairing completes.`);
|
|
230
230
|
}
|
|
231
231
|
return this.waitForSessionState(access, input.sessionId, options?.waitFor ?? 'pairing_or_connected', options?.timeoutMs ?? 20000);
|
|
232
232
|
}
|
|
@@ -317,11 +317,9 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
|
|
|
317
317
|
if (!socket) {
|
|
318
318
|
throw new Error(`Session ${request.sessionId} is not connected`);
|
|
319
319
|
}
|
|
320
|
-
const
|
|
321
|
-
const targetJid = request.jid ??
|
|
322
|
-
linked.find((item) => item.alias === request.channelAlias)?.jid;
|
|
320
|
+
const targetJid = await this.resolveTargetJid(access, request, socket);
|
|
323
321
|
if (!targetJid) {
|
|
324
|
-
throw new Error('Unknown target. Use a linked
|
|
322
|
+
throw new Error('Unknown target. Use a linked chat, WhatsApp number, raw JID, or Yourself.');
|
|
325
323
|
}
|
|
326
324
|
const content = this.buildContent(request, targetJid);
|
|
327
325
|
const response = await socket.sendMessage(targetJid, content, {
|
|
@@ -419,6 +417,28 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
|
|
|
419
417
|
};
|
|
420
418
|
}
|
|
421
419
|
}
|
|
420
|
+
async resolveTargetJid(access, request, socket) {
|
|
421
|
+
if (request.jid) {
|
|
422
|
+
return request.jid;
|
|
423
|
+
}
|
|
424
|
+
if (request.phoneNumber) {
|
|
425
|
+
return `${request.phoneNumber}@s.whatsapp.net`;
|
|
426
|
+
}
|
|
427
|
+
if (request.sendToSelf) {
|
|
428
|
+
const session = await this.getSession(access, request.sessionId);
|
|
429
|
+
const raw = session?.phone ?? socket.user?.id;
|
|
430
|
+
const normalized = raw?.split(':')[0];
|
|
431
|
+
if (!normalized) {
|
|
432
|
+
throw new Error(`Session ${request.sessionId} does not have a known WhatsApp number yet. Connect the session first.`);
|
|
433
|
+
}
|
|
434
|
+
return normalized;
|
|
435
|
+
}
|
|
436
|
+
if (request.channelAlias) {
|
|
437
|
+
const linked = await this.listLinkedTargets(access, request.sessionId);
|
|
438
|
+
return linked.find((item) => item.alias === request.channelAlias)?.jid;
|
|
439
|
+
}
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
422
442
|
async syncGroups(access, sessionId, socket) {
|
|
423
443
|
const groups = await socket.groupFetchAllParticipating();
|
|
424
444
|
for (const [jid, data] of Object.entries(groups)) {
|
|
@@ -508,6 +528,16 @@ class WhatsThatRegistry extends node_events_1.EventEmitter {
|
|
|
508
528
|
}
|
|
509
529
|
return false;
|
|
510
530
|
}
|
|
531
|
+
async waitForConnectedSession(access, sessionId, timeoutMs) {
|
|
532
|
+
const current = await this.getSession(access, sessionId);
|
|
533
|
+
if (!current) {
|
|
534
|
+
throw new Error(`Unknown session ${sessionId}`);
|
|
535
|
+
}
|
|
536
|
+
if (!this.sockets.has(sessionId) && current.status !== 'connected') {
|
|
537
|
+
throw new Error(`Session ${sessionId} is not active. Run Connect Session first and keep the n8n runtime alive until pairing completes.`);
|
|
538
|
+
}
|
|
539
|
+
return this.waitForSessionState(access, sessionId, 'connected', timeoutMs);
|
|
540
|
+
}
|
|
511
541
|
required(value, field) {
|
|
512
542
|
if (value === undefined || value === null || value === '') {
|
|
513
543
|
throw new Error(`Missing required field ${field}`);
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function normalizeSessionId(sessionId: string): string;
|
|
2
2
|
export declare function requireSessionId(sessionId: string): string;
|
|
3
|
+
export declare function normalizeWhatsappNumber(phoneNumber: string): string;
|
|
4
|
+
export declare function requireWhatsappNumber(phoneNumber: string): string;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeSessionId = normalizeSessionId;
|
|
4
4
|
exports.requireSessionId = requireSessionId;
|
|
5
|
+
exports.normalizeWhatsappNumber = normalizeWhatsappNumber;
|
|
6
|
+
exports.requireWhatsappNumber = requireWhatsappNumber;
|
|
5
7
|
function normalizeSessionId(sessionId) {
|
|
6
8
|
return sessionId.trim();
|
|
7
9
|
}
|
|
@@ -12,3 +14,13 @@ function requireSessionId(sessionId) {
|
|
|
12
14
|
}
|
|
13
15
|
return normalized;
|
|
14
16
|
}
|
|
17
|
+
function normalizeWhatsappNumber(phoneNumber) {
|
|
18
|
+
return phoneNumber.trim().replace(/\s+/g, '').replace(/^\+/, '').replace(/^00/, '');
|
|
19
|
+
}
|
|
20
|
+
function requireWhatsappNumber(phoneNumber) {
|
|
21
|
+
const normalized = normalizeWhatsappNumber(phoneNumber);
|
|
22
|
+
if (!normalized || !/^\d+$/.test(normalized)) {
|
|
23
|
+
throw new Error('WhatsApp Number must contain digits only, including country code, without 00 or +.');
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jep182/n8n-nodes-whatsthat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -57,9 +57,7 @@
|
|
|
57
57
|
"dist/credentials/WhatsThatRuntime.credentials.js"
|
|
58
58
|
],
|
|
59
59
|
"nodes": [
|
|
60
|
-
"dist/nodes/
|
|
61
|
-
"dist/nodes/WhatsThatTargets/WhatsThatTargets.node.js",
|
|
62
|
-
"dist/nodes/WhatsThatMessage/WhatsThatMessage.node.js",
|
|
60
|
+
"dist/nodes/WhatsThat/WhatsThat.node.js",
|
|
63
61
|
"dist/nodes/WhatsThatTrigger/WhatsThatTrigger.node.js"
|
|
64
62
|
]
|
|
65
63
|
}
|