n8n-nodes-chat2crm 0.1.0 → 0.1.3
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
ADDED
|
File without changes
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { ITriggerFunctions, INodeType, INodeTypeDescription, ITriggerResponse } from 'n8n-workflow';
|
|
1
|
+
import { ITriggerFunctions, INodeType, INodeTypeDescription, ITriggerResponse, ILoadOptionsFunctions, INodePropertyOptions } 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;
|
|
4
6
|
trigger(this: ITriggerFunctions): Promise<ITriggerResponse | undefined>;
|
|
5
7
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.Chat2CrmTrigger = void 0;
|
|
4
7
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
5
9
|
const RedisConnection_1 = require("./Infra/RedisConnection");
|
|
6
10
|
class Chat2CrmTrigger {
|
|
7
11
|
constructor() {
|
|
@@ -26,64 +30,15 @@ class Chat2CrmTrigger {
|
|
|
26
30
|
],
|
|
27
31
|
properties: [
|
|
28
32
|
{
|
|
29
|
-
displayName: '
|
|
33
|
+
displayName: 'Stream Names or IDs',
|
|
30
34
|
name: 'streams',
|
|
31
35
|
type: 'multiOptions',
|
|
32
|
-
required: true,
|
|
33
|
-
description: '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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'],
|
|
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: [],
|
|
87
42
|
},
|
|
88
43
|
{
|
|
89
44
|
displayName: 'Block Time (Ms)',
|
|
@@ -109,6 +64,188 @@ class Chat2CrmTrigger {
|
|
|
109
64
|
],
|
|
110
65
|
};
|
|
111
66
|
}
|
|
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
|
+
}
|
|
112
249
|
async trigger() {
|
|
113
250
|
const credentials = await this.getCredentials('chat2CrmRedisApi');
|
|
114
251
|
const selectedStreams = this.getNodeParameter('streams', []);
|
package/index.js
CHANGED
|
@@ -1,8 +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
|
-
};
|
|
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
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "n8n-nodes-chat2crm",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-chat2crm",
|
|
3
|
+
"version": "0.1.3",
|
|
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
|
+
}
|