n8n-nodes-signal-cli-rest-api 0.6.1 → 0.7.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
CHANGED
|
@@ -53,7 +53,7 @@ services:
|
|
|
53
53
|
ports:
|
|
54
54
|
- "8085:8080" # Change 8085 to available port if needed (e.g., 8003:8080)
|
|
55
55
|
volumes:
|
|
56
|
-
- /mnt/your-pool/signal-data:/home/.local/share/signal-cli # Replace /mnt/your-pool with your
|
|
56
|
+
- /mnt/your-pool/signal-data:/home/.local/share/signal-cli # Replace /mnt/your-pool with your path to signal data
|
|
57
57
|
# Additionally, for config: - /mnt/your-pool/signal-config:/etc/signal-cli-rest-api (if custom settings)
|
|
58
58
|
environment:
|
|
59
59
|
- MODE=json-rpc # Recommended for speed and resolves group reception issues
|
|
@@ -37,15 +37,9 @@ class Signal {
|
|
|
37
37
|
{
|
|
38
38
|
name: 'Messages: Send Message',
|
|
39
39
|
value: 'sendMessage',
|
|
40
|
-
description: 'Send a text message to a contact or group',
|
|
40
|
+
description: 'Send a text message to a contact or group, optionally with attachments',
|
|
41
41
|
action: 'Send a text message',
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
name: 'Messages: Send Attachment',
|
|
45
|
-
value: 'sendAttachment',
|
|
46
|
-
description: 'Send a file or image to a contact or group',
|
|
47
|
-
action: 'Send an attachment',
|
|
48
|
-
},
|
|
49
43
|
{
|
|
50
44
|
name: 'Messages: Send Reaction',
|
|
51
45
|
value: 'sendReaction',
|
|
@@ -120,11 +114,11 @@ class Signal {
|
|
|
120
114
|
type: 'string',
|
|
121
115
|
default: '',
|
|
122
116
|
placeholder: '+1234567890 or groupId',
|
|
123
|
-
description: 'Phone number or group ID to send the message,
|
|
117
|
+
description: 'Phone number or group ID to send the message, reaction, or typing indicator to',
|
|
124
118
|
required: true,
|
|
125
119
|
displayOptions: {
|
|
126
120
|
show: {
|
|
127
|
-
operation: ['sendMessage', '
|
|
121
|
+
operation: ['sendMessage', 'sendReaction', 'removeReaction', 'startTyping', 'stopTyping'],
|
|
128
122
|
},
|
|
129
123
|
},
|
|
130
124
|
},
|
|
@@ -136,23 +130,40 @@ class Signal {
|
|
|
136
130
|
description: 'The text message to send (optional for attachments)',
|
|
137
131
|
displayOptions: {
|
|
138
132
|
show: {
|
|
139
|
-
operation: ['sendMessage'
|
|
133
|
+
operation: ['sendMessage'],
|
|
140
134
|
},
|
|
141
135
|
},
|
|
142
136
|
},
|
|
143
137
|
{
|
|
144
|
-
displayName: '
|
|
145
|
-
name: '
|
|
146
|
-
type: '
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
displayName: 'Binary Fields',
|
|
139
|
+
name: 'binaryFields',
|
|
140
|
+
type: 'fixedCollection',
|
|
141
|
+
typeOptions: {
|
|
142
|
+
multipleValues: true,
|
|
143
|
+
},
|
|
144
|
+
default: {},
|
|
145
|
+
placeholder: 'Add Binary Field',
|
|
146
|
+
description: 'Binary fields for attachments (empty or invalid fields are ignored)',
|
|
151
147
|
displayOptions: {
|
|
152
148
|
show: {
|
|
153
|
-
operation: ['
|
|
149
|
+
operation: ['sendMessage'],
|
|
154
150
|
},
|
|
155
151
|
},
|
|
152
|
+
options: [
|
|
153
|
+
{
|
|
154
|
+
name: 'binaryFieldValues',
|
|
155
|
+
displayName: 'Binary Field',
|
|
156
|
+
values: [
|
|
157
|
+
{
|
|
158
|
+
displayName: 'Input Binary Field',
|
|
159
|
+
name: 'inputBinaryField',
|
|
160
|
+
type: 'string',
|
|
161
|
+
default: '',
|
|
162
|
+
description: 'Name of the binary field containing the file to send (e.g., data)',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
156
167
|
},
|
|
157
168
|
{
|
|
158
169
|
displayName: 'Group ID',
|
|
@@ -173,8 +184,7 @@ class Signal {
|
|
|
173
184
|
name: 'groupName',
|
|
174
185
|
type: 'string',
|
|
175
186
|
default: '',
|
|
176
|
-
description: 'Name of the
|
|
177
|
-
required: false,
|
|
187
|
+
description: 'Name of the group to create or update',
|
|
178
188
|
displayOptions: {
|
|
179
189
|
show: {
|
|
180
190
|
operation: ['createGroup', 'updateGroup'],
|
|
@@ -188,7 +198,6 @@ class Signal {
|
|
|
188
198
|
default: '',
|
|
189
199
|
placeholder: '+1234567890,+0987654321',
|
|
190
200
|
description: 'Comma-separated list of phone numbers to add to the group',
|
|
191
|
-
required: false,
|
|
192
201
|
displayOptions: {
|
|
193
202
|
show: {
|
|
194
203
|
operation: ['createGroup', 'updateGroup'],
|
|
@@ -272,7 +281,7 @@ class Signal {
|
|
|
272
281
|
description: 'Request timeout in seconds (set higher for Get Groups, e.g., 300)',
|
|
273
282
|
displayOptions: {
|
|
274
283
|
show: {
|
|
275
|
-
operation: ['sendMessage', '
|
|
284
|
+
operation: ['sendMessage', 'sendReaction', 'removeReaction', 'startTyping', 'stopTyping', 'getContacts', 'getGroups', 'createGroup', 'updateGroup', 'listAttachments', 'downloadAttachment', 'removeAttachment'],
|
|
276
285
|
},
|
|
277
286
|
},
|
|
278
287
|
typeOptions: {
|
|
@@ -295,10 +304,16 @@ class Signal {
|
|
|
295
304
|
this.logger.debug(`Signal: Starting execute for operation ${operation}, items length: ${items.length}`);
|
|
296
305
|
for (let i = 0; i < items.length; i++) {
|
|
297
306
|
const timeout = this.getNodeParameter('timeout', i, operation === 'getGroups' ? 300 : 60) * 1000;
|
|
307
|
+
const binaryFields = this.getNodeParameter('binaryFields', i, {});
|
|
308
|
+
const inputBinaryFields = binaryFields.binaryFieldValues
|
|
309
|
+
? binaryFields.binaryFieldValues
|
|
310
|
+
.map(value => value.inputBinaryField)
|
|
311
|
+
.filter(field => field.trim() !== '') // Filter out empty fields
|
|
312
|
+
: [];
|
|
313
|
+
this.logger.debug(`Signal: Input binary fields for item ${i}: ${JSON.stringify(inputBinaryFields)}`);
|
|
298
314
|
const params = {
|
|
299
315
|
recipient: this.getNodeParameter('recipient', i, ''),
|
|
300
316
|
message: this.getNodeParameter('message', i, ''),
|
|
301
|
-
attachmentUrl: this.getNodeParameter('attachmentUrl', i, ''),
|
|
302
317
|
groupId: this.getNodeParameter('groupId', i, ''),
|
|
303
318
|
groupName: this.getNodeParameter('groupName', i, ''),
|
|
304
319
|
groupMembers: this.getNodeParameter('groupMembers', i, ''),
|
|
@@ -306,6 +321,7 @@ class Signal {
|
|
|
306
321
|
targetAuthor: this.getNodeParameter('targetAuthor', i, ''),
|
|
307
322
|
targetSentTimestamp: this.getNodeParameter('targetSentTimestamp', i, 0),
|
|
308
323
|
attachmentId: this.getNodeParameter('attachmentId', i, ''),
|
|
324
|
+
inputBinaryFields,
|
|
309
325
|
timeout,
|
|
310
326
|
apiUrl,
|
|
311
327
|
apiToken,
|
|
@@ -313,7 +329,7 @@ class Signal {
|
|
|
313
329
|
};
|
|
314
330
|
try {
|
|
315
331
|
let result;
|
|
316
|
-
if (['sendMessage', '
|
|
332
|
+
if (['sendMessage', 'sendReaction', 'removeReaction', 'startTyping', 'stopTyping'].includes(operation)) {
|
|
317
333
|
result = await messages_1.executeMessagesOperation.call(this, operation, i, params);
|
|
318
334
|
}
|
|
319
335
|
else if (['listAttachments', 'downloadAttachment', 'removeAttachment'].includes(operation)) {
|
|
@@ -2,10 +2,10 @@ import { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
|
2
2
|
interface OperationParams {
|
|
3
3
|
recipient?: string;
|
|
4
4
|
message?: string;
|
|
5
|
-
attachmentUrl?: string;
|
|
6
5
|
emoji?: string;
|
|
7
6
|
targetAuthor?: string;
|
|
8
7
|
targetSentTimestamp?: number;
|
|
8
|
+
inputBinaryFields?: string[];
|
|
9
9
|
timeout: number;
|
|
10
10
|
apiUrl: string;
|
|
11
11
|
apiToken: string;
|
|
@@ -8,7 +8,7 @@ const n8n_workflow_1 = require("n8n-workflow");
|
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
async function executeMessagesOperation(operation, itemIndex, params) {
|
|
10
10
|
var _a, _b, _c, _d;
|
|
11
|
-
const { recipient, message,
|
|
11
|
+
const { recipient, message, emoji, targetAuthor, targetSentTimestamp, inputBinaryFields, timeout, apiUrl, apiToken, phoneNumber } = params;
|
|
12
12
|
const axiosConfig = {
|
|
13
13
|
headers: apiToken ? { Authorization: `Bearer ${apiToken}` } : {},
|
|
14
14
|
timeout,
|
|
@@ -27,23 +27,74 @@ async function executeMessagesOperation(operation, itemIndex, params) {
|
|
|
27
27
|
};
|
|
28
28
|
try {
|
|
29
29
|
if (operation === 'sendMessage') {
|
|
30
|
-
|
|
30
|
+
if (!recipient) {
|
|
31
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
32
|
+
message: 'Recipient is required for sending a message',
|
|
33
|
+
}, { itemIndex });
|
|
34
|
+
}
|
|
35
|
+
const body = {
|
|
31
36
|
message,
|
|
32
37
|
number: phoneNumber,
|
|
33
38
|
recipients: [recipient],
|
|
34
|
-
}
|
|
39
|
+
};
|
|
40
|
+
// Handle binary attachments if inputBinaryFields are specified and valid
|
|
41
|
+
if (inputBinaryFields && inputBinaryFields.length > 0) {
|
|
42
|
+
const binary = this.getInputData()[itemIndex].binary;
|
|
43
|
+
if (!binary) {
|
|
44
|
+
this.logger.debug(`Signal: No binary data for item ${itemIndex}, skipping attachments`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const base64Attachments = [];
|
|
48
|
+
for (const inputBinaryField of inputBinaryFields) {
|
|
49
|
+
if (!inputBinaryField || !binary[inputBinaryField]) {
|
|
50
|
+
this.logger.debug(`Signal: No binary data for field '${inputBinaryField}' in item ${itemIndex}, skipping`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const binaryData = binary[inputBinaryField];
|
|
54
|
+
// Skip if binary data is empty
|
|
55
|
+
if (!binaryData.data || binaryData.data.length === 0) {
|
|
56
|
+
this.logger.debug(`Signal: Binary data in field '${inputBinaryField}' is empty for item ${itemIndex}, skipping`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Check file size (Signal limit: 100MB)
|
|
60
|
+
const maxFileSizeBytes = 99 * 1024 * 1024; // 99MB to be safe
|
|
61
|
+
const binaryBuffer = Buffer.from(binaryData.data, 'base64');
|
|
62
|
+
if (binaryBuffer.length > maxFileSizeBytes) {
|
|
63
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
64
|
+
message: `File size exceeds Signal's 100MB limit (size: ${(binaryBuffer.length / (1024 * 1024)).toFixed(2)}MB). See https://support.signal.org/hc/en-us/articles/360007320391-What-kinds-of-files-can-I-send`,
|
|
65
|
+
}, { itemIndex });
|
|
66
|
+
}
|
|
67
|
+
// Convert binary data to base64
|
|
68
|
+
const base64Data = binaryBuffer.toString('base64');
|
|
69
|
+
const mimeType = binaryData.mimeType || 'application/octet-stream';
|
|
70
|
+
const fileName = binaryData.fileName || `attachment_${itemIndex}_${inputBinaryField}`;
|
|
71
|
+
// Use data URI format with MIME type and filename (without encoding)
|
|
72
|
+
const base64Attachment = `data:${mimeType};filename=${fileName};base64,${base64Data}`;
|
|
73
|
+
base64Attachments.push(base64Attachment);
|
|
74
|
+
this.logger.debug(`Signal: Added base64 attachment for item ${itemIndex}, field '${inputBinaryField}': ${fileName}, MIME: ${mimeType}, Size: ${binaryBuffer.length} bytes`);
|
|
75
|
+
this.logger.debug(`Signal: Attachment format: ${base64Attachment.substring(0, 100)}...`);
|
|
76
|
+
}
|
|
77
|
+
if (base64Attachments.length > 0) {
|
|
78
|
+
body.base64_attachments = base64Attachments;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.logger.debug(`Signal: No valid attachments for item ${itemIndex}, sending text only`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Use /v2/send if base64_attachments are present, otherwise /v1/send
|
|
86
|
+
const endpoint = body.base64_attachments ? `${apiUrl}/v2/send` : `${apiUrl}/v1/send`;
|
|
87
|
+
this.logger.debug(`Signal: Sending request to ${endpoint} with body: ${JSON.stringify(body, null, 2)}`);
|
|
88
|
+
const response = await retryRequest(() => axios_1.default.post(endpoint, body, axiosConfig));
|
|
89
|
+
this.logger.debug(`Signal: Response: ${JSON.stringify(response.data, null, 2)}`);
|
|
35
90
|
return { json: response.data || { status: 'Message sent' }, pairedItem: { item: itemIndex } };
|
|
36
91
|
}
|
|
37
|
-
else if (operation === 'sendAttachment') {
|
|
38
|
-
const response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/send`, {
|
|
39
|
-
message,
|
|
40
|
-
number: phoneNumber,
|
|
41
|
-
recipients: [recipient],
|
|
42
|
-
attachments: [attachmentUrl],
|
|
43
|
-
}, axiosConfig));
|
|
44
|
-
return { json: response.data || { status: 'Attachment sent' }, pairedItem: { item: itemIndex } };
|
|
45
|
-
}
|
|
46
92
|
else if (operation === 'sendReaction') {
|
|
93
|
+
if (!recipient) {
|
|
94
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
95
|
+
message: 'Recipient is required for sending a reaction',
|
|
96
|
+
}, { itemIndex });
|
|
97
|
+
}
|
|
47
98
|
const response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/reactions/${phoneNumber}`, {
|
|
48
99
|
reaction: emoji,
|
|
49
100
|
recipient,
|
|
@@ -53,6 +104,11 @@ async function executeMessagesOperation(operation, itemIndex, params) {
|
|
|
53
104
|
return { json: response.data || { status: 'Reaction sent' }, pairedItem: { item: itemIndex } };
|
|
54
105
|
}
|
|
55
106
|
else if (operation === 'removeReaction') {
|
|
107
|
+
if (!recipient) {
|
|
108
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
109
|
+
message: 'Recipient is required for removing a reaction',
|
|
110
|
+
}, { itemIndex });
|
|
111
|
+
}
|
|
56
112
|
const response = await retryRequest(() => axios_1.default.delete(`${apiUrl}/v1/reactions/${phoneNumber}`, {
|
|
57
113
|
...axiosConfig,
|
|
58
114
|
data: {
|
|
@@ -64,6 +120,11 @@ async function executeMessagesOperation(operation, itemIndex, params) {
|
|
|
64
120
|
return { json: response.data || { status: 'Reaction removed' }, pairedItem: { item: itemIndex } };
|
|
65
121
|
}
|
|
66
122
|
else if (operation === 'startTyping') {
|
|
123
|
+
if (!recipient) {
|
|
124
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
125
|
+
message: 'Recipient is required for starting typing indicator',
|
|
126
|
+
}, { itemIndex });
|
|
127
|
+
}
|
|
67
128
|
const response = await retryRequest(() => axios_1.default.put(`${apiUrl}/v1/typing-indicator/${phoneNumber}`, {
|
|
68
129
|
recipient,
|
|
69
130
|
action: "start",
|
|
@@ -80,6 +141,11 @@ async function executeMessagesOperation(operation, itemIndex, params) {
|
|
|
80
141
|
};
|
|
81
142
|
}
|
|
82
143
|
else if (operation === 'stopTyping') {
|
|
144
|
+
if (!recipient) {
|
|
145
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
146
|
+
message: 'Recipient is required for stopping typing indicator',
|
|
147
|
+
}, { itemIndex });
|
|
148
|
+
}
|
|
83
149
|
const response = await retryRequest(() => axios_1.default.put(`${apiUrl}/v1/typing-indicator/${phoneNumber}`, {
|
|
84
150
|
recipient,
|
|
85
151
|
action: "stop",
|