n8n-nodes-zalo-custom 1.0.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,324 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendToTelegram = exports.deleteSessionByUserId = exports.updateSessionInfo = exports.saveOrUpdateSession = exports.getZaloApiClient = exports.imageMetadataGetter = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const zca_js_1 = require("zca-js");
6
+ const image_size_1 = require("image-size");
7
+ const fs_1 = require("fs");
8
+ const axios_1 = require("axios");
9
+ const FormData = require("form-data");
10
+ const sql_js_1 = require("sql.js");
11
+ const crypto_helper_1 = require("./crypto.helper");
12
+ const path_1 = require("path");
13
+
14
+ let db;
15
+
16
+ /**
17
+ * Initializes the SQLite database using sql.js.
18
+ * Creates the database file if it doesn't exist and sets up the necessary tables.
19
+ */
20
+ async function initDb() {
21
+ if (db)
22
+ return db;
23
+ const userFolder = process.env.N8N_USER_FOLDER
24
+ ? process.env.N8N_USER_FOLDER
25
+ : path_1.join(process.env.HOME || process.env.USERPROFILE || '.', '.n8n');
26
+ const tempDir = path_1.join(userFolder, 'temp_files');
27
+ if (!fs_1.existsSync(tempDir)) {
28
+ fs_1.mkdirSync(tempDir, { recursive: true });
29
+ }
30
+ const dbPath = path_1.join(tempDir, 'zalo_sessions.db');
31
+ try {
32
+ const filebuffer = fs_1.existsSync(dbPath) ? fs_1.readFileSync(dbPath) : null;
33
+ const SQL = await (0, sql_js_1.default)();
34
+ db = new SQL.Database(filebuffer);
35
+ const createTableQuery = `
36
+ CREATE TABLE IF NOT EXISTS zalo_sessions (
37
+ user_id TEXT PRIMARY KEY,
38
+ name TEXT,
39
+ phone TEXT UNIQUE,
40
+ credential_id TEXT,
41
+ encrypted_data TEXT,
42
+ updated_at TEXT
43
+ );`;
44
+ db.exec(createTableQuery);
45
+ }
46
+ catch (e) {
47
+ console.error(`Failed to initialize database at ${dbPath}:`, e);
48
+ throw e;
49
+ }
50
+ return db;
51
+ }
52
+
53
+ /**
54
+ * Persists the database changes to the file system.
55
+ */
56
+ async function persistDb() {
57
+ if (!db) return;
58
+ try {
59
+ const userFolder =
60
+ process.env.N8N_USER_FOLDER ||
61
+ path_1.join(process.env.HOME || process.env.USERPROFILE || '.', '.n8n');
62
+ const tempDir = path_1.join(userFolder, 'temp_files');
63
+ const dbPath = path_1.join(tempDir, 'zalo_sessions.db');
64
+ const data = db.export();
65
+ await fs_1.promises.writeFile(dbPath, Buffer.from(data));
66
+ } catch (e) {
67
+ console.error(`Failed to persist database to file:`, e);
68
+ throw e;
69
+ }
70
+ }
71
+
72
+ async function imageMetadataGetter(filePath) {
73
+ try {
74
+ const stats = await fs_1.promises.stat(filePath);
75
+ const dimensions = (0, image_size_1.default)(filePath);
76
+ return {
77
+ height: dimensions.height,
78
+ width: dimensions.width,
79
+ size: stats.size,
80
+ };
81
+ }
82
+ catch (e) {
83
+ if (e.code === 'ENOENT' || e.code === 'EACCES') {
84
+ console.log(`Failed to access file at path: ${filePath}. Error: ${e.message}`);
85
+ }
86
+ else {
87
+ console.log(`Could not get metadata for file at path: ${filePath}. It might not be a valid image file. Error: ${e.message}`);
88
+ }
89
+ }
90
+ }
91
+ exports.imageMetadataGetter = imageMetadataGetter;
92
+
93
+ /**
94
+ * Initializes and returns a Zalo API client.
95
+ * It prioritizes credentials in this order:
96
+ * 1. A user ID provided in the 'User ID' field, which is used to look up a session in the `zalo_sessions.json` file.
97
+ * 2. A Zalo API credential selected in the node's UI.
98
+ *
99
+ * @param {IExecuteFunctions} node The execution context of the node (`this`)
100
+ * @param {object} [options={}] Optional parameters.
101
+ * @param {boolean} [options.needsImageMetadataGetter=false] get image metadata.
102
+ * @param {boolean} [options.selfListen=false] listen owner events triggered.
103
+ * @returns {Promise<any>} A promise that resolves to an initialized Zalo API client.
104
+ * @throws {NodeOperationError} If no valid credentials or session can be found.
105
+ */
106
+ async function getZaloApiClient(node, options = {}) {
107
+ const useSession = node.getNodeParameter('useSession', 0, false);
108
+ const userId = node.getNodeParameter('connectToId', 0, '');
109
+ const { needsImageMetadataGetter = false, selfListen = false } = options;
110
+ let cookie, imei, userAgent, sessionInfo = null, actualZaloId = '';
111
+ if (useSession && userId) {
112
+ try {
113
+ await initDb();
114
+ } catch (e) {
115
+ throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[Session] Database initialization failed: ${e.message}. Please ensure a 'Zalo Login by QR' node has run successfully first.`);
116
+ }
117
+
118
+ try {
119
+ const isPhoneNumber = /^\d{10}$/.test(userId);
120
+ let stmt;
121
+ try {
122
+ if (isPhoneNumber) {
123
+ stmt = db.prepare("SELECT user_id, encrypted_data FROM zalo_sessions WHERE phone = :phone");
124
+ const result = stmt.getAsObject({ ':phone': userId });
125
+ if (result && result.user_id) {
126
+ actualZaloId = result.user_id;
127
+ sessionInfo = { data: result.encrypted_data };
128
+ }
129
+ }
130
+ else {
131
+ stmt = db.prepare("SELECT encrypted_data FROM zalo_sessions WHERE user_id = :user_id");
132
+ const result = stmt.getAsObject({ ':user_id': userId });
133
+ actualZaloId = userId;
134
+ if (result && result.encrypted_data) {
135
+ sessionInfo = { data: result.encrypted_data };
136
+ }
137
+ }
138
+ } finally {
139
+ if (stmt) stmt.free();
140
+ }
141
+ if (sessionInfo && sessionInfo.data) {
142
+ const encryptionKey = actualZaloId.repeat(3);
143
+ try {
144
+ const sessionData = (0, crypto_helper_1.decrypt)(sessionInfo.data, encryptionKey);
145
+ cookie = sessionData.cookie;
146
+ imei = sessionData.imei;
147
+ userAgent = sessionData.userAgent;
148
+ }
149
+ catch (e) {
150
+ throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[Session] Failed to decrypt session for Zalo ID: "${actualZaloId}". The file might be corrupt or the key has changed. Error: ${e.message}`);
151
+ }
152
+ }
153
+ else {
154
+ const idType = isPhoneNumber ? 'Phone number' : 'Zalo User ID';
155
+ throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[Session] ${idType} "${userId}" was provided, but no matching session was found.`);
156
+ }
157
+ }
158
+ catch (e) {
159
+ if (e instanceof n8n_workflow_1.NodeOperationError) throw e;
160
+ throw new n8n_workflow_1.NodeOperationError(node.getNode(), `[Session] An unexpected database error occurred: ${e.message}.`);
161
+ }
162
+ } else if (!useSession) {
163
+ const zaloCred = await node.getCredentials('zaloApi');
164
+ cookie = JSON.parse(zaloCred.cookie);
165
+ imei = zaloCred.imei;
166
+ userAgent = zaloCred.userAgent;
167
+ }
168
+ const zaloOptions = {};
169
+ if (needsImageMetadataGetter) {
170
+ zaloOptions.imageMetadataGetter = imageMetadataGetter;
171
+ }
172
+ if (selfListen) {
173
+ zaloOptions.selfListen = selfListen;
174
+ }
175
+ const zalo = new zca_js_1.Zalo(zaloOptions);
176
+ return zalo.login({ cookie, imei, userAgent });
177
+ }
178
+ exports.getZaloApiClient = getZaloApiClient;
179
+ /**
180
+ * Saves or updates a user's session in the database.
181
+ * @param {object} sessionDetails - The details of the session to save.
182
+ * @returns {Promise<{oldCredentialId: string | null}>} - The old credential ID if it existed.
183
+ */
184
+ async function saveOrUpdateSession(sessionDetails) {
185
+ try {
186
+ const { userId, name, phone, credentialId, encryptedData } = sessionDetails;
187
+ await initDb();
188
+ let oldCredentialId = null;
189
+
190
+ const selectStmt = db.prepare('SELECT credential_id FROM zalo_sessions WHERE user_id = :user_id');
191
+ const existing = selectStmt.getAsObject({ ':user_id': userId });
192
+ selectStmt.free();
193
+ if (existing) {
194
+ oldCredentialId = existing.credential_id;
195
+ }
196
+
197
+ const stmt = db.prepare(`
198
+ INSERT INTO zalo_sessions (user_id, name, phone, credential_id, encrypted_data, updated_at)
199
+ VALUES (:user_id, :name, :phone, :credential_id, :encrypted_data, :updated_at)
200
+ ON CONFLICT(user_id) DO UPDATE SET
201
+ name = excluded.name,
202
+ phone = excluded.phone,
203
+ credential_id = excluded.credential_id,
204
+ encrypted_data = excluded.encrypted_data,
205
+ updated_at = excluded.updated_at;
206
+ `);
207
+ stmt.run({
208
+ ':user_id': userId,
209
+ ':name': name,
210
+ ':phone': phone,
211
+ ':credential_id': credentialId,
212
+ ':encrypted_data': encryptedData,
213
+ ':updated_at': new Date().toISOString(),
214
+ });
215
+ stmt.free();
216
+
217
+ await persistDb();
218
+
219
+ return { oldCredentialId };
220
+ } catch (e) {
221
+ console.error(`[saveOrUpdateSession] Database operation failed:`, e); // eslint-disable-line no-console
222
+ throw e;
223
+ }
224
+ }
225
+ exports.saveOrUpdateSession = saveOrUpdateSession;
226
+ /**
227
+ * Updates only the name and phone number for a given user ID in the database.
228
+ * @param {string} userId - The Zalo user ID.
229
+ * @param {string} name - The new display name.
230
+ * @param {string} phone - The new phone number.
231
+ * @returns {Promise<void>}
232
+ */
233
+ async function updateSessionInfo(userId, name, phone) {
234
+ try {
235
+ await initDb();
236
+ const stmt = db.prepare(`
237
+ UPDATE zalo_sessions
238
+ SET
239
+ name = :name,
240
+ phone = :phone,
241
+ updated_at = :updated_at
242
+ WHERE user_id = :user_id;
243
+ `);
244
+ stmt.run({
245
+ ':user_id': userId,
246
+ ':name': name,
247
+ ':phone': phone,
248
+ ':updated_at': new Date().toISOString(),
249
+ });
250
+ stmt.free();
251
+ await persistDb();
252
+ } catch (e) {
253
+ console.error(`[updateSessionInfo] Database operation failed for user ${userId}:`, e);
254
+ throw e;
255
+ }
256
+ }
257
+ exports.updateSessionInfo = updateSessionInfo;
258
+ /**
259
+ * Deletes a user's session from the database by user ID.
260
+ * @param {string} userId - The Zalo user ID of the session to delete.
261
+ * @returns {Promise<void>}
262
+ */
263
+ async function deleteSessionByUserId(userId) {
264
+ try {
265
+ await initDb();
266
+ const stmt = db.prepare('DELETE FROM zalo_sessions WHERE user_id = :user_id');
267
+ stmt.run({ ':user_id': userId });
268
+ stmt.free();
269
+ await persistDb();
270
+ } catch (e) {
271
+ console.error(`[deleteSessionByUserId] Database operation failed for user ${userId}:`, e);
272
+ throw e;
273
+ }
274
+ }
275
+ exports.deleteSessionByUserId = deleteSessionByUserId;
276
+
277
+ /**
278
+ * Gửi tin nhắn hoặc file đến một cuộc trò chuyện trên Telegram.
279
+ *
280
+ * @param {object} params Các tham số để gửi đến Telegram.
281
+ * @param {string} params.token Token của bot Telegram.
282
+ * @param {string} params.chatId ID của cuộc trò chuyện trên Telegram.
283
+ * @param {string} [params.text] Tin nhắn văn bản cần gửi.
284
+ * @param {Buffer} [params.binaryData] Dữ liệu nhị phân của file cần gửi.
285
+ * @param {string} [params.fileName] Tên của file.
286
+ * @param {string} [params.caption] Chú thích cho file.
287
+ * @param {INodeExecutionFunctions['logger']} [params.logger] Logger để ghi lại thông tin.
288
+ * @returns {Promise<void>}
289
+ * @throws {Error} Nếu gửi thất bại.
290
+ */
291
+ async function sendToTelegram({ token, chatId, text, binaryData, fileName, caption, logger, }) {
292
+ if (!token || !chatId) {
293
+ logger === null || logger === void 0 ? void 0 : logger.warn('Telegram token và chatId là bắt buộc nhưng không được cung cấp.');
294
+ return;
295
+ }
296
+ const baseUrl = `https://api.telegram.org/bot${token}`;
297
+ try {
298
+ if (binaryData && fileName) {
299
+ const form = new FormData();
300
+ form.append('chat_id', chatId);
301
+ form.append('photo', binaryData, fileName);
302
+ if (caption) {
303
+ form.append('caption', caption);
304
+ }
305
+ const url = `${baseUrl}/sendPhoto`;
306
+ await axios_1.default.post(url, form, { headers: form.getHeaders() });
307
+ logger === null || logger === void 0 ? void 0 : logger.info(` ..sendPhoto Telegram: ${caption}`);
308
+ }
309
+ else if (text) {
310
+ const url = `${baseUrl}/sendMessage`;
311
+ await axios_1.default.post(url, {
312
+ chat_id: chatId,
313
+ text: text,
314
+ });
315
+ logger === null || logger === void 0 ? void 0 : logger.info(` ..sendMessage Telegram: ${text}`);
316
+ }
317
+ }
318
+ catch (error) {
319
+ const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
320
+ logger === null || logger === void 0 ? void 0 : logger.error(` ..Fail send Telegram: ${errorMessage}`);
321
+ throw new Error(`Gửi đến Telegram thất bại: ${errorMessage}`);
322
+ }
323
+ }
324
+ exports.sendToTelegram = sendToTelegram;
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "n8n-nodes-zalo-custom",
3
+ "version": "1.0.0",
4
+ "description": "n8n nodes for Zalo automation. Send messages, manage groups, friends, and listen to events without third-party services.",
5
+ "keywords": [
6
+ "n8n",
7
+ "n8n-community-node-package",
8
+ "zalo",
9
+ "automation",
10
+ "zalo api"
11
+ ],
12
+ "license": "MIT",
13
+ "homepage": "https://github.com/codedao12/n8n-nodes-zalo#readme",
14
+ "author": {
15
+ "name": "codedao12",
16
+ "email": "codedao12@gmail.com"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/codedao12/n8n-nodes-zalo.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/codedao12/n8n-nodes-zalo/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.10",
27
+ "pnpm": ">=9.1"
28
+ },
29
+ "files": [
30
+ "nodes",
31
+ "credentials"
32
+ ],
33
+ "main": "index.js",
34
+ "n8n": {
35
+ "n8nNodesApiVersion": 1,
36
+ "credentials": [
37
+ "credentials/N8nZaloApi.credentials.js",
38
+ "credentials/ZaloApi.credentials.js"
39
+ ],
40
+ "nodes": [
41
+ "nodes/ZaloLoginByQr/ZaloLoginByQr.node.js",
42
+ "nodes/ZaloTrigger/ZaloTrigger.node.js",
43
+ "nodes/ZaloSendMessage/ZaloSendMessage.node.js",
44
+ "nodes/ZaloUser/ZaloUser.node.js",
45
+ "nodes/ZaloGroup/ZaloGroup.node.js",
46
+ "nodes/ZaloCommunication/ZaloCommunication.node.js"
47
+ ]
48
+ },
49
+ "dependencies": {
50
+ "axios": "^1.8.4",
51
+ "express": "^5.1.0",
52
+ "zca-js": "^2.0.4",
53
+ "image-size": "^1.1.1",
54
+ "sql.js": "^1.10.3"
55
+ },
56
+ "scripts": {
57
+ "start": "n8n start",
58
+ "dev": "nodemon --watch nodes --exec \"n8n start --tunnel\""
59
+ }
60
+ }