n8n-nodes-signal-cli-rest-api 0.2.3 → 0.3.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.
@@ -5,6 +5,7 @@ const n8n_workflow_1 = require("n8n-workflow");
5
5
  const messages_1 = require("./messages");
6
6
  const groups_1 = require("./groups");
7
7
  const contacts_1 = require("./contacts");
8
+ const attachments_1 = require("./attachments");
8
9
  class Signal {
9
10
  constructor() {
10
11
  this.description = {
@@ -57,6 +58,24 @@ class Signal {
57
58
  description: 'Remove a reaction from a message',
58
59
  action: 'Remove a reaction',
59
60
  },
61
+ {
62
+ name: 'Attachments: List Attachments',
63
+ value: 'listAttachments',
64
+ description: 'List attachments for the account',
65
+ action: 'List attachments',
66
+ },
67
+ {
68
+ name: 'Attachments: Download Attachment',
69
+ value: 'downloadAttachment',
70
+ description: 'Download an attachment as binary file',
71
+ action: 'Download attachment',
72
+ },
73
+ {
74
+ name: 'Attachments: Remove Attachment',
75
+ value: 'removeAttachment',
76
+ description: 'Remove an attachment',
77
+ action: 'Remove attachment',
78
+ },
60
79
  {
61
80
  name: 'Contacts: Get Contacts',
62
81
  value: 'getContacts',
@@ -175,46 +194,16 @@ class Signal {
175
194
  allowCustom: true,
176
195
  },
177
196
  options: [
178
- {
179
- name: 'Thumbs Up',
180
- value: '👍',
181
- },
182
- {
183
- name: 'Heart',
184
- value: '❤️',
185
- },
186
- {
187
- name: 'Smile',
188
- value: '😄',
189
- },
190
- {
191
- name: 'Sad',
192
- value: '😢',
193
- },
194
- {
195
- name: 'Angry',
196
- value: '😣',
197
- },
198
- {
199
- name: 'Star',
200
- value: '⭐',
201
- },
202
- {
203
- name: 'Fire',
204
- value: '🔥',
205
- },
206
- {
207
- name: 'Plus',
208
- value: '➕',
209
- },
210
- {
211
- name: 'Minus',
212
- value: '➖',
213
- },
214
- {
215
- name: 'Handshake',
216
- value: '🤝',
217
- },
197
+ { name: 'Thumbs Up', value: '👍' },
198
+ { name: 'Heart', value: '❤️' },
199
+ { name: 'Smile', value: '😄' },
200
+ { name: 'Sad', value: '😢' },
201
+ { name: 'Angry', value: '😣' },
202
+ { name: 'Star', value: '⭐' },
203
+ { name: 'Fire', value: '🔥' },
204
+ { name: 'Plus', value: '➕' },
205
+ { name: 'Minus', value: '➖' },
206
+ { name: 'Handshake', value: '🤝' },
218
207
  ],
219
208
  displayOptions: {
220
209
  show: {
@@ -249,6 +238,20 @@ class Signal {
249
238
  },
250
239
  },
251
240
  },
241
+ {
242
+ displayName: 'Attachment ID',
243
+ name: 'attachmentId',
244
+ type: 'string',
245
+ default: '',
246
+ placeholder: 'attachment_id_from_trigger.png',
247
+ description: 'ID of the attachment to download or remove',
248
+ required: true,
249
+ displayOptions: {
250
+ show: {
251
+ operation: ['downloadAttachment', 'removeAttachment'],
252
+ },
253
+ },
254
+ },
252
255
  {
253
256
  displayName: 'Timeout (seconds)',
254
257
  name: 'timeout',
@@ -257,7 +260,7 @@ class Signal {
257
260
  description: 'Request timeout in seconds (set higher for Get Groups, e.g., 300)',
258
261
  displayOptions: {
259
262
  show: {
260
- operation: ['sendMessage', 'sendAttachment', 'sendReaction', 'removeReaction', 'getContacts', 'getGroups', 'createGroup', 'updateGroup'],
263
+ operation: ['sendMessage', 'sendAttachment', 'sendReaction', 'removeReaction', 'getContacts', 'getGroups', 'createGroup', 'updateGroup', 'listAttachments', 'downloadAttachment', 'removeAttachment'],
261
264
  },
262
265
  },
263
266
  typeOptions: {
@@ -277,7 +280,7 @@ class Signal {
277
280
  const apiUrl = credentials.apiUrl;
278
281
  const apiToken = credentials.apiToken;
279
282
  const phoneNumber = credentials.phoneNumber;
280
- this.logger.debug(`Signal: Starting execute for operation ${operation}, apiUrl: ${apiUrl}, items length: ${items.length}`);
283
+ this.logger.debug(`Signal: Starting execute for operation ${operation}, items length: ${items.length}`);
281
284
  for (let i = 0; i < items.length; i++) {
282
285
  const timeout = this.getNodeParameter('timeout', i, operation === 'getGroups' ? 300 : 60) * 1000;
283
286
  const params = {
@@ -290,6 +293,7 @@ class Signal {
290
293
  emoji: this.getNodeParameter('emoji', i, ''),
291
294
  targetAuthor: this.getNodeParameter('targetAuthor', i, ''),
292
295
  targetSentTimestamp: this.getNodeParameter('targetSentTimestamp', i, 0),
296
+ attachmentId: this.getNodeParameter('attachmentId', i, ''),
293
297
  timeout,
294
298
  apiUrl,
295
299
  apiToken,
@@ -300,6 +304,9 @@ class Signal {
300
304
  if (['sendMessage', 'sendAttachment', 'sendReaction', 'removeReaction'].includes(operation)) {
301
305
  result = await messages_1.executeMessagesOperation.call(this, operation, i, params);
302
306
  }
307
+ else if (['listAttachments', 'downloadAttachment', 'removeAttachment'].includes(operation)) {
308
+ result = await attachments_1.executeAttachmentsOperation.call(this, operation, i, params);
309
+ }
303
310
  else if (['getGroups', 'createGroup', 'updateGroup'].includes(operation)) {
304
311
  result = await groups_1.executeGroupsOperation.call(this, operation, i, params);
305
312
  }
@@ -309,7 +316,7 @@ class Signal {
309
316
  else {
310
317
  throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Unknown operation' });
311
318
  }
312
- this.logger.info(`Signal: Operation ${operation} result for item ${i}: ${JSON.stringify(result.json, null, 2)}`);
319
+ this.logger.info(`Signal: Operation ${operation} result for item ${i}: ${JSON.stringify(result.json || result.binary, null, 2)}`);
313
320
  returnData.push(result);
314
321
  }
315
322
  catch (error) {
@@ -35,25 +35,25 @@ class SignalTrigger {
35
35
  },
36
36
  },
37
37
  {
38
- displayName: 'Only With Text',
39
- name: 'onlyWithText',
38
+ displayName: 'Ignore Messages',
39
+ name: 'ignoreMessages',
40
40
  type: 'boolean',
41
- default: true,
42
- description: 'Retrieve only messages with text content',
41
+ default: false,
42
+ description: 'Enable to ignore messages with text content',
43
43
  },
44
44
  {
45
- displayName: 'Only With Attachments',
46
- name: 'onlyWithAttachments',
45
+ displayName: 'Ignore Attachments',
46
+ name: 'ignoreAttachments',
47
47
  type: 'boolean',
48
48
  default: false,
49
- description: 'Retrieve only messages with attachments',
49
+ description: 'Enable to ignore messages with attachments',
50
50
  },
51
51
  {
52
- displayName: 'Only With Reactions',
53
- name: 'onlyWithReactions',
52
+ displayName: 'Ignore Reactions',
53
+ name: 'ignoreReactions',
54
54
  type: 'boolean',
55
55
  default: false,
56
- description: 'Retrieve only messages with reactions',
56
+ description: 'Enable to ignore messages with reactions',
57
57
  },
58
58
  ],
59
59
  };
@@ -64,21 +64,25 @@ class SignalTrigger {
64
64
  const apiToken = credentials.apiToken;
65
65
  const phoneNumber = credentials.phoneNumber;
66
66
  const reconnectDelay = this.getNodeParameter('reconnectDelay', 0) * 1000;
67
- const onlyWithText = this.getNodeParameter('onlyWithText', 0);
68
- const onlyWithAttachments = this.getNodeParameter('onlyWithAttachments', 0);
69
- const onlyWithReactions = this.getNodeParameter('onlyWithReactions', 0);
67
+ const ignoreMessages = this.getNodeParameter('ignoreMessages', 0);
68
+ const ignoreAttachments = this.getNodeParameter('ignoreAttachments', 0);
69
+ const ignoreReactions = this.getNodeParameter('ignoreReactions', 0);
70
70
  const wsUrl = `${apiUrl.replace('http', 'ws')}/v1/receive/${phoneNumber}`;
71
+ this.logger.debug(`SignalTrigger: Attempting to connect to WS URL: ${wsUrl}`);
71
72
  const processedMessages = new Set();
72
73
  const maxMessages = 1000;
73
74
  const connectWebSocket = () => {
74
75
  const ws = new ws_1.WebSocket(wsUrl, {
75
76
  headers: apiToken ? { Authorization: `Bearer ${apiToken}` } : {},
76
77
  });
78
+ ws.on('open', () => {
79
+ this.logger.debug(`SignalTrigger: Successfully connected to ${wsUrl}`);
80
+ });
77
81
  ws.on('message', (data) => {
78
82
  var _a, _b, _c, _d, _e;
79
83
  try {
80
84
  const message = JSON.parse(data.toString());
81
- this.logger.debug(`SignalTrigger: Received message: ${JSON.stringify(message, null, 2)}`);
85
+ this.logger.debug(`SignalTrigger: Received raw message: ${JSON.stringify(message, null, 2)}`);
82
86
  const timestamp = ((_a = message.envelope) === null || _a === void 0 ? void 0 : _a.timestamp) || 0;
83
87
  if (processedMessages.has(timestamp)) {
84
88
  this.logger.debug(`SignalTrigger: Skipping duplicate message with timestamp ${timestamp}`);
@@ -97,6 +101,7 @@ class SignalTrigger {
97
101
  timestamp: timestamp,
98
102
  account: message.account || '',
99
103
  };
104
+ this.logger.debug(`SignalTrigger: Processed message content: ${JSON.stringify(processedMessage, null, 2)}`);
100
105
  // Ігнорувати події без вмісту
101
106
  if (!processedMessage.messageText &&
102
107
  processedMessage.attachments.length === 0 &&
@@ -104,11 +109,11 @@ class SignalTrigger {
104
109
  this.logger.debug(`SignalTrigger: Skipping empty message with timestamp ${timestamp}`);
105
110
  return;
106
111
  }
107
- // Фільтрація за параметрами
108
- if ((onlyWithText && !processedMessage.messageText) ||
109
- (onlyWithAttachments && processedMessage.attachments.length === 0) ||
110
- (onlyWithReactions && processedMessage.reactions.length === 0)) {
111
- this.logger.debug(`SignalTrigger: Skipping filtered message with timestamp ${timestamp}`);
112
+ // Фільтрація: ігнорувати, якщо увімкнено ignore і відповідний вміст присутній
113
+ if ((ignoreMessages && processedMessage.messageText) ||
114
+ (ignoreAttachments && processedMessage.attachments.length > 0) ||
115
+ (ignoreReactions && processedMessage.reactions.length > 0)) {
116
+ this.logger.debug(`SignalTrigger: Ignoring message with timestamp ${timestamp} due to filter`);
112
117
  return;
113
118
  }
114
119
  const returnData = {
@@ -134,7 +139,7 @@ class SignalTrigger {
134
139
  const ws = connectWebSocket();
135
140
  return new Promise((resolve, reject) => {
136
141
  ws.on('open', () => {
137
- this.logger.debug(`SignalTrigger: Connected to ${wsUrl}`);
142
+ this.logger.debug(`SignalTrigger: Initial connection to ${wsUrl}`);
138
143
  resolve({
139
144
  closeFunction: async () => {
140
145
  ws.close();
@@ -0,0 +1,10 @@
1
+ import { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
2
+ interface OperationParams {
3
+ attachmentId?: string;
4
+ timeout: number;
5
+ apiUrl: string;
6
+ apiToken: string;
7
+ phoneNumber: string;
8
+ }
9
+ export declare function executeAttachmentsOperation(this: IExecuteFunctions, operation: string, itemIndex: number, params: OperationParams): Promise<INodeExecutionData>;
10
+ export {};
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.executeAttachmentsOperation = void 0;
7
+ const n8n_workflow_1 = require("n8n-workflow");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ async function executeAttachmentsOperation(operation, itemIndex, params) {
10
+ var _a, _b, _c, _d, _e;
11
+ const { attachmentId, timeout, apiUrl, apiToken, phoneNumber } = params;
12
+ const axiosConfig = {
13
+ headers: apiToken ? { Authorization: `Bearer ${apiToken}` } : {},
14
+ timeout,
15
+ responseType: operation === 'downloadAttachment' ? 'arraybuffer' : 'json',
16
+ };
17
+ const retryRequest = async (request, retries = 2, delay = 5000) => {
18
+ for (let attempt = 1; attempt <= retries; attempt++) {
19
+ try {
20
+ return await request();
21
+ }
22
+ catch (error) {
23
+ if (attempt === retries)
24
+ throw error;
25
+ await new Promise(resolve => setTimeout(resolve, delay));
26
+ }
27
+ }
28
+ };
29
+ try {
30
+ if (operation === 'listAttachments') {
31
+ const response = await retryRequest(() => axios_1.default.get(`${apiUrl}/v1/attachments/${phoneNumber}`, axiosConfig));
32
+ return { json: response.data, pairedItem: { item: itemIndex } };
33
+ }
34
+ else if (operation === 'downloadAttachment') {
35
+ if (!attachmentId) {
36
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Attachment ID is required' });
37
+ }
38
+ const endpoint = `${apiUrl}/v1/attachments/${attachmentId}`;
39
+ this.logger.debug(`Attachments: Downloading from endpoint: ${endpoint}`);
40
+ const response = await retryRequest(() => axios_1.default.get(endpoint, axiosConfig));
41
+ this.logger.debug(`Attachments: Download response size: ${response.data.byteLength}, content-type: ${response.headers['content-type']}`);
42
+ if (!response.data || response.data.byteLength === 0) {
43
+ this.logger.warn(`Attachments: Empty response data for attachment ${attachmentId}`);
44
+ return { json: { status: 'Empty attachment' }, pairedItem: { item: itemIndex } };
45
+ }
46
+ const contentType = response.headers['content-type'] || 'application/octet-stream';
47
+ const contentDisposition = response.headers['content-disposition'] || '';
48
+ const fileName = ((_a = contentDisposition.match(/filename="(.+)"/)) === null || _a === void 0 ? void 0 : _a[1]) || `attachment_${attachmentId}`;
49
+ const fileExtension = fileName.split('.').pop() || '';
50
+ // Конвертуємо ArrayBuffer в Buffer для n8n
51
+ const buffer = Buffer.from(response.data);
52
+ this.logger.debug(`Attachments: Created buffer of size: ${buffer.length}`);
53
+ // Збираємо всі доступні headers
54
+ const allHeaders = response.headers || {};
55
+ // Визначаємо тип файлу
56
+ const isImage = contentType.startsWith('image/');
57
+ const isVideo = contentType.startsWith('video/');
58
+ const isAudio = contentType.startsWith('audio/');
59
+ const isDocument = contentType.includes('pdf') || contentType.includes('document') || contentType.includes('text');
60
+ // Форматуємо розмір файлу
61
+ const formatFileSize = (bytes) => {
62
+ if (bytes === 0)
63
+ return '0 Bytes';
64
+ const k = 1024;
65
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
66
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
67
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
68
+ };
69
+ return {
70
+ json: {
71
+ // Основна інформація про файл
72
+ attachmentId,
73
+ fileName,
74
+ fileExtension,
75
+ mimeType: contentType,
76
+ // Розмір файлу
77
+ sizeBytes: buffer.length,
78
+ sizeFormatted: formatFileSize(buffer.length),
79
+ // Тип файлу
80
+ fileType: {
81
+ isImage,
82
+ isVideo,
83
+ isAudio,
84
+ isDocument,
85
+ category: isImage ? 'Image' : isVideo ? 'Video' : isAudio ? 'Audio' : isDocument ? 'Document' : 'Other'
86
+ },
87
+ // HTTP headers від API
88
+ headers: Object.keys(allHeaders).reduce((acc, key) => {
89
+ const value = allHeaders[key];
90
+ if (value !== null && value !== undefined && value !== '') {
91
+ acc[key] = value;
92
+ }
93
+ return acc;
94
+ }, {}),
95
+ // Додаткова інформація
96
+ downloadInfo: {
97
+ endpoint,
98
+ downloadedAt: new Date().toISOString(),
99
+ contentDisposition: contentDisposition || null,
100
+ hasValidContent: buffer.length > 0,
101
+ isEmpty: buffer.length === 0
102
+ },
103
+ // Статус завантаження
104
+ status: buffer.length > 0 ? 'downloaded_successfully' : 'empty_attachment'
105
+ },
106
+ binary: {
107
+ attachment: {
108
+ data: buffer.toString('base64'),
109
+ mimeType: contentType,
110
+ fileName: fileName,
111
+ fileExtension,
112
+ // Додаткові метадані для binary даних
113
+ fileSize: buffer.length,
114
+ id: attachmentId,
115
+ directory: `/attachments/${phoneNumber}`,
116
+ // Додаткові поля які можуть бути корисними
117
+ encoding: 'base64',
118
+ originalName: fileName,
119
+ downloadedFrom: endpoint,
120
+ downloadedAt: new Date().toISOString(),
121
+ // Якщо є content-disposition, додаємо його
122
+ ...(contentDisposition && { contentDisposition }),
123
+ // Категорія файлу для зручності
124
+ category: isImage ? 'image' : isVideo ? 'video' : isAudio ? 'audio' : isDocument ? 'document' : 'other',
125
+ // MD5 хеш для ідентифікації (опціонально)
126
+ ...(buffer.length > 0 && {
127
+ checksum: require('crypto').createHash('md5').update(buffer).digest('hex')
128
+ })
129
+ }
130
+ },
131
+ pairedItem: { item: itemIndex }
132
+ };
133
+ }
134
+ else if (operation === 'removeAttachment') {
135
+ if (!attachmentId) {
136
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Attachment ID is required' });
137
+ }
138
+ const response = await retryRequest(() => axios_1.default.delete(`${apiUrl}/v1/attachments/${attachmentId}`, axiosConfig));
139
+ return { json: response.data || { status: 'Attachment removed' }, pairedItem: { item: itemIndex } };
140
+ }
141
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Unknown operation' });
142
+ }
143
+ catch (error) {
144
+ const axiosError = error;
145
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {
146
+ message: axiosError.message,
147
+ description: (((_c = (_b = axiosError.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.error) || axiosError.message),
148
+ httpCode: ((_e = (_d = axiosError.response) === null || _d === void 0 ? void 0 : _d.status) === null || _e === void 0 ? void 0 : _e.toString()) || 'unknown',
149
+ }, { itemIndex });
150
+ }
151
+ }
152
+ exports.executeAttachmentsOperation = executeAttachmentsOperation;
@@ -26,38 +26,34 @@ async function executeMessagesOperation(operation, itemIndex, params) {
26
26
  }
27
27
  };
28
28
  try {
29
- let response;
30
29
  if (operation === 'sendMessage') {
31
- response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/send`, {
30
+ const response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/send`, {
32
31
  message,
33
32
  number: phoneNumber,
34
33
  recipients: [recipient],
35
34
  }, axiosConfig));
36
- this.logger.debug(`Signal messages: sendMessage response: ${JSON.stringify(response.data, null, 2)}`);
37
35
  return { json: response.data || { status: 'Message sent' }, pairedItem: { item: itemIndex } };
38
36
  }
39
37
  else if (operation === 'sendAttachment') {
40
- response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/send`, {
38
+ const response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/send`, {
41
39
  message,
42
40
  number: phoneNumber,
43
41
  recipients: [recipient],
44
42
  attachments: [attachmentUrl],
45
43
  }, axiosConfig));
46
- this.logger.debug(`Signal messages: sendAttachment response: ${JSON.stringify(response.data, null, 2)}`);
47
44
  return { json: response.data || { status: 'Attachment sent' }, pairedItem: { item: itemIndex } };
48
45
  }
49
46
  else if (operation === 'sendReaction') {
50
- response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/reactions/${phoneNumber}`, {
47
+ const response = await retryRequest(() => axios_1.default.post(`${apiUrl}/v1/reactions/${phoneNumber}`, {
51
48
  reaction: emoji,
52
49
  recipient,
53
50
  target_author: targetAuthor,
54
51
  timestamp: targetSentTimestamp,
55
52
  }, axiosConfig));
56
- this.logger.debug(`Signal messages: sendReaction response: ${JSON.stringify(response.data, null, 2)}`);
57
53
  return { json: response.data || { status: 'Reaction sent' }, pairedItem: { item: itemIndex } };
58
54
  }
59
55
  else if (operation === 'removeReaction') {
60
- response = await retryRequest(() => axios_1.default.delete(`${apiUrl}/v1/reactions/${phoneNumber}`, {
56
+ const response = await retryRequest(() => axios_1.default.delete(`${apiUrl}/v1/reactions/${phoneNumber}`, {
61
57
  ...axiosConfig,
62
58
  data: {
63
59
  recipient,
@@ -65,7 +61,6 @@ async function executeMessagesOperation(operation, itemIndex, params) {
65
61
  timestamp: targetSentTimestamp,
66
62
  },
67
63
  }));
68
- this.logger.debug(`Signal messages: removeReaction response: ${JSON.stringify(response.data, null, 2)}`);
69
64
  return { json: response.data || { status: 'Reaction removed' }, pairedItem: { item: itemIndex } };
70
65
  }
71
66
  throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: 'Unknown operation' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-signal-cli-rest-api",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Signal Node for n8n using signal-cli-rest-api",
5
5
  "repository": {
6
6
  "type": "git",