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 dataset/pool in TrueNAS
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, attachment, reaction, or typing indicator to',
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', 'sendAttachment', 'sendReaction', 'removeReaction', 'startTyping', 'stopTyping'],
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', 'sendAttachment'],
133
+ operation: ['sendMessage'],
140
134
  },
141
135
  },
142
136
  },
143
137
  {
144
- displayName: 'Attachment URL',
145
- name: 'attachmentUrl',
146
- type: 'string',
147
- default: '',
148
- placeholder: 'https://example.com/image.jpg',
149
- description: 'URL of the file or image to send (e.g., PNG, JPG, PDF, MP3 for voice notes)',
150
- required: true,
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: ['sendAttachment'],
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 new or updated group',
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', 'sendAttachment', 'sendReaction', 'removeReaction', 'startTyping', 'stopTyping', 'getContacts', 'getGroups', 'createGroup', 'updateGroup', 'listAttachments', 'downloadAttachment', 'removeAttachment'],
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', 'sendAttachment', 'sendReaction', 'removeReaction', 'startTyping', 'stopTyping'].includes(operation)) {
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, attachmentUrl, emoji, targetAuthor, targetSentTimestamp, timeout, apiUrl, apiToken, phoneNumber } = params;
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
- const response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/send`, {
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
- }, axiosConfig));
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-signal-cli-rest-api",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Signal Node for n8n using signal-cli-rest-api",
5
5
  "repository": {
6
6
  "type": "git",