n8n-nodes-chat2crm 0.1.3 → 0.1.5

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.
@@ -0,0 +1,8 @@
1
+ import { IExecuteFunctions, INodeType, INodeTypeDescription, INodeExecutionData, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
2
+ export declare class Chat2CrmSend implements INodeType {
3
+ description: INodeTypeDescription;
4
+ getStreams(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
5
+ private static createRedisConnectionForLoadOptions;
6
+ private static getStreamDb;
7
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
8
+ }
@@ -0,0 +1,358 @@
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.Chat2CrmSend = void 0;
7
+ const n8n_workflow_1 = require("n8n-workflow");
8
+ const ioredis_1 = __importDefault(require("ioredis"));
9
+ const RedisConnection_1 = require("../Chat2CrmTrigger/Infra/RedisConnection");
10
+ class Chat2CrmSend {
11
+ constructor() {
12
+ this.description = {
13
+ displayName: 'Chat2Crm Send',
14
+ name: 'chat2CrmSend',
15
+ icon: 'file:chat2crm.svg',
16
+ group: ['transform'],
17
+ version: 1,
18
+ subtitle: '={{$parameter["streams"].join(", ")}}',
19
+ description: 'Send messages to Chat2Crm Redis streams',
20
+ defaults: {
21
+ name: 'Chat2Crm Send',
22
+ },
23
+ inputs: ['main'],
24
+ outputs: ['main'],
25
+ credentials: [
26
+ {
27
+ name: 'chat2CrmRedisApi',
28
+ required: true,
29
+ },
30
+ ],
31
+ properties: [
32
+ {
33
+ displayName: 'Stream Names or IDs',
34
+ name: 'streams',
35
+ type: 'multiOptions',
36
+ required: true,
37
+ description: 'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
38
+ typeOptions: {
39
+ loadOptionsMethod: 'getStreams',
40
+ },
41
+ default: [],
42
+ },
43
+ {
44
+ displayName: 'Message Data',
45
+ name: 'messageData',
46
+ type: 'json',
47
+ required: true,
48
+ default: '{}',
49
+ description: 'Message data to send to Redis stream (JSON object)',
50
+ },
51
+ {
52
+ displayName: 'Message ID',
53
+ name: 'messageId',
54
+ type: 'string',
55
+ default: '*',
56
+ description: 'Message ID (use * for auto-generate, or specify custom ID)',
57
+ },
58
+ ],
59
+ };
60
+ }
61
+ // Метод для динамической загрузки стримов из Redis
62
+ async getStreams() {
63
+ const credentials = await this.getCredentials('chat2CrmRedisApi');
64
+ const streams = [];
65
+ const foundStreams = new Set();
66
+ // Проверяем стримы в DB 0 и DB 1
67
+ for (const db of [0, 1]) {
68
+ let redis = null;
69
+ try {
70
+ // Создаем временное подключение для получения списка стримов
71
+ redis = await Chat2CrmSend.createRedisConnectionForLoadOptions(this, credentials, db);
72
+ // Получаем все ключи через SCAN
73
+ let cursor = '0';
74
+ do {
75
+ if (!redis) {
76
+ break;
77
+ }
78
+ const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', '*', 'COUNT', 100);
79
+ cursor = nextCursor;
80
+ // Проверяем каждый ключ, является ли он stream
81
+ for (const key of keys) {
82
+ if (foundStreams.has(key)) {
83
+ continue;
84
+ }
85
+ if (!redis) {
86
+ break;
87
+ }
88
+ try {
89
+ const keyType = await redis.type(key);
90
+ if (keyType === 'stream') {
91
+ foundStreams.add(key);
92
+ // Формируем отображаемое имя
93
+ let displayName = key;
94
+ try {
95
+ if (redis) {
96
+ const streamInfo = await redis.xinfo('STREAM', key);
97
+ if (streamInfo && Array.isArray(streamInfo)) {
98
+ const lengthIndex = streamInfo.indexOf('length');
99
+ if (lengthIndex >= 0 && lengthIndex < streamInfo.length - 1) {
100
+ const length = streamInfo[lengthIndex + 1];
101
+ displayName = `${key} (${length} msgs, DB ${db})`;
102
+ }
103
+ else {
104
+ displayName = `${key} (DB ${db})`;
105
+ }
106
+ }
107
+ }
108
+ }
109
+ catch (e) {
110
+ displayName = `${key} (DB ${db})`;
111
+ }
112
+ streams.push({
113
+ name: displayName,
114
+ value: key,
115
+ description: `Redis stream in DB ${db}`,
116
+ });
117
+ }
118
+ }
119
+ catch (e) {
120
+ continue;
121
+ }
122
+ }
123
+ } while (cursor !== '0' && redis);
124
+ }
125
+ catch (error) {
126
+ console.error(`[Chat2Crm Send] Error loading streams from DB ${db}:`, error.message);
127
+ }
128
+ finally {
129
+ if (redis) {
130
+ try {
131
+ redis.disconnect();
132
+ }
133
+ catch (e) {
134
+ // Игнорируем ошибки при закрытии
135
+ }
136
+ }
137
+ }
138
+ }
139
+ // Сортируем стримы по имени
140
+ streams.sort((a, b) => a.name.localeCompare(b.name));
141
+ if (streams.length === 0) {
142
+ return [
143
+ {
144
+ name: 'No Streams Found. Check Redis Connection.',
145
+ value: '',
146
+ description: 'No streams available',
147
+ },
148
+ ];
149
+ }
150
+ return streams;
151
+ }
152
+ // Статический метод для создания подключения к Redis при загрузке опций
153
+ static async createRedisConnectionForLoadOptions(context, credentials, db) {
154
+ let redisHost = credentials.host;
155
+ let redisPort = credentials.port;
156
+ // Проверяем, используется ли SSH туннель
157
+ if (credentials.useSSH) {
158
+ // Пытаемся использовать SSH, если доступен helpers.getSSHClient
159
+ if (context.helpers && typeof context.helpers.getSSHClient === 'function') {
160
+ try {
161
+ const sshCredentials = {
162
+ sshHost: credentials.sshHost,
163
+ sshPort: credentials.sshPort || 22,
164
+ sshUser: credentials.sshUser,
165
+ sshAuthenticateWith: credentials.sshAuthenticateWith || 'password',
166
+ };
167
+ if (sshCredentials.sshAuthenticateWith === 'password') {
168
+ sshCredentials.sshPassword = credentials.sshPassword;
169
+ }
170
+ else {
171
+ sshCredentials.privateKey = credentials.privateKey;
172
+ sshCredentials.passphrase = credentials.passphrase;
173
+ }
174
+ const sshClient = await context.helpers.getSSHClient(sshCredentials);
175
+ const remoteRedisHost = credentials.host || 'localhost';
176
+ const remoteRedisPort = credentials.port || 6379;
177
+ // Создаем SSH туннель
178
+ const net = require('net');
179
+ const localServer = net.createServer((localSocket) => {
180
+ sshClient.forwardOut('127.0.0.1', 0, remoteRedisHost, remoteRedisPort, (err, stream) => {
181
+ if (err) {
182
+ localSocket.destroy();
183
+ return;
184
+ }
185
+ localSocket.pipe(stream);
186
+ stream.pipe(localSocket);
187
+ });
188
+ });
189
+ await new Promise((resolve, reject) => {
190
+ localServer.listen(0, '127.0.0.1', () => {
191
+ const address = localServer.address();
192
+ if (address && typeof address === 'object') {
193
+ redisHost = '127.0.0.1';
194
+ redisPort = address.port;
195
+ resolve();
196
+ }
197
+ else {
198
+ reject(new Error('Failed to get local server address'));
199
+ }
200
+ });
201
+ localServer.on('error', reject);
202
+ });
203
+ }
204
+ catch (error) {
205
+ throw new n8n_workflow_1.ApplicationError(`SSH tunnel failed in loadOptions: ${error.message}`, { level: 'error' });
206
+ }
207
+ }
208
+ else {
209
+ throw new n8n_workflow_1.ApplicationError('SSH tunnel is not supported in loadOptions context', { level: 'error' });
210
+ }
211
+ }
212
+ // Создаем подключение к Redis
213
+ const redis = new ioredis_1.default({
214
+ host: redisHost,
215
+ port: redisPort,
216
+ db: db,
217
+ password: credentials.password,
218
+ enableReadyCheck: true,
219
+ maxRetriesPerRequest: 1,
220
+ connectTimeout: 5000,
221
+ retryStrategy: () => null,
222
+ });
223
+ // Ждем готовности подключения
224
+ await new Promise((resolve, reject) => {
225
+ const timeout = setTimeout(() => {
226
+ redis.disconnect();
227
+ reject(new Error('Connection timeout'));
228
+ }, 5000);
229
+ redis.once('ready', () => {
230
+ clearTimeout(timeout);
231
+ resolve();
232
+ });
233
+ redis.once('error', (err) => {
234
+ clearTimeout(timeout);
235
+ redis.disconnect();
236
+ reject(err);
237
+ });
238
+ });
239
+ return redis;
240
+ }
241
+ // Определяем базу данных для стрима
242
+ static getStreamDb(stream) {
243
+ // Streams в DB 1 (CrmWorker)
244
+ if (stream === 'crm_outgoing_message' ||
245
+ stream === 'crm_incoming_message' ||
246
+ stream === 'crm_lazy_incoming_message' ||
247
+ stream === 'crm_contacts') {
248
+ return 1;
249
+ }
250
+ // Все остальные streams находятся в DB 0
251
+ return 0;
252
+ }
253
+ async execute() {
254
+ const items = this.getInputData();
255
+ const returnData = [];
256
+ const credentials = await this.getCredentials('chat2CrmRedisApi');
257
+ const selectedStreams = this.getNodeParameter('streams', 0);
258
+ const messageId = this.getNodeParameter('messageId', 0);
259
+ // Проверяем, что выбраны streams
260
+ if (!selectedStreams || selectedStreams.length === 0) {
261
+ throw new n8n_workflow_1.ApplicationError('Please select at least one stream to send message to', { level: 'warning' });
262
+ }
263
+ // Группируем выбранные streams по базам данных
264
+ const streamsByDb = new Map();
265
+ selectedStreams.forEach(stream => {
266
+ const db = Chat2CrmSend.getStreamDb(stream);
267
+ if (!streamsByDb.has(db)) {
268
+ streamsByDb.set(db, []);
269
+ }
270
+ streamsByDb.get(db).push(stream);
271
+ });
272
+ // Создаем подключения к Redis только для нужных баз данных
273
+ const redisConnections = new Map();
274
+ try {
275
+ for (const db of streamsByDb.keys()) {
276
+ const redis = await RedisConnection_1.createRedisConnection.call(this, credentials, db);
277
+ redisConnections.set(db, redis);
278
+ }
279
+ // Обрабатываем каждый элемент входных данных
280
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
281
+ const messageDataParam = this.getNodeParameter('messageData', itemIndex);
282
+ // Парсим JSON данные сообщения
283
+ let messageData = {};
284
+ try {
285
+ if (typeof messageDataParam === 'string') {
286
+ messageData = JSON.parse(messageDataParam);
287
+ }
288
+ else if (typeof messageDataParam === 'object') {
289
+ messageData = messageDataParam;
290
+ }
291
+ }
292
+ catch (error) {
293
+ throw new n8n_workflow_1.ApplicationError(`Invalid JSON in messageData: ${error}`, { level: 'error' });
294
+ }
295
+ // Отправляем сообщение в каждый выбранный stream
296
+ const sent = [];
297
+ const errors = [];
298
+ for (const [db, dbStreams] of streamsByDb.entries()) {
299
+ const redis = redisConnections.get(db);
300
+ for (const stream of dbStreams) {
301
+ try {
302
+ // Подготавливаем поля для XADD
303
+ const fields = [];
304
+ for (const [key, value] of Object.entries(messageData)) {
305
+ fields.push(key);
306
+ // Преобразуем значение в строку
307
+ if (typeof value === 'object' && value !== null) {
308
+ fields.push(JSON.stringify(value));
309
+ }
310
+ else {
311
+ fields.push(String(value));
312
+ }
313
+ }
314
+ // Отправляем сообщение в stream
315
+ const id = await redis.xadd(stream, messageId || '*', ...fields);
316
+ if (!id) {
317
+ throw new n8n_workflow_1.ApplicationError('Failed to add message to stream: XADD returned null', { level: 'error' });
318
+ }
319
+ sent.push({
320
+ stream,
321
+ db,
322
+ messageId: id,
323
+ });
324
+ }
325
+ catch (error) {
326
+ errors.push({
327
+ stream,
328
+ db,
329
+ error: error.message,
330
+ });
331
+ }
332
+ }
333
+ }
334
+ // Возвращаем результат
335
+ returnData.push({
336
+ json: {
337
+ ...items[itemIndex].json,
338
+ sent,
339
+ errors,
340
+ },
341
+ });
342
+ }
343
+ }
344
+ finally {
345
+ // Закрываем все подключения
346
+ for (const [db, redis] of redisConnections.entries()) {
347
+ try {
348
+ redis.disconnect();
349
+ }
350
+ catch (error) {
351
+ console.error(`[Chat2Crm Send] Error disconnecting Redis connection for DB ${db}:`, error);
352
+ }
353
+ }
354
+ }
355
+ return [returnData];
356
+ }
357
+ }
358
+ exports.Chat2CrmSend = Chat2CrmSend;
@@ -0,0 +1,25 @@
1
+ <?xml version="1.0" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
+ <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
+ width="640.000000pt" height="640.000000pt" viewBox="0 0 640.000000 640.000000"
6
+ preserveAspectRatio="xMidYMid meet">
7
+
8
+ <g transform="translate(0.000000,640.000000) scale(0.100000,-0.100000)"
9
+ fill="#000000" stroke="none">
10
+ <path d="M2150 4544 l0 -979 416 -416 415 -416 -3 -611 c-2 -337 0 -611 3
11
+ -610 17 7 1036 1008 1192 1170 l177 186 0 248 0 249 -275 275 -275 275 0 -255
12
+ 0 -254 47 -48 47 -48 43 42 43 42 0 -453 c0 -249 -4 -451 -9 -449 -4 2 -231
13
+ 223 -504 492 l-495 489 -1 282 -1 283 -115 212 c-63 117 -114 215 -112 216 2
14
+ 2 52 -46 112 -106 60 -61 112 -110 115 -110 3 0 4 111 3 247 l-2 248 -313 298
15
+ c-172 164 -357 338 -410 388 l-98 91 0 -978z"/>
16
+ <path d="M3092 3547 l-32 -33 253 -249 c139 -138 286 -283 328 -323 l75 -72
17
+ 42 39 c52 49 56 81 10 81 l-32 0 32 33 c31 31 32 37 32 112 l0 79 -57 59 -58
18
+ 58 37 31 38 30 -87 94 -87 94 -231 0 -231 0 -32 -33z"/>
19
+ <path d="M2643 1858 l-243 -243 0 93 c0 50 -1 92 -3 92 -2 0 -58 -55 -125
20
+ -122 l-122 -123 0 -95 0 -95 125 125 125 125 0 -220 0 -220 245 245 245 245
21
+ -2 218 -3 217 -242 -242z"/>
22
+ <path d="M3415 1792 c-27 -25 -138 -131 -245 -236 l-194 -191 -1 -217 c0 -120
23
+ 3 -217 6 -215 4 1 115 108 248 238 l241 237 -2 215 -3 216 -50 -47z"/>
24
+ </g>
25
+ </svg>
@@ -1,7 +1,5 @@
1
- import { ITriggerFunctions, INodeType, INodeTypeDescription, ITriggerResponse, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
1
+ import { ITriggerFunctions, INodeType, INodeTypeDescription, ITriggerResponse } from 'n8n-workflow';
2
2
  export declare class Chat2CrmTrigger implements INodeType {
3
3
  description: INodeTypeDescription;
4
- getStreams(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
5
- private static createRedisConnectionForLoadOptions;
6
4
  trigger(this: ITriggerFunctions): Promise<ITriggerResponse | undefined>;
7
5
  }
@@ -1,11 +1,7 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.Chat2CrmTrigger = void 0;
7
4
  const n8n_workflow_1 = require("n8n-workflow");
8
- const ioredis_1 = __importDefault(require("ioredis"));
9
5
  const RedisConnection_1 = require("./Infra/RedisConnection");
10
6
  class Chat2CrmTrigger {
11
7
  constructor() {
@@ -30,15 +26,74 @@ class Chat2CrmTrigger {
30
26
  ],
31
27
  properties: [
32
28
  {
33
- displayName: 'Stream Names or IDs',
29
+ displayName: 'Streams',
34
30
  name: 'streams',
35
31
  type: 'multiOptions',
36
- required: true,
37
- description: 'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
38
- typeOptions: {
39
- loadOptionsMethod: 'getStreams',
40
- },
41
- default: [],
32
+ required: true, // Вернули required: true
33
+ description: 'Select Redis streams to listen to',
34
+ options: [
35
+ {
36
+ name: 'Chat Incoming Message',
37
+ value: 'chat_incoming_message',
38
+ description: 'Messages to chat (DB 0)',
39
+ },
40
+ {
41
+ name: 'Chat Outgoing Message',
42
+ value: 'chat_outgoing_message',
43
+ description: 'Messages from chat to CRM (DB 0)',
44
+ },
45
+ {
46
+ name: 'Chat Outgoing Status',
47
+ value: 'chat_outgoing_status',
48
+ description: 'Status messages from chat (DB 0)',
49
+ },
50
+ {
51
+ name: 'Chat Outgoing Status (CRM)',
52
+ value: 'chat_outgoing_status',
53
+ description: 'Chat outgoing status in CRM DB (DB 1)',
54
+ },
55
+ {
56
+ name: 'CRM Contacts',
57
+ value: 'crm_contacts',
58
+ description: 'CRM contacts stream (DB 1)',
59
+ },
60
+ {
61
+ name: 'CRM Incoming Message',
62
+ value: 'crm_incoming_message',
63
+ description: 'Messages to CRM (DB 1)',
64
+ },
65
+ {
66
+ name: 'CRM Incoming Status',
67
+ value: 'crm_incoming_status',
68
+ description: 'Status messages to CRM (DB 0)',
69
+ },
70
+ {
71
+ name: 'CRM Lazy Incoming Message',
72
+ value: 'crm_lazy_incoming_message',
73
+ description: 'Lazy incoming messages to CRM (DB 1)',
74
+ },
75
+ {
76
+ name: 'CRM Outgoing Message',
77
+ value: 'crm_outgoing_message',
78
+ description: 'Messages from CRM to chat (DB 1)',
79
+ },
80
+ {
81
+ name: 'CRM Outgoing Status',
82
+ value: 'crm_outgoing_status',
83
+ description: 'Status messages from CRM (DB 1)',
84
+ },
85
+ {
86
+ name: 'N8N Incoming Message',
87
+ value: 'n8n_incoming_message',
88
+ description: 'Incoming messages to N8N (DB 0)',
89
+ },
90
+ {
91
+ name: 'N8N Outgoing Message',
92
+ value: 'n8n_outgoing_message',
93
+ description: 'Outgoing messages from N8N (DB 0)',
94
+ },
95
+ ],
96
+ default: ['chat_outgoing_message'],
42
97
  },
43
98
  {
44
99
  displayName: 'Block Time (Ms)',
@@ -64,188 +119,6 @@ class Chat2CrmTrigger {
64
119
  ],
65
120
  };
66
121
  }
67
- // Метод для динамической загрузки стримов из Redis
68
- async getStreams() {
69
- const credentials = await this.getCredentials('chat2CrmRedisApi');
70
- const streams = [];
71
- const foundStreams = new Set();
72
- // Проверяем стримы в DB 0 и DB 1
73
- for (const db of [0, 1]) {
74
- let redis = null;
75
- try {
76
- // Создаем временное подключение для получения списка стримов
77
- redis = await Chat2CrmTrigger.createRedisConnectionForLoadOptions(this, credentials, db);
78
- // Получаем все ключи через SCAN
79
- let cursor = '0';
80
- do {
81
- if (!redis) {
82
- break;
83
- }
84
- const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', '*', 'COUNT', 100);
85
- cursor = nextCursor;
86
- // Проверяем каждый ключ, является ли он stream
87
- for (const key of keys) {
88
- if (foundStreams.has(key)) {
89
- continue;
90
- }
91
- if (!redis) {
92
- break;
93
- }
94
- try {
95
- const keyType = await redis.type(key);
96
- if (keyType === 'stream') {
97
- foundStreams.add(key);
98
- // Формируем отображаемое имя
99
- let displayName = key;
100
- try {
101
- if (redis) {
102
- const streamInfo = await redis.xinfo('STREAM', key);
103
- if (streamInfo && Array.isArray(streamInfo)) {
104
- const lengthIndex = streamInfo.indexOf('length');
105
- if (lengthIndex >= 0 && lengthIndex < streamInfo.length - 1) {
106
- const length = streamInfo[lengthIndex + 1];
107
- displayName = `${key} (${length} msgs, DB ${db})`;
108
- }
109
- else {
110
- displayName = `${key} (DB ${db})`;
111
- }
112
- }
113
- }
114
- }
115
- catch (e) {
116
- displayName = `${key} (DB ${db})`;
117
- }
118
- streams.push({
119
- name: displayName,
120
- value: key,
121
- description: `Redis stream in DB ${db}`,
122
- });
123
- }
124
- }
125
- catch (e) {
126
- continue;
127
- }
128
- }
129
- } while (cursor !== '0' && redis);
130
- }
131
- catch (error) {
132
- console.error(`[Chat2Crm Trigger] Error loading streams from DB ${db}:`, error.message);
133
- }
134
- finally {
135
- if (redis) {
136
- try {
137
- redis.disconnect();
138
- }
139
- catch (e) {
140
- // Игнорируем ошибки при закрытии
141
- }
142
- }
143
- }
144
- }
145
- // Сортируем стримы по имени
146
- streams.sort((a, b) => a.name.localeCompare(b.name));
147
- if (streams.length === 0) {
148
- return [
149
- {
150
- name: 'No Streams Found. Check Redis Connection.',
151
- value: '',
152
- description: 'No streams available',
153
- },
154
- ];
155
- }
156
- return streams;
157
- }
158
- // Статический метод для создания подключения к Redis при загрузке опций
159
- static async createRedisConnectionForLoadOptions(context, credentials, db) {
160
- let redisHost = credentials.host;
161
- let redisPort = credentials.port;
162
- // Проверяем, используется ли SSH туннель
163
- if (credentials.useSSH) {
164
- // Пытаемся использовать SSH, если доступен helpers.getSSHClient
165
- if (context.helpers && typeof context.helpers.getSSHClient === 'function') {
166
- try {
167
- // Используем существующую логику из createRedisConnection
168
- // Но упрощенную версию для loadOptions
169
- const sshCredentials = {
170
- sshHost: credentials.sshHost,
171
- sshPort: credentials.sshPort || 22,
172
- sshUser: credentials.sshUser,
173
- sshAuthenticateWith: credentials.sshAuthenticateWith || 'password',
174
- };
175
- if (sshCredentials.sshAuthenticateWith === 'password') {
176
- sshCredentials.sshPassword = credentials.sshPassword;
177
- }
178
- else {
179
- sshCredentials.privateKey = credentials.privateKey;
180
- sshCredentials.passphrase = credentials.passphrase;
181
- }
182
- const sshClient = await context.helpers.getSSHClient(sshCredentials);
183
- const remoteRedisHost = credentials.host || 'localhost';
184
- const remoteRedisPort = credentials.port || 6379;
185
- // Создаем SSH туннель (упрощенная версия)
186
- const net = require('net');
187
- const localServer = net.createServer((localSocket) => {
188
- sshClient.forwardOut('127.0.0.1', 0, remoteRedisHost, remoteRedisPort, (err, stream) => {
189
- if (err) {
190
- localSocket.destroy();
191
- return;
192
- }
193
- localSocket.pipe(stream);
194
- stream.pipe(localSocket);
195
- });
196
- });
197
- await new Promise((resolve, reject) => {
198
- localServer.listen(0, '127.0.0.1', () => {
199
- const address = localServer.address();
200
- if (address && typeof address === 'object') {
201
- redisHost = '127.0.0.1';
202
- redisPort = address.port;
203
- resolve();
204
- }
205
- else {
206
- reject(new Error('Failed to get local server address'));
207
- }
208
- });
209
- localServer.on('error', reject);
210
- });
211
- }
212
- catch (error) {
213
- throw new n8n_workflow_1.ApplicationError(`SSH tunnel failed in loadOptions: ${error.message}`, { level: 'error' });
214
- }
215
- }
216
- else {
217
- throw new n8n_workflow_1.ApplicationError('SSH tunnel is not supported in loadOptions context', { level: 'error' });
218
- }
219
- }
220
- // Создаем подключение к Redis
221
- const redis = new ioredis_1.default({
222
- host: redisHost,
223
- port: redisPort,
224
- db: db,
225
- password: credentials.password,
226
- enableReadyCheck: true,
227
- maxRetriesPerRequest: 1,
228
- connectTimeout: 5000,
229
- retryStrategy: () => null,
230
- });
231
- // Ждем готовности подключения
232
- await new Promise((resolve, reject) => {
233
- const timeout = setTimeout(() => {
234
- redis.disconnect();
235
- reject(new Error('Connection timeout'));
236
- }, 5000);
237
- redis.once('ready', () => {
238
- clearTimeout(timeout);
239
- resolve();
240
- });
241
- redis.once('error', (err) => {
242
- clearTimeout(timeout);
243
- redis.disconnect();
244
- reject(err);
245
- });
246
- });
247
- return redis;
248
- }
249
122
  async trigger() {
250
123
  const credentials = await this.getCredentials('chat2CrmRedisApi');
251
124
  const selectedStreams = this.getNodeParameter('streams', []);
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  module.exports = {
2
2
  nodes: [
3
3
  require('./dist/nodes/Chat2CrmTrigger/Chat2CrmTrigger.node.js'),
4
+ require('./dist/nodes/Chat2CrmSend/Chat2CrmSend.node.js'),
4
5
  ],
5
6
  credentials: [
6
7
  require('./dist/credentials/Chat2CrmRedisApi.credentials.js'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-chat2crm",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "n8n node for Chat2Crm Redis integration",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -27,7 +27,8 @@
27
27
  "dist/credentials/Chat2CrmRedisApi.credentials.js"
28
28
  ],
29
29
  "nodes": [
30
- "dist/nodes/Chat2CrmTrigger/Chat2CrmTrigger.node.js"
30
+ "dist/nodes/Chat2CrmTrigger/Chat2CrmTrigger.node.js",
31
+ "dist/nodes/Chat2CrmSend/Chat2CrmSend.node.js"
31
32
  ]
32
33
  },
33
34
  "devDependencies": {