@jep182/n8n-nodes-whatsthat 0.4.3 → 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 -81
- 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/WhatsThatTrigger/WhatsThatTrigger.node.js +46 -2
- package/dist/shared/context.d.ts +2 -2
- package/dist/shared/runtime.d.ts +3 -0
- package/dist/shared/runtime.js +24 -4
- 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,125 +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
|
-
- wait for a session to become connected
|
|
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 `Connect 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. 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.
|
|
99
|
+
If that message is sent inside a group or chat, WhatsThat links that conversation with alias `support`.
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
This is useful when you want users to self-register a group without opening n8n.
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
## How To Send A Message To A Linked Chat
|
|
102
104
|
|
|
103
|
-
|
|
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
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
Example:
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
```text
|
|
116
|
+
Send Message To: Linked Chat
|
|
117
|
+
Linked Chat: support
|
|
118
|
+
Message Type: Text
|
|
119
|
+
Message: Hello from n8n
|
|
120
|
+
```
|
|
109
121
|
|
|
110
|
-
##
|
|
122
|
+
## How To Send A Message To A Number
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 `+`
|
|
115
128
|
|
|
116
|
-
|
|
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`
|
|
117
140
|
|
|
118
|
-
|
|
141
|
+
WhatsThat uses the number already connected for that session.
|
|
119
142
|
|
|
120
|
-
|
|
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
|
|
121
194
|
|
|
122
|
-
|
|
195
|
+
This project uses [Baileys](https://github.com/WhiskeySockets/Baileys).
|
|
123
196
|
|
|
124
197
|
## License
|
|
125
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;
|
|
@@ -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;
|
|
@@ -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;
|
package/dist/shared/runtime.js
CHANGED
|
@@ -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)) {
|
|
@@ -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
|
}
|