@dongdev/fca-unofficial 3.0.0 → 3.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongdev/fca-unofficial",
3
- "version": "3.0.0",
3
+ "version": "3.0.3",
4
4
  "description": "Unofficial Facebook Chat API for Node.js - Interact with Facebook Messenger programmatically",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const logger = require("../../../func/logger");
5
+
6
+ /**
7
+ * Enable automatic AppState saving
8
+ * @param {Object} options - Options for auto-save
9
+ * @param {string} [options.filePath] - Path to save AppState (default: "appstate.json")
10
+ * @param {number} [options.interval] - Save interval in milliseconds (default: 10 minutes)
11
+ * @param {boolean} [options.saveOnLogin] - Save immediately on login (default: true)
12
+ * @returns {Function} Function to disable auto-save
13
+ */
14
+ module.exports = function (defaultFuncs, api, ctx) {
15
+ return function enableAutoSaveAppState(options = {}) {
16
+ const filePath = options.filePath || path.join(process.cwd(), "appstate.json");
17
+ const interval = options.interval || 10 * 60 * 1000; // 10 minutes default
18
+ const saveOnLogin = options.saveOnLogin !== false; // default true
19
+
20
+ // Save function
21
+ function saveAppState() {
22
+ try {
23
+ const appState = api.getAppState();
24
+ if (!appState || !appState.appState || appState.appState.length === 0) {
25
+ logger("AppState is empty, skipping save", "warn");
26
+ return;
27
+ }
28
+
29
+ const data = JSON.stringify(appState, null, 2);
30
+ fs.writeFileSync(filePath, data, "utf8");
31
+ logger(`AppState saved to ${filePath}`, "info");
32
+ } catch (error) {
33
+ logger(`Error saving AppState: ${error && error.message ? error.message : String(error)}`, "error");
34
+ }
35
+ }
36
+
37
+ // Save immediately if requested
38
+ let immediateSaveTimer = null;
39
+ if (saveOnLogin) {
40
+ // Delay a bit to ensure login is complete
41
+ immediateSaveTimer = setTimeout(() => {
42
+ saveAppState();
43
+ immediateSaveTimer = null;
44
+ }, 2000);
45
+ }
46
+
47
+ // Set up interval
48
+ const intervalId = setInterval(saveAppState, interval);
49
+ logger(`Auto-save AppState enabled: ${filePath} (every ${Math.round(interval / 1000 / 60)} minutes)`, "info");
50
+
51
+ // Store interval ID for cleanup
52
+ if (!ctx._autoSaveInterval) {
53
+ ctx._autoSaveInterval = [];
54
+ }
55
+ ctx._autoSaveInterval.push(intervalId);
56
+
57
+ // Return disable function
58
+ return function disableAutoSaveAppState() {
59
+ // Clear immediate save timer if still pending
60
+ if (immediateSaveTimer) {
61
+ clearTimeout(immediateSaveTimer);
62
+ immediateSaveTimer = null;
63
+ }
64
+ // Clear interval
65
+ clearInterval(intervalId);
66
+ const index = ctx._autoSaveInterval ? ctx._autoSaveInterval.indexOf(intervalId) : -1;
67
+ if (index !== -1) {
68
+ ctx._autoSaveInterval.splice(index, 1);
69
+ }
70
+ logger("Auto-save AppState disabled", "info");
71
+ };
72
+ };
73
+ };
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ const logger = require("../../../func/logger");
3
+
4
+ /**
5
+ * Message Scheduler System
6
+ * Allows scheduling messages to be sent at a specific time in the future
7
+ */
8
+ module.exports = function (defaultFuncs, api, ctx) {
9
+ // Initialize scheduler on first call
10
+ if (!ctx._scheduler) {
11
+ ctx._scheduler = createSchedulerInstance(api);
12
+ }
13
+
14
+ return ctx._scheduler;
15
+ };
16
+
17
+ function createSchedulerInstance(api) {
18
+ const scheduledMessages = new Map(); // Map<id, ScheduledMessage>
19
+ let nextId = 1;
20
+
21
+ /**
22
+ * Scheduled Message Object
23
+ * @typedef {Object} ScheduledMessage
24
+ * @property {string} id - Unique identifier
25
+ * @property {string|Object} message - Message content
26
+ * @property {string|string[]} threadID - Target thread ID(s)
27
+ * @property {number} timestamp - When to send (Unix timestamp in ms)
28
+ * @property {number} createdAt - When scheduled (Unix timestamp in ms)
29
+ * @property {Object} options - Additional options (replyMessageID, isGroup, etc.)
30
+ * @property {Function} callback - Optional callback when sent
31
+ * @property {NodeJS.Timeout} timeout - Timeout reference
32
+ * @property {boolean} cancelled - Whether cancelled
33
+ */
34
+
35
+ /**
36
+ * Schedule a message to be sent at a specific time
37
+ * @param {string|Object} message - Message content
38
+ * @param {string|string[]} threadID - Target thread ID(s)
39
+ * @param {Date|number|string} when - When to send (Date, timestamp, or ISO string)
40
+ * @param {Object} [options] - Additional options
41
+ * @param {string} [options.replyMessageID] - Message ID to reply to
42
+ * @param {boolean} [options.isGroup] - Whether it's a group chat
43
+ * @param {Function} [options.callback] - Callback when sent
44
+ * @returns {string} Scheduled message ID
45
+ */
46
+ function scheduleMessage(message, threadID, when, options = {}) {
47
+ let timestamp;
48
+
49
+ // Parse when parameter
50
+ if (when instanceof Date) {
51
+ timestamp = when.getTime();
52
+ } else if (typeof when === "number") {
53
+ timestamp = when;
54
+ } else if (typeof when === "string") {
55
+ timestamp = new Date(when).getTime();
56
+ } else {
57
+ throw new Error("Invalid 'when' parameter. Must be Date, number (timestamp), or ISO string");
58
+ }
59
+
60
+ // Validate timestamp
61
+ if (isNaN(timestamp)) {
62
+ throw new Error("Invalid date/time");
63
+ }
64
+
65
+ const now = Date.now();
66
+ if (timestamp <= now) {
67
+ throw new Error("Scheduled time must be in the future");
68
+ }
69
+
70
+ const id = `scheduled_${nextId++}_${Date.now()}`;
71
+ const delay = timestamp - now;
72
+
73
+ // Create scheduled message object
74
+ const scheduled = {
75
+ id,
76
+ message,
77
+ threadID,
78
+ timestamp,
79
+ createdAt: now,
80
+ options: {
81
+ replyMessageID: options.replyMessageID,
82
+ isGroup: options.isGroup,
83
+ callback: options.callback
84
+ },
85
+ cancelled: false
86
+ };
87
+
88
+ // Set timeout to send message
89
+ scheduled.timeout = setTimeout(() => {
90
+ if (scheduled.cancelled) return;
91
+
92
+ try {
93
+ logger(`Sending scheduled message ${id}`, "info");
94
+
95
+ // Send message
96
+ api.sendMessage(
97
+ message,
98
+ threadID,
99
+ scheduled.options.callback || (() => {}),
100
+ scheduled.options.replyMessageID,
101
+ scheduled.options.isGroup
102
+ ).then(() => {
103
+ logger(`Scheduled message ${id} sent successfully`, "info");
104
+ scheduledMessages.delete(id);
105
+ }).catch(err => {
106
+ logger(`Error sending scheduled message ${id}: ${err && err.message ? err.message : String(err)}`, "error");
107
+ if (scheduled.options.callback) {
108
+ scheduled.options.callback(err);
109
+ }
110
+ scheduledMessages.delete(id);
111
+ });
112
+ } catch (err) {
113
+ logger(`Error in scheduled message ${id}: ${err && err.message ? err.message : String(err)}`, "error");
114
+ scheduledMessages.delete(id);
115
+ }
116
+ }, delay);
117
+
118
+ scheduledMessages.set(id, scheduled);
119
+ logger(`Message scheduled: ${id} (in ${Math.round(delay / 1000)}s)`, "info");
120
+
121
+ return id;
122
+ }
123
+
124
+ /**
125
+ * Cancel a scheduled message
126
+ * @param {string} id - Scheduled message ID
127
+ * @returns {boolean} True if cancelled, false if not found
128
+ */
129
+ function cancelScheduledMessage(id) {
130
+ const scheduled = scheduledMessages.get(id);
131
+ if (!scheduled) {
132
+ return false;
133
+ }
134
+
135
+ if (scheduled.cancelled) {
136
+ return false; // Already cancelled
137
+ }
138
+
139
+ clearTimeout(scheduled.timeout);
140
+ scheduled.cancelled = true;
141
+ scheduledMessages.delete(id);
142
+ logger(`Scheduled message ${id} cancelled`, "info");
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Get scheduled message info
148
+ * @param {string} id - Scheduled message ID
149
+ * @returns {ScheduledMessage|null} Scheduled message or null if not found
150
+ */
151
+ function getScheduledMessage(id) {
152
+ const scheduled = scheduledMessages.get(id);
153
+ if (!scheduled || scheduled.cancelled) {
154
+ return null;
155
+ }
156
+
157
+ // Return a copy without internal properties
158
+ return {
159
+ id: scheduled.id,
160
+ message: scheduled.message,
161
+ threadID: scheduled.threadID,
162
+ timestamp: scheduled.timestamp,
163
+ createdAt: scheduled.createdAt,
164
+ options: { ...scheduled.options },
165
+ timeUntilSend: scheduled.timestamp - Date.now()
166
+ };
167
+ }
168
+
169
+ /**
170
+ * List all scheduled messages
171
+ * @returns {ScheduledMessage[]} Array of scheduled messages
172
+ */
173
+ function listScheduledMessages() {
174
+ const now = Date.now();
175
+ const list = [];
176
+
177
+ for (const scheduled of scheduledMessages.values()) {
178
+ if (scheduled.cancelled) continue;
179
+
180
+ list.push({
181
+ id: scheduled.id,
182
+ message: scheduled.message,
183
+ threadID: scheduled.threadID,
184
+ timestamp: scheduled.timestamp,
185
+ createdAt: scheduled.createdAt,
186
+ options: { ...scheduled.options },
187
+ timeUntilSend: scheduled.timestamp - now
188
+ });
189
+ }
190
+
191
+ // Sort by timestamp
192
+ return list.sort((a, b) => a.timestamp - b.timestamp);
193
+ }
194
+
195
+ /**
196
+ * Cancel all scheduled messages
197
+ * @returns {number} Number of cancelled messages
198
+ */
199
+ function cancelAllScheduledMessages() {
200
+ let count = 0;
201
+ for (const id of scheduledMessages.keys()) {
202
+ if (cancelScheduledMessage(id)) {
203
+ count++;
204
+ }
205
+ }
206
+ logger(`Cancelled ${count} scheduled messages`, "info");
207
+ return count;
208
+ }
209
+
210
+ /**
211
+ * Get count of scheduled messages
212
+ * @returns {number} Count
213
+ */
214
+ function getScheduledCount() {
215
+ return scheduledMessages.size;
216
+ }
217
+
218
+ /**
219
+ * Clear expired/cancelled messages from memory
220
+ */
221
+ function cleanup() {
222
+ const now = Date.now();
223
+ let cleaned = 0;
224
+
225
+ for (const [id, scheduled] of scheduledMessages.entries()) {
226
+ if (scheduled.cancelled || scheduled.timestamp < now) {
227
+ scheduledMessages.delete(id);
228
+ cleaned++;
229
+ }
230
+ }
231
+
232
+ if (cleaned > 0) {
233
+ logger(`Cleaned up ${cleaned} scheduled messages`, "info");
234
+ }
235
+ }
236
+
237
+ // Auto cleanup every 5 minutes
238
+ const cleanupInterval = setInterval(cleanup, 5 * 60 * 1000);
239
+
240
+ /**
241
+ * Destroy scheduler and cleanup all resources
242
+ * @returns {number} Number of cancelled messages
243
+ */
244
+ function destroy() {
245
+ clearInterval(cleanupInterval);
246
+ const count = cancelAllScheduledMessages();
247
+ logger("Scheduler destroyed and all resources cleaned up", "info");
248
+ return count;
249
+ }
250
+
251
+ // Return scheduler API
252
+ return {
253
+ scheduleMessage,
254
+ cancelScheduledMessage,
255
+ getScheduledMessage,
256
+ listScheduledMessages,
257
+ cancelAllScheduledMessages,
258
+ getScheduledCount,
259
+ cleanup,
260
+ destroy,
261
+ // Cleanup interval reference for manual cleanup if needed
262
+ _cleanupInterval: cleanupInterval
263
+ };
264
+ }
@@ -1,7 +1,5 @@
1
1
  "use strict";
2
2
  const { formatID } = require("../../../utils/format");
3
- const uuid = require("uuid");
4
- "use strict";
5
3
  module.exports = function createListenMqtt(deps) {
6
4
  const { WebSocket, mqtt, HttpsProxyAgent, buildStream, buildProxy,
7
5
  topics, parseDelta, getTaskResponseData, logger, emitAuth
@@ -87,7 +85,11 @@ module.exports = function createListenMqtt(deps) {
87
85
  options
88
86
  );
89
87
  const mqttClient = ctx.mqttClient;
90
- global.mqttClient = mqttClient;
88
+ // Remove global reference to prevent memory leak
89
+ // Only set if needed for debugging, but clear on cleanup
90
+ if (process.env.DEBUG_MQTT) {
91
+ global.mqttClient = mqttClient;
92
+ }
91
93
 
92
94
  mqttClient.on("error", function (err) {
93
95
  const msg = String(err && err.message ? err.message : err || "");
@@ -145,7 +147,8 @@ module.exports = function createListenMqtt(deps) {
145
147
  mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
146
148
  mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
147
149
  const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
148
- const rTimeout = setTimeout(function () {
150
+ let rTimeout = setTimeout(function () {
151
+ rTimeout = null;
149
152
  if (ctx._ending) {
150
153
  logger("mqtt t_ms timeout skipped - ending", "warn");
151
154
  return;
@@ -159,8 +162,17 @@ module.exports = function createListenMqtt(deps) {
159
162
  scheduleReconnect(d);
160
163
  }, 5000);
161
164
 
165
+ // Store timeout reference for cleanup
166
+ ctx._rTimeout = rTimeout;
167
+
162
168
  ctx.tmsWait = function () {
163
- clearTimeout(rTimeout);
169
+ if (rTimeout) {
170
+ clearTimeout(rTimeout);
171
+ rTimeout = null;
172
+ }
173
+ if (ctx._rTimeout) {
174
+ delete ctx._rTimeout;
175
+ }
164
176
  if (ctx.globalOptions.emitReady) globalCallback({ type: "ready", error: null });
165
177
  delete ctx.tmsWait;
166
178
  };
@@ -33,6 +33,60 @@ module.exports = function createEmitAuth({ logger }) {
33
33
  ctx.mqttClient = undefined;
34
34
  ctx.loggedIn = false;
35
35
 
36
+ // Clean up timeout references
37
+ try {
38
+ if (ctx._rTimeout) {
39
+ clearTimeout(ctx._rTimeout);
40
+ ctx._rTimeout = null;
41
+ }
42
+ } catch (_) { }
43
+
44
+ // Clean up tasks Map to prevent memory leak
45
+ try {
46
+ if (ctx.tasks && ctx.tasks instanceof Map) {
47
+ ctx.tasks.clear();
48
+ }
49
+ } catch (_) { }
50
+
51
+ // Clean up userInfo intervals
52
+ try {
53
+ if (ctx._userInfoIntervals && Array.isArray(ctx._userInfoIntervals)) {
54
+ ctx._userInfoIntervals.forEach(interval => {
55
+ try {
56
+ clearInterval(interval);
57
+ } catch (_) { }
58
+ });
59
+ ctx._userInfoIntervals = [];
60
+ }
61
+ } catch (_) { }
62
+
63
+ // Clean up autoSave intervals
64
+ try {
65
+ if (ctx._autoSaveInterval && Array.isArray(ctx._autoSaveInterval)) {
66
+ ctx._autoSaveInterval.forEach(interval => {
67
+ try {
68
+ clearInterval(interval);
69
+ } catch (_) { }
70
+ });
71
+ ctx._autoSaveInterval = [];
72
+ }
73
+ } catch (_) { }
74
+
75
+ // Clean up scheduler
76
+ try {
77
+ if (ctx._scheduler && typeof ctx._scheduler.destroy === "function") {
78
+ ctx._scheduler.destroy();
79
+ ctx._scheduler = undefined;
80
+ }
81
+ } catch (_) { }
82
+
83
+ // Clear global mqttClient reference if set
84
+ try {
85
+ if (global.mqttClient) {
86
+ delete global.mqttClient;
87
+ }
88
+ } catch (_) { }
89
+
36
90
  const msg = detail || reason;
37
91
  logger(`auth change -> ${reason}: ${msg}`, "error");
38
92
 
@@ -13,6 +13,7 @@ const createGetSeqID = require("./core/getSeqID");
13
13
  const markDelivery = require("./core/markDelivery");
14
14
  const getTaskResponseData = require("./core/getTaskResponseData");
15
15
  const createEmitAuth = require("./core/emitAuth");
16
+ const createMiddlewareSystem = require("./middleware");
16
17
  const parseDelta = createParseDelta({ markDelivery, parseAndCheckLogin });
17
18
  // Create emitAuth first so it can be injected into both factories
18
19
  const emitAuth = createEmitAuth({ logger });
@@ -32,6 +33,12 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
32
33
  const identity = function () { };
33
34
  let globalCallback = identity;
34
35
 
36
+ // Initialize middleware system if not already initialized
37
+ if (!ctx._middleware) {
38
+ ctx._middleware = createMiddlewareSystem();
39
+ }
40
+ const middleware = ctx._middleware;
41
+
35
42
  function installPostGuard() {
36
43
  if (ctx._postGuarded) return defaultFuncs.post;
37
44
  const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
@@ -160,6 +167,43 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
160
167
  clearTimeout(ctx._reconnectTimer);
161
168
  ctx._reconnectTimer = null;
162
169
  }
170
+ if (ctx._rTimeout) {
171
+ clearTimeout(ctx._rTimeout);
172
+ ctx._rTimeout = null;
173
+ }
174
+ // Clean up tasks Map to prevent memory leak
175
+ if (ctx.tasks && ctx.tasks instanceof Map) {
176
+ ctx.tasks.clear();
177
+ }
178
+ // Clean up userInfo intervals
179
+ if (ctx._userInfoIntervals && Array.isArray(ctx._userInfoIntervals)) {
180
+ ctx._userInfoIntervals.forEach(interval => {
181
+ try {
182
+ clearInterval(interval);
183
+ } catch (_) { }
184
+ });
185
+ ctx._userInfoIntervals = [];
186
+ }
187
+ // Clean up autoSave intervals
188
+ if (ctx._autoSaveInterval && Array.isArray(ctx._autoSaveInterval)) {
189
+ ctx._autoSaveInterval.forEach(interval => {
190
+ try {
191
+ clearInterval(interval);
192
+ } catch (_) { }
193
+ });
194
+ ctx._autoSaveInterval = [];
195
+ }
196
+ // Clean up scheduler
197
+ if (ctx._scheduler && typeof ctx._scheduler.destroy === "function") {
198
+ try {
199
+ ctx._scheduler.destroy();
200
+ } catch (_) { }
201
+ ctx._scheduler = undefined;
202
+ }
203
+ // Clear global mqttClient reference if set
204
+ if (global.mqttClient) {
205
+ delete global.mqttClient;
206
+ }
163
207
  next && next();
164
208
  };
165
209
  try {
@@ -224,11 +268,20 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
224
268
 
225
269
  const msgEmitter = new MessageEmitter();
226
270
 
227
- globalCallback = callback || function (error, message) {
271
+ // Original callback without middleware
272
+ const originalCallback = callback || function (error, message) {
228
273
  if (error) { logger("mqtt emit error", "error"); return msgEmitter.emit("error", error); }
229
274
  msgEmitter.emit("message", message);
230
275
  };
231
276
 
277
+ // Only wrap callback with middleware if middleware exists
278
+ // If no middleware, use callback directly for better performance
279
+ if (middleware.count > 0) {
280
+ globalCallback = middleware.wrapCallback(originalCallback);
281
+ } else {
282
+ globalCallback = originalCallback;
283
+ }
284
+
232
285
  conf = mqttConf(ctx, conf);
233
286
 
234
287
  installPostGuard();
@@ -253,6 +306,60 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
253
306
 
254
307
  api.stopListening = msgEmitter.stopListening;
255
308
  api.stopListeningAsync = msgEmitter.stopListeningAsync;
309
+
310
+ // Store original callback for re-wrapping when middleware is added/removed
311
+ let currentOriginalCallback = originalCallback;
312
+ let currentGlobalCallback = globalCallback;
313
+
314
+ // Function to re-wrap callback when middleware changes
315
+ function rewrapCallbackIfNeeded() {
316
+ if (!ctx.mqttClient || ctx._ending) return; // Not listening or ending
317
+
318
+ const hasMiddleware = middleware.count > 0;
319
+ const isWrapped = currentGlobalCallback !== currentOriginalCallback;
320
+
321
+ // If middleware exists but callback is not wrapped, wrap it
322
+ if (hasMiddleware && !isWrapped) {
323
+ currentGlobalCallback = middleware.wrapCallback(currentOriginalCallback);
324
+ globalCallback = currentGlobalCallback;
325
+ logger("Middleware added - callback re-wrapped", "info");
326
+ }
327
+ // If no middleware but callback is wrapped, unwrap it
328
+ else if (!hasMiddleware && isWrapped) {
329
+ currentGlobalCallback = currentOriginalCallback;
330
+ globalCallback = currentGlobalCallback;
331
+ logger("All middleware removed - callback unwrapped", "info");
332
+ }
333
+ }
334
+
335
+ // Expose middleware API with re-wrapping support
336
+ api.useMiddleware = function (middlewareFn, fn) {
337
+ const result = middleware.use(middlewareFn, fn);
338
+ rewrapCallbackIfNeeded();
339
+ return result;
340
+ };
341
+ api.removeMiddleware = function (identifier) {
342
+ const result = middleware.remove(identifier);
343
+ rewrapCallbackIfNeeded();
344
+ return result;
345
+ };
346
+ api.clearMiddleware = function () {
347
+ const result = middleware.clear();
348
+ rewrapCallbackIfNeeded();
349
+ return result;
350
+ };
351
+ api.listMiddleware = function () {
352
+ return middleware.list();
353
+ };
354
+ api.setMiddlewareEnabled = function (name, enabled) {
355
+ const result = middleware.setEnabled(name, enabled);
356
+ rewrapCallbackIfNeeded();
357
+ return result;
358
+ };
359
+ Object.defineProperty(api, "middlewareCount", {
360
+ get: function () { return middleware.count; }
361
+ });
362
+
256
363
  return msgEmitter;
257
364
  };
258
365
  };