@lazyneoaz/metachat 1.0.7 → 1.0.8

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": "@lazyneoaz/metachat",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "type": "commonjs",
5
5
  "types": "src/types/index.d.ts",
6
6
  "description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
@@ -808,16 +808,39 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
808
808
  } catch (err) {
809
809
  const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
810
810
  const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
811
-
812
- if (/Not logged in/i.test(msg)) {
813
- utils.error("MQTT", "Auth error in getSeqID: Not logged in");
814
- return emitAuthError("not_logged_in", msg);
815
- }
816
- if (/blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir/i.test(msg)) {
817
- utils.error("MQTT", "Auth error in getSeqID: Session/Login blocked");
818
- return emitAuthError("login_blocked", msg);
811
+
812
+ const isNotLoggedIn = /Not logged in/i.test(msg);
813
+ const isLoginBlocked = /blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir/i.test(msg);
814
+
815
+ if (isNotLoggedIn || isLoginBlocked) {
816
+ const reason = isLoginBlocked ? "login_blocked" : "not_logged_in";
817
+ utils.error("MQTT", `Auth error in getSeqID: ${reason} — attempting auto re-login`);
818
+
819
+ // Mirror the close-handler re-login pattern: try handleSessionExpiry first.
820
+ // If it succeeds we schedule a reconnect; only fall back to emitAuthError
821
+ // (which kills listening) when re-login is unavailable or already in progress.
822
+ if (!ctx._mqttReauthing && globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
823
+ ctx._mqttReauthing = true;
824
+ globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', msg)
825
+ .then((ok) => {
826
+ ctx._mqttReauthing = false;
827
+ if (ok && ctx.globalOptions.autoReconnect) {
828
+ ctx._reconnectAttempts = 0;
829
+ scheduleReconnect((ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000);
830
+ } else if (!ok) {
831
+ emitAuthError(reason, msg);
832
+ }
833
+ })
834
+ .catch(() => {
835
+ ctx._mqttReauthing = false;
836
+ emitAuthError(reason, msg);
837
+ });
838
+ return;
839
+ }
840
+
841
+ return emitAuthError(reason, msg);
819
842
  }
820
-
843
+
821
844
  utils.error("MQTT", "getSeqID error:", msg);
822
845
  if (ctx.globalOptions.autoReconnect) {
823
846
  const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
@@ -77,6 +77,16 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
77
77
  }
78
78
 
79
79
  if (isScrapingWarning) {
80
+ // Auto-dismiss before propagating the error. Unhandled scraping warnings
81
+ // escalate to permanent suspension. We await the bypass so the checkpoint is
82
+ // gone before the caller's retry (e.g. MQTT reconnect) fires.
83
+ // Guards: only attempt when ctx has jar + globalOptions available.
84
+ if (ctx && ctx.jar && ctx.globalOptions) {
85
+ try {
86
+ const { bypassScrapingWarning } = require('./checkpointBypass');
87
+ await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID || null, null);
88
+ } catch (_) {}
89
+ }
80
90
  const err = new Error('Facebook scraping warning checkpoint detected.');
81
91
  err.error = 'checkpoint_scraping';
82
92
  err.res = body;
@@ -91,10 +101,14 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
91
101
  // actual login page — NOT generic strings like '"login.php?"' or
92
102
  // '"login_page"' which Facebook embeds in authenticated page payloads as
93
103
  // navigation links and client-side route names, causing false positives.
104
+ // `/checkpoint/block/?next` is the URL Facebook redirects to when it forces
105
+ // a logout; it appears in the response body as a redirect target and is a
106
+ // reliable signal that the session is gone.
94
107
  const isLoginRedirect =
95
108
  bodyStr.includes('<form id="login_form"') ||
96
109
  bodyStr.includes('id="loginbutton"') ||
97
- bodyStr.includes('id="email" name="email"');
110
+ bodyStr.includes('id="email" name="email"') ||
111
+ bodyStr.includes('/checkpoint/block/?next');
98
112
 
99
113
  const isLoginBlocked =
100
114
  typeof body === 'object' && body !== null && body.error === 1357001;
@@ -23,14 +23,33 @@ function formatCookie(arr, url) {
23
23
  function parseAndCheckLogin(ctx, http, retryCount = 0) {
24
24
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
25
25
 
26
+ /**
27
+ * Attempt performAutoLogin if available and not already in progress.
28
+ * Returns true if re-login succeeded, false otherwise.
29
+ * Always resets ctx.auto_login in a finally block.
30
+ */
31
+ async function tryAutoLogin() {
32
+ if (ctx.auto_login || typeof ctx.performAutoLogin !== 'function') return false;
33
+ ctx.auto_login = true;
34
+ try {
35
+ const ok = await ctx.performAutoLogin();
36
+ return !!ok;
37
+ } catch (_) {
38
+ return false;
39
+ } finally {
40
+ ctx.auto_login = false;
41
+ }
42
+ }
43
+
26
44
  return async (data) => {
27
45
  if (data.statusCode === 401) {
46
+ warn("Session Status", "Session expired. Triggering auto re-login...");
47
+ await tryAutoLogin();
28
48
  const err = new Error("Session expired - Authentication required");
29
49
  err.error = 401;
30
50
  err.errorCode = 401;
31
51
  err.errorType = "AUTHENTICATION_REQUIRED";
32
52
  err.requiresReLogin = true;
33
- warn("Session Status", "Session expired. Re-login required.");
34
53
  throw err;
35
54
  }
36
55
 
@@ -49,7 +68,6 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
49
68
 
50
69
  await delay(retryTime);
51
70
 
52
- // Guard against undefined Content-Type header before splitting
53
71
  const contentType = (data.request.headers && data.request.headers["content-type"]) || "";
54
72
  if (contentType.split(";")[0].trim() === "multipart/form-data") {
55
73
  const newData = await http.postFormData(
@@ -61,10 +79,6 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
61
79
  );
62
80
  return await parseAndCheckLogin(ctx, http, retryCount)(newData);
63
81
  } else {
64
- // defaultFuncs.post signature: (url, jar, form, ctxx, customHeader)
65
- // The 4th arg must be ctx (not ctx.globalOptions) — passing globalOptions
66
- // here caused the retry to be treated as a raw network call without
67
- // session context, missing auth headers and session inspection.
68
82
  const newData = await http.post(
69
83
  url,
70
84
  ctx.jar,
@@ -107,8 +121,9 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
107
121
  if (res.jsmods && res.jsmods.require && Array.isArray(res.jsmods.require[0]) && res.jsmods.require[0][0] === "Cookie") {
108
122
  res.jsmods.require[0][3][0] = res.jsmods.require[0][3][0].replace("_js_", "");
109
123
  const requireCookie = res.jsmods.require[0][3];
110
- ctx.jar.setCookie(formatCookie(requireCookie, "facebook"), "https://www.facebook.com");
111
- ctx.jar.setCookie(formatCookie(requireCookie, "messenger"), "https://www.messenger.com");
124
+ // Use setCookieSync to avoid async fire-and-forget that drops rotated session cookies
125
+ try { ctx.jar.setCookieSync(formatCookie(requireCookie, "facebook"), "https://www.facebook.com"); } catch (_) {}
126
+ try { ctx.jar.setCookieSync(formatCookie(requireCookie, "messenger"), "https://www.messenger.com"); } catch (_) {}
112
127
  }
113
128
 
114
129
  if (res.jsmods && Array.isArray(res.jsmods.require)) {
@@ -120,6 +135,9 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
120
135
  for (let j = 0; j < ctx.fb_dtsg.length; j++) {
121
136
  ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j);
122
137
  }
138
+ // jazoest MUST stay in sync with fb_dtsg — stale jazoest causes form-submission
139
+ // failures that Facebook treats as tamper attempts and flags as bot activity.
140
+ ctx.jazoest = `2${Array.from(ctx.fb_dtsg).reduce((a, b) => a + b.charCodeAt(0), 0)}`;
123
141
  }
124
142
  }
125
143
  }
@@ -131,12 +149,16 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
131
149
  };
132
150
 
133
151
  if (res.error && SESSION_EXPIRY_CODES[res.error]) {
152
+ warn("Session Status", `${SESSION_EXPIRY_CODES[res.error]} (Code: ${res.error}) — triggering auto re-login`);
153
+ // Fire re-login so the session is refreshed even though this request fails.
154
+ // Awaiting ensures the new session is ready before the error propagates to
155
+ // callers (e.g. MQTT reconnect) that would immediately retry.
156
+ await tryAutoLogin();
134
157
  const err = new Error(SESSION_EXPIRY_CODES[res.error]);
135
158
  err.error = res.error;
136
159
  err.errorCode = res.error;
137
160
  err.errorType = "SESSION_EXPIRED";
138
161
  err.requiresReLogin = true;
139
- warn("Session Status", `${SESSION_EXPIRY_CODES[res.error]} (Code: ${res.error})`);
140
162
  throw err;
141
163
  }
142
164
 
@@ -155,6 +177,10 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
155
177
  err.errorType = res.error === 1357004 ? "CHECKPOINT" : res.error === 1357031 ? "LOCKED" : "BLOCKED";
156
178
  err.requiresReLogin = res.error === 1357004 || res.error === 1357031;
157
179
  warn("Account Status", `${ACCOUNT_ERROR_CODES[res.error]} (Code: ${res.error})`);
180
+ // For checkpoint and locked states, trigger re-login so the session can recover
181
+ if (err.requiresReLogin) {
182
+ await tryAutoLogin();
183
+ }
158
184
  throw err;
159
185
  }
160
186
 
@@ -166,13 +192,26 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
166
192
  }
167
193
 
168
194
  const resStr = JSON.stringify(res);
169
-
195
+
170
196
  if (resStr.includes("XCheckpointFBScrapingWarningController") || resStr.includes("601051028565049")) {
171
- warn("Bot Detection", "Facebook checkpoint detected - scraping warning (601051028565049)");
172
- try {
173
- globalRateLimiter.setEndpointCooldown("__GLOBAL__", 5 * 60 * 1000);
197
+ warn("Bot Detection", "Facebook checkpoint detected - scraping warning (601051028565049) — auto-dismissing");
198
+ try {
199
+ globalRateLimiter.setEndpointCooldown("__GLOBAL__", 5 * 60 * 1000);
174
200
  configureRateLimiter({ maxConcurrentRequests: 2 });
175
201
  } catch (_) {}
202
+ // Auto-dismiss the scraping warning — unhandled warnings escalate to permanent
203
+ // account suspension. This path is reached when inspectResponseForSessionIssues
204
+ // was skipped (ctx._skipSessionInspect or null ctx). We still throw after
205
+ // cleanup: `res` at this point is checkpoint JSON, not a valid API response,
206
+ // so returning it would corrupt the caller. The bypass ensures the NEXT
207
+ // retry finds a clean session.
208
+ try {
209
+ const { bypassScrapingWarning } = require('./checkpointBypass');
210
+ await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID, null);
211
+ warn("Bot Detection", "Scraping warning dismissed — checkpoint cleared for next retry");
212
+ } catch (bypassErr) {
213
+ warn("Bot Detection", `Scraping warning bypass failed: ${bypassErr && bypassErr.message ? bypassErr.message : String(bypassErr)}`);
214
+ }
176
215
  const err = new Error("Facebook detected automated behavior - Account may be flagged");
177
216
  err.error = "Bot checkpoint detected";
178
217
  err.errorCode = "CHECKPOINT_SCRAPING";
@@ -184,8 +223,8 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
184
223
  if (resStr.includes("1501092823525282")) {
185
224
  warn("Bot Detection", "Critical bot checkpoint 282 detected! Account requires immediate attention!");
186
225
  log("Please check your Facebook account in a browser and complete any security checks.");
187
- try {
188
- globalRateLimiter.setEndpointCooldown("__GLOBAL__", 10 * 60 * 1000);
226
+ try {
227
+ globalRateLimiter.setEndpointCooldown("__GLOBAL__", 10 * 60 * 1000);
189
228
  configureRateLimiter({ maxConcurrentRequests: 1 });
190
229
  } catch (_) {}
191
230
  const err = new Error("Facebook detected automated behavior - Critical checkpoint required");
@@ -206,7 +245,8 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
206
245
  // anywhere in the body JSON, which it does on authenticated pages as a
207
246
  // navigation/share link, causing false-positive session expiry errors.
208
247
  if (String(res.redirect || "").includes("login.php")) {
209
- warn("Session Status", "Redirected to login page - Session expired");
248
+ warn("Session Status", "Redirected to login page - Session expired — triggering auto re-login");
249
+ await tryAutoLogin();
210
250
  const err = new Error("Session expired - Redirected to login page");
211
251
  err.error = 401;
212
252
  err.errorCode = 401;
@@ -215,13 +255,18 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
215
255
  throw err;
216
256
  }
217
257
 
218
- if (typeof data.body === 'string' && (data.body.includes('<title>Facebook - Log In or Sign Up</title>') || data.body.includes('name="login_form"'))) {
258
+ if (typeof data.body === 'string' && (
259
+ data.body.includes('<title>Facebook - Log In or Sign Up</title>') ||
260
+ data.body.includes('name="login_form"') ||
261
+ data.body.includes('/checkpoint/block/?next')
262
+ )) {
263
+ warn("Session Status", "Detected login page or checkpoint redirect — triggering auto re-login");
264
+ await tryAutoLogin();
219
265
  const err = new Error("Session expired - Redirected to login page");
220
266
  err.error = 401;
221
267
  err.errorCode = 401;
222
268
  err.errorType = "LOGIN_REDIRECT";
223
269
  err.requiresReLogin = true;
224
- warn("Session Status", "Detected login page redirect. Session expired.");
225
270
  throw err;
226
271
  }
227
272
 
@@ -231,26 +276,25 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
231
276
 
232
277
  /**
233
278
  * Saves cookies from a response to the cookie jar.
279
+ * Uses setCookieSync to guarantee cookies are saved before the next request fires.
280
+ * Facebook continuously rotates session cookies (xs, c_user, fr, etc.) — any missed
281
+ * rotation leaves the jar stale and Facebook forces an immediate logout.
282
+ *
234
283
  * @param {Object} jar - The cookie jar instance.
235
284
  * @returns {function(res: Object): Object} A function that processes the response and returns it.
236
285
  */
237
286
  function saveCookies(jar) {
238
287
  return function (res) {
239
- const cookies = res.headers["set-cookie"] || [];
288
+ const cookies = (res.headers && res.headers["set-cookie"]) || [];
240
289
  cookies.forEach(function (c) {
241
- // Always attempt to save every Set-Cookie header to both domains.
242
- // The old guard `c.indexOf(".facebook.com") > -1` silently dropped
243
- // cookies whose domain attribute was `facebook.com` (no dot),
244
- // `www.facebook.com`, or absent entirely. Facebook rotates critical
245
- // session cookies (xs, c_user, fr, etc.) continuously — missing a
246
- // single update causes the jar to go stale and Facebook forces logout.
247
- try { jar.setCookie(c, "https://www.facebook.com"); } catch (_) {}
248
-
249
- // Mirror to messenger.com so MQTT / Messenger API calls stay authed.
290
+ // Save to facebook.com
291
+ try { jar.setCookieSync(c, "https://www.facebook.com"); } catch (_) {}
292
+
293
+ // Mirror to messenger.com so MQTT / Messenger API calls stay authenticated.
250
294
  const c2 = c
251
295
  .replace(/domain=[^;]*/i, "domain=.messenger.com")
252
296
  .replace(/secure;?\s*/i, "");
253
- try { jar.setCookie(c2, "https://www.messenger.com"); } catch (_) {}
297
+ try { jar.setCookieSync(c2, "https://www.messenger.com"); } catch (_) {}
254
298
  });
255
299
  return res;
256
300
  };
@@ -265,7 +309,6 @@ function saveCookies(jar) {
265
309
  function getAccessFromBusiness(jar, Options) {
266
310
  return async function (res) {
267
311
  const html = res ? res.body : null;
268
- // Use the same axios wrapper used everywhere else — "request" module does not exist
269
312
  const { get } = require("./axios");
270
313
  try {
271
314
  const businessRes = await get("https://business.facebook.com/content_management", jar, null, Options, { noRef: true });
@@ -278,14 +321,24 @@ function getAccessFromBusiness(jar, Options) {
278
321
  }
279
322
 
280
323
  /**
281
- * Retrieves all cookies from the jar for both Facebook and Messenger domains.
324
+ * Retrieves all cookies from the jar for both Facebook and Messenger domains,
325
+ * deduplicated by key + domain + path so stale copies don't shadow fresh ones.
282
326
  * @param {Object} jar - The cookie jar instance.
283
327
  * @returns {Array<Object>} An array of cookie objects.
284
328
  */
285
329
  function getAppState(jar) {
286
- return jar
287
- .getCookiesSync("https://www.facebook.com")
288
- .concat(jar.getCookiesSync("https://www.messenger.com"));
330
+ const fbCookies = jar.getCookiesSync("https://www.facebook.com");
331
+ const messengerCookies = jar.getCookiesSync("https://www.messenger.com");
332
+ const seen = new Set();
333
+ const all = [];
334
+ for (const cookie of [...fbCookies, ...messengerCookies]) {
335
+ const id = `${cookie.key}|${cookie.domain || ""}|${cookie.path || "/"}`;
336
+ if (!seen.has(id)) {
337
+ seen.add(id);
338
+ all.push(cookie);
339
+ }
340
+ }
341
+ return all;
289
342
  }
290
343
 
291
344
  module.exports = {