n8n-nodes-chat2crm 0.1.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.
@@ -0,0 +1,7 @@
1
+ import { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class Chat2CrmRedisApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Chat2CrmRedisApi = void 0;
4
+ class Chat2CrmRedisApi {
5
+ constructor() {
6
+ this.name = 'chat2CrmRedisApi';
7
+ this.displayName = 'Chat2Crm Redis API';
8
+ this.documentationUrl = '';
9
+ this.properties = [
10
+ {
11
+ displayName: 'Use SSH Tunnel',
12
+ name: 'useSSH',
13
+ type: 'boolean',
14
+ default: false,
15
+ description: 'Whether to use SSH tunnel to connect to Redis',
16
+ },
17
+ {
18
+ displayName: 'SSH Host',
19
+ name: 'sshHost',
20
+ type: 'string',
21
+ displayOptions: {
22
+ show: {
23
+ useSSH: [true],
24
+ },
25
+ },
26
+ default: '',
27
+ required: true,
28
+ description: 'SSH server hostname',
29
+ },
30
+ {
31
+ displayName: 'SSH Port',
32
+ name: 'sshPort',
33
+ type: 'number',
34
+ displayOptions: {
35
+ show: {
36
+ useSSH: [true],
37
+ },
38
+ },
39
+ default: 22,
40
+ required: true,
41
+ description: 'SSH server port',
42
+ },
43
+ {
44
+ displayName: 'SSH Username',
45
+ name: 'sshUser',
46
+ type: 'string',
47
+ displayOptions: {
48
+ show: {
49
+ useSSH: [true],
50
+ },
51
+ },
52
+ default: '',
53
+ required: true,
54
+ },
55
+ {
56
+ displayName: 'SSH Authentication',
57
+ name: 'sshAuthenticateWith',
58
+ type: 'options',
59
+ displayOptions: {
60
+ show: {
61
+ useSSH: [true],
62
+ },
63
+ },
64
+ options: [
65
+ {
66
+ name: 'Password',
67
+ value: 'password',
68
+ },
69
+ {
70
+ name: 'Private Key',
71
+ value: 'privateKey',
72
+ },
73
+ ],
74
+ default: 'password',
75
+ description: 'SSH authentication method',
76
+ },
77
+ {
78
+ displayName: 'SSH Password',
79
+ name: 'sshPassword',
80
+ type: 'string',
81
+ typeOptions: {
82
+ password: true,
83
+ },
84
+ displayOptions: {
85
+ show: {
86
+ useSSH: [true],
87
+ sshAuthenticateWith: ['password'],
88
+ },
89
+ },
90
+ default: '',
91
+ },
92
+ {
93
+ displayName: 'SSH Private Key',
94
+ name: 'privateKey',
95
+ type: 'string',
96
+ typeOptions: {
97
+ password: true,
98
+ },
99
+ displayOptions: {
100
+ show: {
101
+ useSSH: [true],
102
+ sshAuthenticateWith: ['privateKey'],
103
+ },
104
+ },
105
+ default: '',
106
+ description: 'SSH private key in PEM format (supports RSA, ED25519, etc.). Can be pasted with or without line breaks - will be normalized automatically.',
107
+ },
108
+ {
109
+ displayName: 'SSH Passphrase',
110
+ name: 'passphrase',
111
+ type: 'string',
112
+ typeOptions: {
113
+ password: true,
114
+ },
115
+ displayOptions: {
116
+ show: {
117
+ useSSH: [true],
118
+ sshAuthenticateWith: ['privateKey'],
119
+ },
120
+ },
121
+ default: '',
122
+ description: 'SSH private key passphrase (if required)',
123
+ },
124
+ {
125
+ displayName: 'Connection Type',
126
+ name: 'connectionType',
127
+ type: 'options',
128
+ options: [
129
+ {
130
+ name: 'Direct Connection',
131
+ value: 'direct',
132
+ description: 'Connect directly to Redis (localhost, IP, or container name)',
133
+ },
134
+ {
135
+ name: 'Docker Container',
136
+ value: 'docker',
137
+ description: 'Connect to Redis in Docker container (same network)',
138
+ },
139
+ ],
140
+ default: 'direct',
141
+ description: 'How to connect to Redis',
142
+ },
143
+ {
144
+ displayName: 'Docker Container Name',
145
+ name: 'dockerContainer',
146
+ type: 'string',
147
+ displayOptions: {
148
+ show: {
149
+ connectionType: ['docker'],
150
+ },
151
+ },
152
+ default: 'chat2crm-redis-1',
153
+ required: true,
154
+ description: 'Docker container name for Redis (must be in the same Docker network as n8n)',
155
+ },
156
+ {
157
+ displayName: 'Redis Host',
158
+ name: 'host',
159
+ type: 'string',
160
+ default: 'localhost',
161
+ required: true,
162
+ description: 'Redis server hostname. If using SSH: hostname of Redis on the remote server (usually "localhost" or "127.0.0.1" if Redis is on the same server as SSH). If direct connection: use "localhost" or "127.0.0.1" if Redis is in Docker with port mapped to host.',
163
+ },
164
+ {
165
+ displayName: 'Redis Port',
166
+ name: 'port',
167
+ type: 'number',
168
+ default: 6379,
169
+ required: true,
170
+ description: 'Redis server port. If using SSH: port of Redis on the remote server (usually 6379). If direct connection: port on local machine (usually 6379 if Redis is in Docker with port mapped).',
171
+ },
172
+ {
173
+ displayName: 'Redis Database',
174
+ name: 'db',
175
+ type: 'number',
176
+ default: 0,
177
+ required: true,
178
+ description: 'Redis database number (0 for ChatWorker, 1 for CrmWorker)',
179
+ },
180
+ {
181
+ displayName: 'Password',
182
+ name: 'password',
183
+ type: 'string',
184
+ typeOptions: {
185
+ password: true,
186
+ },
187
+ default: '',
188
+ description: 'Redis password (if required)',
189
+ },
190
+ ];
191
+ }
192
+ }
193
+ exports.Chat2CrmRedisApi = Chat2CrmRedisApi;
@@ -0,0 +1,5 @@
1
+ import { ITriggerFunctions, INodeType, INodeTypeDescription, ITriggerResponse } from 'n8n-workflow';
2
+ export declare class Chat2CrmTrigger implements INodeType {
3
+ description: INodeTypeDescription;
4
+ trigger(this: ITriggerFunctions): Promise<ITriggerResponse | undefined>;
5
+ }
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Chat2CrmTrigger = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const RedisConnection_1 = require("./Infra/RedisConnection");
6
+ class Chat2CrmTrigger {
7
+ constructor() {
8
+ this.description = {
9
+ displayName: 'Chat2Crm Trigger',
10
+ name: 'chat2CrmTrigger',
11
+ icon: 'file:chat2crm.svg',
12
+ group: ['trigger'],
13
+ version: 1,
14
+ subtitle: '={{$parameter["streams"].join(", ")}}',
15
+ description: 'Triggers workflow when messages arrive in Chat2Crm Redis streams',
16
+ defaults: {
17
+ name: 'Chat2Crm Trigger',
18
+ },
19
+ inputs: [],
20
+ outputs: ['main'],
21
+ credentials: [
22
+ {
23
+ name: 'chat2CrmRedisApi',
24
+ required: true,
25
+ },
26
+ ],
27
+ properties: [
28
+ {
29
+ displayName: 'Streams',
30
+ name: 'streams',
31
+ type: 'multiOptions',
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
+ default: ['chat_outgoing_message'],
87
+ },
88
+ {
89
+ displayName: 'Block Time (Ms)',
90
+ name: 'block',
91
+ type: 'number',
92
+ default: 1000,
93
+ description: 'Block time in milliseconds when waiting for messages (0 = no blocking)',
94
+ },
95
+ {
96
+ displayName: 'Count',
97
+ name: 'count',
98
+ type: 'number',
99
+ default: 10,
100
+ description: 'Maximum number of messages to read in one batch',
101
+ },
102
+ {
103
+ displayName: 'Polling Interval (Seconds)',
104
+ name: 'pollInterval',
105
+ type: 'number',
106
+ default: 0.5,
107
+ description: 'How often to check for new messages (in seconds). Recommended: 0.1-1 second for real-time processing.',
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ async trigger() {
113
+ const credentials = await this.getCredentials('chat2CrmRedisApi');
114
+ const selectedStreams = this.getNodeParameter('streams', []);
115
+ const block = this.getNodeParameter('block', 1000);
116
+ const count = this.getNodeParameter('count', 10);
117
+ const pollIntervalSeconds = this.getNodeParameter('pollInterval', 0.5);
118
+ const pollInterval = Math.min(pollIntervalSeconds * 1000, 500); // Максимум 500ms
119
+ // Проверяем, что выбраны streams
120
+ if (!selectedStreams || selectedStreams.length === 0) {
121
+ throw new n8n_workflow_1.ApplicationError('Please select at least one stream to monitor', { level: 'warning' });
122
+ }
123
+ // Группируем выбранные streams по базам данных
124
+ const streamsByDb = new Map();
125
+ selectedStreams.forEach(stream => {
126
+ let db = 0; // По умолчанию DB 0
127
+ // Streams в DB 1 (CrmWorker)
128
+ if (stream === 'crm_outgoing_message' ||
129
+ stream === 'crm_incoming_message' ||
130
+ stream === 'crm_lazy_incoming_message' ||
131
+ stream === 'crm_contacts') {
132
+ db = 1;
133
+ }
134
+ // Все остальные streams (chat_incoming_message, chat_outgoing_message,
135
+ // crm_incoming_status, crm_outgoing_status) находятся в DB 0
136
+ if (!streamsByDb.has(db)) {
137
+ streamsByDb.set(db, []);
138
+ }
139
+ streamsByDb.get(db).push(stream);
140
+ });
141
+ // Создаем подключения к Redis только для нужных баз данных
142
+ const redisConnections = new Map();
143
+ for (const db of streamsByDb.keys()) {
144
+ const redis = await RedisConnection_1.createRedisConnection.call(this, credentials, db);
145
+ redisConnections.set(db, redis);
146
+ }
147
+ // Храним последние прочитанные ID для каждого stream
148
+ const lastReadIds = new Map();
149
+ // Собираем информацию о найденных streams для вывода
150
+ const foundStreams = [];
151
+ // Инициализируем lastReadIds для каждого stream и собираем информацию
152
+ for (const [db, dbStreams] of streamsByDb.entries()) {
153
+ const redis = redisConnections.get(db);
154
+ for (const stream of dbStreams) {
155
+ try {
156
+ // Пытаемся получить информацию о stream
157
+ const streamInfo = await redis.xinfo('STREAM', stream);
158
+ if (streamInfo && Array.isArray(streamInfo)) {
159
+ // Ищем 'last-entry' в информации о stream
160
+ const lastEntryIndex = streamInfo.indexOf('last-entry');
161
+ if (lastEntryIndex >= 0 && lastEntryIndex < streamInfo.length - 1) {
162
+ const lastEntry = streamInfo[lastEntryIndex + 1];
163
+ if (Array.isArray(lastEntry) && lastEntry.length > 0) {
164
+ const lastId = lastEntry[0];
165
+ lastReadIds.set(stream, lastId);
166
+ foundStreams.push({ db, stream, exists: true, lastId });
167
+ }
168
+ else {
169
+ lastReadIds.set(stream, '$');
170
+ foundStreams.push({ db, stream, exists: true });
171
+ }
172
+ }
173
+ else {
174
+ lastReadIds.set(stream, '$');
175
+ foundStreams.push({ db, stream, exists: true });
176
+ }
177
+ }
178
+ else {
179
+ lastReadIds.set(stream, '$');
180
+ foundStreams.push({ db, stream, exists: true });
181
+ }
182
+ }
183
+ catch (error) {
184
+ // Если stream не существует или ошибка, используем '$' для новых сообщений
185
+ if (error.message?.includes('no such key') || error.message?.includes('not found')) {
186
+ lastReadIds.set(stream, '$');
187
+ foundStreams.push({ db, stream, exists: false });
188
+ }
189
+ else {
190
+ lastReadIds.set(stream, '$');
191
+ foundStreams.push({ db, stream, exists: false });
192
+ }
193
+ }
194
+ }
195
+ }
196
+ // Выводим список найденных streams
197
+ console.log(`[Chat2Crm Trigger] Connected to Redis. Monitoring ${foundStreams.length} selected stream(s):`);
198
+ foundStreams.forEach(({ db, stream, exists, lastId }) => {
199
+ if (exists) {
200
+ if (lastId) {
201
+ console.log(` ✓ DB ${db}: ${stream} (last ID: ${lastId})`);
202
+ }
203
+ else {
204
+ console.log(` ✓ DB ${db}: ${stream} (empty, waiting for messages)`);
205
+ }
206
+ }
207
+ else {
208
+ console.log(` ✗ DB ${db}: ${stream} (not found, will monitor when created)`);
209
+ }
210
+ });
211
+ // Флаг для отслеживания состояния закрытия
212
+ let isClosing = false;
213
+ // Функция для чтения сообщений
214
+ const readMessages = async () => {
215
+ // Проверяем, не закрывается ли триггер
216
+ if (isClosing) {
217
+ return;
218
+ }
219
+ for (const [db, dbStreams] of streamsByDb.entries()) {
220
+ // Проверяем снова перед каждой итерацией
221
+ if (isClosing) {
222
+ break;
223
+ }
224
+ const redis = redisConnections.get(db);
225
+ // Читаем каждый stream отдельно через XREAD
226
+ // XREAD читает напрямую из stream, независимо от consumer groups
227
+ for (const stream of dbStreams) {
228
+ if (isClosing) {
229
+ break;
230
+ }
231
+ try {
232
+ const lastId = lastReadIds.get(stream) || '$';
233
+ // Используем более короткий block time для возможности быстрого закрытия
234
+ const blockTime = isClosing ? 0 : Math.min(block, 500); // Максимум 500ms для более быстрого отклика
235
+ // Используем XREAD чтобы читать все сообщения напрямую из stream
236
+ // Это позволяет читать сообщения даже если они уже обработаны другими consumer'ами
237
+ const messages = await redis.xread('COUNT', count, 'BLOCK', blockTime, 'STREAMS', stream, lastId);
238
+ // Проверяем снова после блокирующей операции
239
+ if (isClosing) {
240
+ return;
241
+ }
242
+ if (messages && Array.isArray(messages) && messages.length > 0) {
243
+ // XREAD возвращает массив с одним элементом [streamName, messages]
244
+ const streamData = messages[0];
245
+ const [streamName, streamMessages] = streamData;
246
+ if (Array.isArray(streamMessages) && streamMessages.length > 0) {
247
+ // Обновляем lastReadId на последний прочитанный ID
248
+ const lastMessageId = streamMessages[streamMessages.length - 1][0];
249
+ lastReadIds.set(streamName, lastMessageId);
250
+ for (const [messageId, fields] of streamMessages) {
251
+ if (isClosing) {
252
+ break;
253
+ }
254
+ const messageData = {};
255
+ // Parse fields
256
+ for (let j = 0; j < fields.length; j += 2) {
257
+ messageData[fields[j]] = fields[j + 1];
258
+ }
259
+ // Parse commands if it's a string
260
+ if (messageData.commands && typeof messageData.commands === 'string') {
261
+ try {
262
+ messageData.commands = JSON.parse(messageData.commands);
263
+ }
264
+ catch (e) {
265
+ // Ignore parse errors
266
+ }
267
+ }
268
+ // Триггерим workflow с данными сообщения
269
+ const executionData = {
270
+ json: {
271
+ messageId,
272
+ stream: streamName,
273
+ data: messageData,
274
+ commandList: messageData,
275
+ },
276
+ };
277
+ this.emit([[executionData]]);
278
+ }
279
+ }
280
+ }
281
+ }
282
+ catch (error) {
283
+ // Игнорируем ошибки при закрытии
284
+ if (isClosing && (error.message?.includes('Connection is closed') || error.message?.includes('disconnect'))) {
285
+ return;
286
+ }
287
+ // Игнорируем ошибки для несуществующих streams
288
+ if (error.message?.includes('Invalid stream ID') || error.message?.includes('no such key')) {
289
+ continue;
290
+ }
291
+ // Логируем только критические ошибки
292
+ console.error(`[Chat2Crm Trigger] Error reading stream ${stream} from Redis DB ${db}:`, error);
293
+ }
294
+ }
295
+ }
296
+ };
297
+ // Начинаем polling с более коротким интервалом
298
+ let interval = null;
299
+ const startPolling = () => {
300
+ interval = setInterval(async () => {
301
+ if (!isClosing) {
302
+ await readMessages();
303
+ }
304
+ }, pollInterval);
305
+ };
306
+ startPolling();
307
+ // Первый запуск сразу
308
+ await readMessages();
309
+ // Возвращаем объект с функцией closeFunction для cleanup
310
+ return {
311
+ closeFunction: async () => {
312
+ console.log(`[Chat2Crm Trigger] Closing trigger, cleaning up resources...`);
313
+ isClosing = true; // Устанавливаем флаг закрытия
314
+ try {
315
+ if (interval) {
316
+ clearInterval(interval);
317
+ interval = null;
318
+ }
319
+ // Используем disconnect() вместо quit() для более быстрого закрытия
320
+ for (const [db, redis] of redisConnections.entries()) {
321
+ try {
322
+ redis.disconnect();
323
+ }
324
+ catch (error) {
325
+ console.error(`[Chat2Crm Trigger] Error disconnecting Redis connection for DB ${db}:`, error);
326
+ }
327
+ }
328
+ redisConnections.clear();
329
+ console.log(`[Chat2Crm Trigger] Cleanup completed`);
330
+ }
331
+ catch (error) {
332
+ console.error(`[Chat2Crm Trigger] Error during cleanup:`, error);
333
+ }
334
+ },
335
+ };
336
+ }
337
+ }
338
+ exports.Chat2CrmTrigger = Chat2CrmTrigger;
@@ -0,0 +1,3 @@
1
+ import Redis from 'ioredis';
2
+ import { ITriggerFunctions, IExecuteFunctions } from 'n8n-workflow';
3
+ export declare function createRedisConnection(this: IExecuteFunctions | ITriggerFunctions, credentials: any, db?: number): Promise<Redis>;
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.createRedisConnection = createRedisConnection;
40
+ const ioredis_1 = __importDefault(require("ioredis"));
41
+ const net = __importStar(require("net"));
42
+ /**
43
+ * Нормализует приватный ключ SSH
44
+ * Удаляет лишние пробелы и обеспечивает правильные переносы строк
45
+ */
46
+ function normalizePrivateKey(privateKey) {
47
+ if (!privateKey) {
48
+ return privateKey;
49
+ }
50
+ // Удаляем все пробелы в начале и конце
51
+ let normalized = privateKey.trim();
52
+ // Если ключ уже содержит правильные заголовки, просто нормализуем переносы строк
53
+ if (normalized.includes('-----BEGIN')) {
54
+ // Заменяем все варианты переносов строк на \n
55
+ normalized = normalized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
56
+ // Удаляем лишние пустые строки
57
+ normalized = normalized.replace(/\n{3,}/g, '\n\n');
58
+ // Убеждаемся, что в конце есть перенос строки
59
+ if (!normalized.endsWith('\n')) {
60
+ normalized += '\n';
61
+ }
62
+ return normalized;
63
+ }
64
+ // Если ключ в одну строку, пытаемся определить формат и добавить переносы
65
+ // Это для случаев, когда пользователь вставил ключ без переносов
66
+ if (normalized.length > 50 && !normalized.includes('\n')) {
67
+ // Пытаемся найти начало ключа
68
+ const beginMatch = normalized.match(/-----BEGIN[^-]+-----/);
69
+ const endMatch = normalized.match(/-----END[^-]+-----/);
70
+ if (beginMatch && endMatch) {
71
+ const begin = beginMatch[0];
72
+ const end = endMatch[0];
73
+ const keyContent = normalized.substring(beginMatch.index + begin.length, endMatch.index).trim();
74
+ // Добавляем переносы строк каждые 64 символа (стандарт для PEM)
75
+ const formattedContent = keyContent.replace(/.{64}/g, '$&\n').trim();
76
+ return `${begin}\n${formattedContent}\n${end}\n`;
77
+ }
78
+ }
79
+ return normalized;
80
+ }
81
+ async function createRedisConnection(credentials, db = 0) {
82
+ let redisHost = credentials.host;
83
+ let redisPort = credentials.port;
84
+ // Если используется SSH туннель
85
+ if (credentials.useSSH) {
86
+ console.log(`[Redis Connection] Setting up SSH tunnel to ${credentials.sshHost}:${credentials.sshPort}`);
87
+ let sshCredentials;
88
+ if (credentials.sshAuthenticateWith === 'password') {
89
+ sshCredentials = {
90
+ sshHost: credentials.sshHost,
91
+ sshPort: credentials.sshPort || 22,
92
+ sshUser: credentials.sshUser,
93
+ sshAuthenticateWith: 'password',
94
+ sshPassword: credentials.sshPassword,
95
+ };
96
+ }
97
+ else {
98
+ // Нормализуем приватный ключ перед использованием
99
+ const privateKey = normalizePrivateKey(credentials.privateKey);
100
+ sshCredentials = {
101
+ sshHost: credentials.sshHost,
102
+ sshPort: credentials.sshPort || 22,
103
+ sshUser: credentials.sshUser,
104
+ sshAuthenticateWith: 'privateKey',
105
+ privateKey: privateKey,
106
+ passphrase: credentials.passphrase,
107
+ };
108
+ }
109
+ try {
110
+ // Создаем SSH клиент
111
+ console.log(`[Redis Connection] Creating SSH client...`);
112
+ const sshClient = await this.helpers.getSSHClient(sshCredentials);
113
+ console.log(`[Redis Connection] SSH client created successfully`);
114
+ // Redis на удаленном сервере
115
+ const remoteRedisHost = credentials.host || 'localhost';
116
+ const remoteRedisPort = credentials.port || 6379;
117
+ console.log(`[Redis Connection] Creating tunnel to Redis at ${remoteRedisHost}:${remoteRedisPort} on remote server`);
118
+ // Создаем локальный TCP сервер для проксирования через SSH туннель
119
+ const localServer = net.createServer((localSocket) => {
120
+ // Создаем SSH туннель для каждого соединения
121
+ sshClient.forwardOut('127.0.0.1', 0, remoteRedisHost, remoteRedisPort, (err, stream) => {
122
+ if (err) {
123
+ console.error(`[Redis Connection] SSH forwardOut error:`, err.message);
124
+ localSocket.destroy();
125
+ return;
126
+ }
127
+ // Проксируем данные между локальным сокетом и SSH stream
128
+ localSocket.pipe(stream);
129
+ stream.pipe(localSocket);
130
+ // Обработка ошибок
131
+ localSocket.on('error', (err) => {
132
+ console.error(`[Redis Connection] Local socket error:`, err.message);
133
+ stream.destroy();
134
+ });
135
+ stream.on('error', (err) => {
136
+ console.error(`[Redis Connection] SSH stream error:`, err.message);
137
+ localSocket.destroy();
138
+ });
139
+ });
140
+ });
141
+ // Запускаем локальный сервер на случайном порту
142
+ await new Promise((resolve, reject) => {
143
+ localServer.listen(0, '127.0.0.1', () => {
144
+ const address = localServer.address();
145
+ if (address && typeof address === 'object') {
146
+ redisHost = '127.0.0.1';
147
+ redisPort = address.port;
148
+ console.log(`[Redis Connection] SSH tunnel established: localhost:${redisPort} -> ${remoteRedisHost}:${remoteRedisPort} via ${credentials.sshHost}:${credentials.sshPort}`);
149
+ resolve();
150
+ }
151
+ else {
152
+ reject(new Error('Failed to get local server address'));
153
+ }
154
+ });
155
+ localServer.on('error', (err) => {
156
+ reject(err);
157
+ });
158
+ });
159
+ }
160
+ catch (error) {
161
+ console.error(`[Redis Connection] Failed to create SSH tunnel:`, error.message);
162
+ throw new Error(`SSH tunnel failed: ${error.message}`);
163
+ }
164
+ }
165
+ else {
166
+ console.log(`[Redis Connection] Direct connection to Redis at ${redisHost}:${redisPort}`);
167
+ }
168
+ // Создаем подключение к Redis
169
+ console.log(`[Redis Connection] Connecting to Redis: ${redisHost}:${redisPort}, DB: ${db}`);
170
+ const redis = new ioredis_1.default({
171
+ host: redisHost,
172
+ port: redisPort,
173
+ db: db,
174
+ password: credentials.password,
175
+ enableReadyCheck: true,
176
+ maxRetriesPerRequest: 3,
177
+ connectTimeout: 10000, // 10 секунд таймаут подключения
178
+ retryStrategy: (times) => {
179
+ const delay = Math.min(times * 50, 2000);
180
+ return delay;
181
+ },
182
+ });
183
+ // Добавляем обработчики событий для диагностики
184
+ redis.on('connect', () => {
185
+ console.log(`[Redis Connection] Connected to Redis at ${redisHost}:${redisPort}, DB ${db}`);
186
+ });
187
+ redis.on('ready', () => {
188
+ console.log(`[Redis Connection] Redis ready at ${redisHost}:${redisPort}, DB ${db}`);
189
+ });
190
+ redis.on('error', (err) => {
191
+ console.error(`[Redis Connection] Redis error on ${redisHost}:${redisPort}, DB ${db}:`, err.message);
192
+ });
193
+ redis.on('close', () => {
194
+ console.log(`[Redis Connection] Redis connection closed: ${redisHost}:${redisPort}, DB ${db}`);
195
+ });
196
+ return redis;
197
+ }
@@ -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>
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ nodes: [
3
+ require('./dist/nodes/Chat2CrmTrigger/Chat2CrmTrigger.node.js'),
4
+ ],
5
+ credentials: [
6
+ require('./dist/credentials/Chat2CrmRedisApi.credentials.js'),
7
+ ],
8
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "n8n-nodes-chat2crm",
3
+ "version": "0.1.0",
4
+ "description": "n8n node for Chat2Crm Redis integration",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "chat2crm",
8
+ "redis",
9
+ "messaging"
10
+ ],
11
+ "main": "index.js",
12
+ "scripts": {
13
+ "build": "rm -rf dist && tsc && gulp build:icons",
14
+ "dev": "npm run build && npx n8n",
15
+ "dev:watch": "tsc --watch",
16
+ "format": "prettier nodes credentials --write",
17
+ "lint": "eslint \"nodes/**/*.ts\" \"credentials/**/*.ts\" \"package.json\"",
18
+ "lintfix": "eslint \"nodes/**/*.ts\" \"credentials/**/*.ts\" \"package.json\" --fix",
19
+ "prepublishOnly": "npm run build && npm run lint -s"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "n8n": {
25
+ "n8nNodesApiVersion": 1,
26
+ "credentials": [
27
+ "dist/credentials/Chat2CrmRedisApi.credentials.js"
28
+ ],
29
+ "nodes": [
30
+ "dist/nodes/Chat2CrmTrigger/Chat2CrmTrigger.node.js"
31
+ ]
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.10.0",
35
+ "@typescript-eslint/parser": "^6.13.0",
36
+ "@typescript-eslint/eslint-plugin": "^6.13.0",
37
+ "@types/uuid": "^9.0.7",
38
+ "eslint": "^8.57.1",
39
+ "eslint-plugin-n8n-nodes-base": "^1.11.0",
40
+ "gulp": "^4.0.2",
41
+ "n8n": "*",
42
+ "n8n-workflow": "*",
43
+ "prettier": "^3.1.0",
44
+ "typescript": "^5.3.0"
45
+ },
46
+ "dependencies": {
47
+ "ioredis": "^5.3.2",
48
+ "uuid": "^9.0.1"
49
+ },
50
+ "peerDependencies": {
51
+ "n8n-workflow": "*"
52
+ }
53
+ }