@dongdev/fca-unofficial 3.0.7 → 3.0.9

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/CHANGELOG.md CHANGED
@@ -149,3 +149,9 @@ Too lazy to write changelog, sorry! (will write changelog in the next release, t
149
149
 
150
150
  ## v3.0.6 - 2025-11-27
151
151
  - Hotfix / auto bump
152
+
153
+ ## v3.0.7 - 2025-11-27
154
+ - Hotfix / auto bump
155
+
156
+ ## v3.0.8 - 2025-11-27
157
+ - Hotfix / auto bump
package/module/login.js CHANGED
@@ -3,10 +3,77 @@ const { setOptions } = require("./options");
3
3
  const { loadConfig } = require("./config");
4
4
  const { checkAndUpdateVersion } = require("../func/checkUpdate");
5
5
  const loginHelper = require("./loginHelper");
6
+ const logger = require("../func/logger");
6
7
 
7
8
  const { config } = loadConfig();
8
9
  global.fca = { config };
9
10
 
11
+ // Global error handlers to prevent bot crashes
12
+ // Handle unhandled promise rejections (e.g., fetch timeouts, network errors)
13
+ if (!global.fca._errorHandlersInstalled) {
14
+ global.fca._errorHandlersInstalled = true;
15
+
16
+ process.on("unhandledRejection", (reason, promise) => {
17
+ try {
18
+ // Check if it's a fetch/network timeout error
19
+ if (reason && typeof reason === "object") {
20
+ const errorCode = reason.code || reason.cause?.code;
21
+ const errorMessage = reason.message || String(reason);
22
+
23
+ // Handle fetch timeout errors gracefully
24
+ if (errorCode === "UND_ERR_CONNECT_TIMEOUT" ||
25
+ errorCode === "ETIMEDOUT" ||
26
+ errorMessage.includes("Connect Timeout") ||
27
+ errorMessage.includes("fetch failed")) {
28
+ logger(`Network timeout error caught (non-fatal): ${errorMessage}`, "warn");
29
+ return; // Don't crash, just log
30
+ }
31
+
32
+ // Handle other network errors
33
+ if (errorCode === "ECONNREFUSED" ||
34
+ errorCode === "ENOTFOUND" ||
35
+ errorCode === "ECONNRESET" ||
36
+ errorMessage.includes("ECONNREFUSED") ||
37
+ errorMessage.includes("ENOTFOUND")) {
38
+ logger(`Network connection error caught (non-fatal): ${errorMessage}`, "warn");
39
+ return; // Don't crash, just log
40
+ }
41
+ }
42
+
43
+ // For other unhandled rejections, log but don't crash
44
+ logger(`Unhandled promise rejection (non-fatal): ${reason && reason.message ? reason.message : String(reason)}`, "error");
45
+ } catch (e) {
46
+ // Fallback if logger fails
47
+ console.error("[FCA-ERROR] Unhandled promise rejection:", reason);
48
+ }
49
+ });
50
+
51
+ // Handle uncaught exceptions (last resort)
52
+ process.on("uncaughtException", (error) => {
53
+ try {
54
+ const errorMessage = error.message || String(error);
55
+ const errorCode = error.code;
56
+
57
+ // Handle fetch/network errors
58
+ if (errorCode === "UND_ERR_CONNECT_TIMEOUT" ||
59
+ errorCode === "ETIMEDOUT" ||
60
+ errorMessage.includes("Connect Timeout") ||
61
+ errorMessage.includes("fetch failed")) {
62
+ logger(`Uncaught network timeout error (non-fatal): ${errorMessage}`, "warn");
63
+ return; // Don't crash
64
+ }
65
+
66
+ // For other uncaught exceptions, log but try to continue
67
+ logger(`Uncaught exception (attempting to continue): ${errorMessage}`, "error");
68
+ // Note: We don't exit here to allow bot to continue running
69
+ // In production, you might want to restart the process instead
70
+ } catch (e) {
71
+ // Fallback if logger fails
72
+ console.error("[FCA-ERROR] Uncaught exception:", error);
73
+ }
74
+ });
75
+ }
76
+
10
77
  function login(loginData, options, callback) {
11
78
  if (getType(options) === "Function" || getType(options) === "AsyncFunction") {
12
79
  callback = options;
@@ -465,7 +465,7 @@ async function hydrateJarFromDB(userID) {
465
465
  }
466
466
  }
467
467
 
468
- async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions, ctxRef) {
468
+ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions, ctxRef, hadAppStateInput = false) {
469
469
  const getUID = cs =>
470
470
  cs.find(c => c.key === "i_user")?.value ||
471
471
  cs.find(c => c.key === "c_user")?.value ||
@@ -473,6 +473,15 @@ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions,
473
473
  cs.find(c => c.name === "c_user")?.value;
474
474
  let userID = getUID(currentCookies);
475
475
  if (userID) return { html: currentHtml, cookies: currentCookies, userID };
476
+ // If appState/Cookie was provided and is not dead (not checkpointed), skip backup
477
+ if (hadAppStateInput) {
478
+ const isCheckpoint = currentHtml.includes("/checkpoint/block/?next");
479
+ if (!isCheckpoint) {
480
+ // AppState provided and not checkpointed, skip backup - just throw error
481
+ throw new Error("Missing user cookie from provided appState");
482
+ }
483
+ // AppState is dead (checkpointed), proceed to backup/email login
484
+ }
476
485
  const hydrated = await hydrateJarFromDB(null);
477
486
  if (hydrated) {
478
487
  logger("AppState backup live — proceeding to login", "info");
@@ -679,7 +688,8 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
679
688
  cookies.find(c => c.name === "i_user")?.value ||
680
689
  cookies.find(c => c.name === "c_user")?.value;
681
690
  if (!userID) {
682
- const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx);
691
+ // Pass skipBackup=true if appState/Cookie was originally provided
692
+ const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
683
693
  html = retried.html;
684
694
  cookies = retried.cookies;
685
695
  userID = retried.userID;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongdev/fca-unofficial",
3
- "version": "3.0.7",
3
+ "version": "3.0.9",
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",
@@ -226,14 +226,59 @@ module.exports = function(defaultFuncs, api, ctx) {
226
226
  .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
227
227
  .then(parseAndCheckLogin(ctx, defaultFuncs))
228
228
  .then(resData => {
229
- if (resData[resData.length - 1].error_results > 0)
230
- throw resData[0].o0.errors;
231
- if (resData[resData.length - 1].successful_results === 0)
229
+ // Validate resData is an array and has elements
230
+ if (!resData || !Array.isArray(resData) || resData.length === 0) {
231
+ throw {
232
+ error: "getThreadList: Invalid response data - resData is not a valid array",
233
+ res: resData
234
+ };
235
+ }
236
+
237
+ // Validate last element exists and has required properties
238
+ const lastElement = resData[resData.length - 1];
239
+ if (!lastElement || typeof lastElement !== "object") {
240
+ throw {
241
+ error: "getThreadList: Invalid response data - last element is missing or invalid",
242
+ res: resData
243
+ };
244
+ }
245
+
246
+ if (lastElement.error_results > 0) {
247
+ // Check if first element and o0 exist before accessing errors
248
+ if (resData[0] && resData[0].o0 && resData[0].o0.errors) {
249
+ throw resData[0].o0.errors;
250
+ } else {
251
+ throw {
252
+ error: "getThreadList: Error results > 0 but error details not available",
253
+ res: resData
254
+ };
255
+ }
256
+ }
257
+
258
+ if (lastElement.successful_results === 0) {
232
259
  throw {
233
260
  error: "getThreadList: there was no successful_results",
234
261
  res: resData
235
262
  };
236
- if (timestamp) resData[0].o0.data.viewer.message_threads.nodes.shift();
263
+ }
264
+
265
+ // Validate first element and nested data structure
266
+ if (!resData[0] || !resData[0].o0 || !resData[0].o0.data ||
267
+ !resData[0].o0.data.viewer || !resData[0].o0.data.viewer.message_threads ||
268
+ !Array.isArray(resData[0].o0.data.viewer.message_threads.nodes)) {
269
+ throw {
270
+ error: "getThreadList: Invalid response data structure - missing required fields",
271
+ res: resData
272
+ };
273
+ }
274
+
275
+ if (timestamp) {
276
+ const nodes = resData[0].o0.data.viewer.message_threads.nodes;
277
+ if (Array.isArray(nodes) && nodes.length > 0) {
278
+ nodes.shift();
279
+ }
280
+ }
281
+
237
282
  callback(
238
283
  null,
239
284
  formatThreadList(resData[0].o0.data.viewer.message_threads.nodes)